C陷阱与缺陷学习总结

本书深入探讨C/C++编程中的常见问题与解决方案,强调实践中容易忽视的细节,旨在提高编程质量与效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

总结序言:

学习大牛们的书籍,每次都是新潮澎湃,热血沸腾,看他们的书心中就有一种信任,有一种无比的膜拜。最初看到这本书的时候心情也是如此。Andy这个大牛我想大家也都如雷贯耳了吧.从图书馆找到他的书籍,我二话不说便细细的品味起来,希望从中能获得意想不到的东西.然而当我真正的深入阅读之后才发现原来一切都是那么常见的问题,一切都来源于实际的编程,并不是什么高深的东西。然而正是这些常见的细微的编程问题扰乱了我们的生活,也许曾多次深夜时分你还在为一个小小的bug而懊恼不已,只身忙碌于电脑旁,不肯入眠。但我相信如果你熟读此书并理解之后把它用于你的实际编程中之后,那么你这样的“懊恼”与“不必要的忙碌”会相应减少很多。愿每位C/C++程序员或爱好者都能熟读此书,你将从中获得意想不到的收获。即使有些东西你早已知道,再次甚至多次提醒一下自己也不为过。因为只要与正确的优雅的用于实际的编程之中还有很长的一段路要走,在这期间我们要做的就是不断的提醒自己与不断的思考改进。

0

看到的Andy的第一个C程序,有些失望,竟然是下面这种类型的:

main()

{

}

这中方式在旧的编译器上虽然能成功运行,但他确是大家极不推崇的一种方式。我们应该明确写出正确的函数返回类型.正确的写法应该是这样的:

int main()

{

       return 0;

}

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

{

       return 0;

}

目前只有这两种方式才是正确的主函数方式.

第二个代码:

int i,a[N];

for (i = 0; i <= N; i++)

{

       a[i] = 0;

}

作者说这个程序会出现死循环,在此对此坐下解释:

理解这个结果(“死循环”),我们应该首先理解操作系统内存的分配方式.我们的内存是从高地址开始分配的。所对于变量i和数组a[N].在内存中的关系应该是这样的:

   高地址       

i

    a[N-1]

    a[N-2]

     …..

     ……

   a[4]

    a[3]

    a[2]

    a[1]

    a[0]

  低地址

 

所以对于上面的程序我们遍历数组a[N]的时候,a[0]开始,知道a[N].而我们的数组有效位置是从a[0]a[N-1]的。对于a[N]已经不再属于数组的有效范围了。而在内存中a[N]的位置便是i的位置了。而我们每次遍历数组一遍都会将a[N]0.即令i = 0;故就会陷入无限的死循环了。

而对于上述程序若我们稍作改动便会是另外一种结果了:

int a[N],i;

此时虽然我们改变的仅仅是变量定义的顺序,但是他们在内存中的位置关系就不同了,这样我们访问a[N]的时候就不会再访问到i.这样就会出现未定义的结果了。不过一般情况下都会出现内存错误,即弹出一个对话框…..

本章习题:

0-1:

这题目开始我看了不禁迷惑起来,作者为啥出这题目呢。直到看了作者的解答我还是没有明白过来,之后深深思考之后才算略有所悟,作者是想通过这个问题让我们每个人都能站在客户的角度思索一下,返修率极高的产品或是bug极多的软件产品是不会受到用户的欢迎的,故我们在写程序的时候也应该时刻保持高质量的程序,这不但便于我们以后的调试,更加影响到最终产品的质量和公司的效益。

0-3:

看到这道题目,我原以为是一道发挥想象力的题目呢,所以就尽情的思索。我的回答是:

我烹饪时失手切伤过自己的手。我设想对菜刀的改进方法有两种:

:改变刀刃的方向,即令刀刃稍微偏向远离手指的方向.

这种方法不但令菜刀变得难于使用,而且也没有彻底的解决问题,因为有些人还是会因此切伤手指的。

:我设想在刀刃外面加一个框架,就好似弹簧刀那样的,刀刃可以随我们的控制进出和固定。然后在刀刃上安装一个温度传感器,当刀刃接触到的物体温度为37°C左右的时候,刀刃就自动进入框架。

