- 3.1 只有编译器才会喜欢的语法
C语言的声明语法(声明模型)很晦涩,容易成为程序员的障碍,正是由于在组合类型方面的笨拙行为,C语言显得很复杂。
造成这种情况的原因是因为“
类型模型(type model)”这个概念对于当时的编程语言理论而言尚属陌生。BCPL(C语言前身)语言几乎没有类型,其以二进制字作为唯一的数据类型。
C语言的设计哲学之一:
对象的声明形式与它的
使用形式要尽可能
相似。
int *p[3]是一个int类型的指针数组。
其使用方法如下:
// p 是一个指针数组,其中的每个元素都是一个指向 int类型的指针
int * p[3];
// 声明三个 int数组
int a0[] = {2, 3, 4};
int a1[] = {0, 1};
int a2[] = {-8, 9, 0, 1, 3};
// 赋值
p[0] = a0;
p[1] = a1;
p[2] = a2;
int i;
printf( "a0:\t");
for( i = 0; i < 3; i++, p[0]++)
{
printf("%d\t" , *p[0]);
}
printf( "\na1:\t");
for( i = 0; i < 2; i++, p[1]++)
{
printf("%d\t" , *p[1]);
}
printf( "\na2:\t");
for( i = 0; i < 4; i++, p[2]++)
{
printf("%d\t" , *p[2]);
}
其执行结果如下:
C语言的声明所存在的最大问题是你无法以一种习惯性的
从左向右的方式阅读一个声明。
关于const
指针所指向的对象是只读的两种声明:
const int i [] = {1024, 1045};
// 指针所指向的对象是只读的
const int * grape ; // 等同于 const int (*grape)
int const * orange ; // 等同于 int const (*orange)
grape = & i ;
orange = & i ;
printf( "%d\t" , *grape ); // grape: 1024
grape++; // 合法,改变指针本身的值,grape指向i[1]
printf( "%d\n" , *grape ); // grape: 1045
//(*grape) += 4; // 非法,改变指针所指向的 int 对象的值
指针本身是只读 的声明:
int i = 1024;
// 指针本身是只读的
int * const apple = &i;
*apple += 4; // 合法,改变指针所指向的对象的值
// apple += 4; // 非法,改变指针本身的值
printf("i: %d\n", *apple ); // i的值变为1028
上面这段代码所产生的错误如下:
总结的规则:在指针的声明中,只要const“紧挨着”指针变量就说明该指针本身是只读的,否则就是该指针所指向的对象是只读的。
- 3.2 声明是如何形成的
声明器:标识符以及与它组合在一起的任何指针(*)、函数括号(())、数组下标([])等。
关于结构(struct)
结构就是一种把一些数据项组合在一起的数据结构。其语法为:
struct { 内容 ... };
结构的定义后可以跟一些变量名,表示这些变量的类型是这个结构:
struct { 内容 ... } plum, pomegranate, pear ;
struct关键字后可以加一个可选的“结构标签”,可以作为结构的简写形式(
struct
fruit_tag
)用于以后定义变量:
struct fruit_tag { 内容... } plum, pomegranate , pear;
因此,结构的通常形式:
struct 结构标签 (可选){
类型1 标识符1;
类型2 标识符2;
....
} 变量定义( 可选);
作者建议不要把结构的声明和变量的定义混合在一起,而是分开,使代码更容易阅读。
变量的声明应该与类型的声明分开。
两个跟结构有关的参数传递问题:
- 函数在传递参数时,首先尽可能地存放在寄存器中(追求速度)。——一个int型参数一般会被传递到寄存器中,而结构参数则很可能被传递到堆栈中。
- 在结构中放置数组,可将该结构(内包含数组)当做第一等级的类型(赋值语句拷贝整个数组、作为函数的返回类型、传值调用)。
struct s_tag
{
int a[100]; // 结构内包含数组
};
// 可将该结构作为第一等级的类型
struct s_tag orange, lime, lemon ;
// 结构作为函数参数及函数返回值
struct s_tag twofold( struct s_tag s)
{
int j;
for( j = 0; j < 100; j++)
{
s.a [j] *= 2;
}
return s;
}
int main ()
{
int i;
for( i = 0; i < 100; i++)
{
lime.a [i] = 1;
}
lemon = twofold(lime );
orange = lemon; // 给整个结构赋值
}
结构中包含一个指向结构本身的指针,这种方法常用于列表(list)、树(tree)等。
关于联合(union)
结构中每个成员依次存储,而在联合中,所有成员都成偏移量为0处开始存储,在某一时刻,只有一个成员真正存储于该地址。联合因此也叫“变体记录”。
联合的一般形式:
union 可选的标签
{
类型 标识符;
类型 标识符;
...
类型N 标识符N ;
}可选的变量定义;
联合一般作为大型结构的一部分存在。用以节省存储空间,因为某些数据项
不可能同时出现。
联合也可以把同一个数据解释成两种不同的东西,而不是把两个不同的数据解释为同一种东西。
作者最后列举数据(15W行与机器无关的OS源代码),说明结构出现的次数大约是联合的一百倍。
关于枚举(enum)
枚举通过一种简单的途径,把一串名字与一串
整型值联系在一起。
对于C来说,很少有什么事只能考枚举来完成而无法用#define解决的。
枚举的一版形式:
enum 可选标签
{
内容...
}可选变量定义;
缺省情况下,整型值从0开始。如果对列表中的某个标识符进行了赋值,那么后面的标识符依次大1。
枚举与#define的一个不同:#define定义的名字一般在编译时被丢弃,而枚举名字通常在调试器中一直可见。
- 3.3 优先级规则
理解C语言声明的优先级规则:
A 声明从它的名字开始读取,然后按照优先级依次读取。
B 优先级顺序:
B. 1 声明中被括号括起来的部分
B. 2 后缀操作符:
() 表示这是一个函数
[] 表示这是一个数组
B. 3 前缀操作符: * 表示“指向...的指针”。
C 如果const、volatile关键字的后面紧跟类型说明符(int long float),那么它作用于类型说明符。在其他情况下,其作用于它紧邻的指针星号。
- 3.5 关于typedef
一般情况下,typedef用于简洁地表示指向其他东西的指针。
不要在一个typedef中放入几个声明器;不要把typedef嵌入到声明的中间部分。
在同一个代码块中,typedef引入的名字不能与其他标识符同名。
- 3.6 typedef和define的区别
1. 可以用其他类型说明符对宏类型名进行扩展,但对typedef定义的类型名就不能这样做;
#define peach int
typedef int orange;
unsigned peach i; /* 合法,相当于 unsigned int i */
unsigned orange j; /* 不合法*/
2. 在连续几个变量的声明语句中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而#define定义的类型则无法保证。
#define d_string char*
d_string str1 , str2; /* str1 的类型为char *, str2 的类型为 char */
typedef char * t_string;
t_string str3 , str4; /* str3 和str4 的类型均为 char* */
- 3.7 C语言中的名字空间
- 标签名(label name)
- 标签(tag)。——用于所有的结构、枚举、联合;
- 成员名:每个结构或联合都有自己的名字空间;
- 其他
typedef struct fruit
{
int weight ;
int price_per_lb ;
} fruit; /* 结构标签与结构类型具有了相同的名字 */
struct fruit mandarin; /* 使用结构标签 fruit */
fruit orange ; /* 使用结构类型 fruit */
如上这种情况,结构标签与typedef的结构别名具有了相同的名字,作者在此处建议:当你有两个不同的东西时,一个比较好的原则就是用不同的名字来称呼它们。这样做减少了混淆的危险(这始终是软件的一个重要准则)。比如此处可以在结构标签后加一个"_tag"后缀。