拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。
1.使用=定义变量;
2.将一个对象作为实参传递给一个非引用类型的形参;
3.从一个返回类型为非引用类型的函数返回一个对象;
4.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
在构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化;
在析构函数中,首先执行函数体,然后销毁成员,且成员按照初始化顺序的逆序销毁。
无论何时一个对象被销毁,就会自动调用析构函数:
1.变量在离开作用域时会被销毁;
2.当一个对象被销毁时,其成员被销毁;
3.容器(无论时标准库容器还是数组)被销毁时,其元素被销毁;
4.对于动态分配的对象,当对指向它的指针应用delete
运算符时被销毁;
5.对于临时对象,当创建它的完整表达式结束时被销毁。
本质上,这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
拷贝赋值运算符的深拷贝实现
拷贝赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算符对象的资源,类似拷贝构造函数赋值操作会从右侧运算对象拷贝数据。需要注意操作的顺序,保障可以自赋值。
拷贝赋值运算符的浅拷贝(引用计数)实现
当拷贝一个对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
引用计数的计数器应该保存在动态内存中。
对于类类型变量的交换操作,如果使用标准库版本的swap会进行一次拷贝构造,两次拷贝赋值运算符的调用。
可以自定义swap操作代替标准库的版本从而节省一些额外开支。
调用标准库版本的std::swap
示例:
#pragma once
#include
#include
namespace test_swap
{
class HasPtr{
friend void swap(HasPtr&, HasPtr&);
public:
explicit HasPtr(const std::string& s = std::string(""), int i=0): ps_(new std::string(s)), i_(i){
}
HasPtr(const HasPtr& p): ps_(new std::string(*p.ps_)), i_(p.i_){
std::cout << "Using copy-ctor.\n";
}
HasPtr& operator=(const HasPtr& p){
std::cout << "Using copy-assign operator.\n";
if(this == &p){return *this;}
delete this->ps_;
ps_ = new std::string(*p.ps_);
i_ = p.i_;
return *this;
}
~HasPtr(){
delete ps_;
}
void showContent(){
std::cout << i_ << ", " << *ps_ << std::endl;
}
private:
std::string *ps_;
int i_;
};
inline void swap(HasPtr& lhs, HasPtr& rhs) {
std::cout << "HasPtr, Using self-define swap." << std::endl;
using std::swap;
swap(lhs.ps_, rhs.ps_);
swap(lhs.i_, rhs.i_);
}
class Foo{
friend void swap(Foo&, Foo&);
public:
Foo() = default;
Foo(const std::string &s, int i): hp_(s, i){}
void show(){
hp_.showContent();
}
private:
HasPtr hp_;
};
#if 1
inline void swap(Foo& a, Foo& b) {
std::cout << "Foo, Using std::swap." << std::endl;
std::swap(a.hp_, b.hp_);
}
#endif
#if 0
inline void swap(Foo& a, Foo& b) {
std::cout << "Foo, Using self-define swap." << std::endl;
using std::swap;
swap(a.hp_, b.hp_);
}
#endif
int main()
{
std::cout << "test_swap......." << std::endl;
// HasPtr hp1(std::string("Hello swap1."), 1);
// HasPtr hp2(std::string("Hello swap2."), 2);
// hp1.showContent();
// hp2.showContent();
// std::cout << "+++++++++\n";
// swap(hp1, hp2);
// hp1.showContent();
// hp2.showContent();
//
// std::cout << "+++++++++\n";
Foo foo1(std::string("foo1"), 3);
Foo foo2(std::string("foo2"), 4);
foo1.show();
foo2.show();
swap(foo1, foo2);
foo1.show();
foo2.show();
std::cout << "test_swap pass\n----------------------------" << std::endl;
return 0;
}
}
输出:
test_swap.......
3, foo1
4, foo2
Foo, Using std::swap.
Using copy-ctor.
Using copy-assign operator.
Using copy-assign operator.
4, foo2
3, foo1
test_swap pass
----------------------------
调用自定义版本的std::swap
示例:
test_swap.......
3, foo1
4, foo2
Foo, Using self-define swap.
HasPtr, Using self-define swap.
4, foo2
3, foo1
test_swap pass
----------------------------
右值引用(rvalue reference)是必须绑定到右值的引用,通过&&
而不是&
来获得右值引用。
右值引用只能绑定到一个将要销毁的对象。因此,可以将一个右值引用的资源“移动”到另一个对象中。
左值引用:不能将其绑定到要求转换的表达式、字面常量、返回右值的表达式。
右值引用:有着完全相反的绑定特性,可以将一个右值引用绑定到上述类型的表达式上,但不能将一个右值引用直接绑定到一个左值上。
move
函数int i = 42;
int &r = i; // 正确,r引用i
int &&rr = i; // 错误,不能将右值引用绑定到左值
int &r2 = i * 32; // 错误,i*32是右值
const int &r3 = i * 32; // 正确,将const引用绑定到右值
int &&rr2 = i * 42; // 正确,将rr2绑定到乘法结果上
变量是左值,因此不能将一个右值引用直接绑定到变量上,即使该变量是右值引用类型也不行。
其实,区分是左值还是右值,就看能不能取其地址即可。
例如:
int &&rr1 = 43; // 正确,字面常量是右值
int &&rr2 = rr1; // 错误,表达式rr1是左值,左值,左值
虽然不能将一个右值引用直接绑定到一个左值上,但可以通过标准库函数std::move
来获得绑定到左值上的右值引用。
int &&rr1 = 43; // 正确,字面常量是右值
int &&rr3 = std::move(rr1); // 正确
需要注意的是,对于rr1
,除了赋值、销毁之外不能再使用它,因为再调用std::move
之后不能对移动后的源对象的值有任何假设。
需要注意的是,在移动操作之后,移动后源对象必须保持有效的(可赋新值、可安全地使用)、可析构的状态,但不能对其值有任何假设。
#include
#include
#include
namespace test_move
{
class Foo{
public:
Foo(const std::string& str=std::string("init value"), int i = -1): str_(new std::string(str)), i_(new int(i)){
std::cout << "ctor called." << std::endl;
}
Foo(const Foo& f): str_(new std::string(*f.str_)), i_(new int(*f.i_)){
std::cout << "copy-ctor called." << std::endl;
}
Foo& operator=(const Foo& f){
std::cout << "copy-assign-operator called." << std::endl;
if(this == &f){
std::cout << "self-assign" << std::endl;
return *this;
}
delete str_;
delete i_;
str_ = new std::string(*f.str_);
i_ = new int(*f.i_);
return *this;
}
// 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
Foo(Foo&& f) noexcept: str_(f.str_), i_(f.i_){
std::cout << "move-ctor called." << std::endl;
f.str_ = nullptr;
f.i_ = nullptr;
}
Foo& operator=(Foo&& f) noexcept{
std::cout << "move-assign-operator called." << std::endl;
// 如果右侧和左侧运算对象是相同的,不需要做任何事情;否则释放左侧运算对象所使用的内存,并接管给定对象的内存
if(this != &f){
delete str_;
delete i_;
str_ = f.str_;
i_ = f.i_;
}
return *this;
}
~Foo(){
std::cout << "dtor called." << std::endl;
delete str_; str_ = nullptr;
delete i_; i_ = nullptr;
}
private:
std::string *str_;
int *i_;
};
auto main() -> int
{
std::cout << "Testing move......" << std::endl;
Foo foo1(std::string("foo1"), 2);
Foo foo2(foo1);
Foo foo3(std::move(foo2));
foo2 = foo1;
foo1 = std::move(foo1);
std::cout << "------------------------------" << std::endl;
return 0;
}
}
输出:
Testing move......
ctor called.
copy-ctor called.
move-ctor called.
copy-assign-operator called.
move-assign-operator called.
------------------------------
dtor called.
dtor called.
dtor called.
与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
同时,如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
#include
#include
#include
namespace test_move
{
class Foo{
public:
Foo(const std::string& str=std::string("init value"), int i = -1): str_(new std::string(str)), i_(new int(i)){
std::cout << "ctor called." << std::endl;
}
Foo(const Foo& f): str_(new std::string(*f.str_)), i_(new int(*f.i_)){
std::cout << "copy-ctor called." << std::endl;
}
Foo& operator=(const Foo& f){
std::cout << "copy-assign-operator called." << std::endl;
if(this == &f){
std::cout << "self-assign" << std::endl;
return *this;
}
delete str_;
delete i_;
str_ = new std::string(*f.str_);
i_ = new int(*f.i_);
return *this;
}
// 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
// Foo(Foo&& f) noexcept: str_(f.str_), i_(f.i_){
// std::cout << "move-ctor called." << std::endl;
//
// f.str_ = nullptr;
// f.i_ = nullptr;
// }
//
// Foo& operator=(Foo&& f) noexcept{
// std::cout << "move-assign-operator called." << std::endl;
//
// // 如果右侧和左侧运算对象是相同的,不需要做任何事情;否则释放左侧运算对象所使用的内存,并接管给定对象的内存
// if(this != &f){
// delete str_;
// delete i_;
//
// str_ = f.str_;
// i_ = f.i_;
// }
//
// return *this;
// }
~Foo(){
std::cout << "dtor called." << std::endl;
delete str_; str_ = nullptr;
delete i_; i_ = nullptr;
}
private:
std::string *str_;
int *i_;
};
auto main() -> int
{
std::cout << "Testing move......" << std::endl;
Foo foo1(std::string("foo1"), 2);
Foo foo2(foo1);
Foo foo3(std::move(foo2)); // 调用拷贝构造
foo2 = foo1;
foo1 = std::move(foo1); // 调用拷贝赋值
std::cout << "------------------------------" << std::endl;
return 0;
}
}
输出:
Testing move......
ctor called.
copy-ctor called.
copy-ctor called.
copy-assign-operator called.
copy-assign-operator called.
self-assign
------------------------------
dtor called.
dtor called.
dtor called.
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有非static数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
struct X {
int i_; // 内置类型可以移动
std::string s_; // string定义了自己的移动操作
};
struct hasX {
X mem_; // X有合成的移动操作
};
X x;
X x2 = std::move(x); // 使用合成的移动构造函数
hasX hx;
hasX hx2 = std::move(hx); // 使用合成的移动构造函数
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数;赋值操作也是类似。
#pragma once
#include
#include
namespace test_move
{
class Foo {
public:
Foo() = default;
Foo(std::string str, int i): str_(new std::string(str)), i_(new int(i)){
std::cout << "ctor with parameter called.\n";
}
Foo(const Foo& f):str_(new std::string(*f.str_)), i_(new int(*f.i_)){
std::cout << "copy-ctor called.\n";
}
Foo& operator=(const Foo& f){
std::cout << "copy-assign called.\n";
if(this != &f){
delete this->str_;
delete this->i_;
str_ = new std::string(*f.str_);
i_ = new int(*f.i_);
}
return *this;
}
Foo(Foo&& f):str_(f.str_), i_(f.i_){
std::cout << "move-ctor called.\n";
f.str_ = nullptr;
f.i_ = nullptr;
}
Foo& operator=(Foo&& f){
std::cout << "move-assign called.\n";
str_ = f.str_;
i_ = f.i_;
f.str_ = nullptr;
f.i_ = nullptr;
return *this;
}
virtual ~Foo(){
std::cout << "dtor called.\n";
delete str_;
delete i_;
str_ = nullptr;
i_ = nullptr;
}
void printFoo(){
if(str_ && i_){
std::cout << *str_ << ", " << *i_ << std::endl;
}
}
private:
std::string *str_;
int *i_;
};
Foo getFoo(){
return Foo(std::string("get"), 0);
}
int main()
{
std::cout << "test_move......." << std::endl;
Foo f1(std::string("f1"), 1), f2(std::string("f2"), 2);
f1.printFoo();
f2.printFoo();
f1 = f2;
f1.printFoo();
f2.printFoo();
f2 = getFoo(); // f2是函数返回的结果,此表达式是右值,移动赋值运算符是精确匹配(Foo&),而拷贝赋值运算符需要进行一次到const的转换
f1.printFoo();
f2.printFoo();
std::cout << "test_move pass\n----------------------------" << std::endl;
return 0;
}
}
输出:
test_move.......
ctor with parameter called.
ctor with parameter called.
f1, 1
f2, 2
copy-assign called.
f2, 2
f2, 2
ctor with parameter called.
move-assign called.
dtor called.
f2, 2
get, 0
test_move pass
----------------------------
dtor called.
dtor called.
如果一个类有一个拷贝构造函数但未定义移动构造函数,此时编译器不会合成移动构造函数。如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使试图通过调用move
来移动它们时也是如此。
#pragma once
#include
#include
namespace test_move
{
class Foo {
public:
Foo() = default;
Foo(std::string str, int i): str_(new std::string(str)), i_(new int(i)){
std::cout << "ctor with parameter called.\n";
}
Foo(const Foo& f):str_(new std::string(*f.str_)), i_(new int(*f.i_)){
std::cout << "copy-ctor called.\n";
}
Foo& operator=(const Foo& f){
std::cout << "copy-assign called.\n";
if(this != &f){
delete this->str_;
delete this->i_;
str_ = new std::string(*f.str_);
i_ = new int(*f.i_);
}
return *this;
}
// Foo(Foo&& f):str_(f.str_), i_(f.i_){
// std::cout << "move-ctor called.\n";
//
// f.str_ = nullptr;
// f.i_ = nullptr;
// }
// Foo& operator=(Foo&& f){
// std::cout << "move-assign called.\n";
//
// str_ = f.str_;
// i_ = f.i_;
//
// f.str_ = nullptr;
// f.i_ = nullptr;
//
// return *this;
// }
virtual ~Foo(){
std::cout << "dtor called.\n";
delete str_;
delete i_;
str_ = nullptr;
i_ = nullptr;
}
void printFoo(){
if(str_ && i_){
std::cout << *str_ << ", " << *i_ << std::endl;
}
}
private:
std::string *str_;
int *i_;
};
Foo getFoo(){
return Foo(std::string("get"), 0);
}
int main()
{
std::cout << "test_move......." << std::endl;
Foo f1(std::string("f1"), 1); // ctor with parameter called.
Foo f2(std::move(f1)); // copy-ctor called.
f1.printFoo();
f2.printFoo();
f2 = std::move(f1); // copy-assign called.
f1.printFoo();
f2.printFoo();
std::cout << "test_move pass\n----------------------------" << std::endl;
return 0;
}
}
输出:
test_move.......
ctor with parameter called.
copy-ctor called.
f1, 1
f1, 1
copy-assign called.
f1, 1
f1, 1
test_move pass
----------------------------
dtor called.
dtor called.
区分移动和拷贝的重载函数通常有一个版本接受一个const T&
,而另一个版本接受一个T&&
。
#pragma once
#include
#include
namespace test_move
{
void getFoo(std::string && str){
std::cout << "rvale-version called: " << str << std::endl;
}
void getFoo(const std::string &str){
std::cout << "lvale-version called: " << str << std::endl;
}
int main()
{
std::cout << "test_move......." << std::endl;
getFoo("0123456789");
std::string str("abcde");
getFoo(str);
std::cout << "test_move pass\n----------------------------" << std::endl;
return 0;
}
}
输出:
test_move.......
rvale-version called: 0123456789
lvale-version called: abcde
test_move pass
----------------------------
通常,不管对象时一个左值还是一个右值都可以在该对象上调用成员函数。
例如可以对两个std::string
相加的结果(右值)进行赋值。
std::string s1("s1"), s2("s2");
auto found = (s1 + s2).find('s');
s1 + s2 = "Amazing!";
可以使用引用限定符(reference qualifier)来强制左侧运算符对象(即this
指向的对象)是一个左值还是右值。引用限定符可以是&
或&&
,分别指出this
可以指向一个左值或右值。
类似const
限定符,引用限定符只能用于(非static
)成员函数,且必须同时出现在函数的声明和定义中。
#pragma once
#include
#include
namespace test_move
{
class Foo{
public:
Foo(int i = 0):i_(i){
std::cout << "ctor called.\n";
}
Foo& operator=(const Foo& f)&;
Foo& operator=(const Foo& f)&&;
Foo& retLvaue(){
return *this;
}
Foo retRvaue(){
return *this;
}
virtual ~Foo() = default;
private:
int i_;
};
Foo& Foo::operator=(const Foo& f)& {
std::cout << "Only assign to lvalue\n";
this->i_ = f.i_;
return *this;
}
Foo& Foo::operator=(const Foo& f)&& {
std::cout << "Only assign to rvalue\n";
this->i_ = f.i_;
return *this;
}
int main()
{
std::cout << "test_move......." << std::endl;
Foo f1(1), f2(2);
f1 = f2; // Only assign to lvalue
f1.retLvaue() = f2; // Only assign to lvalue
f1.retRvaue() = f2; // Only assign to rvalue
f1 = f2.retLvaue(); // Only assign to lvalue
f1 = f2.retRvaue(); // Only assign to lvalue
std::string s1("s1"), s2("s2");
auto found = (s1 + s2).find('s');
s1 + s2 = "Amazing!";
std::cout << "test_move pass\n----------------------------" << std::endl;
return 0;
}
}
输出:
test_move.......
ctor called.
ctor called.
Only assign to lvalue
Only assign to lvalue
Only assign to rvalue
Only assign to lvalue
Only assign to lvalue
test_move pass
----------------------------
一个函数可以同时使用const
和引用限定,在此情况下,引用限定符必须跟随在const
限定符之后。
#pragma once
#include
#include
namespace test_move
{
class Bar{
public:
Bar() = default;
virtual ~Bar() = default;
std::string& retLval() const &;
std::string retLval() const &&;
private:
std::string str_="default val";
};
auto main()-> void{
std::cout << "test_move......." << std::endl;
std::cout << "test_move pass\n----------------------------" << std::endl;
}
}
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
当定义const
成员函数时,可以定义两个版本,唯一的差别是一个版本有const
限定而另一个没有。
引用限定的函数则不一样,如果定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加。
#pragma once
#include
namespace test_move
{
class Foo{
public:
Foo() = default;
virtual ~Foo() = default;
Foo sorted() &&;
//Foo sorted() const; // error: 'test_move::Foo test_move::Foo::sorted() const' cannot be overloaded with 'test_move::Foo test_move::Foo::sorted() &&'
Foo func();
Foo func() const;
};
auto main()-> void{
std::cout << "test_move......." << std::endl;
std::cout << "test_move pass\n----------------------------" << std::endl;
}
}
C++ Primer(第5版)