C++之对象的初始化、复制和销毁
C++定义了几种不同的初始化形式,对于类类型的对象来说,不同的初始化形式意味着要调用不同的构造函数。
初始化方式:默认初始化,直接初始化,拷贝初始化,列表初始化
默认初始化:如果定义对象时没有指定初值,对象被默认初始化,调用类中的默认构造函数,例:A a; //默认初始化,调用A() A arr[2]; //调用两次A() 初始化数组每个元素
A arr[2]; 初始化数组每个元素代码如下:
class A
{
int i;
public:
A(){cout<<"1"<<endl; i=20;};
void test() {cout<<"test"<<endl;}
int getI() {return i;}
};
int main()
{
A a[1]; //凡是数组越界的均无法成功构造,具体可参考getI()函数
a[1].test();
cout<<a[0].getI()<<endl;
cout<<a[1].getI();
return 0;
}
运行结果:
1
test
20
1966899469
直接初始化:初始值在圆括号“()”中,可以提供多个初始值,根据初始值类型和个数直接调用最匹配的构造函数。
拷贝初始化:用等号“=”初始化一个对象时,执行拷贝初始化,编译器用等号右边的初始值创建一个对象,复制给新创建的对象,等号右边的初始值只能有一个,调用与初始值类型匹配的构造函数。
列表初始化:用花括号“{}”中的初始值构造对象,调用相应的构造函数,与直接初始化类似,花括号可以是初始值列表,用来初始化数组的每个元素,此时对每个值调用构造函数,创建数组元素:如果初始值的个数少于数组大小,对后面的元素调用默认构造函数初始化。
构造函数:
默认构造函数:如果一个类没有定义任何构造函数,编译器会在需要时自动合成一个默认构造函数,类中一旦定义了构造函数,即使不是默认构造函数,编译器也不再合成。
拷贝构造函数:
有时候会用自己已有的对象去初始化另一个同类型的对象:
例:
X one;
X two(one); //用one初始化同类型的对象two
用one 初始化two 时需要构造函数X(X&),称为拷贝构造函数
如果在类中没有定义这样的构造函数,编译器会自动合成一个,默认的行为是逐个成员复制。X two(one)就是用one 中的每个成员分别去初始化two 的每个对应的成员,这种行为也称为浅复制(shallow copy)
浅复制在使用上有它的局限性:在简单类中,只包含内置int与数组arr和string成员等,这种按成员复制的默认行为没有出现错误,但是涉及到指针或者引用时,浅复制的该默认行为便不再恰当。为了避免编译器合成的不恰当的浅复制行为可以自己定义拷贝构造函数,对包括指针和引用在内的成员进行恰当的初始化。我们把这种与浅复制相对立的行为叫做深复制。
下面举例进行说明:
例:指针和引用成员的浅复制:
深复制与拷贝构造函数:
class X {
int m;
int& r;
int* p;
public:
X(const X& a): m(a.m), r(m), p(&m){}
...
};
拷贝构造函数的一般形式是X(X&)或者X(connst X&):其中X(X&)不能用const对象来初始化另一个X类型的对象。
例:
//如果拷贝构造函数为X(X&)
const X a;
X b(a); //错误: 没有匹配的函数'X::X(const X&)'
//如果拷贝构造函数为X(const X&)
const X a;
X b(a); //正确
X c(b); //正确
另外拷贝构造函数的形式不会是X(X)或者X(X*),原因:
如果用X 类的对象a 初始化X 类的对象b,会引起拷贝构造函数的调用,但在调用X(X obj)时,按值传参数是用实参a 初始化形参obj,这同样是用一个对象初始化另一个同类对象,又需要调用拷贝构造函数,重复这一过程,显然会陷入对X(X)的无限循环调用,因此,拷贝构造函数的形参不会是X(X)
用对象初始化另一个对象,不是用地址,因此不会是X(X*)
拷贝构造函数并非要求只有一个形参:如果一个构造函数是自身类型的引用,且任何额外的参数都有默认实参,则此构造函数是拷贝构造函数。
例:X(const X&, int x = 10); //第二个参数有默认值
只需要提供一个本类型对象作实参就可以调用的构造函数是拷贝构造函数。
那我们应何时调用拷贝构造函数:
一、用一个对象显式或隐式初始化另一个同类对象
二、函数调用时,按传值方式传递对象参数
三、函数返回时,按传值方式返回对象
以上三种情况都是用一个对象初始化另一个同类对象的语义,会调用拷贝构造函数。
按引用传递对象或返回对象时并不是创建和初始化对象的语义,因而不会引起拷贝构造函数的调用。
最后当我们使用完一个对象时要对其进行销毁,这时我们就要调用析构函数:
析构函数:
析构函数包括函数体和隐式的析构部分,首先执行函数体,然后执行析构部分销毁成员,成员按初始化的逆序销毁,成员的销毁方式完全依赖于成员的类型,销毁类类型的成员需要执行成员自己的析构函数。
当我们初始化完毕之后,再对对象进行赋值操作复制所要得到的数据:
赋值要使用赋值运算符,下面对赋值运算符作一些介绍:
定义类时要控制对象如何赋值,可以重载赋值运算符,如果类没有定义拷贝赋值运算符,编译器会自动合成,行为是将右操作数对象的每个非static 成员赋值给左操作数对象的对应成员,最后返回左操作数对象的引用,类类型的成员通过其拷贝赋值运算符进行赋值,对数组类型的成员,逐个数组元素赋值.
定义方式:
类X 的赋值运算符要定义为类X 的成员函数:
X& operator=(const X&){…}
当进行X 类的对象赋值如a=b 时,就相当于调用成员函数a.operator=(b)
赋值运算符通常返回一个指向左操作数的引用
X& operator=(const X&){… return *this; }
operator=()的基本行为是将右操作数中的信息复制到左操作数中
例:
//一个简单的字符串类
class my_string{
char* str;
int len;
public:
my_string(const char* s = ""){
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
~my_string(){delete[]str;}
my_string& operator=(const my_string& s);
};
…
my_string a("abcde"), b("hijk");
a = b; //如何赋值?
//第一种方式:浅复制,对应的成员之间直接赋值
my_string& my_string::operator=(const my_string& s)
{
len = s.len;
str = s.str;
return *this;
}
//第二种方式:深复制,复制指针指向的单元
my_string& my_string::operator=(const my_string& s)
{
delete[] str;
len = s.len;
str = new char[len + 1];
strcpy(str, s.str);
return *this;
}
如果类中没有重载operator=(),编译器将在需要时自动合成一个,其行为是按成员赋值
对于复杂的类,尤其是包含指针成员时,应该显式地创建operator=()
赋值运算的方式:
赋值运算的常规模式:
首先进行自赋值检测:检查左右操作数两个对象的地址是否相等,如果相等,则它们是同一对象,如果是同一对象的赋值,一般直接返回当前对象。
然后执行正常的赋值操作序列:注意浅复制和深复制的区别。
最后返回当前对象:return *this;
赋值运算的另一种模式:
一、先将右操作数对象复制到一个局部临时对象中,复制可能会调用拷贝构造函数。
二、销毁左操作数对象的现有成员,复制完成后,销毁左操作数对象的现有成员就安全了。
三、将数据从临时对象复制到左操作数对象的成员中,赋值操作序列。
四.返回左操作数对象的引用,函数返回时,临时对象被析构函数销毁。
例:
my_string& my_string::operator=(const my_string& s){
//赋值之前将右操作数对象复制到临时对象temp 中
my_string temp = s; //调用拷贝构造函数
//销毁当前对象的资源
delete[] str;
//将数据从临时对象复制到当前对象的成员,赋值操作序列
len = temp.len;
str = new char[len + 1];
strcpy(str, temp.str);
//返回当前对象的引用
return *this;
}