1、拷贝控制操作
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
2、只要我们显示的定义了一个构造函数,编译器就不会替我们再定义任何形式的构造函数
class A
{
public:
A(int a):x(a){}
void set(int a) { x = a; }
int get() const { return x; }
private:
int x;
};
void test()
{
A a; //错误 没有默认构造函数
A a2(3); //正确
}
但是拷贝构造不同,即使我们定义了其他形式的拷贝构造,编译器也会为我们合成一个拷贝构造
2、直接初始化与拷贝初始化
string dots(10, '.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string s3 = "hello"; //拷贝初始化
string s4 = string(3, 4); //拷贝初始化
当使用直接初始化时,实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
如果一个类有移动构造的话,拷贝初始化有时会使用移动构造而非拷贝构造。
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生
- 将一个对象作为实参传递给一个非引用类型的实参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
当我们初始化标准库容器或调用其insert或push成员时,容器会对其元素进行拷贝初始化,与之相对,用emplace成员创建的元素都进行直接初始化。
3、析构的顺序
构造函数是先按照成员在类中出现的顺序对他们进行初始化,然后执行函数体。析构正好相反,先执行函数体,再销毁成员。
4、如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。我是这么理解的。需要自定义的析构函数,说明你需要手动释放一些东西,而这些东西一般都是动态分配的,默认的拷贝构造和拷贝赋值运算符并不会在发生拷贝的时候进行动态分配。所以第一句话成立。
5、使用=default来显示要求编译器生成合成的版本
class A
{
public:
A() = default;
A(const A&) = default;
A& operator=(const A&) = default;
~A() = default;
};
6、使用=delete将拷贝构造和拷贝赋值运算符定义为删除的函数来阻止拷贝。可以将=delete用于析构函数,但是删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时对象。虽然我们不能定义这种类型的变量或成员,但可以动态分配这种类型的对象,但是不能释放这些对象
class A
{
public:
A() = default;
~A() = delete;
};
void test()
{
A a; //错误
A* a = new A(); //正确
delete a;//错误
}
7、如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
8、声明但不定义一个成员函数是合法的。但试图访问一个未定义的成员将导致连接失败。
9、定义一个使用引用计数的类
class HasPtr2
{
public:
HasPtr2(const std::string& s = std::string()):
ps(new std::string(s)), i(0), use(new std::size_t(1)){}
HasPtr2(const HasPtr2& other) :
ps(new std::string(*other.ps)), i(other.i) { ++*use; }
HasPtr2& operator=(const HasPtr2& other);
~HasPtr2();
private:
std::string* ps;
int i;
std::size_t* use;
};
HasPtr2& HasPtr2::operator=(const HasPtr2& other)
{
++*other.use;
if (--*use == 0)
{
delete ps;
delete use;
}
ps = other.ps;
i = other.i;
use = other.use;
return *this;
}
HasPtr2::~HasPtr2()
{
if (--*use == 0)
{
delete ps;
delete use;
}
}
10、拷贝并交换技术
定义了swap的类通常用swap来定义它们的赋值运算符
class HasPtr
{
public:
friend void swap(HasPtr&, HasPtr&);
HasPtr(const std::string &s = std::string()):ps(new std::string(s)), i(0){}
HasPtr(const HasPtr& other):ps(new std::string(*other.ps)), i(other.i){}
HasPtr& operator=(const HasPtr& other)
{
if (this != &other)
{
//为处理自赋值情况和new失败情况 先new 再delete
auto tmpps = new std::string(*other.ps);
delete ps;
ps = tmpps;
i = other.i;
}
return *this;
}
~HasPtr()
{
delete ps;
}
private:
std::string* ps;
int i;
};
inline void swap(HasPtr& lhs, HasPtr& rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}
再用swap来实现operator=
HasPtr& HasPtr::operator=(HasPtr rhs)
{
swap(*this, rhs);
return *this;
}
这个技术的有趣之处是它自动处理了自赋值情况且天然是异常安全的。通过值传递,在改变左侧运算对象之前拷贝了右侧对象保证自赋值的正确,然后交换对象元素,有意思的是交换后原对象元素给到了临时对象rhs,而在函数结束时这个临时对象会调用析构,销毁元素,释放内存。
11、StrVec
class StrVec
{
public:
StrVec():element(nullptr), first_free(nullptr), cap(nullptr){}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
~StrVec();
void push_back(const std::string&);
std::size_t size()const { return first_free - element; }
std::size_t capacity()const { return cap - element; }
std::string* begin()const { return element; }
std::string* end()const { return first_free; }
private:
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* element;
std::string* first_free;
std::string* cap;
};
#include "StrVec.h"
StrVec::StrVec(const StrVec& sv)
{
auto newdata = alloc_n_copy(sv.begin(), sv.end());
element = newdata.first;
first_free = cap = newdata.second;
}
StrVec& StrVec::operator=(const StrVec& sv)
{
auto newdata = alloc_n_copy(sv.begin(), sv.end());
free();
element = newdata.first;
first_free = cap = newdata.second;
return *this;
}
StrVec::~StrVec()
{
free();
}
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* begin, const std::string* end)
{
auto data = alloc.allocate(end - begin);
//uninitialized_copy 作用于未初始化的空间,
//具体作用是将 [begin,end)内的元素拷贝到 [data,data+(begin−end))
//返回一个指针 指向最后一个构造元素之后的位置
return { data, std::uninitialized_copy(begin, end, data) };
}
void StrVec::free()
{
if (!element)
{
return;
}
for (auto p = first_free; p != element;)
alloc.destroy(--p);
alloc.deallocate(element, cap - element);
}
void StrVec::reallocate()
{
auto newcap = size() ? 2 * size() : 1;
std::string* newdata = alloc.allocate(newcap);
auto dest = newdata;
auto elem = element;
for (std::size_t i=0; i != size(); ++i)
{
alloc.construct(dest++, std::move(*elem++));
}
free();
element = newdata;
first_free = dest;
cap = element + newcap;
}
12、对象移动
在旧版本的标准库中,容器所保存的类必须是可拷贝的,新标准下可以用容器保存不可拷贝的类型,只要可移动就可以。标准库容器、string、shared_ptr既支持拷贝也支持移动,io类型和unique_ptr支持移动,但不支持拷贝。
13、右值引用
新标准引入了右值引用:必须绑定到右值的引用,通过&&获得。右值引用有个重要性质——只能绑定到一个将要销毁的对象。
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符都是返回左值的表达式的例子,我们可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算数、关系、位以及后置递增/递减运算符的,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
int i = 5;
int& r = i;
int&& rr = i; //错误 不能将一个右值引用绑定到一个左值上
int& r2 = i * 4; // 错误 i*4是个右值
const int& r3 = i * 4; //正确 可以将一个const的引用绑定到一个右值上
int&& rr2 = i * 4; // 正确 将rr2 绑定到右值
int&& rr3 = rr2; //错误 虽然rr2绑定到了右值上 但rr2本身是个变量 变量是左值
左值有持久的状态,而右值要么是字面常量 要么是表达式在求值过程中创建的临时对象
14、std::move 将一个左值转为右值引用
int&& rr = 42;
int&& rr2 = std::move(rr); //正确
在使用std::move移动原对象后,可以对原对象再赋新值或销毁,但不能使用它的值,因为它的原本的值已经被move了,现在的值是未定义的。
如果一个类型并没有定义移动构造 即使调用std::move也是拷贝的
15、不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
16、与拷贝构造和拷贝赋值运算符不一样,编译器不会为某些类合成移动构造函数和移动赋值运算符。只有当一个类没有定义自己的任何拷贝控制成员且类的所有非static成员都可移动构造或移动赋值时,编译器才会为类合成移动构造函数或移动赋值运算符。而定义了移动构造函数或移动赋值运算符的类必须定为拷贝控制成员,否则拷贝控制成员将被定义为删除的函数。