C++---入门基础

发布于:2024-05-05 ⋅ 阅读:(28) ⋅ 点赞:(0)

一、命名空间

在C/C++中,有大量的函数,变量乃至类,这些函数,变量和类的名称都将作用于全局作用域中,这可能会导致命名冲突。针对这个问题,我们就会使用命名空间,命名空间的目的就是对标识符及名称进行本地化,来避免命名冲突。

命名空间的定义

  • 命名空间的一般定义
namespace N1//N1就是命名空间的名称
{
    int a1;  //既可以定义变量
    int add(int x,int y)  //又可以定义函数
    {
        return x + y;
    }
}
  • 命名空间还可以嵌套定义
namespace N2 //定义一个空间名为N2的命名空间
{
    int a;
    namespace N3  //在N2内嵌套定义一个空间名为N3的命名空间
    {
        int b;
    }
}
  • 多个相同名称的命名空间

同一工程内,允许出现多个名字相同的命名空间,编译器会将这些成员整合在同一个命名空间内。故此,程序员不可以在相同名称的命名空间内定义两个相同名称的成员。

知识点:一个命名空间就定义了一个新的作用域,命名空间内的所有内容都局限在该命名空间内。

命名空间的使用

  • 通过“命名空间名称::命名空间成员”的方式访问某一命名空间中的某一成员,当使用该成员的时候,都需要在前面加上“命名空间::”
namespace N1
{
    int num;
}

int main()
{
    N::num = 1;
    cout << N::num << endl;//每次使用时前面都要加上N::
    return 0;
}
  • 通过“using 命名空间名称::命名空间成员”,这样在后面所有的变量都默认是该命名空间下的成员
namespace N
{
	int a;
	double b;
}
using N::a; //在下面的代码中,变量a默认使用的都是N命名空间中的
int main()
{
	a = 10;
	cout << a <<endl;
	return 0;
}
  • 使用using namespace命名空间名称的方法引入
namespace N
{
	int a;
	double b;
}
using namespace N;//将命名空间N的所有成员引入
int main()
{
	a = 1;
    b = 2;
	cout << a <<endl;
    cout << b <<endl;
	return 0;
}

其实我们不建议最后一种方法,最后一种方法在一些小的项目中可以使用,但是在大的项目中很容易造成命名重复或名字污染。

注意事项

我们有时候会见到这种写法 ::a,例如:cout<< ::a <<endl;的写法。::限定符左边是要填入命名空间的名字的,但是我们现在没有填,他的意思就是我们要引入一个命名空间里面的一个变量或者函数,但是这个命名空间的名字为空,什么样的命名空间没有名字呢?那就只剩全局变量了。所以当::左边为空的时候,::右边的那些变量就表示的是没有被封装到命名空间里面的全局变量。

