【2018深信服 醒狮计划】《C陷阱与缺陷》学习笔记

2018深信服“醒狮计划”笔记

先自我介绍一下,湖大研一计算机的菜鸡,本科网络工程的,大学里不务正业一直在做应用,大一自学过一段时间的MFC,Windows网络编程,感觉比控制台好看多了,然后大二开始搞Android,做过一些小的demo,大三的时候参与过一个智能家居的创业项目,拿到了投资,种种原因,最后还是失败了,于是乎把代码开源了,在我的github上面,有兴趣的可以拿走。三年下来东搞西搞,做过MFC,JAVA WEB,Android,Python后端,但是啥都只学到了皮毛。于是乎决定读研,希望能够沉淀一下,不过一直很迷茫,到底做那个方向好,正好看到深信服开设了“醒狮计划”,于是就报名参加了,因为读研期间接触到的项目都跟Linux相关,并且之前有过python后端的一点经验,结合自己的兴趣,于是乎选择了Linux,希望自己能够坚持学下去,也希望跟各位大佬多多交流,后附GitHub和blog地址,虽然没什么干货,欢迎互粉。

CSDN GitHub

第1周(4.22-4.29)

课程 必修 选修 基本要求
C语言进阶 《C陷阱与缺陷》 《C和指针》、《C++沉思录》 掌握C语言的指针和内存管理机制,了解常见的编码错误和陷阱

C陷阱与缺陷(全书188页)

影印版不是很清楚,我自己在网上找了一本高清的,内容相同,压缩排版,总共79页。后附链接。


第一章 .词法陷阱(day1)

1.赋值 = ,比较 == 的误用

本意跳过 空格 水平制表符 换行 ,结果c==’\t’ 误写成了 c=’\t’ 有可能造成了死循环

while (c == ’ ’ || c = ’\t’ || c == ’\n’)
    c = getc (f);

error无法得到执行,

if((filedesc == open(argv[i], 0)) < 0)
    error();

2. & | && ||的不同

3. 语法分析中的“贪心”

连续符号是连起来对待还是拆分之后对待

规则:每个符号尽可能多的包含更多的字符

含义相同

a---b
a-- -b

含义不同

a- --b

将x的值除以p赋值给y

错误的识别成了注释

y = x /*p / * p points at the divisor */;

改进的写法,加空格 ,阔号 。更加的清晰

y = x / *p /* p points at the divisor */;

y = x/(*p) /* p points at the divisor */;

4. 老版本C中允许使用=+ 代替 +=

a =- 1;

上面代码将被编译器理解成

a =a-1;

看上/* 是注释的开始,然后在老的编译器中会解释成

a=/*b

中间多一个空格

a=/ *b;

组合赋值运算符如+=实际上是两个记号。下面两个是同样的

a + /* strange */ = 1
a += 1

5.整型常量的坑

如果一个整型常量的第一个字符数字是0,那么就被视为八进制
10与010完全不同

gcc 提示报错:invalid digit "8" in octal constant

8 9也会被当做八进制来处理,0195将会被处理为1×8^2+9×8^1+5×8^0

坑!有时候无意中就写成了下面这种情况

  struct{
    int part_number;
    char *descriptionion;
  )parttab[]={
    046 “left handed widget”
    047 “right handed widget” ,
    125 “frammis”
  };

6.字符和字符串

一般混用编译器会检查到,并且报错

char *slash='/';

warning: initialization makes pointer from integer without a cast


在一个ASCII实现中,’a’和0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是编写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。

下面的两个程序片断是等价的:

printf("Hello world/n");
char hello[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '/n', 0 };
printf(hello);

使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)

某些C语言编译器不会对函数参数类型进行检查,例如printf()

如果用

printf('\n');

代替

printf("\n");

会产生难以预料的错误,书上说4.4有详解,那先挖个坑。

'/n'表示一个整数,它被转换为了一个指针,这个指针所指向的内容是没有意义的


由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。

这意味着用'yes'代替"yes"将不会被发现。后者意味着“分别包含yes和一个空字符的四个连续存储器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符yes联合构成的一个整数”。这两者之间的任何一致性都纯属巧合。


第二章 .语法陷阱(day2)

1.理解函数声明

what?

(*(void(*)())0)();

这表示((f))求值为float并且因此,通过推断,f也是一个float。

float ((f));

表示表达式ff()是一个float,因此ff是一个返回一个float的函数

