C陷阱与缺陷

这本书很薄,信息量很大。
快速过一遍,然后放在案头时常翻翻就行了。
其中介绍的各种小问题,随着编译器的发展,大部分应该给出提示信息。
C语言很强大,C语言很灵活,你需要知道自己在干什么。

词法陷阱

= 与 ==

  • 你肯定犯过的错,== 误写成 =
  • = 误写成 == 也有可能
  • 确实需要将赋值表达式放在条件判断的位置时,使用显式的条件判断
    if (x = y)          /* 并不好,不要这么写 */
        foo();

    if ((x = y) != 0)   /* 可以这么写 */
        foo();

& | 与 && ||

  • 位运算符,逻辑运算符,也是常见的错误

词法分析中的“贪心法”

  • /, *, = 单字符符号
  • /*, == 多字符符号
  • 编译器怎么决定是哪个符号呢?依据“贪心法”
    • “每一个符号应该包含尽可能多的字符”
    • 空白符(空格,制表,换行)会打断贪心过程
a---b       /* (a--) - b*/
a -- - b    /* (a--) - b*/
a - -- b    /* a - (--b)*/
a ---b      /* (a--) -b)*/

/* int a = 10, b = 5 */
a---b = 5           a = 9, b = 5
a -- - b = 4        a = 8, b = 5
a - -- b = 4        a = 8, b = 4
a ---b = 4          a = 7, b = 4

/* "/*" 的尴尬 */
y = x/*p;           /* x后边的部分被理解位注释的开始 */
y = x / *p;         /* 正确 */
y = x/(*p);         /* 正确*/

整形常量

  • 0开头的而数字是八进制数,而不是十进制数了
/* 为了对齐和美观,造成的数字错误 */
struct
{
    int part_number;
    char *description;
}parttab[] = {
    046, "Left-handed widget",
    047, "right_handed widget",
    125, "frammis"
};

字符与字符串

  • 单引号 是一个数, ‘a’
  • 双引号 是一个char *, “a”

语法陷阱

函数声明

  • 先感受一下
(*(void(*)())0)()
  • C语言变量的声明,包含2个部分
    • 类型
    • 类似于表达式的声明符
float f, g;             /* f, g 是 float类型*/
float ((f));            /* ((f))是float类型,所以f 就是float 类型的*/
float ff();             /* ff() 是float 类型,那么ff()是返回flaot类型的函数 */
float *pf;              /* *pf 是float类型,那么pf是指向float类型的指针 */
float *g(), (*h)();     /* 对*g() 求值时,类型是float,()结合性更高,所以g()是返回float指针类型的函数*/
                        /* 对(*h)()求值时,类型是float,所以h是指向 float返回类型的函数的指针 */
  • 某个类型的强制类型转换符
    • 上面我们已经知道了怎么声明一个变量
    • 声明这个类型的变量,与制作这个类型的强制类型转换符 是有套路的
      • 去掉变量名
      • 去掉末尾的分号
      • 剩余部分用一个括号封装起来
/* 以float (*h)()为例,演示怎么得到这个类型的强制类型转换符 */
float (*h)();

1. flaot (*)();
2. flaot (*)()
3. (flaot (*)())

/* 用它来强制类型转换一个变量 */
(flaot (*)())x
  • 我们想要调用一个地址位0的函数,该函数的类型是void function()
    • 上面的方法
    • typedef方法(推荐,更清晰简洁)
/* 函数指针的使用 */
(*fp)();

/* 对0做强制类型转换,转换成函数指针 */
void (*p)();

1. void (*)();
2. void (*)()
3. (void (*)())

(*((void (*)())0))();   /* 结合 *fp() 和上面的3 */

/* 使用typedef对0做强制类型转换 */
typedef void (*funcptr)();
(*(funcptr)0)();

