c语言讨论学,C语言趣味讨论

额,忽略标题吧,我想了很长时间不知道用什么名字好了。其实是这样的,最近看了一本书 Writing Bug-free C Code(以下简称WBCC),本来打算好好研究下的,因为这本书讲到几点带来bug的原因,比如头文件中过多数据结构声明,过多全局变量,编写难以维护的代码等都是些很常见的问题。但是看了两章多一点的时候发现内容对我来说太高深了,以目前的水平很多地方看不明白,所以暂时放弃了,但是前面还是有一部分觉得很有意思的东西,于是就决定还是写篇博文总结讨论一下。(原书链接:www.duckware.com/bugfreec/index.html)

1. 编译时断言(CompilerAssert)

提到断言,首先想到的应该就是assert()函数,这个函数可以保证如果断言条件不成立,那么程序立即终止运行,我们可以把这个叫做run-time assert,但是有的时候我们可能想在程序编译的时候就断言,也就是如果某一个条件不成立,那么程序就不再继续编译下去,不生成可执行文件,可以称作 compile-time assert。我们知道C语言中如果数组的大小是负数的话,那么程序在编译的时候就会报错,所以如果将需要断言的表达式的判断结果(条件成立结果为1,不成立为-1)作为数组的大小,那么就可以实现编译时的断言。原书作者的方法在这里就不提及了,经过搜索,我在StackOverflow上找到了这么一段容易理解的代码,注意##是用来连接前后的字符,比如a##b就是ab。(原提问链接在此:http://www.voidcn.com/article/p-rkibkglp-bss.html 我将代码注释大致翻译一下):

/** 编译时断言

* 条件成立,编译继续

* 条件不成立,相当于用typedef定义一个类似于下面这种形式的数组:

* typedef assertion_failed_file_h_42[-1]

* 其中file是文件名, 42是断言所在行, -1是断言条件不成立产生的结果

* 参数解释:

* predicate是断言表达式,它的结果必须是一个bool值

* file是断言所在源文件的名字

*/

#define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)

#define _impl_PASTE(a,b) a##b

#define _impl_CASSERT_LINE(predicate, line, file) \

typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];实现方式如下:

/* a simple application */

#include "CAssert.h"

...

struct foo {

... /* 76 bytes of members */

};

CASSERT(sizeof(struct foo) == 76, demo_c);如果结构体foo的大小不是76字节,当编译C文件的时候就会出现下面的错误提示:

$ gcc -c demo.c

