Week 1
C语言中的对象
一个对象一般有一个对象类型,标记为Type-name。
完全对象类型:大小可知,分配后大小确定。
不完全对象类型:大小未知,分配后大小也确定。
所有对象类型在语义上都是整体的,对象类型在语法上不一定是整体的,如果一个Type-name在语法上整体,标记为T。
对象各个bit组成的二进制串是这个对象的对象表示(Object Representation),对象的值和对象表示可以通过对象类型进行映射。
对象的值一样,对象表示不一定一样:bit包括Value Bits和若干无意义的Padding Bits,由对象类型决定。
sizeof(Type-name)
alignof(Type-name)规定了一个类型的确省对其要求。
类型
Type
- Arithmetic Type 算数类型
- Derived Type 派生类型
- Type-name with Qualifier 限定符修饰类型
对于语法上整体的类型T,typedef T Alias,Alias在语法上是一个整体。
数组派生:typedef typeof(int[5]) AINT; -> int[10][5] <==> AINT[10]
例:
int[10][]&int[][10]
int[10][]中的元素类型是int[]不完全累对象类型,不合法。
int[][10]中元素类型T是int[10]。其本身是一个不完全对象类型,不能继续派生数组类型。
指针派生:typedef typeof(int[5]) AINT -> typeof(int[5])* <==> int(*)[5]
例:
int(*[10])[5]是一个长度为10的数组,其中每个元素是一个指向长度为5的数组的指针。
其对应的指针类型是int(*(*)[10])[5]
例:
int*(**(*)[])[4][5]?
限定符
- _Atomic
- const
- volatile
- restrict(只能限定对象指针类型,或其构成的多维数组类型)
给定一个类型T,给定一个限定符Q,其限定修饰类型为Q T或T Q,语法上也是整体。
T和Q T(T Q)是两种不同的Type,但大小,表示值,对齐方式一样。
_Atomic限定修饰类型是原子类型,其他三种限定修饰类型统称为限定类型,都有称为原子限定类型。
对于语法上整体呈现的类型T:
- Q T 等价于 T Q
- Q T[N]:数组类型,N个Q T;T[N] Q:不合法
- Q T*:指针类型,referenced type是Q T;T* Q:Q限定T*类型
| 限定类型形式 | 派生数组类型(N个元素) | 派生指针类型 | Q’ 限定修饰类型 |
|---|---|---|---|
| QT | QT[N] | QT* | QTQ’ / QQ’T / Q’QT |
| TQ | TQ[N] | TQ* | TQQ’ / TQ’Q / Q’TQ |
| T*Q | T*Q[N] | TQ | TQQ’ / TQ’Q |
例:
const int*(* const*)[4][5]?
例:const int* const?
typeof:
const typeof(int*[10]) <==> int* const[10]const typeof(int*[5][10]) <==> int* const[5][10]
_Atomic(T) & _Atomic T:
- _Atomic(T)是基于T派生的一个原子类型,T不能是数组类型,函数类型,限定类型和原子类型
- _Atomic T是基于T的原子限定类型,T不能是数组类型,函数类型
Week 2
分配对象
1.对象标识符(Object Identifier)定义
2.String Literal(字符串字面量)
3.Compound Literal(复合字面量)
4.内存管理函数
对象标识符定义
一次定义多个对象标识符:T * Q O_1 Initializer, * Q O_2 Initializer,..., * O_n Initializer
例:
int(*a)[10],(*b)[10]
Initialzier形式:除逗号表达式之外的任意表达式exp/花括号初始化器(Braced-Initializer)
- 标量类型Initializer
- {},空初始化列表->确省初始化
- 除逗号表达式之外的任意表达式exp或{exp}
- 数组类型Initializer
- {},空初始化列表
- {sub-initializer_1,sub-initializer_2,sub-initializer_n}
- T[N]中N不存在,不能用{}初始化
- 变长数组只能用{}初始化
例:
int a[2][3] = {{1,2},{3,4,5}}<==>{{1,2,0},{3,4,5}}
int a[2][3] = {1,2,3,4}<==>{{1,2,3},{4,0,0}}
int a[10] = {[3] = 5}(Designated Initializer)
无Initializer时(如int a;),a被称为indeterminate representation,但实际上这个值是确定的,不确定只是从程序员视角而言
对象六元组模型
Object = {
Address,
Obj_Type,
Name,
Size,
Obj_value,
Alignment,
}
例:
char b='a';``int* p;``int* p=NULL;``int c[2];``int c[2]={1,2}``const int e[2][3]的对象六元组?
例:定义一个对象
int* f[2][3][4]={},对象f的Object_Type是什么?这个数组对象的元素对象类型是什么?元素对象对应的指针类型是什么?
int*[2][3][4]``int*[3][4]``int*(*)[3][4]
通过字符串字面量(String Literal)
encoding-prefix"s-char-sequence"
五种编码前缀:无编码、u8、u、U、L
| 编码前缀 | 元素字符对象类型 | 编码规则 |
|---|---|---|
| u8 | char8_t | UTF-8 |
| u | char16_t | UTF-16 |
| U | char32_t | UTF-32 |
| L | wchar_t | 实现定义 |
四种对象存储周期:static,automatic,allocated,thread
1 | int main(int argc, char* argv[]) { |
通过复合字面量(Compound Literal)
(storage-class-specifiers type-name) Braced-Initializer
例:
(int[]){1,2}
通过内存管理函数
内存管理函数:aligned_alloc,calloc,malloc,realloc
例:
malloc(16)
定位对象
表达式,lvavlue,evaluate,rvalue
C语言中一共17种表达式,Evaluate优先级从上往下依次降低。
表达式需要根据语法的规定来进行Evaluate,包括:Value Computation, Initiation of Side Effect.
Value Computation得出表达式的rvalue,及其value of expression
对象定义T O = Initializer,其中的O就是对象标识符。表达式中的对象标识符都是合法的lvalue
- lvalue表达式定位的对象类型
- 数组对象类型
- 普通数组对象类型
- 变长数组对象类型
- 非数组对象类型
- 数组对象类型
lvalue表达式Evaluate规则
1.lvalue conversion:如果lvalue定位的对象是一个非数组类型对象,则rvalue就是该对象的对象值
2.Decay:如果lvalue定位的对象是一个数组类型对象,则rvalue就是该对象第一个元素的首地址,rvalue类型是元素对象类型对应的指针类型
例:
int a={};,rvalue是0,rvalue type是int
const int a={},rvalue是0,rvalue type是int
int b[1]={},rvalue是Address,rvalue type是int*
const int b[1]={},rvalue是Address,rvalue type是const int*
int d[1][1],rvalue是Address,rvalue type是int()[1]
int n=1;int e[1][n]={},rvalue是Address,rvalue type是int()[n]
lvalue的对象值 & lvalue的右值
lvalue的对象值:lvalue定位对象的对象值
lvalue的右值:lvalue这个表达式如果被求值得到的右值
例:
int a=0;int b[1]={1}
a的对象值是0,b的对象值是{1}
a的右值是0,b的右值是b第一个元素的地址
Week 3
常量(Constant)
5种常量都是非左值:
1.integer-constant(整数常量)
2.floating-constant(浮点数常量)
3.enumeration-constant(枚举常量)
4.character-constant(字符常量)
5.predefined-constant(预定义常量)
对任何一个标准有符号整数类型T的对象来说,所有bits分成三个部份:1个sign bit,若干个value bits,0到多个padding bits.
假设sign bit加上value bits的个数是N,N是这个对象类型T的宽度(Width),而N-1是这个类型T的精度(Precision)
T类型对象的对象值范围:到
标准无符号整数类型T的宽度宏定义:
| 标准无符号整数类型 | 宽度宏 | 取值 |
|---|---|---|
| unsigned char | UCHAR_WIDTH | CHAR_BIT |
| unsigned short int | USHRT_WIDTH | 至少8 |
| unsigned int | UNIT_WIDTH | 至少16 |
| unsigned long int | ULONG_WIDTH | 至少32 |
| unsigned long long int | ULLONG_WIDTH | 至少64 |
| bool | BOOL_WIDTH | 1 |
*bool类型宽度是1,size由编译器决定,
_BitInt(N) & unsigned _BitInt(N)
_BitInt(N)类型对象:到,
unsigned _BitInt(N)类型对象:到,
例:_BitInt(17):sizeof(_BitInt(17)) * CHAR_BIT - 17
整数常量
三部分组成:进制前缀+数字序列+类型后缀
求值规则:整数常量的右值就是整数常量中的数字序列代表的数值,右值类型与类型后缀相关,无副作用
例:整数常量10的右值以十进制表示是10,右值类型是int
类型后缀:无、U、L、LL、WB、UL/LU、ULL/LLU、UWB/WBU
例:假设int类型宽度是32,因此其数值表征范围为-2147483648~2147483647,则2147483648U是unsigned int
浮点数常量
三部分组成:进制前缀+符号序列+后缀
十进制浮点数(无前缀)
十六进制浮点数(0x前缀)
| 后缀 | 右值类型 |
|---|---|
| 无 | double |
| f,F | float |
| l,L | long double |
| df,DF | _Decimal32 |
| dd,DD | _Decimal64 |
| dl,DL | _Decimal128 |
十进制浮点数常量符号序列由三部分组成:
1.由整数部分,小数点和小数部分构成的一个十进制实数
2.一个字符e
3.一个由+或-引导的表示幂的指数部分,没有+号默认为整数
浮点数常量的右值就是浮点数常量中的符号序列
十进制浮点数常量的右值:
- 100.5 // 100.5
- 0100.5 // 100.5
- .5 // 0.5
-
- // 100.0
- 1.005e2 // 100.5
- 100.5e0 // 100.5
- 100.5e-1 // 10.05
- 1’00.5e0 // 100.5
十六进制浮点数常量的右值:
字符e替换成p,p和指数部分不能省略
- 0x100.5p0 // 256.3125
- 0x0A.Bp2 // 42.75
float类型的规格化数值存储:
1.设置符号位,整数0,负数1
2.设置阶码位,正常取值00000001~11111110,全0和全1是特殊阶码位。中的n+127就是浮点数的阶码位
3.中的xxx就是小数位
float类型的特殊数值:
- 阶码255,小数全0 -> 符号位0是正无穷+∞,1是负无穷-∞
- 阶码255,小数非0 -> 非数值(NaN),小数最高位1静默qNaN,0是信号sNaN
- 阶码0,小数全0 -> 符号位0是+0.0,1是-0.0
- 阶码0,小数非0 -> 次规格化数(Subnormal Number),旧称Denormal
正最小值
float类型的次规格化表示:
1.设置符号位,正数0,负数1
2.设置阶码位,8个阶码位全0
3.有小数部分固定为0.xxx
能表示到
枚举常量
enum Season {Spring, Summer, Autumn, Winter};
枚举常量,右值分别是0,1,2,3
enum Compass : char {East=2, South, West, North};
枚举常量,右值分别是2,3,4,5
枚举成员类型:
1.如果枚举类型是指定底层类型,枚举成员类型就是枚举类型
2.否则,如果int类型的取值范围能覆盖枚举常量的取值范围,枚举成员类型就是int类型
3.否则枚举成员类型就是实现定义的某一种其他整数类型
枚举常量的右值类型就是枚举成员类型
例: Spring…的右值类型是int;East…的右值类型是enum Compass.
字符常量
两部分组成:编码前缀+单引号包围的有效字符
| 编码前缀 | 右值类型 |
|---|---|
| 无 | int |
| u8 | char8_t |
| u | char16_t |
| U | char32_t |
| L | wchar_t |
例:
‘a’ rvalue:int
u8’a’ rvalue:char8_t
u’a’ rvalue:char16_t
U’a’ rvalue:char32_t
L’a’ rvalue:wchar_t
字符编码
标准ASCII码:7个bit编码,0-127
EBCDIC码:8个bit编码,0-255
扩展ASCII码:8个bit编码,0-255
ISO 8859-N编码:N从1-16,12号编码方案缺失
中文:GB2312、GBK、BIG5、GB18030等
Unicode统一字符集:Unicode为每个字符分配了一个唯一标识,称为码点(Code Point),语法形式是“U+”后跟一个十六进制的数字序列,长度为4到6.支持U+0000到U+10FFFF,能够编码1114112个字符。U+D800到U+DFFF一共2048个码点不能用来编码任何单独字符。兼容标准ASCII码,U+0000到U+007F对应标准ASCII码值的128个字符。
UTF-N中N值指这种编码方式下,编码单元是N个比特(编码单元:一个编码方案中用来表示字符的最小二进制单元)
UTF-8:变长编码方式,字符编码值占用1、2、3、4个编码单元,即8、16、24、32比特
UTF-16:变长编码方式,字符编码值占用1、2个编码单元,即16、32比特
UTF-32:定长编码方式,字符编码值占用1个编码单元,即32比特
基本源字符集和基本执行字符集
基本源字符集:A~Z | a~z | 0~9 | !"#%&'()*+,-./:;<=>?[\]^_{|}~ | 空格 | 水平制表符 | 垂直制表符 | 换页符
基本执行字符集,除源字符集还包括:响铃符,退格符,换行符,回车符,空字符(所有比特位都为0)
字符常量中单引号包围的字符五种形式:
1.源字符集中的字符:单引号,反斜杠,换行 不能直接放在一对单引号内
2.简单转移序列:\’ \\ \" \? \t \v \f \a \b \n \r
3.\u或\U引导的通用字符名:例如,“中”的Unicode码点是U+4E2D,通用字符名就是\u4E2D或\U00004E2D
4.\引导的八进制转移序列:例如,\061或\61,十进制是49,对应’1’的ASCII,因此’\061’和’\61’都是字符’1’(八进制整数数字串长度可以是1到3位,但建议都使用3位数字)
5.\x引导的十六进制转移序列:例如,‘\x31’是字符’1’(十六进制转义不限制只有两位)
字符常量的右值:
| 字符 | 字符形式 | 简单转义形式 | 通用字符名形式 | 八进制形式 | 十六进制形式 |
|---|---|---|---|---|---|
| A | ‘A’ | N/A | ‘\u0041’ | ‘\101’ | ‘\x41’ |
| a | ‘a’ | N/A | ‘\u0061’ | ‘\141’ | ‘\x61’ |
| 0 | ‘0’ | N/A | ‘\u0030’ | ‘\060’ | ‘\x30’ |
| 空字符 | ‘\0’ | N/A | ‘\u0000’ | ‘\000’ | ‘\x00’ |
| * | ‘*’ | N/A | ‘\u002A’ | ‘\052’ | ‘\x2A’ |
| @ | ‘@’ | N/A | ‘\u0040’ | ‘\100’ | ‘\x40’ |
| 退格 | N/A | ‘\b’ | ‘\u0008’ | ‘\010’ | ‘\x08’ |
| 删除 | N/A | N/A | ‘\u007F’ | ‘\177’ | ‘\x7F’ |
字符常量的右值类型:
| 序号 | 编码前缀 | 右值类型 | 字符编码方式 |
|---|---|---|---|
| 1 | 无 | int | 编译环境决定 |
| 2 | u8 | char8_t | UTF-8 |
| 3 | u | char16_t | UTF-16 |
| 4 | U | char32_t | UTF-32 |
| 5 | L | wchar_t | 编译环境决定 |
字符常量右值取值范围的限制:
| 序号 | 编码前缀 | 编码值取值范围 |
|---|---|---|
| 1 | 无 | unsigned char |
| 2 | u8 | char8_t |
| 3 | u | char16_t |
| 4 | U | char32_t |
| 5 | L | wchar_t实际类型的无符号类型 |
例:
‘A’和’\u0041’ U+0041这个码点的字符一定是A,字符常量右值就是字符A的编码值
‘\101’和’\x41’ 字符常量右值也是字符A的编码值
‘\x80’和’\xFF’ 合法字符常量,标准ASCII码的编码值只覆盖0到127 Latin-1编码方案中,‘\xFF’表征的是拉丁小写字母ÿ,而在UTF-8编码方案中,’\xFF’并不能表征一个合法的字符,实际编程中要谨慎使用128到255之间的编码值
‘ÿ’ 和’\u00FF’ 编译环境决定,Latin-1都合法,UTF-8下多个编码单元,有问题
‘中’ 其编码值会是一个实现定义的值,对程序的确定性和可移植性带来挑战
‘中’的编码问题:
1.u8’中’ 不合法,3个编码单元
2.u’中’ 合法,1个编码单元
3.U’中’ 合法,1个编码单元
4.L’中’ 实现定义,移植性差
预定义常量
false,true,nullptr
false与true右值类型为bool,nullptr是一个空指针常量,右值类型为在<stddef.h>中定义的nullptr_t类型
字符串
字符串分配(或重用)的对象类型是字符数组类型。如果lvalue定位的对象是一个数组类型对象,则lvalue表达式Evaluate之后的rvalue就是该对象第一个元素的手地址,rvalue类型是元素对象类型对应的指针类型。
例:“hello”,u8"hello",u"hello",U"hello",L"hello"的六元组模型?(见ppt)
u8意味着每个字符编码为UTF-8,u意味着每个字符编码为UTF-16,U意味着字符编码为UTF-32,L意味着字符编码为实现定义
括号表达式
(exp):括号表达式(exp)等价于exp,但(exp)的evaluate优先级提升到最高
lvalue求值的场景差异
lvalue的求值分类:
1.完整求值:定位到一个对象,然后根据对象类型通过左值转换或者退化的机制来得到右值和右值类型
2.部分求值:完成定位对象这个步骤,但不会触发后续左值转换或退化的环节,也就说不会得到一个右值和右值类型
3.不求值:左值不进行求值,不对这个左值求值也意味着组成左值的所有子表达式也都不会被求值
| 规则 | 场景 | 定位 | 求右值 |
|---|---|---|---|
| 不求值 | 左值作为泛型选择的控制表达式 | 否 | 否 |
| 不求值 | 左值定位对象类型非VLA且作为sizeof操作数 | 否 | 否 |
| 不求值 | 左值定位对象类型非VLA且作为typeof操作数 | 否 | 否 |
| 不求值 | 左值定位对象类型非VLA且作为typeof_unal操作数 | 否 | 否 |
| 部分求值 | 字符串字面量作为初始化器初始化字符数组 | 是 | 否 |
| 部分求值 | 左值作为&操作数 | 是 | 否 |
| 部分求值 | 左值定位对象类型是结构体/联合体类型且作为.左操作数 | 是 | 否 |
| 部分求值 | 可修改左值作为赋值运算符左操作数 | 是 | 否 |
| 部分求值 | 满足特定类型的可修改左值作为后缀运算符++操作数 | 是 | 否 |
| 部分求值 | 满足特定类型的可修改左值作为后缀运算符–操作数 | 是 | 否 |
| 部分求值 | 满足特定类型的可修改左值作为一元运算符++操作数 | 是 | 否 |
| 部分求值 | 满足特定类型的可修改左值作为一元运算符–操作数 | 是 | 否 |
| 部分求值 | 左值定位对象类型是VLA且作为sizeof操作数 | 是 | 否 |
| 部分求值 | 左值定位对象类型是VLA且作为typeof操作数 | 是 | 否 |
| 部分求值 | 左值定位对象类型是VLA且作为typeof_unal操作数 | 是 | 否 |
| 完整求值 | 其他情况 | 是 | 是 |
例:
``C
int main(int argc, char argv[]) {
char a[6] = {};
char b[6] = {‘h’,‘e’,‘l’,‘l’,‘o’,‘\0’};
char c[6] = “hello”; // 1
char p = “hello”; // 2return 0;
}1:部分求值
2:完整求值
Week 4
表达式的递归求值机制
1.表达式可能包含子表达式(递归到初等表达式)
2.表达式的求值蕴含着对其子表达式求值(递归完成)
理解
1+2的求值:1:<1,int>,2:<2,int>,1+2:<3,int>;子表达式的求值,是为上一级表达式提供一个值。
| 序号 | 表达式类别 | 序号 | 表达式类别 |
|---|---|---|---|
| 1 | 初等表达式 | 10 | 按位与表达式 |
| 2 | 后缀表达式 | 11 | 按位异或表达式 |
| 3 | 一元表达式 | 12 | 按位或表达式 |
| 4 | 强制类型转换表达式 | 13 | 逻辑与表达式 |
| 5 | 乘性表达式 | 14 | 逻辑或表达式 |
| 6 | 加性表达式 | 15 | 条件表达式 |
| 7 | 移位表达式 | 16 | 赋值表达式 |
| 8 | 关系表达式 | 17 | 逗号表达式 |
| 9 | 相等表达式 |
后缀表达式
1 | postfix-expression: |
后缀表达式:初等表达式、后缀运算符构造的表达式、复合字面量
后缀运算符:[]、()、.、->、++、--
一元表达式
1 | unary-expression: |
一元运算符:前缀++/--、sizeof、alignof、&、*、一元+/-、~、!
赋值表达式
1 | assignment-expression: |
赋值运算符:=、*=、/=、%=、+=、-=、<<=、>>=、&=、^=、|=
typeof运算符
左值也可以作为typeof运算符的操作数,但typeof构造的不是表达式
例:
int a; typeof(a)等价于typeof(int)
什么是Locator Value?
1.Locator Value在标准文本中只出现了一次
2.这是C语言底层核心设计思想体现
3.左值的求值,都必须定位对象,Locator Value就提供了定位对象的手段
4.Locator Value只是一个逻辑概念
5.左值作为子表达式,为上层表达式提供定位自身对应对象的手段
定位非数组对象的lvalue的求值规则
给定一个能定位非数组对象的左值exp,如果这个表达式
1、作为泛型选择的控制表达式(课程后面再介绍)
2、作为sizeof的操作数,例如:sizeof exp
3、作为typeof的操作数,例如:typeof exp,typeof_unqual exp
4、作为alignof的操作数,例如:alignof exp(标准不支持)
-> 不求值
5、作为&的操作数,例如:&exp
6、作为后缀运算符++/–的操作数,例如:exp++
7、作为一元运算符++/–的操作数,例如:++exp
8、作为.的左操作数,例如:exp.
9、作为赋值运算符的左操作数,例如:exp=,exp+=
-> 部分求值
对象标识符的求值示例
int a = 1;,对象六元组:A:0x0061FE10,Obj_T:int,N:a,S:4,Obj_V:1,Align:4
| 表达式 | 场景 | evaluate结果 |
|---|---|---|
a |
完整求值 | <1, int>(Obj_T的非限定非原子类型) |
sizeof(a) |
不求值 | <4, size_t> |
typeof(a) |
不求值 | 等价于int,不是表达式 |
alignof(a) |
不求值 | <4, size_t>(标准不支持,部分编译器提供) |
&a |
部分求值 | <0x0061FE10, int*> |
++a / --a |
部分求值 | <2, int> / <0, int>,有副作用 |
a++ / a-- |
部分求值 | <1, int>,有副作用 |
a=2 |
部分求值 | <2, int>,类型匹配不是相同 |
a+=2 |
部分求值 | <1,int>+<2,int> -> <3, int>,a只求值1次 |
如何理解
a不求值?并不需要真的访问对象a;上层表达式编译时能否推断,不是子表达式lvalue求值的判断标准。
a+=2 vs. a=a+2
a+=2:a只求值1次,部分求值
a=a+2:a求值2次,一次部分求值,一次完整求值
这两个表达式完全不等价;当左操作数exp求值有副作用的时候,
exp=exp+n会造成未定义行为(后续介绍)。
从int a观察const int a
与int a的唯一差别是Obj_T:const int,对象六元组其他属性一样;完整求值a;的右值类型是int
a++;、a=2等不合法,a是不可修改左值
定位普通数组对象的lvalue的求值规则
给定一个能定位普通数组对象的lvalue表达式exp,如果这个表达式
1、作为泛型选择的控制表达式
2、作为sizeof的操作数,例如:sizeof exp
3、作为typeof的操作数,例如:typeof exp,typeof_unqual exp
4、作为alignof的操作数,例如:alignof exp(标准不支持)
-> 不求值
5、作为&的操作数,例如:&exp
6、如果exp定位的是一个字符串字面量并用于初始化字符数组
-> 部分求值
思考:exp为什么跟++/–,=的规则没有了?因为数组类型的左值是不可修改左值
数组对象的求值示例
| 对象 | 完整求值 | 不求值 | 部分求值 |
|---|---|---|---|
int b[1]={1} |
b: <0x0063FE10, int*> |
sizeof(b): <4, size_t>;typeof(b)等价于int[1];alignof(b): <4, size_t> |
&b: <0x0063FE10, int(*)[1]> |
int c[1][1]={1} |
c: <0x0064FE10, int(*)[1]> |
sizeof(c): <4, size_t>;typeof(c)等价于int[1][1];alignof(c): <4, size_t> |
&c: <0x0064FE10, int(*)[1][1]> |
完整求值时,rvalue是对象第一个元素的首地址,rvalue类型是元素对象类型对应的指针类型
b++、b=NULL不行:b的对象值无法修改
可修改左值 vs. 不可修改左值
给定一个左值exp,这个exp定位一个对象的对象类型Type-name:
1、Type-name是数组类型
2、Type-name是const限定类型
3、Type-name是不完全对象类型
4、Type-name是结构体/联合体类型,其成员对象类型有const限定类型
则这个exp被称为不可修改左值,其他exp被称为可修改左值
不可修改左值不能用作前缀/后缀++/–,赋值运算符的操作数
不可修改左值与对象值是不是真的可修改无关,例如:int b[1] = {1};
字符串字面量的求值
1、一个字符串意味着分配(或重用)一个字符数组对象
2、字符串是一个有效的lvalue
3、定位刚刚分配(或重用)的那个字符数组对象
"hello"的对象六元组:A:0x00404000,Obj_T:char[6],N:N/A,S:6,Obj_V:{'h','e','l','l','o','\0'},Align:1
| 表达式 | 场景 | evaluate结果 |
|---|---|---|
"hello" |
完整求值 | <0x00404000, char*> |
sizeof("hello") |
不求值 | <6, size_t> |
typeof("hello") |
不求值 | 等价于char[6],不是表达式 |
alignof("hello") |
不求值 | <1, size_t> |
&"hello" |
部分求值 | <0x00404000, char(*)[6]> |
char str[]="hello" |
部分求值 | 初始化字符数组,不对"hello"求值 |
思考题:
char* p="hello"和char str[]="hello"这两个hello哪一个需要evaluate?
第一个需要evaluate,表达式的rvalue赋值给对象p;第二个用于初始化字符数组str,不进行evaluate
"hello"分配的对象size为什么一定是6?因为这个对象的对象类型是char[6]
char str[8]="hello":第6个’\0’是"hello"自身带的,后面2个’\0’是自动填充的
复合字面量的求值
1、Compound Literal意味着分配一个指定类型的对象
2、Compound Literal是一个有效的lvalue表达式
3、定位刚刚分配的那个对象
(int){1}的对象六元组:A:0x0066FE10,Obj_T:int,N:N/A,S:4,Obj_V:1,Align:4
| 表达式 | 场景 | evaluate结果 |
|---|---|---|
(int){1} |
完整求值 | <1, int> |
sizeof((int){1}) |
不求值 | <4, size_t> |
typeof((int){1}) |
不求值 | 等价于int,不是表达式 |
alignof((int){1}) |
不求值 | <4, size_t> |
&(int){1} |
部分求值 | <0x0066FE10, int*> |
++(int){1} / (int){1}++ |
部分求值 | <2, int> / <1, int>,有副作用 |
--(int){1} / (int){1}-- |
部分求值 | <0, int> / <1, int>,有副作用 |
(int){1}=2 |
部分求值 | <2, int> |
(const int){1}是不可修改左值;(int[1]){1}完整求值是<0x0067FE10, int*>,&(int[1]){1}是<0x0067FE10, int(*)[1]>
(int){1}+=2vs.(int){1}=(int){1}+2:前者分配一个对象,后者分配两个对象
理解sizeof和&
给定一个定位非变长数组对象的lvalue表达式
1、作为sizeof操作数,该lvalue不求值
2、sizeof(lvalue)的语义是获得lvalue定位对象的大小
3、这个对象的大小由对象类型可知,编译时可以推断出来
4、对象的size在生命周期内不会变化
给定一个定位对象的lvalue表达式
1、跟&结合,lvalue做部分求值
2、如果lvalue定位对象的Storage Duration是static,其对象地址编译时可知
3、否则该对象地址运行时才可获得
定位指针类型对象的lvalue的求值
int a=1; int* p=&a;,p定位一个对象,其对象类型为int*
&a部分求值得到<0x0061FE10, int*>,赋值后p的对象值变成0x0061FE10
p的对象六元组:A:0x0068FE10,Obj_T:int*,N:p,S:4,Obj_V:0x0061FE10,Align:4
p; -> <0x0061FE10, int*>;sizeof(p)/typeof(p)不求值;&p -> <0x0068FE10, int**>;p++/--、p=NULL部分求值
*exp形式的lvalue
设一个表达式exp的evaluate之后的值为<rvalue, rvalue_Type>
如果rvalue_Type是一个完全对象的指针类型,则*exp可以定位一个对象M
1)对象M的Address为Value
2)对象M的Object Type为Value_Type对应的Referenced Type
3)对象M其他属性随之确定
*exp是一个lvalue,定位一个对象,仅此而已,不是获得*exp这个表达式的值;表达式的值只有在evaluate之后才有
Referenced Type和Pointer Type的对应关系是指针关系的核心概念
例:
int a=1; int* p=&a;
p是*的操作数,做完整求值,即<0x0061FE10, int*>
*p定位对象M:地址0x0061FE10,对象类型是int*的Referenced Type即int,其他属性依次确定
&(*p)是<0x0061FE10, int*>,*p是<1, int>,sizeof(*p)是<4, size_t>
给定两个左值E1和E2,假设定位到的对象分别是O1和O2,如果O1和O2的地址相同,且对象类型也一样,则
思考:为什么C语言标准里面不使用变量这个概念?a和*p在行为上是完全一致的,a如果是变量,p为什么不是?
int a=1; const int* p=&a;:a和p定位的是同一块内存吗?是同一个对象吗?
const int a=1; int* p=&a;:*p=2能否编译通过?会引起什么问题?未定义行为
指针算术:p+n
1、p完整求值,得到<rvalue, rvalue_type>
2、计算sizeof(*p)
3、p+n的value = p的rvalue + sizeof(*p)*n
4、p+n的rvalue_Type不变
例:
int a=10; int* p=&a;,p是<0x0061FE10, int*>,p+n是<0x0061FE10+4*n, int*>
p+1是合法表达式;*(p+1)导致未定义行为;*(p+2)也导致未定义行为;p+2呢?
重要概念:One Past The Last Element of The Array Object,后续详细介绍