#include <iostream>
namespace n1
{
	int a = 10;
}
namespace n2
{
	int a = 20;

int a = 30;
int main()
{
	int a = 40;
	cout << ::a << endl;
	return 0;
}

我们打印这段代码的话得到的结果为30,是因为::a这种没给命名空间的名字默认是去全局变量中找,在全局变量中我们定义的a=30,所以打印结果为30。

命名空间内定义的变量都是全局变量。

二、缺省参数

缺省参数就是在声明或定义函数的时候,为函数参数指定一个默认值。当调用该函数的时候,若没指明参数的值,则采用这个默认值。

全缺省参数

指的是全部参数都有缺省值

void Print(int a = 1, int b = 2, int c = 3)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

半缺省参数

有部分参数有缺省值,部分参数没有

void Print(int a, int b = 2, int c = 3)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

三点注意事项

1.半缺省参数必须从右至左给,不能间隔

例如上面我们举得例子,就只能有三种缺省方法,1:给c的缺省值 2:给b,c的缺省值 3:给a,b,c的缺省值。例如:只给b的缺省值,不给c的类似的做法是错误的

2.缺省参数不能在函数的声明和定义中同时出现
void Print(int a, int b, int c = 3);//在.h文件中的声明

void Print(int a, int b, int c = 3) //在.c文件中的定义
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

//这是错误示例

缺省参数只能在声明或定义中出现,二者不能同时出现。

3.缺省值只能是常量或者全局变量
int num = 3;//全局变量
void Print(int a, int b = 2, int c = num)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

类似于上面的代码,2是常量,num是全局变量,就是正确的

缺省参数的意义

在我们使用c语言实现数据结构的时候,例如在我们进行顺序表初始化的时候,我们并不知道要开辟一个多大的空间,所以我们当时的做法是一开始将其初始化为4,随后不够用的话,再接着扩容,但是realloc扩容是需要付出代价的,不停的扩容,会影响程序的效率。如果我们一开始就需要一个非常大的数组,那么我们就需要扩容很多很多次,导致效率很低。

void SLCheckCapacity(SL* psl)
{
	assert(psl);
	if (psl->capacity == psl->size)
	{
		psl->capacity = psl->capacity == 0 ? 4: psl->capacity * 2;
		SLDataType* p1 = realloc(psl->a, (psl->capacity)*sizeof(SLDataType));
		//注意这里是容量乘以元素的大小,不是单独的一个容量
		if (p1 == NULL)
		{
			perror("realloc fail");
		}
		psl->a = p1;
	}
}

void SLInit(SL* psl)
{
	assert(psl);
	psl->a = NULL;
	psl->capacity = psl->size = 0;
}

//这是我们以前的初始化顺序表的操作
//就算一开始的需求很大,也只能一点点的扩容

所以如果我们一开始就需要一个很大的空间的话,好的办法是一开始就给他创建一个很大的空间,

使用缺省参数能很好的解决这个问题:

void SLInit(SL* psl,int defaultCP=4)
{
    ps1->a =(int*)malloc(sizeof(int)* defaultCP);
	assert(psl);
	psl->capacity = psl->size = defaultCP;
}

我们为初始化函数多创建一个参数defaultCP,用于记录初始化要创建的空间大小,设置其缺省参数为4,当不需要很大的空间的时候,我们可以不传这个参数,就用缺省值4。但当我们一开始需要的空间就很大的时候,我们就可以穿过来一个很大的参数数值,例如我们可以传过来一个10000,这时我们就不用一遍又一遍地扩容了,就提高了很高的效率。

三、函数重载

函数重载的概念

函数重载就是指C++允许在同一作用域中声明几个功能类似的同名函数,前提是:这些函数的形参列表不能相同。常用于处理功能类似,数据类型不同的问题。

例如:

int add(int x, int y)
{
	return x + y;
}

double add(double x, double y)
{
	return x + y;
}

两个函数的功能都是将参数相加,但是参数的类型一个是int,另一个是double。

需要注意:形参列表不同是指参数个数,参数类型不同。仅仅返回值不同则不能构成函数重载。

函数重载的奇异性

大家看下面的代码:其中两个函数参数个数不同,所以他们构成函数重载。但是有一个问题就是有参数的f()函数两个参数都有缺省值。

void f()
{
	cout << "f()" << endl;
}

void f(int a = 1, int b = 1)
{
	cout << "f(int a, int b)" << endl;
}

当我们传有参数调用时,是可以正常调用的:

但当我们没有传入参数调用时,会出现问题:

这是因为编译器不知道要调用哪个函数,第一个函数没有参数,第二个函数两个参数都有缺省值,所以两个函数在调用的时候可以都不传参数。这就导致了我们调用时,如果不传参数,就会出现编译器不知道调用哪个函数的现象。这被称之为函数重载的奇异性

函数重载的原理

我们知道C语言不支持重载,C++支持重载,这是为什么呢?

一个C/C++源文件生成一个可执行程序需要做四个工作:预处理,编译,汇编,链接

在编译阶段会将程序的每个源文件全局变量和函数的名字和地址分别汇总起来。在汇编阶段会给每个源文件汇总出来的符号分配一个地址(若符号只是一个声明的话,则给他分配一个无意义的地址),然后分别生成一个符号表。最后在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(这叫重定位)。

C语言中,汇编阶段中,进行符号汇总时,一个函数汇总后的符号就是函数名。所以若有相同名字的函数存在,汇总时就会发现多个相同的函数符号,编译器就会报错。但在C++中,进行符号汇总时,不是直接使用函数名,而是通过参数的顺序,类型,个数的信息汇总出一个符号,这样的话,即使有相同函数名的函数存在,也会因为他们的参数列表不一致而产生不同的符号,这就是函数重载的原理。

这是在Linux环境下C语言所产生的符号:

这是在Linux环境下C++所产生的符号:

extern "C"

说到了C和C++的差异,有时候我们在C++的工程内,可能需要以C的风格来编译,这时在函数前面加上extern "C"就会使编译器以C语言的规则来编译这个函数。和本章联系起来的话,加上extern "C"后,该函数就不支持函数重载了。

四、引用

引用导入

在生活中,每个人或许都有自己的别名或者其他人对自己的爱称。例如一个人叫李明,他在家里被爸妈叫做乖儿子,被朋友叫做小明,被同事叫做小李。乖儿子,小明,小李都是李明这个变量的别名,而这些别名在C++中被称为引用。

引用的形式

类型名  &引用名  = 被引用的变量名

引用之后,两个变量实际上就是同一个变量,就像小李,小明,实际上都指的是同一个人。所以引用导致的就是两个变量有一个值变化,另一个也会跟着变化,因为两者实际上是同一个变量。

引用的注意事项

引用时必须初始化
int main()
{
    int a = 10;
    int& b= a;//引用时必须初始化
    return 0;
}
//这是正确的

int main()
{
    int a = 10;
    int& b;
    b = a;
    return 0;
}
//这是错误的,引用的时候没有初始化

至于被引用的实体有没有初始化,这个无所谓。

引用只能引用一个实体,不能再引用其他实体
int main()
{
    int a = 10;
    int b = 20;
    int&c = a;
    c = b;
    cout << c <<endl;
    cout << a <<endl;
    return 0;
}

我们运行这段代码,会发现此时c的值为20,这是不是c改成引用指向b了呢,并不是!这只是将b的值赋给了a,这是赋值操作,我们再观察就会发现a的值也被改成了20。

一个变量可以有多个引用
int main()
{
    int a = 10;
    int& b = a;
    int& c = a;
    int& d = a;
    return 0;
}

这里的b,c,d都是a的引用。

常引用

我们前面使用引用的时候都是这样使用的:

#include<iostream>
int main()
{
	int a = 10;
	int& b = a;
	return 0;
}

但是如果我们在引用b前面加上const呢?使其称为一个常量:

#include<iostream>
int main()
{
	int a = 10;
	const int& b = a;
	return 0;
}

此时b变成了一个常量,我们无法改变他的值,那我们能改变a吗?是可以的!!

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& b = a;
    a+=10;
    cout<<a<<" "<<b<<endl;
	return 0;
}

