一、复习

1.1 .data和.test

初始化的变量和未初始化的变量是否会占用空间大小。

int a;
int b;
int max[10] = {12,23,34,45,56,67,78};
int main()
{

}

a、b不占用exe文件的空间,max会占用空间。当程序加载到内存的时候,在.bss开辟一些空间,有几个就是几个0,表示未初始化的变量。

1.2 常引用和普通引用

int main()
{
	int a = 10;
	const int b = 20;

	int& x = a;			//普通变量普通引用
	const int& y = a;		//普通变量常引用
	const int& xb = b;		//常变量只能常引用
}

总结:

  • 普通变量既可以用常引用也可以用普通引用。
  • 常变量只能用常引用。

1.3 字面常量

int main()
{
	int a = 10;
	const int& b = a;
	const int& c = 10;		//right:字面常量,常引用来引用

	int& d = 10;		//error:字面常量只能用常引用修饰
	//error:
	return 0;
}

总结:

  • 常引用可以引用字面常量
  • 字面常量需要使用常引用来修饰

问题:思考下面两种方式的底层是否一样?
回答:不一样

本质如下

const int a = 10;
const int& b = a;
//相当于:
//const int *const b = &a;

const int& c = 10;
//相当于:
//int tmp = 10;
//const int &c = tmp;

//上一句也相当于:
//const int *const c = &tmp;

字面常量10的引用:首先是定义了一个临时变量,赋值为10。
接着使用常引用来引用tmp;
而 int &c = tmp; 的本质是 int *const c = &tmp;
加了const修饰也就是const int *const c = &tmp;

总结:
从上述可以看出来,常引用引用字面值 和 普通变量引用的时候是有区别的。
常引用引用字面值需要先构造一个临时量来存储字面值。

那么编译器中是如何实现的呢?

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

反汇编代码如下:
image

1、const int a = 10;

image

常变量的特点是:在编译时期如果遇到常变量a,会直接使用10来进行替换。对他进行引用实际上指向的是a变量的地址。const int a 仍然需要占用空间。

2、const int &b = a;

拷贝构造函数和运算符的重载-小白菜博客
将 a 存放在eax寄存器中,再将 eax 寄存器中的值传递给 b;

我们知道引用的底层是指针,使用常引用的时候也就相当于
const int *const b = &a;

3、const int& c = 10;

相当于:
//int tmp = 10;
//const int &c = tmp;

拷贝构造函数和运算符的重载-小白菜博客
这里 [ebp - 30h] 这个地址实际上就是 tmp 的地址

二、几种对象的创建

在数据区、堆区、栈区中创建的对象有什么区别呢?

class Object
{
	int value;
public:
	Object(int x = 0) : value(x) {}
	void Print()const { cout << value << endl; }
};
Object g_obj(10);		//data

void fun()
{
	Object obja(20);		//.stack
	obja.Print();
}

int main()
{
	Object objb(40);
	fun();
	Object* p = new Object(30);	//.heap

	p->Print();
	delete p;

	return 0;
}

.data
对于全局对象,在进入主函数之前,我们就先构造了全局对象。全局对象的创建和主函数的位置没有关系,不论定义在主函数之前,还是主函数之后。
当整个程序结束后,才能销毁。

.stack
栈区的对象,有如下特点。
当fun函数被调用时,我们对象就会调用构造函数。在fun的栈帧中创建一个对象 ,首先开辟栈帧,在这个栈帧中我们创建obja对象当函数结束的时候,obja对象的生存期也会结束,接着调用析构函数

.heap
使用new在栈区构建对象的过程:
1、开辟空间
2、调用对象
3、将创建好的对象的地址返回给p

三、对象数组

有如下类

class Object
{
	int value;
public:
	Object(int x = 0) : value(x) {cout << "Create Object:"<< this << endl;}
	void Print()const { cout << value << endl; }
	~Object(){cout << "Object :: ~Object :" << this << endl;}
};

3.1 静态创建

int main()
{
	Object objar[10];	//类名 数组名[数量]
	return 0;
}

创建方法与数组的声明一致。
当我们进入到我们这个函数的时候,构造函数要调动10次.因为要申请10个空间。当程序结束的时候,也同样要调用10次析构函数。

运行结果也是如此:
image

3.2 动态创建

3.2.1 动态构建单个空间

int main()
{
 	int n;
    cin >> n;
    Object *s = new Object(n);	//只有一个对象,调用构造函数
	delete s;

	return 0;
}

