C语言的角落(二)——你不一定知道的C语言特性

之前写过一篇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位。







猜你喜欢

转载自blog.csdn.net/yang_yulei/article/details/46337583
今日推荐