当我们定义一个类时,我们显示或隐式地指定在此类型对象拷贝、赋值和销毁时做什么。
一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的令一个对象时做什么,析构函数定义了当此类型对象销毁时做什么。
在本片文章主要介绍拷贝构造函数、拷贝赋值运算符和析构函数。
拷贝构造函数
拷贝构造函数:一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值
class foo{
public:
foo();
foo(const foo&);
};
几点说明:
1、第一个参数是引用类型?
在函数调用过程中,具有非引用类型的参数要进行拷贝初始化,即拷贝构造函数被用来初始化非引用类类型参数,如果不是,为了调用拷贝构造函数,我们必须拷贝他的实参,为了拷贝实参,我们又需要调用拷贝构造函数如此无限循环。
2、通常此参数是const的,并且拷贝构造函数在几种情况下都可以被隐式的使用,因此拷贝构造函数通常不应该是explicit的。
- 合成的拷贝构造函数
与合成的默认构造函数不同,即使我们定义了一个拷贝构造函数,编译器也会为我们合成拷贝构造函数。
合成的拷贝构造函数作用:
1、阻止我们拷贝该类类型的对象;
2、将其参数的成员(非static)逐个拷贝到正在创建的对象中(对于类类型,使用其拷贝构
造函数拷贝,对内置类型直接拷贝)。
class sales_data(){
public:
//与合成的拷贝构造函数等价的拷贝构造函数的声明
sales_data(const sales_data&);
private:
string bookno;
int units_sold=0;
double revenue=0.0;
};
sales_data:sales_data(const sales_data &orig):
bookno(orig.bookno),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
- 拷贝初始化
string dots(10,'.'); //直接初始化
string s(dots); //直接初始化
string s2=dots; //拷贝初始化
string null_book="9-999-99999-9";//拷贝初始化
string nines=string(10,'9'); //拷贝初始化
当我们使用直接初始化时,实际上是要求编译器使用普通的函数匹配,来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要可能会进行类型转换。
拷贝初始化通常发生在:
1、用=号定义变量时
2、讲一个对象作为实参传递给一个非引用类型的形参
3、从一个返回类型为非引用类型的函数返回一个对象
4、用花括号列表初始化一个数组中的元素或一个聚合类中的对象某些类类型对象会对他们所分配的对象使用拷贝初始化。容器调用push 或insert,与之相对调用 emplace 使用直接初始化。
note:
1、当我们使用的初始化值要求通过一个explicit的构造函数来进行类型转换时:
vector<int> v1(10);//正确:直接初始化
vector<int> v2=10; // 错误:接收大小参数的构造函数是 explicit
void f(vector<int>); //f 的参数进行拷贝初始化
f(10);// 错误
f(vector<int>(10));//正确
2、在拷贝初始化过程中,编译器可以跳过拷贝构造函数,直接创建对象
string null=”9-999-9”;//拷贝初始化
改写为:
string null(”9-999-9”);//编译器跳过了拷贝构造函数
即使编译器跳过了拷贝构造函数,但在这个程序点上,拷贝构造函数必须是存在的且可以访问的。
example:
为给定类编写一个拷贝构造函数:
class hasptr{
public:
hasptr(const string&s=string()):
ps(new string(s)),i(0){}
hasptr(const hasptr&hp);
private:
string *ps;
int i;
};
//应该动态分配一个新的string,并将对象拷贝到ps指向的位置,而不是拷贝ps本身
hasptr::hasptr(const hasptr&hp)
{
ps=new string(*hp.ps);
i=hp.i;
}
拷贝赋值运算符
与类控制其对象如何初始化一样,类也可以控制其对象如何赋值,如果一个类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
- 重载赋值运算符
重载运算符本质上是一个函数,其名字由operator关键字后接表示要定义的运算符的符号组成。如:operator=。运算符函数也有一个返回类型和一个参数列表。重载运算符的参数表示运算符的运算对象。某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧对象被绑定到隐式的this参数。对于一个二元运算符,其右侧对象,作为显示参数传递。
拷贝赋值运算符接收一个与其所在类相同类型的参数:
//通常返回一个指向其左侧对象的引用
class foo{
public:
foo& operator=(const foo&);//拷贝赋值运算符
};
- 合成拷贝赋值运算符
作用: 1、禁止该类型对象的赋值; 2、将右侧的每个非 static 对象赋予左侧对象的成员,这一工作通过成员类型的拷贝赋值运算符来完成的。
//等价于合成的拷贝运算符
sales_data& sales_data::operator=(const sales_data &rhs)
{
bookno=rhs.bookno;
units_sold=rhs.units_sold;
revenue=rhs.revenue;
return *this;
}
example:
为给定类编写一个拷贝赋值运算符
class hasptr{
public:
hasptr(const string&s=string()):
ps(new string(s)),i(0){}
hasptr(const hasptr&hp);
hasptr& opeartor=(const hasptr&);
private:
string *ps;
int i;
};
//应该动态分配一个新的string,并将对象拷贝到ps指向的位置,而不是拷贝ps本身
hasptr::hasptr(const hasptr&hp)
{
ps=new string(*(hp.ps));
i=hp.i;
}
//赋值运算符应该将对象拷贝到ps指向的位置
hasptr& hasptr::opeartor=(const hasptr &rhs)
{
auto newps=new string(*rhs.ps);//拷贝指针指向的对象
delete ps; //销毁源对象
ps=newps; //指向新的string
i=rhs.i;
return *this;
}
析构函数
参考C++常见问题总结_构造函数与析构函数
三/五法则
通常并不要求我们定义所有这些拷贝操作,可以定义其中一个或两个,而不必定义所有。下面介绍一些常见的准则。
1、需要析构函数的类也需要拷贝和赋值操作
通常使用了动态内存的类,一般需要定义自己的析构函数
//
class hasptr{
public:
hasptr(const string&s=string()):
ps(new string(s)),i(0){}
~hasptr(){delete ps;}
private:
string *ps;
int i;
};
//如果我们没有为我们的hasptr类定义拷贝和赋值操作将会有错误
例如:
hasptr f(hasptr hp)
{
hasptr ret=hp;
return ret; //ret 和hp被销毁
}
/*
ret和hp被销毁时,会执行对应的构造函数,
此时会delete ret和hp中的指针成员,
此时会导致被delete两次。*/
2、如果一个类需要一个拷贝构造函数,则它肯定需要一个拷贝赋值运算符,反之亦然。并且,一个类无论是需要一个拷贝构造函数还是需要一个拷贝赋值运算符,都不意味着它需要一个析构函数。
两点说明
1、使用=default
可以通过将拷贝控制成员定义为=default 来显示要求编译器生成合成的版本。当我们在类内用=default 修饰成员的声明时,合成的函数将隐式的声明为内联的。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default。(只能对具有合成版本的成员函数使用delete)
2、阻止拷贝
定义删除的函数
虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符,但对于某些类来说,这些操作没有合理的意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。
在参数列表后面加上=delete 来指出我们希望将它定义为删除的(我们虽然定义了它,但不希望以任何方式使用它)
与=default 不同,=delete 必须出现在函数第一次声明的时候
与=default 不同,我们可以对任何函数指定=delete(只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default)
struct nocopy{
nocopy()=default;
nocopy(const nocopy&)=delete;
nocopy &operator=(const nocopy&)=delete;
~nocopy()=default;
};
析构函数不能是删除的成员
析构函数被删除, 就无法销毁此类型的对象了。 对于删除了析构函数的类型, 我们不能定义这种类型的变量或成员, 但可以动态分配这种类型的对象, 但是不能释放这些对象。
struct nodtor{
nodtor()=default;
~nodtor()=delete;
};
nodtor nd;//错误:他的析构函数是删除的
nodtor *p=new nodtor();//正确但是不能delete p
delete p;//错误
3、合成的拷贝控制成员可能是删除的
如果一个类中有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将会被定义成删除的。本质上,当不能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
NOTE:
- 类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。
虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,不是引用本身。如果为这样的类合成的拷贝赋值运算符,则赋值后,左侧对象(引用)仍然绑定与赋值前一样的对象,而不是与右侧对象指向相同的对象。
4、拷贝控制 private
通过将其拷贝构造函数和拷贝赋值运算符声明为private 的来阻止拷贝:
class HomeForSale{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
拷贝构造函数和拷贝赋值运算符声明为 private,用户代码将不能拷贝这个类型对象,但成员函数和友元可以拷贝对象,为了阻止友元和成员进行拷贝,我们只声明而不定义他们,也做到了即使是成员函数和友元也无法进行调用。
试图访问拷贝对象的用户代码在编译阶段是标记为错误;
成员函数或友元函数中的拷贝操作将会导致链接(访问一个未定义的成员) 时错误。
example:定义一个类,它包含雇员姓名和唯一的雇员证件号。
#include<iostream>
#include<string>
using namespace std;
class employee
{
public:
employee() { mysn = sn++; }
employee(const string&s) { name = s; mysn = sn++; }
//必须定义,自己的拷贝和赋值,如果使用合成的版本,将简单的复制,使得证件号相同
employee(employee&e) { name = e.name; mysn = sn++; }
employee& operator=(employee&e) { name = e.name; mysn = sn++; return *this; }
const string&get_name() { return name; }
int get_mysn() { return mysn; }
private:
static int sn ;
string name;
int mysn;
};
int employee::sn = 0;
void f(employee&s)
{
cout << s.get_name() << ":" << s.get_mysn() << endl;
}
int main()
{
employee a("毛"), b = a, c;
c = b;
f(a); f(b); f(c);
getchar();
}