拷贝构造函数
class Sales_data {
public:
Sales_data(const Sales_data&); private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0; }; Sales_data::Sales_data(const Sales_data& data) : bookNo(data.bookNo),
units_sold(data.units_sold),
revenue(data.revenue)
{
}
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-999-99999-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化
直接初始化和拷贝初始化的区别:对于直接初始化,实际上时要求编译器调用普通的函数匹配来选择我们所提供的参数最匹配的构造函数。 对于拷贝初始化,要求编译器将右侧正在运算的对象拷贝到正常创建的对象中,需要的话进行类型转换。
- 将一个对象作为参数传递给一个非引用类型的形参
- 将一个对象作为返回值从一个返回值为非引用类型的函数返回时
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员
vector<int> v1(10); // 正确,直接初始化
vector<int> v2 = 10; // 错误,接收大小参数的构造函数时explicit的
void f(vector<int>); // f的参数进行拷贝初始化
f(10); // 错误,不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); // 正确,从一个int直接构造一个临时vector
接受一个大小参数的构造函数时explicit的,意味着我们只能进行直接初始化而不能进行拷贝初始化;因此,我们不能用一个explicit的构造函数来拷贝一个参数,这里的拷贝有参数传递和函数的非引用返回值,如果我们希望使用一个explicit的构造函数时,必须显示的使用,比如上述代码的最后一行那样。
string null_book = "123456";
可以改写为:
string null_book("123456");
即使有时候编译器可以跳过拷贝/移动构造函数,但是我们必须保证拷贝/移动构造函数是存在而且是可访问的(例如:不能是private)。
class MyClass{ public: MyClass(int data) : m_data(data){ std::cout << "Construct a MyClass! The m_data = " << m_data << std::endl;} MyClass(const MyClass& myclass){ this->m_data = myclass.m_data; std::cout << "Copy Construct a MyClass that right m_data = " << myclass.m_data << std::endl; } MyClass& operator = (const MyClass& myclass){ this->m_data = myclass.m_data;
std::cout << "copy a MyClass that right m_data = " << myclass.m_data << std::endl;
return *this; } ~MyClass(){ std::cout << "Delete a MyClass that m_data = " << m_data << std::endl; } int getData(){return m_data;}
private: int m_data; }; int main(int argc, char** argv)
{ std::vector<MyClass> vecClass;
MyClass mclass1(1);
MyClass mclass2(2); std::cout << vecClass.capacity() << std::endl; // emplace_back进行直接初始化
vecClass.emplace_back( 1); std::cout << vecClass.capacity() << std::endl; // 由于vecClass的空间不能容下mclass2, 因此先拷贝一份mclass2,然后再析构原来的mclass2
vecClass.push_back(mclass2); std::cout << vecClass.capacity() << std::endl; MyClass* pMyClass = new MyClass(3); // 和上面同理
vecClass.insert(vecClass.cend(), *pMyClass); std::cout << vecClass.capacity() << std::endl; for (size_t i = 0; i < vecClass.size(); ++i)
std::cout << vecClass[i].getData() << std::endl; delete pMyClass; return 0;
}
输出为:
Construct a MyClass! The m_data = 1
Construct a MyClass! The m_data = 2
0
Construct a MyClass! The m_data = 1
1
Copy Construct a MyClass that right m_data = 1
Delete a MyClass that m_data = 1
Copy Construct a MyClass that right m_data = 2
2
Construct a MyClass! The m_data = 3
Copy Construct a MyClass that right m_data = 3
Copy Construct a MyClass that right m_data = 1
Copy Construct a MyClass that right m_data = 2
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 2
3
1
2
3
Delete a MyClass that m_data = 3
Delete a MyClass that m_data = 2
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 2
Delete a MyClass that m_data = 3
在调用emplace_back时,进行直接初始化,因此调用的是构造函数; 调用push_back时,进行拷贝初始化,调用拷贝构造函数,因为此时vecClass的空间不足,因此先拷贝了原来在vecClass中值为1的元素,然后销毁原来的vector,然后再拷贝新的元素,下面的代码中调用insert函数也是这样的道理,由于空间不够,先拷贝旧容器中的元素,然后销毁,再拷贝新的元素。
拷贝赋值运算符
Sales_data trans, accum;
trans = accum; // 使用Sales_data的拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,则编译器替我们合成一个。
class Foo { public:
Foo& operator =(const Foo&); // 拷贝赋值运算符 // ...
};
为了和内置类型的赋值保持一致,类的拷贝赋值运算符通常返回一个指向其左则对象的引用。应当注意的是,标准库要求保存的类型要具有赋值运算符,且返回值是左则运算对象的引用。
Sales_data& Sales_data::operator = (const Sales_data& data) {
bookNo = data.bookNo; // 调用std::string::operator =
units_sold = data.units_sold; // 使用内置的int赋值
revenue = data.revenue; // 使用内置的double赋值
return *this; // 返回左则运算对象的引用
}
析构函数
class Foo { public:
// 其他操作
~Foo(); // 析构函数
};
析构函数由波浪号加类名组成,没有返回值,不接受参数列表。
{
Sales_data* p = new Sales_data; // p是一个内置指针
auto p2 = make_shared<Sales_data>(); // p2是一个shared_ptr Sales_data item(*p); // 拷贝构造函数将*p拷贝到item中
vector<Sales_data> vec;
vec.push_back(*p2); // 拷贝p2指向的对象,增加p2的计数
delete p; // 对p指向的对象执行析构函数
} // 退出局部作用域,对item、vec、p2调用析构函数
// 销毁p2会递减其引用计数,如果引用计数变为0, 对象被释放
// 销毁vec会销毁它的元素
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
三/五法则
class HasPtr { public:
HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0){} private:
std::string* ps;
int i;
};
对于HasPtr类来说,其构造函数为ps分配动态内存,合成的析构函数不会delete一个指针数据成员,因此,这个类需要定义自己的析构函数:
~HasPtr() { delete ps; }
参考上面的原则,我们理应给HasPtr定义一个拷贝构造函数和拷贝赋值运算符,如果我们使用合成的拷贝构造函数和拷贝赋值运算符时, 将会发生严重的错误。
HasPtr f (HasPtr hp) {
HasPtr ret = hp; // 拷贝给定的HasPtr
return ret; // ret和hp被释放
}
当f返回时,hp和ret都被释放,两个对象的析构函数都被执行。于是会delete掉ret和hp的指针成员,但这个两个对象包含相同的指针值,因此会导致此指针被delete两次,发生了一个严重的错误; 另外使用函数 f 的返回值作为初始化值的对象时,指针被销毁,指向无效地址。因此,我们需要定义自己的拷贝构造函数和拷贝赋值运算符。
HasPtr& HasPtr(const HasPtr& has) { ps = new std::string(*(has.ps));
i = has.i;
return *this;
}
需要拷贝操作的类也需要赋值操作,反之亦然
=default和=delete
class Sales_data { public:
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator =(const Sales_data&);
~Sales_data() = default;
// ...
}; Sales_data& Sales_data::operator = (const Sales_data& ) = default;
在类内使用=default修饰成员的声明时,合成的函数隐式的被声明为内联的,如果不希望是内联的,我们应该在类外定义使用=default。
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy&) = delete; // 阻止拷贝
NoCopy& operator = (const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default;
// 其他成员
};
=delete告诉编译器,我们不希望定义这些函数。
- 如果类的某个成员的析构函数是删除的或者不可访问的(例如,是private的),则类的合成析构函数被定义为删除的
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的,如果类的某个成员析构函数是删除的或不可访问的,,则类合成的拷贝构造函数也被定义为删除的。
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或者是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的
- 如果类的某个成员的析构函数是删除的或是不可访问的,或是类有一个const成员,它没有类内初始化器且其类型未显示定义默认构造函数 ,或是类有一个引用成员,它没有类内初始化器,则该类的默认构造函数被定义为删除的