我们成功改变了a的值,连带着b的值也被改了:

但是如果我们直接对b进行更改就会出错:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& b = a;
	b += 10;
	cout << a << " " << b << endl;
	return 0;
}

并不是每个别名都能和原名有一样的权限,我们上面的行为被称之为权限的缩小,a原本不是常量,可以被更改,但我们的引用b却加上了const无法被修改了,这就叫做权限的缩小。有权限的缩小,就应该有权限的平移和扩大。

我们先来看权限的平移,顾名思义,权限的平移就是指,在引用前后的权限不变,这种不会出什么乱子,就类似于下面的代码:

int main()
{
	int a = 10;
	const int& b = a;
	return 0;
}

我们再来看权限的扩大:

int main()
{
	const int a = 10;
	int& b = a;
	return 0;
}

上面的代码就属于权限扩大,原本被引用的变量不能被修改,但是引用后的值却可以被修改,这样的话会报错,因为语法规则中不允许权限放大。

总结一句话就是:在我们引用初始化的时候,权限可以不变或缩小,但是不能放大。当然在其他背景下,例如函数传参的场景下,也是要遵守权限不能放大的原则的。当我们函数在定义时参数没加const,但是调用时参数却加了const时就属于权限的放大。例如下面的情况就会出现报错:

f()函数定义时参数没用const修饰,但是调用的时候却加了const,这就会产生权限放大。

