💻 C | 语法与语义的“陷阱”

《C 陷阱与缺陷》,这本书从初学者容易犯错的方面入手,详细解释了 C 语言的一些坑,个人感觉挺有用的,就总结了一部分内容,以防以后自己会犯错。

语法“陷阱”

2.2 运算符的优先级问题

比较常见的一个陷阱

1
2
3
while (b = getchar() != EOF){
...
}

上述写法在运行的时候,得到的 b 只会是 0 或 1,原因在于!= 的优先级要高于 = ,要实现我们想要的效果,应该写成

1
2
3
while ((b = getchar()) != EOF){
...
}

还有关于指针的陷阱

1
2
3
*p()
*(p())
(*p)() //不同于上面两个
1
2
3
*p++
*(p++)//取指针p所指的对象,然后 p 加一
(*p)++ //取指针p所指的对象,然后 对象 加一

2.3 注意作为语句结束标志的分号

1
2
if (a > b);
a++;

多了一个分号, 这样的代码是没有任何警告信息的,但是往往和我们预期的结果不同。

1
2
3
4
5
if (a = b);
return
a = a + b;
b = a - b;
a = a - b;

这段代码的本意是交换 a、b 的值(这是个不用额外变量调换值的方法)
但是如果缺少了分号,返回的值就是 a 了,然而编译器还是不会有任何报错
(当函数类型没有声明的时候容易出 bug 而不容易发现)
(return 后不带任何值默认为 0)

2.4 switch 语句

注意 break;
这是 c 的一大弱点,但也是优势所在。

2.5 函数调用

1
2
f();
f;

前者是调用 f 函数,后者是返回 f 函数的地址,并没有调用函数。所以我们调用函数的时候无论是否带有参数,都要带个括号。

2.6 “悬挂” else 引发的问题

对于多重嵌套的 if 语句,最好不要省略大括号

语义“陷阱”

3.1 指针与数组

  • 任何一个数组下标的运算都等同于一个对应的指针运算。

声明数组

  • 一般整型数组:
1
int a[3];
  • 结构体数组:
1
2
3
4
struct {
int p[4];
double x;
}b[17];
  • 二维数组:
1
int c[4][5];

1
2
3
4
int *p;
int a[10];
p = a; //正确, 把数组a中下标为0的元素的地址赋值给p
p = &a;//错误, &a是一个指向数组的指针, p是一个指向整型变量的指针
  • a 除了被用作 sizeof 运算符的参数的时候,都是被视作指向数组 a 下标为 0 的元素的地址

我们回顾一下最开始的定义,二维数组。

实际上二维数组的含义是等价于结构体的,也就是说,上面二维数组的定义可以写成:

1
2
3
struct {
int b[5];
}c[4];

其含义可以当成 c 这个数组拥有 4 个数组类型的元素,其中每一个元素都是一个拥有 5 个整型元素的数组,他们在内存里的数据是完全一样的,只是使用方式有点不同。

这样说来,对于二维数组 c[4][5] , 应该不难理解 c[2]就是一个数组名,其含义就是里面第三个数组的首地址。

而对于一个数组,我们可以通过下标来读取其中的元素,

自然而言就会写成c[2][3] 了。


下面我们再看

1
2
3
int c[3][4];
int *p;
p = c;//错误

这个语句是非法的,因为 c 是一个二维数组,就是说数组的数组,c 其实等价于几个指向数组的指针, 而 p 是指向整型变量的指针。

那么问题就来了

我们应该怎么样去声明一个指向数组的指针呢 ,其实也并不难

1
int (*a)[4];

这个语句的实际效果就是声明 *a 是一个拥有 4 个整型元素的数组, 那么 a 就是一个指向数组的指针了。

那么怎么样用指针来操作二维数组

1
2
3
4
5
6
7
int a[3][4];
int (*p)[4];
p = &a[2];
*((*p)+1) = 0;//等价
(*p)[1] = 0;//等价
a[2][1] = 0;//等价
*(*(a+2)+1) = 0; //等价

