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来实现类型安全等特性,也即完全对象类型)。完全对象类型拥有sizealign两大属性,通过size_ofalign_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_tint16_tint32_tint64_t等类型,这些类型的位宽被C语言确定为固定的。在移植情境中,为了提高软件的可移植性,通常会使用typedef来定义所使用的类型来避免不同平台的位宽差异。

对于有符号整数,其数据区域由Signed bit,value bit以及padding bit三部分组成。对于无符号整数,其数据区域由value bit以及padding bit两部分组成,需要注意CHAR型没有Padding bit

对于浮点数,C语言中有floatdoublelong double三种类型,分别对应于单精度浮点数,双精度浮点数,长双精度浮点数。这些类型的位宽并没有被C语言规定,而是通过float.h头文件来定义。对于浮点数,其数据区域由sign bitexponent bitfraction 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。这些限定符对应的类型可以通过_Atomicconstvolatilerestrict来定义,除去Atomic外其他类型与原类型对齐,大小,值等属性相同。

限定类型的形式化定义为Q TT Q,其中Q为限定符,T为原类型。这两种是不同的类型,但大小,表示值与对齐方式相同。 对于所限定的类型,考虑其情况如下:

  1. 非数组非指针类型: T, 则有Q typeof(T) = typeof(T) Q

  2. 数组类型: T[N], 则有Q typeof(T)[N] = typeof(T)[N] Q

  3. 指针类型: T*, 则有Q typeof(T)* = typeof(T*) Q

如果不使用typeof将类型作为整体,则对于数组类型与指针类型将修饰符作为左右有所不同:

  1. 数组类型: T[N] ,如果为Q T[N],则为数组类型,派生类型为Q T。

  2. 指针类型: 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 typevalue type相同;对于数组类型,value为对象值;对于数组类型,object type为数组类型,value type为数组元素对应的指针类型,value为首元素地址。

String Literal

String Literal由编码前缀与双引号引导的字符序列两部分组成,编码前缀可分为无编码,U,L,u,u8五种,分别对应于charwchar_twchar_tchar16_tchar8_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,reallocfree总计5个函数,前四个函数用于分配内存,最后一个函数用于释放内存。需要注意malloc用于分配无对象类型的匿名对象,并且提供了fundamental alignment的对齐大小,以便类型转换时不会出错。aligned_alloc用于指定对齐分配大小的内存分配,但是根据指定的对齐大小可能存在分配失败返回NULL或者类型转换不满足对齐要求的情况。calloc用于分配数组类型的匿名对象,并且初始化为0。realloc用于重新分配内存,如果新的内存大小小于原内存大小,则会截断,如果新的内存大小大于原内存大小,则会在原内存后面分配新的内存,如果分配失败则返回NULL。

lvalue与rvalue

  • 表达式:表达式是由运算符与操作数组成的序列。

  • lvalue(locatedValue)左值指的是能定位对象的表达式。

evaluate

C语言提供了17种表达式,表达式可以根据语法进行求值,这一过程分为两部分,包括:

  1. 值的计算:得到对应表达式的rvalue与rvalue type。

  2. 确定副作用:环境状态的改变

基础表达式具有最高优先级,包括:

  1. 标识符

  2. 常量

  3. 字符串

  4. 括号表达式

  5. 泛型选择

常量

常量包括:整数常量,浮点数常量,枚举常量,字符常量

整数常量主要由:** 编码前缀,数字序列,后缀**三部分组成。

前缀主要包括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_tchar32_tchar16_tchar8_t类型,无编码前缀则为int类型(整形提升)。字符常量的rvalue为对应的字符类型,其背后的类型可显式指定或是编译器自行确定。

attribute

参考

  1. C Specification