C++ 标准笔记
本文始于 2018/6/27,用于汇聚关于 C++ 的实用知识和碰到的坑,温故而知新。
2022/1/2 【更改】标题改为《C++ 标准笔记》
2022/1/2 【新增】StandardLayout
2022/1/2 【新增】bit field更新预告:
- CRTP 原理,目的和应用
- CRTP 应用之表达式模板
union 共用体
- union可匿名,常在结构定义中。
- 用于节省内存,尤其在嵌入式系统中。
- union的定义形式与struct相同。
enum 枚举
- enum A {a,b,c}; 首项默认为0,默认后项比前项大一。
A被看做一种类型,甚至可以省略;a,c,b被看做常量。
a,b,c可以自动提升为int, 但int不能自动转换为枚举类型,除非强制转换A(1)。
每个enum根据其常量的最大最小值确定其上下限。 - enum创建的常量是静态的, 可以用作静态类成员常量. 运行时所有对象不会包含枚举
- 更强的安全性--类作用域内枚举(C++11)
- enum class name : type {...};
- enum struct name {...};
- class或struct二选一,
:type
可选. - 作用域内枚举不允许隐式地转换为整型
- 默认底层为int
- 调用格式为name::x
- :type 放在枚举名后以指定底层, 否则将随实现而异.
char 字符型
- char类型被输入输出流区别对待。cout << (int*) st << endl;
- char类型数组的初始化被C++区别对待:
1
2
3char s1[5] = "abcd"; 合法,"abcd"被转换为{'a','b','c','d'}, 可供修改
char s2[5] = s1; 错误,即使s1是const。
char* s3 = "abcd"; 警告, "abcd"是常量,不能修改. 参数char s3[]="abcd"亦等价警告. const char []
所定义的字符串被保护,有内存位置隔离,不允许跟踪(但允许查看)地址。- char是否带符号取决于系统。
- wchar_t可以表示系统使用的最大扩展字符集,输入输出用wcin,wcout
- raw r"(a/b\c回车defg*)", 标识符 "( )" 可变为 "*@( )*@" 等等,保持平移对称
<cstring>常用函数
char* strcpy(char* dest, const char* src)
- 以src覆盖dest, 直到遇到src的终止符.返回dest.
char* strncpy(char* dest, const char* src, size_t count)
- 以src的前n位覆盖dest, 若遇到src的终止符则用0填充剩余位.返回dest.
char* strcat(char *dest, char *src)
- 将src复制到dest的末尾.返回dest.
int strcmp(char *str1, char *str2)
- 按字典序比较, 返回-1,0,1.
int stricmp(char *str1, char *str2)
- 按字典序但对大小写不敏感比较.
int strncmp(char *str1, char *str2, size_t count)
- 按字典序比较前n位
int strnicmp(char *str1, char *str2, size_t count)
- 按字典序但对大小写不敏感比较前n位
vector与array 模板类
vector<typename> arr(n_elem);
也可以不指定长度array<typename, n_elem> arr;
定长数组。 等长数组可以直接复制。- 下标可越界,欲防止越界用
arr.at(x)
读入行
cin.getline(arr, arsize)
空行不设置failbitcin.get(arr, arsize)
行读取结束后不丢弃换行符(无法跨越)cin.get()
读取一个任意字符,返回char值cin.get(char)
返回cinstring
类读行getline(cin, str);
fstream 文件流(以fin, fout为例)
- 打开文件:
.open("filename");
- 关闭文件:
.close();
- 检测最后一次读入遇到EOF:
fin.eof();
- 检测最后一次读入遇到类型不匹配(包括EOF):
fin.fail();
- 检测最后一次读入文件损坏、故障:
fin.bad();
- 检测最后一次读入完全正常:
fin.good();
等价于(bool) fin >> value;
- 清空错误标记,准许读入:
.clear();
switch 结构控制
1 | switch(num) |
杂项
exit(EXIT_FAILURE);
: cstdlib, 同操作系统通信的参数值nullptr
: C++11 关键字, 空指针值.
指针
- 应当将空指针写为
nullptr
.delete
和delete []
都可以作用于空指针. - const 指针 将受保护,非const 指针 不能复制其值(除非利用强制类型转换),不允许通过const 指针 修改所指向值。
为了防止欺骗const检查,不允许令const 二级指针指向非const 一级指针
函数指针
- 声明:
typename (*pointer name)(parameter list...);
- 获得某函数的地址:
pointer = function name;
- 使用函数指针:
(*pointer)(parameter list...);
或pointer(parameter list...);
- 举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21原函数:
const double* f1(const double [], int);
指向f1的指针:
const double* (*p1)(const double [], int) = f1; 或 auto p1 = f1;
由f1,f2,f3组成数组://[]优先级高于*
const double* (*ar[3])(const double [], int) = {f1, f2, f3}; 不允许auto
指向ar[3]的指针:
const double* (*(*arp)[3])(const double[], int) = &ar; 或 auto arp = &ar;
还可以用typedef简化声明:
typedef const double* (*p_fun)const(const double [], int);
p_fun ar[3] = {f1, f2, f3}; - 分析方法: 从变量名开始, 往右往左结合, 逐步解释结合体.
- 当遇到一个指针, 总是关心它指向什么类型
- 当遇到一个数组, 总是关心它的元素的类型
- 当遇到一个函数, 总是关心它的返回值类型
参数传递
- 参量(argument)是实参,参数(parameter)是形参。
函数重载(函数多态)
- 同名函数共存要求:函数特征标(函数参数列表)不同。
- 当需要自动类型转换但选项不唯一时,编译不通过。
- 不区分const和非const变量(包括指针)。
- 区分指向const的指针和指向非const的指针。
- 区分指向const的引用和指向非const的引用。(编译器调用最匹配的版本)
- 如果没有右值引用参数版本,右值参量将赋给const引用参量。
引用
- 左值引用
- 非const引用可以引自可修改的左值(防止修改const值和修改临时变量)
- const引用可以引自任何左值、右值(毕竟数值不会变)
- 基类引用可以指向派生类对象
- 指针也能被引用, 例如 int*& x = ptr;
短路运算符
- ||, &&具有短路作用,结果必然时短路,不计算右边的表达式
inline
- 在声明时用或在第一次调用前的定义用都行
- 不能处理递归, 编译器有权拒绝采用.
- 函数默认值必须放在原型声明中, 不能放在和声明分离的定义中.只能从右到左地提供默认值
decltype(expression)
decltype(expression)核对表
- 如果expression是一个不带括号的标识符, 则返回相同类型, 包括const,*等限定符
- 否则,如果expression是一个函数调用(需要提供参数),则返回相同类型
- 否则,如果expression是一个带括号的左值,则返回其引用
- 否则,返回expression的类型(例:右值)
可以利用typedef decltype(expression) somename;简化声明
decltype 用于函数返回类型声明
- 声明返回类型时未知参数列表, 所以需要后置声明
1
2template <typename T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y) // "-> 已知类型" 也行
- 声明返回类型时未知参数列表, 所以需要后置声明
模板
创建模板
1
2template <typename T> //也可以用class代替typename
void swap(T&, T&);显式具体化
1
2template <> void swap(int&, int&);
template <> void swap<int>(int&, int&);为什么需要显示具体化?常规函数不能替代吗?
显式实例化
- 不使用函数就能依照模板生成实例, 常用在库中
template void swap<int>(int&, int&);
- 使用函数时
add<double>((int)a, (double)b);
会强制使用模板.注意引用类型不能隐式类型转换.后续的强制使用仍需加<double>
::变量名
- 表明采用全局变量而非局部变量
储存空间
- 静态储存: 保证初始化为0, 生命周期和程序相同.
- 自动储存(栈内存): 不保证初始化;
- 动态储存(堆内存): 不保证初始化;
储存说明符
- static用于变量 编译阶段尽量初始化, 运行时直接分配空间,程序结束时销毁, 首次遇到时再保证初始化
- 静态外部链接性变量: 直接在非被包括区域定义, 在其他单元中可用extern引用声明(不允许再次初始化)(ODR单定义规则)
- 静态内部链接性变量: 在非被包括区域加static, 限定在本单元内可以访问
- 静态无链接性变量:在被包括区域加static, 限定在本包括区域内可以访问
- static用于函数 覆盖函数的默认外部链接性为内部链接性,必须同时用于原型和定义中.
- thread_local 变量持续性与所属线程的持续性相同, 可与static, extern结合使用(其他声明中不能使用多个储存说明符)
- mutable 使得const对象中的属性能被修改,而不受const限制
- register C++11之前是寄存器变量,不能取地址; 之后是显式指明自动变量(无实际作用,避免旧代码非法)
显式指出语言链接性以帮助链接程序寻找匹配函数:
extern "C" void spiff(int);
cv-限定符
- const全局变量带有隐式static使得变量链接性为内部, 导致多文件同时include一个头文件时不会发生定义冲突 *可以使用extern覆盖隐式static使变量链接性为外部. 在其他单元中仍需用extern来引用它.*
- volatile 提示编译器该变量会在程序之外被修改, 不要进行寄存器缓存优化. 多见于驱动程序.
new运算符
- 原函数:
1
2
3
4void * operator new(std::size_t);
void * operator new[] (std::size_T); // new int[40] --> new(40*sizeof(int))
void operator delete(void *);
void operator delete[] (void *); - new, delete函数可以替换.
- new在堆中查找满足要求的内存块
- 定位new运算符
typename* p = new (MemAddress) typename
- 返回(void *)指定的内存地址
- 允许重载
- 注意非堆内存不可delete
- 若内存中存放了对象,则需要手动调用析构函数,再由MemAddress释放内存,且需注意new []与delete []对应
- 元素的创建与销毁应遵循FILO顺序
名称空间
- 声明区域: 可以声明变量的区域, 例如所在的文件, 代码块.
- 潜在作用域: 从声明点开始到声明区域的结尾. 可能被局部变量隐藏,故称潜在.
- 自定义名称空间:
namespace Somewhat {...}
- 只能在函数外部定义, 允许在另一个名称空间中嵌套
- 因此, 默认情况下声明的名称的链接性为外部(除非它引用了常量)
- 可以在其他合法位置继续添加名称, 提供定义.
using namespace Somewhat;
编译指令- 每个声明区域中都有一条隐式的
using namespace 全局名称空间;
- 若某处使用过
using namespace
编译指令,则其子声明区域也隐式添加这条语句. - 局部变量拥有最高优先权,能隐藏各种using namespace同名变量(因为名称空间都在函数外部定义)
- 每个声明区域中都有一条隐式的
using
声明- 类似于声明了一个局部变量, 在代码块中不允许同级同名.
- 因此使用using声明比使用using编译指令更安全.
- 名称空间可以拥有别名, 用于简化代码: namespace MEF = myth::elements::fire;
- 名称空间可以匿名, 声明之后自动隐式using, 用于避免其他using并替代static全局变量.
- <.h>文件是不支持名称空间的版本.新版一般将函数搬到std中.
- 使用建议: 在大型程序/多单元程序使用
- 少用
using namespace
- 避免在头文件中使用using编译指令,若必须使用,则在所有#include之后使用
- 避免直接声明外部全局变量和静态全局变量,改用已命名的名称空间
- using声明首选用在局部而不是全局
- 少用
类
- 构造函数
- 调用示例:
1
2
3
4
5
6
7
8Classname object; // 调用空参数构造函数
Classname object(); // 警告, 正在声明函数
Classname object(one, two, ...); // 调用对应的构造函数
Classname object = Classname(one, ...); // 有可能先构造临时对象
Classname* p = new Classname(...);
Classname object{...}; Classname object = Classname{...}; // C++11
Classname* p = new Classname{}; // C++11
Classname object = value; // 调用一个参数的构造函数, 可用explicit(修饰构造函数)关闭这种特性 - 重载构造函数中使用的new或new []必须对应,因为析构函数只有一个。
- 初始化列表
- 参数列表后由冒号引出的初始化信息。在此初始化对象将使用复制构造函数,而不是空构造函数加赋值运算符,因此效率更高。
- 初始化的顺序按照成员变量的声明顺序,而与初始化列表顺序无关。
- 一旦进入花括号,成员变量将完成默认的初始化,对象初步创建完成。因此,成员变量中,非静态常量与引用必须由此列表进行初始化。
- 类内初始化可等价于默认的列表初始化。列表初始化会覆盖类内初始化。
- 初始化列表负责调用基类构造函数(基类已在派生类类域中,无需加作用域解析符)
- 初始化列表负责调用成员对象的非默认构造函数
- 默认不能继承构造函数(C++11)
- 调用示例:
- 析构函数
- 只能有一个析构函数, 且参数必须为空
- 注意用delete对应构造函数或其他过程的new
- 若对象由定位new运算符创建,则需要手动调用析构函数,且遵循FILO顺序。
- 必须以delete或delete []对应构造函数中的new或new []。
- 最后会自动调用基类的析构函数
- 应当将析构函数声明为虚函数
- 不能继承析构函数
- 封装是一个编译期的概念
- 编译期不存在实例,编译器仅针对类型做检查
- 可以在类方法中访问同类对象的私有成员
- 同struct : 避免环形构造
- 编译器禁用简单环形定义, 如
struct A { A a; }; // 使用了未完成的定义
- 编译器不能辨别复杂环形定义, 如
struct A { A(){} A* a = new A(); }
- 编译器禁用简单环形定义, 如
- 非静态变量在运行时才会创建, 所以如int arr[MAXN]的MAXN必须是静态量,可以是全局const, 类中static const, enum{MAXN=x}.
- 友元函数
- 在共有部分声明友元函数原型, 也可以紧接定义以设为内联函数.后置的定义无需friend修饰
- 友元函数视为类外函数, 但可以访问类私有成员变量.
- 类的显式二元重载运算符应当用友元, 尽管没有直接修改类私有成员变量
- 重载<< :
std::ostream& operator << (const std::ostream& os, const Classname& obj)
以方便输出. - 友元函数可方便隐式类型转换, 不必苛求由对象发起函数调用.例如cmp("asd", obj),可以对应原型cmp(string, string);
- 可以在派生类友元函数中,强制向上转型并使用基类友元函
数据类型, 否则易有二义性
函数, 避免隐式转换出错
ass_name&)`
始化、按值传递、按值返回、上转型并使用基类友元函数。
- 转换函数
operator typeName()
- 用途:将类转换成基础数据类型
- 必须是成员方法
- 不能指定返回类型
- 不能有参数
- 用于cout时应显式标明类型
- 应当用explicit修饰
- 复制构造函数
Class_name(const Class_name&)
- 在初始化对象时使用(显示的初始化、按值传递、按值返回、编译器生成临时对象)
- 新版本C++可用移动构造函数
- 例:
1
2
3
4
5Class_name a(b);
Class_name* pa = new Class_name(b);
Class_name a = b; // 可能生成临时对象后调用赋值运算符函数,根据实现而异.
Class_name a = Class_name(b); // 同上
按值传递函数调用func(b); // 按值传递也初始化了参数 - 默认的复制构造函数,是在初始化列表中调用所有成员的复制构造函数。默认复制派生类对象的基类部分
- 应注意深度复制, 即妥善处理指针所指向内容的复制
- 赋值运算符函数
- 考虑妥善处理原来已有的,即将被抛弃的数据.
- 考虑自己赋值给自己的情况, 小心赋值前删除了自身数据.
- 记住返回自身引用, 即
return *this
- 不能继承赋值运算符函数
- 不建议设置为虚方法,为了避免同基类的不同派生类互相赋值
- 默认方法的潜在危险
- 某个类在开发初期不需要复制构造函数/赋值运算符重载,但在以后可能需要
- 可能写出符合常理的代码,但由于未覆盖默认方法而运行时异常
- 可能在未察觉的情况下调用了默认方法(用重载加法时按值传递、按值返回将调用复制构造函数)
- 解决办法:
- 不管默认方法是否需要,总是提供正确的代码
- 将空方法设为private,并留下错误信息
- 使用delete(C++11)
[ ]
(取位)运算符: 两个操作数分立于左中括号两侧。只能是成员函数。- const对象
- 只能使用const函数, 若返回引用, 则返回类型为const type&
- 静态类成员函数
- 原型含static, 定义不含
- 和Java不同, 不能通过对象来调用静态成员函数
- 调用格式
Class_name::s_method();
- 静态类成员变量
- 在类中声明, 不可定义
- 在类外定义并分配内存, 可以不初始化
- 静态常量在声明同时定义.
- 派生/继承
class newClass : public baseClase
- 默认继承为private继承。私有继承的向上转型必须显式写出
- 多重继承的向上转型也应显示写出,以防不同基类的方法冲突
- 派生类中一旦定义某方法与基类方法同名,则基类所有该名方法被隐藏。与参数列表(特征标)无关。
- 可用基类名::方法名的途径调用隐藏的方法
- 重新定义继承的方法,应确保与原来的原型完全相同
- 若返回类型为基类引用或指针,则可以修改为派生类的引用或指针(返回类型协变)
- 若基类虚方法被重载了,则应在派生类中重新定义所有的基类版本
- 若派生类构造函数使用了new,则应提供复制构造函数/赋值运算符(含
base::operator=(o);
)/虚析构函数的定义 - 公有继承表达了is-a关系,私有继承/包含表达了has-a关系。通常使用组合包含层次化来建立has-a关系。如果需要访问原有类的保护成员或重新定义虚方法,才使用私有继承。
- 欲调用基类对象,对(*this)强制向上转型(const Base &)即可
- 用using改变继承成员权限:在派生类的public处
using Base::methodName;
(省略using的技术是即将被抛弃的旧技术)
- 虚方法
- 若希望通过基类引用或指针调用派生类方法,则需要将基类方法声明为virtual虚方法(一般也将派生类方法声明为virtual)
- 应当将析构函数定义为虚方法,以确保正确地销毁对象
- 在类中欲调用基类方法(而不是本类方法),需使用作用域解析符
Base::baseMethod();
- 编译器对虚方法的一般实现:把类的所有虚函数的地址制作成表,在对象中添加隐藏的虚函数表指针,在运行时通过指针检索虚函数。
- 与Java不同,派生类虚方法的访问权限允许变严格,但由基类引用或指针的多态效果仍然生效。
- 派生类中一旦定义虚方法,就将隐藏基类所有同名方法,故应在派生类中重新定义所有的基类重载方法
- 可以令虚方法声明结尾处为
=0
,使方法成为纯虚方法。纯虚方法可以不提供定义。含有至少一个纯虚方法的类为抽象类,不能声明对象。
- 访问权限
- private:仅本类和友元函数有权访问。对数据成员的理想访问控制。
- protected:本类和派生类有权访问。在派生链中,此权限类似public;在类外部,此权限类似peivate。此权限通常只给成员函数。
- public:在同一域中就能访问。
- 引用限定成员函数
- 函数签名的最后可以标注
&
或&&
来做「引用限定 ref-qualified」。- 非限定成员函数可由左值调用,也可由右值调用。此时不能再定义引用限定成员函数。
- 左值限定成员函数仅可由左值调用。
- 右值限定成员函数仅可由右值调用。
- 函数签名的最后可以标注
Lambda
Lambda是一种匿名函数,它在普通函数的基础上增加了一些功能。Lambda是通过函数对象实现的,具有在编译器内联的能力,因此性能可以比函数指针更好。
使用举例
1 | [](int q, int p) { return q > p; } // 常见的比较函数 |
右值
rvalue,(非严格定义: )匿名的临时值,常出现在等号的右方。最大特征是不可取地址,例如
1 | int a; |
右值引用
有时候运算过程中会生成一些体积大的临时对象,在完成表达式后这些临时对象会析构,例如
1 | struct MyString { |
第8行发生如下事件:
- MyString("Hello_rvalue") 产生一个右值。
- push_back接收一个右值参数,并调用MyString的拷贝构造函数新建一个对象。
- MyString("Hello_rvalue")生命周期结束,析构。
发现拷贝构造函数存在资源浪费:既然右值的资源已经没有后续价值,大可以将其资源“偷”过来使用。
于是,MyString类新增移动构造函数(还有移动赋值函数)
1 | struct MyString { |
如此一来,push_back将自动调用移动构造函数来创建对象,避免大量new内存。
右值引用的存在,就是为了尽量榨干临时对象的价值。
要利用右值引用,最重要的是合理地定义移动构造和移动赋值。
移动语义
有时候一些左值就像右值一样即将消亡,弃之可惜,例如
1 | for (int i=0; i<10; ++i) { |
如果能像右值引用一般把tmp的资源“偷”走就好了,于是
1 | for (int i=0; i<10; ++i) { |
注意,move(tmp)后由于资源已经被偷走,tmp可能像野指针一样危险。被move后对象的行为由程序员负责。
完美转发
完美转发是针对c++模板函数的概念。
我们不得不先介绍c++引用折叠概念:
typename T | T& | T&& |
---|---|---|
G | G& | G&& |
G& | G& | G& |
G&& | G& | G&& |
模板函数的右值引用具有欺骗性!在模板实例化时,T& &&
和T&& &
都会被转换为T&
。只有T&& &&
会成为T&&
。利用这一点,我们可只定义T&&
的函数行为。
(对于简单的情况,T &&
等价于 T&&
)
但此时也出现了另一个问题:
1 | template <typename T> |
我们希望,若f1接收左值,那么f2也应该接收左值;如果f1接收右值,那么f2也应该接收右值。但c++规定,“右值引用”是一个左值,因为它有名字,还可以取地址。在这里,f2无论如何都会接收左值。怎样才能写出自动接收左右值的模板函数呢?
利用任意表达式都能生成匿名值,最简单的,就像static_cast<T&&>(x1)
,此时表达式的结果可以转化为右值。再加上引用折叠的特性,就是std::forward
的基本原理。
1 | // T可以是scalar,object,lref。 但不可能是rref! |
异常
- 捕获异常时,从具体到抽象,最后到
...
。 - 如果异常携带字符串信息,则可能引发
std::bad_alloc
,惊不惊喜! - std定义的异常分类:
#include <stdexcept>
logic_error
逻辑出错。invalid_argument
无效参数。domain_error
值域错误。(继承自logic_error
)length_error
试图超越最大体积。(继承自logic_error
)out_of_range
试图越界。(继承自logic_error
)runtime_error
仅在运行时可知的错误。range_error
计算结果无法用目标类型表示。(继承自runtime_error
)overflow_error
算术上溢。(继承自runtime_error
)underflow_error
算术下溢。(继承自runtime_error
)tx_exception
可用于回滚或取消transaction的异常。(Transactional Memory Technical Specification, TM TS)
constexpr if
使用 if constexpr
,将在编译期计算分支选择
常常配合 <type_traits>
或变参模板使用。
- 条件必须能在编译期计算,并转换为
bool
。 - 被抛弃的分支:
- 不参与函数返回值类型推导
- 可以使用有声明但没有定义的变量(不属于 ORD-use)
- 在一个模板实体中,被抛弃的分支不引起后续的实例化。常用于
if constexpr (sizeof...(rs) > 0) { f(rs...) }
- 在一个非模板实体中,被抛弃的分支依然参与语义分析(例如你不能使用一个未声明的变量)。所以
if constexpr
不能替代#if
- 任何时候,被抛弃的分支都不允许 ill-formed(例如
static_assert(false,"")
)。你可能需要 Concept。
StandardLayout
StandardLayout 的前身是 POD。为了内存对齐,各路厂商的编译器会对结构体的数据成员进行偏移量编排。如果需要字节级粒度的操作(常见于网络),我们需要弄清楚结构体内部的具体布局。
结构体布局
C++标准 对结构体布局仅做了很有限的规定:
- 同一访问权限的字段的相对顺序与定义相对顺序相同:先定义的在低地址,后定义的在高地址。
- 不同访问权限的字段的相对顺序是未指定的。
- 为了内存对齐,允许编译器在字段之间、结构体的末尾添加空字节。
标准局部(原 POD)
- C++11 以前,PODType 等价于 StandardLayoutType
- C++11 开始,StandardLayoutType 要求类满足以下条件:
- 所有非静态数据成员具有同一个访问权限。
- 没有虚函数,没有虚基类。
- 任何非静态数据成员都不是引用类型。
- 所有非静态数据成员和基类都是 standard layout。
- 不存在菱形继承。
- bit-fields(位域)和所有非静态数据成员在同一个类内定义。
- 其他复杂的条件(参考 cppref)
StandardLayout(标准布局) 是 C++ 标准提出的概念,但没做具体布局。
常见的实现遵循以下原则,可以帮助我们肉眼判断 size 和 alignment:
- 结构体自身对齐到最宽的基础类型数据成员。(嵌套 struct 的情况下要展开判断)
- 结构体内基础类型字段对齐到自身大小,子 struct 则按照上一条对齐。(在字段之间 padding)
- 结构体的 size 必须是对齐字节的整数倍。(在结构体末尾 padding)
例子:
1 | /* |
bit field
bit field(位域)是一种语法糖。它用于极致紧凑地在内存中排列结构体内容,但是需要额外位操作指令。
这里介绍位域的实现原理。
- C++ 标准没有规定位域应该如何实现,所以,使用位域的 C++ 程序本质上是不可移植的。
- 要考虑字节内外部的大小端问题。通常内外部的大小端保持一致,如 x86 就是不同字节小端在前(低地址),同一字节内小端在前(低编号)。
- 编译器会额外添加位操作指令(如
shl
,shr
,sar
,or
等)来保证读写位域的语义正确性。 - 位域的类型决定了对齐边界。例如
int x : 17
会保证x
不会越过 4 字节的对齐边界。 - 相邻的位域,如果对齐长度相同,编译器会尽量将他们装在一起,如果装得下的话。
- 可以使用匿名位域做人工 padding。长度为 0 的匿名位域会强制下一个位域重新开始对齐。