float ff();

表示*g()(*h)()都是float表达式。由于()*优先级高,*g()*(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。

float *g(), (*h)();

声明h,是一个指向一个返回值是float的函数指针

float(*h)()
(float(*)())

有了这些知识的武装,我们现在可以准备解决(*(void(*)())0)()

首先,假设我们有一个变量fp,它包含了一个函数指针,并且我们希望调用fp所指向的函数。可以这样写:

(*fp)();

如果fp是一个指向函数的指针,则 *fp就是函数本身,因此(*fp)()是调用它的一种方法,(*fp)中的括号是必须的,否则这个表达式将会被分析为*(fp())


第二步,我们现在要找一个适当的表达式来替换fp。如果C可以读入并理解类型,我们可以写:

(*0)();

但这样并不行,因为*运算符要求必须有一个指针作为它的操作。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。因此,我们需要将0转换为一个可以描述指向一个返回void的函数的指针的类型。

如果fp是一个指向返回void的函数的指针,则(*fp)()是一个void值,并且它的声明将会是这样的:

void (*fp)();

因此,我们需要写:

void (*fp)();
(*fp)();

来声明一个哑变量。一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将0转换为一个指向返回void的函数的指针

(void(*)())0

因此,我们用(void(*)())0来替换fp

(*(void(*)())0)();

结尾处的分号用于将这个表达式转换为一个语句

在这里,我们解决这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:

typedef void (*funcptr)();
(*(funcptr)0)();

此问题并不是孤立的,经常存在,书中P18有一个signal的例子


2.运算符的优先级问题

常见的优先级使用错误引起的bug

假设有一个声明了的常量FLAG,它是一个整数,其二进制表示中的某一位被置位(换句话说,它是2的某次幂),并且你希望测试一个整型变量flags该位是否被置位。通常的写法是:

if(flags & FLAG)
{
  ....
}

if语句测试括号中的表达式求值的结果是否为0。出于清晰的目的我们可以将它写得更明确:

if(flags & FLAG != 0)
{
  ...
}

这个语句现在更容易理解了。但它仍然是错的,因为!=&的优先级更高,因此它被分析为:

if(flags & (FLAG != 0))
{
  ...
}

除了FLAG是恰好是1的情况,其他时候都是错误的


假设你有两个整型变量,hilow,它们的值在0和15(含0和15)之间,并且你希望将r设置为8位值,其低位为low,高位为h。一种自然的写法是:

r = hi << 4 + 1ow;

不幸的是,这是错误的。加法比移位绑定得更紧密,因此这个例子等价于:

r = hi << (4 + low);

正确的方法有两种:

r = (h << 4) + l; //加括号
r = h << 4 | l; //逻辑或

一个方法是将所有的东西都用括号括起来,但表达式中的括号过多就会难以理解,因此最好还是是记住C中的优先级。

ps:道理是这么讲,但是真的很难记,这么多:

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右

()

圆括号

(表达式)/函数名(形参表)

.

成员选择(对象)

对象.成员名

->

成员选择(指针)

对象指针->成员名

++

后置自增运算符

++变量名

单目运算符

后置自减运算符

–变量名

单目运算符

2

-

负号运算符

-表达式

右到左

单目运算符

(类型)

强制类型转换

(数据类型)表达式

++

前置自增运算符

变量名++

单目运算符

前置自减运算符

变量名–

单目运算符

*

取值运算符

*指针变量

单目运算符

&

取地址运算符

&变量名

单目运算符

!

逻辑非运算符

!表达式

单目运算符

~

按位取反运算符

~表达式

单目运算符

sizeof

长度运算符

sizeof(表达式)

3

/

表达式/表达式

左到右

双目运算符

*

表达式*表达式

双目运算符

%

余数(取模)

整型表达式/整型表达式

双目运算符

4

+

表达式+表达式

左到右

双目运算符

-

表达式-表达式

双目运算符

5

<<

左移

变量<<表达式

左到右

双目运算符

>>

右移

变量>>表达式

双目运算符

6

>

大于

表达式>表达式

左到右

双目运算符

>=

大于等于

表达式>=表达式

双目运算符

<

小于

表达式<表达式

双目运算符

<=

小于等于

表达式<=表达式

双目运算符

7

==

等于

表达式==表达式

左到右

双目运算符

!=

不等于

表达式!= 表达式

双目运算符

8

&

按位与

表达式&表达式

左到右

双目运算符

9

^

按位异或

表达式^表达式

左到右

双目运算符

10

|

按位或

表达式|表达式

左到右

双目运算符

11

&&

逻辑与

表达式&&表达式

左到右

双目运算符

12

||

逻辑或

表达式||表达式

左到右

双目运算符

13

?:

条件运算符

表达式1? 表达式2: 表达式3

右到左

三目运算符

14

=

赋值运算符

变量=表达式

右到左

/=

除后赋值

变量/=表达式

*=

乘后赋值

变量*=表达式

%=

取模后赋值

变量%=表达式

+=

加后赋值

变量+=表达式

-=

减后赋值

变量-=表达式

<<=

左移后赋值

变量<<=表达式

>>=

右移后赋值

变量>>=表达式

&=

按位与后赋值

变量&=表达式

^=

按位异或后赋值

变量^=表达式

|=

按位或后赋值

变量|=表达式

15

,

逗号运算符

表达式,表达式,…

左到右

从左向右顺序运算

这有15个,太困难了。然而,通过将它们分组可以变得容易


一元运算符

运算符中的最高优先级

一元运算符是右结合的,因此*p++表示*(p++),而不是(*p)++

二元运算符

其中算数运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。

  1. 所有的逻辑运算符的优先级比所有关系运算符都低。
  2. 移位运算符比算数运算符的优先级低,但比关系运算符高。

有一些奇怪的地方

乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级

还有就是六个关系运算符并不具有相同的优先级:==!=的优先级比其他关系运算符要低。这就允许我们判断ab是否具有与cd相同的顺序,例如:

a < b == c < d

在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(^)运算符介于按位与和按位或之间。

三元运算符

比我们提到过的所有运算符的优先级都低。这可以保证选择表达式中包含的关系运算符的逻辑组合特性,如:

z = a < b && b < c ? d : e

复合赋值运算符具有相同的优先级并且是从右至左结合

a = b = c

等价于

b = c; a = b;

最低优先级的是逗号运算符

这很容易理解,因为逗号通常在需要表达式而不是语句的时候用来替代分号


赋值是另一种运算符,通常会混用的优先级。例如,考虑下面这个用于复制文件的循环:

while(c = getc(in) != EOF)
    putc(c, out);

这个while循环中的表达式看起来像是c被赋以getc(in)的值,接下来判断是否等于EOF以结束循环。不幸的是,赋值的优先级比任何比较操作都低,因此c的值将会是getc(in)EOF比较的结果,并且会被抛弃。因此,“复制”得到的文件将是一个由值为1的字节流组成的文件。

上面这个例子正确的写法并不难:

while((c = getc(in)) != EOF)
    putc(c, out);

然而,这种错误在很多复杂的表达式中却很难被发现。

随UNIX系统一同发布的lint程序通常带有下面的错误行:

if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY)
{
  ...
}

