拷贝控制操作包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
拷贝构造函数
class foo{
public:
foo();//默认构造函数
foo( const foo&);//拷贝构造函数
//……
};
这里有个简单的规则:如果你需要定义一个非空的析构函数,那么,通常情况下你也需要定义一个拷贝构造函数。
拷贝初始化发生在以下情况:
(1)用“=”定义变量
(2)将一个对象作为实参传递给一个非引用类型的形参。
(3)从一个返回类型为非引用类型的函数返回一个对象。
(4)用{}列表初始化一个数组中的元素或一个聚合类中的成员。
补充:编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝。
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
Student s1;
Student s2(s1);//Student s2 = s1;//复制对象
return 0;
}
上述程序中,由于系统调用的是默认拷贝构造函数,没有重新分配内存,两个指针指向同一块内存,而析构函数调用了两次,引起内存泄露。
所以在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
Student(const Student &s);//拷贝构造函数,const防止对象被改变
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::Student(const Student &s)
{
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student " << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
Student s1;
Student s2(s1);//Student s2 = s1;//复制对象
return 0;
}
拷贝赋值运算符
class foo{
public:
foo& operator=( const foo&);//赋值运算符
//……
};
合成拷贝赋值运算符:
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo=rhs.bookNo;//调用string::operator=
units_sold=rhs.units_sold;//调用内置的int赋值
revenue=rhs.revenue;//调用内置的double赋值
return *this;//返回一个对象的引用
}
析构函数
不能被重载。成员按初始化顺序的逆序销毁。
无论何时一个对象被销毁,就会自动调用其析构函数:
- 变量在离开作用域时被销毁;
- 当一个对象被销毁时,其成员被销毁;
- 容器被销毁时,其元素被销毁;
- 动态分配的对象,delete;
- 对于临时对象,当创建它的完整表达式结束时被销毁。
4.三五法则
可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。
(如果一个类需要自定义析构函数,几乎肯定它也需要自定义拷贝赋值运算符和构造函数。)
(如果一个类需要自定义拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符)
5.使用=default
在类中用=default修饰成员的声明,合成的函数将隐式地声明成内联的,并且只能对合成版本的成员函数使用。
6.阻止拷贝
定义删除的函数 =delete 虽然进行了声明,但不能以任何方式使用。=delete必须出现在第一次声明的时候。注:析构函数不能是删除的成员
C++11发布前是通过private来阻止拷贝
class privateCopy{
//默认是private的,声明但不定义
privateCopy(const privateCopy&);
privateCopy &operator=(const privateCopy&);
public:
privateCopy();
~privateCopy();
}
拷贝控制和资源管理
管理类外资源的类必须定义拷贝控制成员。
1.行为像指针的类
class hasPtr{
public:
HasPtr(const string &s=string()):ps(new string(s)),i(0){}
//对ps指向的string 每个HasPtr都有自己的拷贝
HasPtr(const HasPtr &p):ps(new string(*p.ps)),i(p.i){}
HasPtr& operator=(const HasPtr &);
//释放ps
~HasPtr(){delete ps;}
private:
string *ps;
int i;
}
HasPtr::operatpr = (const HasPtr& has) {
// 先定义一个局部变量保存右侧对象,如果右侧对象和左侧对象是同一个对象,那么先删除右侧对象就会发生错误
auto newPtr = new std::string(*has.ps);
// 此时再释放左侧对象资源
delete ps;
ps = newPtr;
i = has.i;
}
2.定义行为像指针的类
引用计数的工作方式如下:
- 除了初始化对象之外,每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象共享正在创建的对象共享状态,当创建一个对象时,引用计数为1,因为此时只有一个对象共享。
- 拷贝构造函数不分配新得引用计数器,拷贝给定对象的数据成员,包括引用计数器,拷贝构造函数递增共享的计数器,表示给定对象更的状态又被一个新用户所共享
- 拷贝赋值运算符递减左侧运算对象的引用计数器,递增右侧对象的引用计数器,如果左侧对象的引用计数器为0,则销毁左侧对象。
- 析构函数判断引用计数是否为0,如果为0,则销毁左侧对象。
class HasPtr {
public:
//构造函数分配新的string和新的计数器,并将计数器设为1
HasPtr(const string& s = string()) :
ps(new string(s)), i(0), use(new size_t(1)) {}
//拷贝构造函数拷贝所有三个成员,并递增计数器
HasPtr(const HasPtr& has) : ps(has.ps), i(has.i), use(has.use) { ++ *use;}
HasPtr& operator = (const HasPtr& has);
~HasPtr();
private:
string* ps;
int i;
size_t* use; // 计数
};
HasPtr::HasPtr& operator = (const HasPtr& has) {
//递增右侧的计数器 ,递减左边的计数器,类似于智能指针
++ *has.use;
if ( --*use == 0) {
delete ps;
delete use;
}
ps = has.ps;
i = has.i;
use = has.use;
return *this;
}
HasPtr::~HasPtr() {
//确保没有其他对象指向这块内存,需要递减计数器再判断
if (--*use == 0) {
delete ps;
delete use;
}
}
交换操作
管理类外资源的类通常还定义了名为swap的函数。
class HasPtr{
friend void swap(HasPtr& ,HasPtr&);
//其他这里不给出
}
inline void swap(HasPtr &lhs ,HasPtr &rhs)
{
using std::swap;//一般情况下直接调用swap,此例中内置数据类型没有特定的swap,所以要调用标准库中的swap
swap(lhs.ps,rhs.ps);//变换指针,而不是string数据
swap(lhs.i,rhs.i);
}
在赋值运算符中使用swap,拷贝并交换技术。
//异常安全的,且能正确处理自赋值
HasPtr& HasPtr::operator=(HasPtr rhs)
{
//
swap(*this,rhs);
return *this;
}
移动构造函数和移动赋值运算符
类似于对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源
//类似于拷贝构造函数,但注意这里的引用参数是一个右值引用
//通常不分配任何资源,因此移动通常不会抛出异常,所以加上noexcept
StrVec::StrVec(StrVec &&s)noexcept
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//移动后确保源对象不再指向被移动的医院,即所有权归新创建的对象
s.elements=s.first_free=s.cap=nullptr;
}
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
StrVec::StrVec operator=(StrVec &&rhs) noexcept
{
//直接检测自赋值
if(this!=&rhs){
free();
elements=rhs.elements;
first_free=rhs.first_free;
cap=rhs.cap;
//使rhs可析构
this.elements=this.first_free=this.cap=nullptr;
}
return *this;
}
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
struct X{
int i;//内置类型可以移动
string s;//string定义了自己的移动操作
};
struct hasX{
X mem;//X有合成的移动操作
}
X x,x2=move(x);//使用合成的移动构造函数
hasX hx,hx2=move(x);//使用合成的移动构造函数
- 移动构造函数定义为删除的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值符类似。
- 如果有类成员的移动构造函数或移动赋值运算符定义为删除的或不可访问的,则类的移动构造函数或者移动辅助运算符被定义为删除的。
- 如果类的析构函数定义为删除的或者不可访问的(这点没明白,析构函数不是不可以定义成删除的么??),则类的移动构造函数被定义为删除的
如果有类成员是const的或是引用,则类的移动赋值运算符定义为删除的
拷贝构造函数与移动构造函数,确定参数是左值还是右值来确定使用哪个,移动右值,拷贝左值