拷贝控制操作基本概念:
拷贝定义了当用同类型的另一个对象初始化本对象时做什么。
赋值定义了将一个对象赋予同类型的另一个对象时做什么。
折构定义了当此类型对象销毁时做什么。
拷贝构造函数:如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
合成拷贝函数:如果我们没有为类定义拷贝构造函数,编译器会为我们定义一个。编译器会从指定对象中依次将每个非static成员拷贝到正在创建的对象当中。
class A{
public:
A();//默认构造函数
A(A&);//拷贝构造函数,参数必须是引用,不然会无限递归调用。
//.......
}
直接初始化是编译器使用普通函数匹配,而拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化不仅在我们用=定义变量时会发生,下列情况也会发生:
(1)将一个对象作为实参传递给一个非引用类型的形参
(2)从一个返回类型为非引用类型的函数返回一个对象
(3)用花括号列表初始化一个数组的元素或一个聚合类的成员
#include <iostream>
#include <string>
class A
{
public:
A() = default;
A(int a, std::string b):i(a), s(b) { std::cout << "直接初始化" << std::endl;} //直接初始化
A(const A&a):i(a.i),s(a.s) { std::cout << "调用拷贝构造函数" << std::endl; } //拷贝初始化
private:
int i;
std::string s;
};
int main()
{
A a1(1,"123");//直接初始化
A a2(a1);//拷贝初始化
A a3 = a1; //拷贝初始化
}
拷贝赋值运算符是通过重载运算符来实现的。重载运算符的参数表示运算符的对象。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的 this参数。
如果一个类为定义自己的拷贝赋值运算符,则编译器会为其生成一个合成拷贝赋值运算符。运算过程与拷贝构造函数相似。
class A{
public:
A& operator=(const A&);//赋值运算符返回一个指向其左侧对象的引用。
//...
}
折构函数释放对象使用资源,并销毁对象的非static数据成员。在一个折构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。成员在销毁时发生什么完全依赖于成员的类型。折构函数体本身并不直接销毁成员,成员是在折构函数体之后隐含的折构阶段被销毁的。特别注意,当指向一个对象的引用或指针离开作用域时,折构函数不会执行。
class A{
public:
~A();//折构函数
}
折构函数调用于:
(1)变量离开其作用域时被销毁
(2)当一个对象被销毁时,其成员被销毁
(3)容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
(4)对于动态分配的对象,当对指向它的指针应用delete运算符被销毁
(5)对于临时对象,当创建它的完整表达式结束时被销毁。
#include <iostream>
#include <vector>
#include <initializer_list>
struct X {
X() { std::cout << "X()" << std::endl; }
X(const X&) { std::cout << "X(const X&)" << std::endl; }
X& operator=(const X&) { std::cout << "X& operator=(const X&)" << std::endl; return *this; }
~X() { std::cout << "~X()" << std::endl; }
};
void f(const X &rx, X x,X x1)// X(const X&)
{
std::vector<X> vec;
x=x1;//X& operator=(const X&)
vec.reserve(2);
vec.push_back(rx);// X(const X&)
vec.push_back(x);// X(const X&)
x=x1;//X& operator=(const X&)
vec.push_back(x1);X(const X&)*3,vec空间不够,先拷贝先前的元素到另一个空间,再拷贝新加的元素
//退出局部作用域,vec_old两个元素,vec_new三个元素,x,x1
}
int main()
{
X *px = new X;//X()
f(*px,*px,*px);
delete px;// ~X()
return 0;
}
*三/五 法则
(1)当我们决定一个类是否需要定义自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个折构函数。如果一个类需要一个折构函数,我们几乎可以确定它也需要一个拷贝构造函数和一个拷贝赋值运算符。
(2)如果一个类需要一个拷贝构造函数,几乎可以确定它也需要一个拷贝赋值运算符,反之亦然。而无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要折构函数。
#include <iostream>
class numbered {
public:
numbered() {
std::cout<<"numbered()"<<std::endl;
mysn = unique++;
std::cout << mysn << std::endl;
}
numbered(const numbered& n) {
std::cout<<"numbered(const numbered& n)"<<std::endl;
mysn = unique++;
std::cout << mysn << std::endl;
}
int mysn;
static int unique;
};
int numbered::unique = 10;
void f(numbered s) {
std::cout << s.mysn << std::endl;
}
int main()
{
numbered a, b = a, c = b;// numbered() 10,numbered(const numbered& n) 11,numbered(const numbered& n)12
f(a);// numbered(const numbered& n) 13
f(b);//numbered(const numbered& n) 14
f(c);//numbered(const numbered& n) 15
}
我们可以通过将拷贝控制成员定义成=default来显式的要求编译器生成合成的版本。同时,在c++11标准中,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。但是需要注意的是,折构函数不能是删除的成员。对于折构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
struct Nocopy{
Nocopy=default;//使用合成的默认构造函数
Nocopy=(const Nocopy&)=delete;//阻止拷贝
Nocopy& operator=(const Nocopy&)=delete;//阻止赋值
~Nocopy()=default;//使用合成的折构函数
}