demo.c:32: error: size of array `assertion_failed_demo_c_32' is negative

$这种实现方式比WBCC作者提供的版本更清晰明了,而且从打印的结果我们就能很明显地看出问题出在什么地方。

2. 关于C语言声明

又是C语言声明!相信很多人一看到这个就很头大,光光理解一个又臭又长的声明就很头疼,更不要说这个又臭又长的声明还要自己去定义了,但是这个问题又不不得去面对。经过比对,我发现作者提到理解C语言声明的方法跟《C专家编程》中提到的方法有异曲同工之处,或者说其实几乎就是一样的。所以我把《C专家编程》中总结的判断步骤搬到这里。

A 声明从它的名字开始读取,然后按照优先级顺序依次读取。

B 优先级从高到低依次是:

B.1 声明中被括号括起来的那部分。

B.2 后缀操作符:

括号 () 表示这是一个函数,而方括号 [] 表示这是一个数组

B.3 前缀操作符:星号 * 表示“指向...的指针”。

C 如果const和(或)volatile关键字的后面紧跟类型说明符(如 int, long等),那么它作用于类型说明符。在其他情况下,const和(或)volatile关键字作用于它左边紧邻的指针星号。

举例如下:

char * const *(*next)();

首先找到名字next,然后发现它被括号括起来,那么就把括号里面的作为一个整体来看,就是“next是一个指向...的指针”

然后考虑括号外面的,括号后缀要比星号前缀优先级高,所以得出“next是一个函数指针,指向一个返回...的函数”

再考虑星号前缀,就是“指向的函数返回一个指向...的指针”

再往右读,发现右边已经没有了,这时候处理左边剩下的部分

星号前缀的左边,找到了const关键字,它后面没有紧跟类型说明符,左边有一个星号,那么它作用于左边的这个指针星号,星号是char,也就是一个“char类型的常量指针”

将前面结合在一起,即“next是一个函数指针,它指向的函数没有参数且返回一个指针,该指针指向一个类型为char的常量指针”。

再用同样的方式去理解signal()函数的原型

void (*signal(int sig, void(*func)(int)))(int);

首先找到第一个名字signal,它有括号后缀和星号前缀,根据优先级,先考虑括号,即“signal是一个函数,该函数返回一个指向...的指针”

如果我们暂时忽略signal函数的参数,就可以看出函数返回的指针用括号括起来,并且括号的后面又有一个括号,因此有“signal函数返回一个指向函数的指针,指针指向的函数带有一个int类型的参数,返回void”

再折回去看signal函数的参数,第一个参数是int类型,第二个参数又是一个复杂的声明

同样的方式,找到第一个名字func,它被括号括起来,括号里还有一个星号,因此它是一个指针,括号后面又紧跟括号,因此这是一个函数指针,且该函数参数为int,返回void

总结起来,就是“signal函数返回一个函数指针,该函数指针指向的函数参数为int,返回void。signal函数的参数第一个为int,第二个参数是一个函数指针,该函数指针指向的函数参数为int,返回void”

再臭再长的声明只要按照这种方式来处理,最后都可以分析出来,关键要在于多加练习。

补充一点,在《C陷阱与缺陷》中,Koening提到当我们理解如何声明某一类型的变量的时候,该类型的类型转换符就很容易得到:

只需要把声明中的变量名和声明末尾的分号去掉,再将剩余部分用一个括号整个“封装”起来即可。比如,有下面的声明:

float (*h)();表示h是一个指向返回值为float类型的函数的指针。因此

(float (*)())表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。

3. 可变长参数函数过程调用的堆栈实现

C语言中对于可变长参数的实现,大家最熟悉的莫过于printf函数了,该函数可以接受多个数量不定的参数,那么这个函数在调用的时候它的堆栈是如何实现的?

对于普通固定参数函数的过程调用,就是先把所有参数压入栈中,然后再压入返回地址,接着就调用函数。问题就在于参数压入的顺序是如何的?是从左往右压入还是从右往左?看下面这个例子

// Printf example

printf( "Testing %s %d", pString, nNum );假设是从左往右压入堆栈,也就是像下面这样

0818b9ca8b590ca3270a3433284dd417.png

字符串“Testing %s %d”的地址很容易确定,因为是第一个参数,但是由于printf函数带的参数不固定,所以导致返回地址存放的位置相对于第一个参数的地址就是不固定的,这样也就很难确定返回地址。

相反,假如是从右往左压入堆栈,也就是

0818b9ca8b590ca3270a3433284dd417.png

第一个参数压入之后紧跟着就是返回地址,这样返回地址就很容易取出来。

不止可变长参数函数的调用如此,普通函数也是一样,这中调用方式还有一个名词,叫“C调用惯例(C calling convention)”。

其实这点内容,在之前的博文

[CSAPP学习笔记] 栈帧 已经体现出来,只是很难注意到(我自己都没注意到,还是WBCC作者提到了才想起来)。如果仔细看中间那个图,就可以看到参数列表最上面的是Argument n,最下面的是Argument 1,这也是从右向左压入栈的结果。

4. 宏编写的几个小技巧

如果需要编写一个有自己的作用域的宏,只用一个大括号括起来是解决不了问题的。比如这样

// A macro with scope, has problems

#define POW3(x,y) {int i; for(i=0; i

// Using POW3(), has problems

if (expression)

POW3(lNum, nPow);

else

(statement)if后面跟着一个大括号,然后括号后面又有一个分号,相当于一条空语句,那么后面的那个else就没有相应的if与之配对了。

解决办法是在大括号外面加上 do...while(0),形如 do {...} while(0)的结构,这样的话,就算是刚才那样的情况,后面分号也正好作为do...while的结束,而不会引起悬挂的else(Dangling else)出现。

在编写带有 if 的宏,稍不注意,也会有if和else不配对的情况出现,比如

// Macro containing if/else, has problems

#define ODS(s) if (bDebugging) OutputDebugString(#s)(提醒一点,#的作用是将后面的一串字符编程字符串,比如#s,在OutputDebugString(#s)里就相当于"s")

还用上面那个例子,把POW3换成ODS(s),根据else的就近原则,它就会跟宏里的if 匹配,出现了逻辑上的错误关系。

解决方法也很巧妙,就是在宏里面加上一个else与之配对,像下面这样

// Macro containing if/else, problem solved

#define ODS(s) if (!bDebugging) {} else OutputDebugString(#s)注意if里面的条件判断多了一个非,真正的执行放在了else里面。

使用宏定义的时候,需要注意的问题,引用WBCC作者的原话,就是:

Never conclude a macro with an ending brace.

When using a Macro, ending it with a Semicolon or a Block of Code也就是“不要让宏定义以右半括号结束”和“使用宏定义的时候,用分号结束或者用一个代码块包起来”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值