3.2.2动态构建连续空间

int main()
{
    int n;
    cin >> n;
    Object *p = new Object[n];	//连续创建使用[]括号,与数组一样

    return 0;
}

//创建20个对象
image

那么这样有什么问题呢?

3.2.2.1 连续创建不要忘记连续释放

使用new必须使用delete,我们之前也知道delete的用法。
当连续创建对象时,也必须连续使用delete去释放空间。

Object *p = new Object[n];
delete []p;

这种区别:圆括号和方括号

image

方括号至于说有多少个对象,系统在new的时候它自动可以识别

我们可以构建连续10个对象
我们也可以依次析构10次

3.2.2.2 连续创建必须要有缺省构造函数

如果我们在这地方动态开辟一组对象时,没有缺省构造函数还能构建对象吗?
不能

class Object
{
    int value;
public:

    Object(int x) : value(x)	//将(int x = 0)改为(int x)
    {
        cout << "create:" << this << endl;
    }
    void Print() const
    {
        cout << value << endl;
    }
    ~Object()
    {
        cout << "~Object:" << this << endl;
    }
};

Object obj3(4);

int main()
{
    int n;
    cin >> n;
    Object* p = new Object[n];		//error

    delete[] p;

    return 0;
}

原因如下:
我们要申请n个空间。每个空间都需要调用一次构造函数,但是这个地方需要值没给值(构造函数的参数为int x,并没有初始化),那我就没有办法来在这个地方调动。

能否类似于单一创建加()使用?这样是否意味着每个对象都用23来初始化?
不行,非法使用

Object* p = new Object[n](23);
delete[] p;

总结:
我们在创建连续对象时,一定要设计缺省的构造函数。
拷贝构造函数和运算符的重载-小白菜博客
如果没有则无法连续的创建对象。

四、拷贝构造

4.1 拷贝构造是什么样子

拷贝过程只需要拷贝成员数据,而函数成员是公用的。
没有构造函数,会有默认构造
没有析构,会有默认析构
没有拷贝构造函数,会给缺省的拷贝构造函数

class Object
{
	int value;
public:
	Object() {}	//默认构造函数
	Object(int x = 0) : value(x) {}
	~Object() {}	//默认析构函数

	Object(Object& obj):value(obj.value)	//拷贝构造函数
	{
		cout << "Copy Create " << endl;
	}
};

int main()
{
	Object objb(10);
	Object obja(objb);		//调用拷贝构造函数
}

拷贝构造函数的参数是引用类型,去掉引用会怎么样呢?
拷贝构造函数和运算符的重载-小白菜博客
没有引用会出现死递归,我们用objb构造obja需要调用拷贝构造,但是拷贝构造里面又有一个obj对象

加const的意思是什么?
拷贝构造函数和运算符的重载-小白菜博客
防止参数对象被修改

4.2 一个实例,观察创建了几个对象

思考:在这个程序的运行期间,一共创建了几个对象

class Object
{
	int value;
public:
	Object() {}
	Object(int x = 0) : value(x) {}
	~Object() {cout << "Object::~Object " << this << endl;}
	void SetValue(int x)	{value = x;}	//新加
	int GetValue() const {return value;}

	Object(const Object& obj):value(obj.value)
	{
		cout << "Copy Create " << this << endl;
	}
};

//新加
Object fun (Object obj) //3
{
	int val = obj.GetValue();
	Object obja(val);  //4
	return obja;	//5
}

int main()
{
	Object objx(0);//1
	Object objy(0);//2
	objy = fun(objx);
}

在VS2019和VC6.0中

一共创建了五个对象。
运行结果也是析构五个对象。
image

第五个的理解:
此处不能用obja直接给objy赋值,obja是函数中临时对象,此处地址在fun函数结束后会销毁,传递出去的是一个已经被释放掉的对象。将亡值,调用拷贝构造函数建立一个副本,将这个副本传递出去。

在VS2022和g++中一共只有四个

image

两者的区别是没有fun函数返回时创建的将亡值对象。
至于为什么这样,我也没有搞明白,暂定于C14和之前标准的差异吧,可能是C14以上版本对地址有所优化,并不会创建一个临时对象来拷贝一个obja,而是直接将obja进行传递。

4.3将亡值以及拷贝构造函数

将亡值,为右值引用打基础

补充带参构造
image

image

拷贝构造的调用和析构:
第一次拷贝构造:objy = fun(objx)处调用拷贝构造
image

第二次拷贝构造:建立将亡值对象
image

