面对基本类型的时候,我们可以使用动态内存(new和delete)。
而面对类的时候,也可以使用动态内存,只不过使用方法有区别。
例如声明一个私有成员,想要这个私有成员的值为一个字符串(但这个字符串是什么是未知的)。
首先,不考虑用char word[40];这样的。原因有两点:①实际字符串可能超过40个字符;②对于没有超过的,很可能导致内存浪费(例如创建了1w个对象,有9000个只要一个字符长度,1000个需要40个字符长度);
因此,可以使用指针,让指针来指向字符串。
但单纯用指针指向字符串也存在问题:
①直接指向字符串的话,若字符串被修改,那么该对象的成员的值也会随之改变;
因此,具体做法是一般用new来分配一个动态内存,然后让成员的指针指向这个动态内存,并将字符串的内容复制到这个动态内存之中;等类对象消失时,用析构函数delete这个动态内存,防止内存泄露。
如代码:
//new.cpp #pragma once #include<iostream> class Player { char *name; //名字 int id; //id编号 static int players; //玩家数量,注意,它被static所限定,所以是静态变量(并且是全局的)。可以通过Player::players来访问,且这里不能初始化(因为类声明不分配内存,即使是静态内存) //静态类变量有一个特点,无论有多少个对象,都只有一个副本。也就是说,所有的对象,都共享这个静态变量 public: Player(const char*); //构造函数 Player(); //默认构造函数 ~Player(); //析构函数 friend std::ostream& operator<<(std::ostream& os, Player&); //运算符重载<< }; //new.cpp #include<iostream> #include"new.h" Player::Player(const char* a) //构造函数 { name = new char[strlen(a) + 1]; //strlen是不计算末尾空字符的。注意,这里有new,那么构造函数都要对应的delete strcpy_s(name, strlen(a) + 1, a); //将a复制到name之中(name指向的是new出来的字符串) //这里假如直接用name=a的话,那么其实是让name指向了a的地址,而不是将a的值赋给了name players++; //新建的话,人数加一 id = players; //id为人数。之所以在players++后赋值,是因为其+1指的是当前新建的对象 std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; } Player::Player() //默认构造函数 { name = new char[10]; strcpy_s(name,10, "未起名"); players++; id = players; //id为人数 std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; } Player::~Player() //析构函数 { std::cout << "姓名:" << name << " 的玩家已被删除。其ID号为:" << id << "剩余玩家数:" << --players << std::endl; delete []name; //delete对应new。name指向的是new出来的内存,注意,构造函数是new[],所以这里是delete[] } std::ostream& operator<<(std::ostream& os, Player& m) //运算符重载<< { os << "姓名:" << m.name << ",ID:" << m.id; return os; } //1.cpp main函数,用于测试 #include<iostream> using namespace std; #include "new.h" //to avoid confusion with complex.h int Player::players = 0; //声明全局静态变量,才能引用。 int main() { Player a; { Player b("charname"); cout << b << endl; { Player c("成龙"); cout << c << endl; } } cout << "Done!\n"; system("pause"); return 0; }
显示:
姓名:未起名,ID:1的玩家已被创建。 姓名:charname,ID:2的玩家已被创建。 姓名:charname,ID:2 姓名:成龙,ID:3的玩家已被创建。 姓名:成龙,ID:3 姓名:成龙 的玩家已被删除。其ID号为:3剩余玩家数:2 姓名:charname 的玩家已被删除。其ID号为:2剩余玩家数:1 Done! 请按任意键继续. . .
注意:
①new和delete应对应。因为是new char[],所以也应该是delete[],而不是delete
②在类中,被static(但不包含const)所限定的成员,只能在类外进行初始化。又因为需要全局可以使用(因为是类,不能不允许只在某个代码块使用),所以需要在代码块外进行初始化。所以int Player::players = 0;的位置才在main函数外。
③b和c在代码块内声明,因此离开代码块时,b和c将调用析构函数删除。
特殊成员函数:
析构函数和按值传递函数所产生的问题:
假设,将1.cpp的代码修改,新增2个函数,分别参数为Player类的引用和Player类的按值传递。其他不变。
新的代码部分如下:
//1.cpp main函数,用于测试 #include<iostream> using namespace std; #include "new.h" //to avoid confusion with complex.h int Player::players = 0; //声明全局静态变量,才能引用。 void show1(Player& a); void show2(Player a); int main() { //1#大括号 Player a; { //2#大括号 Player b("charname"); show2(b); { //3#大括号 Player c("成龙"); show1(c); } } cout << "Done!\n"; system("pause"); return 0; } void show1(Player& a) { cout << a << endl; } void show2(Player a) { cout << a << endl; }
在这段新的代码中,会出现错误。
错误表现在:
(1)show2(b);这行代码,运行后,提示charname这个对象(也就是Player b)被删除。正常的话,是不会删除的。
(2)由于删除了,因此Player c创建并初始化的时候,id为2,而不是期望的3。
而show1(c)这个是正常运行的——即单纯的显示了对象c。
(3)在2#大括号结束后(即离开2#大括号所包括的代码块时),程序提示出错。原因在于,Player b对象,理应在离开代码块时被删除,但是在show2(b)函数调用时就被删除了(见(1)),那么离开代码块时理应调用析构函数删除对象b,则delete[]了其 堆中对象b的私有成员name原本指向的内存地址 。相当于这个地址被二次delete[]。
可见,当析构函数delete删除对象new出来的数据时,若使用按值传递,则会出现错误。而按引用传递,则一切正常。
之所以这样,是因为show2(按值传递)错误的调用了析构函数(因为调用了析构函数才会输出xxx已被删除这段话)。
按照书上的来看,假如编译器不提示错误,而显示出所有的内容,那么问题会反应的更明显。
析构函数意外的被调用:
可以观察到黑线,在离开这个代码块时,有5个析构函数被调用(因为先手创建了5个对象,headline1、headline2、sports、sailor、knot),
但是前2个还正常,后三个则出现了错误。
第三个之所以出错,原因在于callme2(headline2);这个函数,错误的调用了析构函数,导致提示headline2的字符串被删除。我们也可以看到,根据自动变量的栈的LIFO原则,第一个退出的应该是knot,而knot=headline1(因此其指向同一个字符串),于是第一个退出对象的析构函数调用正常,第二个是sailor=sports,因此也正常。而第三个到了sports,但因为第三个和第二个指向的是同一个字符串(“Spinach.......”),已经被delete[]过了,因此第三个字符串显示便不正常(这时姑且不论计数),而第4个是headline2(在callme2()函数中已经被调用过析构函数了),第5个道理同第三个。因此,第3,4,5个退出的对象都不正常。
创建对象但未按预计那样调用构造函数:
除了析构函数,另外还有1个对象在创建时,被调用,却未按我们计划进行声明,是:
Stringbad sailor = sports;
这个对象。对象sailor是通过另外一个对象赋值创建的,按照我们计划中,构造函数有无参数的默认构造函数,和带一个字符串地址的构造函数。无论是哪个,在创建一个新对象时,都应该声明新对象被创建,并且计数加一。但实际上,并没有(看图中的绿色圆圈)。
原因分析:
之所以会这样,是因为 特殊成员函数 引起的。而这些函数是 自动定义 的,就书上这个例子Stringbad类和我修改过的1.cpp源代码文件而言,这些函数的行为和类设计的不符(我并没有想让类这样做)。
这些 自动定义 的 特殊成员函数 包括5种:
①默认构造函数——如果没有定义构造函数的话(例如Player类的默认构造函数是Player(){}且无赋值);
②默认析构函数——如果没有定义的话(例如退出一个类对象在退出代码块的话,会清除其所有私有成员——前提是没有new出来的,new出来的需要自己编写析构函数清除);
③复制构造函数——如果没有定义的话(这个之间并没有遇见过);
④赋值运算符——如果没有定义的话;
⑤地址运算符——如果没有定义的话。
更准确的说,是编译器将在做以上最后三个这个行为时,生成其定义。
例如,我们没有定义赋值运算符,然后我们要将Player类的对象a赋值给对象b,编译器会自动生成赋值运算符的定义。
结果表明:Stringbad类中的问题,是由隐式复制构造函数和隐式赋值运算符引起的(编译器自行生成的定义)。
另外:C++11还提供了另外2个特殊成员函数:①移动构造函数;②移动赋值运算符。这两个在18章(所以好远)
①关于第一个——默认构造函数:
之前已经学到过,默认构造函数可以由用户自行定义,也可以不定义默认构造函数但定义构造函数。在这两种情况下,编译器不会定义默认构造函数。
否则,如果用户不定义构造函数/默认构造函数,编译器将提供一个默认构造函数(无任何赋值)。
②复制构造函数:
复制构造函数用于将一个对象 复制 到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。
类的复制构造函数原型如下:
Class_name (const Class_name &);
它接受一个指向类对象的常量引用作为参数。例如,Stringbad类的复制构造函数的原型是:Stringbad (const Stringbad&);
(1)何时调用复制构造函数
新建一个对象,并将其初始化为同类现有对象时,复制构造函数就会被调用。
简单的来说:在声明一个对象的同时,进行初始化,且这个初始化的形式是用另一个已有对象。
例如已有Player类对象m;
Player m;
Player a(m);
//给参数的形式
Player b = m;
//赋值形式
Player c = Player(m);
//m作为参数创建一个对象并赋值,注意,必须使用对象,否则会调用构造函数,而不是复制构造函数。
Player*d = new Player(m);
//new一个对象并赋值,这里d是指针
复制构造函数为:
Player (const Player& m) { std::cout << "a"; }
//复制构造函数
实测证明,在创建对象a、b、c三个对象和d这个指针时,复制构造函数被调用。(但由于这里的复制构造函数的定义只是表示调用,因此无法输出这4个对象,会出错)
对象b和c,可能使用复制构造函数直接创建b和c,也可能使用复制构造函数生成一个临时的对象,然后将临时对象的内容赋值给b和c,这取决于具体的实现(也就是看代码怎么写)。
指针d,使用对象m初始化一个匿名的对象,然后将这个匿名的对象的地址,赋给d指针。
每当程序生成对象副本时,编译器都将使用复制构造函数。具体包括:
《1》当函数按值传递对象(例如void show2(Player a));
《2》当函数返回对象时(例如return a);
《3》编译器生成临时对象时(例如a+b+c时,有可能生成临时对象。但这种情况是否生成,需要看编译器);
都将调用复制构造函数(特别是《1》和《2》)
例如,函数:
void show2(Player a)
{
cout << a << endl;
}
将使用复制构造函数,生成一个Player类的形参。而结束这个函数时,生成的形参消亡,于是触发了析构函数,delete了实参new出来用于储存字符串的地址。
(2)默认的复制构造函数的功能
默认的复制构造函数的功能,是逐个复制非静态成员(成员复制也称为浅复制)(注意,没有复制例如static限定的,以及enum枚举类型),复制的是成员的值。
例如Player类有int a和int b两个成员。那么Player A=Player B; 就相当于A.a=B.a; A.b=B.b;这样(当然,实际上是不能这么写的,这里表示意思如此)。
如果成员本身就是类对象(就是说一个类A的成员,是另一个类B的对象,嵌套效果),则使用该类(类B)的复制构造函数来复制成员对象(类B的对象)。
这也就是说,如果类对象A的成员a是一个指针,那么通过复制构造函数来将A的值赋给类对象B时,B的成员a也是一个指针,且和A的成员a指向同一个地址。
这样的话,当A.a指向的地址的值改变时,B.a指向的地址的值也随之改变(因为他们是同一个地址)。那么A.a如果是new出来的对象,被删除后,B.a也同时受到了影响。
因此,
应显式的声明并自定义一个复制构造函数:
特别是在成员中有使用new来请求动态内存时,应显示的自定义一个复制构造函数。
自定义复制构造函数例子如下:
函数原型:Player(const Player& m);
//复制构造函数
函数定义:
Player::Player(const Player& m) //被传递的 { players++; //计数器(如果有)应该相应增加 name = new char[strlen(m.name) + 1]; //new一个地址 strcpy_s(name, strlen(m.name)+1, m.name); //将参数(传递的对象)复制进去。 id = players; //给上id std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; //声明 }
部分源代码:
int main() { { Player m; Player a(m); //给参数的形式 Player b = m; //赋值形式 Player c = Player(m); //m作为参数创建一个对象并赋值,注意,必须使用对象,否则会调用构造函数,而不是复制构造函数。 Player*d = new Player(m); //new一个对象并赋值 } cout << "\nDone!\n"; system("pause"); return 0; }
显示:
姓名:未起名,ID:1的玩家已被创建。 姓名:未起名,ID:2的玩家已被创建。 姓名:未起名,ID:3的玩家已被创建。 姓名:未起名,ID:4的玩家已被创建。 姓名:未起名,ID:5的玩家已被创建。 姓名:未起名 的玩家已被删除。其ID号为:4剩余玩家数:4 姓名:未起名 的玩家已被删除。其ID号为:3剩余玩家数:3 姓名:未起名 的玩家已被删除。其ID号为:2剩余玩家数:2 姓名:未起名 的玩家已被删除。其ID号为:1剩余玩家数:1 Done! 请按任意键继续. . .
分析:
①首先看到,只调用了4个析构函数,却使用了1次默认构造函数(第一个),4个复制构造函数(第2~5个)。
之所以析构函数只调用4次,是因为最后的对象d是一个类对象指针,而不是类对象,但这种类对象指针会调用复制构造函数。
指针在离开时,不会调用析构函数(析构函数面对的是类对象)。
如果要删除指针指向的对象,应使用delete命令,如:delete d;即可调用指针d指向的对象的析构函数——需要记住,delete和new对应(我差点忘了)。
②在复制构造函数中,有几个特点:
(1)没有返回值(不需要return,函数头也没有类型);
(2)被传递的对象(例如代码里的对象m)是参数(onst Player& m),调用的时候需要用类成员运算符(例如m.name);
(3)传递给的对象(例如代码里的对象a、b、c等),被隐式传递给函数,调用的时候,直接使用私有成员名即可。就像是成员函数的运算符重载函数那样,运算符左边的(这里是被初始化的)对象被隐式传递给函数。
(4)需要给对象的哪些成员赋值,则在赋值构造函数里进行逐个赋值。
③另外注意,默认的复制构造函数不影响静态成员(如players,如果有这种计数器变量,在有必要的情况下,需要加上计数器变量的变化)。
现在回过头来看上面的代码,问题在于:
①按值传递时,调用默认的复制构造函数,但默认的复制构造函数是逐成员赋值,从而导致(1)未给计数器加上数字;(2)让指针指向同一个地址;
②在将一个对象传递给一个新对象时,调用默认复制构造函数,同样犯了①中的两个错误。
③于是,在调用析构函数时,则释放了已释放的内存(而且还要输出已释放内存的内容),故导致错误。
另一个问题:赋值运算符:
除了调用复制构造函数会导致逐成员复制之外,使用赋值运算符也会导致逐成员复制(记得,指针复制后指向的是同一个地址)。
ANSI C允许结构赋值,C++允许类对象赋值,实现的原理是自动为类重载赋值运算符(所以才能类对象a=类对象b)。
这种运算符重载的函数原型就像普通的赋值运算符重载那样:
Class_name& operator=(const Class_name &);
其中,Class_name是类名,例如上面的Player类。
解释是:接受一个被const限定的类对象的引用,然后返回一个类对象的引用。
注意:赋值运算符不影响静态成员。
赋值运算符存在的问题同隐式复制构造函数——遇见指针会导致指针指向同一个地址(遇见new出来的动态内存,可能导致二次或多次delete)。
解决办法:自定义一个赋值运算符(=)的重载函数,注意以下几点:
①由于是赋值运算符,因此对象之前已经存在了,因此无需更新计数(如果有计数器的话);
②因为对象已经存在了,假如有指针指向new出来的动态内存,那么赋值运算符需要delete进行释放,才能重新new(特别是new出来的字符串长度不一的情况下,长的字符串赋值到短的new出来的,可能导致超出限界);
③如果赋值运算符中有delete释放内存,那么需要保证每个构造函数(包括默认和复制构造函数),都需要使用new来申请动态内存(不然会导致释放静态内存区或者其他非动态内存区,比如栈)。
例如,将上面的赋值运算符的重载函数修改如下:
函数原型:
Player& operator=(const Player& m);
函数定义:
Player& Player::operator=(const Player& m) { if (this == &m) { return *this; } //首先,this是指针,m是对象,所以应用this==&m(判定地址而不是判定值)来防止将自己赋值给自己。 //其次,之所以return *this,是因为m被const所限定,所以假如地址相同的话(是自己赋值给自己),返回自己(相当于什么事都没做) delete[] name; //因为是指针指向字符串,new char[],所以也是delete[] name = new char[strlen(m.name) + 1]; //new一个 strcpy_s(name, strlen(m.name) + 1, m.name); //复制字符串的值到新的地址 return *this; //返回调用的对象 }
解释:
①m作为参数传递,因为是引用,为了防止被修改,所以使用const进行限定;
②因为m被const限定,因此不能返回m(会导致指针指向同一个地址),只能返回*this(也就是调用这个函数,被隐式传递的对象);
③如果要new,需要先delete(因为指针之前是指向一个new出来的地址的),不然会导致内存泄露。
④因为是赋值,不是创建新对象,因此不应更改计数器的数字(在这里,那是构造函数和析构函数干的事)。
⑤具体方法有一些类似构造函数。
附上修改后的全部代码:
//new.cpp #pragma once #include<iostream> class Player { char *name; //名字 int id; //id编号 static int players; //玩家数量,注意,它被static所限定,所以是静态变量(并且是全局的)。可以通过Player::players来访问,且这里不能初始化(因为类声明不分配内存,即使是静态内存) //静态类变量有一个特点,无论有多少个对象,都只有一个副本。也就是说,所有的对象,都共享这个静态变量 public: Player(); //默认构造函数 Player(const char*); //构造函数 ~Player(); //析构函数 Player(const Player& m); //复制构造函数 friend std::ostream& operator<<(std::ostream& os, Player&); //运算符重载<< Player& operator=(const Player& m); }; //new.cpp #include<iostream> #include"new.h" Player::Player() //默认构造函数 { name = new char[10]; strcpy_s(name,10, "未起名"); players++; id = players; //id为人数 std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; } Player::~Player() //析构函数 { std::cout << "姓名:" << name << " 的玩家已被删除。其ID号为:" << id << "剩余玩家数:" << --players << std::endl; delete []name; //delete对应new。name指向的是new出来的内存,注意,构造函数是new[],所以这里是delete[] } std::ostream& operator<<(std::ostream& os, Player& m) //运算符重载<< { os << "姓名:" << m.name << ",ID:" << m.id; return os; } Player::Player(const Player& m) //被传递的 { players++; //计数器(如果有)应该相应增加 name = new char[strlen(m.name) + 1]; //new一个地址 strcpy_s(name, strlen(m.name)+1, m.name); //将参数(传递的对象)复制进去,注意,之所以是strlen(m.name)+1,是因为strlen不计算空字符,而这里要将空字符也复制进去 id = players; //给上id std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; //声明 } Player& Player::operator=(const Player& m) { if (this == &m) { return *this; } //首先,this是指针,m是对象,所以应用this==&m(判定地址而不是判定值)来防止将自己赋值给自己。 //其次,之所以return *this,是因为m被const所限定,所以假如地址相同的话(是自己赋值给自己),返回自己(相当于什么事都没做) delete[] name; //因为是指针指向字符串,new char[],所以也是delete[] name = new char[strlen(m.name) + 1]; //new一个 strcpy_s(name, strlen(m.name) + 1, m.name); //复制字符串的值到新的地址 return *this; //返回调用的对象 } Player::Player(const char* a) //构造函数 { name = new char[strlen(a) + 1]; //strlen是不计算末尾空字符的。注意,这里有new,那么构造函数都要对应的delete strcpy_s(name, strlen(a) + 1, a); //将a复制到name之中(name指向的是new出来的字符串) //这里假如直接用name=a的话,那么其实是让name指向了a的地址,而不是将a的值赋给了name players++; //新建的话,人数加一 id = players; //id为人数。之所以在players++后赋值,是因为其+1指的是当前新建的对象 std::cout << "姓名:" << name << ",ID:" << id << "的玩家已被创建。" << std::endl; } void show1(Player& a) //按值引用传递 { std::cout << "按引用传递:" << a << std::endl; } void show2(Player a) //按值传递 { std::cout << "按 值 传递:" << a << std::endl; } //1.cpp main函数,用于测试 #include<iostream> using namespace std; #include "new.h" //to avoid confusion with complex.h int Player::players = 0; //声明全局静态变量,才能引用。 void show1(Player& a); void show2(Player a); int main() { { //1#大括号 Player a; { //2#大括号 Player b("charname"); show2(b); //按值传递 { //3#大括号 Player c("成龙"); show1(c); //按引用传递 Player d; //新建一个对象 d = b; //将b赋值给他 show2(d); } cout << "3#大括号在这行之前结束。" << endl; } cout << "2#大括号在这行之前结束。" << endl; } cout << "1#大括号在这行之前结束。" << endl; cout << "Done!\n"; system("pause"); return 0; }
显示:
姓名:未起名,ID:1的玩家已被创建。 姓名:charname,ID:2的玩家已被创建。 姓名:charname,ID:3的玩家已被创建。 按 值 传递:姓名:charname,ID:3 姓名:charname 的玩家已被删除。其ID号为:3剩余玩家数:2 姓名:成龙,ID:3的玩家已被创建。 按引用传递:姓名:成龙,ID:3 姓名:未起名,ID:4的玩家已被创建。 姓名:charname,ID:5的玩家已被创建。 按 值 传递:姓名:charname,ID:5 姓名:charname 的玩家已被删除。其ID号为:5剩余玩家数:4 姓名:charname 的玩家已被删除。其ID号为:4剩余玩家数:3 姓名:成龙 的玩家已被删除。其ID号为:3剩余玩家数:2 3#大括号在这行之前结束。 姓名:charname 的玩家已被删除。其ID号为:2剩余玩家数:1 2#大括号在这行之前结束。 姓名:未起名 的玩家已被删除。其ID号为:1剩余玩家数:0 1#大括号在这行之前结束。 Done! 请按任意键继续. . .
解释:
姓名:未起名,ID:1的玩家已被创建。 //这一行是Player a;,创建新对象,使用默认构造函数 姓名:charname,ID:2的玩家已被创建。 //这一行是Player b("charname");,使用构造函数 姓名:charname,ID:3的玩家已被创建。 //这行和以下两行是show2(b); //按值传递,按值传递,因此先调用复制构造函数 按 值 传递:姓名:charname,ID:3 //这里是show2(b)函数的std::cout << "按 值 传递:" << a << std::endl; 姓名:charname 的玩家已被删除。其ID号为:3剩余玩家数:2 //这里是因为show2(b)函数结束,临时对象被删除,调用了析构函数 姓名:成龙,ID:3的玩家已被创建。 //这里是Player c("成龙");,调用了构造函数 按引用传递:姓名:成龙,ID:3 //函数show1(c);,按引用传递,不创造新对象 姓名:未起名,ID:4的玩家已被创建。 //Player d;,默认构造函数 姓名:charname,ID:5的玩家已被创建。 //d = b赋值运算符重载被跳过(因为函数没有输出内容),然后这行和下面两行都是函数show2(d)调用 按 值 传递:姓名:charname,ID:5 //略,见上 姓名:charname 的玩家已被删除。其ID号为:5剩余玩家数:4 //函数结束,析构函数调用 姓名:charname 的玩家已被删除。其ID号为:4剩余玩家数:3 //注意,之所以是charname,是因为使用了赋值运算符将b赋值给他(由于使用的不是默认的,因此d和b的成员char*name,指向的是不同的地址。这行调用了对象d的析构函数 姓名:成龙 的玩家已被删除。其ID号为:3剩余玩家数:2 //调用了对象c的析构函数 3#大括号在这行之前结束。 //括号结束,表示位置 姓名:charname 的玩家已被删除。其ID号为:2剩余玩家数:1 //调用了对象b的析构函数,注意,没有和对象d的指针指向同一个地址(因为赋值运算符被自定义了) 2#大括号在这行之前结束。 //括号结束,表示位置 姓名:未起名 的玩家已被删除。其ID号为:1剩余玩家数:0 //对象a的析构函数被调用。此时,所有对象的析构函数均已被调用,因此剩余为0 1#大括号在这行之前结束。 //表示位置 Done! 请按任意键继续. . .
解释完。
C++11的空指针:
当一个指针,字面值为0时(char *a=0;),有两个含义:
①可以表示数字值是0(例如int *a=0);
②也可以表示空指针(这样人和编译器都难以区分)。
因此有些程序员使用(void*)0(或者是(int*)(char*)等,总之用强制类型转换后,0就是表示指针了)来标识空指针(空指针的内部表示可能不为0,只不过一般表示NULL==0?)。
还有程序员使用NULL(这是一个表示空指针的C语言宏)。
C++11引入了新的关键字nullptr,用于表示空指针。(有点像NULL,只不过NULL是C语言宏)。
但是我还是不太清楚空指针有什么意义,我查了查别人的说法,感觉争论还蛮激烈的。