文章目录
本章内容主要了解c++,通过c++弥补c语言的不足
什么是C++?
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出OOP(object oriented programming:面向对象) 思想,支持面向对象的程序设计语言应运而生。1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此,C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++的发展史
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要∶修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即∶计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如∶正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如∶泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如∶static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等 |
C++关键字(C++98)
C++中总计有63个关键字,其中有32个是C语言中的关键字
命名空间
在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都将作用于全局作用域中,可能会导致很多命名冲突。使用命名空间的目的就是对标识符和名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
命名冲突问题
1、我们自己定义的变量、函数可能跟库里面重名冲突
2、进入公司项目组以后,做的项目通常比较大。多人协作,两个同事写的代码,命名冲突。
C语言没有办法很好的解决这个问题
CPP提出一个新语法,命名空间
命名空间的定义
定义命名空间,需要使用到 namespace 关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。该命名空间也叫域,常见的域全局域,局部域,c++新增的指定域,也就是我们接下来要学习。编译器查找成员的顺序,指定域,局部到全局。
补充:
//全局域的使用方式:
int a=10;
int main()
{
int a=20;
printf("%d\n",::a);
return 0;
}
一、命名空间的普通定义
//命名空间的普通定义
namespace BBQ //N1为命名空间的名称
{
//在命名空间中,既可以定义变量,也可以定义函数
int a;
int Add(int x, int y)
{
return x + y;
}
//类
class A{
int age;
}
二、命名空间可以嵌套定义
//命名空间的嵌套定义
namespace BBQ //定义一个名为N1的命名空间
{
int a;
int b;
namespace BBQ2//嵌套定义另一个名为N2的命名空间
{
int c;
int d;
}
}
三、 定义命名空间注意
同一个工程中允许存在多个相同名称的命名空间,编译器最后会将其成员合成在同一个命名空间中。
所以我们不能在相同名称的命名空间中定义两个相同名称的成员。
例如:
// List.h文件 申明
#pragma once
namespace szy
{
static int rand;
struct ListNode
{
//...
};
void ListInit();
void ListPushBack(struct ListNode* phead, int x);
}
//List.cpp文件 定义
#define _CRT_SECURE_NO_WARNINGS 1
namespace szy
{
void ListInit()
{
// ...
}
void ListPushBack(struct ListNode* phead, int x)
{
//...
}
}
//main文件
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include "List.h"
namespace szy
{
int add(int left, int right)
{
return left + right;
}
struct node
{
struct node* next;
int val;
};
}
int main()
{
szy::ListInit();
return 0;
}
上述代码,申明和定义分离,但都是放在szy的命名空间里,我们在main.cpp里展开.h文件,申明就被展开了,有了申明,申明检查就能通过,先申明后编译链接。
注意:一个命名空间就定义了一个新的作用域,命名空间中所有内容都局限于该命名空间中。
命名空间的使用
一、错误使用
namespace BBQ
{
// 他们还是全局变量,放到静态区的
int a = 10;
int b = 20;
//例1、不能在非执行区,写执行语句
a=20;//编译报错
//例2
void Swap(int a,int b)
{
// 函数执行区
int tmp=a;
a=b;
b=tmp;
}
struct person{
int age;
int hight;
}
}
int main()
{
// 例3
printf("%d\n", a); // 该语句编译出错,无法识别a
return 0;
}
二、命名空间的使用有三种方式
- 加命名空间名称及作用域限定符
通过这样的方式目的是为了指定作用域,访问相应的成员。
使用不方便,但做到最好的保护。
int main()
{
printf("%d\n", BBQ ::a);
return 0;
}
- 使用using将命名空间中成员引入
通过这样的方式把BBQ的部分成员展开;
用于展开常用的成员。
using BBQ::a;
int main()
{
struct BBQ::person p;
printf("%d\n", a);
printf("%d\n", BBQ::b);
return 0;
}
- 使用using namespace 命名空间名称引入
通过这样的方式把BBQ的所有成员展开;
全部展开,用起来方便,但是隔离失效了。慎用!
using namespace BBQ;
int main()
{
Swap(a,b);
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
C++输入&输出
我们在学C语言的时候,第一个写的代码就是在屏幕上输出一个"hello world",按照学习计算机语言的习俗,现在我们也应该使用C++来和这个世界打个招呼了:
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
在C语言中有标准输入输出函数scanf和printf,而在C++中有cin标准输入和cout标准输出。在C语言中使用scanf和printf函数,需要包含头文件stdio.h。在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间。
C++的输入输出方式与C语言相比是更加方便的,因为C++的输入输出不需要增加数据格式控制,例如:整型为%d,字符型为%c。
#include <iostream>
using namespace std;
int main()
{
int i;
double d;
char arr[20];
cin >> i;//读取一个整型
cin >> d;//读取一个浮点型
cin >> arr;//读取一个字符串
cout << i << endl;//打印整型i
cout << d << endl;//打印浮点型d
cout << arr << endl;//打印字符串arr
return 0;
}
注意:代码中的endl的意思是输出一个换行符。
缺省参数
缺省参数是指在声明或定义函数时,为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
#include <iostream>
using namespace std;
void Print(int a = 0)
{
cout << a << endl;
}
int main()
{
Print();//没有指定实参,使用参数的默认值(打印0)
Print(10);//指定了实参,使用指定的实参(打印10)
return 0;
}
缺省参数分类
全缺省参数,即函数的全部形参都设置为缺省参数。
void Print(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
半缺省参数
半缺省参数,即函数的参数不全为缺省参数。
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
1、半缺省参数必须从右往左依次给出,不能间隔着给。
2、缺省参数不能在函数声明和定义中同时出现(出现只能二选一)
3、缺省值必须是常量或者全局变量。
4. C语言不支持(编译器不支持)
函数重载
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表必须不同。函数重载常用来处理实现功能类似,而数据类型不同的问题。
#include <iostream>
using namespace std;
int sub(int x, int y)
{
return x + y;
}
double sub(double x, double y)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl;//打印1+2的结果
cout << Add(1.1, 2.2) << endl;//打印1.1+2.2的结果
return 0;
}
注意:形参列表不同是指参数个数、参数类型或者参数顺序不同,若仅仅是返回类型不同,则不能构成重载。
重载的特殊情况
无参参数列表和缺省参数列表
#include <iostream>
using namespace std;
void f()
{
}
void f(int a=10)
{
}
int main()
{
f(10);// 传参时没问题
f();// 编译错误,存在二义性
return 0;
}
上述代码符合重载规则,列表参数不同肯定构成重载,只是在调用时编译器不知道调用谁;
函数重载的原理
为什么C++支持函数重载,而C语言不支持函数重载呢?
我们知道,一个C/C++程序要运行起来都需要经历以下几个阶段:预处理、编译、汇编、链接。
我们知道每个源文件编译阶段都会把例如函数名,变量,等进行符号汇总,在汇编阶段会把每个符号分配一个地址(若符号只是用于声明,则给其分配一个无用的地址),然后生成一个符号表。最后在链接阶段会间所有的源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)。
在C语言中,汇编阶段进行符号汇总时,一个函数汇总后的符号就是其函数名,所以当汇总时发现多个相同的函数符号时,编译器便会报错。而C++在进行符号汇总时,对函数的名字修饰做了改动,函数汇总出的符号不再单单是函数的函数名,而是通过其参数的类型和个数以及顺序等信息汇总出 一个符号,这样一来,就算是函数名相同的函数,只要其参数的类型或参数的个数或参数的顺序不同,那么汇总出来的符号也就不同了。
详细讲解:
上图使用c语言编译是不通过的,因为c语言不支持重载。
为什么c语言不支持重载,而c++支持重载?
回顾一下编译器编译这个程序的过程
func.h func.c test.c
1.预处理 -> 头文件、宏替换、条件编译、去掉注释
生成 func.i test.i
2.编译 -> 检查语法,生成汇编代码
生成func.s test.s
3.汇编 -> 汇编代码转换成二进制码,生成符号表
生成func.o test.o
4.链接
生成 a.out
图上在汇编完以后,每个源文件都生成一个符号表,符号名对应一个地址,
图上在链接阶段,因为test.c文件在汇编阶段该源文件没有定义,只有声明,那么该调用指令call只能填一个无意义的地址,只能在链接时通过查找其他的符号表,发现有符号表有两个符号名相同的,存在歧义,链接失败报错。但是c++在这里就不一样了,c++通过命名修饰这样就不存在重名符号了。
总结:
1、c语言不支持重载原因在于,函数名没法区分,c++可以使用命名规则把相同的函数名通过参数列表不同区分开来。
函数重载补充
为什么函数重载要求参数不同!而跟返回值没关系?
假如与返回值有关系,当参数列表都一样时,返回类型不同构成重载时,我们传参时,编译器就无法确认该调用那个函数,产生异性,所以不行!
重载的特殊情况
当函数重载时,但是在调用时存在歧义的情况,我们首先分析编译器会先根据参数进行推演类型,然后与符号表进行匹配,例如有两个重载函数void add(int,int) void add(int*,int*);,但我们需要调用add(&a,&b) 时,a和b为int类型,会调用 void add(int*,int*),调用add(a,b)时会调用void add(int,int)。但是接下来的这种情况比较特殊void add(int,int) ,void add(int&,int&),调用add(a,b)时存在歧义,但又调用add(1,2)确又能正确调用。说明编译器通过参数的符号进行推演,而常数就不需要了,默认就是int类型。
引用
引用的概念
引用不是定义一个变量,而是已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
其使用的基本形式为:类型& 引用变量名(对象名) = 引用实体。
void Test()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注:引用类型必须和引用实体是同种类型。
引用的特性
一、引用在定义时必须初始化
二、一个变量可以有多个引用
三、引用一旦引用了一个实体,就不能再引用其他实体
常引用
上面提到,引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用其对应的类型,但该类型被const所修饰,那么引用将不会成功。被const修饰的权限比非const修饰的权限要小,权限放大不可以,权限不行可以,权限缩小,不可以。
void test()
{
const int a = 10;
//权限放大
//int& ra = a; //该语句编译时会出错,a为常量
//权限不变
const int& ra = a;//正确
//权限放大
//int& b = 10; //该语句编译时会出错,10为常量
//权限不变
const int& b = 10;//正确
//权限缩写
int d=10;
const int& c = d;//正确
//
//编译报错,隐式类型转换时产生临时变量,临时变量具有常性;
double b = 11.1;
int& a = b;//错误
const int& a = b;//正确使用
}
引用的应用场景
1.做参数
void add(int& a,int& b)
{//...}
形参a,b的改变能够影响实参,并且可以提升性能。
2.做返回值
#include<iostream>
using namespace std;
const int N = 10;
int& at(int idx)
{
static int arr[N];
return arr[idx];
}
int main()
{
for (int i = 0; i < N; i++)
{
at(i) = 1 + i;
}
for (int i = 0; i < N; i++)
{
cout << at(i) << ' ';
}
return 0;
}
上面的这种场景引用返回不仅可以读还可以写,对返回值进行修改。
说明:
a.返回值为引用时,可以减少返回值带来的临时拷贝情况,提升性能。
b.我们返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量可能会随着函数调用的结束而被销毁(根系统有关系)。我们返回的数据必须是被static修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
使用引用返回的原则:
如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已
经还给系统了,则必须使用传值返回。
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是
传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是
当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
struct A{ int a[10000]; };
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
引用和指针的区别
在语法概念上,引用就是一个别名,没有独立的空间,其和引用实体共用同一块空间。
int main()
{
int a = 10;
//在语法上,这里给a这块空间取了一个别名,没有新开空间
int& ra = a;
ra = 20;
//在语法上,这里定义了一个pa指针,开辟了4个字节(32位平台)的空间,用于存储a的地址
int* pa = &a;
*pa = 20;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点:
1.引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
第八点的理解:
void f1(int* a)
{
*a = 10;
}
void f2(int& a)
{
a = 10;
}
int main()
{
f1(NULL);
f1(0);
int a = 10;
f2(a);
return 0;
}
f1与f2函数进行对比,传参,函数体里的赋值操作,都是引用更加的安全。
使用指针要考虑空指针,野指针等待问题,指针太灵活了,所以相对而言,没有引用安全。
小结:引用的作用主要体现在传参和传返回值
1、引用传参和传返回值,有些场景下面,可以提高性能。(大对象、深拷贝)。
2、引用传参和传返回值,输出型参数和输出型返回值。通俗点说,有些场景下面,形参的改变可以改变实参。
有些场景下面,引用改变返回对象。
内联函数
概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
函数调用,需要建立栈帧,栈帧中药保存一些寄存器,结束又要恢复,可以看到这些都是由销毁的,对于频繁调用的小函数,能否优化一下?
通过如下代码对比:
//inline int Add(int a, int b) 内联函数
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
不加内联inline
加内联inline
从汇编代码中可以看出,内联函数调用时并没有调用函数这个过程的汇编指令。
有了inline,我们就不需要用宏,因为宏很复杂,很容易出错。
内联函数的特性
1、inline是一种以空间换时间的做法,省了去调用函数的额外开销。由于内联函数会在调用的位置展开,所以代码很长或者有递归的函数不适宜作为内联函数。频繁调用的小函数建议定义成内联函数。
2、inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有递归等,编译器优化时会忽略掉内联。
3、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了链接就会找不到。
说明一下:
a.以空间换时间实际,因为代码的指令增多了,我们来对比分析,
inline void Func()
{
编译好后是10行指令
}
不展开
1000 条调用 编译后合计多少指令 10+1000(省略开辟栈帧)
展开
1000 条调用 编译后合计多少指令 1000 * 10
所以长函数和递归函数不适合展开,调用地方很多,展开后程序可能会一下变得很大。inline对于编译器而已只是一个建议。
b.inline函数定义和声明不能分离
//F.h
#include<iostream>
using namespace std;
inline void f(int a);
//F.cpp
#include"F.h"
void f(int a)
{
cout << a << endl;
}
//main.cpp
#include"F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)"
// (?f@@YAXH@Z),该符号在函数 _main 中被引用
f.cpp生成f.o 符号表中不会生成f函数的地址,因为inline函数是不需要地址的都在调用的地方展开了。
auto关键字(C++11)
auto简介
在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
#include <iostream>
using namespace std;
double Fun()
{
return 3.14;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'A';
auto d = Fun();
//打印变量b,c,d的类型
cout << typeid(b).name() << endl;//打印结果为int
cout << typeid(c).name() << endl;//打印结果为char
cout << typeid(d).name() << endl;//打印结果为double
const double f = 1.11;
auto& ff = f; // ff为const double类型的引用
return 0;
}
注意:使用auto变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此,auto并非是一种“类型”的声明,而是一个类型声明的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用细则
一、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&。
#include <iostream>
using namespace std;
int main()
{
int A = 10;
auto B = &A ; //自动推导出b的类型为int*
auto* C = &A ; //自动推导出c的类型为int*
auto& D = A ; //自动推导出d的类型为int
//打印变量B,C,D的类型
cout << typeid(B).name() << endl;//打印结果为int*
cout << typeid(C).name() << endl;//打印结果为int*
cout << typeid(D).name() << endl;//打印结果为int
return 0;
}
注意:用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量。
二、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能推导的场景
一、auto不能作为函数的参数
二、auto不能直接用来声明数组
int main()
{
int a[] = { 1, 2, 3 };
auto b[] = { 4, 5, 6 };//error
return 0;
}
这是语法规定的,记住即可!
基于范围的for循环(C++11)
范围for的语法
若是在C++98中我们要遍历一个数组,可以按照以下方式:
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部加1
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
arr[i] += 1;
}
//打印数组中的所有元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
cout << endl;
以上方式也是我们C语言中所用的遍历数组的方式,但对于一个有范围的集合而言,循环是多余的,有时还容易犯错。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部加1
for (auto& e : arr)
{
e += 1;
}
//打印数组中的所有元素
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
注意:与普通循环类似,可用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
一、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
二、迭代的对象要实现++和==操作
这是关于迭代器的问题,大家先了解一下。
指针空值nullptr
C++98中的指针空值
在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其实是一个宏,在传统的C头文件(stddef.h)中可以看到如下代码:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
可以看到,NULL可能被定义为字面常量0,也可能被定义为无类型指针(void*)的常量。但是不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,例如:
#include <iostream>
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); //打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
Fun((int*)NULL); //打印结果为 Fun(int*)
return 0;
}
程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。
注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换。
C++11中的指针空值
对于C++98中的问题,C++11引入了关键字nullptr。
注意:
1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。
2、在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
3、为了提高代码的健壮性,在后序表示指针空值时建议最好使用nullptr。
小结:c里面的NULL是((void*)0),c++98修订了NULL,变为#define NULL 0,c++11在c++98的基础上增加了nullptr 关键字,与((void*)0)用法一样。