这条语句希望给t赋一个值,然后看t是否与STRTYUNIONTY相等。而实际的效果却大不相同


3.语句的结束符号

C中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个警告消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的ifwhile语句中。考虑下面的例子

if(x[i] > big);
    big = x[i];

这不会发生编译错误,但这段程序的意义与:

if(x[i] > big)
    big = x[i];

就大不相同了。


遗漏一个分号,也是十分尴尬的

if (n<3)
  return
logrec.date = x[0];
logrec.time = x[0];
logrec.code = x[0];

这个代码一样能够照常通过,编译不会报错,只是吧logrec.date = x[0];当作了return的操作数,相当于:

if (n<3)
  return logrec.date = x[0];
logrec.time = x[0];
logrec.code = x[0];

这里样可能会出现一个隐藏bug,如果n>3,就会跳过return,这个代码就尴尬了,而且这种bug还很难发现。当然如果函数声明的返回值是void,最终返回的却不一样,这样就会报错


还有这种情况:

struct logrec{
  int date;
  int time;
  int code;
}

main()
{
  ...
}

logrec结尾少写了一个分号,使得代码变成了,main函数的返回值是logrec


4.switch语句

优点缺点都在于break的使用,最好在没写break的地方加上注释

case SUBTRACT:
    opnd2 = -opnd2;
    /* no break; */
case ADD:

5.函数调用

和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数

f();

上面语句是对函数进行调用,下面求函数地址,但不会调用它

f();

