C语言编程规范 — 宏

一、宏的命名规则建议

规则1:对于数值或者字符串等常量的定义,建议采用全大写的英文字母,单词之间加下划线‘_’的方式命名(枚举常量同样建议使用此方式定义)。

示例:

#define PI_ROUNDED 3.14
#define LOG_LEVEL_DEBUG 1
#define LOG_LEVEL_INFO  2
#define LOG_LEVEL_WARN  3
#define LOG_LEVEL_ERROR 4
#define LOG_LEVEL_FATAL 5
#define LOG_LEVEL_HIGHLIGHT 6
#define LOCAL_IP "127.0.0.1"

二、宏的使用规范

规则1  用宏定义表达式时,要使用完备的括号。

说明:因为宏只是简单的文本替换,不会像函数那样先将参数计算后,再传递。

示例:下面的宏定义都存在一定的风险。

#define RECTANGLE_AREA(a, b)  a * b
#define RECTANGLE_AREA(a, b)  (a * b)
#define RECTANGLE_AREA(a, b)  (a) * (b)

正确的定义应为:

#define RECTANGLE_AREA(a, b)  ((a) * (b))

这是因为:如果定义 #define RECTANGLE_AREA(a, b)  a * b 或 #define RECTANGLE_AREA(a, b) (a * b),则 c / RECTANGLE_AREA(a, b) 将扩展成 c / a * b,c 与 b 本应该是除法运算,结果却变成了乘法运算,造成错误。

如果定义 #define RECTANGLE_AREA(a, b)  (a) * (b) 则 RECTANGLE_AREA(c + d, e + f) 将扩展成:(c + d * e + f),d 与 e 先运算,造成错误。

规则2  将宏所定义的多条表达式放在大括号中。

说明:更好的方法是将多条语句写成 do-while(0) 的方式。

示例:

#define SWAP(x,y) do { \
	(x)->buffer = (y)->buffer; \
	(x)->orig_buffer = (y)->orig_buffer; \
	(x)->misalign = (y)->misalign; \
	(x)->totallen = (y)->totallen; \
	(x)->off = (y)->off; \
} while(0)

规则3  使用宏时,不允许参数发生变化。

示例:如下用法可能导致错误。

#define SQUARE(a)  ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++);  //结果:a = 7,即执行了两次自增加1

正确的做法是:

b = SQUARE(a);
a++;  //结果:a = 6,即只执行了一次自增加1

建议4  除非必要,应尽可能使用函数代替宏。

说明:宏对比函数,有一些明显的缺点:

(1)宏缺乏类型检查,不如函数调用检查严格。

(2)宏展开可能会产生意想不到的副作用,如 #define SQUARE(a)  ((a) * (a)) 这样的定义,如果是 SQUARE(i++),就会导致 i 被加两次;如果是函数调用 double square(double a) { return a * a; } 则不会有此副作用。

(3)以宏形式写的代码难以调试难以打断点,不利于定位问题。

(4)宏如果调用得很多,会造成代码空间的浪费,不如函数空间效率高。

示例:下面的代码无法得到想要的结果:

#define MAX_MACRO(a, b)  ((a) > (b) ? (a) : (b))
int MAX_FUNC(int a, int b)
{
	return ((a) > (b) ? (a) : (b));
}
int testFunc()
{
	unsigned int a = 1;
	int b = -1;
	printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b);
	printf("FUNC: max of a and b is: %d\n", MAX_FUNC(++a, b));
	return 0;
}

上面的宏代码调用中,结果是(a < b),所以 a 只加了一次,所以最终的输出结果是:

MACRO: max of a and b is: -1

FUNC: max of a and b is: 2

这是因为宏是没有类型检查的,在宏的展开表达式中,((++a) > (b) ? (++a) : (b)) 由于 a 是无符号整数,b 是有符号整数,在编译阶段,编译器会先将 b 转换为无符号整数类型,然后再参与表达式运算,所以是(a < b)。

-1 在内存中是以补码的形式存放的,在4字节的int类型中,即为:0xFFFF FFFF FFFF FFFF,当转换为无符号整数类型时,表示的就是无符号整数的最大值。

建议5  常量建议使用 const 定义代替宏常量。

说明:“尽量使用编译器而不用预处理”,因为 #define 经常被认为好像不是语言本身的一部分。看下面的语句:

#define ASPECT_RATIO  1.653

编译器是永远也看不到 ASPECT_RATIO 这个符号名的,因为在源码进入编译器之前,它会被预处理器去掉,于是 ASPECT_RATIO 不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是 1.653,而不是 ASPECT_RATIO。如果 ASPECT_RATIO 不是在你自己写的头文件中定义的,你就会奇怪 1.653 是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。

解决这个问题的方案很简单:不用预处理宏,定义一个 const 常量:

const double ASPECT_RATIO = 1.653;

这种方法很有效,但是有两个特殊情况要注意。首先,定义指针常量时会有所不同。因为定义常量一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成 const 外,重要的是指针也要经常定义成 const,即指针的指向也是不能改变的。例如,要在头文件中定义一个基于 char* 的字符串常量,你要写两次 const:

const char * const authorName = "Scott Meyers";

<说明> authorName指针是一个指向常量的常指针,即指针变量指向一个固定的变量,该变量的值不能改变(指的是不能通过指针变量改变该变量的值)。被 const 修饰的变量本质含义是只读变量,即变量的值不能被修改,否则会编译报错,所以可以理解为常量,它与字面常量和宏定义的符号常量还是有区别的,区别就是在编译阶段编译器会对 const 常量进行类型检查。

建议6  宏定义中尽量不要使用 return、goto、continue、break 等改变程序流程的语句。

说明:如果在宏定义中使用这些改变流程的语句,很容易引起内存或资源泄漏问题,使用者很难自己察觉。

示例:在某头文件中定义宏 CHECK_AND_RETURN

#define CHECK_AND_RETURN(cond, ret) { if (cond == NULL_PTR) { return ret; }}

然后在某函数中使用(只说明问题,代码并不完整):

pMem1 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem1, ERR_CODE_XXX);
pMem2 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem2, ERR_CODE_XXX);  //此时如果pMem2==NULL_PTR,则pMem1未释放堆内存,函数就返回了,造成内存泄漏

所以说,类似于 CHECK_AND_RETURN 这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。

参考

do {...} while (0) 的用途汇总

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值