一些不一定的概念

  1. 一比特不一定是8bit,已经有16bit的机器了,但是至少包含8个,定义了一个宏CHAR_BIT,规定其≥8。
  2. 字节是C中基础存储单元,每个字节都有一个编号,即地址
  3. 内存为一系列字节顺序排列一起组成的存储结构,地址是字节在内存中的编号,只有字节才有地址

关键字

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]) PAINTint*[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

类型

算术类型

都是完全对象类型。
alt text
基础类型,除了枚举类型以外的算术类型。
charsigned charunsigned char三者互不相同。编译器规定char类型与后两者之一的行为一致。
signed charunsigned 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 intintlong intlong long int)也可以没有填充位(为了向后兼容的方便)**。 bool类型是标准无符号整数类型的一种,宽度恰好为1,但是具体的size由编译器决定,使用sizeof获取,则其padding bitssizeof(bool)*(CHAR_BIT - 1) char的有无符号类型的对齐要求是最小的,但是**不一定为1**. **任何类型的对齐要求都要使用alignof()`获取

枚举类型实际的实现类型有两种方式决定:

  1. 编译器决定(不常用),自行从char、标准有符号整数类型、标准无符号整数类型、扩展有符号整数类型、以及扩展无符号整数类型5种中选择一个设置。
  2. enum后面跟一个整数类型(常用):enum Season : int {...}

派生类型

指针类型

任何类型都有指针类型。
形式化定义:T*:1. T必须为一个对象类型或者函数类型,2. T*视为T的派生类型。
由于T[N]不是语法上的整体,所以T[N]*不是合法的类型,其指针类型应为**T(*)[N]**。
任何指针类型都是完全对象类型
指针类型也有自己的指针类型。
如何将T、T[N]、T*分别派生为数组类型和指针类型:

  1. T -> T[N]:数组类型
  2. T -> T*:指针类型
  3. T[N] -> T[M][N]:多维数组类型,在[N]前加[M]
  4. T[N] -> T(*)[N]:指向数组类型的指针,在[N]前加 *
  5. T* -> T*[N],指针派生数组类型,在*后加[N]
  6. T* -> T**:指向指针类型的指针,在 *后加 *

例:

1
2
3
4
5
6
7
1int,派生指针类型int*
2int*派生数组类型,5个元素,int*[5]
3int*[5]派生数组类型,4个元素,int*[4][5]
4int*[4][5]派生指针类型,int*(*)[4][5]
5int*(*)[4][5]派生指针类型,int*(**)[4][5]
6int*(**)[4][5]派生数组类型,元素个数不指定,int*(**[])[4][5]
7int*(**[])[4][5]派生指针类型,int*(**(*)[])[4][5]

标量类型包括算术类型和指针类型、以及空指针类型(nullptr_t)
int* p = &a;中的*p就是一个左值,与其余左值没有任何差别,可以理解成一个匿名对象。p应该理解成一个数组的首元素的首地址,
指针类型永远蕴含着对于数组的访问

若表达式exp求值后的右值为<Value,V-T>,且V-T为一个对象指针类型,则可用* exp来定位到Value对应字节编号开头的一段内存,定位对象M该对象的地址为ValueO-TV-T对应的Referenced Type(即去掉一个*),其他对象属性相应确定下来。
alt text

任何表达式求值后能得到一个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
2
3
4
5
// 如下均为变长数组
int a[m]; //一维数组,大小为m,元素类型为int
int a[m][2]; //一维数组,大小为m,元素类型为int[2]
int a[2][m]; //一维数组,大小为2,元素类型为int[m]
int a[m][m]; //一维数组,大小为m,元素类型为int[m]

常量表达式:编译时进行求值,像常量一样使用,限制条件:1.不包含赋值、自增/自减,函数调用,逗号运算符,除非他们包含在不被求值的子表达式中;2.求值后右值的取值范围,应该在右值类习惯的表征范围内。

整数常量表达式:1.表达式右值类型为整数类型;2.能在编译时进行求值,像整数常量一样使用。

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
28
// C23增加了一个新的关键字constexpr
constexpr int n = 10;
constexpr float f = 1.0;
typedef struct str {
int a;
} STR;
constexpr STR x = {10};
// 这时候的n(const int)、f(cosnt ...)和x(const ...)都是常量表达式,被称为named constant,剧名常量

enum Season {Spring, Summer, Autumn, Winter};
/* Spring、Summer、Autumn和Winter也是named constant
true、false、nullptr也是named constant
*/
typedef struct str {
int a;
} STR;
(constexpr STR){10}; // 对象类型为const STR
(constexpr STR[1]){10}; // 对象类型为const STR[1]
// 被constexpr修饰的Compound Literal被称为Compound Literal Constant
(constexpr STR){10};
(constexpr STR[1]){10};

int m = 1; //m是不是整数常量表达式?
// 不是,常量表达式能在编译时进行evaluate
(int)(+5.0);//是不是整数常量表达式?(int)(x)为cast表达式,而+5.0是一元表达式,因此不满足operand要求,不是整数常量表达式
1?1:(+5.0);//是不是整数常量表达式?一样的道路也不是整数常量表达式
// 不满足Operand的要求

注意:满足以下条件为整数常量表达式,是一个基础表达式由进制前缀、数字序列和类型后缀三部分组成,而整数常量表达式是一种表达式,整数常量表达式的值都不会变,这只是一个基本条件:

  1. 表达式rvalue类型为整数类型
  2. 操作数是
    1. 整数常量
    2. 字符常量
    3. 类型为整数的named constant
    4. 类型为整数的compound literal constant
    5. rvalue类型是整数常量的sizeof表达式
    6. alignof表达式,algnof(type-name)都是整数常量表达式
    7. cast表达式,其中cast的子表达式是浮点常量、类型为算术类型的named constant、compound literal constant,除非该cast表达式是typeof、sizeof或alignof操作符的操作数

alt text
其中注意:+10是整数常量表达式,但不是整数常量,其中10是整数常量,因此满足操作数是整数常量,且表达式右值类型为整数类型,因此int[+10]不是VLA,但是int[(int)(+10.0)]是VLA,因为(int)(x)为cast表达式,且其中子表达式x,即(+10.0)不是浮点常量、类型为算术类型的named constant、compound literal constant中的那几种。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
int a[1+2];
int b[1+'a'];
int c[1+(int)5.0];
// 上述都不是VLA-------
int a[2][1+2];
int b[2][1+’a’];
int c[2][1+(int)5.0];
int i = 1;
sizeof(a[++i]);
sizeof(b[++i]);
sizeof(c[++i]);
// 上述i都为1,因为++i均未被求值,a[++i]、...都是左值,且都不为VLA,
int i=1;
int a[2][sizeof(int[5])]; // sizeof(int[5])是整数常量表达式
sizeof(a[++i]); // a[++i]是lvalue定位的对象类型是int[20],非VLA,++i不做求值
printf("%d\n", i); // 1
printf("%zd\n", sizeof(a));//160
// ----------------------
int i=1;
int m=5;
int a[2][sizeof(int[m])]; // sizeof(int[m])返回的不是整数常量,则其不是整数常量表达式,
sizeof(a[++i]); // 需要对子表达式进行求值
printf("%d\n", i); // 2
printf("%zd\n", sizeof(a)); // 160
// -----------------------
int i=1;
int m=5;
int a[2][sizeof(int[++m])]; // sizeof(int[++m]),其返回的不是整数常量,不是整数常量表达式,且++m要做evaluate,++m的rvalue等于6
sizeof(a[++i]);
printf("%d\n", i); //2
printf("%zd\n", sizeof(a)); // 192
// --------------
int i=1;
int m=5;
int a[sizeof(int[++m])][2]; //a是VLA, sizeof(int[++m])不是整数常量表达式,++m要做evaluate,++m的rvalue等于6
sizeof(a[++i]); // 但是a[++i]定位的对象对象类型是int[2],不是VLA,不用求值
printf("%d\n", i); //1
printf("%zd\n", sizeof(a)); // 192
//- ------------
int i=1;
int a[2][1+(int)(+5.0)]; // 1+(int)(+5.0)不是整数常量表达式, 1+(int)5.0是常量表达式,注意区别
sizeof(a[++i]); // 定位的对象是int[1+(int)(+5.0)]是VLA类型,需对++i求值,则i=2,得到a[2][6]
printf("%d\n", i); // 2
printf("%zd\n", sizeof(a)); // 48
//-----------------------------
const int m=5;
//其中m不是常量,若为,则下列++i均不会被求值
// 但实际上都执行了
int i=1;
const int m=5;
int a[2][m];
int b[2][sizeof(int[m])];
sizeof(a[++i]);
sizeof(b[++i]);
printf("%d\n", i); // 3
printf("%zd\n", sizeof(a)); // 40
printf("%zd\n", sizeof(b)); // 160
// ------------------------
int i=1;
int m=5;
int a[2][alignof(int[++m])]; // a不是VLA,alignof(int[++m])返回整数常量表达式
sizeof(a[++i]);
printf("%d\n", m); // 5
printf("%d\n", i); // 1
// --------------
int i=1;
int a[2][5];
sizeof(a[++i]); // a[++i]是lvalue,定位对象为非VLA
printf("%d\n", i); // 1
sizeof(a[++i]+1); // a[++i]+1为non-lvalue
printf("%d\n", i); // 1,非左值表达式,直接是右值类型的size大小
// ------------------
int i=1;
const int m=5; // 若加上constexpr,则m为整数常量
int a[2][sizeof(int[m])]; // a[++i]是lvalue,定位对象为VLA
sizeof(a[++i]); // 因此此时对i++求值
printf("%d\n", i);
sizeof(a[++i]+1); // 非左值表达式,直接为右值类型的size
printf("%d\n", i);
char c = 'a';
sizeof(c); //1
sizeof(c++); //1
sizeof('a'); //4,无编码字符的类型是int,视为整数字符常量
// ----------------
int i=1;
int j;
int a[5][j=10]; // j=10不是整数常量表达式
sizeof(a[++i]);
printf("%d", i); // i = 2
通过sizeof()理解VLA

可以跟两种operand:type-name(sizeof(type-name))和exp(sizeof(exp) / sizeof exp;sizeof(non-lvalue-exp) /sizeof non-lvalue-exp)
对于VLA,typeof()处理跟sizeof()逻辑一致

  1. 考虑sizeof(type-name),
    1. 且type-name不为变长数组类型,sizeof(type-name)在编译时得到type-name的大小,其返回值是整数常量,type-name作为operand,整体不求值,例:sizeof(int[2+3]),2+3不做求值。
    2. 如果type-name是VLA,则sizeof(type-name)不能在编译时得到type-name的大小,其返回值不是整数常量,需要在运行时得到类型大小,type-name作为operand,子表达式需要求值,例:int m = 5; sizeof(int[m]); sizeof(int[++m]);
  2. 考虑sizeof(lvalue)
    1. 如果lvalue定位的对象类型是Non-VLA,则sizeof(lvalue)在编译时得到lvalue定位对象的大小,其返回值是整数常量lvalue作为operand,整体不做求值,例如:int a[10][10]; sizeof(a); sizeof(a[0]);
    2. 如果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]进行求值
  3. 考虑sizeof(non-lvalue)
    1. sizeof(non-lvalue)返回这个non-lvalue做求值之后右值类型的大小
    2. 这个non-lvalue并不会真的做求值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      int 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的限定原子类型,大小和缺省条件都使用sizeofalignof来获取。_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
2
3
4
5
6
7
8
9
1、给定int、其const限定类型是const int/int const,这里以const int为例
2const int,派生指针类型const int*
3const int*派生数组类型,5个元素,const int*[5]
4const int*[5]派生数组类型,4个元素,const int*[4][5]
5const int*[4][5]派生指针类型,const int*(*)[4][5]
6、给定const int*(*)[4][5],其const限定类型是const int*(* const)[4][5]
7const int*(* const)[4][5]派生指针类型,const int*(* const*)[4][5]
```
使用`typedef`为`Q T/T Q/ T* Q`起别名:同样,只需放在`T`或`Q`之后,例:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
`const int* const <=> int const* const`      
```c
// 复杂例子
const int*[10]:数组类型,个数10,元素类型const int*,const int*指针类型,引用类型为const int
int* const[10]:数组类型,个数10,元素类型int* const,int* const为int*的限定类型
```
注意:**限定符限定整体数组类型含义**,`Q typeof(T[N]) <=> Q typeof(T)[N]`,注意与`Q T[N]`的区别,例如:`const typeof(int*[10]) <=> const typeof(int *)[10] <=> int* const[10]`,而不是`const int*[10]`,其实也就是因为`typeof`作用的被看作一个整体了,导致不能直接理解成`Q T[N]`。
`restrict`只能限定**对象指针类型**,或**对象指针类型构成的多维数组类型**。
限定符**不能**直接限定数组类型。
`Q T`、`T Q`、`T* Q`派生数组和指针类型时,直接在`T`或`Q`后加`[N]`或`*`即可,使用`typedef`设定别名时的放置规制与其一致。
多个不同限定符的语义一致,即`const volatile int`与`volatile const int`等价,与顺序无关。
`typeof_unqual(T)`用于去除T的所有限定符。
多个同样限定符与单个限定符语义一致。
`Q T`或`T Q`仍是一个语法上的整体,但`T* Q` **不是**一个语法上的整体。
```c
//注意
int * const //不是一个语法上的整体
typedef int* PINT
const PINT //是语法上的整体

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
2
#define MYNUM (*(int volatile*)0x12340000)
// MYNUM蕴含的对象类型是int volatile

MYNUM这个值就可能因为硬件的行为而改变
这种改变对于程序来说是不可知的(Unknown)
易理解的例子:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
MYNUM的类型是int,除初始化外没有代码对其赋值
编译器可能会将这个while条件语句完全优化掉,因为
MYNUM现在是10,不可能超过20
*/
int MYNUM = 10;
while (MYNUM > 20) {
do something
}


/**
MYNUM现在类型是int volatile,除了初始化赋值10之外
也没有其他代码对其赋值
编译器不会对MYNUM>20这个表达式做任何优化,因为
MYNUM有被修改的潜在可能
*/

#define MYNUM (*(int volatile*)0x12340000)
MYNUM = 10;
while (MYNUM > 20) {
do something
}

/**
编译器也可以忽略这个while循环,因为YOURNUM是无符号整数,取值一定不会小于0
C语言标准规定如果编译器能推断出一个表达式无效,也可以选择不evaluate这个表达式,即使这个表达式包括volatile的对象
*/
#define YOURNUM (*(unsigned int volatile*)0x12340004)
while (YOURNUM < 0) {
do something
}

if((MYNUM = 2+3) == 5)
/**
if的条件判断表达式结果一定是真吗?
赋值表达式的值是表达式执行完成后等号左边对象的值
不强制要求通过访问左边对象来获得表达式的值,即使这个对象是volatile的也不强制
1、如果通过访问左边对象获得值,则可能是5,也可能是硬件正好刚刚又进行一次修改的值
2、如果不是通过访问左边对象来获得值,比如通过缓存刚才计算的值,则结果就是5
*/

int volatile a = 10;
int volatile* p = &a;
// p: Obj_T是int volatile*, V_T是int volatile*,是指针类型
int* q = (int*) &a; *q = 20;
// 上述是一个undefined behavior,试图通过non-volatile-qualified的类型来访问一个volatile修饰的内存

// const + volatile
int const volatile a = 10;
// a不能被显式赋值和修改,但是可以被未知的方式(硬件)隐式修改
/**
因此const修饰的对象不是常量,可修改或不可左值,只是面向程序员而言
*/

volatile对象若要evaluate,都必须去访问对应的内存,而不可以通过寄存器等别的方式获取相应的内存中的值
使用注意事项:int a = MYNUM + MYNUM两次调用MYNUM其side effect,可能有影响。

restrict
1
2
3
char str[100] = “hello”;
strcpy(str, str);
strcpy(str, str+1); // 上述两个字符串拷贝的语句均会导致UB

restrict也是一种限定符,只能用来修饰指针类型
T* restrict O=initializer; //注意这里restrict放在T*的右边

Based on

T* restrict O=initializer;
表达式O能够定位一个对象Obj(也可以称为对象O),对象类型为T* restrict
注意表达式O和对象O的区别:后者是内存中那几个字节内容

  1. 对象O的值是T*类型,蕴含着一个数组访问,表达式O求值后的右值就是Obj的值,因此O[n]可用来访问数组的任意元素对象
  2. 给定表达式E,求值之后是一个指针类型的右值,如果对象Obj的值修改,表达式E被求值之后的值也会被修改,则E Based on Obj例如:表达式O Based on 对象O
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    char 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对象P
int* 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
alt text
restrict是由程序员来保证的,主要了为了编译器优化的效率问题,若可以确保某几个指针类型不是同一个对象则可优化空间大大提升
alt text

对象属性

最核心属性:对象类型,一个对象可能没有对象类型
对象名称(根据有无分为名具名对象和匿名对象)、大小(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
2
3
4
5
6
7
同时申明两个int类型对象: int a, b;
同时申明两个int[10]类型对象: int a[10], b[10];
同时申明两个int*类型对象: int *a, *b;
同时申明两个int(*)[10]类型对象:int (*a)[10], (*b)[10];
typedef int AINT[10];
AINT *a, *b;
int(*)[10]这个类型中,int是以整体形式存在的,因此必须是int (*a)[10], (*b)[10]

即除了整体的部分,其他都需要重新复用。
类似的,使用Q TT QT* Q定义多个指针类型的对象时,Q TT Q都是整体只需要重复写*即可,但T* Q不是整体,* Q需要重复写

1
2
3
4
const int a=0, b=0, c=0; a, b, c都是const类型
int* const a, b; a是int* const类型,b是int类型
int* const a, *b; a是int* const类型,b是int*类型
int* const a, * const b; a是int* const类型,b是int* const类型

标量类型包括了算术类型、指针类型和空指针类型
对于标量类型以及与之相关的限定类型Q T,其初始化共有两种形式:1.{},空初始化列表,意味着将对象的值设置为该类型的缺省值,整数是+0或无符号0,十进制浮点数对象缺省值是+0,其他浮点数的缺省值是+0,或者无符号0,指针类型是空指针;2.除逗号表达式外的任意表达式exp{exp}(包括函数调用),即若初始化式为表达式则是表达式求值后的右值来初始化对象。
对于数组类型对象的初始化:1.{},注意其不适用N不存在的情况,即数组为不完全对象类型时,但是对于变长数组,只能使用{}进行初始化工作,因为对于变长数组来说,运行时才进行空间的分配,不应该显式指出相应的空间大小;2.{sub-initializer-list},其中sub-initializer-list是逗号分隔的初始化子对象的initializer列表,可以嵌套使用。
数组类型对象的designated initializer

1
2
3
4
5
给定一个数组类型T[N],当数组是普通数组时
还可以指定初始化特定位置的 元素
{[constant-expressioni]=initializeri, …}
将指定的第i个元素设置成指定值
int a[10] = {[3]=5};

对象定义中完整形式的initializer就是这个对象的对象值,用来初始化对象表示。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    const int a = 1; // 对象值为1,类型为const int
int b[1] = {1}; // 对象值为{1},类型为int[1]
int c[1][1] = {{1}}; // 对象值为{{1}},类型为int[1][1]
// 但是这三个对象的对象表示一样,即0/1比特串一样
// a表示值及其类型:<1, int>
// b:<'首元素地址',int *>
// c:<'首元素地址', int(*)[1]>
```
此外,对象还有相应的逻辑属性:对象的表示值(Value)和表示值类型(Value Type),即从逻辑的视角来观察,或者说从程序员的角度来看。
规则:
1. 对非数组对象类型:**表示值就是对象值,表示值类型就是对象类型的非限定类型**。
2. 对数组对象类型:**表示值是第一个元素的地址,也即整个对象的地址,表示值类型是该数组元素的指针类型**。
![alt text](image-38.png)
#### String Literal定义
`String Literal`由**编码前缀**(u、U、L、u8和无编码(只能编码ascii码))和**双引号引导的字符序列**(字符序列包括1.所有源字符集中的字符除了实际的回车、`"`、`\`;2.\引导的转义字符)构成:`prefix"s-char-seq"`。
双引号中**不显式包含**`\0`字符的`String Literal`的是`String`,后者一定是前者,前者不一定是后者。
不同编码多个`String Literal`合并,允许每个字符串自行设置字符编码前缀,除了无编码字符串,其余若想何并,则字符编码必须一致,即**两个有编码字符串若想合并除非是两者编码相同,否则不能进行合并,任意一个有编码字符串和无编码的均可合并,且合并后编码为有编码的编码形式**。
##### 编码形式
无编码、u8、u、U、L
对无编码来说,使用"hello"分配对象流程如下
1. 在内存中分配一个大小合适的char类型数组对象
2. 分别用‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’初始化数组对象的每一个元素对象
3. 该对象为匿名对象

`u8`意味着字符编码为`UTF-8`,其编码的字符长度不定,要使用`sizeof(char_8[sizeof(u8"xxx")/sizeof(char8_t)])`来获取相应的大小,其中`xxx`代表相应的字符串内容,对应的对象类型为`char_8[sizeof(u8"xxx")/sizeof(char8_t)]`,对象表示值类型为`char8_t*`
`u`为`UTF-16`编码,字符长度也不定,字符对象类型:`char16_t`,其对象类型为`obj_T`:` char16_t[sizeof(u“xxx”)/sizeof(char16_t)]`,大小为`sizeof(obj_T)`,对象表示值类型:`char16_t*`
`U`为`UTF-32`编码,字符宽度确定,字符对象类型:`char32_t`。因此对于`U"hello"`有:
![alt text](image-39.png)
`L`由实现定义,在不同平台上不一样,字符对象类型:`wchar_t`,以`hello`为例:对象类型为:`wchar_t[sizeof(L“hello”)/sizeof(wchar_t)]`,大小为` sizeof(L“hello”)`,对象表示值类型为`wchar_t*`
对齐要求即为`alignof(对应的字符对象类型)`
###### 无编码
就是正常的分配过程。
###### 其余编码形式
若不特意标出,在不同机器上编码形式并不相同,不一定能够正常输出想要的值:`u8"我们"`、`U"我们"`...

##### 对象存储周期
C语言中没有栈和堆的概念。
4种存储周期,决定对象的有效期:`static`、`automatic`(代表栈)、`allocated`(代表堆,`malloc`分配的内存,要调用free释放)、`thread`。
有`static`的对象,其有效期覆盖整个程序运行期间,程序运行前值会被初始化(若未被显式初始化),所有使用`String Literal`都有静态存储周期。
在生命周期内
1、系统确保对象的内存有效
2、在对象声明周期内,地址不变
3、对象保有最后赋值(last-stored value)不变
超出对象生命周期之外对对象的访问是UB。
```c
T O=initializer;
/*
1、声明了对象标识符O
2、分配了一个对象
*/

T O;
/*
1、声明了对象标识符O
2、是否分配对象视情况而定
*/
// 这两者都是对象标识符声明

Storage-Class Specifiers

一般来说,对象标识符声明的时候,Storage-Class Specifier只能有一个,除了:

  1. thread_local可以和static或extern同时出现
  2. auto可以跟除了typedef的其他Specifier同时出现
  3. constexpr可以跟auto,register或static同时出现
    Storage-Class Specifier规定了标识符(Identifier)的不同属性,也就是改变不同对象的不同属性
  4. 存储周期:thread_local,auto,register,以及block scope中的static
  5. linkage: extern,file scope中的static和constexpr,typedef
  6. object value and type: constexpr
  7. type : typedef

Identifier(用来指代实体):Object(对象)、Function(函数)、Label Name(goto等)、Tag(结构体名)、Member of Structure(结构体成员名)、Typedef Name、Macro Name(宏名称)、Macro Parameter(宏参数)
同一个标识符,若指代不同的实体,则这些实体要么在不同的Scope,要么在不同的Name Space
Scope:function, function prototype(就是函数原型说明,即声明且没有相关定义的地方), file(全局变量所处的环境), block(函数具体实现内函数的参数列表或者花括号括起来的区域内),label name是唯一的拥有function scope的标识符
alt text
Name Space(一个文件中):

  1. label name
  2. tag(结构体名)
  3. member of structures or unions(结构体/联合体成员变量)
  4. standard attributes and attribute prefixes
  5. trailing identifier in an attribute prefixed token
  6. ordinary identifiers(普通标识符)
    alt text
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:

  1. 对象标识符不指向任何函数或对象,比如:typedef命名的名字
  2. 函数参数
  3. 拥有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
    28
    static 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(暂定定义)

暂定定义满足如下条件:

  1. File Scope
  2. 没有初始化列表
  3. 没有extern或thread_local
    多个暂定定义会合并成一个对象定义
    alt text

多个同样和不同的对象表示的String Literal分配对象

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc,char* argv[])
{
"hello"; // 分配一个字符数组对象
"hello"; // 不一定会分配一个,看系统实现,可能分配一个,可能分配两个
return 0;
}