6.悬挂的else

注意:else与最近的if结合,这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。看下面代码。

if(x == 0)
    if(y == 0) error();
else {
    z = x + y;
    f(&z);
}

实际上elseif(y == 0)结合了,这就尴尬了,解决办法也很简单,加上{}进行封装就好了

if(x == 0) {
  if(y ==0)
    error();
}
else {
  z = z + y;
  f(&z);
}

代价就是代码稍微长了一点,感觉可以接受,括号反而使得代码更加的清晰调理了。


有些C语言大佬会用宏定义来解决这个问题,简直瑟瑟发抖

#define IF    {if(
#define THEN  ){
#deflne ELSE  }else{
#define FI    }}


IF x==0
THEN IF Y==0
  THEN error():
  FI
ELSE  z=x+y:
f(&z):
FI

第三章 .语义陷阱(day3)

1.指针与数组

  1. 确定数组的大小
  2. 获得指向该数组下标为0元素的指针

重点:数组与指针的转换,C99中允许变长数组VLA.GCC编译器中实现了变长数组。


声明一个含有17个元素的数组,每个元素是一个结构体

struct{
  int p[4];
  double x;
}b[17];

声明一个名字为calendar的数组,该数组拥有12个数组元素,每个元素含有31个整型的数组。shape=(12,13)

int calendar[12][31];

如果两个指正指向同一个数组,指针相减也是有意义的,如下代码q-p=i

int *q = p + i

数组名可以直接赋个指正,指向0的位置

p = a;

但是,在ANSI C 中,下面代码是非法的,应为&a是一个指向数组的指针,而p是指向整形变量的指针

p=&a;

下面两种写法相同的

p=p+1;
p++;

重点: 数组与指针转换

*(a+i)是指数组a中的第i元素a[i],实际上a+ii+a的含义是一样的,因此a[i]i[a]也具有相同的含义

sizeof(calendar[4])的结果是31*sizeof(int)


如下代码所表达的意思是一样的

i=calendar[4][7];
i=*(calendar[4]+7);

进一步可以改写成

1=*(*(Calendar+4)+7);

和明显可以发现带括号下标的写法要简明的多


这个是非法的,calendar是数组的数组,在此处上下文中会转回成指向数组的指针,而p是一个整型变量的指针。

p=calendar

我们可以申请一个指向数组的指针,来存放calendar的指针

int(*ap)[31];

2.非数组的指针

下面字符串拷贝的代码,看上面貌似Ok但是包含3个bug
1. malloc可能无法请求内存
2. r的地址没有释放
3. strlen并没有计算\n的位置,所以其实应该分配+1个内存空间

char *r *malloc();
r=malloc(strlen(s)+strlen(t));
strcpy(r, s);
strcat(r, t);

正确的写法大概是这个样子

char *r *malloc();
r=malloc(strlen(s)+Strlen(t)+1);
if(!r)(
  complain();
  exit(1);
)
strcpy(r s);
strcat(r t);
/*一段时间后调用*/
free(r);

3.作为参数的数组声明

C语言中,我们没办法将数组作为函数参数直接传递,例如下面这个代码只是将第一个元素的指针传了进去

char hello[] = “hello”;

下面两个代码完全等效

printf("%s\n", hello);
printf("%s\n", &hello[0]);

同理,下面两个也是一个意思

int strlen(char s[]) {
  ...
}
int strlen(char *s)
{
  ...
}

下面代码有天壤之别,挖坑,后边讲

extern char *hello;
extern char hello[];

如果指针并不实际代表一个数组,则会产生误导..

如果代表数组,则下面代码是等价的

main(int argc, char* argv[])
main(int argc,char** argv)

虽然意思相同,但是前一种写法明显的看出argv是数组的第一个元素指针,选择最清楚的表达。

4.避免“举隅(yu)法”

没文化,真可怕,打了半天没打出隅这个字….
这里写图片描述

ANSIC标准中禁止对stringliteral做出修改,C语言编译器还是允许q[1]=’y’这种修改行为

5.空指针并非空字符串

常数0经常用一个符号来代替

#define NULL 0

合法

if(p == (char *)0) ...

非法

if(strcmp(p, (char *)0) == 0) ...

原因是strcmp会检查指针指向内存中的内容,如果p是一个空指针,gg

printf(p);
printf("%s", p);

gcc上打印(null)类似语句在不同计算机上有不同的效果。

