拷贝构造函数、深拷贝与浅拷贝、何时返回何种对象
1.字符串并不保存在对象中,而是单独保存在堆内存中,对象仅保存了指向字符串地址的指针。因此在构造函数中如果用new初始化字符串内存后,需要将构造函数参数传入的字符串用strcpy函数复制到new出的内存中去,而不能直接指针赋值,这样只是保存了它的地址。
2.删除对象删除的是对象本身占用的内存,析构函数是释放属于对象成员的指针指向的内存。因此在析构函数中delete可以在对象过期时删除由new分配的内存。
3.C++中是提供了五种隐式成员函数,分别是:
默认构造函数(如果没有定义构造函数)、拷贝构造函数(也叫复制构造函数,如果没有定义)、赋值操作符(如果没有定义)、默认析构函数(如果没有定义)、地址操作符(如果没有定义)
其中,后四种是编译器自动生成其函数定义。
4.在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
1) 一个对象作为函数参数,以值传递的方式传入函数体;
2) 一个对象作为函数返回值,以值传递的方式从函数返回;
3) 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。
5.拷贝构造函数可以用于将一个对象赋值到新创建的对象中,原型如下:
Class_name(const Class_name &);
拷贝构造函数的调用可以有如下几种:
StringBad ditto (motto); //
StringBad metoo=motto; //拷贝构造直接创建metoo,或声明临时对象进行赋值给metoo
StringBad also = StringBad(motto); //拷贝构造直接创建also,或声明临时对象进行赋值给also
StringBad * pStringBad = new StringBad(motto); //使用motto声明匿名对象,并将其地址给pStringBad指针
按值传递和返回对象以及生成临时对象时都将调用拷贝构造,例如3个Vector对象相加时,编译器可能自动生成一个临时的Vector用来保存中间结果。因此为了节省调用构造函数的时间及存储新对象的空间,同时避免某些错误,应尽量避免使用按值传递对象,而使用按引用传递对象。
6.拷贝构造函数的调用可以逐个复制非静态成员(浅拷贝),但是静态成员不拷贝。
7.如果类中包含在新对象创建时会被更改的静态成员数据,应提供一个显示复制构造函数进行处理计数。
8.使用浅拷贝时,如果类中对象是new初始化的、指向数据的指针(如一个指向某个字符串的指针),则浅拷贝将复制一个指向这个字符串的指针(此时,原来的类和拷贝后的类各有一个指针,但都指向同一块字符串地址),而非上述情况的普通成员值将会被直接复制。浅拷贝会带来一个问题,当两个类中有一个类调用析构时会释放它的普通值成员和指针指向的字符串成员,但未被删除的类由于字符串的指针也指向这块内存,因此将会发生指针读取一块已被释放的内存数据的现象,此时报错。
解决浅拷贝的办法是使用深拷贝,即拷贝类的值的同时拷贝字符串的地址。
举个例子:
如果类中仅有构造函数如下:
StringBad::StringBad(const char *s)
{
n=10; //假定n是一个int型成员变量
len=strlen(s);
str=new char[len+1]; //new了一个字符串对象
strcpy(str,s);
num_strings++; //一个静态变量
}
上种情况时,当存在类之间的赋值时,由于缺少参数为类的情况,将不会调用该构造,而是调用默认构造函数,此时默认构造函数拷贝n和拷贝字符串s的指针,该指针仍旧指向s,如果被赋值的类为S2,赋值的类为S1 ,每次调用析构函数中会对静态变量num_strings--,则最终由于浅拷贝的调用,在S1或S2析构执行后会造成下面的后果:num_string的值将不为0,且为负数,同时s的输出错误。因为浅拷贝时调用默认构造函数而不是我们定义的构造,会使得num_string少++ 一次,因此值可能为负数。而其中一个类析构调用后,删除了字符串s的内容,然而另一个类的指针仍旧指向这个字符串,因此对另一个类字符串的输出将会报错。
为了避免上述情况的发生应该调用深拷贝,用户自己进行定义拷贝构造函数。可以定义的方法如下:
StringBad::StringBad(const StringBad &st)
{
num_string++; //对调用拷贝构造时修改num_string次数进行补充
len=st.len;
str=new char[len +1]; //生成新的指针并为其分配地址,将字符串内容拷贝进去
strcpy(str , st.str);
}
由于深拷贝参数为类StringBad,在编译过程中,需要调用拷贝构造函数时,匹配到了参数对应的用户定义的拷贝构造,此时将不会再调用默认拷贝构造进行浅拷贝,而是调用深拷贝。在深拷贝中,声明的新的指针同时,为这个指针开辟了新的内存去存储字符串数据,因此相当于为每个类都创造了自己的字符串,而不是多个拷贝出的类公用一个字符串,所以如果吗,某个类删除自己的字符串将不会影响到其他的类。
同时,类的赋值语句也容易产生上述问题。比如对于两个StringBad的对象knot 和 headline
knot=headline;
这种语句调用的赋值语句也容易产生数据受损,因此可以重载赋值语句:
StringBad &StringBad::operator=(const StringBad & st) //赋值函数不会影响num_string,因此无需修改num_string
{
if(this == &st)
return this; //如果是a=a这种赋值语句,则返回自身
delete[]str; //接下来要把新的串地址赋给str,因此要释放原来指针指向的内存
len=str.len;
str = new char [len+1];
strcpy(str,st.str);
return *this;
}
9.如果函数析构使用delete [ ],则对于声明为new char的指针无法释放,而对于new char[ ] 则可以,因为类型兼容。因此为了防止析构delete [ ] 释放不出错,可以在声明时给 str = new char [1]; 这与 new char分配的内存大小相同,且兼容delete[ ]的析构。
10.当重新对[ ] 符号进行重载时,要注意常量和非常量的分别重载。如:
char & String::operator[](int i) //可写
{
return str[i];
}
const char & String::operator[](int i)const //只读
{
return str[i];
}
像上述方法这样做是必要的,如果只有对普通变量的重载,对常量 answer[1] 这类输出将会报错,因此重载函数要区分开。
11.返回对象的方式可以为:返回指向对象的引用、返回对象、返回const对象
(1)返回指向对象的引用有分为两种:指向const对和指向非const对象:
指向const引用——当函数调用对象的方法或将对象作为参数返回:
如Vector A,B; //Vector类的两个对象A和B
max = Max(A,B); //假定Max是Vector类的成员函数,max为Vector类的对象
此时Max的定义如下:
conts Vector & Max(Const Vector &v1,const Vector &v2) //const对象作为参数传递,此时返回const引用
{
if(...)
return v1;
else
return v2;
}
指向非const对象,如重载赋值操作符或重载cout等,需要改变类的内容,因此是非const,同时为了避免浅拷贝所以传引用。
(2)返回对象:被返回的对象是函数中的局部变量
由于局部变量在函数中被申请,在函数执行完毕时调用它的析构释放,因此如果返回它的引用指向的对象将不存在,所以应该返回对象而不是引用。常用于重载算数操作符。如:
Vector A,B;
net = A+B; //net为Vector类的对象
则 Vector Vector::operator+ (const Vector &b)const
{
return Vector(x+b.x, y+b.y); //调用Vector的构造生成一个新的临时对象并返回,随着函数调用完毕后调用它的析构
}
(3)返回const对象:在返回一个局部对象的前提下,如果要返回的是一个没有公用拷贝构造函数的类(这种用的很少)