int main(int argc,char* argv[])
{
"hello"; // 分配一个字符数组对象
L"hello"; // 一定会分配一个新的,因为这两个的对象类型不一样,前者是char,后者是wchar_t
return 0;
}

Compound Literal定义

基本语法:(type-name){Initializer-list},定义一个匿名对象,对象类型是type-name,由Initializer-list进行初始化。
例子:int* p = &(int){1};.具体存储周期与放置位置有关。

1
2
3
4
5
6
7
int* p = &(int){1}; // static
int main(int argc, char *argv[])
{
int* q = &(int){1}; //automatic
printf("%x, %x", p, q);
return 0;
}
  1. Compound Literal意味着分配一个指定类型的对象
  2. Compound Literal是一个有效的lvalue表达式
  3. 定位刚刚分配的那个对象
    分配一个对象后马上定位到该对象。
    (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>

由于其不为可修改左值,因此可以被修改:
alt text
一般情况下这四个(int){1}不是定位同一个对象,但是特殊情况下有可能定位同一个。
alt text
对于(const int){1}其对象类型为const int,因此其为不可修改左值,则不能对其进行++/--或赋值等修改值的操作。

内存管理函数

malloc(x)没有对象类型,其对齐要求是Fundamental Alignment alignof(max_align_t),课程假设基础对齐值为16,保证返回值地址可以强制转化成int*char*等,大小为x,地址已知,其余诸如对象类型、对象表示值和对象表示值类型均未知。该对象的存储周期是allocated
callocreallocaligned_alloc
malloc返回的值需要强制转换成一个有意义的对象类型,因此不能void * p = malloc(4);
如果需要获得指定对齐要求的空间,可以使用void *aligned_alloc(size_t alignment, size_t size);
alt text
此处Value未知是由于malloc不进行初始化,因此存储的数据是未知的,且因为以double的视角进行观察所以相应的对齐要求为8.
理解malloc(sizeof(Obj_T)*N)

1
2
3
4
5
6
7
8
    int a[10];
int* p = a;
/* 对象a对应10个连续int组成的内存块,a这个表达式rvalue类型是int*
malloc(sizeof(int)*10)申请10个连续int空间
语义可视为申请一个int[10]的对象,int[10]类型->返回值类型为int*
*/
// 因此如下也是强制转换成int *
int* p = (int*)malloc(sizeof(int)*10);

形式化定义:假设一个对象类型Obj_T,malloc申请N个Obj_T大小的内存可形式化定义为
Obj_T* p = (Obj_T*)malloc(sizeof(Obj_T)*N)
可视为分配了一个Obj_T[N]对象类型空间
例:alt text
使用malloc直接分配高维数组,如上,的工程扩展性较差。
工程上创建高维数组:
alt text
不同高维数组实现方式对比:
alt text
malloc(sizeof(int))隐含的语义是分配了一个int[1]类型的空间
指针类型总是蕴含了一个对数组元素的访问
执行了多少次malloc,就需要执行多少次free。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
一、利用malloc分配240个字节,如何定义指针对象p,让该240字节类型视为
1、char[240]
2、int[6][10]
3、int[3][4][5]

1、char* p = (char*)malloc(sizeof(char)*240);
2、int (*p)[10] = (int(*)[10])malloc(sizeof(int[10])*6);
3、int (*p)[4][5] = (int(*)[4][5])malloc(sizeof(int[4][5])*3);
*/
/*
二、void* p = malloc(32);
1、int* q = (int*)p;
2、char* r = (char*)p;
3、int (*s)[4] = (int(*)[4])p;
4、char (*t)[2][4] = (char(*)[2][4])p;
请给出q+1, r+1, s+1, t+1的值,假设P的值是Add,形如Add+Offset

1、Add+4;2、Add+1;3、Add+16;4、Add+8
*/

左值

左值表达式

1.对象标识符;2.String literal;3.Compound Literal
用来定义对象,因此左值(located value)是能定位对象的表达式(一系列operatoroperand组成的一个序列)的总称。
C语言中一共有17种表达式。
alt text
其中,左值只有以下几种形式:对象标识符、数组下标运算表达式、*exp,String Literal、Compound Literal、指向结构体/联合体的lvalue.member,指向结构体/联合体指针表达式->member
表达式要根据语法的规定来进行求值
给定表达式过程:1.求出计算值(右值),用来帮助理解语法;2.确定副作用(是否对环境状态产生改变),用于帮助理解优化
alt text
alt text

1
2
3
4
5
6
7
8
9
i + i++;
i++ + i++;
++i + i++;
++i + ++i
++i + ++i + ++i

/*这些表达式的在C语言标准中都被定义为Undefined Behavior
在实际工程中,不能使用这样的表达式
*/

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
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

/**
1、Function Designator和实参的evaluation,和实际函数调用执行之间
意味着Function designator和所有实参的evaluate先后顺序不定
*/
int main(void) {
static int a = 0;
printf("%d %d\n", a++, a++);
return 0;
// 上述产生未定义行为,对a产生两次副作用且两次副作用没有先后顺序要求
// 这两个a++之间的逗号不是逗号运算符
}

/***
逗号运算符分隔的前后两个
两个a++之间的逗号不是逗号运算符
表达式之间有sequence point
*/

// &&、||运算符会带来sequence point
int a=0;
a++ && a++; // 短路运算
printf("%d", a); // 1

int a=0;
a++ || a++;
printf("%d", a); // 2

/***
1、Logic AND优先级高于Logic OR
2、Logic AND和Logic OR表达式 Evaluate之后的rvalue类型是int
3、Logic AND中,如果exp1表达式的rvalue等价0,exp2不做evaluate
4、Logic OR中,如果exp1表达式的rvalue等价1,exp2不做evaluate
*/

// 三目运算符会在?前面的表达式和后面2选1的表达式之间插入一个sequence point
// 所以下式没问题
int a = 0;
a++ ? a++ : a++;


/***
i++,i++是普通对象定义的initializer,不存在UB,但存在unspecified,
a[0]和a[1]的值不一定是预想的0和1,谁先执行不一定

j++,j++是UB, compound literal中的initializer,中间没有序列点

k++,k++是well defined的语句
*/
int i=0;
int j=0;
int k=0;
int a[2]={i++, i++};
(int[2]){j++, j++};
k++,k++;

// C语言库函数调用返回之前保证所有副作用都完成, 不包含自定义函数
int b[10];
scanf("%d %d", &b[0], &b[0]);
/**
上述例子中,不用管该例

*/

/**
比较函数前后的序列点
1、在每次调用比较函数之前,有序列点
2、在每次调用比较函数之后,有序列点
3、保证以下顺序
3.1 调用比较函数
3.2 比较函数返回结果
3.3 根据结果移动被比较的那两个对象
保证了比较函数中*a和*b对象是一致和最新的
*/
int comp(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
int main(int argc, char *argv[])
{
int a[10] = {3,5,2,1,4,9,8,7,0,6};
qsort(a, 10, sizeof(int), comp);
return 0;
}
```
自增/自减运算符同时会带来Valuation Computation和Side Effect,谨慎使用。
### 基础表达式
**注意:其中对象标识符和字符串都是左值**
包括:**对象标识符,函数标识符,字符串,(),常量和泛型选择**。
#### 标识符:**对象标识符**和**函数标识符**。
**表达式中的对象标识符都是合法的左值**。
`T O = Initializer`中`O`即为对象标识符。
左值表达式定位的对象类型分为数组对象类型和非数组,前者还分为普通数组和变长数组类型。
左值表达式求值规则:1. (1).若左值定位对象是一个**非数组**类型,则求值后的右值即为**该对象的对象值**,右值类型为该对象类型的非限定类型,**与此前的value和value-type**串起来了。(**左值转换**)
(2).若左值定位的是一个数组类型,则求值后右值为对象**第一个元素的首地址**,类型为**元素对象类型对应的指针类型**。(`Decay`,退化)。
构建对象七元组的概念:`Address`(系统分配,无法改变)、`Object_Type`(对象类型)、`Name`(对象名称)、`Size`(大小,字节数)、`Value`(对象表示值)、`Value_Type`(对象表示值类型)和`Alignment`(对齐要求)。
其实**对于对象标识符的左值求解,就是取其对象七元组的`Value`和`Value-Type`赋给右值**。
```c
int n=1;int e[1][n]={}; // 当e被求值时,右值类型可以在编译阶段推断出来int(*)[n],而n在编译阶段可以确定
int n=1;scanf("%d",&n);int f[1][n]={}; // 当e被求值时,右值类型在编译阶段不能推断出来,因为编译阶段n不确定

对于左值表达式,其会定位一个对象,对象在物理空间中会有一个对象表示,相应的会产生一个对象值(Object Value),而经过左值表达式求值后会有一个右值,因此讲述某个对象标识符的时候不确定是哪一个值,一定情况下认为是变量是因为可以修改对象值进而使右值发生变化,导致认为可以修改。
两个对象表示一样则对象值一样,反之不正确,peddling bits的存在导致的。
对象值一样,作为表达式求值后的右值不一定一样。

常量

整常量、浮点数常量、枚举常量、字符常量和预定义常量。

整数常量

三部分组成:进制前缀、数字序列和类型后缀。
数字序列还支持在数字中间插入字符“’”,也叫数字分隔符,“’”不能出现在数字序列的第一个数字之前,也不能出现在最后一个数字之后
前缀有0b/0B(二进制)、无、0(八进制)和0x/0X(十六进制)。
后缀有u、Ul、Lll、LLwb、WB(单独使用或联合使用,包括无、U、L、LL、WB、UL/LU、ULL/LLU、UWB/WBU等,不同后缀规定了不同的整数类型序列按进制编码的字符序列从上到下第一个能覆盖的其表征的值的类型就是这个整数常量的类型),例如:对于整数常量2147483647,若其无前缀和后缀,则其是一个十进制整数常量,查表可知其类型为int。否则对于2147483648,若平台longint大,且无前后缀,则类型为long int
表如下:
alt text
整数常量即为基础表达式,求值的右值就是其表征的值,类型为按表查出来的类型,也即整数常量的类型。
**不考虑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类型为float1.0类型为double
alt text
十进制浮点数常量符号序列由三部分组成:一个整数部分、小数点和小数部分组成的十进制实数、一个e和标识幂的+/-。
1、实数部分不可省略,但在实数里面,整数部分如
果没有,则表示整数部分的值为0,小数点或小
数部分也可以没有,表示这个实数没有小数部分
2、浮点数常量通过e以及后面的指数来表达10的幂
次方,以科学计数法的形式表征浮点数的值
3、实数部分的整数和小数部分,以及指数部分,也
可以使用“’”这个数字分隔符(Digital Separator)

十六进制浮点数常量:前缀0x,实数部分以十六进制表示,e换成p,幂值由十进制表示表示的是2的幂次方,其中p和指数部分不可省略,可以使用'这个数字分隔符。0x100.5p0 // 256.3125(1*256+5*1/16)*2^00x0A.Bp2 // 42.75 (10+11*(1/16))*22.

枚举常量

例如:enum Season {Spring, ...}的右值类型为enum SeasonUnderlying 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

预定义常量

falsetrue(分别为0、1,右值类型为bool)和nullptr(一个空指针常量,类型为<stddef.h>中定义的nullptr_t类型)

字符串

字符串分配或重用的对象类型是字符数组类型,分配出来即定位相应的对象,则其对应求值规则和数组类型一致
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char * p1 = "hello";
char * p2 = "hello";
char str1[] = "hello";
char str2[] = "hello";
if (p1 == p2) {
printf("111\n");
}
if (str1 == str2) {
printf("222\n");
}
/*
其中新分配的"hello"可能有1、2、3、4个,具体几个取决于编译器的实现。
"111"有可能被打印出来,但是"222"一定不会被打印出来,因为str1与str2的首地址一定不一致。
*/
"hello"; //对"hello"做求值
char str[] = "hello";//对"hello"不做求值
对于字符串进行求值

定位刚分配或重用那个字符数组对象,其不做求值的类型与数组对象一致
alt text

字符数组对象的定义问题
初始化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”

alt text

1
2
char* p = “hello”; // 需要求值,表达式的右值赋值给对象p
char str[] = “hello”; // 用于初始化字符数组对象str,不进行求值

括号表达式

(exp)等价于exp,但是优先级更高。

后缀表达式和一元表达式

后缀表达式
  1. []exp1[exp2]
  2. .:exp.identifier,结构体变量索引成员变量
  3. ->:exp->identifier结构体指针索引成员变量
  4. ++/--a++
  5. (type-name){Initializer-list}(int){2}, (int[3]){1,2,3}
  6. ():exp()(函数调用)
一元表达式
  1. ++/–:++exp
  2. &,任何对象的地址都不会变。
  3. *:*exp
  4. +/-
  5. ~/!
  6. sizeof和alignof

非数组对象左值的求值规则

有7种情况不做左值求值:

  1. sizeof结合,对象的大小由对象类型可知,编译时可推断出来,对象size在生命周期内不变化。
  2. typeof结合;
  3. alignof结合(gcc扩展情况下可以,但是标准不支持);
  4. 作为赋值语句左侧的被赋值体,exp =
  5. 跟一元运算符++/–或后缀运算符++/–结合:exp++/exp--/++exp/--exp
  6. 与&结合,如果左值定位对象的存储周期为static,则其对象地址编译时可知,否则地址运行时才可获得
  7. 若左值定位的对象类型是结构体或者联合体,跟.结合: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>

定位指针类型对象的左值

alt text
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)对象其他属性随之确定
    • alt text
      对于*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在行为上是完全一致的,如果称前者为变量,那么后者是否也为变量呢?
      alt text
  • 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
    7
    1int(*p)[2][3],p+2的rvalue相对于p的rvalue的offset是多少?
    2int (*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;

数组对象左值的求值规则

以下情况不进行求值:

  1. sizeof结合,由对象类型可知,编译时可推断,声明周期内不变化
  2. typeof/typeof_unqual结合;
  3. &结合,若左值定位对象的存储周期为static,则对象地址编译时可知
  4. 若左值定位的是一个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,是一个整体,可以进行赋值)

  1. 数组类型
  2. const限定类型
  3. 不完全对象类型
  4. 结构体/联合体类型,但其成员对象类型有const限定类型,即只要递归成员有const限定符,则整个对象都不能修改
    alt text
    alt text
    alt text
    alt text
    alt text
    需要考虑对象属性中的对象类型进而来判断是否为不可修改左值,推荐使用Obj_T const O的形式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        int 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
    重要:字符串不是常量
    */

地址常量

  1. 空指针
  2. 指向一个static storage duration对象的指针
  3. 指向Function Designator的指针。
    右值角度定义的,与从表达式定义的常量(5种表达式统称)不同。
    地址常量是特定表达式求值后的结果,可以获得地址常量的表达式统称为地址常量表达式

获得方法

1、显式使用一元操作符&来获得的rvalue
2、显式将整数常量强制转换成指针类型获得的rvalue
3、隐式使用数组类型表达式获得的rvalue
4、隐式使用函数类型表达式获得的rvalue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc,char* argv[])
{
static int z = 1;
int* p = &z; // &z的返回值就是地址常量,因为其右值类型为指针类型,且指向一个`static storage duration`对象
// 但对于p则,p求值后返回的不是地址常量
"hello"; // 返回的值也是地址常量
return 0;
}