6.边界计算不对称边界

缓冲区声明

#define N 1024
static char buffer[N];

设置一个指针指向缓冲区

static char *bufptr

这里写图片描述

把字符c放到缓冲区中,buffer有加一,指向下一个没有被使用的空间

*bufptr++ = c;

初始化缓冲区可以这样写

bufptr=&buffer[0];

更简洁点可以这样

bufptr=buffer;

bufwrite的代码

void
bufwrite(char *p, int n)
{
  while(--n >= 0){
    if(bufptr==&buffer[N])
      flushbuffer();
    *bufptr++= *p++;
}

由于不对称边界原则,虽下面两个代码是等价的,但是应该写成后一个

if(bufptr==&buffer[N])
if(bufptr > &buffer[N-1];

手写一个内存拷贝,面试必考

void memcpy(char *dest const char *source
{
  while(--k >=0)
    *dest++ = *source++;
}

7.求值顺序

会产生一个除0的错误

if(count != 0 && sum/count < smallaverage)
  printf(“average < %g\n”, smallaverage);

C语言定义规定a < b首先被求值。如果a确实小于bc < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。

a < b && c < d

要对a < b求值,编译器对ab的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。

C中只有四个运算符&&||?:和,指定了求值顺序。&&||最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而?:运算符中的三个操作数:abc,最先对a进行求值,之后仅对bc中的一个进行求值,这取决于a的值。,运算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值


坑,其中的问题是y[i]的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能,这就尴尬了。

i = 0;
while(i < n)
    y[i] = x[i++];

正确的写法是下面这种

i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}

简写成这样也是OK的

for(i = 0; i < n; i++)
    y[i] = x[i];

8.运算符 && ||

10 || f()中的f()也不会被求值

9.整数溢出

假设ab是两个非负整型变量,你希望测试a + b是否溢出。一个明显的办法是这样的:

if(a + b < 0)
    complain();

然而这是有问题的,一旦a + b发生了溢出,对于结果的任何判断都是没有意义的。一种正确的做法是这样

if((int)((unsigned)a + (unsigned)b) < 0)
    complain();

10.为函数main提供返回值

None

第四章 .连接(day4)

1. 什么是连接器

一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为连接器、连接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。

2. 声明与定义

外部整型变量,显示的说明a的存储空间在程序的其他地方分配

extern int a;

3. 命名冲突与static修饰符

static 是常用来减少类命名冲突的有用工具

static int a;

static函数

static int fun(int x)
{
  ...
}

4. 形参、实参与返回值

如果函数在不同文件中,调用之前需要在所调用的文件中声明

double square(double);//square在其他文件中实现的

main()
{
  printf(“%g\n”, square(0.3));
}

square(2)的写法是合法的,因为2会被自动转换成一个双精度的类型,square((double)2)square(2.0),这两种写法也是可以的


一个输入、输出的错误例子

#include<stdio.h>
main()
{
  int i;
  char c;
  for(i = 0; i < 5; i++){
    scanf("%d", &c);
    printf("%d", i);
  }
  printf("\n");
}

表面上应该输出

0 1 2 3 4

实际上输出的却是

0 0 0 0 0 1 2 3 4

因为c的声名是char而不是int。当你令scanf()去读取一个整数时,它需要一个指向一个整数的指针。但这里它得到的是一个字符的指针。但scanf()并不知道它没有得到它所需要的:它将输入看作是一个指向整数的指针并将一个整数存贮到那里。由于整数占用比字符更多的内存,这样做会影响到c附近的内存。c附近确切是什么是编译器的事;在这种情况下这有可能是i的低位。因此,每当向c中读入一个值,i就被置零。当程序最后到达文件结尾时,scanf()不再尝试向c中放入新值,i才可以正常地增长,直到循环结束。

5. 检查外部类型

如果C程序被划分为两个文件

int n;
long n;

这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。


这个程序运行时实际会发生什么?这有很多可能性:
1. C编译器足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。
2. 也有可能你所使用的实现将intlong视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。
3. n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。
4. n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。

6. 头文件

最好的解决办法就是使用头文件,只在头文件中声明,例如定义一个file.h,他声明了

extern char filename[];

在需要使用的地方加上头文件

#include “file.h”
char filename[] = "/etc/passwd";

第五章 .库函数(day5)

1. 返回整数的getchar函数

看看下面这个例子

#include <stdio.h>
main()
{
  char c;
  while((c=getchar()) != EOF)
    putchar(c);
}

这段程序看起来好像要将标准输入复制到标准输出。实际上,它并不完全会做这些。原因是c被声明为字符而不是整数。这意味着它将不能接收可能出现的所有字符包括EOF。因此这里有两种可能性。

  1. 有时一些合法的输入字符会导致c携带和EOF相同的值,有时又会使c无法存放EOF值。
  2. 在前一种情况下,程序会在文件的中间停止复制。在后一种情况下,程序会陷入一个无限循环。
  3. 实际上,还存在着第三种可能:程序会偶然地正确工作。

C语言参考手册严格地定义了表达式

((c = getchar()) != EOF)

当一个较长的整数被转换为一个较短的整数或一个char时,它会被截去左侧;超出的位被简单地丢弃。

2. 更新顺序文件

看上去貌似可以同时进行读写操作,其实不然

FILE *fp;
fp=fopen(file, "r+");

若真正要实现同时读写,需要插入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);
  }
}

