『languages-1』C system programming
C语言系统级编程
本篇内容全部总结自北航荣文戈教授在bilibili平台分享的《C语言系统级编程》课程
一、内存的基本概念
- 字节:内存的基本单位,由8个0/1比特位组成
- 机器位宽:表示内存中byte的编号,如32位机表示共可以访问\(2^{32}\)个字节(0 ~ \(2^{32} - 1\))
二、非数组与数组变量的声明
内存的相关操作:分配内存、内存赋值、读取内存、释放内存
分配内存:C语言中主要有两种方式,即“变量声明”(无需手动free)与“使用malloc”(需要手动free)
变量要素:变量类型(约定了内存类型与大小) + 变量名称(标记内存的别名)
非数组变量类型:包括int、char、结构体等
数组变量类型:由多个相同变量类型(可以是非数组 or 数组类型)组成的一维构造类型
如int[2]表示由2个int类型构成的数组;double[2][3][4]表示由2个double[3][4]类型构成的数组注意:可以使用 sizeof 关键字获取某个变量类型的占用内存大小
指针变量类型:实质上是一种非数组变量类型,任意一种类型都有指向其的指针类型:
- 指向非数组变量类型变量的指针:Var_T \(\rightarrow\) Var_T*
- 指向数组变量类型的指针:Var_T[10] \(\rightarrow\) Var_T(*)
[10],注意加括号
注意:指针类型也是一种普通的变量类型,要将Var_T* 看作一个整体PTYPE
如int* [2]对应的指针变量为int*(*) [2],其中int* 可看作一个整体PINT - 指向指针变量类型的指针:Var_T* \(\rightarrow\) Var_T**,Var_T(*) [10] \(\rightarrow\) Var_T(**) [10]
注意:使用sizeof计算任意指针的大小都是4字节,这与机器位宽有关
三、内存的变量类型和表示值类型
- 值(Value):包括值本身 + 表示值类型 <V, V_T>
注意:C语言中任何表达式都是有值的,如10 对应值<10, int>,(10 > 20) 对应值<0, int> - 变量类型(Var_T)与表示值(V_T)类型:
- 变量类型:包括非数组和数组变量类型,是系统分配内存的依据(物理视角)
- 表示值类型:从系统外部观察内存得到的值的类型,由变量类型Var_T决定:
- Var_T为非数组类型:V_T = Var_T,V表示通过 Var_T 解释内存比特串代表的值
- Var_T为数组类型:V_T表示数组元素对应的指针变量类型,V表示数组在内存中首字节的编号 如变量类型为char [2][3][4] 对应的表示值类型为 char(*)[3][4]
- “内存六元组”模型:M = {Address, Var_T, Name, Size, Value, V_T }
- Address:该段内存的首字节编号,由系统分配,一旦确定直至回收都无法更改
- Var_T:分配内存的变量类型,在变量声明语句中确定
- Name:分配内存的变量名称,在变量声明语句中确定
- Size:该段内存的大小(字节数量)
- Value:该段内存的表示值,取值由Var_T确定
注意:对于结构体或联合体变量,由于其内存块可能包含多个成员变量,故无法获取内存块的具体值 - V_T:分配内存的表示值类型,类型由Var_T确定
- malloc( ):分配指定大小的连续内存,其变量类型与表示值类型均不确定
四、声明与赋值
变量声明:利用声明语句,在内存中某处(Address)分配特定字节的Size,并为其标记Name,设置两个类型T
注意:仅声明不赋值,内存中的表示值Value是随机的(undefined)变量赋值:对一块内存进行赋值
- 赋值表达式:包括左值(用于定位待赋值内存块)与右值(用于为内存块赋值)
- 左值:通过变量名称或*运算符(V_T为指针类型)定位特定的内存
- 右值:一个有效表达式,可求出对应的值
- 赋值流程:
- 根据左值定位待赋值内存
- 计算右值表达式的值<V, V_T>
- 检查内存表示值类型与右值表示值类型是否一致,若不一致则warning/error
如int a; a = "123";
会报错 - 将右值V按V_T类型转化为比特串放入待赋值内存中
注意:无法直接向数组类型变量赋值(数组类型变量的表示值Value总是等于其首地址Address,改不了)
- 赋值表达式:包括左值(用于定位待赋值内存块)与右值(用于为内存块赋值)
五、观察内存的三种视角
&:对于变量类型为Var_T的变量a,表达式&a的返回值为<Address, Var_T*>;故有赋值语句
int* p = &a;
sizeof:对于内存大小为Size的变量a,表达式sizeof(a)的返回值为<Size, size_t>;故有赋值语句
size_t n = sizeof(a);
注意:size_t是C语言中表示内存大小的类型变量名:对于变量类型为Var_T,表示值为V的变量a,表达式a的返回值为<V, V_T>,即直接取其表示值;故有赋值语句
int b = a;
注意:与变量有关的三种表达式依次代表了内存地址、内存大小以及内存表示值
六、左值与右值
- 表达式:一般由变量、常量、操作符构成
- 左值:能够定位内存的表达式
注意:等号左侧识别出的内存符号,一旦与任何操作符结合,就会变成对表达式的取值操作,即不再能够定位一块内存,会报错 - 右值:一个表达式的返回值<V, V_T>
七、sizeof 操作符
- sizeof(Var_T):计算变量类型Vat_T的空间大小
- sizeof(Name):计算变量Name占用的空间大小
- sizeof(exp):计算表达式exp返回值的类型的大小
注意:对于表达式exp的情形,并不需要先计算exp的返回值<V, V_T>再求出sizeof(V_T)
这是因为sizeof是编译时关键字,exp的返回值类型早在编译时(而非运行时)就已经确定了 - 有关字面量的问题:由于'a'、'b'等字符在C语言中被视为Integer
Character Constant,故sizeof('a') = 4 != 1
注意:在C++中 sizeof('a') = 1,故需要在移植C/C++程序时考虑这个问题
八、 * 操作符
定位内存的两种方式:变量名称 or *操作符
*操作符:用于定位指针变量指向的内存,定位流程如下:设
int* p; *p = 10;
p不与&和sizeof结合,故定位p的内存并仅取其值(即某块内存的始址,表示值类型为指针)
p与*操作符结合,定位指针变量p指向的内存
对变量内存 (*p) 的操作: \[ \begin{cases} \text{左值操作:对内存赋值,注意会检查类型是否匹配} \\ \\ \text{右值操作:包括取地址}\text{&}、\text{求大小sizeof、或与其它操作符结合} \end{cases} \]
注意:*操作符与指针变量结合获得该指针变量指向的类型;若不与指针变量结合就会报错
九、指针加减法
对任意表达式exp,若exp的返回值类型是一个有效的指针类型,则有: \[ *(\text{exp+n}) \Leftrightarrow \text{exp[n]} \] 故任何一个返回值为指针类型的表达式,都蕴含着位于对应内存的数组(大小未知),其元素类型为指针变量类型指向的类型(Reference Type)
如 (p + 1)[2] \(\Leftrightarrow\) *(p+1+2) \(\Leftrightarrow\) *(p + 3) \(\Leftrightarrow\) p[3]指针加常数:设 p : <Value, Value_T >,则 p+n : <Value + n \(\times\) sizeof(*p), Value_T >
指针相减:设 p : <Value1, int*>,q : <Value2, int*>,则 p-q : < (Value1 - Value2) / sizeof(*p), ptrdiff_t >
注意:相减的指针类型必须相同;相减得到的表示值是两个地址之间的变量个数
十、数组的内存访问
- 设数组变量g为int g[2][3],则有:
- &g:<address of g, int(*)[2][3]>
- sizeof(g):<size of g, size_t>
- g + 1:<(address of g) + 1 \(\times\) sizeof(*g), int(*)[3]>,注意V_T为数组元素的指针类型保持不变
- 数组名:是一个标识符,一个左值表达式(可以定位一块内存)
十一、参数传递
- 实参与形参:先获取实参表达式exp的返回值,再用该返回值初始化函数形参名对应的内存中(Pass
By Value)
注意:实参表示值类型与形参类型必须一致 - 非数组变量传参:将实参返回值写入形参的内存块中
- 数组变量传参:可以省略形参的第一维大小,即省略数组大小信息
注意:由于函数形参与实参的表示值类型都是元素的指针变量类型,故数组的大小信息可以被省略
十二、malloc
使用malloc分配堆内存的特点:
- 内存没有名称Name
- malloc返回值类型为void*,指向分配内存的首字节,需要先强转类型
如
Var_T* p = (Var_T*)malloc(sizeof(Var_T)*N);
- malloc分配的内存空间未初始化
- 需要用 free(p) 释放内存,有多少次malloc就有多少次free(避免memory leak)
注意:一般使用 malloc(sizeof(Var_T) * N) 分配内存空间,表示申请了一个 Var_T[N] 类型的空间,其表示值类型为Var_T*
十三、typedef
- 为非数组类型提供别名:
typedef char INT1;
,即将char重定义为INT1类型 - 为数组类型提供别名:
typedef int AINT[2];
,即将int[2]重定义为AINT类型
十四、const限定符
限定非数组变量类型:
Var_T const a;
,Var_T const是一个整体,表示Var_T指向的内存是只读的
注意:由const修饰的变量类型,其表示值类型为对应的非限定变量类型;如 int* const 的表示值类型为 int*限定数组变量类型:Var_T const[2][3], 其对应的表示值类型为Var_T const(*)[3],表示其元素类型是只读的
注意:此时应把 Var_T const 看作一个整体Var_T const对应的指针变量类型:Var_T const*,其表示值类型也是Var_T const*
注意:只有最后一个“干净”的const修饰只读变量,如:
由 Var_T const* 定义的变量是可以修改的,但指向的内存是只读的(其V_T为Var_T const*,解引用后为Var_T const)
由 Var_T const* const 定义的变量是不可被修改的(const修饰),其指向的内存也是只读的(其V_T为Var_T const*,解引用后为Var_T const)Var_T const VS. const Var_T:推荐使用前者,避免解读歧义;C语言中对const的位置无明确语义规范
注意:指针变量 const Var_T* 会存在解读歧义:不确定*是与整个 const Var_T 结合还是只跟 Var_T 结合
十五、字符串
- 字符数组:变量类型为char[N],表示值类型为char*,与其它数组变量类型类似
- 字符串初始化:使用双引号括起来的字符串常量,其末尾包含一个隐藏的'\0'
注意:字符串"hello"被放置在静态区,其元素无法被修改,如"hello"[0] = 'e'
会运行崩溃 - String Literal VS.
String:前者字符串内部可以包含任何字符(包括'\0'),后者只是在最后有'\0'
注意:两者使用 sizeof 得到的大小是相同的,但使用 strlen 得到的长度是不同的 - 访问字符串:字符数组本身可以定位一块内存,如&"hello",sizeof("hello"),"hello"[0]等
十六、左值与表达式
表达式:C语言中由一系列操作符和操作对象组成的序列
左值(lvalue):一种可以定位一个对象的表达式
注意:C语言中Type分为Object Type和Function Type- 变量标识符:int a
- 字符串:"hello"
- 解引用 *exp:*(p + 1)
- 数组取值 exp1[exp2]:e[1] 注意:E1[E2] \(\Leftrightarrow\) *(E1 + E2),要求其中一个表达式返回指针类型、另一个E返回整数类型
- 复合字面量 (type-name){ initializer }:如(int){1},(int[3]) {1, 2, 3}
- 成员变量引用:h.name,p->name
可被放在等号左边的左值(modifiable lvalue):需同时满足以下条件:
- 非数组变量类型:表示值总是等于内存首地址,无法被修改
- 不是不完全类型:如extern int a[],其中不完全类型是指除函数类型外大小无法确定的类型
- 不能被 const 修饰;对于结构体类型变量,其各成员变量不能被 const 修饰
注意:若对非modifiable lvalue赋值,则会报编译错误
右值(rvalue):即表达式的值<V, V_T>,表达式之间通过表示值计算返回值
十七、函数指针
- 函数类型(Function
Type):规定了函数返回值类型与参数数量及类型,如int(char,
int)
注意:区别于对象类型,函数类型不是lvalue,故函数类型变量不能被赋值 - 函数类型对应的指针类型:如int(*)(char, int)
- 函数类型六元组:仿照对象类型六元组提出
- Address:函数入口的地址
- Var_T:函数类型,如int(int, int)
- Name:函数标识符
- Size :N/A,函数类型无大小,不能与 sizeof 结合
- V:表示值与Address的值相同
- V_T:表示值类型是函数类型对应的指针类型,如int(*)(int, int)
- 函数调用:函数标识符 + 参数列表,其中函数标识符即为函数名称
注意:函数标识符func属于基础表达式,其返回值为<函数入口地址, 函数对应的指针类型> - &func与*pfunc:
- &func:由&操作符的性质,&func返回函数指针变量,其表示值即为address 由此可见&func与func表达式的返回值是完全相同的
- *pfunc:由解引用操作的性质,*pfunc返回函数类型变量,其表示值类型为函数指针 由此可见*pfunc与pfunc表达式的返回值是完全相同的
十八、Compound Literal
复合字面量:匿名对象类型,其定义为 (type-name){ initializer-list }
- type-name:该对象的变量类型
- initializer-list:对该对象进行初始化的列表
注意:符合字面量即匿名的变量,除了不含Name外与非数组/数组变量类型类似
赋值问题:复合字面量本身是合法的左值,但其是否为 modifiable lvalue 取决于存储位置:
- 位于函数内部:生命周期为 automatic storage duration,是 modifiable lvalue
- 作为全局变量:生命周期为 static storage duration,不是 modifiable lvalue,只能初始化
注意:复合字面量是合法的左值,可以定位一块内存,并能够与 &、sizeof 结合
空间分配规则:同一作用域内,各 Compound Literal 在内存中仅保持一份
注意:由上述特点可知,在循环中反复声明同值复合字面量,实际获得的是同一内存块中的数据使用同一个复合字面量:使用对应的指针类型存储变量地址,通过指针解引用操作访问
十九、Type
Object Type:定位一个内存块,可通过变量类型解析出一个值
Integer Related:主要包括以下类型:
- Signed Integer Type:标准有符号整型(signed)+ 扩展有符号整型
- Unsigned Integer Type:布尔型(_Bool)+ 对应无符号整型(unsigned)+ 扩展无符号类型
Floating Type:主要包括以下类型:
- Real Floating Type:标准浮点类型(float、double)+ 高精度浮点类型(_Decimal32等)
- Complex Type:复数浮点类型(float_complex、double_complex)
注意:C语言中并未规定必须支持复数类型
Character Related:字符类型(char)+ 有符号字符类型(signed)+ 无符号字符类型(unsigned)
注意:对于平时使用的char,其是否为有符号类型取决于平台,一般只用于存储ASCII码(0~127)Enumerated Type:定义一系列文字表述的整数
Basic Type :char + Signed Integer Type + Unsigned Integer Type + Floating Type,均为完全对象类型
Integer Type :char + Signed Integer Type + Unsigned Integer Type + Enumerated Type
Arithmetic Type :Integer Type + Floating Type
Derived Type:可由其它类型递归地构造出来,包括数组类型、结构体类型、函数类型、指针类型等
Array Type:给定任何一个Object Type,都可以其为元素类型构造对应的一维数组类型
注意:对于“高维数组”类型,均可通过“低维数组”类型递归地构造Function Type:其定义包括返回值类型 + 参数数量及类型
Pointer Type:给定任何一个Type(Reference Type),都有对应的指针类型(Pointer Type)
注意:任何指针类型都是一个普通的Object TypeScalar Type :Arithmetic Type + Pointer Type(可进行标量加减操作)
Aggregate Type :Array Type + Struct Type
注意:Union Type不是聚合类型Incomplete Object Type:缺少确定object大小的信息,如数组缺少元素个数、void类型、包含不完全类型的结构体或联合体等
二十、赋值表达式
赋值操作符:包括 =、+=、>>= 等赋值操作符
赋值表达式:定义为 exp1 assignment_operator exp2,其中exp1一定是modifiable lvalue
- C:上面表达式不是lvalue,其返回值为exp1更新过的值
- C++:上面表达式是lvalue,指向exp1定位的、已被更新的对象
后缀加减 or 前缀加减:exp++ or ++exp
- exp++:返回值为exp的值,副作用是让exp对应对象的值+1;exp++不是lvalue
- ++exp:等价于
exp += 1
,返回值是exp更新后的值,故其在C语言中是lvalue、而在C++中不是lvalue
注意:为了维护程序的可读性,GJB中规定禁止使用 ++、-- 操作符
Evaluation of Expression:主要包括两个步骤
- Value Computation:计算表达式的返回值<V, V_T>
- Initiation of Side
Effect:确定表达式的副作用,即执行状态的变化,如改变内存值
注意:副作用利用等号右侧表达式的返回值,改变等号左侧的内存值
注意:a++ 和 ++a 这两个表达式都有各自的返回值,但什么时候真的+1(即副作用的出现顺序)则是不确定的
Sequenced Before:描述两个表达式之间 evaluation 的先后顺序
exp1 Sequenced Before exp2 表示exp1的求值与副作用全部发生在exp2的求值与副作用之前Sequence Point :exp1 Sequenced Point exp2 \(\Leftrightarrow\) exp1 Sequenced Before exp2
常见的Sequence Point:实参求值与函数调用之间、两个独立分隔的表达式之间 等
注意:两个Sequence Point内部的表达式之间没有约定 evaluation 的先后顺序有关自增/自减的undefined behavior:对于一个scalar object,如果满足其中一种条件:
- 产生两次副作用,且两次副作用之间没有先后顺序的约定,如
int i = 1; i = ++i + 1;
若 ++i 副作用先执行,则最后将 i 赋值为表达式 ++i + 1 的返回值3
若 i = ++i + 1 副作用先执行,则先将 i 赋值为 ++i + 1 的返回值3,最后 i 自增得到4 - 产生的副作用与对其取值之间没有先后顺序的约定,如
int i = 1; a[i++] = i;
若 i++ 的副作用发生在等号右侧对 i 求值之前,则 i 先自增为2,再将 a[1] 赋值为2
若 i++ 的副作用发生在等号右侧对 i 求值之后,则先将 a[1] 赋值为1,i 再自增为2
注意:表达式 i++ 的返回值始终为1,故均是对 a[1] 赋值
则最终内存结果不确定,取决于编译器的优化方法等行为
- 产生两次副作用,且两次副作用之间没有先后顺序的约定,如
二十一、volatile限定符
- 变量声明:
Var_T volatile N;
,其中变量N的表示值类型为Var_T
volatile 限定符表示这块内存的值可能以未知的方式变化 - Volatile 在 MMIO 中的应用:
- MMIO:内存和I/O共享同一个地址空间
- 定义外设映射内存:
#define NUM (*(int volatile*)0x12340000)
这样变量NUM的内存值可能被硬件而非程序改变,表现为NUM值不可控
- abstract machine 和 volatile
:“抽象机”是一种不考虑优化的模型;volatile
修饰的变量遵循“抽象机”模型
每次访问 volatile 修饰的变量都会严格从内存中获取值,而不是直接从寄存器等缓存中获取值 - volatile
的使用注意事项:由于每次都只从内存中读数据,而数据又可能被硬件改变,所以每次值都不确定
- 编译器优化:如果从寄存器等缓存中读值,结果就是正确的;如果只从内存中读值,结果就是不确定的
- volatile access:获取被 volatile 修饰的值的行为是副作用
- Var_T volatile 对应的指针类型:变量类型 和 表示值类型 都是Var_T volatile*(Var_T volatile是一个整体)
- Var_T const volatile:从程序代码角度看是只读的,但从硬件角度看是会被随机修改的
二十二、Literal
Literal的含义:仅从字面义去理解,不做任何演绎
String Literal:按字面意思理解的字符串(不包含转义),其内存值无法修改(编译警告、运行出错)
Compound Literal:按字面意思理解 (type-name){ initializer-list } 这种组合形式,其内存值可以修改
C语言常量(constant):整数常量 + 浮点常量 + 枚举常量 + 字符常量
注意:常量不是左值,不能像 Literal 和 const修饰变量一样定位一块内存C++中的Literal:主要包括integer-literal、floating-literal、character-literal和string-literal
- string-literal:在C++中,其返回值类型为const
char*;若尝试修改string-literal的值则会直接编译出错
注意:C语言中string-literal的返回值类型为char*;const char* 体现literal在C++中具有常量的含义 - compound-literal :C++中没有compound-literal,程序移植时要特别注意
注意:C++中的literal,对应C中的constant
- string-literal:在C++中,其返回值类型为const
char*;若尝试修改string-literal的值则会直接编译出错
二十三、padding
字节大小:C语言中规定一个Byte含有 CHAR_BIT 个二进制位,其中 CHAR_BIT 规定要 \(\ge\) 8
注意:“1Byte = 8bits”是长期发展形成的工业标准,部分平台的字节大小大于8padding:填充类型中剩余的二进制位;设某类型的 size 为 n \(\times\) CHAR_BIT 个比特,即 n 个字节:
无符号整数类型:包含 value bits 和 padding bits
设 value bits 共有N位(宽度为N),则该类型的表示值范围是 0 ~ \(2^N - 1\)有符号整数类型:包含 sign bit,value bits 和 padding bits
设 sign bit 和 value bits 共有N位(宽度为N),则该类型的表示值范围是 \(-(2^{N-1})\) ~ \(2^{N-1} - 1\)注意:其中C语言规定 unsigned char 和 signed char 类型均不允许有 padding bits
整型变量 int / unsigned int:规定宽度N必须$$16,具体宽度不确定
intN_t 和 uintN_t:表示无 padding,且宽度确定为N的整型变量
注意:若某平台提供了无 padding,且宽度为32位的int类型,则应该typedef int int32_t;
int_leastN_t 和 uint_leastN_t:表示宽度至少为N的整型变量
注意:C语言规定编译器必须定义含 least 的整数类型;若编译器定义了intN_t,则 int_leastN_t 和 intN_t相同int_fastN_t 和 uint_fastN_t:表示宽度至少为N,且处理速度最快的整型变量(最快最小)
注意:C语言规定编译器必须定义含 fast 的整数类型;fast不代表总是最快,而是代表尽可能满足速度要求
二十四、alignment
地址对齐:规定对象内存的首地址的限制;对齐值必须是2的n次方
注意:“对象对齐”是指 对象首地址 % 对象的alignment == 0,一般char的对齐数为1、int的对齐数为4_Alignof :对于类型为T的对象O,_Alignof(T) 表示类型T的对齐数,_Alignof(O) 表示对象O的对齐数
注意:对象O的对齐数默认为其类型T的对齐数,且可被修改_Alignas:为类型为T的对象O设置更大的对齐数,声明句
_Alignas(N) T O;
表示将对象O的对齐数调大到N- 修改非数组类型的对齐数:_Alignas(64) int a; \(\Rightarrow\) _Alignof(a) == 64;且int的大小和类型对齐数均不变
- 修改数组类型的对齐数:数组类型的对齐数 == 其元素类型的对齐数(递归定义) _Alignas(64) int a[N]; \(\Rightarrow\) _Alignof(a) == 64;且int[N]的大小和类型对齐数均不变
注意:通过 _Alignas 改变对象的对齐数,并不会改变该对象的size,也不会改变其类型的对齐数
结构体的对齐要求:对于类型T为为结构体类型的对象O,_Alignof(T) == max(_Alignof(\(\text{E}_i\)))
其中\(\text{E}_i\)表示结构体中第 i 个成员对象的对齐数- 通过 _Alignas(\(\text{E}_i\)) 调整结构体成员对象的对齐数,会改变该结构体类型的对齐数
- 通过 _Alignas(stru) 调整结构体对象的对齐数,会使该对象的对齐数 > 结构体类型对齐数
二十五、结构体类型的size
计算结构体类型的大小:遍历所有成员对象,确定各成员对象的 offset 和 padding;对于每个成员对象:
- 根据该对象的对齐数,找到满足其对齐要求的最小offset 该对象与上一个成员对象间的空白距离称为 internal padding
- 将该成员对象O填充在 offset 处,其占据了sizeof(O) 的内存空间
填写了所有成员变量后,根据该结构体类型的对齐数,在末尾留出最小trailing padding
\(\Rightarrow\) sizeof(T) = \(\sum\) sizeof(\(\text{O}_i\)) + \(\sum\) internal padding + trailing padding
注意:由上述过程可知,结构体类型的size是其对齐数的整数倍
#pragma pack(n) :调整结构体内部对象的对齐要求
对于结构体类型T,设其第 i 个成员对象为\(\text{E}_i\),则 _Alignof(\(\text{E}_i\)) = min(_Alignof(\(\text{E}_i\)), n)
注意:对于结构体类型T,_Alignof(T) = max(_Alignof(\(\text{E}_i\))),该规则保持不变,用于最后计算 trailing padding申请指定对齐要求的空间:
void* aligned_alloc(size_t alignment, size_t size);
指针类型强转的对齐问题:对指针表达式的类型强制转换后需要保持地址对齐
如 char* 类型的内存对齐数为1,int* 类型的内存对齐数是4,则char* p; (int*)(p + 1);
会不对齐,属于ub
二十六、restrict限定符
- 指针变量声明:
Var_T* restrict p;
注意:restrict 只能修饰指针变量类型 - restrict 的使用:设 p 为 restrict 修饰的指针变量,则程序员需要保证在指针 p 的生命周期内,其指向的对象不会被其它指针同时引用
- restrict 与编译器优化:对于两个位于同一作用域的指针变量,若其指向了不同的内存块,编译器可以采用载入立即数等方式修改两个内存块数据,以达到优化代码的目的(减少访存次数)