int main(int argc,char* argv[])
{
static int z[1];
int* p = z; // p求值后得到的右值不是地址常量,但是z求值后是地址常量
return 0;
}

int main(int argc,char* argv[])
{
"hello"; // 地址常量
&"hello"; // 地址常量
return 0;
}

一维数组

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// g[1][2]是一个表达式,这其中,基础表达式有g、1、2,g是一个object identifier,是一个lvalue,因此可以定位到这块内存<0x0082FB30, int[2][3], g, 24, 0x0082FB30, int(*)[3], 4>,假设起始地址为0x0082FB30;g是g[1]这个表达式的子表达式,因此表达式g的取值如下:g: < 0x0082FB30, int(*)[3]>,g[1]等价于*(g+1),下面运算g+1,获得值如下:<0x0082FB30+1*sizeof(*g), int(*)[3]>,即<0x0082FB3C, int(*)[3]>,*(g+1)定位的内存:<0x0082FB3C, int[3], 12, N/A, 0x0082FB3C, int* , 4>,对于g[1]同理,*(g[1]+2)定位的内存:<0x0082FB44, int, 4, N/A, 0, int, 4>
g[1][2] = 1; // g[1][2]经过求值后得到对象类型为int,为可修改左值,可赋值
/*
1、识别基础表达式,包括g、1、2 (等号左边)、2(等号右边)
2、g是lvalue,定位这24个字节内存(对象类型int[2][3])
3、g和&结合,&g返回值: <0x0082FB30, int(*)[2][3]>
4、观察*(&g),这个表达式是lvalue,定位这24个字节内存(对象类型int[2][3])
6、*(&g)是(*(&g))[1]子表达式,没有跟&、sizeof、typeof结合, *(&g)取值: < 0x0082FB30, int(*)[3]>后续过程就跟之前g[1][2]一样了
由于左值是能够定位对象的表达式,或者称为定义某部分内存的表达式,因此这个表达式中有4个左值:g、*(&g)、 (*(&g))[1]、 (*(&g))[1][2]
*/
(*(&g))[1][2] = 2;
/*
1、识别基础表达式,包括g、1、2 、3
2、g是lvalue,定位这24个字节内存(对象类型int[2][3])
3、g是*g的子表达式,没有跟&、sizeof、typeof结合,g取值: <0x0082FB30, int(*)[3]>
4、*g这个表达式是lvalue,定位这12个字节内存(对象类型int[3])
6、*g是&(*g)子表达式,&(*g)取值: < 0x0082FB30, int(*)[3]>后续过程就跟之前g[1][2]一样了
同样有4个左值:g、*g、 (&(*g))[1]、 (&(*g))[1][2]
*/
(&(*g))[1][2] = 3;
/*
1、识别基础表达式,包括g、1、1(g+1-1中的两个1)、1、2 、4
2、g是lvalue,定位这24个字节内存(对象类型int[2][3])
3、g是g+1-1的子表达式,没有跟&、sizeof、typeof结合,g取值: <0x0082FB30, int(*)[3] >
4、计算a+1-1表达式的值,结果是<0x0082FB30, int(*)[3]>后续过程就跟之前g[1][2]一样了
此处只有3个左值:g、(g+1-1)[1]、 (g+1-1)[1][2]
为什么此处g+1-1不能算作是左值,仔细看左值的定义,能够定位对象的表达式,此处虽然g+1-1也代表了一个对象,但实际定位对象的只是g,也就是根据g才得到g+1-1的对象,+1-1只是相应的偏移,因此,这里只有g可以算作是左值,那为什么(g+1-1)[1]又是左值了呢,因为(g+1-1)[1] <=> *(g+1-1+1),而对于*exp,来说,如果exp是一个合法的指针类型,则*exp将定位地址为exp的Value,对象类型为exp的Value-Type的Ref-type的,大小为sizeof(其对象类型)的对象,所以这里是*exp作为一个整体定位一个对象,因此此处*(g+1-1+1)是作为一个整体定位对象,所以(g+1-1)[1]是一个左值
*/
(g+1-1)[1][2] = 4;
/*
1、识别基础表达式,包括1、g、2、5
2、1是常量表达式,取值<1, int>
3、1[g]等价于*(1+g), g是lvalue,定位这24个字节内存(对象类型int[2][3])
4、计算1+g, 返回值:<0x0082FB3C, int(*)[3]>
后续过程就跟之前g[1][2]一样了
5、观察*(1+g),这个表达式是lvalue,定位这12个字节内存(对象类型int[3])
有三个左值g、1[g] 、1[g][2]
*/
1[g][2] = 5;
// 上述五个赋值语句均成功修改g[1][2]
/*
1、识别基础表达式,包括1、2、g、6
2、1、2是常量表达式,取值<1, int>,<2, int>
3、1[2]等价于*(1+2)
4、1+2的返回值是<3, int>,不是一个有效指针类型,无法跟*结合
报错
*/
1[2][g] = 6; // 编译不能通过
```
数组名是一个对象标识符,不可左值表达式。
## 函数
### 函数类型和函数指针类型
```c
/**
定义了一个返回值类型为int,参数数量
为2,类型均为int类型的函数func,其函数类型为int,func为函数标识符
*/