怎么去理解这四种写法呢

  1. 第一种是先声明一个指向数组的指针,然后指向 a[2],然后用指针运算取值来算
  2. 第二种其实和第一种差不多,就是把指针简单地换成了数组的写法(这是一种简写,前面几章应该有提到)
  3. 第三种很明显是直接操作数组下标来操作
  4. 第四种其实和第三种差不多, a 是一个指向数组的指针,然后+2 运算让他指向 a[2](注意这是一个数组名,前面有说到),然后再用指针操作指向目标

3.2 非数组的指针

这一章主要是学会用,malloc 和 free 给指针开辟内存空间

有几点值得注意一下

  • malloc 的原型是 void* , 使用的时候一般要经过强制类型转换才能赋给相应类型的指针。
  • 即使经过了类型转换,但是 malloc 生成的内存空间大小是以字节计算的,相应的,大小参数要乘以相应的sizeof(类型)
  • 当 malloc 为用于字符串的时候,要注意为 \0 预留一个字节的空间,否则会发生难以预料的错误

3.3 作为参数的数组声明

当数组作为参数传递给函数的时候,事实上传递的只是数组的首地址, 并不会把数组复制一份传递给函数,那么我们在使用的时候要注意函数可以改变数组的内容,我们也可以利用这一特点,来使得函数返回多个值。


题外话:刚刚了解了一下 main 的两个参数,然后写了一段代码

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(int argc, char const *argv[])
{
if (argc > 1 && argv[1][0] == 97) {
printf("Good!\n");
} else{
printf("SB!\n");
}
return 0;
}

你猜猜是怎么用的?

评论猜中了奖励傻币。


3.4 避免“举隅法”

“举隅法” 是一种文学修辞上的手段,意思是用含义更宽的词语代替含义相对较窄的词语,或者相反。

1
2
char *q, *p  = "xyz";
q = p;

如果你是刚刚接触 C 语言或者从没接触过得话,这样一看,还以为是把 p 中的字符串复制给了 q,实际上这只存在一个字符串,两个指针都是指向这个字符串的首地址。

这一章对于学过 C 指针的人来说的确有点鸡肋。


3.5 空指针和非空字符串

当我们定义一个指针的时候,默认赋值为 NULL,或者是 0.

或者说,如果当 0 被转为指针使用的时候,这个指针就绝对不能被解除引用

1
2
3
char *p = (char *)0;
if (p == (char *)0) ... //合法
if (strcmp(p, (char *) 0) == 0)... //非法

原因是 strcmp 函数内部会查看指针所对应的内容,就是解引用操作。

这时就会引起非法访问,因为内存中 0 的位置是系统核心位置,程序是没有权限访问的。


3.6 边界计算与不对称边界

定义一个有 10 个元素的数组,那么数组的上标和下标是什么呢?

这对于不同语言有着不同的答案

对于 Fortran, PL/I, Snobol4 等语言,数组下标缺省是从 1 开始,而且允许编程者指定其他下标。

对于 Algol, Pascal 等语言, 数组没有缺省的下标,要编程者显式地指定上下界。

对于标准的 Basic 语言,缺省下标为 0,声明的时候是声明数组的上标。

对于 C 语言,声明有 10 个元素,那么下标就是 0-9.

其实,C 语言这种表示方式遵循不对称边界原则,在某些方面有着很大的好处。

下界是“入界点”,包括在取值范围内,上界是“出界点”,不包括在取值范围内,也就是左闭右开区间。这种数学上看上去并不优美,但是却可以简化我们程序设计。

  1. 取值范围的大小就是上界与下界之差
  2. 如果数组为空,上界等于下界
  3. 即使数组为空,上界也不可能小于下界

平时我们在使用 for 循环的时候大概也感受到了一点,如果说起来篇幅可能有点长,那就不展开了。

为了遵循这种原则,我们使用指针处理缓冲区的时候尽可能让指针指向第一个未被占用的字符

后记

转眼间又到了 12 点,今天就写到这里了,剩下的明天再搞吧,有空还要写一写 C 大学教程里面的输入输出,感觉比较有用。

土豪通道
0%