函数结束后,要将obja和//4处的构造析构掉。

在objy处付完值后,将将亡值对象析构掉
image

4.3 如何节省空间

在整个调用过程中,如何最节省空间呢?或者说创建对象的代价最小。

4.3.1 引用类型形参

首先:将形参改为引用,并且防止对形参修改加const。

Object fun(const Object &obj)
{
	int val = obj.GetValue();
	Object obja(val);
	return obja;
}

4.3.2 形参是否加const修饰的问题?

  • 目的就是为了通过形参改变实参,就用引用不用const
  • 仅仅读取值,不修改,加const

Value函数写两份:

  • 增强函数的通用性

类中修改

int &Value(){return value;}
const int &Value()const {
	return value;
}

4.3 fun函数能否以引用返回

4.3.1 不以引用返回:
Object fun(const Object &obj)
{
	int val = obj.Value() + 10;
	Object obja(val);
	return obja;
}

int main()
{
	Object objx(0);
	Object objy(0);
	objy = fun(objx);
	cout << objy.Value() << endl;
}

image

逐步过程分析

调用fun函数开辟栈帧(红色)
拷贝构造函数和运算符的重载-小白菜博客
obj只占用四个字节,底层是个指针

运行到obj.Value()+10的时候,随后构建obja的对象,值是10
image

在return obja对象的时候,实际上是以值返回。会在此处创建一个将亡值对象。
此将亡值对象创建在主函数的栈帧中(主函数是调用者,调用fun需要构建在调用者的栈帧空间中)。会有12的字节用不了,所以可能在箭头处。
image

随后,会将将亡值对象的地址放入eax寄存器中,将亡值对象是通过拷贝构造函数创建的。
拷贝构造函数和运算符的重载-小白菜博客
将亡值对象构建结束后,return就意味着fun函数的栈帧空间会被销毁。我们会将空间还给堆区,obja会调用析构函数,obj是指针,所以不需要析构函数

从fun函数回到主函数的时候,我们会将将亡值对象给objy赋值。在赋值结束后,将亡值对象也会被析构。随后objy和objx也会被析构掉,主程序结束。

完整过程分析

1、当函数执行后,会先为main函数开辟栈帧。预留出objx和objy所需要的空间。

​ 注意:当两个对象调用构造函数后,才会将对象存放在空间当中。

2、调用fun函数,fun函数的形参是obj,32位系统占据4个字节,因为引用的底层是指针实现的,仅仅存放objx的地址。const 说明对象是个常指针,不可改变。

3、接着创建一个val变量,存储的是10。

4、随后创建一个obja对象,会调用拷贝构造函数来创建,存放在main函数的栈帧中。由于main函数栈帧顶部需要存放寄存器,它会在下面存放。创建完后,会将将亡值对象的地址传入eax寄存器。

5、fun函数结束,会将fun函数的栈帧全部销毁,销毁前调用obja的析构函数。从fun函数回到主函数的时候,我们将我们的将亡值对象给objy赋值,随后,将亡值对象也要被析构。

6、最后objy被析构,随后objx也被析构。

4.3.2 以引用返回:
//修改后的fun函数
Object &fun(const Object &obj)
{
	int val = obj.Value() + 10;
	Object obja(val);
	return obja;
}

int main()
{
	Object objx(0);
	Object objy(0);
	objy = fun(objx);
	cout << objy.Value() << endl;
}
//能不能打印处10,可以

逐步过程分析

objx和objy会申请空间,但没有对象,只有运行到调用构造函数的时候才会构建对象。
image

拷贝构造函数和运算符的重载-小白菜博客
调用fun函数创建栈帧(上方蓝色),fun函数的的形参是引用类型,保存的是objx的地址。

拷贝构造函数和运算符的重载-小白菜博客
fun函数的返回值是引用类型,引用的底层就是一个指针,所有返回的时候不需要创建一个将亡值对象,而是将obja对象的地址给了eax寄存器保存。

return结束后,我们会将fun函数的空间销毁,obja对象也会被析构。
回到主函数后,通过eax寄存器解引用,去指向这个对象,将这个对象的值再传递给objy,但是很可惜,obja对象的空间已经被收回。
image

从一个已经死亡的对象中获取数据,本身的用法就是错误的。如果这个空间没有被侵扰,会输出10;如果被侵扰,会输出一个随机的数字。
image

完整分析过程