int func(int a, int b)
{
// do something
return 0;
}

函数类型说明: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之后添加()引导的函数类型即可,注意是严格添加:alt text
相应的函数标识符摆放在()引导的参数类型列表之前,最后加上{}引导的函数内容。
返回值类型必须是非数组对象类型包括其限定类型)或者*void

函数类型不是语法上的整体的,因此有设置别名的需求:使用typedef设置时,将别名设置在()引导的参数类型列表之前,例如:typedef T Alias(...)/typedef T* Alias(...)/typedef Q T Alias(...)/typedef T* Q Alias(...)
alt text
函数类型的指针类型:
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
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int* foo(int* p, int* q) 
{
// do something
return NULL;
}
/* 该函数的函数类型是什么?
对应的指针对象类型是什么?
答案:
Function Type: int*(int*, int*)
Pointer Type: int*(*)(int*, int*)
*/

// 复杂的例子:
int*(*const*(*(*)(int,char))[3])[4][5];
/**
1、给定int,派生指针类型,int*
2、int*,派生数组类型,5个元素,int*[5]
3、int*[5],派生数组类型,4个元素,int*[4][5]
4、int*[4][5]派生指针类型,int*(*)[4][5]
5、int*(*)[4][5],其const限定类型,int*(*const)[4][5]
6、int*(*const)[4][5],派生指针类型,int*(*const*)[4][5]
7、 int*(*const*)[4][5],派生数组类型,3个元素, int*(*const*[3])[4][5]
8、int*(*const*[3])[4][5],派生指针类型, int*(*const*(*)[3])[4][5]
9、int*(*const*(*)[3])[4][5],派生函数类型,两个参数int, char,int*(*const*(*(int, char))[3])[4][5]
10、int*(*const*(*(int, char))[3])[4][5],派生指针类型,int*(*const*(*(*)(int, char))[3])[4][5]
*/

