Last Updated on 2020年4月15日
嵌套类和局部类
- 定义在类X内的类Y称为嵌套类,它就是一个普通类,只不过使用的作用域被限制了.
- 定义在函数内的类Y称为局部类,这种类限制很多,主要用于在语言层面支持Lambda,实践中基本不用.
struct和class的内存布局
- C++的struct和class实例大小不会为0,即便它内部没有任何成员函数或成员变量.主要是因为编译器总是需要为实例分配内存,不占内存的实例是不存在的
- 数据成员全为全public,无构造函数,无类内初始值,不使用继承及virtual的类,称为聚合类.聚合类可以通过
{}
进行逐成员初始化 - 如果需要把sturct或class对象通过C风格直接从内存层面管理(如memcpy),那么就必须要求它的内存布局是平凡的(标准的),这写类型称为POD. 标准库中,is_pod
::value可以判断其是否是可以直接操作的 - 如果类内存在多个
public
或private
声明,则编译器可能重排该类的内存布局(移动部分对象),这将导致成员排布和程序员的预期产生差异. - 对齐方法一般是由编译选项或编译指示决定的.
OOP基础特性:
- 如果没有特别说明,就永远只使用
public继承
,private
及protect
继承没有动态绑定和多态的特性,这两种继承并非面向对象设计
中的组件,而是为了实现其他功能的,在C++中应当避免使用. - 继承过程中基类成员的可见性可以通过
using T::name
手动控制.- 例如,基类中有public的
function()
,我们可以在派生类的private中写using Base::function
,那么就不能通过派生类实例访问function了
- 例如,基类中有public的
- 对于派生类,按min的原则判断基类成员的访问权限,例如,基类的public成员经protect继承后,相当于派生类的min{public,protect}=protect成员.
- 从实现上说,const成员函数只约束类实例内存区域不可写(bitwise-constness).例如,一个class有一个
int *p
成员,那么,在const成员函数中,修改p
是不可以的,修改*p
则是可以的.
friend
friend
是编译期的,其作用是将当前类的自定义成员向某个作用域开放访问(撤销private限制),继承得到的成员则不受任何影响- 这个作用域可以是一个函数,或一个类
- 只要该作用域能够访问到类实例,就能访问该实例的数据成员,或使用成员函数.
- 即便该作用域不能访问到类实例,也可以直接使用不含
this
的private成员函数,或者调用private构造函数创建对象.
friend
语句不是函数声明语句.
this
- 从逻辑上看,
this
在形参列表中为(T * const this, ...)
- 我们可以额外为
this
添加&
,&&
及const
const
要求this指向const
对象&
则要求this指向一个左值&&
要求this指向一个右值
- 我们可以额外为
this
的实现方式随编译器ABI而定.
类内基本控制函数
构造函数ClassName(args):{}
- 对于
const
类实例,仅当构造函数的代码区执行完后实例才获得const
属性. - 只有函数调用才能触发隐式类型转换.(重载运算符也是函数调用,也能触发)
- 添加
explicit ClassName(TYPE n)
限定符可以阻止编译器进行隐式转换.
拷贝控制
- 涉及拷贝构造(移动构造),拷贝赋值(移动赋值),析构这三类操作的控制函数被称为copy control,对于旧版本C++,有3个,对于C++11,则有五个
- 在一些场合,编译器可能优化掉拷贝初始化操作. 例如
string str_obj="mystr";
,它实际是string str_obj(string("mystr"))
, 可能会被优化为string str("mystr")
,第二步的拷贝初始化就被绕过了.
= delete(C++11)
- 用于定义”不能被调用”的函数,
=delete
的函数会参与函数重载,当匹配到=delete
的函数时,编译器将报错.例如,对于一个mutex
,它显然是不具备拷贝意义的,我们就可以让class Mutex
的拷贝构造/拷贝移动为=delete
的
=default(C++11)
- 使用
=default
可以强制编译器按默认规则生成对应的函数- 如果编译器无法生成,则生成的版本是
=delete
的.
- 如果编译器无法生成,则生成的版本是
- 在声明处的
=default
将会使编译器生成inline
的版本
编译器的隐式合成行为
- 编译器会自动插入
xxx=default
这样的代码,称之为隐式合成.具体而言,只有默认构造函数T()
和五个拷贝控制函数是可能被隐式合成的.- 有了任意显式构造函数都不会合成
T();
- 有了任意显式拷贝控制都不会合成其余的拷贝控制;(有的编译器不严格执行)
- 注意:即便是
T(xxx) =default
这样的代码,仍被认为是”显式”的
- 有了任意显式构造函数都不会合成
- 如果T存在不能拷贝的成员,则编译器将合成
=delete
版本的拷贝赋值及拷贝初始化. - 只有当所有成员都可以移动时,编译器才会合成移动构造和移动赋值.换言之,只要编译器隐式合成了移动控制函数,就一定是可用的,不会是
=delete
的- “没有合成”与”合成delete的版本”是不同的.
=delete
的版本会参与重载,而未合成的版本则不会。例如a=T(std::move(b))
中,如果没有合成移动构造函数T(T&&)
,那么将使用拷贝构造T(const T&)
,但是如果合成了T(T&&)=delete
版本,则会编译失败。
- “没有合成”与”合成delete的版本”是不同的.
动态绑定与多态
- 返回类型放松:对于
T
内的virtual函数,如果返回类型是T&
或T*
我们可以在override的时将返回类型修改为派生类的T2&
或T2*
- 在构造函数和析构函数中调用虚函数仅会静态绑定.这是为了避免安全问题.例如,对于
class B: public A{virtual void fun();}
,在构造A时需要调用fun()
,若因动态绑定调用了B的版本,由于此时B仍未构造,此时调用B的版本就可能访问未初始化的区域,这是十分危险的. - 虚函数表的典型实现:
- 每一个多态类都有一个大致形如
void* __vptr[N]
的虚函数表. 因此,每一个实例内都附带一个指针,指向该实例对应类型的虚函数表.(非多态类则没有这个表) - 对于类X,它每多一个
多态基类
,头部就会多一个虚函数表指针.这将引起内存偏移等复杂的问题,实用中一般禁用多继承.
- 每一个多态类都有一个大致形如
- 多态调用与普通调用的一个简单逻辑展示如下:
- 当编译器发现
p->fun(args)
是普通调用时,将其翻译为T::fun(p,args)
,本质仍是一个普通调用. - 当编译器发现
p->vfun(args)
是虚函数调用时,就将其翻译为:p->__vptr[ind_of_vfun](p,args)
,所以虚函数的调用依赖实例.
- 当编译器发现