第一章:C++入门!
1.1C++关键字(C++98)
1.C++总计63个关键字,C语言32个关键字
注:下面,我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面学到再讲。
2.关键字:
1.2命名空间
1.2.1命名空间的定义
1.定义命名空间:
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
2.细节:
1)命名空间中可以定义变量、函数、类型:
//命名空间可以起自己名字的缩写
//1.正常的命名空间定义
namespace LHY
{
//命名空间中可以定义变量/函数/类型
int rand = 10;
int ADD(int x, int y)
{
return x + y;
}
struct Node
{
struct Node* next;
int data;
};
}
2)命名空间可以嵌套:
//test.cpp
namespace N1
{
namespace N2
{
int a;
int b;
int Sub(int x, int y)
{
return x - y;
}
}
}
3)重命名的命名空间会被合并:
//同一个工程中可以出现多个重名的命名空间,编译器最终会将他们合并成一个命名空间
//test.h
//ps:一个工程中的test.h和上面的test.cpp中的N1会合并成一个
namespace N1
{
int Mul(int x, int y)
{
return x * y;
}
}
如果重名的命名空间中还有重名成员,这样也是不行的,会报重定义的错误。
3.注意:
一个命名空间就定义了一个新的作用域,命名空间中所有内容都局限于该命名空间中。
1.2.2命名空间的使用
1.样例命名空间:
namespace LHY
{
int rand = 0;
//结构体类型数据
struct Node
{
struct Node* next;
int data;
};
//函数
int ADD(int left, int rigth)
{
return left + right;
}
namespace mine
{
float f = 3.14;
}
}
2.命名空间的使用方式有三种:
1)使用空间名及作用域限定符:
int main()
{
printf("%d\n", LHY::rand);
//加在struct后面,不能加在struct前面
struct LHY::Node Tnode = { NULL, 10 };
printf("%d\n", LHY::ADD(1, 3));
//命名空间套娃
printf("%f\n", LHY::mine::f);
return 0;
}
2)使用using将命名空间中的某个成员引入:
using LHY::Node; // 不是using struct LHY::Node
using LHY::ADD;
int main()
{
struct Node node;
int c = ADD(1, 4);
//float f2 = mine::f;//报错,因为没有展开mine这个成员
return 0;
}
//部分展开如果出现重名仍然报错
3)使用using namespace将命名空间全部展开:
using namespace LHY;
int main()
{
struct Node node;
int b = ADD(2, 5);
// int c = rand; // 仍然报错,因为LHY展开后,里面的rand和全局的rand函数重名
printf("%d\n", LHY::rand); // 这样就不会报错了
printf("%d\n". mine::f);
return 0;
}
3.再去理解using namespace std;
#include<iostream>
// 先展开头文件,头文件中有命名空间std
// using namespace std;
// 将命名空间std全展开
// 如果上面没有这句话,而我们又想使用std中的内容,怎么办?
// 部分展开
using std::cout;
using std::endl;
int main()
{
int i;
std::cin >> i; //加访问限制符
cout << i << endl;
return 0;
}
1.3C++输入&输出
1.4缺省参数
1.4.1省略参数概念
1.缺省参数的概念:
缺省参数是声明或定义函数时,为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
2.代码演示
using namespace std;
//如果不传参数,默认是0
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); //不传参数,a是0
Func(10); //传参,a是10
return 0;
}
输出结果:
0
10
1.4.2省略参数分类
1.全缺省
1)含义:
全部的参数都是缺省参数。
2)代码演示:
//全缺省
void Func(int a = 10, int b = 20, int c =30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func();
//从左往右传参
Func(1);
Func(1, 2);
Func(1, 2, 3);
//不能指定传给哪一个参数
//Fun(, 2, ); //错误写法
return 0;
}
输出结果:
a = 10
b = 20
c = 30
a = 1
b = 20
c = 30
a = 1
b = 2
c = 30
a = 1
b = 2
c = 3
2.半缺省
1)含义:
有一部分参数是缺省参数。
2)代码演示:
//半缺省 -- 必须从右往左给值
void Func(int a, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//错误给法
//void Func(int a = 10, int b, int c = 30);
int main()
{
// Func(); //a必须给,不给报错
Func(1);
Func(1, 2);
Func(1, 2, 3);
return 0;
}
输出结果
a = 1
b = 20
c = 30
a = 1
b = 2
c = 30
a = 1
b = 2
c = 3
3.注意:
1)半缺省参数必须从右往左来给缺省值,不能间隔着给。
2)缺省参数不能再函数声明和定义中同时出现。
//注意
//缺省参数不能在定义和声明中同时出现
void Func(int a = 10);
void Func(int a = 10)
{
}
//直接报错
C++中,规定声明和定义分离时,声明给,定义不给。
3)缺省参数必须是常量或者全局变量。
4)C语言不支持缺省参数(编译器不支持)。
4.缺省参数的一个实际运用:
namespace LHY
{
typedef struct Stack
{
int* base;
int top;
int capacity;
}Stack;
}
using LHY::Stack;
void StackInit(Stack* stack, int N = 4)
{
stack->base = (int*)malloc(sizeof(int) * N);
stack->top = stack->capacity = 0;
}
int main()
{
Stack stack;
StackInit(&stack, 100); //用多少空间,开辟多少空间
int i = 0;
for (i = 1; i <= 100; i++)
{
stack.base[i] = i;
printf("%d ",stack.base[i]);
}
return 0;
}
1.5函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词的真实含义,即该词被重载了。
1.5.1函数重载的概念
1.含义:
是函数的一种特殊情况,C++允许在同一个域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)不同,常用来处理实现功能类似,数据类型不同的问题。
2.函数重载的几种情况:
1)参数类型不同:
int ADD(int left, int right)
{
return left + right;
}
double ADD(double left, double right)
{
return left + right;
}
int main()
{
cout << ADD(1, 2) << endl;
cout << ADD(1.1, 2.2) << endl;
return 0;
}
输出结果:
3
3.3
2)参数个数不同:
void fun() //无参数
{
cout << 1 << endl;
}
void fun(int a) //一个参数
{
cout << a << endl;
}
int main()
{
fun();
fun(10);
return 0;
}
输出结果:
1
10
3)参数顺序不同:
void Fun(int a, double b)
{
cout << "int, double" << endl;
}
void Fun(double a, int b)
{
cout << "double, int" << endl;
}
int main()
{
Fun(1, 2.3); // int, double
Fun(2.3, 1); // int, double
return 0;
}
输出结果:
int, double
double, int
注意:
顺序不同是指类型顺序不同,不是变量的名字不同。
4)返回值不同,不构成重载:
int Fun()
{
}
void Fun() // 报错,编译器将其认作同一个函数
{
}
5)有缺省参数的情况(重载结合缺省参数):
void fun(int a)
{
cout << a << endl;
}
// 第二个函数
void fun(int a, int b = 20)
{
cout << a << b << endl;
}
// 这个函数和第二个函数不构成重载函数,因为参数的类型,顺序,个数都相同
// void fun(int a, int b)
// {
//
// }
int main()
{
// fun(1); //报错,因为编译器不知道调用哪一个函数,相当于缺省参数没有用
fun(1, 2); //调用第二个函数
return 0;
}
fun(1)的情况,是因为语法存在二义性,第一个和第二个函数是构成重载的。
3.总结:
函数重载和缺省参数没有任何关系,不要混淆概念。
1.5.2C++支持函数重载的原理——函数名修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持?
1.在C/C++中,一个程序要运行起来,需要经历以下几个阶段:
预处理、编译、汇编、链接
2. 实际项目通常由多个头文件和多个源文件构成,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,由于Add是在b.cpp中定义的,a.o的目标文件中没有Add的函数地址,所以Add的地址在b.o中。
3. 链接阶段会专门处理这种问题,连接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
4. 那么链接时,面对Add函数,链接器会使用哪个名字去找呢?这里每个编译器有自己的函数名修饰规则。
5.Windows下VS的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用g++演示这个修饰后的名字。
6.采用C语言编译器(gcc)编译后的结果:
结论:在Linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。7.采用C++编译器(g++)编译后的结果:
结论:在Linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。8.windows下名字修饰规则:
我们以int N::C::func(int)这个函数签名来猜测Visual C++的名称修饰规则。修饰后名字由“?”开头,接着是函数名由“@”符号结尾的函数名;后面跟着由“@”结尾的类名“C”和名称空间“N”,再一个“@”表示函数的名称空间结束;第一个“A”表示函数调用类型为“__cdecl”(函数调用类型我们在第四章详细介绍),接着是函数的参数类型及返回值,由“@”结束,最后由“Z”结尾。可以看到,函数名、参数类型、名称空间都被加入了修饰后名称。
1.6引用
1.6.1引用概念
1.概念:
引用不是新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间。
2.引用类型的定义:
1)定义语法:
类型& 引用变量名(对象名)= 引用实体
2)代码:
int main()
{
int a = 0;
int& b = a;
//a,b地址一样,说明a,b就是一个变量
cout << &a << endl;
cout << &b << endl;
//对a++,b会++,对b++,a也会++
a++;
b++;
cout << a << endl;
cout << b << endl;
return 0;
}
一个可能的输出结果:
000000D36FDDF5C4
000000D36FDDF5C4
2
2
1.6.2引用特性
特性:
1)引用在定义时必须初始化。
2)一个变量可以有多个引用。
3)引用一但引用一个实体,再也不能引用其他实体。
//测试引用的特性
void TestRef()
{
int a = 10;
//int &ra; //这条语句编译时报错
int& ra = a; //初始化
int& rra = a; //多个别名
printf("%p %p %p\n", &a, &ra, &rra);//地址一样
int x = 0;
ra = x; //不是让ra成为x的别名,是将x的值赋给ra
//int& ra = x; //错误写法,ra已经是a的别名了
//int& a = x; //错误写法
}
int main()
{
TestRef();
return 0;
}
1.6.3常引用
1.常引用涉及权限的问题
1)原则:权限可以平移,可以缩小,但不能放大
int main()
{
const int a = 0;
//权限的放大,报错
//int& b = a;
//报错,本来a不可以被更改,如果这句代码可行,那就可以通过b来改变a了,相当于放大了a的权限
//int b = a;可以,因为这是赋值拷贝,b不影响a
//权限的平移
const int& c = a;
//权限的缩小
int x = 0;
const int& y = x;
x = 3; //x是可以改的
printf("%d\n", y); //y也随着x改,但是y不能自己改
return 0;
}
2.拓展:
1)代码1:
int main()
{
int i = 0;
double b = i; //可以
double& c = i; //不可以
const double& d = i; //可以
return 0;
}
//为什么?
原因:
-
int变double我们知道,是整形提升(如果这一块有问题,可以去我的
C语言进阶-整型提升)。 -
int变double&,就涉及权限的问题。
类之间相互转换时,会创建一个临时变量(编译器自动创建的)这个临时变量具有常属性的,不能被修改,所以上述过程是一个权限提升,是不被允许的。 -
int变const double&属于权限平移,可以。
2)代码2:
int func()
{
int a = 0;
return a;
}
int main()
{
int& ret = func(); //报错
return 0;
}
原因:
func在返回的时候也创建了一个临时变量来存放a,这个临时变量是具有常属性的。
1.6.4使用场景
1.做参数:
1)交换函数
void Swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
2)给单链表写一个PushBack
原C语言:
typedef struct ListNode
{
int val;
struct ListNode* next;
}ListNode, *PListNode;
//C语言中二级指针的玩法
void PushBack(PListNode* pphead, int x)
{
PListNode newnode = (PListNode)malloc(sizeof(ListNode));
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//...
}
}
C++的引用:
//改成用引用的方式
void PushBack(PListNode& phead, int x)
{
PListNode newnode = (PListNode)malloc(sizeof(ListNode));
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
2.做返回值:
1)对比传值返回和传引用返回:
- 传值返回:
原理:
返回时,n的值会先给到一个临时变量(如果n的空间比较小,这个临时变量一般在寄存器中,是函数自己创建的)Count函数的栈帧销毁后,将临时变量中的值给到ret。
总结:
传值返回只是返回一个值,count函数结束后,栈帧销毁,n的那一块内存被释放。
- 传引用返回
原理:
a.不管传值返回还是传引用返回,返回时都需要经历函数栈帧的销毁。
b.但是,传引用返回,会有机会接触到销毁空间的地址,我们不用关心这个引用类型的名字是什么,只用知道,我们可以通过返回的这个引用,来改变引用对应地址的空间。
c.如果引用对应的空间在函数返回时被释放了,那么,这个引用会成为一个野指针一样的存在,很危险。
代码演示:
i.没有被覆盖的情况
//传引用返回
int& Count()
{
int n = 0;
n++;
//...
return n;
}
void test2()
{
printf("test2:\n");
int ret = Count(); //用ret去接收别名所指向空间的值
//无论输出多少回,ret的结果都不变,因为ret是一个新的变量,独占一块空间
cout << ret << endl;
cout << ret << endl;
}
void test3()
{
printf("test3:\n");
int& ret = Count(); //用ret去接收别名,相当于原来的那块空间现在又有了一个名字ret
cout << ret << endl;
cout << ret << endl;//若之前n的那块空间被覆盖,这里输出的结果将是一个随机值
}
int main()
{
cout << "test2:\n" << endl;
test2();
cout << "test3:\n" << endl;
test3();
return 0;
}
在VS2022上测试的结果:
显然,n的空间没有被覆盖。
ii.被覆盖的情况
//ret被覆盖了
int& Add(int a, int b)
{
int c = a + b;
return c;
}
void test4()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
}
int main()
{
test4();
return 0;
}
VS2022测试结果:
ret变成了随机值。
总结:
如果函数返回时,出了函数作用域,如果返回对象还在(还没给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
3.传引用传参和传引用返回的特点:
1)传引用传参(任何时候都可以用):
- 提高效率;
- 用于输出型参数(形参的修改会影响实参);
2)传引用返回(若出了函数作用域,对象还在,可以用):
- 提高效率;
- 简化语法;
代码示例:(查询或修改一个顺序表)
C语言版:
typedef struct SeqList
{
int arr[10];
int size;
int capacity;
}SeqList;
//C语言版本
//查询第i个位置的数据
int SLAT(SeqList* ps, int i)
{
assert(i < ps->size);
//...
return ps->arr[i];
}
void SLModify(SeqList* ps, int i, int x)
{
assert(i < ps->size);
//...
ps->arr[i] = x;
}
void test6()
{
SeqList s;
s.size = 3;
SLModify(&s, 0, 10);
SLModify(&s, 1, 20);
SLModify(&s, 2, 30);
printf("%d\n", SLAT(&s, 0));
printf("%d\n", SLAT(&s, 1));
printf("%d\n", SLAT(&s, 2));
}
C++版本:
//读取或修改第i个位置的值
int& SLAT(SeqList& s, int i)
{
assert(i < s.size);
//...
return s.arr[i];
}
//这种返回没有任何风险,因为函数SLAT返回后,顺序表s的空间本身不会被释放
void test5()
{
SeqList s;
s.size = 3;
SLAT(s, 0) = 10; //修改
SLAT(s, 1) = 20;
SLAT(s, 2) = 30;
cout << SLAT(s, 0) << endl; //读取
cout << SLAT(s, 1) << endl;
cout << SLAT(s, 2) << endl;
}
实现了查询和修改的一体化,简化了代码。
在理解C++版本的代码时,我们可以理解成,还有一个中间变量
int& k
。就拿SLAT(s, 0) = 10;
这段代码来说,SLAT
函数在返回是,先把s.arr[0]
的引用给了k
,此时k
就是s.arr[0]
,我们让k = 10
,当然就是让s.arr[0] = 10
。在执行cout << SLAT(s, 1) << endl;
时,就是打印k
,也就是打印s.arr[0]
。
1.6.5传值、传引用效率比较
1.6.6引用和指针的区别
1.误区:
1)在语法概念上,引用是一个别名,没有独立空间,和其引用实体共用同一块空间。
2)但是,引用在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
2.看一段代码
int main()
{
int a = 0;
int* p1 = &a;
int& ref = a;
++(*p1);
++ref;
return 0;
}
查看汇编代码
可以发现:
引用和指针的汇编代码几乎一致,说明引用的底层仍然是指针。
结论:
1)引用的底层实现,是依靠指针完成的。
2)既然底层是指针,那么引用的效率也是非常高的。
3.引用和指针的不同点:
1.引用概念上定义一个变量的别名,指针存储一个变量地址。
2.引用在定义时必须初始化,指针没有要求。
3.引用在初始化时引用一个实体后,就不能引用其他实体,而指针可以再在任何时候指向任何一个同类型实体。
4.没有NULL引用,但有NULL指针。
5.在sizeof中含义不同,引用为引用类型的大小,但指针始终是地址空间所占字节个数(32为平台下占4个字节)。
6.引用自加即引用的实体加1,指针自加即指针向后偏移一个类型大小。
7.有多级指针,但是没有多级引用。
8.访问实体方式不同,指针需要先解引用,引用编译器会自己处理。
9.引用比指针使用起来相对安全。
1.7内联函数
引入:
宏的优缺点:
1.缺点:
1)容易出错,语法坑很多。
2)不能调试。
3)没有安全检查。
2.优点:
1)没有类型的严格限制。
2)针对频繁调用的小函数,不需要再建立栈帧了,提高了效率。
3.思考:
有没有什么东西可以替代宏?在包含宏全部优点的情况下,比宏更加实用?
答:有,内联函数。
1.7.1概念
1.何为内联函数:
1)笼统的介绍:
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有正常函数调用建立栈帧的开销,内联函数提升程序运行的效率。
2)看一张图:
不使用内联函数时,编译器会使用call指令跳转到函数所在位置,进行栈帧的创建和销毁。
2.使用内联函数:
inline int Add(int left, int right)
{
return left + right;
}
//以inline修饰的函数叫做内联函数,
//编译时C++编译器会在调用内联函数的地方展开,
//没有函数调用建立栈帧的开销,
//内联函数提升程序运行的效率
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
在函数前加上inline关键字,将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
3.查看方式:
1)在release模式下,查看编译器生成的汇编代码中是否存在call。
2)在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出VS2022的设置方法):
1.7.2特性
1.内联的本质(下面的inline指的是内联函数):
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
缺陷:
可能会使目标文件变大。
优势:
少了调用开销,提高程序运行效率。
例:
- 现在有一个函数Func,里面有100行代码:
我们在100个位置调用这个函数:
1)如果这个函数时inline,合计有100*100共10000行代码。
2)如果不是inline,算上100句call和Func内部的代码,一共也才200行。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现的机制可能不同:
一般建议:
1)将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰。
2)函数规模过大(一般是超过10行)、或递归函数使用inline,大概率会被编译器忽略。
3.inline不建议声明和定义分离(写在不同文件中),分离会导致链接错误:
1)原因:
因为inline被展开,没有必要生成函数地址,链接就会找不到函数地址。
2)代码演示:
3)解决方案
-
内联函数不要定义和声明分离,统统写在.h的文件中即可。
-
如果不定义在.h文件中,只能当前文件定义,当前文件使用。
-
引入新问题:
如果有两个.cpp文件,里面都包含了含有内联函数定义的头文件,生成符号表时会不会重复?
答:不会,因为内联函数不生成指令,不生成地址进入符号表。
4.除了可以用内联函数来替代宏,还可以用const enum。
1)inline替代的是宏函数。
2)const enum替代的是宏常量。
5.扩展(inline的一些神奇现象):
下面是我gitee仓库中的一些代码:
1. 写函数的.cpp文件。
2. 测试用的.cpp文件,包含主函数。
3. .h头文件。
1.8auto关键字(C++11)
1.8.1类型别名思考
1.8.2auto简介
1.8.3auto的使用细则
1.8.4auto不能推导的场景
1.9基于范围的for循环(C++11)
1.9.1范围for的语法
1.在C++98中如果要遍历一个数组,可以这样写:
1)没有范围for之前的写法:
//没有范围for之前的写法
void Testfor()
{
int array[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
array[i] *= 2;
}
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); p++)
{
cout << *p << " ";
}
cout << endl;
}
输出结果:
2 4 6 8 10
2)有了范围for以后的写法:
//有了范围for的写法
void Testfor2()
{
int array[] = { 1,2,3,4,5 };
//改变一个数组
for (auto& x : array) //这里必须要取别名,否则就是一个赋值操作,没有意义
{
x *= 2;
}
for (auto x : array)
{
cout << x << " ";
}
cout << endl;
}
输出结果:
2 4 6 8 10
for(auto& x:array)
是什么意思?
相当于将array数组中的元素依次取出,每取一个就执行一次
auto& x = array[i]
,将取出的这个元素取别名为x。该写法用于修改数组元素。
for(auto x:array)
是什么意思?
就是
auto x = array[i]
,每取出一个元素就赋值给x。该写法只是为了遍历数组,不做修改。
2.注意:
for的范围循环和不同循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
3.思想:
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错。因此C++11中引入了基于范围的for循环。for循环后的括号由" : "分为两部分:
1)第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
2)之所以迭代变量的类型通常写成auto,是因为这样写可以涵盖所有的类型情况,要用什么类型的变量接收数组元素交给编译器自己来判断(当然要是你自己知道该用什么类型的变量,自己写也没问题)。
1.9.2范围for的使用条件
1.for循环迭代的范围必须是确定的:
1) 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环的迭代范围。
2)看一段问题代码:
// 数组名降级为指针
void Testfor3(int array[])
{
for (auto x : array) //直接报错
{
cout << x << " ";
}
cout << endl;
}
int main()
{
int array[] = { 1,2,3,4,5 };
Testfor3(array);
return 0;
}
如果要迭代数组,迭代对象必须写成数组名,不能写成指针。
2.迭代的对象要实现++和==的操作(关于迭代器的问题,以后会讲)。
1.10指针空值nullptr(C++11)
引入:
C语言中的一个坑:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
输出结果:
f(int)
f(int)
f(int*)
f(int*)
思考:
为什么传参传NULL,不会调用第二个函数?
1.10.1C++98中的指针空值
1.NULL实际是什么?
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的空指针时,都不可避免的会遇到一些麻烦。
2.引入中介绍的问题代码:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看做是一个整型常量,如果要讲其按照指针来使用,必须对其进行强转(void*)0。
3.注意:
1)在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++作为新关键字引入的。
2) 在C++11中,sizeof(nullptr)与sizeof(void*0)所占的字节数相同。
3)为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。