C++常见的初始化方式有两种,分别是直接初始化以及复制初始化(在primer中也被称为拷贝初始化)。
①什么是拷贝初始化?什么是直接初始化?
通俗点来说,直接初始化是指不使用等号的初始化, 而拷贝初始化则是使用等号的初始化。
我们将使用string举一个例子:
string test("test");//直接初始化
string test = string("test")//拷贝初始化
那么这两种初始化有什么区别呢?
初始化跟类一直都是紧密相连的,因此这两种初始化其实是跟类调用它的构造函数方式有关系。
直接初始化会根据函数匹配调用类的构造函数,而拷贝初始化则会调用它的拷贝构造函数或移动构造函数。(移动构造函数可查看下一篇文章)
②什么是拷贝构造函数?
既然称为拷贝构造函数,那么自然也是构造函数的一种。拷贝构造函数是第一个参数为自身类类型的引用(最好加上const),其他参数都为默认参数的构造函数,而当类没有定义自己的拷贝构造函数的时候,编译器会自动合成一个拷贝构造函数。我们接下来看一个例子:
#include<iostream>
class test{
public:
test(int v) : value(v){}
test(const test temp){
value = temp.value;
std::cout << "test constructor";
}
void print(){std::cout << value;}
private:
int value;
};
int main(void){
test A(10);
test B(A);
B.print();
return 0;
}
在这个例子中,编译器将会报出一个错误:
error: invalid constructor; you probably meant 'test (const test&)'
在这个例子中,为什么编译器提醒我们要将第一个参数定义成引用类型呢?难道我们不能定义这样一个构造函数吗?我们刚才定义的构造函数它的第一个参数不是对自身类类型的引用,所以它并非一个拷贝构造函数,既然没有定义拷贝构造函数,那么编译器将为我们合成一个,但是在函数匹配方面,第一个参数是引用类类型(编译器合成)以及类类型(自己定义)明显是不能进行匹配的,所以编译器才会报错。
当我们清楚了拷贝构造函数以后,我们知道拷贝初始化将会调用拷贝构造函数,那么直接初始化会不会调用拷贝构造函数呢?
我们之前说了,直接初始化是根据函数匹配的,那么当函数匹配到拷贝构造函数的时候,直接初始化就会调用拷贝构造函数。我们看一个例子:
#include<iostream>
class test{
public:
test(int v) : value(v){
std::cout << "test constructor" << std::endl;
}
test(const test& temp){
value = temp.value;
std::cout << "test copy constructor" << std::endl;
}
void print(){std::cout << value;}
private:
int value;
};
int main(void){
test A(10);//直接初始化, 输出test constructor
test B(A);//直接初始化, 输出test copy constructor
return 0;
}
我们再来看一个有趣的例子:
#include<iostream>
class test{
public:
test(int v) : value(v){
std::cout << "test constructor" << std::endl;
}
test(const test& temp){
value = temp.value;
std::cout << "test copy constructor" << std::endl;
}
void print(){std::cout << value;}
private:
int value;
};
int main(void){
test A(10);//直接初始化,输出test constructor
test B = test(10);//拷贝初始化, 输出test constructor
return 0;
}
仔细观察main函数的第二条初始化语句,我们就会发现,拷贝初始化为什么没有调用拷贝构造函数呢?并非是以上的这条规则拷贝初始化调用拷贝构造函数发生了错误,而是编译器对这一条语句进行了优化,编译器发现第二条语句可以直接优化为test B(10),因此没有调用拷贝构造函数,但是,虽然没有调用,拷贝构造函数必须有定义。
③什么是拷贝赋值运算符呢?
如果说拷贝初始化用于类的初始化,那么拷贝赋值运算符(函数运算符重载)则用于相同对象之间的复制赋值。我们来看一下例子:
#include<iostream>
class test{
public:
test(int v) : value(v){
std::cout << "test constructor" << std::endl;
}
test(const test& temp){
value = temp.value;
std::cout << "test copy constructor" << std::endl;
}
test& operator=(const test& temp){
value = temp.value;
std::cout << "test copy operator" << std::endl;
return *this;
}
void print(){std::cout << value;}
private:
int value;
};
int main(void){
test A(10);//直接初始化,输出test constructor
test B = A;//拷贝初始化,输出test copy constructor
test C(10);//直接初始化,输出test constructor
C = B;//输出test copy operator
return 0;
}
我们可以看到拷贝构造函数与拷贝赋值运算符的明显区别,当我们在声明一个类的实例之后立即初始化就会使用拷贝构造函数,而当我们声明一个类之后,另起一行再赋值则会调用赋值运算符。
其中,在拷贝赋值运算符的定义中返回一个左边对象的引用类型是一个最佳实践。