设计理念:
C语言的一个设计理念就是声明变量和使用变量的形式应该是一致的
优点:声明变量和使用变量时的运算符优先级是相同的
缺点:运算符的优先级是C语言过度解析的部分之一
术语:
变量声明中使用到的符号的术语:(并不是所有的组合是合法的)
数量 | 名称 | 举例 |
0或更多 | 指针(pointer) | * |
一个 | 说明符(declarator) | identifier identifier[size] identifier(args) (declarator) |
0个或一个 | 初始化器(initializer) | = initial_value |
至少一个类型限定符 | 类型说明符 存储类型 类型修饰符 |
void char short extern static register const volatile |
0个或一个 | 更多的说明符 | ,declarator |
一个 | 分号 | ; |
关于struct和union:
- 类型说明符是:struct {stuff...}
- 声明形式:struct {stuff...} s;
- tag:为了简写,可以在struct后面加上结构体tag:struct struct_tag {stuff...},这样就声明了struct_tag代表具体的类型集合{stuff...},之后的声明就可以使用struct struct_tag s;
关于参数传递的两点说明:
- 某些书中会说"参数传递到调用函数的时候是从右到左压到栈中",这种说法是不对的。参数传递的时候会尽可能使用寄存器,所以一个整数和一个只含有一个整数的结构体的传递方法是完全不同的,一个整数可能通过寄存器传递,结构体会通过栈传递
- 通常一个数组是不能直接通过赋值来传递整个数组的,或者被一个函数返回,但是通过把数组作为结构体的唯一一个成员就可以实现
虽然union具有和结构体类似的结构,但是它们有完全不同的存储方式
- 结构体将每个成员存储到其前一个成员后面
- union的所有成员都存储在相同的起始地址,所以不同的成员是相互覆盖的,同时只能有一个成员可以被存储
union的一个明显的问题就是存储了一种类型但是用另一种类型取的类型安全问题,Ada语言主张在记录中存储说明字段来解决这个问题,但是C语言依赖于程序员能够记住存了什么而不采取任何措施
union有两个用途:
- 节约空间
- 所有的成员有相同的存储空间大小的时候,就可以用不同的方式解析相同的二进制数据而不用显式的进行类型转换
声明语句的解析:
C语言可以有非常复杂的声明语句而让人无法轻易的搞清楚到底定义了什么东西
有两种解析方式:
方式一:优先级法则
- 声明的解析从名称开始,然后按照优先级规则继续执行
- 优先级从高到低:
- 将声明的各个部分组合在一起的括号
- 后缀操作符:指明一个函数的"()"和指明数组的"[]"
- 前缀操作符:指明是"指向..."的星号
- 如果"const"或"volatile"关键字和一个类型说明符相邻,就应用到这个类型说明符;否则,如果应用到左边紧邻的"*"
方式二:状态机规则
- 从最左侧的标识符开始,"identifier是" "identifier is"
- 如果右侧是"[]"就获取,"一个...的数组" "array of"
- 如果右侧是"()"就获取,"参数为...返回值为...的函数" "function returning"
- 如果左侧是"("就获取整个括号中的内容,这个括号包含的是已经处理过的声明,回到步骤2
- 如果左侧是"const""volatile""*"就获取,持续读取左侧的符号直到不再是这三个之中的,之后返回步骤4
- "const":"只读的" "read only"
- "volatile":"volatile" "volatile"
- "*":"指向..." "pointer to"
- 余下的就是基本数据类型
举例:char *(*c[10])(int **p);
- 按照优先级规则解析:
- c是一个...数组---c[10]
- c是一个指向...的指针的数组---*c[10]
- c是一个指向参数为...的返回值为...函数的指针的数组---(*c[10])()
- c是一个指向参数为整数的指针的指针的返回值为...函数的指针的数组---(*c[10])(int **p)
- c是一个指向参数为整数的指针的指针的返回值为指向...的指针函数的指针的数组---*(*c[10])(int **p)
- c是一个指向参数为整数的指针的指针的返回值为指向char的指针函数的指针的数组---char *(*c[10])(int **p)
- 按照状态机规则解析:
- c是...---c---1->2
- c是一个...的数组---c[10]---2->3
- c是一个指向...的指针的数组---*c[10]---3,4,5->4
- c是一个指向...的指针的数组---(*c[10])---4->2
- c是一个指向参数为int的指针的指针返回值为...的函数的指针的数组---(*c[10])(int **p)---2,3->4
- c是一个指向参数为int的指针的指针返回值为...的函数的指针的数组---(*c[10])(int **p)---4->5
- c是一个指向参数为int的指针的指针返回值为指向...的指针的函数的指针的数组---*(*c[10])(int **p)---5->6
- c是一个指向参数为int的指针的指针返回值为指向char的指针的函数的指针的数组---*(*c[10])(int **p)---5->6
实现程序:
状态机可以实现为自动翻译程序:
https://github.com/biaoJM/translate-C-declaration-statement
typedef和#define:
1.宏定义的类型名和其他类型说明符一起执行定义,但是typedef只能使用它本身
#define peach int
unsigned peach i; /* works fine */
typedef int banana;
unsigned banana i; /* Bzzzt! illegal */
2.typedef的类型会实施到每个说明符,但是宏定义不会
#define int_ptr int *
int_ptr chalk, cheese;
// 结果为:
int * chalk, cheese;
导致chalk是int的指针类型,而cheese是int类型
typedef char * char_ptr;
char_ptr Bentley, Rolls_Royce;
Bentley和Rolls_Royce都是char指针类型
命名空间:
C语言的命名空间
- 标签名,所有的标签名的命名空间
- tags,对于所有的结构体、枚举类和联合体的tag具有的命名空间
- 成员名称,对每个结构体、枚举类或联合体都有自己的成员命名空间
- 其他,其他名称的命名空间
所以对声明:
typedef struct baz {int baz;} baz;
这样的定义是合法的:
struct baz variable_1; /*这里baz是定义的类型名*/
baz variable_2; /*这里baz是tag*/
对于这样的定义:
struct foo {int foo;int foo2;} foo;
第一个foo是这个结构体的tag,第二个foo是一个结构体变量
sizeof(foo)的结果是变量foo的大小,所以如果声明时这样的:
struct foo {int foo;int foo2;} *foo;
sizeof(foo)返回的就是4而不是8,如果想要用tag获取结构体的大小:sizeof(struct foo)——tag只有和struct关键字一起才起作用
而如果这样定义:
typedef struct foo {int foo;int foo2;} foo;
那么就不能再用foo作为变量名,因为此时foo不再是tag而和变量有相同的命名空间
参考:
《expert C programming:deep C secrets》
Chapter 3. Unscrambling Declarations in C