运算符优先级

  • 这节作者的意思是需要记住一些优先级规则,语句中有太多的括号其实增加了阅读的难度
  • 我觉得记忆并不可靠,不行就打出来贴墙上吧
  • 优先级的大方向,高到低(不完整,挑关键的)
    • 算数运算符,+-*/%
    • 移位运算符 >> <<
    • 关系运算符 <,>,==,!=
    • 位运算符 &, |, ^
    • 逻辑运算符 && ||
    • 赋值运算符

  • 一般容易出问题的是这么几个地方
    • 条件分支语句, if else, while
    • 声明语句
/* 两个忘记写分号,但是编译没有问题的例子 */
if (n < 3)
    return
logrec.date = x[0];     /* 这个x[0] 会作为返回值返回,问题还不算太严重;但是如果n >= 3,那么logrec.data永远都不会有赋值操作,这个错误就太隐晦了*/
logrec.time = x[1];
logrec.code = x[2];

struct logrec{
    int date;
    int time;
    int code;
}
main()              /* 这里直接变成了main的返回值类型是struct logrec,合乎语法,完全正确*/
{
    ...
}

switch

  • switch 中需要 break才能跳出,是个很强大,很方便,同时也很危险的设计

函数调用

  • f()调用一个函数
  • f 什么也不做的语句,计算f的地址,然后就没有然后了

悬挂的else

  • 老老实实加括号吧

语义陷阱

指针与数组

  • 几个基本原则
    • C语言中只有一维数组,但是数组元素可以是任何类型的对象,如果数组元素还是数组,那么就构成了多维数组
    • 对于一个数组,我们能做的只有2件事
      • 确定该数组的大小
      • 获取该数组下标是0的元素的指针
      • 其他操作都是指针操作
  • 有基本原则得到的推论
int a[3];
int *p = a;
- 数组名在sizeof 的时候返回数组的大小,其他时候都是指向数组0元素的指针
- *(a + i)是数组a中下标位i的元素的引用,也记为 **a[i]** ,所以其实 **i[a]** 也是同样的,但是绝对不推荐这么写
- 二维数组的操作
int calendar[12][31];
int *p;
int i;

p = calendar[4];            /* 这样p指向了calendar[4]中下标位0的元素 */

i = calendar[4][7];         /* 这么写很清晰,下边就开始晦暗不清了 */
i = *(calendar[4] + 7);     /* calendar[4]是一个数组的首地址,这个数组是31个int类型的 */
i = *(*(calendar + 4) + 7); /* calendar 是一个数组的首地址,这个数组是12个 int [31] 类型的 */

p = calendar;               /* **这是错误的,因为calenar表示的地址不是int类型,是int [31]类型** */

int (*ap)[31];
ap = calendar;              /* 这是正确的 */

字符串

  • 考虑这么一个问题,给定该字符串s和t,现在需要把他们组合起来形成一个新的字符串r
    • 几个容易出错的点
      • 分配内存的时候,strlen(s) + strlen(t)是不够的,需要多一个给结束符’\0’
      • 分配内存是否成功是需要判断的,不能分配的时候一定不要继续
      • 分配的内存需要回收
char *r;
r = (char *)malloc(strlen(s) + strlen(t) + 1);
if (!r)
{
    exit(1);
}
strcpy(r, s);
strcat(r, t);
...
free(r);

数组作为参数

  • 数组作为参数,与指向它0元素的指针是等价的,可以参考数组作为参数与指针的等价的
  • 但是在参数以外的部分,不能够做这样的假设,以下代码的2个声明,有天渊之别
extern char *hello;
extern char hello[];

空指针

  • 0 (通常被宏定义为 NULL),可以赋值给指针变量,可以判断指针变量的值是否是0
  • 但是0并不能被解引用(dereference),也就是说不能企图使用该指针指向的内容
  • 我的理解
void *p;
p = NULL;                   /* OK */
if (p != NULL)              /* OK */
    ...
if (strcmp(p, s) == 0)      /* NOK */
    ...

边界

求值顺序

  • C语言中只有4个运算符存在规定的求值顺序,其他的未规定
    • &&, || 短路求值
    • ? : ,先求?左边的,根据结果求右边的
    • , 从左到右的顺序,求完就扔(这里不是说的函数参数中的逗号)
