移动和拷贝的区别:
move操作后移动源不一定具有其原始值;copy操作后两个对象具有相同的值。如果源对象在操作后不再使用,就可以使用移动操作。
构造函数和析构函数执行过程:
1 首先,构造函数调用基类的构造函数
2 然后,调用成员的构造函数
3 执行自身的函数体
析构:反向拆除一个对象
1 执行自身函数体
2 调用其成员的析构函数
3 调用基类的析构函数
virtual析构函数
析构函数可以声明为virtual,对于含有虚函数的类应该这么做。例如:
class Shape{ public: //... virtual void draw(); virtual ~Shape(); }; class Circle{ public: //... void draw(); ~Circle(); //覆盖了~Shape(); //... };
需要virtual析构函数的原因是,如果通过基类提供的接口来操纵一个对象,那么通常也应通过此接口来delete它:
void user(Shape *p) { p->draw(); //... delete p; }
假如Shape的析构函数不是virtual的,则delete在调用恰当的派生类的析构函数时(如:~Circle())就会失败。这种失败会导致被释放对象所拥有的资源(如果有的话)泄露。
- 使用构造函数进行初始化的时候,当定义了一个接受参数的构造函数后,默认构造函数就不存在了。但拷贝构造函数并不消失。
- 引用和const必须被初始化。因此,一个包含这些成员的类不能默认构造,除非程序员提供了类内成员初始化器或者定义了一个默认构造函数来初始化他们。例如:
int glob{9}; struct X{ const int a1{7}; //OK const int a2; //错误:需要自定义构造函数 const int& r{9}; //OK int& r1{glob}; //OK int& r2; //错误:需要自定义构造函数 }; X x; //错误:X没有默认构造函数
一个类什么情况下应该具有默认构造函数?一个头脑简单的技术性答案是“当你将它用作数组等的元素类型时”。
初始化器列表构造函数:
接受单一std::initializer_list参数的构造函数称为初始化器列表构造函数,一个初始化器列表构造函数使用一个{}列表作为其初始化值来构造对象。
如果一个类有多个构造函数,则编译器会使用常规的重载解析规则根据给定参数选择一个正确的构造函数。当选择构造函数时,默认构造函数和初始化器列表构造函数优先。考虑如下代码:
struct X { X(initializer_list<int>); X(); X(int); }; X x0{}; //空列表:优先选择默认构造函数 X x1{ 1 }; //一个整数,优先选择初始化器列表构造函数
具体规则如下:
1 如果默认构造函数或者初始化器列表构造函数都匹配,优先选择默认构造函数
2 如果一个初始化器列表构造函数和一个“普通构造函数”都匹配,优先选择列表初始化器构造函数
可以将接受一个initializer_list<T>参数的函数作为一个序列来访问,即,通过成员函数begin(),end(),size()访问。,但initializer_list不提供下标操作。例如:
void f(initializer_list<int> args) { for (int i = 0; i != args.size(); ++i) cout << args.begin()[i] << " "; }
initializer_list<T>是以传值方式传递的。这是重载解析规则要求的,而不会带来额外开销。因为一个initializer_list<T>对象只是一个小句柄(通常两个字大小),指向一个元素类型为T的数组。上述循环等价于:
void f(initializer_list<int> args) { for (auto p = args.begin(); p != args.end(); ++p) cout << *p << " "; } //或者 void f(initializer_list<int> args) { for(auto x:args) cout << x << " "; }
initializer_list的元素是不可变的,不要考虑修改他们的值。因此,不能对其使用移动构造函数。
成员和基类的初始化:
如果对一个类型而言,初始化的含义与赋值不同,那么对其使用成员初始化器就是必须的。引用成员和const成员必须初始化。但是对大多数类型,可以选择使用初始化器或者使用赋值。对此,我更倾向与使用成员初始化器语法,这能明确表示我正在进行初始化操作,使用初始化器语法(与使用赋值相比)通常还有性能上的优势。
基类初始化器:
派生类的基类的初始化方式与非数据成员相同。即,如果基类要求一个初始化器,我们就必须在构造函数中提供相应的基类初始化器。如果我们希望默认构造,可以显示指出。例如:
class B1 { B1(); }; //具有默认构造函数 class B2 { B2(int); }; //无默认构造函数 struct D1 :B1, B2 { D1(int i) :B1{}, B2{ i } {} }; struct D2:B1,B2 { D2(int i):B2{i}{} //隐式使用B1{} }; struct D1:B1,B2 { D1(int i){} //错误:B2要求一个int初始化器 };
基类的初始化在成员之前,销毁在成员之后。 参考链接:C++派生类中如何初始化基类对象
委托构造函数/转发构造函数:
如果你希望两个构造函数做相同的操作,可以重复代码,也可以定义一个"init() 函数"来执行两者相同的操作。两种解决方案都很常见。例如:
class X { int a; validate(int x) if (0 < x && x <= max) a = x; else throwBad_X(x); public: X(int x) { validate(x); } X() { validate(42); } X(string s) { int x = to<int>(s); validate(x); } //.... };
冗长的代码影响可读性,且容易出错。一种替代方法是用一个构造函数定义顶一个:
class X { int a; public: X(int x) { if (0 < x && x <= max) a = x; else throw Bad_X(x); } X() :X{42} {} X(string s) X<int>(s) {} //.... };
即,使用一个成员风格的初始化器,但用的是类自身的名字(也是构造函数名),它会调用另一个构造函数,作为这个构造过程的一部分。这样的构造函数称为委托构造函数(delegating constructor)或者转发构造函数(forwarding constructor)。
static 成员变量: 一般非const成员变量都要在类外定义。但在static成员变量为整型或者枚举型的const,或者字面值类型的constexpr,且初始化器是一个常亮表达式时,可以在雷内定义:
class Curious{ public: static const int c1 = 7; //OK static int c2 = 11; //错误:非const const int c3 = 13; //OK static const int c4 = sqrt(9); //错误,雷内初始化器不是常量 stastic const float c5 = 7.0; //错误:类内初始化成员不是整型(应使用constexpr而非const) };
拷贝:
类X的拷贝有两种:
1 拷贝构造函数: X(const X&)
2 X& operator = (const X&)
拷贝基类:
从拷贝的目的看,一个基类就是一个成员:为了拷贝派生类的一个对象,必须拷贝基类。例如:
struct B1 { B1(); B1(const B1&) //... }; struct B2 { B2(); B1(const B2&) //... }; struct D:B1,B2 { D(int i) :B1{},B2{i},m1{},m2{2*i}{} D(const D& a):B1{a},B2{a},m1{a.m1},m2{a.m2}{} B1 m1; B2 m2; }; D d{ 1 }; //用int参数构造 D dd{ d }; //拷贝构造
一个virtual基类在 类层次中可能作为多个类的基类。默认拷贝构造函数能正确拷贝它。
拷贝的语义:
拷贝操作必须满足两个准则:
1 等价性。在x = y 之后,对x和y执行相同的操作应该得到相同的结果。
2 独立性。在x = y之后,对x的操作不会隐式的改变y的状态。
大多数与独立性相关的问题都设计包含指针的对象。默认拷贝语义是逐成员拷贝,因此一个默认拷贝操作会拷贝指针成员,但不会拷贝指针指向的对象(if have)。这个问题称为“浅拷贝”,一个完整的方法是拷贝对象完整的状态,称为“深拷贝”。通常,比深拷贝更好的替代方法不是浅拷贝,而是移动操作,它能最小化拷贝量而又不增加复杂性。
一个指向派生类的指针可以隐式转换为指向其共有基类的指针。当这一简单且必要的规则应用于拷贝操作时,就会导致一个容易让人中招的陷阱。考虑如下代码:
struct Base{ int b; Base(const Base&); //... }; struct Derived : Base{ int d; Derived(const Derived&); //... }; void naive(Base* p) { B b2 = *p; //可能切片:调用Base::Base(const Base&) //... } void user() { Derived d; naive (&d); Base bb = d; //切片:调用Base::Base(const Base&) 而不是 Derived::Derived(const Derived) //... }
变量b2和bb包含d的Base部分的副本,即,d.b的副本。成员d.d不会被拷贝。这种现象称为切片。但这通常是一个微妙的错误。如果不希望切片可采用如下两种方法防止这种现象:
【1】 禁止拷贝基类:delete拷贝操作
【2】防止派生类指针转换为基类指针:将基类声明为private或者protected基类
方法【1】会令b2和bb的初始化出现错误;方法【2】会令naive()调用和bb的初始化出现错误。
使用delete删除的函数
我们可以删除一个函数;即,声明一个函数不存在,从而令隐式或者显示的使用它的尝试成为错误。这种机制最明显的应用是消除其他默认函数。例如:防止基类拷贝。
class Base{ //... Base& operator=(const Base&) = delete; //不允许拷贝 Base(const Base&) = delete; Base& operator=(Base&&) = delete; //不允许移动 Base(Base&&) = delete; }; Base x1; Base x2{x1}; //错误:没有拷贝构造函数
移动:
移动构造函数和移动赋值运算符参考其他博客。
编译器是如何知道它什么时候使用移动操作什么时候使用拷贝操作呢?一般情况下必须通过传递右值的引用参数告知编译器。例如:
template<class T> void swap(T& a,T& b) //几乎是完美的swap { T tmp = std::move(a); a = std::move(b); b = std::move(tmp); }
move是一个标准的库函数,它返回其实参的一个右值引用:move(x) 意味着“给我一个x的右值引用”;即,std::move(x)本身并不移动任何东西;他只是允许用户移动x。