/**
设计函数七元组,类似的由地址、函数类型、函数名、大小、对象表示值(即地址)、对象表示值类型(即函数类型的指针类型)、对齐要求
*/

int func(int a, int b)
{
// do something
return 0;
}
int (*p)(int, int);
p = func; // 合法
p = &func; // 合法
p = *func; // 合法
p = **func; // 合法
p = ***func; // 合法
func(2, 3); // 合法
(*func)(2, 3); // 合法
(**func)(2,3); // 合法
(&func)(2, 3); // 合法
p(2, 3); // 合法
(*p)(2, 3); // 合法
(**p)(2, 3); // 合法
(&p)(2, 3); // 不合法,因为对于p作为int(*)(int, int)类型,此时不能再取地址,取址有误,这里p和&func不是函数定位表达式,但是求值后为指向func的指针

函数定位表达式

是可以定位一个函数的表达式,一共只有两种函数定位表达式:
1、Function Identifier(函数标识符)
2、如果一个表达式exp,其rvalue类型是函数指针类型,则*exp是一个合法的Function Designator
因此有如下类似规则:alt text
其中,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的对象,即直接又定位该函数
alt text
同理,*(&func)同样定位到func函数,*(*func)同样定位到func函数。 alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int func(int a, int b) 
{
// do something
return 0;
}
int (*p)(int, int);
p = func;
p = &func;
p = *func;
p = **func;
p = ***func;
p = &func;// 上述均合法
p = &&func; // 不合法
/**
func, *func, **func, ***func这些表
达式都是Function Designator
Function Designator没有跟typeof,
&结合的时候,如果被evaluate,
则值为指向该函数的指针
&func表达式返回值也是指向该函
数的指针
func,
*func, **…**func和&func结果一样,但evalute取值的路径不同
*/

