拷贝构造函数和运算符的重载
一、复习
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;
}
反汇编代码如下:
1、const int a = 10;
常变量的特点是:在编译时期如果遇到常变量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次析构函数。
运行结果也是如此:
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个对象
那么这样有什么问题呢?
3.2.2.1 连续创建不要忘记连续释放
使用new必须使用delete,我们之前也知道delete的用法。
当连续创建对象时,也必须连续使用delete去释放空间。
Object *p = new Object[n];
delete []p;
这种区别:圆括号和方括号
方括号至于说有多少个对象,系统在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中
一共创建了五个对象。
运行结果也是析构五个对象。
第五个的理解:
此处不能用obja直接给objy赋值,obja是函数中临时对象,此处地址在fun函数结束后会销毁,传递出去的是一个已经被释放掉的对象。将亡值,调用拷贝构造函数建立一个副本,将这个副本传递出去。
在VS2022和g++中一共只有四个
两者的区别是没有fun函数返回时创建的将亡值对象。
至于为什么这样,我也没有搞明白,暂定于C14和之前标准的差异吧,可能是C14以上版本对地址有所优化,并不会创建一个临时对象来拷贝一个obja,而是直接将obja进行传递。
4.3将亡值以及拷贝构造函数
将亡值,为右值引用打基础
补充带参构造
拷贝构造的调用和析构:
第一次拷贝构造:objy = fun(objx)处调用拷贝构造
第二次拷贝构造:建立将亡值对象
函数结束后,要将obja和//4处的构造析构掉。
在objy处付完值后,将将亡值对象析构掉
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;
}
逐步过程分析
调用fun函数开辟栈帧(红色)
obj只占用四个字节,底层是个指针
运行到obj.Value()+10的时候,随后构建obja的对象,值是10
在return obja对象的时候,实际上是以值返回。会在此处创建一个将亡值对象。
此将亡值对象创建在主函数的栈帧中(主函数是调用者,调用fun需要构建在调用者的栈帧空间中)。会有12的字节用不了,所以可能在箭头处。
随后,会将将亡值对象的地址放入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会申请空间,但没有对象,只有运行到调用构造函数的时候才会构建对象。
调用fun函数创建栈帧(上方蓝色),fun函数的的形参是引用类型,保存的是objx的地址。
fun函数的返回值是引用类型,引用的底层就是一个指针,所有返回的时候不需要创建一个将亡值对象,而是将obja对象的地址给了eax寄存器保存。
return结束后,我们会将fun函数的空间销毁,obja对象也会被析构。
回到主函数后,通过eax寄存器解引用,去指向这个对象,将这个对象的值再传递给objy,但是很可惜,obja对象的空间已经被收回。
从一个已经死亡的对象中获取数据,本身的用法就是错误的。如果这个空间没有被侵扰,会输出10;如果被侵扰,会输出一个随机的数字。
完整分析过程
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函数的影响,我们将对象加静态。但是这个静态只能修饰函数名字,不修饰返回类型。
添加到此处,编译器会忽略此处的静态
注意
:静态只能修饰函数,不能修饰返回类型
静态修饰
这样是正确的
五、运算符的重载
设计一个复数类型,写一下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改为+,那么+可以当函数名吗?
不行,+是操作符,不能当作有效函数名。
在c++中,可以在操作符前面加上operator,来说明他是一个有效的函数名
我们将这个称为运算符的重载
c3 = c1 + c2;
//相当于
c3 = c1.operator+(c2);
c3 = operator+ (&c1, c2);
七、6个缺省函数
C++中 6个
普通对象的取地址符
常对象的取地址符
C11标准中,8个
移动构造
移动赋值
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赋值时
重载一个等号运算符
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;这句的&不是引用,是取地址符号
输出结果:
此处仍然不是一个随机值,不是应该是fun函数结束后,对象会被析构,返回的是随机值吗,难道是此处的地址没有被扰动吗
此时在fun函数中,obja 的地址是80,它的value是10.
随后调用析构函数销毁对象
在析构函数中,this指针的地址也是b0。
析构函数结束,fun函数也就结束,此时b0存放的obja对象已经死亡。
在我们使用赋值的时候,使用的是已死亡的对象,取的地址是死亡对象的地址
在使用的时候,我们发现已死亡的对象的值还是10,复制后没有发生变化,既然成功返回了,并且还能重新打印。
VS2012中运行上面的程序:
才返回了我们认同的答案。
VS2019和VS2012的不同,最主要的一点是VS2019完全和WIN10结合,每次运行的时候,局部变量的地址都会发生变化。
VS2019中打印
第一次运行:
第二次运行:
两次运行都不一样
VS2012中打印:
第一次:
第二次:
VC6.0中打印:
第一次:
第二次:
在VC中,当主函数调用fun函数,开辟的栈帧就在主函数的上一层
当我们以引用返回时候,就会被销毁,当我们再次调用赋值语句的时候
赋值语句的栈帧就会覆盖fun函数的栈帧,所以数据值会发生改变
VC中运行时候我们会发现它将会变成随机值
变成随机值
VC中每次都不变,VS中每次都变。
这样可以防止病毒的入侵,每次程序的地址都在变化。
由于每次运行的地址空间不一样,所以obj的存储空间不会被侵扰。所以每次都可以打印,但是实际上我们的对象已经死亡。
在析构函数中将value赋值为0。
结果也会变成0
所以:当我们使用的时候,一定要清楚什么时候以引用返回。
总结:
- 首先对引用进行了复习,常引用和普通引用,引入了字面常量的概念
- 对于堆区、栈区、数据区这三区对象构建的不同进行了说明
- 对象数组的静态构建和动态构建,区别了单一构建。new完记得也要delete,必须有默认构造函数,或者形参必须给初始值才能进行连续构建。
- 了解了拷贝构造函数,拷贝构造函数是否以引用返回的问题。
- 运算符重载的本质
- C++中给定的6个缺省函数
遗留:每个对象在内存中的分布图