1.类型行为
1.1.概述
c++类型需要把握的关键行为是:类型构造,类型析构,类型赋值。
以下是一个例子,后续基于此例子具体分析上述行为。
class Norm1 { public: Norm1() { printf("Norm1()\n"); } Norm1(char i):m_i(i) { printf("Norm1(int)_%c\n", m_i); } Norm1(const Norm1 &a):m_i(a.m_i){ printf("Norm1(const Norm1&)_%c\n", m_i); } Norm1(Norm1&& a):m_i(a.m_i) { printf("Norm1(Norm1&&)_%c\n", m_i); } Norm1& operator=(const Norm1& a) { m_i = a.m_i; printf("Norm1=(const Norm1&)_%c\n", m_i); return *this; } Norm1& operator=(Norm1&& a) { m_i = a.m_i; printf("Norm1=(Norm1&&)_%c\n", m_i); return *this; } ~Norm1() { printf("~Norm1()_%c\n", m_i); } private: char m_i='d'; }; class Norm2 { public: Norm2() { printf("Norm2()\n"); } Norm2(char i):m_i(i) { printf("Norm2(int)_%c\n", m_i); } Norm2(const Norm2 &a):m_i(a.m_i){ printf("Norm2(const Norm2&)_%c\n", m_i); } Norm2(Norm2&& a):m_i(a.m_i) { printf("Norm2(Norm2&&)_%c\n", m_i); } Norm2& operator=(const Norm2& a) { m_i = a.m_i; printf("Norm2=(const Norm2&)_%c\n", m_i); return *this; } Norm2& operator=(Norm2&& a) { m_i = a.m_i; printf("Norm2=(Norm2&&)_%c\n", m_i); return *this; } ~Norm2() { printf("~Norm2()_%c\n", m_i); } private: char m_i='d'; }; class Base { public: Base():m_n2('b'),m_n1('b') { printf("Base()\n"); } Base(char i):m_n2(i),m_n1(i) { printf("Base(int)\n"); } Base(const Base &a):m_n2(a.m_n2), m_n1(a.m_n1){ printf("Base(const Base&)\n"); } Base(Base&& a):m_n2(std::move(a.m_n2)),m_n1(std::move(a.m_n1)) { printf("Base(Base&&)\n"); } Base& operator=(const Base& a) { m_n1 = a.m_n1; m_n2 = a.m_n2; printf("Base=(const Base&)\n"); return *this; } Base& operator=(Base&& a) { m_n1 = std::move(a.m_n1); m_n2 = std::move(a.m_n2); printf("Base=(Base&&)\n"); return *this; } ~Base() { printf("~Base()\n"); } private: Norm1 m_n1; Norm2 m_n2; }; class A : public Base { public: A():Base('b'),m_n1('a'),m_n2('a') { printf("A()\n"); } A(char i): Base(i),m_n1(i),m_n2(i) { printf("A(int)\n"); } A(const A &a):Base(a),m_n1(a.m_n1),m_n2(a.m_n2){ printf("A(const A&)\n"); } A(A&& a):Base(a),m_n1(std::move(a.m_n1)),m_n2(std::move(a.m_n2)) { printf("A(A&&)\n"); } A& operator=(const A& a) { Base::operator=(a); m_n1 = a.m_n1; m_n2 = a.m_n2; printf("A=(const A&)\n"); return *this; } A& operator=(A&& a) { Base::operator=(std::move(a)); m_n1 = std::move(a.m_n1); m_n2 = std::move(a.m_n2); printf("A=(A&&)\n"); return *this; } ~A() { printf("~A()\n"); } private: Norm1 m_n1; Norm2 m_n2; };1.2.关于类的构造
1.2.1.构造函数执行顺序
#include "test.h" int main() { A a1; return 0; }上述代码编译运行后的输出:
这是因为对任意一个c++类型A,其构造顺序为:
- 基类构造。
- 成员变量按类内出现顺序构造。
- 类自身的构造函数体。
说明:
- 上述定义一个递归式的定义。
- 构造函数的构造列表中可以显式指定如何执行基类构造,成员字段构造。未指定时,对应项采用默认构造。
1.2.2.默认构造
定义
不需要传递任何参数即可执行的构造函数称为默认构造。
class A1 { public: A1(){} }; class A2 { public: A2(int i = 0){} };上述A1()算默认构造,A2(int i = 0)也算默认构造。因为这些构造函数可以不用传递任何实参即可执行。如A1 a1;A2 a2;将分别执行上述构造函数。
合成版本(默认构造函数)
当一个c++类型定义中用户没有自定义任何构造函数时,编译器默认为其提供一个默认构造函数。
#include <iostream> class A { public: A(const A&) { printf("A(const A&)\n"); } }; int main() { A a;// err,因为已经存在自定义构造函数,无合成版本 return 0; }默认构造函数可行的条件
- 基类支持默认构造(若基类默认构造用private限定,派生类将无权限调用,会阻止默认构造的合成。)。
- 类的所有成员变量支持默认构造。
#include <iostream> class T { public: T(int){} }; class A { private: T t; }; int main() { // err,因为类的成员t对A来说不支持默认构造 A a; return 0; }合成的默认构造的行为
- 基类默认构造。
- 成员变量按类内出现顺序执行其默认构造.
- 类自身的构造函数体(合成下函数体为空).
上述定义是一个递归式的定义。
显式请求合成版本
当用户自定义了构造函数,而我们依然希望编译器提供合成版本默认构造时可以用下述方式
#include <iostream> class A { public: A(int i){ printf("A(int)\n"); } A()=default; }; int main() { A a; return 0; }显示请求只有在满足默认构造可行条件下才能编译通过。
1.2.3.拷贝构造
拷贝构造属于构造的范畴。由于使用比较频繁,故单独拿出来再说一遍。
拷贝构造执行顺序
拷贝构造依然属于构造,所以遵循构造执行顺序。
什么样的构造是拷贝构造
只需要传递类型自身一个实例即可执行的构造函数称为拷贝构造。
class A1 { public: A1(const A1& a){} A1() = default; }; class A2 { public: A2(const A2& a, int i = 0){} A2() = default; };上述A1(const A1& a)算拷贝构造,A2(const A2& a, int i = 0)也算拷贝构造。因为这些构造函数只需要传递类型自身一个实例即可执行。如A1 a1;A2 a11(a1);A2 a2;A2 a22(a2);中A2 a11(a1)和A2 a22(a2)将分别执行上述拷贝构造。
因为拷贝构造也属于构造函数,所以定义了拷贝构造下,若希望提供合成版本默认构造函数,必须采用A1()=default这样的方式显式请求。
合成版本拷贝构造–to do
a. 对于拷贝构造,不同于默认构造。
默认构造是只要存在任何构造函数,就不会提供编译器合成的默认构造版本。
拷贝构造,允许存在其他构造函数,只要用户没有定义只接收自身实例即可执行的构造函数,编译器就会提供合成的拷贝构造。
b.合成版本行为表现
#include "test.h" class B : public Base { public: private: Norm1 m_n1; Norm2 m_n2; }; int main() { B b1; B b2(b1); return 0; }
上述输出中红色对于合成版本默认构造。蓝色对应合成版本拷贝构造。
不过此时,对基类,执行的是基类的拷贝构造。对成员变量,执行的也是其拷贝构造。类自身的拷贝构造函数体是空的。需要注意合并拷贝构造和显式定义的拷贝构造的区别.显式定义下,基类,成员如何拷贝依赖构造函数初始值列表中的指定,未显式下将采用类型的默认构造,而非拷贝构造.
c.合成的拷贝构造函数可行的前提是:
(1). 基类支持拷贝构造
(2). 类的所有成员变量支持拷贝构造
#include <iostream> class T { public: T(){} private: T(T& t){} }; class A { public: private: T t; }; int main() { A a; A a2(a);// err,因为A的成员t不支持拷贝构造。 return 0; }(3).显式请求合成版本
拷贝构造严格来说没有显式请求的必要,因为只要用户没有通过只接收类型自身实例即可执行的构造函数,编译器总是会提供合成的拷贝构造。
4.关于类的赋值
4.1.赋值顺序
int main() { A a1; A a2; a2 = a1; return 0; }
上述紫红色的1,2,3对应a2=a1;
值得注意的是,赋值运算符不像构造函数,析构函数。
像构造函数无论如何总是会对基类执行构造,对类的成员执行构造。类的定义者只能通过构造初始化列表决定采用那个版本的基类构造,成员构造。但基类构造,成员构造作为两个执行阶段无论针对合成版本构造函数,还是用户自定义的构造函数总是存在的。
对赋值,如果用户自定义了赋值运算符,则,基类部分赋值,成员赋值全部依赖自定义函数体的实现。
上述紫红色包含1,2,3的原因是我们提供的自定义赋值运算符里面分别处理了基类赋值,成员赋值。
如果将A的赋值运算符修改为:
A&operator=(constA&a){printf("A=(const A&)\n");return*this;}再次执行
intmain(){A a1;A a2;a2=a1;return0;}输出将为:
此时不会对基类,成员执行赋值处理.
4.2.默认赋值运算符
(1).定义
class A1 { public: A1& operator=(const A1& a) { ... } };作为赋值运算符:
a. 返回值必须是类型自身的引用类型。
b. 只能接收一个形参。
c. 传递类型自身实例时,可以匹配到形参。所以下述,也是合法的自定义赋值运算符。
class A1 { public: A1& operator=(A1 a) { ... } };(2).合成版本
a.何时合成
a.1. 用户没有自定义赋值运算符时.
a.2. 类型没有定义移动拷贝构造函数.
a.3. 类的成员,基类的赋值运算符存在.
b.合成版本行为表现
#include "test.h" class B : public A { private: Norm1 n1; }; int main() { B b1; B b2 = b1; return 0; }
上述紫红色1,2对应B的合成版本赋值运算符。
合成的赋值运算符:
(1). 通过基类赋值运算符完成基类赋值。
(2). 对每个成员执行其赋值运算符完成成员赋值。
c.合成版本可行的前提是:
(1). 基类支持赋值运算符.
(2). 类的所有成员变量支持赋值运算符.
(3). 类型不含移动拷贝构造函数(实际验证,类型包含移动拷贝构造时,无法合成赋值运算符).
#include <iostream> class T { public: T(){} private: T& operator=(T&){} }; class A { public: private: T t; }; int main() { A a1; A a2; a2 = a1;// err,因为A的成员t不支持赋值运算符(t的赋值运算符对A不可见) return 0; }(3).显式请求合成版本
假设类型是A.
class A{public:A&operator=(constA&)=default;A&operator=(A&&)=default;};4.关于类的析构
4.1.析构顺序
#include "test.h" int main() { A a; return 0; }
上述蓝色1,2,3对应A a;中实例a的析构过程。
析构函数执行顺序为:
(1).类自身的析构函数体
(2).成员变量按类内出现顺序反向析构
(3).基类析构
上述定义一个递归式的定义。
不像构造函数,析构函数没有初始值列表这样的东西。每个类型的析构函数也不存在多个版本。
4.2.合成版本
a.何时合成
只要用户没有自定义析构函数,编译器就会合成析构函数。
b.合成版本行为表现
(1). 类自身的析构函数体。合成版本函数体为空。
(2). 成员变量按类内出现顺序反向析构
(3). 基类析构
上述定义一个递归式的定义。
c.合成版本可行前提
(1). 类的基类支持析构
(2). 类的成员支持析构
4.3.显式请求合成版本
析构函数严格来说没有显式请求的必要,因为只要用户没有定义析构函数,编译器总是会提供合成的析构函数。
补充
构造
c++中考虑多重继承,虚拟继承,各类情况。一个类型的构造函数经过类型自身定义和编译器处理后最终会以以下形态展开:
构造行为:
- 若继承体系上存在虚基类,虚基类按从左到右,从深到浅顺序被逐个构造
- 所有类直接继承的非虚基类按声明顺序被构造
- 完成基类构造后,若类存在虚函数,会正确设置虚表指针
- 完成基类构造后,若类直接继承了虚基类,也会正确设置指向虚基类指针
- 成员按声明顺序构造
- 执行类的构造函数体
注意点:
- 在构造过程中执行每个类型的构造函数时,this指针均会被调节保证指向类对象中此子对象起始位置。
- 构造过程,析构过程执行虚函数调用不会产生多态行为。
从意义上讲,因为构造顺序中先构造基类,基类构造时派生部分处于未初始化状态,禁止多态是安全保护。析构顺序中,基类析构时,派生部分由于已被析构处理,禁止多态也是安全保护。 - 继承体系中特定虚基类,无论出现多少次,由于整个对象只有一个虚基类子对象,其构造只会在一开始执行一次。
析构
析构行为:
- 执行类的析构函数体
- 成员函数按声明相反顺序析构
- 基类析构(顺序和构造相反)
- 虚基类析构(顺序和构造相反)