这个设计必然令一个简单的菜刀繁琐起来,并且还增加了它的成本,不免是一种“画蛇添足”的行为,而且对技术的要求极高。

当看过作者的解答之后我才顿悟过来,原来作者是想通过这道题来提醒我们,有些东西的缺点正是他的优点。若C是一门简洁的语言,易学,但是很多东西都需要我们自己去写,远没有JAVAC#Delphi开发来的便宜。可是他简单易用,灵活效率。我们不能为了改变它的缺点,从而去掉他的优点。这反而失去他的本性了。万事万物的存在都有他存在的道理。我们不应该轻易去否定它的价值。

1

1.2  & | 不同于 && ||

开始的时候自己根本分不清这两种符号。直到今天才算明白:

& | 是位运算符。

&& || 是逻辑运算符.

:

int a,b;

a = 2;

b = 3;

a & b; a | b;是对a,b的位操作.

a的二进制为: 0010 b的二进制为: 0011;

:

          0010

a & b = =  & 0011  = =  0010 = = 2;

         0010

a | b = =  |  0011  = =  0011 = = 3;

而,a && b, a || b;都是对a,b的逻辑操作.

: a && b由于a,b均为非0.a && b = = 1: a || b = =1;

1.3 词法分析中的贪心法

以前只对这种方式有所了解,根本不知道有这个名字。现终于明白啦..O(_)O

