一、拷贝构造函数
定义: 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都是默认值,则此构造函数是拷贝构造函数。
class Foo {
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
// ...
}
1.1 拷贝构造函数的参数类型为什么必须是一个引用类型?
如果其参数不是引用类型,则需要拷贝实参,又需要调用拷贝构造函数,如此无限循环。
1.2 拷贝构造函数的参数类型可不可以是指针类型?
不可以,根据定义,参数是指针类型的构造函数可以是转换构造函数,但不可以是拷贝构造函数。
1.3 拷贝构造函数可以声明显式调用explicit吗?
不能,拷贝构造函数在几种情况下会被隐式调用,因此不能是explict。
拷贝初始化应用场景:
- 用 = 定义变量时
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 标准容器insert/push等也会使用
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
二、拷贝赋值运算符
拷贝赋值运算一般组合了拷贝构造和析构操作,操作前要先判断是否是自赋值。
class Foo {
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
// 拷贝赋值运算符通常返回一个指向左侧运算对象的引用。
Foo& operator=(const Foo&); // copy-assignment Operator,(翻译成赋值构造函数有点牵强,一般翻译为拷贝赋值运算符)
// ...
}
2.1 拷贝赋值运算符返回值有什么用?返回值是否为引用类型有区别吗?
拷贝赋值运算符的返回值为引用类型比较好,在给多个对象赋值时,可以节省内存开支。在进行链式操作时,不但可以节省内存开支,还可以对其进行修改性操作
2.2 拷贝赋值运算符参数也必须为引用吗?
class Person {
public:
Person() {}
Person(const Person& p) {
cout << "Copy Constructor" << endl;
}
Person& operator=(const Person &p) {
// Person& operator=(const Person p) { // change
cout << "Assign" << endl;
return *this;
}
private:
int age;
string name;
};
int main() {
Person p;
Person p2;
p2 = p;
return 0;
}
// 引用传递运行结果
// 值传递运行结果
可见,参数不必为引用,但是使用引用可以减少多余操作。
2.3 拷贝构造函数和拷贝赋值运算符区别?
// 使用上文Person类
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
- 这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
- 首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值。
- 拷贝构造函数和赋值运算符的行为比较相似,却产生不同的结果;拷贝构造函数使 用已有的对象创建一个新的对象 ,赋值运算符是将 一个对象的值复制给另一个已存在的对象 。区分是调用拷贝构造函数还是赋值运算符,主要是否有新的对象产生。
三、关于类中指针成员的问题
拷贝赋值和移动赋值指针成员数据时,需要先释放旧数据,再将rhs赋值到旧数据。
希望阻止拷贝的类应该使用=delete 来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为private。
一个带指针变量拷贝控制示例:
#ifndef __MYSTRING__
#define __MYSTRING__
class String {
public:
String(const char* cstr=0);
String(const String& str); // 拷贝构造
String& operator= (const String& str); // 拷贝赋值
~String();
char* get_c_str const(){
return m_data;
}
private:
char* m_data; // 带指针成员的类:一定要注意拷贝赋值,拷贝构造,析构
// String的底部通过char实现,在外部表现为string
};
inline String::String ( const char* cstr=0 ) {
if(cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data,cstr); // char * strcpy ( char * destination, const char * source );
} else { // 考虑cstr=0;
m_data=new char[1];
m_data[0]='\0';
}
}
inline String::String(const String& str) {
m_data=new char[strlen(str.get_c_str())+1];
strcpy(m_data,str.m_data);
}
inline String::~String() {
delete[] m_data;
}
inline String& String::operator=(const String& str){
if(this==&str){ // self assignment :1.**自我检验** (如果没有进行这样的处理,在自我赋值会产生严重的错误)
return *this;
}
// 2.**构造函数是第一次构造,因此不需要删除原内存对象,但是赋值需要先进行delete**
delete[] m_data; // 先删除,重新分配一样大小!
m_data=new char[strlen(str.get_c_str())+1];
strcpy(m_data,str.m_data);
return *this; // 其实不用返回*this也可以,因为已经实现了修改,但是这样有一个好处 可以实现 a=b=c; (因为返回的类型继续支持=)
}
// 调用形式: cout << String() ; 第一个参数为 << 左边 ,第二个参数为 << 右侧;返回ostream 可以实现 cout << a << b ;
ostream& operator<<(ostream& os,const String& str){
os<<str.get_c_str();
return os;
}
#endif