(一二七)动态内存和类——第十二章

面对基本类型的时候,我们可以使用动态内存(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语言宏)。

 

但是我还是不太清楚空指针有什么意义,我查了查别人的说法,感觉争论还蛮激烈的。

 

 

时间: 2024-09-10 17:54:57

(一二七)动态内存和类——第十二章的相关文章

c++ 动态内存管理类 静态成员

问题描述 c++ 动态内存管理类 静态成员 C++primer 5th private 中 alloc 是static吗?是的话默认构造函数应该初始化没了,应该还要定义. 解决方案 动态内存管理类 解决方案二: 有static才是静态的.但它allocator的实现可能达到了静态的效果.

第十二章 委托[《.net框架程序设计》读书笔记]

.net框架|笔记|程序|设计 第十二章 委托 一. 委托的使用 静态委托和实例委托,使用方法类似,这里给出一个使用可变参数委托的例子: using System; public class DelCls { public delegate void DelDef(params string[] strParams); public static void CallDel(DelDef dd) { if(dd != null) //请务必在此处进行判断,这是个好习惯 { dd("Hello&qu

Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 这一章很多,但是很有趣,也是这书的最后一章知识点了,我现在还在考虑要不要写这个拼图和2048的案例,在此之前,我们先来玩玩Android5.X的新特性吧!

python 教程 第二十二章、 其它应用

第二十二章. 其它应用 1)    Web服务 ##代码 s 000063.SZ ##开盘 o 26.60 ##最高 h 27.05 ##最低 g 26.52 ##最新 l1 26.66 ##涨跌 c1 -0.04 ##涨幅 p2 -0.15% ##总手 v 9190865 ##日期 d1 6/15/2011 ##时间 t1 3:00am #!/usr/bin/env python from time import ctime from urllib import urlopen import

【PMP】Head First PMP 学习笔记 第十二章 采购管理

第十二章 采购管理 某些工作就是太大,以至于你的公司无法自行完成. 找到合适的卖方,选择正确类型的关系并确保合同的目标被打成. 有时候,你需要雇用外部公司做一部分的项目工作.这叫做采购(procurement),该外部公司被称为卖方. 项目采购管理包括合同管理和变更控制过程 规划采购管理 实施采购 控制采购 结束采购 项目采购管理围绕包括合同在内的协议来进行.协议是买卖双方之间的法律文件. 合同也可称作协议.谅解.分包合同或订购单. 在合同生命周期中,卖方首先是投标人.然后是中标人,之后是签约供

python 教程 第十二章、 标准库

第十二章. 标准库 See Python Manuals ? The Python Standard Library ? 1)    sys模块 import sys if len(sys.argv) < 2: print 'No action specified.' sys.exit() if sys.argv[1].startswith('--'): option = sys.argv[1][2:] if option == 'version': print 'Version 1.2' el

第十二章——SQLServer统计信息(3)——发现过期统计信息并处理

原文:第十二章--SQLServer统计信息(3)--发现过期统计信息并处理 前言:         统计信息是关于谓词中的数据分布的主要信息源,如果不知道具体的数据分布,优化器不能获得预估的数据集,从而不能统计需要返回的数据.         在创建列的统计信息后,在DML操作如insert.update.delete后,统计信息就会过时.因为这些操作更改了数据,影响了数据分布.此时需要更新统计信息.         在高活动的表中,统计信息可能几个小时就会过时.对于静态表,可能几个星期才会过

第十二章——SQLServer统计信息(2)——非索引键上统计信息的影响

原文:第十二章--SQLServer统计信息(2)--非索引键上统计信息的影响 前言:         索引对性能方面总是扮演着一个重要的角色,实际上,查询优化器首先检查谓词上的统计信息,然后才决定用什么索引.一般情况下,默认会在创建索引时,索引列上均创建统计信息.但是不代表在非索引键上的统计信息对性能没有用.         如果表上的所有列都有索引,那么将会是数据库负担不起,同时也不是一个好想法,包括谓词中用到的所有列加索引同样也不是好方法.因为索引会带来负载.因为需要空间存放索引,且每个D

第十二章——SQLServer统计信息(4)——在过滤索引上的统计信息

原文:第十二章--SQLServer统计信息(4)--在过滤索引上的统计信息 前言:         从2008开始,引入了一个增强非聚集索引的新功能--过滤索引(filter index),可以使用带有where条件的语句来创建非聚集索引,过滤掉不需要的数据,降低索引的维护开销和存储空间,提高查询性能.   准备工作: 在AdventureWorks2012上,有一个Production.WorkOrder表,将使用这个表来做演示.   步骤: 1.  创建一个非聚集索引在Production