当定义一个类时,我们显示地或隐示地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称
这些操作为拷贝控制操作(copy control)。
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作的默认定义会导致灾难。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。
示例一:
#include <string>
#include <vector>
#include <iostream>
class Company
{
public:
Company();
Company(std::string, int);
Company(const Company& com);
~Company();
Company &operator=(const Company&com);
private:
std::string m_name;
int m_road;
};
Company::Company(){
m_name = "";
m_road = 0;
std::cout << "Default Constructor called." << std::endl;
}
Company::Company(std::string name, int road) :m_name(name), m_road(road){
std::cout << "Parameter Constructor called." << std::endl;
}
Company::~Company(){
std::cout << "Deconstructor called." << std::endl;
}
Company::Company(const Company&com){
m_name = com.m_name;
m_road = com.m_road;
std::cout << "Copy Constructor called." << std::endl;
}
Company &Company::operator=(const Company&com){
m_name = com.m_name;
m_road = com.m_road;
std::cout << "Overload = called." << std::endl;
return *this;
}
int main(){
std::string name = "OmniVision";
int road = 56;
Company acom;
/* 以下两句等价 */
Company bcom(acom); /* 此处调用拷贝构造函数 */
//Company bcom = acom; /* 此处调用拷贝构造函数 */
Company ccom(name,road);
std::cout << "-----------------------------------------" << std::endl;
}
运行的结果是:
示例二:
#include <string>
#include <vector>
#include <iostream>
class Company
{
public:
Company();
Company(std::string, int);
Company(const Company& com);
~Company();
Company &operator=(const Company&com);
private:
std::string m_name;
int m_road;
};
Company::Company(){
m_name = "";
m_road = 0;
std::cout << "Default Constructor called." << std::endl;
}
Company::Company(std::string name, int road) :m_name(name), m_road(road){
std::cout << "Parameter Constructor called." << std::endl;
}
Company::~Company(){
std::cout << "Deconstructor called." << std::endl;
}
Company::Company(const Company&com){
m_name = com.m_name;
m_road = com.m_road;
std::cout << "Copy Constructor called." << std::endl;
}
Company &Company::operator=(const Company&com){
m_name = com.m_name;
m_road = com.m_road;
std::cout << "Overload = called." << std::endl;
return *this;
}
int main(){
std::string name = "OmniVision";
int road = 56;
Company acom;
Company bcom; /* 此处调用默认构造函数 */
bcom = acom; /* 此处调用重载赋值运算符 */
Company ccom(name, road);
std::cout << "-----------------------------------------" << std::endl;
}
运行的结果是:
以上两段代码旨在展示创建对象bcom
时调用函数的差异。
三/五法则
如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然——如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
在类中实现自己的swap()函数
#include <iostream>
#include <string>
class HasPtr
{
public:
HasPtr(std::string *val = new std::string, int i = 0) :m_ps(val), m_i(i){};
HasPtr(const HasPtr& ahp); /* 对m_ps指向的string,每个HasPtr对象都有自己的拷贝 */
HasPtr& operator=(const HasPtr& ahp);
~HasPtr();
friend void swap(HasPtr &, HasPtr&);
int getVal(){ return m_i; }
std::string getStr(){ return *(this->m_ps); }
private:
std::string *m_ps;
int m_i;
};
inline HasPtr& HasPtr::operator=(const HasPtr& ahp){
std::string astr = *ahp.m_ps;
delete this->m_ps; /* 释放旧内存 */
this->m_ps = new std::string(astr); /* 从右侧新创建的对象拷贝数据到本对象 */
this->m_i = ahp.m_i;
return *this; /* 返回本对象 */
}
HasPtr::HasPtr(const HasPtr& ahp){
m_ps = new std::string(*ahp.m_ps);
m_i = ahp.m_i;
}
HasPtr::~HasPtr()
{
delete m_ps;
}
inline void swap(HasPtr &lhs, HasPtr&rhs){
using std::swap;
/* 下面两个swap()是std::swap(),而不是自己定义的友元函数swap() */
swap(lhs.m_i, rhs.m_i); /* 交换int成员 */
swap(lhs.m_ps, rhs.m_ps);/* 交换指针,而不是string数据 */
}
int main(){
HasPtr chp(new std::string("OmniVision"), 3);
HasPtr ahp;
ahp = chp; /* 此处只会调用拷贝赋值运算符,不会调用拷贝构造函数 */
HasPtr bhp(new std::string("Himax"), 2);
/* 交换前的数据 */
std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
std::cout << "------------------------------------------" << std::endl;
/* 交换;此处的swap()是自己定义的友元函数,而不是std::swap() */
swap(ahp, bhp);
/* 交换后的数据 */
std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
}
使用拷贝并交换实现自赋值异常安全的赋值运算符:
上面的代码可修改为(两段代码只有拷贝赋值运算符部分不同,其它地方完全相同):
#include <iostream>
#include <string>
class HasPtr
{
public:
HasPtr(std::string *val = new std::string, int i = 0) :m_ps(val), m_i(i){};
HasPtr(const HasPtr& ahp); /* 对m_ps指向的string,每个HasPtr对象都有自己的拷贝 */
HasPtr& operator=(HasPtr aph);
~HasPtr();
friend void swap(HasPtr &, HasPtr&);
int getVal(){ return m_i; }
std::string getStr(){ return *(this->m_ps); }
private:
std::string *m_ps;
int m_i;
};
HasPtr& HasPtr::operator=(HasPtr aph){
swap(*this, aph);
return *this;
}
HasPtr::HasPtr(const HasPtr& ahp){
m_ps = new std::string(*ahp.m_ps);
m_i = ahp.m_i;
}
HasPtr::~HasPtr()
{
delete m_ps;
}
inline void swap(HasPtr &lhs, HasPtr&rhs){
using std::swap;
/* 下面两个swap()是std::swap(),而不是自己定义的友元函数swap() */
swap(lhs.m_i, rhs.m_i); /* 交换int成员 */
swap(lhs.m_ps, rhs.m_ps);/* 交换指针,而不是string数据 */
}
int main(){
HasPtr chp(new std::string("OmniVision"), 3);
HasPtr ahp;
ahp = chp; /* 此处会先调用拷贝构造函数生成chp的副本(该副本即是拷贝赋值运算符的形参),再调用拷贝赋值运算符用chp的副本给ahp赋值 */
HasPtr bhp(new std::string("Himax"), 2);
/* 交换前的数据 */
std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
std::cout << "------------------------------------------" << std::endl;
/* 交换;此处的swap()是自己定义的友元函数,而不是std::swap() */
swap(ahp, bhp);
/* 交换后的数据 */
std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
}
上面两段代码的输出都是:
拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符示例
#include <iostream>
#include <utility>
#include <string>
class Ref
{
public:
Ref(std::string astr = "", int *i = nullptr) :m_str(astr), m_i(i){}
Ref(const Ref&aref); /* 拷贝构造函数 */
Ref& operator=(const Ref&aref); /* 拷贝赋值运算符 */
Ref(Ref&&aref)noexcept; /* 移动构造函数 */
Ref& operator=(Ref&&aref)noexcept; /* 移动赋值运算符 */
~Ref();
private:
std::string m_str;
int *m_i;
};
/* 拷贝构造函数 */
Ref::Ref(const Ref&aref){
int *i = new int;
this->m_i = i;
memcpy(this->m_i, aref.m_i, 1);
this->m_str = aref.m_str;
}
/* 拷贝赋值运算符 */
Ref& Ref::operator=(const Ref&aref){
/* 申请内存,并将aref中指针的值拷贝到i */
int *i = new int;
memcpy(i, aref.m_i, 1);
/* 由于在释放内存前对aref进行了拷贝,这样可以正确处理自赋值了 */
delete this->m_i;
this->m_i = nullptr;
/* 使用aref进行赋值 */
this->m_i = i;
this->m_str = aref.m_str;
return *this;
}
/* 移动构造函数 */
Ref::Ref(Ref&&aref)noexcept{
std::cout << "移动构造函数" << std::endl;
this->m_str = aref.m_str;
this->m_i = aref.m_i;
/* 将成员m_i置于nullptr的状态,对aref运行析构函数是安全的 */
aref.m_i = nullptr;
}
/* 移动赋值运算符 */
Ref& Ref::operator=(Ref&&aref) noexcept{
std::cout << "移动赋值运算符" << std::endl;
/* 直接检测自赋值 */
if (this != &aref)
{
/* 释放已有元素 */
delete this->m_i;
this->m_i = nullptr;
/* 从aref接管资源 */
this->m_i = aref.m_i;
this->m_str = aref.m_str;
/* 将aref置于可析构状态 */
aref.m_i = nullptr;
}
return *this;
}
Ref::~Ref(){
std::cout << "Destructor called." << std::endl;
delete this->m_i;
this->m_i = nullptr;
}
int main(){
Ref aref;
Ref bref = std::move(aref);
Ref cref;
cref = std::move(aref);
system("pause");
}
右值引用
通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,我们可以自由地将有一个右值引用的资源“移动”到另一个对象中。
我们不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
int i = 42;
int &r = i; // 正确:r引用i
int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误:i * 42是一个右值
const int &r3 = i * 42; // 正确:可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; //正确:将rr2绑定到乘法结果上
返回左值引用给的函数,连同赋值、下标、解引用和前置递增/递减运算丰富,都是返回左值的表达式的例子。可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久;右值短暂
左值具有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,所以:
- 所引用的对象将要被销毁
- 该对象没有其它用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值
变量可以看作是只有一个运算对象而没有运算符的表达式。类似其它任何表达式,变量表达式也有左值、右值属性。变量表达式都是左值。带来一个惊人的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上:
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是左值!
代码示例说明:
#include <vector>
#include <iostream>
#include <algorithm> // std::sort
class Foo
{
public:
Foo() = default;
Foo & operator=(const Foo&)&; // 只能向可修改的左值赋值
Foo &retFoo(); // 返回一个引用;retFoo调用是一个左值
Foo retVal(); // 返回一个值;retVal调用是一个右值
Foo sorted() && ; // 可用于可改变的右值
Foo sorted()const &; // 可用于任何类型的Foo
void setValue(int i) { this->m_vec.push_back(i); }
~Foo() = default;
private:
std::vector<int> m_vec;
};
Foo & Foo::operator=(const Foo & rts) &
{
this->m_vec = rts.m_vec;
return *this;
}
// 返回一个引用;retFoo调用是一个左值
Foo & Foo::retFoo()
{
return *this;
}
// 返回一个值;retVal调用是一个右值
Foo Foo::retVal()
{
return *this;
}
// 针对右值对象使用,因此可以原址排序
Foo Foo::sorted() &&
{
std::sort(m_vec.begin(), m_vec.end());
return *this;
}
// 针对const左值、或左值使用,两种情况都无法进行原址排序
Foo Foo::sorted() const &
{
Foo ret(*this); // 拷贝一个副本
std::sort(ret.m_vec.begin(), ret.m_vec.end());
return ret;
}
int main()
{
Foo afoo;
afoo.setValue(5);
afoo.setValue(3);
afoo.setValue(9);
auto it1 = afoo.retVal().sorted(); // 调用sorted的右值版本,afoo本身的值不变
auto it2 = afoo.retFoo().sorted(); // 调用sorted的左值版本,afoo本身的值不变
// 程序运行的结果是:
// it1: 3 5 9
// it2: 3 5 9
// afoo仍然是5 3 9
// afoo.retVal() = it1; // 此句报错,未定义向右值赋值的赋值运算符
afoo.retFoo() = it1; // 此句正确,定义了向左值赋值的赋值运算符
system("pause");
}