上面这个函数看上去貌似没有什么问题,sizeof(res)也被转换成了long类型,因为int可能无法存放一个文件的大小,sizeof返回的是一个unsigned,所以必须先将其转换为有符号类型才能将其反号,但是这段代码依然有可能会出错

问题情况:如果记录需要被重新写入文件,fwriteh获得执行,这个操作的下一个操作就是fread,之间就缺少了一个fseek函数,解决办法如下:

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);
  }
}

第二个fseek看上去啥也没做,但是它却改变了文件的状态,是的文件现在可以正常的进行读取了

运行测试代码:

#include <stdio.h>
int main()
{
  FILE *fp;
  struct record{
  char a;
  char b;
};

struct record rec;
if((fp=fopen("test", "rb+"))==NULL)
{
  printf("open error!\n");
}
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
  /*rec执行某些操作*/
  printf("record.a=%d\n", rec.a);
  printf("record.b=%c\n", rec.b);
  if(1){
    fseek(fp, -(long)sizeof(rec), 1);
    rec.a=rec.b;
    fwrite((char*)&rec, sizeof(rec), 1, fp);
    fseek(fp, 0L, 1); /*
    */
    }
  }
  fclose(fp);
}

3. 缓冲输出与内存分配

程序员可以通过setbuf函数来控制生产数据量,如果buf是一个适当的字符数组

setbuf(stdout, buf);

上段代码将告诉I/O库写入到stdout中的输出要以buf作为一个输出缓冲,并且等到buf满了或程序员直接调用fflush()再实际写出。缓冲区的合适的大小定义为BUFSIZ,在<stdio.h>中。


下面一个使用setbuf的例子

#include
main() {
    int c;

    char buf[BUFSIZ];
    setbuf(stdout, buf);
    while((c = getchar()) != EOF)
        putchar(c);
}

不幸的是这个程序是错误的,因为一个细微的原因。原因在于,我缓冲区最后一次刷新是在主程序完成之后,库将控制交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!


解决的办法有很多,比如定义静态的缓冲区

static char buf[BUFSIZ];

另一种可能的方法是动态地分配缓冲区并且从不释放它:

char *malloc();
setbuf(stdout, malloc(BUFSIZ));

注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。

4. 使用errno检测错误

在系统调用中,如果调用失败通常会通过一个名为errno的外部变量来通知程序失败,下面这个程序貌似可以处理错误情况,然后却是有问题的

/*调用库*/
if (errno)
  /*处理错误*/

原因是系统调用没有失败的情况下,没有强制要求errno设置为0 ,改进一下

errno = 0;
/*调用库*/
if (errno)
  /*处理错误*/

上面代码依然有问题,原因是系统调用成功的情况下,没有强制要求errno设置为0 ,但是也没有禁止设置errno。系统调用可能回去调用其他的系统调用,其它调用也有可能去修改errno的值。


正确的写法是:先检测错误指示的返回值,确定程序执行失败后在检查errno

/*调用库*/
if(返回的错误值)
  检查errno

5. 库函数signal

头文件

#include <signal.h>

调用

signal(signal type, handler function);

signal处理函数中使用longjmp退出,是不安全的

异步操作是很容易留bug,很麻烦,尽量简单的使用…

第六章 .预处理器(day6)

1. 不能忽略宏定义中的空格