注意:函数标识符本身不是函数指针

函数调用

1、对exp表达式evaluate之后rvalue是指向func函数的指针,且
2、arg1和arg2进行evaluate之后rvalue类型符
合func函数对应参数的对象类型,则
exp(arg1, arg2)称为函数func的函数调用,函数调用是一个后缀表达式,函数调用这个表达式求值后的右值类型是函数类型中的返回值类型,因此只有函数调用这个后缀表达式的右值有可能是特殊类型void,而其他表达式求值后返回的都是非数组的完全对象类型,函数调用返回的若是结构体/联合体类型,则支持.操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int func(int a, int b) 
{
// do something
return 0;
}

func(2, 3); // 合法
(*func)(2, 3); // 合法
(**func)(2,3); // 合法
(&func)(2, 3); // 合法
p(2, 3); // 合法
(*p)(2, 3); // 合法
(**p)(2, 3); // 合法
(&p)(2, 3); // 不合法,因为对于p作为int(*)(int, int)类型,此时不能再取地址,取址有误,这里p和&func不是函数定位表达式,但是求值后为指向func的指针

func = NULL; // 不合法,func不是左值
(1?func:NULL)(2, 3); // 合法,1?func:NULL这个表达式返回值是指向func函数的指针
&func(2, 3); // 不合法,&func(2, 3)是&(func(2, 3)), func(2, 3)返回的是rvalue,不是合法的lvalue,注意与(&func)(2, 3)的区别,&exp是一元表达式,func(2, 3)是后缀表达式,后缀表达式优先级比一元表达式高,注意判断优先级问题

