Last Updated on 2022年9月30日
嵌套类和局部类
- 定义在类X内的类Y称为嵌套类,它就是一个普通类,只不过使用的作用域被限制了.
- 定义在函数内的类Y称为局部类,这种类限制很多,主要用于在语言层面支持Lambda,实践中基本不用.
struct和class的内存布局
- C++的struct和class实例大小不会为0,即便它内部没有任何成员函数或成员变量.主要是因为编译器总是需要为实例分配内存,使得对象能获得有效的地址,且不同对象的地址总应该是不同的.
- 类型布局相关的种类有很多,如POD,Aggragate,naive等,具体我也不是很清楚,总之很麻烦.
- 如果需要把sturct或class对象通过C风格直接从内存层面管理(如memcpy),那么就必须要求它的内存布局是平凡的.
- C++11之后,编译器只保证:
- 同一个权限声明符block内的对象是顺序存储的(可能因对齐而不连续)
- 同一个权限声明符level的不同block相对顺序是确定的
- 编译器允许在满足上面约束的前提下任意重排类的内存布局.
- 多继承时,相当于每个基类对象顺序排布在内存中.
- 例如,若C继承自A和B,那么C的布局整体上看是
[[vptrA,...][vptrB,...]]
,vptrA
指向了TableC_A
,vptrB
指向了TableC_B
.一般来说TableC_A
内不但包含了A
相关的虚函数地址,还包含了B
相关的虚函数, 及自身新定义的虚函数,用于加快查表的效率.TableC_B
内则仅包含了B
相关的虚函数. - 虚函数表的每个entry一般需要保存两个值,一个是函数的入口,另一个是this指针的调整量.
- C可以直接截断变成A的一个实例, 但是当涉及B时, 编译器则可能需要插入指令调整指针值.
- 例如,若C继承自A和B,那么C的布局整体上看是
#include <cstdio>
struct A {
virtual void a() {
printf("%p A::a called \n", this);
}
};
struct B {
virtual void no_override_b() {
printf("%p B::no_override_b called \n", this);
}
virtual void override_b() {
printf("%p B::no_override_b called \n", this);
}
void normal_b() {
printf("%p B::normal_b called \n", this);
}
};
struct C : public A, public B {
void override_b() override {
printf("%p C::override_b called \n", this);
}
void normal_c() {
printf("%p C::normal_c called \n", this);
}
};
int main() {
C *p = new C;
p->no_override_b();
p->override_b();
p->normal_b();
p->normal_c();
printf("p is %p \n",p);
auto p_b = static_cast<B *>(p);
printf("p_b is %p \n",p_b);
p_b->no_override_b();
p_b->override_b();
p_b->normal_b();
}
// 0x55d374db4e78 B::no_override_b called
// 0x55d374db4e70 C::override_b called
// 0x55d374db4e78 B::normal_b called
// 0x55d374db4e70 C::normal_c called
// p is 0x55d374db4e70
// p_b is 0x55d374db4e78
// 0x55d374db4e78 B::no_override_b called
// 0x55d374db4e70 C::override_b called
// 0x55d374db4e78 B::normal_b called
OOP基础特性:
- 如果没有特别说明,就永远只使用
public继承
,private
及protect
继承并非面向对象设计
中的组件,而是为了实现其他功能的,在C++中从不应该使用. - 继承过程中基类成员的可见性可以通过
using T::name
手动控制.- 例如,基类中有public的
function()
,我们可以在派生类的private中写using Base::function
,那么就不能通过派生类实例访问function了
- 例如,基类中有public的
- 从实现上说,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
的版本会参与函数匹配和重载,如果=delete
的版本被匹配到,将导致编译错误.
动态绑定与多态
- 返回类型放松:对于
T
内的virtual函数,如果返回类型是T&
或T*
我们可以在override的时将返回类型修改为派生类的T2&
或T2*
- 在构造函数和析构函数中调用虚函数仅会静态绑定.这是为了避免安全问题.例如,对于
class B: public A{virtual void fun();}
,在构造A时需要调用fun()
,若因动态绑定调用了B的版本,由于此时B仍未构造,此时调用B的版本就可能访问未初始化的区域,这是十分危险的. - 多态调用与普通调用的一个简单逻辑展示如下:
- 当编译器发现
p->fun(args)
是普通调用时,将其翻译为T::fun(p,args)
,本质仍是一个普通调用. - 当编译器发现
p->vfun(args)
是虚函数调用时,就将其翻译为:p->__vptr[ind_of_vfun](p + offset,args)
,所以虚函数的调用依赖实例.
- 当编译器发现