拷贝控制
拷贝、赋值和销毁
- 有5种拷贝操作:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数
拷贝构造函数
- 拷贝构造函数的第一个参数必须是引用类型,为了明确避免在构造函数中修改传参,我们一般也令其为const,形式为
Foo(const Foo&)
- 如果用户没有自己定义拷贝构造函数,编译器会为我们定义一个,但是涉及到内存空间管理等问题时,默认的拷贝构造函数往往会有问题,因此建议尽量自己实现。
- 直接初始化指的是:我们要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数;而拷贝初始化指的是,我们在要求将右侧运算对象拷贝到正在创建的对象中,如果需要的话还需要进行类型转换。
- 拷贝初始化发生的几种情况
- 用
=
定义变量 - 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 从大括号列表对一个数组中的元素或者一个聚合类中的成员进行初始化
- 用
合成拷贝赋值运算符
- 如果类未定义自己的拷贝赋值运算符,则编译器会为它生成一个合成拷贝赋值运算符,
析构函数
- 析构函数不接受任何参数,因此他无法被重载,一个类只有一个析构函数
- 析构函数用于释放对象在生存期间分配的所有资源。
- 对于普通指针,我们需要自己实现对其的析构;而智能指针是类类型,它具有析构函数,在析构阶段会被自动销毁。
- 需要析构函数的类,通常也都需要拷贝构造和拷贝赋值运算符。
阻止拷贝
- 想要不允许拷贝构造函数和拷贝赋值运算符的操作的话,光是不定义是不可行的,因为编译器会生成默认的,对于iostream,如果允许拷贝,则会造成
多个对象写入或者读取相同IO缓冲
的问题,因此需要阻止这种类型的类的拷贝,直接赋值为delete即可。 - 析构函数是不能删除的成员
- 当无法拷贝、赋值或者销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
拷贝控制和资源管理
- 之前提到的是对于每个对象,都希望其指向不同的内存空间;而对于有些对象或者类,则希望指向同一块内存空间,比如说智能指针的概念,此时就需要定义引用计数的概念,当引用计数值为0时,将对应的资源释放;为了可以使得引用计数被所有的对象使用,可以将其以指针的形式进行存储。
上面的内容对应的代码
class A
{
public:
A(int size) : size_(size) { arr_ = new int[size_]{0}; }; // 构造函数
// 如果不实现这个拷贝构造函数,则在使用的过程中,不同的对象可能会共用同一块内存空间
A(const A& a) : size_(a.size_)
{
cout << "I am being copy constructed." << endl;
arr_ = new int[size_];
for (int i = 0; i < size_; i++)
arr_[i] = a.arr_[i];
}
// 拷贝赋值
// 返回值是引用
A& operator=(const A& rhs)
{
cout << "copy assigned." << endl;
auto tmp = new int[rhs.size_];
delete[]arr_;
size_ = rhs.size_;
arr_ = tmp;
for (int i = 0; i < size_; i++)
arr_[i] = rhs.arr_[i];
return *this;
}
void set(int index, int val)
{
assert( index >= 0 && index < size_ );
*(arr_ + index) = val;
}
int get(int index)
{
assert(index >= 0 && index < size_);
return *(arr_ + index);
}
~A() { delete[] arr_; }
private:
int *arr_;
int size_;
};
class B
{
public:
B() = default;
B(int val) : val_(val) {}
// 禁止拷贝构造与拷贝赋值
B(const B&) = delete;
B& operator=(const B&) = delete;
int get()
{
return val_;
}
private:
int val_;
};
void test()
{
A a1(3); // 直接初始化
A a2(a1); // 直接初始化
A a3 = a1; // 拷贝初始化
a3 = a2; // 拷贝赋值
a1.set(0, 2);
cout << a2.get(0) << endl;
B b1(3);
cout << b1.get() << endl;
// B b2 = b1;// 无法编译通过,因为已经禁止拷贝了
}
交换操作
- 标准库中的交换函数
swap
,为了进行一次交换,需要进行1次拷贝与2次赋值,但是有些内存分配可能是不必要的,我们可以自定义自己的swap
函数,省去交换时额外的操作 - 注意在使用的过程中,要避免与标准库的swap相混淆。
- 可以使用自定义的swap函数来实现赋值运算,之前的赋值运算符都是传入const引用,我们可以传入一个非const、非引用的参数,swap之后,该参数指向的对象会被自动释放,从而实现了安全的赋值操作。
code
class A { friend void swap(A& lhs, A& rhs); public: A(const string &str = string(), int val = 0) : str_(new string(str)), val_(val) { } A(const A& rhs) : str_(new string(*rhs.str_)), val_(rhs.val_){ } A& operator=(A tmp) { swap( *this, tmp ); return *this; } void print(ostream& os = cout) { os << "string : " << *str_ << ", val : " << val_ << endl; } ~A() { delete str_; } private: int val_; string *str_; }; inline void swap(A& lhs, A& rhs) { cout << "user defined swap for class A is used." << endl; using std::swap; swap(lhs.str_, rhs.str_); swap( lhs.val_, rhs.val_ ); } void test() { using std::swap; // 如果需要使用标准库的swap,需要使用std::swap A a1("hello", 1); A a2("world", 2); cout << "curr a1 and a2 :" << endl; a1.print(); a2.print(); swap( a1, a2 ); cout << "curr a1 and a2 after swap :" << endl; a1.print(); a2.print(); // 在这里因为没有对A定义拷贝与赋值操作,实际交换的过程中会出现异常 //std::swap(a1, a2); //cout << "curr a1 and a2 after std::swap :" << endl; //a1.print(); //a2.print(); A a3; a3 = a1; cout << "a3 :" << endl; a3.print(); }
对象移动
- 在某些情况下,拷贝完对象之后,对象就会被销毁,在这种情况下,移动而非拷贝对象会大幅度提升性能。
右值引用
- 右值引用即必须绑定到右值的引用,通过
&&
来获得右值引用。 - 右值引用的一个重要性质:只能绑定到一个将要销毁的对象。因此可以将一个右值引用的资源移动到另一个对象中。
- 相对左值引用(
&
),右值引用只能绑定到临时对象,同时该对象没有其他用户,因此使用右值引用可以自由地接管所引用的对象的资源。 code
void test() { int i = 5; int &r1 = i; //int &r2 = i * 2; // 错误:&必须引用左值,这里i*2是右值 int &&r3 = i * 2; //正确:i*2是右值临时对象 // int &&rr3 = i; //错误,i是左值,无法通过右值引用 i = i * 2; // 修改i不影响r3的值(右值引用),但是影响r1的值(左值引用) cout << "r1 : " << r1 << endl; cout << "r3 : " << r3 << endl; const int &r4 = 50; // 可以将一个const引用绑定到一个右值上 cout << "r4 : " << r4 << endl; }
标准库move函数
- move可以显示地将一个左值转换为对应的右值引用类型。
- 调用move意味着承诺:除了对源对象进行赋值或者销毁之外,我们将不再使用它。在调用move之后,不能对移后源对象的值做任何假设。
- 移动操作只使用原始对象的资源,它本身通常不会分配任何资源。