/* 短路求值 */
if ( y != 0 && x / y > tolerance)
    ...

/* 这样很可能就错了 */
i = 0;
while (i < n)
    y[i] = x[i++];

/* 这样才正确 */
i = 0;
while (i < n)
    y[i] = x[i];
    i++;

/* 这也是正确的*/
for (i = 0; i < n; i++)
    y[i] = x[i];

main的返回值

  • 返回0表示成功执行,对调用者来说很重要
  • 在main中return 0,或者exit(0)

链接

声明与定义

  • 外部对象的声明和定义
    • 所有函数之外的变量,全局变量
    • 定义一次,声明可以多次
/* 一个文件中 */
int a;

/* 可以多个文件中 */
extern int a;

命名冲突

  • static 变量或者函数
  • 本文件有效,别的文件不可见

头文件

  • 头文件的正确使用方式
[file.h]
extern char filename[];

[用到filename的其他C文件]
#include "file.h"

[定义filename的C文件]
#include "file.h"
char filename[] = "/etc/passwd";

库函数

getchar

  • 注意返回类型是int而不是char,特别是EOF本身是int型的-1
  • 接收getchar的时候注意类型

更新顺序文件

  • 打开一个文件以后,同时进行读写的时候,会遇到比较隐晦的问题
  • 这里使用的库函数 fread, fwrite, fseek这几个
  • 这里展示的问题是,以读写方式打开文件“r+”的时候,在读和写,也就是fread和fwrite之前,都需要做一次fseek,重新设置当前位置,即使看起来当前位置没有问题
FILE *fp;
struct record rec;
..
while(fread((char *)&rec, sizeof(rec), 1, fp) == 1)
{
    /* 对 rec 执行某些操作 */
    if (/* rec必须要被重新写入 */)
    {
        fseek(fp, -(long)sizeof(rec), 1);
        fwrite((char *)&rec, sizeof(rec), 1, fp);
        fseek(fp, 0L, 1);
    }
}

缓冲输出与内存分配

  • setbuf
  • 可以控制并不会实时复制到目标中去,通过buf填满或者手动fflush最终复制过去
  • 这个没见过
#include <stdio.h>
int main()
{
    int c;
    char *buf;
    buf = (char *)malloc(BUFSIZ);
    setbuf(stdout, buf);
    while((c = getchar()) != EOF)
        putchar(c);
    return 0;
}

预处理器

宏定义中的空格

  • 宏定义如果带有参数,那么一定不能与替换前的名称之间有空格
  • 但是奇怪的是使用这个名称的时候,和参数之间是可以有空格的
/* 这就应该是写错了的一个宏定义 */
#define f (x) ((x) - 1)

/* 这回符合预期,使用的时候却可以有空格,那么函数调用都是可以有空白字符的吧? */
#define f(x) ((x) - 1)
...
a = f (3);

宏定义与函数

  • 宏定义的时候给参数加括号已经知道了,宏定义最后完整的再加一个括号可能就不知道了
#define abs(x) ((x) >= 0 ? (x) : (-(x)))
abs(a) * 3                  /* 如果没有最外边的括号,这句会怎么样? */
  • 宏定义当函数用可能有隐晦的问题,就比如还是上面这个求绝对值的
    • 这就是个简单的例子,能看到如果用宏定义,变量i可能经过多次求值,跟程序的本意已经南辕北辙了
    • 解决办法
      • 使用没有副作用的参数,比如没有运算过程的变量
      • 使用函数代替宏
      • 使用语句直接完成,不用函数或者宏
int a[10];
int i = 0;
while(i < 10)
    a[i] = abs(i++);
  • 宏的另以问题是展开以后可能会很长
/* 展开这个试试 */
#define max(a, b) ((a) > (b) ? (a) : (b))
max(a, max(b, max(c, d)))
max(max(a, b), max(c, d))

后面的部分没有特别的笔记

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值