所以我们又有一个结论:在我们以后写函数的时候,采用引用传参的时候,如果函数参数本身不用发生变化,那么我们在定义的时候直接就给函数参数都加上const,这样就不管使用函数的时候传的参数是否加上const都不会出现错误。

还有一个知识点就是,我们的引用是可以用常量对其初始化的,但是前提必须是前面加上const,是常引用。因为常量具有常性,不可被修改,我们引用就不能权限扩大。

所以我们的引用作为参数时,如果有缺省值的情况下要注意,不要出现下面的情况,会出现权限扩大:

#include<iostream>
using namespace std;
void f(int& a = 10)//这里权限扩大了,报错
{
	cout<<a<<endl;
}
int main()
{
	f();
	return 0;
}

在类似上面的情况中,如果我们一定要给函数参数设置一个缺省值的话,就要给函数参数前加上const做修饰,这样才能防止权限扩大。

这里我们还有一个情况需要解释,我们大家都了解隐式类型转换,就类似下面的代码,用一个double值初始化一个int值:

#include<iostream>
int main()
{
	double b = 1.2;
	int a = b;
	cout<< a << endl;
	return 0;
}
//打印出来结果为1

问题就是:如果我们采用引用的方式,还可以正确的得到初始化的值吗?例如下面的代码:

#include<iostream>
int main()
{
	double b = 1.2;
	int& a = b;
	cout<< a << endl;
	return 0;
}

又会报出我们见过好多次的错误,权限扩大

这是为什么呢?我们的b也没被const修饰,为什么会报出权限扩大的错误?

这是因为我们通过不同的类型变量来初始化,会出现叫做截断现象的情况。我们将b赋值给a,并不是直接将b的值改为1后赋给a,而是再创建出一个临时空间,这个空间中装的是被截断后的值,即1,是将这个临时空间的值1赋给a的。问题就出在创建出来的这个临时变量身上,这个临时空间具有常性,不可以被修改,所以在把这个临时变量赋值给a的时候,a如果不加const修饰,就会出现权限扩大的错误。

这种情况在我们使用函数的时候也会出现,例如下面的代码:

int f()
{
	int a = 1;
	return a;
}
int main()
{
	int& b = f();
	return 0;
}
//会报出权限增大的错误

函数f()采用传值返回,main()内部接收的时候采用引用接收,在函数f()执行返回a后,函数栈帧就被销毁了,所以此时b接收的值也是一个临时变量,临时变量具有常性,所以上面的代码中b也要被const修饰,否则报错。

引用的使用场景

引用做参数

引用做参数可以使我们能在函数内部用改变形参的方式改变函数外的实参,例如下面的swap代码:

void swap(int&a,int&b)
{
    int tmp=a;
    a=b;
    b=tmp;
}

用引用作为参数的时候也可以构成函数重载,我们要注意的是用引用构成的函数重载在调用的时候容易产生歧义,例如下面的代码:

void swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
void swap(int &x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
    int x=1;
    int y=2;
	swap(x,y);
	return 0;
}

最终的结果就是调用产生歧义:

引用做返回值

在介绍之前,我们先看一段代码做例子:

#include<iostream>
using namespace std;
int count1()
{
	static int n = 0;
	n++;
	return n;
}
int count2()
{
	int x = 0;
	x++;
	return x;
}
int main()
{
	int a = count1();
	int b = count2();
	cout << a << endl;
	cout << b << endl;
	return 0;
}

这里我们创建两个函数,第一个返回的是一个静态变量的值,第二个返回的是一个局部变量放在栈区的值,我们在main函数内用两个数接收。

运行程序,两个值完全没错。

我们刚刚函数返回值的类型是int,现在我们把他改为int&测试一下,看看有没有问题:

#include<iostream>
using namespace std;
int& count1()
{
	static int n = 0;
	n++;
	return n;
}
int& count2()
{
	int x = 0;
	x++;
	return x;
}
int main()
{
	int& a = count1();
	int& b = count2();
	cout << a << endl;
	cout << b << endl;
	return 0;
}