参数传递

传入的全是表达式的值,实参要按要求进行求值,实际传递的就是实参evaluate之后的rvalue,因此形参中没有数组类型,即所有类型求值后的V-T都是非数组类型,实参传递的机制就是传值,只有pass by value
alt text
alt text
alt text

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
28
29
30
31
32
33
34
35
36
37
38
39
void func1(int g[][3]) {
...
}

void func2(int* g, int row, int col) {
...
}

void func3(int* g, int size) {
...
}

void func4(int row, int col, int a[row][col]) {
// 此处中a不是VLA,该函数被调用时,对应传给参数a的参数arr需要进行求值,得到的右值类型是指针类型,因此此处a的类型也应该是一个指针类型,而非VLA类型,但是其Ref-Ttpe是int[col]类型是VLA类型,因此对于a[++i]是定位VLA的左值,所以对其求sizeof时需要进行对++i进行求值
int i = 1;
sizeof(a[++i]); //
printf("%d", i); // 2
...
}
// 上述均可

void func5(int** pg){
// 不合法,
// 传入实参求值后强制转换为int**后的右值,
// 对于一般的一维存储,只存了一层地址,*pg的对象表示值将为NULL,导致**pg无法正常求值而报错
p[0][0] = 1;
}



int main() {
int g[2][3] = {0};
func1(g);
func2((int *)g, 2, 3);
func3((int*)g, 6);
func4(2, 3, g);
func5((int**)g);
return 0;
}

