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位。

时间: 2024-10-04 22:08:18

C语言的角落(二)的相关文章

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++

JavaScript--基于对象的脚本语言学习笔记(二)

第二部分:DOM编程 1.文档象模型(DOM)提供了访问结构化文档的一种方式,很多语言自己的DOM解析器. DOM解析器就是完成结构化文档和DOM树之间的转换关系. DOM解析器解析结构化文档:将磁盘上的结构化文档转换成内存中的DOM树 从DOM树输出结构化文档:将内存中的DOM树转换成磁盘上的结构化文档 2.DOM模型扩展了HTML元素,为几乎所有的HTML元素都新增了innerHTML属性,该属性代表该元素的"内容",即返回的某个元素的开始标签.结束标签之间的字符串内容(不包含其它

OC语言的特性(二)-Block

本篇文章的主要内容 了解何谓block. 了解block的使用方法. Block 是iOS在4.0版本之后新增的程序语法. 在iOS SDK 4.0之后,Block几乎出现在所有新版的API之中,换句话说,如果不了解Block这个概念就无法使用SDK 4.0版本以后的新功能,因此虽然Block本身的语法有点难度,但为了使用iOS的新功能我们还是得硬着头皮去了解这个新的程序概念. 一.看一看什么是Block 我们使用'^'运算符来声明一个Block变量,而且在声明完一个Block变量后要像声明普通

C语言学习(二)——字符串和格式化输入输出

C语言学习(二)——字符串和格式化输入输出 1.char数组类型和空字符 C没有为字符串定义专门的变量类型,而是把它存储在char数组里.数组的最后一个位置显示字符\0.这个字符就是空字符,C用它来标记字符串的结束,其ASCII码的值为(或者等同于)0.C的字符串存储时通常以这个空字符结束,该字符的存在意味着数组的单元数必须至少比要存储的字符数多1.计算机可以自己处理大多数这些细节问题(例如,scanf( )会添加'\0'使得数组内容成为C字符串). 2.strlen( )函数与sizeof运算

机器学习算法的R语言实现(二):决策树

1.介绍 ?决策树(decision tree)是一种有监督的机器学习算法,是一个分类算法.在给定训练集的条件下,生成一个自顶而下的决策树,树的根为起点,树的叶子为样本的分类,从根到叶子的路径就是一个样本进行分类的过程. ?下图为一个决策树的例子,见http://zh.wikipedia.org/wiki/%E5%86%B3%E7%AD%96%E6%A0%91 ? 可见,决策树上的判断节点是对某一个属性进行判断,生成的路径数量为该属性可能的取值,最终到叶子节点时,就完成一个分类(或预测).决策树

Go语言学习笔记(二) [变量、类型、关键字]

日期:2014年7月19日 1.Go 在语法上有着类 C 的感觉.如果你希望将两个(或更多)语句放在一行书写,它们 必须用分号分隔.一般情况下,你不需要分号. 2.Go 同其他语言不同的地方在于变量的类型在变量名的后面.例如:不是,int a,而是 a int.当定义了一个变量,它默认赋值为其类型的 null 值.这意味着,在 var a int后,a 的 值为 0.而 var s string,意味着 s 被赋值为零长度字符串,也就是 "". 3.Go语言的变量声明和赋值 在Go中使

嵌入式 Linux C语言(十二)——单链表

嵌入式 Linux C语言(十二)--单链表 一.单链表简介 1.单链表的结构 单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素. 链表中的数据是以节点来表示的,每个节点由两部分构成:一个是数据域,存储数据值,另一个是指针域,存储指向下一个节点的指针. 2.单链表的节点 单链表节点的数据结构如下: typedef struct data { unsigned int id;//学生身份ID char name[LENGTH];//学生姓名 char subject[

0709 C语言常见误区----------二维数组做参数

总结: 1.二维数组名是指向一位数组的指针,本例中,其类型为 int (*)[4],在传递的过程中丢失了第一维的信息,因此需要将第一维的信息传递给调用函数. 关于二维数组名代表的类型,可通过下面的例子看出. 1 /************************************************************************* 2 > File Name: test_2arr.c 3 > Author:Monica 4 > Mail:[email prot

机器学习算法的R语言实现(二):决策树算法

1.介绍 ?决策树(decision tree)是一种有监督的机器学习算法,是一个分类算法.在给定训练集的条件下,生成一个自顶而下的决策树,树的根为起点,树的叶子为样本的分类,从根到叶子的路径就是一个样本进行分类的过程. ?下图为一个决策树的例子,见http://zh.wikipedia.org/wiki/%E5%86%B3%E7%AD%96%E6%A0%91 ? 可见,决策树上的判断节点是对某一个属性进行判断,生成的路径数量为该属性可能的取值,最终到叶子节点时,就完成一个分类(或预测).决策树