首页 > 代码库 > 《C和指针》整理一

《C和指针》整理一

1.C语言的注释
    在C语言中,如果需要注释掉一段代码,且代码中可能会已经存在/**/注释形式,那么可以使用:
#if 0
    statements
#endif
    这种形式来注释掉这段代码(statements代表这段代码)。这样做的原因是C语言不允许嵌套注释,也就是说第一个/*和第一个*/符号之间的内容都被看作注释,不管里面还有多少个/*符号。

2.换行符的处理
#include <stdio.h>
int main()
{
    int ch, i;
    ch = getchar();
    i = getchar();
    printf("%d %d\n", ch, i);
    return 0;
}
输入:A+回车键
输出是多少?
分析:当我们使用getchar()函数接收一个字符时,在键盘输入至少需要键入一个字符(除非只按一个回车键)和一个回车键,这样实际上用户是敲击了两次键盘,也就是实际上是键入了两个字符,也就是一个真正的字符和一个换行符(ASCII码为10对应的字符为‘\n‘)。所以上面这种情况下的输出会输出A的ASCII码和换行符的ASCII码。正是由于这种情况的存在,如果我们需要接收用户从键盘输入的字符时,需要程序中自动清理掉其后面跟的换行符,防止这个换行符污染到下一次的输入中。同样的下面的一个例子:
#include <stdio.h>
int main()
{
    int ch, ca, cb;
    scanf("%d", &ch);
    scanf("%d", &ca);
    cb = getchar();
    printf("%d %d %d\n", ch, ca, cb);
    return 0;
}
输入为:12+回车+34+回车的时候输出为多少?
分析:输出为12 34 10。原因和上面差不多,但是scanf比较智能,在接收输入的时候会自动去匹配stdin中的下一个输入时候匹配类型,匹配则接收,不匹配则丢弃,所以34还是能够正确的接收,然而最后的一个换行符(键盘上敲击回车键产生的)还是会留在stdin中,所以最后会被getchar()函数接收到cb中。

3.读取字符时的注意

    一个经常闻到的问题是:为什么ch被声明为整数,而我们实际上需要用它来读取字符(这个在从文件中读取字符时尤其需要注意)?答案是EOF是一个整型值,它的位数比字符类型要多,把ch声明为整型可以防止从输入读取的字符意外地被解释为EOF。但同时,这也意味着接收字符的ch必须足够大,足以容纳EOF,这就是ch使用整型值的原因。字符只是小整数而已,所以用一个整型变量容纳字符值并不会引起任何问题。

4.三字母词和转义字符
    C的标准中定义了几个三字母词,三字母词也就是几个字符的序列,合起来表示另一个字符。三字母词使C环境可以在某些缺少一些必需字符的字符集上实现。这里列出了一些三字母词以及它们所代表的字符:
??(  [        ??<  {       ??=  #
??)  ]        ??>  }       ??/  \
??!  |        ??‘  ^       ??-  ~
比如printf("??)");将输出一个]符号。
转义字符:
\?  在书写连续多个问号时使用,防止它们被解释为三字母词。
\"  用于表示一个字符串常量内部的双引号。
\‘  用于表示字符常量‘。
\\  用于表示一个反斜杠,防止它被解释为一个转义字符。

5.关于数据类型大小的ANSI规定
整型之间的大小规则:长整型至少应该和整型一样长,而整型至少应该和短整型一样长。
浮点型之间的大小规则:long double至少和double一样长,而double至少和float一样长。

6.变量作用域
编译器可以确认4种不同的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。
>>> 代码块作用域
    位于一对花括号之间的所有语句被称为一个代码块。下图中的5、6、7、9、10的变量都具有代码块作用域,函数的形式参数5在函数体内部也具有代码块作用域。另外声明9的f和声明6的f是不同的变量,在声明9所在的花括号内6声明的f被隐藏。
>>> 文件作用域
    任何在所有代码块之外声明的标识符都具有文件作用域,它表示这些标识符从它们的声明之处直到它所在的源文件结尾处都是可以访问的。下图中1、2具有文件作用域,同时文件中定义的函数名也具有文件作用域,因为函数名本身不属于任何代码块,所以4也具有文件作用域。
>>> 原型作用域
    原型作用域只适用于在函数原型声明中的参数名,下图中3、8具有这样的作用域。在原型中,参数的名字并非必需。
>>> 函数作用域
    它只适用于语句标签,语句标签用于goto语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。


7.链接属性
    链接属性一共有3种——external(外部)、internal(内部)和none(无)。没有链接属性的标识符(none)总是被当做单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。属于internal链接属性的标识符在同一个原文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体。最后,属于external链接属性的标识符不论声明多少次、位于几个源文件都表示同一个实体。
    函数名、外部变量默认的链接属性为external,而局部变量的默认链接属性为none。同时关键字extern和static用于在声明中修改标识符的链接属性,如果某个声明在正常情况下具有external链接属性,在它的前面加上static关键字可以使它的链接属性变为internal。static只对缺省链接属性为external的声明才有改变链接属性的效果。而extern关键字的规则更为复杂,一般而言,它为一个标识符指定external链接属性,这样可以访问在其他任何位置定义的这个实体。如下的例子:
static int i;
int func()
{
    int j;
    extern int k;
    extern int i;
    ......
}
分析:extern int k;这个声明表示在该函数体内的后续部分可以访问其他源文件中定义的k变量实体。而当extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有external链接属性;但是如果它用于该标识符的第2次或以后的声明时,它并不会更改由第1次声明所指定的链接属性。所以extern int i;声明不会修改i的static属性。

8.数组名
    在C中,几乎所有使用数组的表达式中,数组名的值都是一个指针常量,也就是数组第一个元素的地址。只有在两种场合下数组名并不是指针常量:作为sizeof操作符的运算对象;当做单目运算符&的操作数。

9.指针与下标
    下标绝不会比指针更有效率,但指针有时会比下标更有效率。
例子1:
int array[10], a;
for(a = 0; a < 10; a++)
    array[a] = 0;
    为了对下标表达式求值(array[a]),编译器在程序中插入指令,取得a的值,并把它与整型的长度相乘,这个乘法需要花费一定的时间和空间。再看指针形式的写法:
int array[10], *ap;
for(ap = array; ap < array+10; ap++)
    *ap = 0;
    尽管这里不存在下标,但是还是存在乘法运算。1这个值必须与整型的长度相乘,然后再与指针相加。但是这里存在一个重大的区别:循环每次执行时,执行乘法运算的都是两个相同的数(1和4)。结果这个乘法只有在编译时执行一次——程序现在包含了一条指令,把4与指针相加。程序在运行时并不执行乘法运算。但是下面的两段代码:
a = get_value();
array[a] = 0;

a = get_value();
*(array+a) = 0;
    两组语句产生的代码并无区别,a可能是任何值,在运行时才能知道。所以两种方案都需要乘法指令,用于对a进行调整。这个例子说明了指针和下标的效率完全相同的场合。

10.指针的进一步优化
void try1()
{
    for(p1 = x, p2 = y; p1-x < SIZE;)
        *p1++ = *p2++;
}
●这段代码使用p1-x < SIZE来作为终点判断,但是这个判断其实会被处理为除法,因为p1-x的值是指针的运算,这个差值必须要除以一个类型的大小做调整。这一点其实会耗费比较大的代价。
void try2()
{
    for(i = 0, p1 = x, p2 = y; i < SIZE; i++)
        *p1++ = *p2++;
}
●这段代码重新使用了计数器,用于控制循环退出,这样可以消除除法,并且缩短汇编后的代码长度。但是和上面的代码一样,代码在执行时p1和p2一般都会被重新复制到别的寄存器之后才开始运算。
void try3()
{
    register int *p1, *p2;
    register int i;
    for(i= 0, p1 = x, p2 = y; i < SIZE; i++)
        *p1++ = *p2++;
}
●这段代码的优化在于将p1和p2声明为寄存器变量,这样的话在汇编之后可以减少复制p1和p2的值到别的寄存器的步骤。
void try4()
{
    register int *p1, *p2;
    for(p1 = x, p2 = y; p1 < &x[SIZE]; )
        *p1++ = *p2++;
}
●&x[SIZE]会在编译时求值(因为SIZE是个数字常量),这段代码进一步消除掉了计数器i,汇编之后的代码相当紧凑,几乎达到和汇编代码一样的效率。这同时也是C语言不对称边界以及不检查数组越界给编程带来的方便体现。

《C和指针》整理一