C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁
拷贝构造函数
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成员,它没有类内初始化器且其类型未显示定义默认构造函数 ,或是类有一个引用成员,它没有类内初始化器,则该类的默认构造函数被定义为删除的
C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁的更多相关文章
- C++Primer 第十三章
//1.当定义一个类时,我们显示地或隐式地指出在此类型的对象(注意这里是此类型的对象,而不包括此类型的指针)拷贝,移动,赋值,销毁时做什么.一个类通过定义五种特殊的成员函数来控制这些操作:拷贝构造函数 ...
- C++ Primer : 第十三章 : 拷贝控制之对象移动
右值引用 所谓的右值引用就是必须将引用绑定到右值的引用,我们通过&&来绑定到右值而不是&, 右值引用只能绑定到即将销毁的对象.右值引用也是引用,因此右值引用也只不过是对象的别名 ...
- C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理
定义行为像值的类 行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr. 在实现之前,我们需要: 定义一个拷贝构造函数,完成stri ...
- 【C++ Primer 第十三章】4. 拷贝控制示例
拷贝控制示例 #include<iostream> #include<string> #include<set> #include<vector> us ...
- [C++]类的设计(2)——拷贝控制(拷贝控制和资源管理)
1.类的行为分类:看起来像一个值:看起来想一个指针. 1)类的行为像一个值,意味着他应该有自己的状态.当我们拷贝一个像值的对象时,副本和原对象是完全独立的.改变副本不会对原有对象有任何影响 ...
- C++ Primer : 第十三章 : 拷贝控制示例
/* Message.h */ #ifndef _MESSAGE_H_ #define _MESSAGE_H_ #include <iostream> #include <strin ...
- C++ Primer : 第十三章 : 动态内存管理类
/* StrVec.h */ #ifndef _STRVEC_H_ #define _STRVEC_H_ #include <memory> #include <string> ...
- [C++ Primer] : 第13章: 拷贝控制
拷贝, 赋值与销毁 当定义一个类时, 我们显示地或隐式地指定在此类型的对象拷贝, 移动, 赋值和销毁时做什么. 一个类通过定义5种特殊的成员函数来控制这些操作, 包括: 拷贝构造函数, 拷贝赋值运算符 ...
- 【C++】《C++ Primer 》第十三章
第十三章 拷贝控制 定义一个类时,需要显式或隐式地指定在此类型地对象拷贝.移动.赋值和销毁时做什么. 一个类通过定义五种特殊的成员函数来控制这些操作.即拷贝构造函数(copy constructor) ...
随机推荐
- Linux下的多线程编程
1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...
- Chrome 开发者工具有了设备模拟器
今天从哥们那里学到了一个小技巧,使用chrome自带的多设备模拟器来调试页面在不同设备下的显示效果. 特地上网查了一下,记录一下. 如果想要在 Chrome 上测试网站在不同设备,不同分辨率的显示情况 ...
- Android双击返回按钮退出程序
//双击退出事件 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if(keyCode == KeyEvent.KE ...
- jquery easyui DataGrid 动态的改变列显示的顺序
$.extend($.fn.datagrid.methods,{ columnMoving: function(jq){ return jq.each(function(){ var target = ...
- bzoj 2440 简单莫比乌斯反演
题目大意: 找第k个非平方数,平方数定义为一个数存在一个因子可以用某个数的平方来表示 这里首先需要考虑到二分才可以接下来做 二分去查找[1 , x]区间内非平方数的个数,后面就是简单的莫比乌斯反演了 ...
- c# 多线程创建 ---简单
Thread t = new Thread(new ParameterizedThreadStart(UploadCard)); t.IsBackground = false;//后台线程 前台线程 ...
- C++学习之静态成员
一.静态数据成员 C++允许将类的数据成员定义为静态成员.静态数据成员是属于类的,整个类只有一个备份,相当于类的全局变量,能够被该类的所有对象共用. 1.静态成员的声明 在类数据成员的声明前加上关键字 ...
- iOS应用崩溃日志分析
转自raywenderlich 作为一名应用开发者,你是否有过如下经历? 为确保你的应用正确无误,在将其提交到应用商店之前,你必定进行了大量的测试工作.它在你的设备上也运行得很好,但是,上了应 ...
- 《Java程序性能优化:让你的Java程序更快、更稳定》
Java程序性能优化:让你的Java程序更快.更稳定, 卓越网更便宜,不错的书吧
- 让多个Fragment 切换时不重新实例化、FragmentTabHost切换Fragment时避免UI重新加载
http://www.tuicool.com/articles/FJ7VBb FragmentTabHost切换Fragment时避免UI重新加载 不过,初次实现时发现有个缺陷,每次FragmentT ...