C语言系统编程
Published: 10/1/2023
对象与类型
对象
C语言中的对象是指存储在内存中的数据区域,并且通常具有若干值,这与面对对象中的对象的概念有所不同。C语言很灵活,对于变量与常量的界限往往并不明朗,比如const
修饰定义时我们通常认为是常量,但是const volatile
在OS中是可变的。这一概念的不明朗其实会让编译器实现变得复杂,所以C语言并没有真正的变量与常量的概念,将其理解为对象会更加合适。对象就是内存中的某段连续区域,我们使用标识符Identifier
来引用这一对象,尽管引用这一词与其他高级语言中的引用定义的操作有所不同。
类型
内存可以被简单地理解为字节数组。一字节Byte通常由8比特bit组成,对于特定的架构,内存的寻址位宽通常有16位,32位,64位,128位,这一位宽确定了机器的最大寻址范围。需要注意,在某些特定架构中一字节可能并不等于8比特。
对象既然是内存中的数据区域,那么就可被解释成某一类型,也即对象的表示。对象的值相同,对象的表示相同;对象的表示相同,对象的值可能不同。类型又分为完全对象类型与不完全对象类型,其区别在于是否定长(如果了解过RUST会发现RUST很大程度上利用了Sized这一Trait来实现类型安全等特性,也即完全对象类型)。完全对象类型拥有size
与align
两大属性,通过size_of
与align_of
来获取。
C语言中的类型主要被定义为 基本数据类型,派生数据类型。基本类型主要包括:CHAR型,有符号整数,无符号整数,浮点数等 CHAR包含char,unsigned char与signed char 三类类型。 C语言中的CHAR型被定义为CHAR_BITS
比特大小(该宏由C语言提供,并且在大多数情况下值为8,某些情况可能并不为8)。对于无符号整数类型,C语言有着其他的定义,主要有:
-
short int
:至少16位 -
int
:至少16位 -
long int
:至少32位 -
long long int
:至少64位
请注意上面提到的“至少”一词,这是因为C语言并没有规定具体的位宽,而是通过limits.h
头文件来定义。早期诸如Object-C对于size_of(int)
的返回值则为2,也即16位,虽然目前大多数情况都为32位。为了使用确切的位宽,C语言在stddef.h
中提供了int8_t
,int16_t
,int32_t
,int64_t
等类型,这些类型的位宽被C语言确定为固定的。在移植情境中,为了提高软件的可移植性,通常会使用typedef
来定义所使用的类型来避免不同平台的位宽差异。
对于有符号整数,其数据区域由Signed bit
,value bit
以及padding bit
三部分组成。对于无符号整数,其数据区域由value bit
以及padding bit
两部分组成,需要注意CHAR型没有Padding bit
。
对于浮点数,C语言中有float
,double
,long double
三种类型,分别对应于单精度浮点数,双精度浮点数,长双精度浮点数。这些类型的位宽并没有被C语言规定,而是通过float.h
头文件来定义。对于浮点数,其数据区域由sign bit
,exponent bit
,fraction bit
三部分组成。
对于派生数据类型,C语言中有数组
,指针
,结构体
,共用体
,枚举
,原子
与函数
等,其中函数之前的类型为对象类型,而函数单独作为函数类型。这些类型的定义与基本数据类型有所不同,其定义更加灵活,可以根据需要定义。对于数组,其定义为type name[size]
,对于结构体,其定义为struct name{type name;type name;...}
,对于共用体,其定义为union name{type name;type name;...}
,对于枚举,其定义为enum name{value,value,...}
。这些类型的定义可以嵌套,比如结构体中可以包含数组,共用体,枚举等。
数组类型
形式化定义为T[N],T为元素类型,且必须为完全对象类型,N表示数组元素的个数。若没有指定N,则该类型为不完全对象类型。如果N为常量或常量表达式,则T[N]为普通数据类型,否则为变长数组类型。数组被视为Array of T
。
常量指单纯的数,而常量表达式指常量与运算符的组合。
如果使用typedef
来别名数组类型,通常用法为typedef int Array[5]
,这样Array
就被定义为int[5]
类型,请注意中括号的位置。C23中提供了typeof(T)
来将T处理为整体。
完全对象数组类型仍然能够派生。对于int[5]
,若N为10,则int[10][5]
的引用数据类型为int[5]
,请注意[10]的位置。
所以对于数组类型的代码解析,只需要将从左至右的第一个[N]去掉即为元素类型。
指针类型
形式化定义为T*,T为元素类型,可为不完全对象类型。指针被视为Pointer to T
。指针类型的引用数据类型为T。
对于引用类型为数组类型的派生,通过在第一个中括号前添加(*)
来表示对应指针类型;对于引用类型为指针类型的派生,通过在表示其为指针的*
后添加*
来表示对应指针类型。
对于引用类型为指针类型的派生,通过在表示其为指针的*
后添加中括号来表示对应数组类型。
在识别过程中,括号的优先级最高,优先寻找最内层的括号。
该图取自课程PPT。
限定类型
C语言中存在四种限定符,需要注意限定类型与对应的原类型是不同类型,限定符分别为:atomic,const,volatile,restrict。这些限定符对应的类型可以通过_Atomic
,const
,volatile
,restrict
来定义,除去Atomic外其他类型与原类型对齐,大小,值等属性相同。
限定类型的形式化定义为Q T
或T Q
,其中Q为限定符,T为原类型。这两种是不同的类型,但大小,表示值与对齐方式相同。
对于所限定的类型,考虑其情况如下:
-
非数组非指针类型: T, 则有Q typeof(T) = typeof(T) Q
-
数组类型: T[N], 则有Q typeof(T)[N] = typeof(T)[N] Q
-
指针类型: T*, 则有Q typeof(T)* = typeof(T*) Q
如果不使用typeof将类型作为整体,则对于数组类型与指针类型将修饰符作为左右有所不同:
-
数组类型: T[N] ,如果为Q T[N],则为数组类型,派生类型为Q T。
-
指针类型: T* ,如果为Q T*,则为指针类型,则引用类型为Q T;如果T* Q,则为Q限定T*类型。
如果多个不同限定符同时出现,则出现顺序并不重要,如果多个相同限定符同时出现,则可视为一个限定符。
对象的属性
对于name
,如果有名称则对象为具名对象,否则为匿名对象。对于object type
,对象可以拥有对象类型,也可以不拥有对象类型。
对象声明的形式化定义为T O =initializer
.
标量类型包括算数类型与指针类型。对于标量类型与对应的限定类型,Initializer一共有两种形式,分别为空初始化列表与除逗号表达式以外的表达式或表达式列表。其中空列表表示将对象值设置为该类型的缺省值。对于数组类型,其Initializer可为空初始化列表与对应数组元素的子元素初始化列表,其中的每个子元素初始化都用来初始化数组中的一个元素。
需要注意不完全数组类型不能使用空列表初始化,并且使用非空列表初始化时,数组长度为初始化列表的长度;对于完全数组类型,如果initializer长度小于数组长度,则剩余元素初始化为缺省值,如果initializer长度大于数组长度,则报错。
如果不存在initializer,则对象的值为indeterminate
。
对象的基本属性有: size
,align
,name
,object type
,value
,value type
,address
七部分。
对于非数组类型,object type
与value type
相同;对于数组类型,value
为对象值;对于数组类型,object type
为数组类型,value type
为数组元素对应的指针类型,value
为首元素地址。
String Literal
String Literal
由编码前缀与双引号引导的字符序列两部分组成,编码前缀可分为无编码,U
,L
,u
,u8
五种,分别对应于char
,wchar_t
,wchar_t
,char16_t
,char8_t
类型。
C语言定义了4种对象存储周期,分别为static
,automatic
,allocated
,thread
。其中static存储周期的对象其有效期覆盖整个程序运行期间,并且在运行之前,其值会被初始化。
合法字符串字面量不能包含"
,\
与换行。对于不显式包含\0
的字符串字面量被称为字符串。
Compound Literal
Compound Literal
由类型名与初始化列表两部分组成,用于分配一个匿名对象,其存储周期为automatic。其形式化定义为(T) {initializer}
,其中T为类型名,initializer为初始化列表。
内存管理函数
C语言在stdlib.h
中提供了malloc
,calloc
,aligned_alloc
,realloc
与free
总计5个函数,前四个函数用于分配内存,最后一个函数用于释放内存。需要注意malloc用于分配无对象类型的匿名对象,并且提供了fundamental alignment
的对齐大小,以便类型转换时不会出错。aligned_alloc
用于指定对齐分配大小的内存分配,但是根据指定的对齐大小可能存在分配失败返回NULL或者类型转换不满足对齐要求的情况。calloc
用于分配数组类型的匿名对象,并且初始化为0。realloc
用于重新分配内存,如果新的内存大小小于原内存大小,则会截断,如果新的内存大小大于原内存大小,则会在原内存后面分配新的内存,如果分配失败则返回NULL。
lvalue与rvalue
-
表达式:表达式是由运算符与操作数组成的序列。
-
lvalue
(locatedValue)左值指的是能定位对象的表达式。
evaluate
C语言提供了17种表达式,表达式可以根据语法进行求值,这一过程分为两部分,包括:
-
值的计算:得到对应表达式的rvalue与rvalue type。
-
确定副作用:环境状态的改变
基础表达式具有最高优先级,包括:
-
标识符
-
常量
-
字符串
-
括号表达式
-
泛型选择
常量
常量包括:整数常量,浮点数常量,枚举常量,字符常量
整数常量主要由:** 编码前缀,数字序列,后缀**三部分组成。
前缀主要包括0b
,0
,0x
分别对应于二进制,八进制,十六进制,无前缀表示十进制。
后缀主要包括: u
,l
,ll
,ul
,ull
分别对应于unsigned,long,long long,unsigned long,unsigned long long。 后缀主要用于确定常量的类型范围。很有意思的是需要注意十六进制与十进制类型范围确定的不同。具体概括就是,十进制只会在显式指定unsigned才会在unsigned对应范围的类型中确定,而十六进制无论是否显式指定unsigned都会在unsigned与signed对应范围的类型中确定。l与ll的指定则是指定了类型确定的开始类型。
浮点数常量与整数常量类似,同样由:进制前缀,数字序列,后缀四部分组成。
枚举常量由enum
定义,rvalue对应类型为对应的枚举类型,其背后的类型可显式指定或是编译器自行确定。
字符常量由编码前缀与单引号引导的有效字符组成,编码前缀主要包括L
,U
,u
,u8
,分别对应于wchar_t
,char32_t
,char16_t
,char8_t
类型,无编码前缀则为int类型(整形提升)。字符常量的rvalue为对应的字符类型,其背后的类型可显式指定或是编译器自行确定。