alt text

结构体类型的对象及对象值

结构体/联合体类型是派生类型给,也是非数组类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct str {
int a;
int b[1];
} x = {1, {1}};

typedef struct str {
int a;
int b[1];
} STR;
STR x = {1, {1}};

/**
T O = Initializer;
{1, {1}}就是对象x的对象值
*/

alt text
其中m、m.a、m.b都是lvalue
&m, &m.a, &m.b都是合法的
值不能打印出来,但是仍然存在。
例外,其中foo()虽然不是左值,但是可以引用相应结构体的成员变量
alt text

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
typedef struct _MyStruct {
int a;
int b;
} MyStruct;
MyStruct foo()
{
MyStruct m;
m = (MyStruct){5, 10};
return m;
}
MyStruct* func()
{
MyStruct* p;
p = (MyStruct*)malloc(sizeof(MyStruct)*1);
p->a=5;
p->b=10;
return p;
}
int main()
{
MyStruct* p=func();
foo().a = 10; // foo().a不为左值,因此不能被修改,报错
p->a=10; // p为左值,p->a也是左值,类型为int,可修改
return 0;
}

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
alt text

_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
2
3
4
5
6
7
8
9
// 牢记修改对象的alignment**不改变对象类型的对齐大小,也不改变对象的大小**    
_Alignas(64) struct stru {
char a;
_Alignas(32) short b;
int c[10];
double d;
} s;
_Alignof(struct stru) =><32, size_t> //只考虑结构体成员中的最大对齐要求
_Alignof(s) 返回<64, size_t> // _Alignas(64)修改了该结构体对象的对齐要求
结构体对象的size

一个结构体类型T,成员对象分别是Ei,1<i<=n (假设有n个成员对象)
_Alignof(T)是这个结构体类型T的对齐要求
结构体第1个成员对象E1的首地址就是结构体的首地址,地址偏移量offset为0;
结构体第2个成员对象E2的地址偏移量确定方法如下
E2的首地址偏移量:

1
2
3
4
5
6
7
offset += sizeof(E1)
// E2的首地址偏移:
offset += offset % _Alignof(E2) ==0 ? 0:(_Alignof(E2) - offset % _Alignof(E2))
...
offset += sizeof(En) // 结构体内部填充
// 注意最后的偏移还要满足该结构体类型的对齐要求,注意这里不是满足结构体对象的对齐要求,这两者不一定一样
sizeof(T) = offset + offset % _Alignof(T) == 0 ? 0: (_Alignof(T) - offset % _Alignof(T))
#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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 不建议使用pragma pack()
#pragma pack(1)
_Alignas(64) struct stru {
char a;
_Alignas(32) short b;
int c[10];
double d;
} s;
// 则_Alignof(s.a) = 1 _Alignof(s.b)=1 _Alignof(s.c) =1 _Alignof(s.d) = 1
// _Alignof(struct stru) = 1
// _Alignof(s) = 64,结构体对象的对齐要求不受影响
/***
假设对象s的首地址是0x0061FD80(可以被64整除)
1、s.a的offset等于0,且sizeof(s.a)=1
2、offset += sizeof(s.a),结果为1
3、offset % _Alignof(s.b),结果为0,s.b的offset为1
4、offset += sizeof(s.b),结果为3
5、offset % _Alignof(s.c),结果为0,s.c的offset为3
6、offset += sizeof(s.c),结果为43
7、offset % _Alignof(s.d),结果为0,s.d的offset为43
8、offset += sizeof(s.d),结果为51
9、offset % _Alignof(struct stru),结果为0,sizeof(s)为51
*/

Compound Literal+结构体类型

Compound Literal用于构造匿名对象,特别是结构体类型很有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct stru {
char a;
short b;
int c[10];
double d;
} ;

void func(struct stru s) {
...
}

int main() {
// 直接使用匿名对象进行传参
func((struct stru){'a', 1,{1,2}, 8});
return 0;
}

指针转换中的对齐问题

指针的强制转换隐含着地址对应的对象类型发生了变化,如果转换后的指针对应的对象对齐方式不正确 ,则指针的强制转换行为是未定义行为

1
2
3
4
5
char a[10],假设对象a的首地址 % 2 = 0
a+1一定是奇数,除以4的余数肯定不为0
(int*)(a+1)是未定义行为
如果对象a的首地址 % 2 = 0,但 % 40
(int*)(a)也是未定义行为

register

register T O,
告诉编译器越快越好,但编译器可以不理会,
register修饰的对象不能取地址,因为其修饰对象不一定在内存中。register不能修饰数组对象,因为对其修饰的对象求值后不一定存在首地址,则后续相应的访问无从谈起,也是UB。

auto

auto O = initializer;Initializer必须有,且只能是赋值表达式或更高优先级表达式,对象O的类型是initializer这个表达式rvalue的类型

1
2
3
4
auto a;
auto a = {1, 2, 3}; // 上述不合法
auto a = 1.0 // 合法
// auto不是指对象的存储周期为automatic

关于算数类型在表达式内适配问题

常规/常用/标准算术转换

当有表达式涉及多个算术类型子表达式,该机制决定算术类型子表达式rvalue类型转换规则,并决定整个表达式rvalue类型如何获得.

与浮点相关规则

  1. 如果一个operand是decimal floating type,另一个operand也必须是decimal floating type确保十进制浮点数不和非十进制浮点数一起运算
  2. 如果一个operand是_Decimal128,另一个operand也提升为_Decimal128
  3. 如果一个operand是_Decimal64,另一个operand也提升为_Decimal64
  4. 如果一个operand是_Decimal32,另一个operand也提升为_Decimal32
  5. 如果一个operand是long double,另一个operand也提升为long double
  6. 如果一个operand是double,另一个operand也提升为double
  7. 如果一个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规则去考虑,只有满足则计算整型提升结束

  1. 如果两个operand都是有符号或者都是无符号,less rank的operand向greater rank转换
  2. 如果无符号类型operand的rank大于等于另一个有符号operand的rank,有符号operand转换成无符号operand的类型
  3. 如果有符号operand的类型表征范围能覆盖另一个无符号operand类型的表征范围,无符号operand转换成有符号operand类型
  4. 两个operand都转成有符号operand类型对应的无符号类型
    1
    2
    3
    4
    unsigned int a = 1;
    int b = -1;
    if(a>b)
    printf("hello"); // 无法正常打印,unsigned int和int的rank一样,有符号operand转换成无符号的unsigned int类型
    operand类型小于int/unsigned int规则
    bool、signed char、unsigned char、signed short、unsigned short,operand如果是以上类型
  5. 如果int类型能表征,转换成int类型
  6. 如果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