C语言系统级编程
一些不一定的概念
- 一比特不一定是8bit,已经有16bit的机器了,但是至少包含8个,定义了一个宏
CHAR_BIT,规定其≥8。 - 字节是C中基础存储单元,每个字节都有一个编号,即地址
- 内存为一系列字节顺序排列一起组成的存储结构,地址是字节在内存中的编号,只有字节才有地址。
关键字
const volatile int a;表示a的值不可以改变但是可以以系统未知的方式进行修改(volatile)。
所有的变量名都统称为对象标识符。
typedef T Alias中Alias与T在语义上一致,且Alias在语法上是一个整体。typedef可以提高跨平台代码的可移植性。_BitInt(N)指定有符号整数类型的宽度为N,一个_BitInt(N)类型的对象,值范围:-(2^(N-1))到2^(N-1)-1,不同的N是不同的类型,占用的字节数需用sizeof获得,例如:_BitInt(17)的填充bits为:_BitInt(17):sizeof(_BitInt(17))×CHAR_BIT - 17。
对于指针类型使用typedef时,typedef T* Alias其中Alias要放在*后面,而对于数组类型则需要将别名以数组的形式命名,因此:int(*)[10] -> typedef int(* PAINT)[10] <=> typedef typeof(int (*)[10]) PAINT、int*[10] -> typedef int* APINT[10] <=> typedef typeof(int *[10]) APINT.
对象
定义
数据存储的区域,可以传递值的内容,由至少一个比特的连续空间组成,形式化定义成T。
对象类型
完全~
大小程序员可知。
不完全~
程序员不可知。
没有`size`,不可以使用`sizeof`获取`size`。
对象表示
该对象各个bit组成的二进制串就是其对象表示。
对象的值和对象表示通过对象类型形成映射。
若两个对象,对象类型一样,则若对象表示一样,则值一样;反之,值一样,对象表示不一定一样(因为,组成对象表示的bit中包括值bit和填充bit,一个对象可能有若干个无意义的填充bits,值bits和填充bits的构成由对象类型决定)。
可能的考点:对任何对象而言,若其包含字节数为n,则其包含比特数为n*CHAR_BIT个,而非8n。
对象地址
对象占用连续内存编号的最小值。
相应的有对齐要求,即对象的地址能被对齐要求整除。alignof(T)规定了一个T类型对象的缺省对齐要求(表明对齐要求可变)。
类型本身没有大小和对齐要求,只有对象有。
本课程假设常用类型缺省对齐要求如下:
char 1,short 2,int 4,float 4,double 8,pointer type 4
类型
算术类型
都是完全对象类型。
基础类型,除了枚举类型以外的算术类型。char、signed char和unsigned char三者互不相同。编译器规定char类型与后两者之一的行为一致。signed char和unsigned char的宽度一定是CHAR_BIT。则sizeof(signed char)一定为1,而其余有符号整数类型的宽度分别为 short int(至少8)、int(至少16)、long int(至少32)和long long int(至少64).sizeof(int)在不同平台上结果可能不一致。
除了signed char其他4种标准有符号整数类型(short int、int、long int和long long int)也可以没有填充位(为了向后兼容的方便)**。 bool类型是标准无符号整数类型的一种,宽度恰好为1,但是具体的size由编译器决定,使用sizeof获取,则其padding bits为sizeof(bool)*(CHAR_BIT - 1) 。 char的有无符号类型的对齐要求是最小的,但是**不一定为1**. **任何类型的对齐要求都要使用alignof()`获取。
枚举类型实际的实现类型有两种方式决定:
- 编译器决定(不常用),自行从char、标准有符号整数类型、标准无符号整数类型、扩展有符号整数类型、以及扩展无符号整数类型5种中选择一个设置。
enum后面跟一个整数类型(常用):enum Season : int {...}。
派生类型
指针类型
任何类型都有指针类型。
形式化定义:T*:1. T必须为一个对象类型或者函数类型,2. T*视为T的派生类型。
由于T[N]不是语法上的整体,所以T[N]*不是合法的类型,其指针类型应为**T(*)[N]**。
任何指针类型都是完全对象类型。
指针类型也有自己的指针类型。
如何将T、T[N]、T*分别派生为数组类型和指针类型:
- T -> T[N]:数组类型
- T -> T*:指针类型
- T[N] -> T[M][N]:多维数组类型,在[N]前加[M]
- T[N] -> T(*)[N]:指向数组类型的指针,在[N]前加 *
- T* -> T*[N],指针派生数组类型,在*后加[N]
- T* -> T**:指向指针类型的指针,在 *后加 *
例:
1 | |
标量类型包括算术类型和指针类型、以及空指针类型(nullptr_t)。int* p = &a;中的*p就是一个左值,与其余左值没有任何差别,可以理解成一个匿名对象。p应该理解成一个数组的首元素的首地址,指针类型永远蕴含着对于数组的访问。
若表达式exp求值后的右值为<Value,V-T>,且V-T为一个对象指针类型,则可用* exp来定位到Value对应字节编号开头的一段内存,定位对象M该对象的地址为Value,O-T为V-T对应的Referenced Type(即去掉一个*),其他对象属性相应确定下来。
任何表达式求值后能得到一个pointer type的右值,则这个表达式就能用来指向一个对象或函数,是一系列概念的体系化应用,包括对象,表达式,lvalue,求值等等
任何给定类型T,都有对应的指针类型Pointer toT
指针表达式的-操作
*(p-1)<=>p[-1]<=>-1[p]
两个指针相减,指针类型必须一致,p-q<=>
数组类型
形式化定义:T[N]:1. T必须为一个完全对象类型,2. 若N为一个大于0的整数常量表达式,若N未知则该数组为一个不完全对象类型;否则为普通数组类型;若N不为整数常量表达式则为变长数组类型。3.T[N]视为T的派生类型。
对于完全对象数组类型能够继续派生,例如:int[5]当作T的话,N为10时,派生出来的类型即为int[10][5],也就是10个int[5]组成的数组,因此,第一维是10,而不是5个10int[5][10]。
同理还可以继续派生为更多”维”的数组。
反直觉:若N为const int m=10; T[m];则该数组为变长数组类型,因为m不是整数常量表达式。
数组类型的大小为sizeof(T)*N。
数组类型的对齐要求为alignof(T)。
不完全对象没有大小和对齐要求的限定。typedef int AINT[5](合法)且与typedef typeof(int[5]) AINT等价。int[5]和AINT在语义上等价。前者在语法上不是一个整体,但后者是,typeof可以将其包裹的内容处理成一个整体。
所有数组都是一维的。void也是不完全对象类型。
变长数组
边长数组类型:T[M],一个数组类型,且为不完全对象类型,长度确定时完全化,元素类型为T,元素个数为表达式M,M不为一个整数常量或整数常量表达式,则T为一个边长数组。
数组的长度(M的rvalue)*sizeof(T)必须在运行时才能确定,首次执行到T[M]时数组大小确定,确定之后不能再修改数组大小,不是可以变化,而是在运行时才可知数组长度。
1 | |
常量表达式:编译时进行求值,像常量一样使用,限制条件:1.不包含赋值、自增/自减,函数调用,逗号运算符,除非他们包含在不被求值的子表达式中;2.求值后右值的取值范围,应该在右值类习惯的表征范围内。
整数常量表达式:1.表达式右值类型为整数类型;2.能在编译时进行求值,像整数常量一样使用。
1 | |
注意:满足以下条件为整数常量表达式,是一个基础表达式,由进制前缀、数字序列和类型后缀三部分组成,而整数常量表达式是一种表达式,整数常量表达式的值都不会变,这只是一个基本条件:
- 表达式rvalue类型为整数类型
- 操作数是
- 整数常量
- 字符常量
- 类型为整数的named constant
- 类型为整数的compound literal constant
- rvalue类型是整数常量的sizeof表达式
- alignof表达式,
algnof(type-name)都是整数常量表达式 - cast表达式,其中cast的子表达式是浮点常量、类型为算术类型的named constant、compound literal constant,除非该cast表达式是typeof、sizeof或alignof操作符的操作数

其中注意:+10是整数常量表达式,但不是整数常量,其中10是整数常量,因此满足操作数是整数常量,且表达式右值类型为整数类型,因此int[+10]不是VLA,但是int[(int)(+10.0)]是VLA,因为(int)(x)为cast表达式,且其中子表达式x,即(+10.0)不是浮点常量、类型为算术类型的named constant、compound literal constant中的那几种。
1 | |
通过sizeof()理解VLA
可以跟两种operand:type-name(sizeof(type-name))和exp(sizeof(exp) / sizeof exp;sizeof(non-lvalue-exp) /sizeof non-lvalue-exp)
对于VLA,typeof()处理跟sizeof()逻辑一致
- 考虑sizeof(type-name),
- 且type-name不为变长数组类型,sizeof(type-name)在编译时得到type-name的大小,其返回值是整数常量,type-name作为operand,整体不求值,例:
sizeof(int[2+3]),2+3不做求值。 - 如果type-name是VLA,则sizeof(type-name)不能在编译时得到type-name的大小,其返回值不是整数常量,需要在运行时得到类型大小,type-name作为operand,子表达式需要求值,例:
int m = 5; sizeof(int[m]); sizeof(int[++m]);
- 且type-name不为变长数组类型,sizeof(type-name)在编译时得到type-name的大小,其返回值是整数常量,type-name作为operand,整体不求值,例:
- 考虑
sizeof(lvalue)- 如果lvalue定位的对象类型是Non-VLA,则sizeof(lvalue)在编译时得到lvalue定位对象的大小,其返回值是整数常量lvalue作为operand,整体不做求值,例如:
int a[10][10]; sizeof(a); sizeof(a[0]); - 如果lvalue定位的对象类型是VLA,则sizeof(lvalue)不能在编译时得到lvalue定位对象的大小,其返回值不是整数常量,需要在运行时得到对象大小,意味着lvalue作为operand,子表达式需要进行求值,例如:
int m = 5; int n=10; int a[m][n]; sizeof(a); // a为VLA类型sizeof(a[0]); // a[0]的对象类型是int[n],也为VLA,需要进行求值,但是对于a[0][0],其对象类型是int,不是VLA,因此sizeof(a[0][0])不需要对a[0][0]进行求值
- 如果lvalue定位的对象类型是Non-VLA,则sizeof(lvalue)在编译时得到lvalue定位对象的大小,其返回值是整数常量lvalue作为operand,整体不做求值,例如:
- 考虑
sizeof(non-lvalue)- sizeof(non-lvalue)返回这个non-lvalue做求值之后右值类型的大小
- 这个non-lvalue并不会真的做求值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int i = 1;
int j = 2;
size_t ss;
ss = sizeof(int[i++]);
print(ss); // 4, i = 2
ss = sizeof(int[++i]);
print(ss); // 12, i = 3
ss = sizeof(int[++j]);
print(ss); // 12, j = 3
ss = sizeof(int[++i][++j]);
print(ss); // 64, i = 4, j = 4
// 如下属于未定义UB行为
sizeof(int[++i][++i])
int m = 10;
int a[10][10];
int b[m][10];
int c[10][m];
ss = sizeof(a[++i]); // a[++i]的对象类型为int[10],因此不需要对a[++i]的式子进行求值处理,也就是++i不会被执行,i=1
ss = sizeof(b[++i]); // 同理不会被执行,i=1
ss = sizeof(c[++i]); // 对象类型为int[m]为VLA,需要进行求值处理,则++i将被执行,i=2
结构体类型
联合体类型
原子类型
函数类型
限定类型
_Atomic
_Atomic T或 T _Atomic是T的限定原子类型,大小和缺省条件都使用sizeof和alignof来获取。_Atomic(T)是T的派生原子类型,两者不相同,前者的T不能是数组和函数类型,但后者除了不能是数组和函数类型,还不能是限定和原子类型(原因是对于前者来说,相当于限定符的顺序无关性,即_Atomic const int可以看作是const _Atomic int,本质上还是_Atomic只能修饰一种类型,但是限定符的顺序无关导致其可以去与其余的限定符和原子类型组合),例如当T为const int、_Atomic int和_Atomic(int)时_Atomic(T)均不合法,_Atomic T均合法。
两者不是同一种类型,大小、对齐要求和相同对象值对应的对象表示等都可能不同。
const、volatile、restrict
对于给定类型T,限定符Q,Q T或T Q都是T的限定类型,这两者类型不同,大小、对齐要求、对象表示相同。
当T为一个整体时,限定符在T前后位置无区别;当T不是一个整体时,限定符只能在T前面。
关于T[N]和T*的限定类型:
只有Q T[N]合法,表示元素类型为Q T,个数为N,反过来则不合法。Q T*和T* Q都合法,且语义不同,前者为指针类型,索引类型为Q T,后者为限定类型,限定符作用于指针,其非限定类型是T*。Q typeof(T[N])等价于Q typeof(T)[N],等价于T Q [N],主要要注意T是指针类型时的区别,不是Q T[N]。
例:
1 | |
const int: typedef const int CINT
const int*(* const)[4][5]: typedef const int*(* const Alias)[4][5]
const int*(* const*)[4][5]:
typedef const int*(* const* Alias)[4][5]
1 | |
const限定符在函数参数中可以用来禁止函数对于传入指针的修改,即设置int const*只允许获取相应的值,而不能通过传入的地址进行值得修改
volatile
Obj_T volatile/volatile Obj_T是一个新的类型int volatile a = 10;则其对象类型为int volatile,对象表示值类型为int。
其修饰内存的值可能会以未知的方式变化,代码没有修改,但是值变为了20.
应用场景:1、Memory-Mapped Input/Output (MMIO)端口对应的对象
2、异步中断函数访问的对象
MMIO场景下的应用
MMIO中,内存和I/O设备,共享同一个地址空间,会给各种I/O设备预留出相应的地址区域。
因此一个地址可能访问内存,也可能访问某个I/O设备。
1、假设0x12340000是某一个I/O设备对应的映射地址
2、假设这个地址开始的I/O设备对应的对象是int类型
3、将这个地址开始的I/O设备对应对象值定义成MYNUM
1 | |
MYNUM这个值就可能因为硬件的行为而改变
这种改变对于程序来说是不可知的(Unknown)
易理解的例子:
1 | |
volatile对象若要evaluate,都必须去访问对应的内存,而不可以通过寄存器等别的方式获取相应的内存中的值。
使用注意事项:int a = MYNUM + MYNUM两次调用MYNUM其side effect,可能有影响。
restrict
1 | |
restrict也是一种限定符,只能用来修饰指针类型T* restrict O=initializer; //注意这里restrict放在T*的右边
Based on
T* restrict O=initializer;
表达式O能够定位一个对象Obj(也可以称为对象O),对象类型为T* restrict
注意表达式O和对象O的区别:后者是内存中那几个字节内容
- 对象O的值是T*类型,蕴含着一个数组访问,表达式O求值后的右值就是Obj的值,因此O[n]可用来访问数组的任意元素对象
- 给定表达式E,求值之后是一个指针类型的右值,如果对象Obj的值修改,表达式E被求值之后的值也会被修改,则
E Based on Obj例如:表达式O Based on 对象O1
2
3
4
5
6
7
8
9
10
11
12char e[4]={0};
char* restrict q = e;
// q, q+1, q+2,&(p[n])这些表达式 based on对象q
int** restrict p =initializer;
/*
不失一般性,这里用p指代p指向的对象
指针表达式p based on 对象p
指针表达式p+1 Based on 对象p
指针表达式p[0], p[1]不是based on p,因为p的值修改了,p[0]不一定修改,例如两个数组一模一样,p先后等于这两个数组的首地址,p值改变,但是p[0]和p[1]并不会发生改变
指针表达式p[0], p[0]+1是based on p[0]指向的对象
指针表达式p[1], p[1]+1是based on p[1]指向的对象
*/
restrict作用
在一个Block里面
1、如果有一个左值表达式L,&L表达式是Based on一个对象P
2、该左值表达式L其定位的对象假设为X,如果X对象的值会被修改
3、有另一个左值表达式M能访问X,则M的地址也必须based on对象Pint* restrict p=initializer; 标识符p定位的对象,不失一般性称为对象p
左值表达式p[2](也就是L),&(p[2]) 或p+2是Based on对象p
p[2]这个左值表达式定位的这个对象称为X
如果对象X的值会被修改,任何访问对象X的其他左值表达式M,&M必须based on对象p
例如(p+1)[1]也能访问X,这个表达式的地址也是based on对象p
restrict是由程序员来保证的,主要了为了编译器优化的效率问题,若可以确保某几个指针类型不是同一个对象则可优化空间大大提升
对象属性
最核心属性:对象类型,一个对象可能没有对象类型。
对象名称(根据有无分为名具名对象和匿名对象)、大小(Size,连续字节的个数,可通过其类型获得,sizeof(T))。
本课程假设常用类型大小如下:
char 1,short 2,int 4,float 4,double 8,pointer type 4
分配对象的方法
对象定义
基本语法形式:Storage-class specifier T O = Initializer;,分配一个T类型对象,声明一个对象标识符O,将O同T关联起来,并用initializer初始化该对象的对象表示,若没有initializer,则不构成对象定义,Sto...指定对象的存储周期。
对象定义中的T要看作一个整体。
指针是一个动作,没有东西称作指针。
对象定义中完整形式的initializer就是该对象的值。
使用T[N]去定义对象时应写成: T O[N]=...,此处注意是严格置于[N]之前,
使用T* 去定义对象时应写成:T* O=...,这里注意是严格写在*后,例如:int(*)[5]-> int (*p)[5],int*[5] -> int *p[5],或者typedef int (*PAINT)[10]; PAINT d = NULL。
无initializer时,….
一次定义多个对象,要求T必须是语法上整体形式存在的,例如:T O1[N] = ..., O2[N] = ...<=>typeof(T [N]) O1 = ..., O2 = ....对于指针类型同理。
1 | |
即除了整体的部分,其他都需要重新复用。
类似的,使用Q T、T Q和T* Q定义多个指针类型的对象时,Q T和T Q都是整体只需要重复写*即可,但T* Q不是整体,* Q需要重复写。
1 | |
标量类型包括了算术类型、指针类型和空指针类型。
对于标量类型以及与之相关的限定类型Q T,其初始化共有两种形式:1.{},空初始化列表,意味着将对象的值设置为该类型的缺省值,整数是+0或无符号0,十进制浮点数对象缺省值是+0,其他浮点数的缺省值是+0,或者无符号0,指针类型是空指针;2.除逗号表达式外的任意表达式exp或{exp}(包括函数调用),即若初始化式为表达式则是表达式求值后的右值来初始化对象。
对于数组类型对象的初始化:1.{},注意其不适用N不存在的情况,即数组为不完全对象类型时,但是对于变长数组,只能使用{}进行初始化工作,因为对于变长数组来说,运行时才进行空间的分配,不应该显式指出相应的空间大小;2.{sub-initializer-list},其中sub-initializer-list是逗号分隔的初始化子对象的initializer列表,可以嵌套使用。
数组类型对象的designated initializer:
1 | |
对象定义中完整形式的initializer就是这个对象的对象值,用来初始化对象表示。
1 | |
Storage-Class Specifiers
一般来说,对象标识符声明的时候,Storage-Class Specifier只能有一个,除了:
- thread_local可以和static或extern同时出现
- auto可以跟除了typedef的其他Specifier同时出现
- constexpr可以跟auto,register或static同时出现
Storage-Class Specifier规定了标识符(Identifier)的不同属性,也就是改变不同对象的不同属性 - 存储周期:thread_local,auto,register,以及block scope中的static
- linkage: extern,file scope中的static和constexpr,typedef
- object value and type: constexpr
- type : typedef
Identifier(用来指代实体):Object(对象)、Function(函数)、Label Name(goto等)、Tag(结构体名)、Member of Structure(结构体成员名)、Typedef Name、Macro Name(宏名称)、Macro Parameter(宏参数)
同一个标识符,若指代不同的实体,则这些实体要么在不同的Scope,要么在不同的Name SpaceScope:function, function prototype(就是函数原型说明,即声明且没有相关定义的地方), file(全局变量所处的环境), block(函数具体实现内,函数的参数列表或者花括号括起来的区域内),label name是唯一的拥有function scope的标识符
Name Space(一个文件中):
- label name
- tag(结构体名)
- member of structures or unions(结构体/联合体成员变量)
- standard attributes and attribute prefixes
- trailing identifier in an attribute prefixed token
- ordinary identifiers(普通标识符)

Linkage
C语言有三种Linkage:internal、none、external
如果一个对象标识符拥有file scope,且被static或constexpr修饰,或者若一个函数标识符拥有file scope,且被static修饰,则该标识符internal linkage
如果一个标识符具有Internal Linkage,则其他编译单元无法使用这个标识符,有助于程序的模块化
拥有file scope或者被static修饰的标识符会被自动初始化,若两者同时成立,则是因为static而被初始化,而不是因为file scope。
以下三种的linkage是no linkage:
- 对象标识符不指向任何函数或对象,比如:typedef命名的名字
- 函数参数
- 拥有
block scope且没有externlinkage进行修饰的对象标识符
若一个对象标识符此前未出现过或者显式指定extern,则linkage为extern,若此前见过则保持此前的linkage:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28static int a = 10; // internal
int main()
{
int* p = &a;
extern int a; // 还是internal,因为保持此前的链接
a++;
printf("%d", *p);
return 0;
}
static void foo()
{
}
int main()
{
extern void foo(); // foo()仍为interanl,不冲突
return 0;
}
//------------
int main()
{
extern void foo(); // external
return 0;
}
static void foo() // internal,发生冲突,编译出错
{
}
Tentative Definition(暂定定义)
暂定定义满足如下条件:
- File Scope
- 没有初始化列表
- 没有extern或thread_local
多个暂定定义会合并成一个对象定义
多个同样和不同的对象表示的String Literal分配对象
1 | |
Compound Literal定义
基本语法:(type-name){Initializer-list},定义一个匿名对象,对象类型是type-name,由Initializer-list进行初始化。
例子:int* p = &(int){1};.具体存储周期与放置位置有关。
1 | |
- Compound Literal意味着分配一个指定类型的对象
- Compound Literal是一个有效的lvalue表达式
- 定位刚刚分配的那个对象
分配一个对象后马上定位到该对象。(int){1};的对象类型和对象表示值类型均为int,初始化对象值为1。(int[2]){1,2}的对象类型为int[2],对象表示值类型为int*,初始化对象值为{1,2},对象表示值为首地址,typeof((int){1})不是表达式。
对(int){1}这个左值进行求值,同样可以用对象七元组来表示,对其求值的规则与int a一致,即将(int){1}当作一个标识符来理解,例如:++(int){1}: <2, int>、--(int){1}: <0, int>、(int){1}++: <1, int>、(int){1}--: <1, int>。
由于其不为可修改左值,因此可以被修改:
一般情况下这四个(int){1}不是定位同一个对象,但是特殊情况下有可能定位同一个。
对于(const int){1}其对象类型为const int,因此其为不可修改左值,则不能对其进行++/--或赋值等修改值的操作。
内存管理函数
malloc(x)没有对象类型,其对齐要求是Fundamental Alignment alignof(max_align_t),课程假设基础对齐值为16,保证返回值地址可以强制转化成int*、char*等,大小为x,地址已知,其余诸如对象类型、对象表示值和对象表示值类型均未知。该对象的存储周期是allocated。calloc、realloc、aligned_alloc。malloc返回的值需要强制转换成一个有意义的对象类型,因此不能void * p = malloc(4);
如果需要获得指定对齐要求的空间,可以使用void *aligned_alloc(size_t alignment, size_t size);
此处Value未知是由于malloc不进行初始化,因此存储的数据是未知的,且因为以double的视角进行观察所以相应的对齐要求为8.
理解malloc(sizeof(Obj_T)*N):
1 | |
形式化定义:假设一个对象类型Obj_T,malloc申请N个Obj_T大小的内存可形式化定义为
Obj_T* p = (Obj_T*)malloc(sizeof(Obj_T)*N)
可视为分配了一个Obj_T[N]对象类型空间
例:
使用malloc直接分配高维数组,如上,的工程扩展性较差。
工程上创建高维数组:
不同高维数组实现方式对比:
malloc(sizeof(int))隐含的语义是分配了一个int[1]类型的空间
指针类型总是蕴含了一个对数组元素的访问
执行了多少次malloc,就需要执行多少次free。
示例:
1 | |
左值
左值表达式
1.对象标识符;2.String literal;3.Compound Literal
用来定义对象,因此左值(located value)是能定位对象的表达式(一系列operator和operand组成的一个序列)的总称。
C语言中一共有17种表达式。
其中,左值只有以下几种形式:对象标识符、数组下标运算表达式、*exp,String Literal、Compound Literal、指向结构体/联合体的lvalue.member,指向结构体/联合体指针表达式->member
表达式要根据语法的规定来进行求值。
给定表达式过程:1.求出计算值(右值),用来帮助理解语法;2.确定副作用(是否对环境状态产生改变),用于帮助理解优化。

1 | |
Sequenced Before
一种非对称、可传递的成对Evaluation之间的关系
A sequenced before B的含义是:
A的evaluation在B的evaluation之前
与A相关的Valuation Computation和side effects全部在与B相关的Valuation Computation和side effects之前
C语言定义了一系列sequence point来规范sequenced before这种行为
A sequence point B保证A sequenced before B
sequence point:
1、Function Designator和实参的evaluation,和实际函数调用执行之间插入一个
2、在&&、||、逗号运算符分隔的前后两个表达式之间
3、?:三目运算表达式中,?之前表达式以及之后执行的表达式之间
4、两个full expression之间,full expression包括例如:
表达式语句(分号)、if、switch、while、do的控制语句、 return(exp)中的exp,
for(exp1;exp2;exp3)中的expi,变长类型的声明,非compound literal中的initializer
5、库函数调用返回之前,保证调用完成后才返回
6、printf/scanf、fpritnf/fscanf、sprint/sscanf等按转换说明符执行完转换动作之后
7、bsearch,qsort等比较函数调用之前和之后以及调用比较函数和对象移动之间
Sequence Points之间表达式内执行顺序是如何规定的呢?
除了显式的语法规定,表达式内子表达式的evaluation之间顺序关系没有约定(UB的来源)
对1个标量(指针类型、算术类型和nullptr_t)对象(Scalar Object),如果
• 产生两次副作用且两次副作用没有先后顺序要求
• 产生的副作用和同样标量对象取值之间没有先后顺序要求
则其结果是undefined behavior,例如int i=1; i = ++i + 1; int i=1; a[i++] = i;
1 | |
对于左值表达式,其会定位一个对象,对象在物理空间中会有一个对象表示,相应的会产生一个对象值(Object Value),而经过左值表达式求值后会有一个右值,因此讲述某个对象标识符的时候不确定是哪一个值,一定情况下认为是变量是因为可以修改对象值进而使右值发生变化,导致认为可以修改。
两个对象表示一样则对象值一样,反之不正确,peddling bits的存在导致的。
对象值一样,作为表达式求值后的右值不一定一样。
常量
整常量、浮点数常量、枚举常量、字符常量和预定义常量。
整数常量
三部分组成:进制前缀、数字序列和类型后缀。
数字序列还支持在数字中间插入字符“’”,也叫数字分隔符,“’”不能出现在数字序列的第一个数字之前,也不能出现在最后一个数字之后
前缀有0b/0B(二进制)、无、0(八进制)和0x/0X(十六进制)。
后缀有u、U、l、L、ll、LL和wb、WB(单独使用或联合使用,包括无、U、L、LL、WB、UL/LU、ULL/LLU、UWB/WBU等,不同后缀规定了不同的整数类型序列,按进制编码的字符序列从上到下第一个能覆盖的其表征的值的类型就是这个整数常量的类型),例如:对于整数常量2147483647,若其无前缀和后缀,则其是一个十进制整数常量,查表可知其类型为int。否则对于2147483648,若平台long比int大,且无前后缀,则类型为long int。
表如下:
整数常量即为基础表达式,求值的右值就是其表征的值,类型为按表查出来的类型,也即整数常量的类型。
**不考虑BitInt系列,整数常量的类型最小是int(整数提升)**。
此外数字序列还支持在数字中插入字符”‘“,即数字分隔符。
整数常量表达式的值都不会变,这只是一个基本条件,但是如果不能用在整数常量能用的地方,就不是整数常量表达式,比如int[10],这个10是一个整数常量,表示这个是一个ordinary array type,那么int[(int)(+10.0)]里面的这个(int)(+10.0),是一个强制类型转换表达式,值的类型也是int,但是(int)(+10.0)不是一个整数常量表达式,所以int[(int)(+10.0)]不是ordinary array type,而是一个VLA type(变长数组类型)
浮点数常量
进制前缀+符号序列+后缀,有两种:十进制浮点数(无前缀)和十六进制浮点数(0x前缀),后缀约定了浮点数常量求值后的右值类型,例如:1.0f类型为float,1.0类型为double。
十进制浮点数常量符号序列由三部分组成:一个整数部分、小数点和小数部分组成的十进制实数、一个e和标识幂的+/-。
1、实数部分不可省略,但在实数里面,整数部分如
果没有,则表示整数部分的值为0,小数点或小
数部分也可以没有,表示这个实数没有小数部分
2、浮点数常量通过e以及后面的指数来表达10的幂
次方,以科学计数法的形式表征浮点数的值
3、实数部分的整数和小数部分,以及指数部分,也
可以使用“’”这个数字分隔符(Digital Separator)
十六进制浮点数常量:前缀0x,实数部分以十六进制表示,e换成p,幂值由十进制表示表示的是2的幂次方,其中p和指数部分不可省略,可以使用'这个数字分隔符。0x100.5p0 // 256.3125(1*256+5*1/16)*2^0、0x0A.Bp2 // 42.75 (10+11*(1/16))*22.
枚举常量
例如:enum Season {Spring, ...}的右值类型为enum Season,Underlying Type由编译器决定。
enum Season {Spring, Summer, Autumn, Winter};
Springer、Summer、Autumn和Winter
被称为枚举常量,值分别是0、1、2和3。
enum Season {Spring=2, Summer, Autumn=6, Winter};
4个常量的值分别就是2、3、6和7
rvalue类型依然是enum Season
字符常量
两部分组成:编码前缀(无(int)、u(char16_t)、U(char32_t)、u8(char8_t)和L(wchar_t))+单引号引导的有效字符,其无编码的字符类型为int:例如:a的右值类型是int(易错)。
1、在一对单引号里面引导的除了字符,还可以是一个以反斜杠’'引导的八进制
的整数,这个八进制的整数数字串的长度可以是1到3位
1、’\061’:十进制值是49,字符’1’的ASCII编码是49,因此’\061’就是字符’1’。
2、’\61’:十进制值是49,字符’1’的ASCII编码是49,因此’\61’也是字符’1’。
反斜杠引导的八进制整数长度不一定必须3位,但建议都使用3位数字
2、在一堆单引号里面的也可以是一个十六进制的整数,以反斜杠’\x’引导
1、’\x31’:以十六进制整数表示的字符,十进制值是49,依然是字符’1
预定义常量
false、true(分别为0、1,右值类型为bool)和nullptr(一个空指针常量,类型为<stddef.h>中定义的nullptr_t类型)
字符串
字符串分配或重用的对象类型是字符数组类型,分配出来即定位相应的对象,则其对应求值规则和数组类型一致
例子:
1 | |
对于字符串进行求值
定位刚分配或重用那个字符数组对象,其不做求值的类型与数组对象一致
字符数组对象的定义问题
初始化char str[8] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}
1、 ‘h’, ‘e’, ‘l’, ‘l’, ‘o’用于初始化前5个byte
2、 后面3个’\0’(null character)是自动填充的
初始化char str[8] =“hello”

1 | |
括号表达式
(exp)等价于exp,但是优先级更高。
后缀表达式和一元表达式
后缀表达式
[]:exp1[exp2].:exp.identifier,结构体变量索引成员变量->:exp->identifier结构体指针索引成员变量++/--:a++(type-name){Initializer-list}:(int){2}, (int[3]){1,2,3}():exp()(函数调用)
一元表达式
- ++/–:
++exp - &,任何对象的地址都不会变。
- *:
*exp - +/-
- ~/!
- sizeof和alignof
非数组对象左值的求值规则
有7种情况不做左值求值:
- 与
sizeof结合,对象的大小由对象类型可知,编译时可推断出来,对象size在生命周期内不变化。 - 与
typeof结合; - 与
alignof结合(gcc扩展情况下可以,但是标准不支持); - 作为赋值语句左侧的被赋值体,
exp =; - 跟一元运算符++/–或后缀运算符++/–结合:
exp++/exp--/++exp/--exp; - 与&结合,如果左值定位对象的存储周期为
static,则其对象地址编译时可知,否则地址运行时才可获得 - 若左值定位的对象类型是结构体或者联合体,跟.结合:
exp.
除了上述情况,都进行求值,求值是都向对象七元组中取相应的值和类型,例如对于:
&a; // 应该取<Address, Obj_T*>。++a; //取<Value incremented by 1,V_T>, ++的语义与V_T有关,--a同理。a++; //取<Value, V_T>,a--同理。typeof(a); // 取<Obj_T>,其本身不是表达式,int a = 1;typeof(a) x; <=> int x;。a;//取<Value,V_T>。sizeof(a); //取<size,size_t>。alignof(a); //取<Alignment,size_t>
定位指针类型对象的左值

int *p=&a中对p进行求值:
p;求值,取<V,V_T>*p,用于定位对象,间接定位内存。若p求值后的右值类型为一个对象指针类型,则可用*p来定位到右值值对应字节编号开头的一段内存。- 假设对表达式exp进行evaluate,rvalue为<Value, Value_Type>
- 如果Value_Type是一个对象指针类型,则可以用*exp的方式来定位一个对象M
- 1)该对象的Address为Value
- 2)该对象的Object Type为Value_Type对应的Referenced Type(即去掉一个*后的类型)
- 3)对象其他属性随之确定

对于*p同样可进行相应的求值:<0x0068FE10,int*> :int* p = &(*p);<4, size_t>:size_t x = sizeof(*p)< 1, int>:int x = *p;<4, size_t>:size_t x = alignof(*p)
其只是定位一个对象,不是获得*p表达式的值,值只有在求值后才有。
正因此这个的存在,由于a和*p都是lvalue,指向同样一个对象,且对象类型相同
a和*p在行为上是完全一致的,如果称前者为变量,那么后者是否也为变量呢?
- 若
p:<V, V-T>,则p+n:<V+sizeof(*p)*n, V-T>,注意:***(exp+n) <=> exp[n]*。由于exp1+exp2或exp2+exp1这个表达是的rvalue的类型依然是一个对象指针类型(exp1+exp2)或者*(exp2+exp1)依然可以用之前*exp的规则来定位一个对象将exp1+exp2或者exp2+exp1视做exp即可 - 给定两个表达式,只要其中一个表达式evaluate后rvalue的类型是一个有效的对象指针类型,而另一个表达式evaluate后rvalue的类型是一个合法的整数类型,则这两个表达式就可以用[]的方式进行对象定位,但C语言并没有规定哪一个必须放在[]里面。因此:
exp1[exp2] <=> exp2[exp1],则p[0] <=> 0[p] - 指针表达式的
-操作,假设对表达式exp1进行evaluate,rvalue为<Value, Value_Type>(任何表达式都有rvalue);假设对表达式exp2进行evaluate,rvalue的value_type为一个整数类型。则:只能exp1-exp2,或者视为exp1+(-exp2)或(-exp2)+exp1;int* p;;*(p-1)等价于p[-1]或(-1)[p],两个指针相减,指针类型必须一致。假设p: <Value1, int*>, q: <Value2, int*>,则p – q: <(Value1-Value2)/sizeof(*p), ptrdiff_t>,注意两个指针相减的表达式的rvalue类型为ptrdiff_t,此处即为int,也就是typeof(*p)1
2
3
4
5
6
71、int(*p)[2][3],p+2的rvalue相对于p的rvalue的offset是多少?
2、int (*r)[30], int (*t)[30], r-t=?
假设t的值是<0x0035AB10, int(*)[30], r的值是<0x0035AC00, int(*)[30]>
答案
1、Offset是48,因为sizeof(*p)的返回值是<24, size_t>
2、r-t的值: <2, ptrdiff_t>
(0x0035AC01-0x0035AB11)/sizeof(int[30]) = 2;
数组对象左值的求值规则
以下情况不进行求值:
- 跟
sizeof结合,由对象类型可知,编译时可推断,声明周期内不变化 - 跟
typeof/typeof_unqual结合; - 跟
&结合,若左值定位对象的存储周期为static,则对象地址编译时可知 - 若左值定位的是一个
String Literal且用于初始化一个字符数组,如:char str[] = "hello"
此外都要进行求值处理。
例如:对于int b[1] = {1}; &b取<Address, Obj_T*>,比如<0x..., int(*)[1]>,因此,将&b赋值时应该对应的变量是int(*)[1]类型:int(*x)[1] = &b.
b; //取<Value, Value_Type>sizeof(b); // 取<size, size_t>typeof(b); // 取<Obj_T>,若int b[1] = {1};则typeof(b) <=> int [1],typeof(b) x <=> int x[1]; 该式不为表达式&b; // 取<Address, Obj_T*>alignof(b);// 与非数组类型一致b++和b=NULL为什么不行,因为其值与Address永远一样,其是不可修改左值,而非因为b为常量。
此处注意:其中特别注意sizeof和typeof,特别注意区分lvalue是VLA(变长数组类型)的时候,作为sizeof和typeof的operand,是需要做evaluate的
高维数组左值求值方式与一维一致。
泛型选择
不可修改左值(指的是程序员不可通过这个左值对其进行修改,不一定不能修改,并不绝对,从对应的左值类型来判断,其含义在于不能整体赋值,例如:int e[2];,e对象类型为int[2]数组类型,无法对8个字节整体赋值,而e[0]或e[1]对象类型为int,是一个整体,可以进行赋值)
- 数组类型
const限定类型- 不完全对象类型
- 结构体/联合体类型,但其成员对象类型有
const限定类型,即只要递归成员有const限定符,则整个对象都不能修改




需要考虑对象属性中的对象类型进而来判断是否为不可修改左值,推荐使用Obj_T const O的形式
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int const a=1; int* p=(int*)&a; *p=10;
/* a是不可修改左值,所以a=10会出现编译错误
&a返回值类型应该int const*
强制转换成int*并赋值给p
*p=10会发生什么?
*p是可修改左值,但*p=10是一个未定义行为
可修改左值和不可修改左值是程序员视角,和对象是否真的能改无关
*/
/*
“hello”是lvalue,不可修改左值,对象类型是char[6]
“hello”做evaluate之后rvalue type是char*
“hello”[0]是lvalue,是可修改左值,对象类型是char
“hello”[0]=‘a’;是未定义行为
“hello”定位的对象内容不是不可以改,而是试图修改会造成UB
重要:字符串不是常量
*/
地址常量
- 空指针
- 指向一个
static storage duration对象的指针 - 指向
Function Designator的指针。
从右值角度定义的,与从表达式定义的常量(5种表达式统称)不同。
地址常量是特定表达式求值后的结果,可以获得地址常量的表达式统称为地址常量表达式
获得方法
1、显式使用一元操作符&来获得的rvalue
2、显式将整数常量强制转换成指针类型获得的rvalue
3、隐式使用数组类型表达式获得的rvalue
4、隐式使用函数类型表达式获得的rvalue
1 | |
一维数组
1 | |
函数类型说明:1、函数返回值类型;2、参数数量和类型
Type-name(argument1-type, argument2-type…, argumentn-type)
func函数的函数类型是int(int, int)
函数类型是派生类型,是基于返回值类型的派生
返回值形式可以是T, T*, Q T, T Q, T* Q,派生规则,即分别在T、、T、Q、Q之后添加()引导的函数类型即可,注意是严格添加:
相应的函数标识符摆放在()引导的参数类型列表之前,最后加上{}引导的函数内容。
返回值类型必须是非数组对象类型(包括其限定类型)或者*void
函数类型不是语法上的整体的,因此有设置别名的需求:使用typedef设置时,将别名设置在()引导的参数类型列表之前,例如:typedef T Alias(...)/typedef T* Alias(...)/typedef Q T Alias(...)/typedef T* Q Alias(...)
函数类型的指针类型:
Type-name()(argument1-type, argument2-type…, argumentn-type)
*在表示函数参数的()之前加上()*
示例:int(int,char) <=> int(*)(int, char),或typedef int Func(int,char); Func<=>Func *
因此,int(*)(int, char)也是一种指针类型,也有对应的指针类型int(**)(int, char)
还有相应的数组类型:int(*s[5])(int, char):对象s的类型是一个数组类型5个元素,每个元素类型是int(*)(int, char)
1 | |
函数定位表达式
是可以定位一个函数的表达式,一共只有两种函数定位表达式:
1、Function Identifier(函数标识符)
2、如果一个表达式exp,其rvalue类型是函数指针类型,则*exp是一个合法的Function Designator
因此有如下类似规则:
其中,func;//取<Value,Value-T>,typeof(func) //取Func_T,但不是表达式,且typeof(func) x;//不合法缺少{},typeof(foo)得到的是函数类型,函数类型不能用来声明对象标识符x,但是typeof(func)* x;// 合法,&func; // 取<A,Func_T*,则有int(*x)(int, int) = &func;>,可见func和&func取值之后的结构都一致<A,Func_T*>,&(&func) // 不合法,因为&func既不是左值也不是函数定位表达式,不能再取址。
函数类型不能跟alignof和sizeof结合.
类似的,对于*func; // 取<Value, V-T的Ref-type>,因为<V,V-T>=<A,Func_T*>则*func定位对象表示值为func的地址,对象表示值类型为Func_T的对象,即直接又定位该函数
同理,*(&func)同样定位到func函数,*(*func)同样定位到func函数。 
1 | |
注意:函数标识符本身不是函数指针
函数调用
1、对exp表达式evaluate之后rvalue是指向func函数的指针,且
2、arg1和arg2进行evaluate之后rvalue类型符
合func函数对应参数的对象类型,则
exp(arg1, arg2)称为函数func的函数调用,函数调用是一个后缀表达式,函数调用这个表达式求值后的右值类型是函数类型中的返回值类型,因此只有函数调用这个后缀表达式的右值有可能是特殊类型void,而其他表达式求值后返回的都是非数组的完全对象类型,函数调用返回的若是结构体/联合体类型,则支持.操作。
1 | |
参数传递
传入的全是表达式的值,实参要按要求进行求值,实际传递的就是实参evaluate之后的rvalue,因此形参中没有数组类型,即所有类型求值后的V-T都是非数组类型,实参传递的机制就是传值,只有pass by value。


1 | |

结构体类型的对象及对象值
结构体/联合体类型是派生类型给,也是非数组类型。
1 | |

其中m、m.a、m.b都是lvalue
&m, &m.a, &m.b都是合法的
值不能打印出来,但是仍然存在。
例外,其中foo()虽然不是左值,但是可以引用相应结构体的成员变量
1 | |
size、padding和alignment
size
给定一个完全对象类型T,声明一个对象O(即T O):分配了一系列字节,这段内存的对象类型为T,且用O这个标识符可以定位,注意:O不是对象(O是标识符,是表达式),这段内存才是对象
sizeof(T)返回值的含义是为一个类型T的对象分配空间需要多少个字节
sizeof(O)返回值的含义是用标识符O来定位的那个对象共占用了多少个字节
padding
无符号整数
假设一个无符号整数类型T,一共占用n × CHAR_BIT个bit位,分为值位和填充位,若值位个数为N,则该对象范围为0-2^N^-1,N为这个类型T的宽度,则sizeof(T)返回值为<n, size_t>。
unsigned char类型不允许有padding bits,其他无符号整数类型可以没有padding bits。
有符号整数
假设一个有符号整数类型T,一共占用n × CHAR_BIT个bit位,sizeof(T)返回值为<n, size_t>
除了值位和填充位,还有符号位。
假设sign bit+value bits的个数是N,该有符号整数对象表值范围为-(2(^N^-1))~ 2(^N^-1)-1
这个N值就被称为这个有符号整数类型T的宽度
signed char类型不允许有padding bits,其他有符号整数类型可以有padding bits
1、sizeof(unsigned char)/sizeof(signed char)恒等于1
2、unsigned char/signed char类型不允许有padding bits
结论:signed/unsigned char的大小一定是1个byte,且没有padding
例如对于一个unsigned char类型的对象,其取值范围为 0 to 2^CHAR_BIT^− 1
int类型的sign bit+value bits长度必须大于等于16,目前主流编译器的int类型都没有padding bits,但是依然不能假设所有int没有padding bits
intN_t/uintN_t
C语言标准规定编译器可以定义一类1)没有padding和2)确定宽度的整数类型Exact-width integer types,形如intN_t,uintN_t
例如int16_t:意味着这个有符号整数类型没有padding bits且width恰好等于16
C语言标准不要求编译器必须提供intN_t类型
但如果编译器提供了宽度为8,16,32和64,且没有padding的整数类型
则应该通过typedef提供相应的intN_t类型
例如某平台int类型width是32且没有padding bit
则应该通过typedef int int32_t定义出int32_t类型
int_leastN_t/uint_leastN_t
C语言标准规定编译器需要定义一类宽度至少是某个N值的整数类型Minimum-width integer types,形如int_leastN_t,uint_leastN_
注意:如果编译器定义了intN_t,那么int_leastN_t和intN_t一样
int_fastN_t/uint_fastN_t
C语言标准规定编译器需要定义一类宽度至少是某个N值且处理速度最快的整数类型Fastest minimum-width integer types,形如int_fastN_t,uint_fastN_t。
注意:这个fast并不保证所有情况下处理速度都是最快,编译器可以简单选择满足符号要求和宽度要求的整数类型来进行typedef
alignment
对齐值必须是2的n次方,例如1、2、4、8、16、32
对齐的要求和编译器、硬件系统等紧密相关
各类编译器对同样的数据类型可能有不同的对齐要求
利用alingof(T)/_Alignof(T)可以获得对象类型T的Alignment
利用alignof(O)/_Alignof(O)可以获得对象O的Alignment(标准不支持)
对象O的Alignment缺省等于对象类型T的Alignment
_Alignas修改对齐要求
规定编译器必须支持的对齐叫做fundamental alignment
fundamental alignment <= _Alignof(max_align_t)**
max_align_t是一个类型,拥有最大的基础对齐要求
这个值由实现去约定,目前主流编译器一般是8或16
当声明一个T类型的对象O,可以为该对象设置更大(Stricter)的对齐要求:alignas(N)或者_Alignas(N) T O
**N >= alignof(T) (注意:如果N大于alignof(max_align_t),编译器决定是否支持),使对齐要求更加严格。
思考:alignof(O) 总是等于alignof(T), 不一定
修改对象的alignment不改变对象类型的对齐大小,也不改变对象的大小
结构体类型的对齐要求
当声明一个T类型的对象O,如果T是一个结构体类型,成员对象分别是Ei_Alignof(T) = max{_Alignof(Ei), 1<i<=结构体成员个数},结构体类型的对齐要求是成员类型中对齐要求最大的那一个值
1 | |
结构体对象的size
一个结构体类型T,成员对象分别是Ei,1<i<=n (假设有n个成员对象)
_Alignof(T)是这个结构体类型T的对齐要求
结构体第1个成员对象E1的首地址就是结构体的首地址,地址偏移量offset为0;
结构体第2个成员对象E2的地址偏移量确定方法如下
E2的首地址偏移量:
1 | |
#pragma pack(n)调整结构体对象的对齐
利用#pragma pack(n)可以进一步调整结构体的对齐要求,规则如下:
当声明一个T类型的对象O,如果T是一个结构体类型,成员对象分别是Ei
利用#pragma pack(n)可以进一步调整结构体的对齐要求,规则如下:
1、_Alignof(Ei) = min{_Alignof (Ei) , n}
2、_Alignof(T) = max{_Alignof(Ei), 1<i<=结构体成员个数},注意此处为在规则一修改后的成员变量类型的对齐要求的最大值
1 | |
Compound Literal+结构体类型
Compound Literal用于构造匿名对象,特别是结构体类型很有用
1 | |
指针转换中的对齐问题
指针的强制转换隐含着地址对应的对象类型发生了变化,如果转换后的指针对应的对象对齐方式不正确 ,则指针的强制转换行为是未定义行为
1 | |
register
register T O,
告诉编译器越快越好,但编译器可以不理会,
register修饰的对象不能取地址,因为其修饰对象不一定在内存中。register不能修饰数组对象,因为对其修饰的对象求值后不一定存在首地址,则后续相应的访问无从谈起,也是UB。
auto
auto O = initializer;Initializer必须有,且只能是赋值表达式或更高优先级表达式,对象O的类型是initializer这个表达式rvalue的类型
1 | |
关于算数类型在表达式内适配问题
常规/常用/标准算术转换
当有表达式涉及多个算术类型子表达式,该机制决定算术类型子表达式rvalue类型转换规则,并决定整个表达式rvalue类型如何获得.
与浮点相关规则
- 如果一个operand是decimal floating type,另一个operand也必须是decimal floating type确保十进制浮点数不和非十进制浮点数一起运算
- 如果一个operand是_Decimal128,另一个operand也提升为_Decimal128
- 如果一个operand是_Decimal64,另一个operand也提升为_Decimal64
- 如果一个operand是_Decimal32,另一个operand也提升为_Decimal32
- 如果一个operand是long double,另一个operand也提升为long double
- 如果一个operand是double,另一个operand也提升为double
- 如果一个operand是float,另一个operand也提升为float
与整数相关,整数提升
优先级:
long long int = unsigned long long int >
long int = unsigned long int >
int = unsigned int >
short int = unsigned short int >
signed char = unsigned char = char >
bool
规则:
依次满足,从1向4规则去考虑,只有满足则计算整型提升结束
- 如果两个operand都是有符号或者都是无符号,less rank的operand向greater rank转换
- 如果无符号类型operand的rank大于等于另一个有符号operand的rank,有符号operand转换成无符号operand的类型
- 如果有符号operand的类型表征范围能覆盖另一个无符号operand类型的表征范围,无符号operand转换成有符号operand类型
- 两个operand都转成有符号operand类型对应的无符号类型operand类型小于int/unsigned int规则:
1
2
3
4unsigned int a = 1;
int b = -1;
if(a>b)
printf("hello"); // 无法正常打印,unsigned int和int的rank一样,有符号operand转换成无符号的unsigned int类型
bool、signed char、unsigned char、signed short、unsigned short,operand如果是以上类型 - 如果int类型能表征,转换成int类型
- 如果int类型不能表征,转换成unsigned int类型
unsigned char a, b;
a+b这个表达式的rvalue类型是什么?
如果unsigned char是8位,int是32位,rvalue类型是int
如果unsigned char是16位,int是16位,rvalue类型是unsigned int