C语言中的某些符号,例如/*=,只有一个字符长,称为单字符符号,而C语言中的其他符号,例如/*==,以及标识符,包括了多个字符,称为多字符符号。当C语言编译器读入一个字符'/'后又跟了一个字符'*',那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是后起来作为一个符号对待。C语言对这个问题的解决方案可以归纳为一个很简单的规则:每一个符号应包含尽可能多的字符。即编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分:如果可能,继续读入下一个字符符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个策略有时被称为“贪心法”。

借此趁着这个对这个“贪心法”的理解解释一下习题:

1-3:

n-->0的含义.

首先编译器读进一个字符n,而后接着读进‘-’经判断他是一个运算符,而后接着读下一个,又读进一个‘-’。此时’—‘能够组成一个新的运算符,编译器继续读取字符,又读进一个‘>,此时’- ->’便不再是一个运算符了。n-->0便被解释成为n-- > 0;了。

1-4:

a+++++b的含义是什么?

这个按照上面的方法逐个字符的分析便可得知含义为:

a++ + ++b;

2

2.1 理解函数声明

这里仅仅理解一下作者给出的一个语句:

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

我们从最内部开始分析,并逐步扩展:

void(*)();

为了便于分析我们定义一个变量来帮助分析:

typedef void (*PFUN)();

PFUN p;

这个是一个函数指针,它指向返回类型为void,空参数的函数.

(void(*)())0;

根据上一步的变量定义,上面语句就可以转换成:

(p)0;

再进一步扩展:

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

这一步是对用函数指针对函数进行调用:

我们用上面的变量进行替换一下就明了了:

(*(p)0)();

这就很清楚明了了。

这里我获得了一个知识点,以前我本以为用函数指针对函数进行调用直接用函数指针加上括号就OK了。如:

typedef void (*PFUN)();

PFUN p;

若有一个这样的函数:

void visit();

p = visit;

我们现在就可以这样调用了.

p();

我原以为就应该这样调用。现在我才明白,其实我们也可以这样调用:

(*p)();并且我原以为的调用p();只是(*p)();的简化调用。

3:

3.1指针和数组

int a[12][31];

一个二维数组:

看似很简单的一个二维数组,现在我们分析一下它所包含的知识:

a是什么类型?

很多同学对此很迷糊,有的甚至就直接说,他不就是一个二维数组名吗?看似很简单。其实a是一个指向一个维数为31的一维数组指针.类型是这样的:

typedef int (*p)[31];

②:我们若通过指针遍历此二维数组该如何遍历呢?我们该如何定义指针呢?

有两种方法:

1:定义一个int型指针

int *p = &a[0][0];定义一个指针指向二维数组的第一个元素的指针.而后以此遍历即可.

:

int a[2][3] = {{1,2,3},{4,5,6}};

int *p;

for (p = &a[0][0]; p != &a[1][3]; ++p)

{

    printf("%d ",*p);

}

printf("/n");

这里说一下,我代码里用了&a[1][3]做结束标志.很多同学可能对此不解:为什么不用&a[2][0]?呵呵,其实这样&a[1][3]&a[2][0]是一样的。他们指向同一个内存地址.

int (*ap)[31];

此时ap是一个指向有31个元素的一维数组的指针.这就引来了我们的第二种方法。

2:定义一个一维数组指针.

int (*p)[3];

代码如下:

int a[2][3] = {{1,2,3},{4,5,6}};

int (*p)[3];

int i;

p = a;

for (i = 0; i < 2; ++i)

{

    int *q;

    for (q = p[0]; q != p[0]+3; ++q)

    {

       printf("%d ",*q);

    }

    ++p;

}

printf("/n");

呵呵,这个相比上面那个代码麻烦了许多。不过也确是一种方法;

4

4.3

这一节只要明白static类型的变量默认为文件内变量,即从其他文件中无法访问的变量。同样函数也是如此。

这一章的学习还让我联想到printf函数的一种是用方式;

printf("hello,world",INT_MAX);

这种方式虽然是无聊的,无用的。但是它却可以正确的运行。

6

6.1不能忽视宏定义中的空格

宏定义中的空格往往会引起本质的变化,我们定义的时候千万要小心,不该加空格的地方千万不要加。

:

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

本来我们可能是想把 f(x) ((x)-1)替换,现在到弄巧成拙的让f (x) ((x)-1)替换了。

6.2宏不是函数

①我们最好在宏定义中把每个参数都用括号括起来、

②整个结果表达式也应该用括号括起来

另宏也不是语句,不是类型,它仅仅是简简单单的替换.

:#define FOOTYPE struct foo;

这样题目中所有的FOOTYPE 都会被struct foo替换;

:我们程序中有这样的定义:

FOOTYPE a;

FOOTYPE b,c;

将会被替换成为:

struct foo a;

struct foo b,c;

可以看出没什么问题。好像运行的也很好。我们再看另一种情况:

#define T1 struct foo *

若我们这样定义:

T1 a,b;

将会被替换成:

struct foo *a,b;

这样的结果便是,我们的a变量是一个指针,b变量是一个结构体类型。完全违背了我们的原意。所以说宏不是类型,它仅仅是简单的替换,我们若是想定义类型的话还是应该用typedef.

7

移位运算的速度远快于除法运算。

附录A

简单的格式类型:

%u 输出无符号十进制数

 

%o 打印八进制数

 

%x 打印十六进制数,格式项字符用小写字母

 

%X 打印十六进制数,格式项用大小字母

 

%s 打印字符串

 

printf(s);

printf(%s,s);

这两个式子的区别:

在打印普通字符串时,两者效果相同,但是第一个若要想打印回车符的话必须再写一句话。

在打印含有格式符的字符串时,第一个将会事与愿违,因为他后面没有参数,产生的字符串中将会产生乱码。

 

%g%f ,%e用于打印浮点数。

%g 打印出的数值会去掉该数值尾缀的0,保留6位有效数字。需包含math.h头文件,但是如果一个数的位数大于6位,即绝对值大于999999%g会按照科学计数法打印这样的数值,科学计数法中的小数部分仍保留6位有效数字。

%f打印出来的额数字总会保留6位小数,不管是否是0.

%e,一律使用科学计数法打印,但是它是小数点后保留6位有效数字。

%E,%G%e%g一样,只不过他们用E替换了e.

修饰符

%% 打印出一个%

 

%p 用于打印指针

 

%n 用于打印已打印的字符.

 

%#o 打印结果前加0

%#x 打印结果前加 0x

%#X 打印结果前加 0X

%+d 0和整数前加+

 

m.n

对于 %d,%o,%x%u,精度修饰符指定了打印数字的最少位数,

:%.nd

如果待打印的数值并不需要这么多位数的数字来表示,就会在他前面补0,若是整数修饰,

:%md

输出的宽度应为m列,若是输出的字符比列数小,就左补空格。

 

对于%e,%E,%f。精度修饰符指定了小数点后应该出现的数字位数。仅当精度大于0时打印的数值中才会实际出现小数。

 

对于%g,%G.精度修饰符指定了打印数值的有效数字位数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值