运行上述代码后,我们会发现有的编译器的第二个值为一个随机数,而第一个数一直是没问题的。

这是为什么呢?这里的原因就是我们用引用变量来接收的时候,该变量依旧会指向函数里面创建的那个变量的空间,至于第一个全局变量无所谓,因为他是全局的,保存在全局区,count函数结束了他也不会消失,但是第二个变量就不行了,这时引用b指向的依然是局部变量x的空间,但是count2函数执行完毕后,x的空间就被销毁了,那么现在b依然指向这一空间,就会产生随机值。

所以我们可以得出一个结论:若函数返回值离开函数作用域后就不存在了,就不能使用引用返回,若出了函数作用域,返回变量还存在的话可以使用引用返回。

引用的好处

我们为什么要采用引用,更多的原因是在效率上,无论在函数传参还是函数返回值方面,引用都要比传值的效率高太多了。用传值的方法,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。而采用引用就都不用做拷贝工作,提高了很高的效率。

引用和指针的区别

   1、引用在定义时必须初始化,指针没有要求。

 2、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
 3、没有NULL引用,但有NULL指针。
 4、在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
 5、引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
 6、有多级指针,但是没有多级引用。
 7、访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
 8、引用比指针使用起来相对更安全

    9、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。

实际上,在底层实现方面,引用和指针是一样的,引用实际上是按照指针方式来实现的。他俩是同根同源。

五、内联函数

使用内联函数的原因

我们在函数调用的时候会创建函数栈帧,创建函数栈帧是会消耗性能的,如果我们的函数很简单,但是我们调用的时候还要开辟一个栈帧,这样的话就会非常浪费性能,尤其是对那种功能简单的小函数。在C语言中,为了解决这个问题,使用的是宏,在函数调用的时候不创建栈帧,而是直接把代码替换到那里,但是宏函数也有自己的缺点:

  • 不方便调试宏(在预编译阶段就进行了替换)
  • 代码可读性,可维护性差,易误读
  • 没有类型安全的检查

为了解决上述宏的缺点,C++给我们提供了内联函数。

内联函数就是用inline修饰的函数,在编译时编译器会在调用内联函数的地方将函数展开,减少了函数调用建立栈帧的性能开销,提升程序的运行效率。

使用内联函数的方法

在函数定义前加上inline即可,例如:

#include<iostream>
using namespace std;
inline int add(int left, int right)
{
	return left + right;
}
int main()
{
	int ret = 0;
	ret = add(1, 2);
	return 0;
}

通过反汇编来观察内联函数的普通函数调用时的区别

我们先调用普通函数,将上面的代码去掉inline,调试时转到反汇编:

我们会发现汇编代码中存在call命令,而call命令就是函数调用的意思,所以这里就说明我们调用了函数,创建了函数栈帧,我们接下来再看调用内联函数的反汇编:

我们发现,调用内联函数的反汇编跟普通函数的反汇编一样,那么是不是我们搞错了呢?其实不是,而是因为我们现在是debug调试环境,如果要替换的话,我们程序员就不好调试了,所以我们要在release环境下看反汇编。下面我们将环境进行改变:

这次我们会发现没有了call指令,也就是没有了函数调用,减少函数栈帧的创建。

内联函数的特性

  1. 使用内联函数是一种以空间换时间的策略,省去了创建函数栈帧的开销。因为内联函数会在调用的位置展开,所以代码太长或者内有递归操作的函数不建议被定义为内联函数。被频繁调用的小函数建议定义为内联函数。
  2. inline对编译器而言只是一个建议,编译器会自动优化,假设一个函数被加上了inline,但是他本身内部有递归操作,编译器优化时也会取消掉其内联函数的身份。
  3. inline不能声明和定义分开,因为调用内联函数的时候是将该函数代码直接展开在调用的地方的,没有了函数地址,链接的时候就找不到函数了。

六、C++11中的auto关键字

在早期版本的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但是遗憾的是没有人习惯用它。

在2011年,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

