之前写过一篇C的语言的角落,介绍了一些C中的一些非常用特性(http://blog.csdn.net/yang_yulei/article/details/34557625),最近又整理了一些边角的知识,特开此文。
switch语句中的case
(case 关键词可以放在if-else或者是循环当中)
switch (a) { case 1:; // ... if (b==2) { case 2:; // ... } else case 3: { // ... for (b=0;b<10;b++) { case 5:; // ... } } break; case 4: }
指定初始化(C99)
在C99之前,你只能按顺序初始化一个结构体。在C99中你可以这样做:
struct Foo {
int x;
int y;
int z;
};
Foo foo = {.z = 3, .x = 5};
这段代码首先初始化了foo.z,然后初始化了foo.x. foo.y 没有被初始化,所以被置为0。
这一语法同样可以被用在数组中。以下三行代码是等价的:
int a[5] = {[1] = 2, [4] = 5};
int a[] = {[1] = 2, [4] = 5};
int a[5] = {0, 2, 0, 0, 5};
受限指针(C99)
关键字restrict仅对指针有用,修饰指针,表明要修改这个指针所指向的数据区的内容,仅能通过该指针来实现,此关键字的作用是使编译器优化代码,生成更高效的汇编代码。
例如:
int foo (int* x, int* y) { *x = 0; *y = 1; return *x; }
很显然函数foo()的返回值是0,除非参数x和y的值相同。可以想象,99%的情况下该函数都会返回0而不是1。然而编译起必须保证生成100%正确的代码,因此,编译器不能将原有代码替换成下面的更优版本:
int f (int* x, int* y) { *x = 0; *y = 1; return 0; }
现在我们有了restrict这个关键字,就可以利用它来帮助编译器安全的进行代码优化了,由于指针 x 是修改 *x的唯一途径,编译起可以确认 “*y=1; ”这行代码不会修改 *x的内容,因此可以安全的优化:
int f (int *restrict x, int *restrict y) { *x = 0; *y = 1; return 0; }
很多C的库函数中用restrict关键字:
void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n) 这是一个很有用的内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,即 dest指针所指的区域,不能让别的指针来修改,即src的指针不能修改. 相对应的别一个函数 memmove(void *dest,const void
* src,size_t)则可以重叠。
静态数组索引(C99)
void f(int a[static 10]) {
/* ... */
}
你向编译器保证,你传递给f 的指针指向一个具有至少10个int 类型元素的数组的首个元素。我猜这也是为了优化;例如,编译器将会假定a 非空。编译器还会在你尝试要将一个可以被静态确定为null的指针传入或是一个数组太小的时候发出警告。
void f(int a[const]) {
/* ... */
}
你不能修改指针a.,这和说明符int * const a.作用是一样的。然而,当你结合上一段中提到的static 使用,比如在int a[static const 10] 中,你可以获得一些使用指针风格无法得到的东西。
多字符常量
int x = ‘ABCD‘ ;
这会把x的值设置为0×41424344(或者0×44434241,取决于大小端)我们一般的小端机上,低位存在低字节处,DCBA依次从低字节到高字节排列。
这只是一种看起来比较炫酷的写法,一般没什么用。
关于EOF
EOF是初学者比较困惑的一个东西,算不上生僻的知识点,但容易造成误解,出错,所以在这里也总结一下。
EOF是end of file的缩写,表示”文字流”(stream)的结尾。这里的”文字流”,可以是文件(file),也可以是标准输入(stdin)。
比如,下面这段代码就表示,如果不是文件结尾,就把文件的内容复制到屏幕上。
int c; while ((c = fgetc(fp)) != EOF) { putchar (c); }
很自然地,我就以为,每个文件的结尾处,有一个叫做EOF的特殊字符,读取到这个字符,操作系统就认为文件结束了。
但是,后来我发现,EOF不是特殊字符,而是一个定义在头文件stdio.h的常量,一般等于-1。
如果EOF是一个特殊字符,那么假定每个文本文件的结尾都有一个EOF(也就是-1),还是可以做到的,因为文本对应的ASCII码都是正值,不可能有负值。但是,二进制文件怎么办呢?怎么处理文件内部包含的-1呢?
这个问题让我想了很久,后来查了资料才知道,在Linux系统之中,EOF根本不是一个字符,而是当系统读取到文件结尾,所返回的一个信号值(也就是-1)。至于系统怎么知道文件的结尾,资料上说是通过比较文件的长度。(系统的文件分配表里记录的有文件长度)
这样写有一个问题。fgetc()不仅是遇到文件结尾时返回EOF,而且当发生错误时,也会返回EOF。因此,C语言又提供了feof()函数,用来保证确实是到了文件结尾。上面的代码feof()版本的写法就是:
int c; while (!feof(fp)) { c = fgetc(fp); do something; }
但是,这样写也有问题。fgetc()读取文件的最后一个字符以后,C语言的feof()函数依然返回0,表明没有到达文件结尾;只有当fgetc()向后再读取一个字符(即越过最后一个字符),feof()才会返回一个非零值,表示到达文件结尾。
所以,按照上面这样写法,如果一个文件含有n个字符,那么while循环的内部操作会运行n+1次。所以,最保险的写法是像下面这样:
int c ; while ((c = fgetc(fp)) != EOF) { do something; } if (feof(fp)) { printf("\n End of file reached."); } else { printf("\n Something went wrong."); }
除了表示文件结尾,EOF还可以表示标准输入的结尾。
但是,标准输入与文件不一样,无法事先知道输入的长度,必须手动输入一个字符,表示到达EOF。
Linux中,在新的一行的开头,按下Ctrl-D,就代表EOF(如果在一行的中间按下Ctrl-D,则表示输出”标准输入”的缓存区,所以这时必须按两次Ctrl-D);Windows中,Ctrl-Z表示EOF。(顺便提一句,Linux中按下Ctrl-Z,表示将该进程中断,在后台挂起,用fg命令可以重新切回到前台;按下Ctrl-C表示终止该进程。)
那么,如果真的想输入Ctrl-D怎么办?这时必须先按下Ctrl-V,然后就可以输入Ctrl-D,系统就不会认为这是EOF信号。Ctrl-V表示按”字面含义”解读下一个输入,要是想按”字面含义”输入Ctrl-V,连续输入两次就行了。
位域(位段)
位域也不算是很冷僻的知识点,但关于其内存对齐的问题,还是有些小的细节需要注意。
所谓“位域”是把一个字节中的二进制位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。
位域列表的形式为: 类型说明符 位域名:位域长度;
一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。
例如:
struct A { char a:6 ; char b:5 ; char c:5 ; }; // sizeof(struct A) 为3,而不是2。
0位段
长度为0的位段,其作用是使下一个位段从下一个存储单元开始存放。(下个存储单元的大小是其位段类型的大小)
例如:
struct bs { unsigned a:4; unsigned :0; // 空域 unsigned b:4; // 从下一存储单元开始存放 unsigned c:4; }; // sizeof(struct bs) 为8,若没有0位段则是4
关于内存排布
先举个栗子:
struct foo { char a; int b:1; int :0; int c:7; char d; char e:4; char f:6; char g:6; } ;
它的sizeof大小为多少? 位段的内存排列,在不同的编译器上有不同的实现。我用的是UNIX环境下,Xcode编译的,gcc编译的结果也一样。VC++的结果可能不同。
位段的压缩存储规则是:
1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
4. 如果位域字段之间穿插着非位域字段,则不进行压缩;
5. 整个结构体的总大小为最宽基本类型成员大小的整数倍。
sizeof(struct foo) 为12
下面慢慢解释:char a占一个字节,b占1bit,gcc采用压缩方式,即把a和b压缩在一个int单元里, 其后是0位段,故其前面是一个存储单元,a和b占4字节。
之后又是int c,故先开一个4字节,然后把后面的位段填充进去,但后面是一个完整的char,这个完整的char是不能放到位段中的,故int c占4字节,char d占1字节。
其后的e,f,g都是位段,注意,位段是不能跨越边界的(以其数据类型的大小为边界),故e,f,g各占1字节,而不是合占2字节。 故sizeof(struct foo) 为12。
简单图解:
|---a(8)---|-b(1)-|---------------------(23)----------------------|
|---c(7)---|-------------------------(25)--------------------------|
|---d(8)---|---e(4)---|--(4)--|---f(6)---|-(2)-|---g(6)---|-(2)-|
注意:
位段最好不要跨字节,我在C教材中看到过警示:不要使位段超过8位,但定义多位的位段(我曾定义过64位的位段,用于提取double型变量的各位)仍然可行,可以编译以及运行,但是是跨字节之后,其排列顺利就会受到大小端的影响,故最好不要让位段超过8位。