1、首先我们运行程序,会构造出main函数和fun函数的栈帧,程序预留出objx和objy对象的地址空间。当调用构造函数后,对象才会被实际的存放在内存空间中。

2、当我们调用fun函数后,由于fun函数的形参是引用类型,就不需要调用构造函数创建对象,而是直接在fun的栈帧中保存objx对象的地址。

3、随后会创建一个val变量,然后构造一个obja对象,二者都保存在fun函数的栈帧空间中。

4、当执行到return obja的时候,由于返回值是引用类型,它不会产生一个将亡值对象,而是将obja对象的地址空间存放在eax寄存器中。

5、回到main函数中,会将eax存储的寄存器中obja的地址赋值给objy,但是由于fun函数结束,栈帧空间被销毁,obja对象被析构。

能否以静态修饰这个函数呢?
不能,但可以以静态修饰对象构造

如果想要以引用返回,必须要求obja对象的生存周期不受fun函数的影响,我们将对象加静态。但是这个静态只能修饰函数名字,不修饰返回类型。

拷贝构造函数和运算符的重载-小白菜博客
添加到此处,编译器会忽略此处的静态
注意:静态只能修饰函数,不能修饰返回类型

静态修饰
这样是正确的
image

五、运算符的重载

设计一个复数类型,写一下Add函数,怎么样建立的对象最小并且安全性最高?

class Complex
{
private:
	double Real,Image;
public:
Complex():Real(0),Image(0){}
Complex(double r, double i):Real(r),Image(i) {}
~Complex() {}
};

int main()
{
	Complex c1(1.2, 2.3);
	Complex c2(4.5,5.6);
	Complex c3;
	c3 = c1.Add(c2);
	return 0;
}
...
public:
Complex Add(const Complex &c) const 
//前一个const 限制c的成员属性不被修改,后一个const限制this指针的成员属性不被修改
{
	return (Complex( c.Image + this->Image, c.Real + this->Real));
}
...

拷贝构造函数和运算符的重载-小白菜博客
类型名 + 括号,调用一个无名的构造函数。
拷贝构造函数和运算符的重载-小白菜博客
此处return以值进行返回无名对象,会将无名对象当作将亡值通过eax保存,并且将将亡值对象赋值给c3。此时只需要构建一次对象。

如果将上面一句return 替换成这样两句
拷贝构造函数和运算符的重载-小白菜博客
首先会先构造一个tmp对象,接着return的时候,又需要一个对象初始化将亡值对象,返回将亡值对象的时候,tmp有需要析构。

那么此处能否以引用方式返回
拷贝构造函数和运算符的重载-小白菜博客
给出它的析构函数与拷贝构造函数,并将之前函数都输出this指针

~Complex() {cout << "~Complex " << this <<endl;}
Complex(const Complex &x):Real(x.Real),Image(x.Image) 
{
	cout << "Copy Create: " << this << endl;
}

拷贝构造函数和运算符的重载-小白菜博客
此处A4这个地址的对象已经被析构了,为什么此处还可以输出正确的值呢?

将亡值对象是一个常性对象
临时空间具有常性,以引用返回可以进行修改。

六、加号运算符

下面我们将函数Add改为+,那么+可以当函数名吗?
image

拷贝构造函数和运算符的重载-小白菜博客
不行,+是操作符,不能当作有效函数名。

在c++中,可以在操作符前面加上operator,来说明他是一个有效的函数名
拷贝构造函数和运算符的重载-小白菜博客
我们将这个称为运算符的重载

c3 = c1 + c2;
//相当于
c3 = c1.operator+(c2);
c3 = operator+ (&c1, c2);

七、6个缺省函数

C++中 6个
拷贝构造函数和运算符的重载-小白菜博客
普通对象的取地址符
常对象的取地址符

C11标准中,8个
移动构造
image

移动赋值
image

class Object
{
	int value;
public:
	Object(){cout << "Object::Object " << this << endl;}
	Object(int x):value(x) { cout << "Object::Object " << this << endl;}
	~Object(){cout << "Object::~Object " << this << endl;}

	int &Value() { return value; }
	const int &Value()const {return value;}

	Object(const Object & obj):value(obj.value)
{
	cout << "Copy Create " << this << endl;
}
};

Object &fun(const Object &obj)
{
	int val = obj.Value() + 10;
	Object obja(val);
	return obja;
}

int main()
{
	Object objx(0);
	Object objy(0);
	objy = fun(objx);
	cout << objy.Value() << endl;
	return 0;
}

当我们用objb给obja赋值时
image