多加了个空格,意思理解有歧义

#define f (x) ((x) - 1)

f(x)代表什么呢

((x) - 1)

还是

(x) ((x) - 1)

第二种答案是正确的,下面这个写法要好一些

#define f(x) ((x) - 1)

2. 宏并不是函数

因为宏的一些写法,容易被认为与函数等同,比如下面两种

#define abs(x) (((x)>=0)?(x): -(x))

#define max(a, b) ((a))(b)?(a):(b))

使用过多的括号是为了防止出现优先级不清晰的问题

一个混合了宏和递增运算符的写法,看上去就要炸

#define putc(x, p) (--(p)->_cnt>=0?(*(p)->_ptr++=(x)):_flsbuf(x,p))

这样写是很危险的,putc()的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据结构的指针。注意第一个参数完全可以使用如*z++之类的东西,尽管它在宏中两次出现,但只会被求值一次。而第二个参数会被求值两次(在宏体中,x出现了两次,但由于它的两次出现分别在一个:的两边,因此在putc()的一个实例中它们之中有且仅有一个被求值)。由于putc()中的文件参数可能带有副作用,这偶尔会出现问题。不过,用户手册文档中提到:“由于putc()被实现为宏,其对待stream可能会具有副作用。特别是putc(c, *f++)不能正确地工作。”但是putc(*c++, f)在这个实现中是可以工作的。


宏的另外一个缺点是可能会产生巨大的表达式

#define max(a, b) ((a) > (b) ? (a) : (b)) 

假设我们这个定义来查找a、b、c和d中的最大值。如果我们直接写:

max(a, max(b, max(c, d))) 

它将被扩展为:

((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ? 
(a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d)))))) 

这种写法还不如写if else

3. 宏并不是语句

宏的定义很不直观,经常产生一些隐藏bug,举一个例子

#define assert(e) if(!e) assert_error(__FILE__, __LINE__)

下面代码貌似Ok

if(x > 0 && y > 0)
  assert(x > y);
else
  assert(y > x);

但是展开后就是这个鬼样子

if(x > 0 && y > 0)
  if(!(x > y)) assert_error("foo.c", 37);
else
  if(!(y > x)) assert_error("foo.c", 39);

很明显if else会出问题,于是乎准备价格{}试试水

#define assert(e) \
{ if(!e) assert_error(__FILE__, __LINE__); }

然后就变成了这样

if(x > 0 && y > 0)
  { if(!(x > y)) assert_error("foo.c", 37); };
else
  {if(!(y > x)) assert_error("foo.c", 39); };

看上去十分的诡异。

4.宏并不是类型定义

宏的一个通常的用途是保证不同地方的多个事物具有相同的类型:

#define FOOTYPE struct foo 
FOOTYPE a; 
FOOTYPE b, c; 

这样如果要改代码中的某个变量类型就很方便了,而且所有的C编译器都支持它,所以使用这样的宏定义还有着可移植性的优势


多变量声明是有bug,下面一个栗子

#define T1 struct foo * 
typedef struct foo * T2; 

多变量声明

T1 a, b; 
T2 c, d; 

然后被扩展成

struct foo * a, b; 

这里a被定义为一个结构指针,但b被定义为一个结构(而不是指针)。相反,第二个声明中c和d都被定义为指向结构的指针,因为T2的行为好像真正的类型一样。

第七章 .可移植性缺陷(day6)

1. 应对C语言标准的变更

标准往往需要很长时间才能得到编译器的支持,然后即使支持了,用户也懒得去升级他们的编译器,所以编写程序时候,选择那个标准也是很尴尬的,这个有点类似与Android开发里面的api level的选择,尽可能的选择用户覆盖范围广的那个版本

2. 标识符号名称的限制

并不是所有都区分标示符名称的大小写,C标准只要求能够区分6个字符不同的外部名称

3. 整数的大小

C为程序员提供三种整数尺寸:shortintlongC语言定义对各种整数的大小不作任何保证:
1. 整数的三种尺寸是非递减的,后面的能容纳前面的。
2. 普通整数的大小要足够存放任意的数组下标。
3. 字符的大小应该体现特定硬件的本质。

4. 字符是有符号整数还是无符号整数

有一种误解是认为当c是一个字符变量时,可以通过写(unsigned)c来得到与c等价的无符号整数。这是错误的,因为一个char值在进行任何操作(包括转换)之前转换为int。这时c会首先转换为一个int,这可能会产生奇怪的结果。