int main()
{
	int a = 3;
	char b = 'A';

	//通过右边的赋值对象,自动推导变量类型
	auto c = a;
	auto d = b;

	//我们可以通过typeid得出变量的类型
	cout << typeid(c).name() << endl;//打印结果是int
	cout << typeid(d).name() << endl;//打印结果是char
	
	return 0;
}

注意:在使用auto定义变量的时候必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非一种“类型”的声明,而是一个类似于声明时的“占位符”,编译器在编译期间会将auto替换成变量实际的类型。

auto的使用细则

1.auto与指针和引用结合使用

        用auto声明指针的时候,用auto和auto*没有任何区别。但是用auto声明引用的时候必须加上&

int main()
{
	int y = 0;
	auto x1 = &y;
	auto* x2 = &y;
	auto& x3 = y;
	auto x4 = y;
	cout << typeid(x1).name() << endl;//int*
	cout << typeid(x2).name() << endl;//int*
	cout << typeid(x3).name() << endl;//int
	cout << typeid(x4).name() << endl;//int
	return 0;
}

2.在同一行定义多个变量

        用auto在同一行定义多个变量时,要保证这些变量是同一类型,否则会报错:

int main()
{
    auto a=1,b=2;//ok
    auto x=1,y=1.2;//err
    return 0;
}

auto不能使用的场景

1.auto不能作为函数参数的类型,因为编译器无法对x的实际类型进行推导。

void test(auto x)//err
{}

2.auto不能直接用来声明数组

int main()
{
	auto b[] = { 1,2,3 };//err
	return 0;
}

auto的好处与缺点

auto的好处在初学的时候体现不出太多,在后面STL的时候就能体现出来了,目前的好处和缺点就有俩:

好处:auto可以自动推导类型简化代码

缺点:一定程度上牺牲了代码的可读性

七、C++11中的范围for

在C++98中,我们想遍历一个数组,通常采取下面的方法:

	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		arr[i] *= 2;
	}
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

上面也是我们在C语言中常用的遍历数组的方式,但是对于一个有范围的集合来说,循环是多余的,还可能会犯错。所以C++11引入了基于范围的for循环。佛如循环后由冒号分为两部分:前面是范围内用于迭代的变量,后面表示是被迭代的范围。

	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	//将数组元素值全部乘以2
	for (auto& e : arr)//当想改变元素内容时要用引用
	{
		e = 1;
	}
	//打印数组中的所有元素
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;

注意: 与普通循环一样,可以使用continue和break,作用一样。

范围for的使用条件

1.for循环迭代的范围必须是确定的:

        对数组而言,就是第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

2.迭代的对象要实现++和--操作:

        这是有关于迭代器的东西,我们以后会介绍。

八、空指针nullptr

我们要有良好的编程习惯,例如声明变量的时候我们一般都会给一个合适的初始值,否则可能会出现意想不到的错误。指针就是如此,如果一个指针我们一开始没有想指向的地址,我们基本上都会将他初始化成一个空指针NULL:

int* p1 = 0;
int* p2 = NULL;

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,这个0既可以表示一个整型数字0,也可以表示无类型的指针(void*)常量。但不论采用哪种定义,在使用空值的指针时,都不可避免地会遇见一些麻烦,如:

#include <iostream>
using namespace std;
void F(int p)
{
	cout << "F(int)" << endl;
}
void F(int* p)
{
	cout << "F(int*)" << endl;
}
int main()
{
	F(0);           //结果为 F(int)
	F(NULL);        //结果为 F(int)
	F((int*)NULL);  //结果为 F(int*)
	return 0;
}

我们本来的意思是想F(0)调用参数为int类型的F(int p)函数,F(NULL)来调用参数为指针的F(int* p)函数。

但是由于NULL被定义为0,编译器将NULL看成了一个整型常量0,最终F(NULL)调用的也是F(int p)函数。只有在强转之后,才能得到我们想要的效果。

因此,为了解决这个问题,C++11引入了关键字nullptr。

三点注意

1、用nullptr表示空指针时不需要包含头文件,因为nullptr是C++11作为关键字引入的。

2、在C++11中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。

3、为了提高代码的健壮性,以后我们使用的时候尽量都使用nullptr。