# C 语言学习心得 ## C 语言的函数 虽然 ANSI 的确规定了一些库函数,但我还是倾向于认为:C 语言本身一个函数也没有,所有的函数都是用户定义或系统提供的。所以用 C 语言编程严重依赖于所用的平台。 ## i++ 和 ++i 两者都是使 `i` 的值增加 1, 不同点是前者的返回值是 `i` 增加以前的值,后者的返回值是 `i` 增加以后的值(不一定是 `i+1` 哦,取决于 `i` 被增加了几次)。然而,当它们用在复杂的表达式中时行为往往是未定义的,取决于编译器的实现。因此我们倾向于单独使用它们,这样就没区别了。当然,在 C++ 中,它们还是不同的操作符,可以重载成不同的功能。 ## 条件运算符 条件运算符是 C 语言中唯一的三目运算符,形式是: ```c b ? x : y ``` C 语言中它的含义是:`b != 0` 时返回 `x` 的值,`b == 0` 时返回 `y` 的值。 C++ 中还有另外一个用法,例如 `(b ? x : y) = 1`, `b != 0` 时执行 `x = 1`, `b == 0` 时执行 `y = 1`. 也就是说它可以当左值使用。不过这一用法很少见,多数人会把它写成如下的语句: ```c if (b) x = 1; else y = 1; ``` 另外一个问题就是它与赋值类表达式的优先级谁高谁低的问题。C 语言中它比赋值类表达式高。C++ 嘛,自己试试,注意“尽信书则不如无书”的老话。建议多加括号来解决问题。 ## 头文件内容 头文件中应包含什么内容?这是一个见仁见智的问题。不过想想头文件是用来干什么用的。它是给别的模块来用的,而不是给自己用的(当然自己顺便用用我也不反对)。所以任何别的模块不需要的信息都不应该放在头文件里,不然引用你的头文件的那个模块就遭殃了,也许人家的一个好听的全局标识符就被你征用了。 ## 函数指针 有人说,通过函数指针调用函数时要这样写 `(*p)()`, 说这样可以清晰的看出 `p` 是一个函数指针。我睁大眼睛瞧呀瞧,`p` 为什么就不能是一个指向函数指针的指针呢?不要告诉我它很少出现,那只能说明你编的程序太少。我想 C 语言允许这么写一定是为了迁就那些不懂指针的人,反正对函数指针取其内容也没意义,就返回它本身吧,你可以试试 `*p`, `**p`, `***p` 是什么。此时 `*p == p`, 徒增混乱耳。类似的还有 `&p == p`, 如果 `p` 是函数名的话,不过就不要尝试 `&(&p)` 了。 当你看到 `p()` 的时候你的第一反应是什么?`p` 是一个函数?错了,`p` 只是一个指针,`p()` 才是函数。函数名可以理解为一个函数指针常数。 ## char s[] 和 char *s 前者 `s` 是个常数而后者 `s` 是个变量。顺便提一下,字符串等同于一个指向常数字符的指针常数,初始化数组时例外。所以 `char s[] = "abc"` 是把 `s[]` 初始化为 `{'a', 'b', 'c', 0}`; `char *s = "abc"` 是把 `s` 的值初始化为指向字符串 `"abc"` 的指针。前者初始化了四个数组元素;后者只初始化了一个指针变量。前者 `s` 本身不能被修改(编译时错误),`s` 指向的内容可以修改;后者 `s` 本身可以修改,`s` 指向的内容不能修改(运行时错误,当然也可能有的系统根本不管非法的内存访问),除非你给 `s` 重新赋值使之指向可写的内存。 ## sizeof `sizeof(x)` 不是函数而是编译时确定的一个常数。如果 `x` 是类型名,它等于 `x` 型数据占用的字节数。如果 `x` 是变量名,它等于 `x` 占据的字节数。如果 `x` 是数组,它等于此数组占用的字节数。如果 `x` 是一个常数,?……有兴趣自己试试,不过这也太无聊了吧。 ## static C 语言中 `static` 有两个用法,一种是用在函数内声明变量时,表示该变量为静态变量,在程序运行期间一直存在,相当于一个全局变量(当然其他函数就访问不了),因此必须用常数来初始化;一种是用在声明全局变量和函数时,表示该变量或函数只能在本文件内被引用。所谓静态,也就是它在进程的地址空间中的位置是固定的。 C++ 中又多了一些用法,可以用在声明类成员变量时,表示该变量全局只有一个拷贝,所有该类的对象访问的全是它。用在声明类成员函数时,表示该函数只引用类中的静态成员变量。静态成员都可以直接通过类名访问。这个所谓静态看来跟 C 语言中的含义也是一样的。 ## 数组的地址 `int a[10];` `a` 就是数组的首地址,可视为 `int *` 型的常数。那 `&a` 是什么?`&a` 数值上跟 `a` 相等,不过它的类型是 `int (*)[10]`. `*&a` 就又等于 `a` 了。什么,你还想 `&(&a)`? 那可是非法的。 这一点跟函数指针有点类似,不同的是函数指针前面加个 `*` 号得到的结果还是同类型的函数指针,你可以一直指下去,子子孙孙无穷匮也。 ## 移位操作 右移运算符对于无符号数和有符号数分别对应着汇编指令的逻辑右移和算术右移,这一点想必是很清楚的。但更令人迷惑的在后头,请问 `(unsigned char)0xFF>>32` 的值是多少? 看上去应该是 `0`, 实际却很有可能是 `0xFF`. 这里有两点要明确: 1. 在 x86 CPU 上,移位指令的第二个操作数只有低 5 位是有效的,在 Intel 64 CPU 上则只有低 6 位是有效的 2. 编译器在处理移位操作时,即便操作数的长度在 32 位以下,汇编成机器指令后都是对 32 位寄存器操作的(在 32 位 x86 CPU 上) 对 16 位寄存器逻辑右移 16 位则得到 0, 但通常编译器不会生成这样的指令。 对 64 位超长整型逻辑移位 64 位会得到 0, 如果你用的是 Visual C++ 的话,因为 Visual C++ 是做了一个子程序来处理,当移位数达到 64 位时直接返回 0. ## 按位取反操作 按位取反操作也是 C 语言中最基本的运算符,请看下面的例子: ```c unsigned short a = 0x0000; unsigned short b = 0xFFFF; a == ~b; ``` 你大概会觉得最后一个表达式为真,实际却很有可能为假。这是由于编译器在取反之前把数据类型转成 `int` 型的缘故。