拷贝构造函数:
如果一个构造函数的第一个参数是:自身类类型的引用,(且任何额外参数都有默认值)。
没有拷贝构造函数,或者定义了其他构造函数,则编译器,合成拷贝构造函数。
可使用合成拷贝构造函数,阻止拷贝某类类型的对象。
在合成拷贝构造函数中,类中每个成员的类型决定了它如何拷贝,该调用拷贝构造函数就调用。
直接初始化:要求编译器使用普通的函数匹配。
拷贝初始化:要求编译器,将右侧运算对象,拷贝到正在创建的对象中。
可依靠:拷贝或移动构造函数来完成。
发生情况:
1、将对象传递给非引用形参;2、函数返回一个对象,函数返回类型为非引用。3、用花括号列表初始化一个数组
vector接受单一大小参数的构造函数是,explicit:
虽然编译器可略过,拷贝/移动构造函数,但他们必须是存在且可访问的,也不能是private。
拷贝赋值运算符:
与处理拷贝构造函数一样,如果一个类未定义,则编译器自己合成拷贝赋值运算符。
合成的拷贝赋值运算符,可以用来禁止该类型对象的赋值。
它返回一个指向其左侧运算对象的引用。
析构函数:
不接受参数,不能被重载,给定类,只有唯一一个析构函数。
作用:释放对象在生存期分配的所有资源。在该函数中,首先执行函数体,然后销毁成员,按初始化顺序逆序销毁。
与普通指针不同,智能指针成员,在析构阶段会被自动销毁。
调用时机:变量离开作用域;对象(容器)被销毁,成员(元素)也被销毁;应用delete;临时对象;
析构函数自动运行,无须担心何时释放资源。
未定义,则编译器回定义一个,合成析构函数(可用于,阻止该类型的对象被销毁)。
成员是在,析构函数体之后,隐含的析构阶段中,被销毁的。
三/五法则:
1、若一个类需要析构函数,则几乎肯定,也需要拷贝构造函数和拷贝赋值运算符。
若使用合成的拷贝构造函数和拷贝赋值运算符,有可能析构函数会释放同个指针两次:
2、若一个类需要拷贝构造函数,则几乎,也需要一个拷贝赋值运算符。
使用=default:
显示地要求,编译器生成合成的版本。
若在类内使用=default,合成的函数,将隐式地声明为内联的。
阻止拷贝:
iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。
可定义删除的函数,来阻止拷贝。
=delete必须,出现在函数第一次声明的时候。并可对任何函数指定=delete
析构函数不能是删除的成员。
若析构函数是删除了的,则不能定义该类型变量,但可动态分配这种类型的对象。
合成的拷贝控制成员,也可能是删除的:
类的某个成员的 | 类中会被定义为,删除的函数 |
析构函数(删除或不可访问) | 合成的析构函数、合成的拷贝构造函数、默认构造函数 |
拷贝构造函数(删除或不可访问) | 合成的拷贝构造函数 |
拷贝赋值运算符(删除)。或有一个const\引用成员 | 合成的拷贝赋值运算符 |
有引用\const成员(类内没初始化器,且其类型未显式定义) | 默认构造函数 |
拷贝控制和资源管理:
管理类外资源的类,必须定义拷贝控制成员。
拷贝一个像值的对象时,副本和原对象是完全独立的。(如标准库容器和string类)
拷贝一个像指针的类对象时,副本和原对象使用相同的底层数据。(如shared_ptr)
定义一个行为像值的类:
一个对象赋予它自身,赋值运算符也能正确工作:
HasPtr& HasPtr::operator=(const HasPtr &rhs){
auto newp = new string(*rhs.ps);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}
定义行为像类指针的类:
可使用shared_ptr来管理类中的资源。
若想直接管理资源,则使用引用计数。可保存在动态内存中。
析构函数的定义:
赋值运算符定义:
HasPtr& HasPtr::operator=(const HasPtr &rhs){
++*ths.use;
if(--*use==0){
delete ps;
delete use;
}
ps = ths.ps;
i = ths.i;
use = rhs.use;
return *this;
}
交换操作:
void swap(Foo &lhs,Foo &rhs){
using std::swap;
swap(lhs.h,rhs.h);
}
如果存在类型特定的swap版本,swap调用会与之匹配。
自己编写的swap函数:
ps:swap的存在就是为了优化代码,因此要将函数声明为inline函数。
ps:参数不是一个副本,因此传参时会发生拷贝。拷贝HasPtr操作,会分配一个该对象的string副本。函数销毁时,rhs会释放掉交换出来的string的内存。这个函数也天然地确保,自赋值的安全。
拷贝控制示例:
资源管理,并不是一个类需要定义自己的,拷贝控制成员的唯一原因。
设计示例思路:
1、析构函数和拷贝赋值运算符,都必须从包含一条Message的所有Folder中删除它。
2、拷贝构造函数和拷贝赋值运算符,都要将一个Message添加到给定的一组Folder中。
动态内存管理类:
某些类需要自己进行内存分配,也就是必须定义自己的拷贝控制成员来管理所分配的内存。
class StrVec {
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec &);
StrVec &operator=(const StrVec &);
~StrVec();
void push_back(const std::string &);//拷贝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
private:
static std::allocator<std::string> alloc;
void chk_n_alloc() {
if (size() == capacity())
reallocate();
}
std::pair<std::string *, std::string *> alloc_n_copy(const std::string *, const std::string *);
void free();//销毁元素并释放内存
void reallocate();//获得更多内存,并拷贝已有内存
std::string *elements;//指向数组首元素的指针
std::string *first_free;//指向数组第一个空闲元素的指针
std::string *cap;//指向数组尾后位置的指针
};
void StrVec::push_back(const std::string &s) {
chk_n_alloc();
alloc.construct(first_free++, s);
}
//两个指针分别指向新空间的,开始位置和拷贝的尾后的位置
std::pair<std::string *, std::string *> StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
auto data = alloc.allocate(e - b);
//first:指向分配的内存的开始位置; second:指向最后一个构造元素之后的位置
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free() {
if (elements) {
for (auto p = first_free; p != elements;)
alloc.destroy(--p);//运行string的析构函数
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s) {
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() {
free();
}
StrVec &StrVec::operator=(const StrVec &rhs) {
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
//元素移动完后,string成员不再管理它们曾经指向的内存
void StrVec::reallocate() {
//将分配当前大小两倍的内存空间
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;//指向新数组中,下一个空闲位置
auto elem = elements;//指向旧数组中,下一个元素
for (size_t i = 0; i != size(); ++i) {
//使用标准库函数move,且提供一个using声明
alloc.construct(dest++, std::move(*elem++));
}
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
ps:string具有类值行为,每个string对构成它的所有字符都会保存自己的一份副本。
可以想象,string的移动构造函数,进行了指针的拷贝,而不是字符分配内存空间,然后拷贝字符。
对象移动:
IO类或unique_ptr,都不包含能被共享的资源。这些对象不能拷贝但可移动。
如果对象较大,进行不必要的拷贝代价非常高。
右值引用:必须绑定到右值的引用。
重要特性:只能绑定到一个将要销毁的对象。
对于常规引用,则是左值引用,可将其绑定到,要求转换的表达式、字面常量、返回右值的表达式。
标准库move函数:
可以显式地将一个左值转换为对应的右值引用类型。
move:获得绑定到左值上的右值引用。
可以销毁rr1或给它重新赋值,但不能再使用它的值。
移动构造函数和移动赋值运算符:
ps:移后原对象会被销毁。
需显式地,告诉标准库,移动构造函数可以安全使用,将函数标记为弄except来做到这一点。
移动赋值运算符:
StrVec &StrVec::operator=(StrVec &&rhs) noexcept{
//直接检测自赋值
if(this != &rhs){
free();//释放已有元素
elements = rhs.elements;//从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
ps:先释放左侧运算对象所使用的内存,并接管给定对象的内存。
需主要的细节:
1、在移动操作之后,移后源对象必须保持有效的、可析构的状态,但用户不能对其值进行任何假设。StrVec的移动操作,是通过,将移后源对象,的指针成员,置为nullptr来实现。
2、只当类定义了自己的拷贝成员,且它的所有数据成员都有移动成员,编译器才会为这个类,合成移动成员。
3、合成的移动操作,被定义为删除的函数:
一个Y类型的类成员,它定义了,自己的拷贝构造函数,但未定义自己的移动构造函数。某个类包含Y类型数据成员,则这个类的合成移动操作,被定义为删除的函数。
4、定义了移动操作的成员的类,必须要定义自己的拷贝操作,否则,合成的拷贝操作,将被定义为删除的。
5、如果一个类有,拷贝控制成员,而没有移动控制成员,则其对象是,通过拷贝成员来"移动"的。
6、单一的赋值运算符,实现了拷贝和移动赋值运算符两种功能:
上述,赋值运算符,有一个非引用参数,因此,会发生拷贝初始化。而拷贝初始化,要么使用拷贝构造函数,要么使用移动构造函数。
第一个赋值,拷贝初始化时,使用拷贝构造函数。第二个赋值,拷贝初始化时,则调用移动构造函数。
7、一个类要是定义了,任何一个拷贝操作,则就应该定义所有五个操作。
Message类的移动操作:
ps:从m的folders中,删除掉m,然后在folders中加入,本msg。
移动构造函数:
ps:contents直接调用string的移动构造函数,folders就直接删除m,然后加入,在新地址的Message
移动赋值构造函数:
ps:remove_from_Folders,要销毁左侧运算对象,就得从左侧对象的folders中,移除掉指向本Message的指针。
移动迭代器:
通过改变,给定迭代器的解引用运算符,来适配此迭代器。解引用运算符,会生成一个右值引用。
可调用标准库的make_move_iterator函数,将一个普通迭代器,转换为一个移动迭代器。
ps:对于转换后的输入序列,当解引用时,会生成一个右值引用,意味着,construct将使用,移动构造函数来构造元素。