TOC
Open TOC
- C++ 中 struct 与 class 的区别与比较
- Object Based
- Header 中的防卫式声明
- inline 内联函数
- access level 访问级别
- constructor 构造函数
- const member functions 常量成员函数
- 参数传递:pass by value vs. pass by reference (to const)
- 返回值传递:return by value vs. return by reference (to const)
- friend 友元
- operator overloading (成员函数) this
- operator overloading (非成员函数) 无 this
- temp object (临时对象) typename ()
- Big Three
- 生命期
- 探索 new 操作
- 探索 delete 操作
- 探索创建对象的内存分配情况
- 进一步补充:static
- 进一步补充:namespace
- 进一步补充:cout
- Object Oriented
- 转换函数
- pointer-like classes
- function-like classes
- class template 类模板
- function template 函数模板
- member template 成员模板
- specialization 模版特化
- partial specialization 模版偏特化
- template template parameter 模板模板参数
- C++ 标准库速览
- reference
- Object Model 对象模型
- 关于 new 和 delete
- C++ 2.0
C++ 中 struct 与 class 的区别与比较
- struct 默认访问权限是 public 的,而 class 默认为 private 的
- struct 默认继承关系是 public 的,而 class 默认为 private 的
- class 这个关键字还可用于定义模板参数,就像 typename
在 C 中使用结构体时需要加上 struct,或者对结构体使用 typedef 取别名,而 C++ 可直接使用
Object Based
- Class without pointer member(s)
complex 类
- Class with pointer member(s)
string 类
必须有 copy ctor 和 copy op=
Header 中的防卫式声明
inline 内联函数
在类内部定义的成员函数默认为 inline
inline 仅建议编译器内联
access level 访问级别
- public
- private
- protected
- 派生类的成员函数中可以访问基类的保护成员
constructor 构造函数
default argument
可能引发构造函数 overloading 冲突
initialization list
发生在 assignments 之前
设计模式 - Singleton - 构造函数 private
考虑 delete 关键字
此处不将 static A a
放在类声明里面,是考虑到 lazy load
const member functions 常量成员函数
- 常量对象只能调用常量成员函数
- 非常量对象可以调用非常量成员函数,也可以调用常量成员函数
- const 是函数签名的一部分,可以用于重载
- 当成员函数的 const 和 non-const 版本同时存在
- const object 只会调用 const 版本
- non-const object 只会调用 non-const 版本
参数传递:pass by value vs. pass by reference (to const)
Item 41: 对于那些可移动总是被拷贝的形参使用传值方式
因为引用传递参数和值传递参数的用法相同,所以两个函数的函数签名 (signature) 相同,不能同时存在
返回值传递:return by value vs. return by reference (to const)
传递者无需知道接收者是以 reference 形式接收
与指针对比
- 若返回值类型为 pointer,则必须返回一个 pointer
- 若返回值类型为 reference,可以返回一个 object,也可以返回一个 reference
friend 友元
相同 class 的各个 objects 互为 friends
operator overloading (成员函数) this
隐式 this
operator overloading (非成员函数) 无 this
重载 cout
- 不可能是成员函数,因为 cout 不认识 complex 类
- friend 可选
- return by reference 必选
temp object (临时对象) typename ()
绝不可 return by reference
因为返回的必定是个 local object
Big Three
- 拷贝构造
必须是 pass by reference
如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归
- 拷贝赋值
检测 self assignment
- 析构
生命期
- stack objects
- 其生命在作用域 (scope) 結束之際結束
- static local objects
- 其生命在作用域 (scope) 結束之後仍然存在,直到整個程序結束
- global objects
- 其生命在整個程序結束之後才結束
- 你也可以把它視為一種 static object,其作用域是「整個程序」
- heap objects
- 其生命在它被 deleted 之際結束
探索 new 操作
- 先分配 memory
- 再调用 ctor
探索 delete 操作
- 先调用 dtor
- 再释放 memory
探索创建对象的内存分配情况
array new 一定要搭配 array delete
32 位环境下
注意内存分配的 header 和 footer 为 21h
,代表大小为 32 且已分配
如果不使用 array delete,实际上分配给类本身内存可以被完整回收,但是构造函数中分配给 data 的内存会泄露
如果换成 complex 类,也许就不会发生内存泄漏
进一步补充:static
static 成员变量,类的里面是声明,类的外面才是定义 / 分配内存
调用 static 成员函数的方式有二
- 通过 object 调用
- 通过 class name 调用
进一步补充:namespace
- using directive
- using declaration
进一步补充:cout
虚继承
Object Oriented
Composition
构造与析构顺序
默认调用 default 构造函数
设计模式 - Adapter
queue 和 deque
Delegation (Composition by reference)
设计模式 - Handle / Body or Pointer to Implementation or PIMPL
Inheritance
构造与析构顺序
Inheritance with virtual
- non-virtual 函数 - 你不希望 derived class override 它
- virtual 函数 - 你希望 derived class override 它,且你对它已有默认定义
- pure virtual 函数 - 你希望 derived class 一定要 override 它,你对它没有默认定义
设计模式 - Template Method
Inheritance + Composition
构造与析构顺序
Delegation + Inheritance
设计模式 - Observer
设计模式 - Composite
设计模式 - Prototype
转换函数
conversion function
把类类型转换为其他类型(基本类型、类类型)
在 STL 中的实例 - 设计模式 - 代理模式
non-explicit-one-argument ctor
把其他类型(隐式)转换为类类型
配合 conversion function 可能引发二义性
在 non-explicit-one-argument ctor 或者 conversion function 前面加上 explicit,只能通过显式地进行构造
pointer-like classes
重载运算符 *
和 ->
关于智能指针
注意此处 ->
被消耗后仍然存在,这是语法定义
关于迭代器
function-like classes
仿函数,函数对象
重载运算符 ()
class template 类模板
function template 函数模板
member template 成员模板
这种结构通常用于实现子类到父类的转换
另一个例子是智能指针
specialization 模版特化
上述代码实现针对 char
、int
和 long
这三个数据类型的 hash 使用指定代码,其它数据类型使用默认的通用代码
partial specialization 模版偏特化
- 个数
- 范围
template template parameter 模板模板参数
一个模板的参数是模板类型,如
实例化
可以看到上面这样实例化时,需要指定 int 类型两次,而且这个类型是一样的,能不能实例化直接写成
很显然是可以的,使用模板的模板参数,声明形式类似
上面声明中的第二个模板参数 Cont 是一个类模板,注意声明中要用到关键字 class
在 C++17 之后,模板的模板参数中的 class 也可以替换成 typename
只有类模板可以作为模板参数,这样就可以允许我们在声明 Stack 类模板的时候只指定容器的类型而不去指定容器中元素的类型
一个较为复杂的例子
C++ 标准库速览
-
算法
-
容器
-
迭代器
-
仿函数
reference
编译器其实把 reference 视作一种 pointer
- 引用是被引用对象的一个别名
- 引用一定要有初始化
- object 和其 reference 的大小相同,地址也相同 (假象)
Object Model 对象模型
关于 vptr 和 vtbl
-
vptr - virtual pointer
-
vtbl - virtual table
在讲虚指针和虚表之前,先要知道
- 当子类继承父类时,除了继承数据之外,同时会继承父类的虚函数
- 继承父类的函数,继承的其实是它的调用权,而不是大小
三个条件
- pointer 或 reference
- up-cast
- virtual function
关于 this
一个隐式的 this 参数
对成员函数的调用会添加 this->
,从而产生可能的 Dynamic Binding
关于 Dynamic Binding
- 对于一般的非虚成员函数来说,其在内存中的地址是固定的,编译时只需将函数调用编译成
call
命令即可,这被称为静态绑定 - 对于虚成员函数,调用时根据虚表
vtbl
判断具体调用的实现函数,相当于先把函数调用翻译成(*(p->vptr)[n])(p)
,这被称为动态绑定
关于 new 和 delete
new 和 delete 作为表达式,不可以重载
但是其内部的 operator new 和 operator delete 作为操作符,可以重载
重载全局函数
重载成员函数
placement new
basic_string
使用 new(extra)
扩充申请量
Rep
为引用计数,extra
存放实际的 string 内容
示例
注意 placement delete function 是直接调用不到的东西,当 placement new expression 调用 placement new function,如果构造函数函数构造的时候发生了异常,这个时候要防止内存泄露,那么要清理掉已分配的内存,就需要这个 placement delete function
另外,对于 operator delete 的重载,可以添加可选的 size_t
参数,不过似乎会与 placement delete functions 冲突
C++ 2.0
variadic templates
数量不定的模版参数
实现递归函数,注意边界情况
另外标准库中的 tuple 利用 variadic templates,使用递归继承的方式来实现
nullptr
Item 8: 优先考虑 nullptr 而非 0 和 NULL
uniform initialization
- 当
{}
内的值与变量的类型不匹配时,不允许类型的收缩转换 - 编译器看到
{t1, t2, ... tn}
,便构造一个initializer_list<T>
,其内部引用了array<T, n>
(浅拷贝)- 调用函数如 ctor 时,该 array 内的元素可以被编译器分解逐一传给函数
- 但若不存在对应的 ctor,且存在函数参数为
initializer_list<T>
的 ctor,则作为整体传入
explicit for ctors taking more than one argument
在多个实参的 ctors 上也可以加上关键字 explicit 来禁止做隐式转化
ranged-base for
实际上等价于
default 和 delete
- 默认构造函数:和 C++98 规则相同。仅当类不存在用户声明的构造函数时才自动生成
- 析构函数:基本上和 C++98 相同;稍微不同的是现在析构默认 noexcept,和 C++98 一样,仅当基类析构为虚函数时该类析构才为虚函数
- 拷贝构造函数:和 C++98 运行时行为一样:逐成员拷贝 non-static 数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是 delete 的。当用户声明了拷贝赋值或者析构,自动生成该函数是被废弃的行为
- 拷贝赋值运算符:和 C++98 运行时行为一样:逐成员拷贝赋值 non-static 数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是 delete 的。当用户声明了拷贝构造或者析构,自动生成该函数是被废弃的行为
- 移动构造函数和移动赋值运算符:都对非 static 数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成
换句话说
- 两个拷贝操作是独立的:声明一个不会限制编译器生成另一个。尽管这是被废弃的行为
- 两个移动操作不是相互独立的。如果你声明了其中一个,编译器就不再生成另一个
三五法则
- 如果一个类定义了析构函数,那么您必须同时定义或删除拷贝构造函数和拷贝赋值函数,否则出错
- 如果一个类定义了拷贝构造函数,那么您必须同时定义或删除拷贝赋值函数,否则出错,删除可导致低效
- 如果一个类定义了移动构造函数,那么您必须同时定义或删除移动赋值函数,否则出错,删除可导致低效
- 如果一个类定义了拷贝构造函数或拷贝赋值函数,那么您必须最好同时定义移动构造函数或移动赋值函数,否则低效
Item 11: 优先考虑使用 deleted 函数而非使用未定义的私有声明
using
alias template
在 C++17 以前,下述用法会报错
因为 std::vector
声明为
这里不会自动 deduce 出 Allocator
模板参数
所以需要写成
这便使用了 alias template
在 C++17 之后,第一种写法可以通过
Matching of template template-arguments excludes compatible templates
type alias
noexcept
Item 14: 如果函数不抛出异常请使用 noexcept
noexcept
允许编译器生成更好的目标代码
unwind 调用栈和可能 unwind 调用栈两者对于代码生成有非常大的影响
对于 std::vector
而言,其扩容默认为复制操作,可以提供异常安全保证,即如果在复制元素期间抛出异常,std::vector
状态保持不变
如果将复制操作替换为移动操作,除非知晓移动操作绝不抛异常,这时复制替换为移动就是安全的
C++11 后,默认情况下,内存释放函数和析构函数都是隐式 noexcept
的,如果一个对象的析构函数可能被标准库使用(比如在容器内或者被传给一个算法),析构函数又可能抛异常,那么程序的行为是未定义的
override
要想 override 一个函数,必须满足下列要求
- 基类函数必须是
virtual
- 函数名必须完全一样(除非是析构函数)
- 函数的形参类型必须完全一样
- 函数的常量性必须完全一样
- 函数的返回值和异常说明必须兼容
- 函数的引用限定符必须完全一样
final
- 修饰类,禁止被继承
- 修饰方法,禁止被 override
auto & decltype
auto
需要根据 expr
推导出 ParamType
和 T
auto 类型推导即为模板类型推导,例如
此处 ParamType
即为 T&
ParamType
是指针或者引用,但不是万能引用
若 expr
是个引用类型,先将引用部分忽略
对 expr
的类型和 ParamType
的类型执行模式匹配,来决定 T
的类型
总结为
- 常量性保留
- 引用性忽略
ParamType 是万能引用
如果 expr
是个左值,T
和 ParamType
都会被推导为左值引用
在模版类型推导中,
T
唯一被推导为左值引用的情况
如果 expr
是个右值,则按照第一种规则进行推导
ParamType 既非指针也非引用
类似第一种规则 (忽略引用性),但也需要忽略常量性 (和易变性)
区分 auto 和 decltype(auto)
举几个例子,对于
推导规则为 decltype 的规则
对于
推导规则为 auto 的规则 (模板类型推导)
所以会导致下述代码失败
若改成
推导规则为 decltype 的规则
区分 decltype(E) 和 decltype((E))
https://zhuanlan.zhihu.com/p/593957444
在开始值类别之前,我们先回顾一下 decltype
有哪些作用。虽然 decltype
只是一个关键字,但是 decltype(E)
对于不同的 E
是两种完全不同的运算:
- 如果
E
是一个没有加括号的 id-expression (也就是x, s.field, S::field
这样的表达式),那么decltype(E)
返回这个变量、字段或非类型的模板参数在定义时的原本类型,如果定义时有包含左值(lvalue, &)或右值(rvalue, &&)引用,那么decltype(E)
也会包含对应的引用。 - 在其他的任何情况,也包括加了括号的 id-expression (比如
(x), (s.field)
),C++ 会完全隐去E
类型中的引用,使得引用完全不可见。此时decltype(E)
先提取E
的不含引用的类型T
,然后根据E
的三种值类别来确定decltype(E)
的结果- 当
E
是 prvalue,decltype(E)
返回T
- 当
E
是 lvalue,decltype(E)
返回T&
- 当
E
是 xvalue,decltype(E)
返回T&&
- 当
lambda
注意这里的 mutable noexcept -> void
- 三个部分均为 optional
- mutable 代表是否可以修改值捕获的变量
lambda 本质上是一个 functor,所以上述程序等价于
这说明了 id = 42;
的赋值不会影响内部的 id_
rvalue references
右值引用是一种新的引用类型,它可以用来减少不必要的拷贝
当赋值运算的右边是一个右值的话,那么左边的对象可以偷右边对象的资源
一般的赋值运算,实际上是 operator=
,在 C++11 之前,只有 copy op=
必须有语法让我们写出一个专门处理右值的所谓 move assignment 函数
如果一个左值也想作为右值引用来使用的话,可以使用 std::move
,但是必须保证 obj 后续不再使用,否则行为未定义
下面是一个具有 move aware 的 string 类,需要注意这里的移动构造和移动赋值需要加上 noexcept
perfect forwarding
非常重要的一点是要牢记形参永远是左值,即使它的类型是一个右值引用
比如,假设
形参 w
是一个左值,即使传入的实参是一个右值
所以会出现下面的情况
当出现对右值引用的传递时,就会出现问题
需要将 forward 修改为
实际上这里的 T &&
是一个万能引用,既可以绑定到左值,也可以绑定到右值,其实现机制为引用折叠
Item 25: 对于右值引用使用 std::move,对于万能引用使用 std::forward
inline variable
SFINAE
smart pointers
Item 19: 对于共享资源使用 std::shared_ptr
手写 std::shared_ptr