一个类有5种特殊的成员函数:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。如果没有定义这些拷贝控制成员,编译器会自动为它定义缺失的操作。
A a;
A b = a;//报错
13.1 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,而且任何额外参数都有默认值(也就是说最少只需一个实参),则此构造函数是拷贝构造函数。
class Foo
{
public:
Foo();
Foo(const Foo&);
//...
};
第一个参数必须是引用类型。此参数几乎总是一个const的引用。
拷贝构造函数通常不应该是explicit的,因为在几种情况下都会被隐式地使用。
如果拷贝构造函数是explicit的,那么
A a;
A b = a;//报错
就会报错
即使我们定义了其他构造函数(不是拷贝构造函数),编译器也会为我们定义一个合成的拷贝构造函数。
合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。(除了static成员)
如果类成员是A类类型,则会调用A类的拷贝构造函数来拷贝成员类。内置成员直接拷贝。数组逐个拷贝。
http://blog.csdn.net/ljianhui/article/details/9245661
string dots(10, '.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string null_book = "9-999-99999-9"; //拷贝初始化
string nines = string(100, '9'); //拷贝初始化
class Foo
{
public:
int x;
/*1*/
Foo(){cout << 1 << endl;};
/*2*/
Foo(const int xx){x = xx;cout << 2 << endl;}
/*3*/
Foo(const Foo&){cout << 3 << endl;};
/*4*/
Foo& operator=(const Foo&){cout << 4 << endl;}
};
int main()
{
Foo f1(0); /*2*/
Foo f2 = 0; /*2和3,但是编译器把3优化掉了所以看不到,假如把2设为explicit,或者把3设为private:,都会报错,说明有一个先用2后3的带类型转化的过程*/
Foo f3 = f1; /*3*/
Foo f4(f1); /*3*/
Foo f5 = Foo(); /*1和3,但是编译器把3优化掉了所以看不到*/
Foo f6; /*1*/
f6 = f1; /*4*/
return 0;
}
直接初始化时,我们要求编译器使用普通的函数匹配,来选择参数最匹配的构造函数。使用构造初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中(还可能要类型转换)。
拷贝初始化首先使用指定构造函数创建一个临时对象,然后后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
传递非引用实参,函数返回非引用类型,花括号列表初始化数组或聚合类时都用到拷贝构造函数。
复制构造函数 不等于 operator=(), 后者是赋值运算符。
拷贝初始化首先使用构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
拷贝构造函数被用来初始化非引用类类型参数,这一特性揭示了为什么拷贝构造函数自己的参数必须是引用类型,如果其参数不是引用类型,则调用永远不会成功(参考不完全类型)。为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们有需要调用拷贝构造函数,如此无限循环(递归)。
vector v1(10); //正确:直接初始化
vector v2 = 10;//错误,接受大小参数的vector构造函数是explicit的
void f(vector v);
f(10); //错误,原因同上
f(vector(10)); //正确,拷贝构造函数
string null_book = "9-999-99999-9";//拷贝初始化
编译器允许将上面的代码,改成下面的
string null_book("9-999-99999-9"); //编译器掠过了拷贝构造函数
虽然编译器掠过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的。(比如说不能是private的)。
拷贝赋值运算符
Sales_data& Sale_sdata::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo;//调用string::operator=();
units_sold = rhs.units_sold; //使用内置的int赋值
revenue = rhs.revenue;//同上
return *this;//返回引用是习惯做法
}
返回引用是习惯做法。
析构函数:
构造函数初始化对象的非static数据成员,以及其他一些工作。析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数不能被重载,一个类只能有一个析构函数。
在构造函数中,成员初始化是在函数体执行之前完成的,并按照他们在类中出现的顺序进行初始化(使用初始化列表才是初始化,函数体中是给成员赋值)。
而在析构函数中,首先执行函数体,然后销毁成员。成员按照初始化的顺序逆序销毁。
通常,析构函数释放对象在生存期分配的所有资源。static不是在某个对象的生存期分配的,是贯穿程序的。
内置类型没有析构函数,销毁内置类型什么都不用做,销毁类类型时,调用其析构函数。
什么时候调用析构函数:
1 变量离开其作用域是被销毁
2 让一个对象被销毁时,其成员被销毁(比如类里面还有一个类,大类对象被销毁时,小类对象也被销毁)
3 容器(无论是标准库容器还是数组被销毁时,其元素被销毁)
4 动态分配内存的对象,当对指向它的指针应用delete运算符时被销毁
5 对于临时对象,当创建它的完整表达式结束时被销毁。A a = 1; 这里面就有一个临时对象。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段被销毁的,在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的。
(先执行函数体,然后再析构)
13.1.4 三/五法则
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数。通常他们作为一个整体出现,只需要一个操作而不需要其他操作的情况是很少见的。
要析构 =》 要delete =》要new =》不能直接copy或直接给指针赋值需要copy构造函数和operator=)。
如果对象里有指针,但使用了合成的copy构造函数和operator=(), 拷贝的时候两个对象里的指针都指向同一个内存,当其中一个对象被销毁的时候,指针会被delete,导致另一个对象的指针指向了一块被释放掉的内存。shared_pointer 可破之。
如果一个类需要自定义析构函数,几乎可以可定它也需要自定义拷贝复制运算符和拷贝构造函数。
有析构函数 ====几乎一定需要====》 有拷贝构造函数和拷贝赋值运算符
有拷贝构造函数====几乎一定需要====》拷贝赋值运算符
有拷贝构造函数和拷贝赋值运算符 =====不一定需要====》析构函数(????)
C++11还定义了移动构造函数和移动赋值函数。
13.1.5 使用=default
class Sales_data
{
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data&);
~Saled_data() = default;
};
Sales_data: Sales_data::operator=(const Sales_data&) = default;
我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员),也就是说普通的成员函数不能用=default。
13.1.6 阻止拷贝
大多数类应该定义默认构造函数、拷贝构造函数、和拷贝赋值运算符,无论是隐式地还是显式地。
iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。
为了阻止拷贝,我们可以定义删除的函数(=delete)。
struct NoCopy
{
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; //阻止赋值
~NoCopy() = default; //使用合成的析构函数
};
试图拷贝对象的用户代码将在编译阶段报错,成员函数或友元函数中的拷贝操作将在链接时报错。
当定义一个类的拷贝构造函数和赋值操作运算符时,我们要确定要让这个类的的行为向一个值还是像一个指针。
行为像一个值:有自己的状态,拷贝时,副本和原对象是相互独立的,改变副本不会影响原对象。
行为像一个指针:意味着状态是共享的,当我们拷贝这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象。反之亦然。
标准库容器和string类的行为像一个值,而不出意外,share_ptr类提供 类似指针的行为。IO类型和unique_ptr不允许拷贝和赋值,因此他们的行为既不像值也不像指针。
通常:类直接拷贝内置类型(不包括指针)成员:这些成员本身就是值,因此通常应该让他们的行为像值一样。我们如何拷贝指针成员决定了类具有类值行为还是类指针行为。
类(似)值版本的HasPtr如下所示:
class HasPtr
{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { };
HasPtr(const HasPtr &p):
ps(new std::string(*p.ps)), i(p.i) { }; //注意new了一个新指针,并用p.ps指向的值初始化,然后两者再无关联了
HasPtr& operator=(const HasPtr &);
~HasPtr() {delete ps;}
private:
std::string *ps;
int i;
};
类值拷贝赋值运算符:
赋值运算符通常组合了析构函数和构造函数的操作。类似拷贝构造函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧对象拷贝数据。但是我们要注意自身赋值给自身的这种情况。
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new std::string(*rhs.ps); //new并拷贝string
delete ps;
ps = newp;
i = rhs.i;
//delete newp; //这里不能delete,否则ps指向的内存会被释放掉
return *this;
}
赋值操作时,先用【右】操作数的指针指向的值初始化【左】操作数的string(这里要用一个临时变量newp),然后再释放掉【左】操作数原来指向的内存,最后【左】操作数的指针赋为newp的值。最后复制一些内置类型并返回*this。
13.2.2 定义行为像指针的类(例如:shared_ptr)
对于行为类似像指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。我们的类仍然需要自己的析构函数来释放接受string参数的构造函数分配的内存。本例中,只有当最后一个指向string的HasPtr销毁时,它才可以释放string
【引用计数】:计算被某内存被指针引用的次数
class HasPtr
{
public:
HasPtr(const std::string& s = std::string()):
ps(new std::string(s)), i(0), use(new std::size_t(1)) {}; //计数器置为1
HasPtr(const HasPtr &p):
ps(p.ps), i(p.i), use(p.use) {++ *use;} //递增计数器
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use; //记录有多少和对象共享*ps的成员。
};
如果计数器变为0,则析构函数释放ps和use指向的内存:
HasPtr::~HasPtr()
{
if(--*use == 0) //递减计数器
{
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use;
if(--*use == 0)//如果这步放在++*rhs.use;的右边的话,自赋值就会把自身(本不应被delete的内存delete掉了)
{
delete ps;
delete use;
}
ps = rhs.ps;
use = rhs.use;
i = rhs.i;
return *this;
}
除了定义拷贝控制函数,管理资源的类通常还定义一个名为swap的函数。对于重排元素的算法,这个很有用。
交换两个HasPtr对象代码可能像下面这样:
HasPtr temp = v1;
v1 = v2;
v2 = temp;
这段代码将v1中原来的string拷贝了两次,理论上,这些拷贝都是不必要的。只需swap交换指针即可。即:
string *temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;
class HasPtr
{
friend void swap(HasPtr&, HasPtr&);
}
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}
swap就是为了优化代码,所以声明为inline,在swap内部逐个交换内置类型,所以使用std::swap.
swap并不是必要的,但是对于分配了资源的类,定义swap可能是一种很重要的优化手段。
void swap(Foo &lhs, Foo &rhs)
{
//假设Foo有类型为HasPtr的类成员
//错误,使用了标准库的swap
std::swap(lhs.h, rhs.h);
}
void swap(Foo &lhs, Foo &rhs)
{
using std::swap;
swap(lhs.h, rhs.h); //使用HasPtr版本的swap
}
先用using std;,再用swap的话,会优先使用HasPtr版本的swap,并不会被隐藏这与模板的匹配优先级有关,详见第16章(p616)以及p706.
在赋值运算符中使用swap,使用了一种名为【拷贝并交换】的技术,将左侧运算对象的一个对象和右侧运算对象的一个对象进行交换。
HasPtr& HasPtr::operator=(HasPtr rhs)
{
swap(*this, rhs);
//rhs里的指针指向了*this里面指针原来指向的位置,函数结束之后自动销毁
return *this;
}
rhs里的指针指向了*this里面指针原来指向的位置,函数结束之后自动销毁
使用拷贝和交换的赋值运算符自动就是【异常安全】的,且正确处理自赋值。
13.4 拷贝控制示例
class Message
{
friend class Folder;
public:
//folders被隐式初始化为空集合
explicit Message(const std::string &str = "");
Message(const Message&);
~Message();
//从给定的Folder中添加/删除本Message
void save(Folder&);
void remove(Folder&);
private:
std::string contents; //包含消息文本
//folders被隐式初始化为空集合
std::set folders; //包含本Message的Folder
//拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
//将本Message添加到指定参数的Folder中
void add_to_Folders(const Message&);
//从folders中的每个Folder中删除本Message
void remove_from_Folders();
};
save和remove成员:
void Message::save(Folder &f)
{
//在Message中添加Folder的指针,然后在Folder中添加Message的指针
folders.insert(&f); //将给定Folder的指针添加到我们的Folder列表中
f.addMsg(this);
}
void Message::remove(Folder &f)
{
folders.erase(&f); //对应上面的insert
f.remMsg(this); //对应上面的addMsg
}
void Message::add_to_Folders(const Message &m) //将*this对象添加到参数Msg所在的所有Folder中。
{
for(auto f : m.folders)
f->addMag(this);
}
void Message::remove_from_Folders()//析构和赋值都会用到这个工具函数
{ for(auto i : folders)
folders.remMsg(this);
}
Message::~Message()
{
remove_from_Folders();
}
Msg类的拷贝构造函数:
Message::Message(const Message &m):contest(m.contest),folders(m.folders) { /* for(auto f : m.folders) //也可以,不过使用工具函数更方便 f->addMsg(this); */ //使用上一个工具函数 add_to_Folders(m); }
Message& operator=(const Message& rhs)
{
remove_from_Folders();
folders = rhs.folders;
add_to_Folders(rhs);
contents = rhs.contents;
return *this;
}
void swap(const Message& lhs, const Message& rhs)
{
//将每个Msg从原来的Folder中删除
for(auto f : lhs.folder)
f.remMsg(&lhs);
for(auto f : rhs.folder)
f.remMsg(&rhs);
//交换Folders
using std::swap;
swap(lhs.content, rhs.content); //使用swap(string&, string&);
swap(lhs.folders, rhs.folders); //使用swap(set&, set&);
//将每个Msg添加到新的Folder中。
for(auto f : lhs.folder)
f.addMsg(&lhs);
for(auto f : rhs.folder)
f.addMsg(&rhs);
}
****************************************//
13.6 对象移动
在某些情况下,对象拷贝后马上就被销毁了,所以移动而非拷贝对象会大幅度提高性能。
使用移动而不是拷贝的另一个原因是源于IO类或unique_ptr这样的类。他们包含不能被共享的资源(指针或IO缓冲)。因此,这些类型不能被拷贝但能被移动。
标准库容器、string和shared_ptr类技能支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值移动
为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue referrece),就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。
右值引用只能绑定到一个将要销毁的对象。
可以绑定到临时对象的引用:左值const引用、右值引用
左值持久,右值短暂。
左值有持久的状态,右值要么是字面常量,要么是表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知:
一个右值引用一旦绑定之后,就成了一个左值。如:
int && rr1 = 666; //正确
int &&rr2 = rr1; //错误,变量rr1是左值。
变量是左值!!!!!不能把右值引用绑定到变量上,即使这个是右值引用类型的变量。
我们可以用标准库中的std::move()来获取【某个左值】的【右值引用】。
int && rr1 = 666; //正确
int &&rr2 = rr1; //错误,变量rr1是左值。
int &&rr3 = std::move(rr2); //调用move之后,除了对rr1赋值和销毁它之外,将不能再使用它。不能对他的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予他新值,但不能使用一个移后源对象的值。就是说给他赋新值之前不能使用它
对move我们不提供using 声明。我们直接调用std::move() 而不是move,这样做可以避免潜在的名字冲突。原因在18.2.3节 p707中解释。
13.6.2 移动构造函数和移动赋值运算符
移动构造函数和移动赋值运算符类似相应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
移动构造函数的第一个参数是该类类型的一个引用(拷贝构造函数也是一样),而且其他参数都必须有默认值(拷贝构造函数也是一样)。
不同于拷贝构造函数的是:这个引用参数在移动构造函数中是一个右值引用。