拷贝构造函数和运算符的重载-小白菜博客
重载一个等号运算符
void operator=(const Object &obj)
{
this->value = obj.value;
}
返回的是无类型,不能进行连续赋值

operator的本质:

obja = objb = objc;
obja = objb.operator=(objc);
obja = operator=(&objb,objc);

void函数有一个this指针,objb.value就是this->value,objc就是obj.value。但是是个void类型的函数,我们就不能把这个结果operator=(&objb,objc)赋值给Obja。

如果进行一些修改:

Object &operator=(const Object &obj) const
{
	this->value = obj.value;
	return *this;
}

括号外的const限定了this的成员属性的修改,this->value不能作为左值使用。

去掉&号

Object operator=(const Object & obj)

如果此对象的生存期不会被函数的影响就可以以引用返回。在整个赋值过程中,不会产生临时变量,效率高。

连续赋值的本质

obja = operator=(&objb,objc);
obja.operator=(operator=(&objb,objc));
operator=(&obja,operaotr=(&objb,objc));

那么自己给自己赋值呢?

obja = obja;

添加一条判断语句
拷贝构造函数和运算符的重载-小白菜博客
this != &obj;这句的&不是引用,是取地址符号
image

输出结果:

拷贝构造函数和运算符的重载-小白菜博客
此处仍然不是一个随机值,不是应该是fun函数结束后,对象会被析构,返回的是随机值吗,难道是此处的地址没有被扰动吗
拷贝构造函数和运算符的重载-小白菜博客
此时在fun函数中,obja 的地址是80,它的value是10.
随后调用析构函数销毁对象
拷贝构造函数和运算符的重载-小白菜博客
在析构函数中,this指针的地址也是b0。
析构函数结束,fun函数也就结束,此时b0存放的obja对象已经死亡。

在我们使用赋值的时候,使用的是已死亡的对象,取的地址是死亡对象的地址
拷贝构造函数和运算符的重载-小白菜博客
在使用的时候,我们发现已死亡的对象的值还是10,复制后没有发生变化,既然成功返回了,并且还能重新打印。
image

VS2012中运行上面的程序:
拷贝构造函数和运算符的重载-小白菜博客
才返回了我们认同的答案。

VS2019和VS2012的不同,最主要的一点是VS2019完全和WIN10结合,每次运行的时候,局部变量的地址都会发生变化。

VS2019中打印
拷贝构造函数和运算符的重载-小白菜博客
第一次运行:
拷贝构造函数和运算符的重载-小白菜博客
第二次运行:
拷贝构造函数和运算符的重载-小白菜博客
两次运行都不一样

VS2012中打印:
第一次:
拷贝构造函数和运算符的重载-小白菜博客
第二次:
image

VC6.0中打印:
第一次:
拷贝构造函数和运算符的重载-小白菜博客
第二次:
拷贝构造函数和运算符的重载-小白菜博客
在VC中,当主函数调用fun函数,开辟的栈帧就在主函数的上一层
拷贝构造函数和运算符的重载-小白菜博客
当我们以引用返回时候,就会被销毁,当我们再次调用赋值语句的时候
拷贝构造函数和运算符的重载-小白菜博客
赋值语句的栈帧就会覆盖fun函数的栈帧,所以数据值会发生改变

VC中运行时候我们会发现它将会变成随机值
拷贝构造函数和运算符的重载-小白菜博客
变成随机值
image

VC中每次都不变,VS中每次都变。
这样可以防止病毒的入侵,每次程序的地址都在变化。

拷贝构造函数和运算符的重载-小白菜博客
由于每次运行的地址空间不一样,所以obj的存储空间不会被侵扰。所以每次都可以打印,但是实际上我们的对象已经死亡。

在析构函数中将value赋值为0。
拷贝构造函数和运算符的重载-小白菜博客
结果也会变成0
image

所以:当我们使用的时候,一定要清楚什么时候以引用返回。

总结:

  1. 首先对引用进行了复习,常引用和普通引用,引入了字面常量的概念
  2. 对于堆区、栈区、数据区这三区对象构建的不同进行了说明
  3. 对象数组的静态构建和动态构建,区别了单一构建。new完记得也要delete,必须有默认构造函数,或者形参必须给初始值才能进行连续构建。
  4. 了解了拷贝构造函数,拷贝构造函数是否以引用返回的问题。
  5. 运算符重载的本质
  6. C++中给定的6个缺省函数

遗留:每个对象在内存中的分布图