正确的方法是写(unsigned char)c,无需转换成int,直接进行转换。

5. 移位运算符

常见的有两个问题
1. 在右移运算中,空出的位是用0填充还是用符号位填充.
2. 移位的范围就是位数

第一个问题的答案很简单,但有时是实现相关的。如果要进行移位的操作数是无符号的,会移入0。如果操作数是带符号的,则实现有权决定是移入0还是移入符号位。如果在一个右移操作中你很关心空位,那么用unsigned来声明变量。这样你就有权假设空位被设置为0。

第二个问题很简单,(例如32位,移位范围n[0,31])

移位操作最好不要用除法来实现,效率低

6. 内存位置0

不同b编译器有不同情况,比如:结束,读到垃圾数据,读取到关键位置的文件(操作系统)

#include <stdio.h>
int main()
{
    char *p;
    p = NULL;
    printf("Location 0 contain %d\n", *p);
    return 0;
}

我电脑上运行报错:Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

7. 除法运算时发生的截断

#include <stdio.h>
main()
{
  int a=-3, b=2;
  int q,r;
  q = a / b;
  r = a % b;
  printf("q=%d, r=%d\n", q, r);
}

带符号整数的除法与余数

8. 随机数的大小

rand

RAND_MAX

9. 大小写转换

toupper()tolower(),他们最初都被实现为

#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')

这些宏确实有一个缺陷,即:当给定的东西不是一个恰当的字符,它会返回垃圾。下面这个是无法工作的。

int c;
while((c = getchar()) != EOF)
    putchar(tolower(c));

必须写成

int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);

后来AT&T的大佬有重写了宏

#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))

但要知道,这里c的三次出现都要被求值,这会破坏如toupper(*p++)这样的表达式。因此,可以考虑将toupper()和tolower()重写为函数,大概想这个样子。

int toupper(int c) {
  if(c >= 'a' && c <= 'z')
    return c + 'A' - 'a';
  return c;
}

后面考虑到有的人不愿意付出效率上的损失,有写了,但是用了新名字

#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')

10. 首先释放,然后重新分配

三个内存分配函数

malloc
malloc(n)
realloc

释放后又分配是合法的

free (p);
p = realloc(p, newsize);

释放一个链表中所有元素

for(p = head; p != NULL; p = p->next)
  free((char *)p);

11. 可移植性问题的一个栗子

一个移植的例子,它将整数转换位十进制数,并用代表其中每一个数字的字符来调用给定的函数。

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}

这个程序——由于它的简单——具有很多可移植性问题。首先是将n的低位数字转换成字符形式的方法。用n % 10来获取低位数字的值是好的,但为它加上’0’来获得相应的字符表示就不好了。这个加法假设机器中顺序的数字所对应的字符数顺序的,没有间隔,因此’0’ + 5和’5’的值是相同的,等等。尽管这个假设对于ASCII和EBCDIC字符集是成立的,但对于其他一些机器可能不成立。避免这个问题的方法是使用一个串:

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}

另一个问题发生在当n < 0时。这时程序会打印一个负号并将n设置为-n。这个赋值会发生溢出,因为在使用2的补码的机器上通常能够表示的负数比正数要多。例如,一个(长)整数有k位和一个附加位表示符号,则-2k可以表示而2k却不能。
一个简单的方法是将这个程序划分为两个函数.

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        printneg(n, p);
    }
    else
        printneg(-n, p);
}

void printneg(long n, void (*p)()) {
    if(n <= -10)
      printneg(n / 10, p);
    (*p)("0123456789"[-(n % 10)]);
}

我们使用n / 10和n % 10来获取n的前导数字和结尾数字(经过适当的符号变换)。调用整数除法的行为在其中一个操作数为负的时候是实现相关的。因此,n % 10有可能是正的!这时,-(n % 10)是负数,将会超出我们的数字字符数组的末尾。
为了解决这一问题,我们建立两个临时变量来存放商和余数。

void printneg(long n, void (*p)()) {
  long q;
  int r;
  if(r > 0) {
    r -= 10;
    q++;
  }
  if(n <= -10) {
    printneg(q, p);
  }
  (*p)("0123456789"[-r]);
}

为了满足可移植性,我们需要付出很高的代价!

猜你喜欢

转载自blog.csdn.net/luojie140/article/details/80057094