拷贝构造函数
同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或者拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员(只有一分拷贝),在建立对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为拷贝构造函数。
class Object:
{
int value;
public:
Object(){} //缺省构造函数
Object(int x = 0):value(x){} //普通构造函数
~Object(){} //缺省析构函数
//拷贝构造函数
Object(const Object & obj):value(obj.value)
{
cout<<"Create Cpoy"<<endl;
}
};
int main()
{
Object obj(10);
Object obj(obja);
}
如果将拷贝构造函数中的引用符号去掉,会出现什么问题?
——死递归
,因为调动拷贝构造函数时,拷贝构造中还有一个obj
,会反复调动行成死递归。
辨析下列程序一共生成了几个对象?
Object fun(Object obj)//3 objx拷贝构造obj
{
int val = obj.Getvalue;
Object obja(val);//4
return obja;//
}
int main()
{
Object objx(0);//1
Object objy(0);//2
objy = fun(objx);
return 0;
}
fun函数快结束时,返回obja
时,并不是直接将obja
传给objy
,而是利用一个将亡值(临时量),此时需要调动拷贝构造函数在该临时空间构建第五个对象。
不难看出,上述程序在调用过程中生成了五个对象,对于空间和资源的利用较大,不太理想,那么我们如何修改程序,使得产生最少的对象却又能够到达同样的效果呢?
- 形参添加引用,减少一次构造函数的调动,为了防止对obj的修改,添加const(此时不需要形参去改变实参)
Object fun(const Object &obj)//3 objx拷贝构造obj
{
int val = obj.Value() + 10;
Object obja(val);//4
return obja;//
}
int main()
{
Object objx(0);//1
Object objy(0);//2
objy = fun(objx);
return 0;
}
图示为各对象的存储空间和关系分布:
问题:为什么obja
将亡值对象构建在了主函数的栈帧空间中,而不是fun函数的栈帧空间中?——(构建在调用者空间中)
因为主函数是调用者,其调用了fun,若构建在fun的栈帧空间中,fun函数结束,就无法得到该对象了
若将上述函数修改为以引用的形式返回?会有哪些变化?想一想之前学过的以引用返回的坏处。
Object & fun(const Object &obj)
此时,由于返回的是一个引用,所以不需要中间变量,故直接将obja
的地址出传递给Eax寄存器,但是传递之后,fun函数结束,那么其中空间的对象会自动调动析构函数销毁自身,并将所在的栈空间回收系统,此时,主函数通过Eax内的值解引用,并不一定会正确访问到所期望的结果,这取决于这块空间是否在这段时间内受到其他资源的利用和侵扰。
所以,尽量不要使用引用返回,一定要万分谨慎。如果一定要以引用返回,还是那句话,此时该对象的生存期不应受到函数生存期的影响。
总结:以引用返回和不以引用返回的区别:
- 以引用返回,将亡值对象构建在被调用函数的栈帧空间中,在该对象调用析构函数后,已死亡的对象被访问,主函数的使用会受到影响
- 不以引用返回,将亡值对象构建在主函数的栈帧空间中,使用过后再析构,较为安全。
- (这里还衍生出一个问题在VS2019中,变量的地址在不同次的运行时是不一样的,这也就更加保证了程序的安全,这与win10兼容,正因为这一机制有时候并不会干预以引用返回的地址空间,所以在这类编译器中可以正常运行,但实际上,以引用返回的对象已经死亡,即便可行,我们仍然不推荐这种写法,在VC6.0中每次的地址一样,是因为会覆盖之前使用的空间。)
运算符重载
运算符的重载实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,通常为类的成员函数。
定义运算符重载函数的一般格式:
返回值类型 类名::operator重载的运算符(参数表)
{……}
operator是关键字,它与重载的运算符一起构成函数名。
今天对于该问题的学习,我们以下面的类为例:
class Int
{
private:
int value;
public:
Int(int x = 0):valie(x){}
Int(const Int &it):value(it.value){}
~Int(){}
}
我们先来写一个加法函数:Add
int main()
{
Int a(10),b(10);
Int c;
c = a.Add(b); //c.value = a.value + b.value;
// c = Add(&a,b);
}
第一种:这种写法创建了3个对象,x(拷贝构造),tmp,临时对象(返回给c)
Int Add(Int x)
//Int Add(Int * const this ,Int x)
{
int val = this->value + x.value;
Int tmp(val);
return tmp;
}
第二种:添加引用,只创建了一个临时对象
Int Add(Int &x)
//Int Add(Int * const this ,Int &x)
{
int val = this->value + x.value;
return Int(val);
}
第三种:为避免修改x或this对象的属性值,为了避免这种错误,我们常常将此类方法写为常方法
Int Add(Int &x)
//Int Add(Int * const this ,Int &x)
{
x.value += this->value;//改了x
//this->value += x.value;//改了this(b)
return Int(x.value);
}
Int Add( const Int &x) const
//Int Add( const Int * const this ,Int &x)
改法如下:
Int Add( const Int &x) const
{
int val = this->value + x.value;
return Int(val);
}
函数名Add被改为+号是不被允许的,因此我们需要如下的运算符重载函数:
Int operator+(const Int &x) const
//Int operator+(const Int * const this, const Int &x) const
{
int val = this->value + x.value;
return Int(val);
}
编译过程:
c = a+ b;
c = a.operator+(b);
c = operator+(&a,b);
减法,乘法很好改写,只有除法需要处理一下:
Int operator/( const Int &x) const
{
if(x.value == 0)exit(EXIT_FAILURE);
int val = this->value / x.value;
return Int(val);
}
C++中禁止重载4个的运算符:
下面进一步完善Add函数,将“对象+对象”扩展到“对象+ 变量”和“变量 + 对象”
1.对象+变量
Int operator+(const int x) const
{
int val = this->value + x;
return Int(val);
//return *this + Int(x);
//Int(x)将构建一个对象
//这一句可以将其转化为对象+对象,只是Int(x)没有姓名,调试到此处会转调对象+对象
}
思考:对于内置类型变量x是否需要添加引用?
答:
不需要,因为加了引用后,从引用的本质上来看,反而增加了对内存的访问次数。
2.变量+对象
我们常会写成这样:
Int operator+(const int x , const Int &it)
但由于参数过多,编译不会通过,运算符重载函数不允许参数过多,这里有三个参数。
因此不能将这个函数设置成类的成员函数,而需要将其设计成全局函数。
当改为全局函数时,就没了this指针。
Int operator+(const int x,const Int &it)
{
return it + x;
//变量+对象——》对象+变量-》对象+对象
}
下面我们学完了一些基本的函数,其实任何一个类型,在C++中系统默认添加6个缺省函数
class Empty
{
public:
//构造函数
Empty(){}
//析构函数
~Empty(){}
//拷贝构造函数
Empty(const Empty & e){}
//赋值
Empty & operator=(const Empty &x)
{
return *this;
}
//取地址符(普通对象)
Empty * operator&() {return this;}
//重载取地址符(常对象)
const Empty * operator&() const {return this;}
}
在C11标准中,变成了8个缺省,添加了如下两个(后面会谈到):
//移动构造函数
Empty(Empty &&x){}
//移动赋值
Empty & operator=(Empty &&x)
赋值函数:
void operator=(const Int &it)
{
this->value = it.value;
}
我们可以从函数的本质上看出这个函数不能完成连续赋值,obja = objb = objc
- obja = objb.
operator=(objc)
-
obja =
operator=(&objb,objc)
因为从右往左赋值时,不能将无类型赋值给一个对象。
改写为:
Object & operator=(const Object &it)
{
if(this != &it)//c = c这种情况需要特殊处理这里的&不是引用,是取地址符
{
this->value = it.value;
}
return *this;
//c = a = b;
//c.operator=(a.operator=(b));
//operator=(&c,operator=(&a,b));
}
-
*this
代表主函数中的一个对象,不受该函数的影响,因此可以以引用返回,由此也不会产生其他对象用来过渡