Effective C++ 阅读心得-条款02:尽量以const, enum, inline替换 #define

重要的话写在前面

  • 对于单纯常量,最好以const对象或enums替换#defines。
  • 对于形似函数的宏(macros),最好改用inline函数替换#defines。

1. 对于常量,为什么最好以const对象或enums替换#defines

宏定义(Macro Definition)是 C++ 中一种预处理器(Preprocessor)指令,用于在编译前将某个标识符替换为指定的文本。宏定义的主要作用是在程序中定义一些常量、函数、条件编译等预处理任务,以便简化程序代码、提高代码的可读性和可维护性。

1.1 #define在出现编译错时误产生的迷惑

  • #define的缺点:通过 #define 定义一个常量,可以提高代码的执行效率(其实和const定义常量差不多快)。例如:#define PI 3.14159。上文提到宏定义是在编译前将某个标识符替换为指定文本,那么上述的宏定义中PI意味着从来就没出现在C++编译器记号表(此概念见附录)中。这意味着当你使用PI时如果出现了编译错误,编译器并不会告诉你是PI出现的问题,而只会给丢给你一个3.14159,因为编译前PI已经被3.14159代替啦。这时候,如果你在庞大的代码海中要定位到3.14159应该并不容易。
    那如何解决上面的问题呢?
  • const常量代替#defineconst double PI = 3.14159,此时,PI会进入C++编译器记号表,编译器会看见它,避免了上面让人摸不到头脑的事情;const常量则是一个真正的变量,可以在程序中输出它的值,以便调试和定位问题。

1.2 #define定义不会被编译器进行安全检查

  • 宏定义不会被编译器检查,因此可能会存在语法错误或类型不匹配的问题。相比之下,const常量会被编译器类型检查,并在编译时发现潜在的错误。

1.3 #define定义常量会占用更多内存

  • 宏定义只是简单的文本替换,因此可能会导致代码重复,因此占用更多的内存(宏定义本身不占用内存,只是在文本替换后,可能会导致代码重复),以及在调试时难以定位问题(上一个问题提到过)。
  • 相比之下,const常量会在程序的数据段中分配内存,并可以被多个代码块共享,从而减少代码重复和内存使用。

因此,在大多数情况下,使用const定义常量是更好的选择,因为它具有更好的可读性和可维护性

1.4 为什么const定义常量具有更好的可读性和可维护性

  • const 关键字可以将常量的类型和作用域限定在一个特定的作用域内,而宏定义则是一种简单的文本替换机制,不能对常量的类型和作用域进行限制。

1.5 使用const定义常量的注意事项

  • 对于头文件中的常量指针的注意:常量定义一般都被放在头文件内,所以对于常量指针的定义有必要将指针本身也使用const修饰,可以有效防止在程序运行期间意外地修改指针的地址,从而保证程序的正确性,例如:const int* const ptr = &value
  • 对于class中的专属常量:如果想让一个常量的作用域被限制在class中,并且这份常量只有一份实体,也就是需要为class声明一份专属常量,那么必须在这个常量前加上static:
class MyClass {
public:
	//下面这个定义只是常量的声明式,如果编译器一定要提供一个定义式,那可以在实现文件中给出定义而不需赋值
    static const int MAX_VALUE = 100; // 定义类的专属常量
    //如果编译器不允许在声明式中赋值,那需要在实现文件中给出定义去赋值,但此时你的类在编译期需要一个常量呢,如下面的使用。
    int numbers[MAX_VALUE];//使用常量
};
  • 然而#define不能用来定义class的专属常量,因为上文提到,#define并不限定作用域,也不会提供任何的封装特性。

1.6 使用enum充当常量ints使用

  • 在1.5的例子中,如果你希望在class中使用专属常量,但编译器不允许你对这个常量进行声明时获得初值,你可以使用一个枚举类型enum代替,这被称作‘the enum hack’。更换如下:
class MyClass {
public:
	enum { MAX_VALUE = 100 };//使用enum代替专属常量
    int numbers[MAX_VALUE];//使用常量
};

1.7 另外使用enum的优点

  • 如果你在进行某些程序设计时,不希望用户使用指针获取到你定义常量的地址(条款18:撰写代码时的某些决定实施设计上的约束条件),可以将其定义为enum,因为取一个enum的地址是不合法的,这与#define有异曲同工之妙。
  • enum#define一样不会导致编译器的非必要内存分配,而整数型const有时候会被(不优秀的)编译器分配额外的存储空间。

1.8 #define也不是一无是处

  • 宏定义可以根据不同的编译器或不同的编译选项定义不同的常量值,这在一些特定的情况下是非常有用的。例如,可以使用宏定义在调试模式下定义不同的常量值,在发布模式下定义另外的常量值,以便于程序的调试和发布。

2. 对于形似函数的宏,为什么最好以inline函数替换#defines

2.1 宏定义有什么好的,非要用它

  • 可以避免函数调用的开销:宏定义函数是在编译时展开的,不会像普通函数一样在运行时调用,从而避免了函数调用的开销,可以提高程序的执行效率。例如,定义一个计算平方的宏:
#define SQUARE(x) ((x) * (x))

int a = 5;
int b = SQUARE(a); // 编译时会被展开为 int b = ((a) * (a));
  • 灵活性:宏定义函数可以定义任意的参数类型和数量,使得程序员能够根据需要自由地定义宏,增加程序的灵活性和可扩展性。例如,定义一个宏来交换两个变量的值:
#define SWAP(x, y) do { typeof(x) _temp = (x); (x) = (y); (y) = _temp; } while(0)

使用do-while循环可以确保宏定义可以安全地在所有上下文中使用,包括if-else语句和其他控制结构。循环将执行一次,不管while语句中的条件如何,因此可以避免任何可能的括号缺失或意外行为。

  • 简洁性:宏定义函数可以让代码更加简洁易读,避免了重复的代码,提高了代码的可维护性。例如,定义一个宏来打印调试信息:
#ifdef DEBUG
#define DEBUG_PRINT(fmt, args...) fprintf(stderr, fmt, ##args)
#else
#define DEBUG_PRINT(fmt, args...)
#endif

这个宏可以根据定义的 DEBUG 宏来控制是否打印调试信息,避免了代码中大量的 if 判断,从而提高了代码的可读性和可维护性。

  • 可以避免一些由于函数调用引起的问题:例如传递参数的副本、返回值的处理等等。举个例子:定义一个宏来判断是否是奇数:
#define IS_ODD(x) ((x) & 1)

这个宏不会改变 x 的值,也不需要返回值,从而避免了一些由函数调用引起的问题。

2.2 使用宏有什么隐患么,非要使用inline替换它

  • 样子不好看:宏定义的模样让人很不适应,就是光想想就头痛
  • 定义一个好宏需要绞尽脑汁:想要定义一个非常棒的宏,不是那么简单的事情,其需要很多需要注意的点,才能保证一个优雅的,鲁棒性高的宏定义。你看看2.1那个例子,真下头。
  • 最重要的是可能会出错!:看看下面这个例子
#define DOUBLE(x) ((x) + (x))

int main() {
    int a = 5;
    int b = DOUBLE(a++);//a加了两次,不是我想要的!
    printf("%d\n", b);
    return 0;
}

那既然宏定义最主要是为了在编译时展开,使其不受调用函数之苦,那么相同作用的inline函数可以作为其良好的代替品。

2.3 inline函数的作用与优点,会了不用看

  • inline函数是一种C++中的函数特性,用于告诉编译器在编译时将函数的定义插入到每个调用该函数的地方,而不是在运行时执行函数调用。这样可以减少函数调用的开销,提高程序的执行效率。

inline函数的优点如下:

  1. 函数调用开销小:函数调用会有一定的开销,包括参数传递、栈帧的建立和销毁等。将函数定义插入到调用点可以避免这些开销,提高程序的执行效率。
  2. 代码可读性好:使用inline函数可以将代码的可读性提高,因为函数的定义被插入到调用点,可以更方便地看到函数的执行流程。
  3. 编译器优化更好:由于函数的定义被插入到调用点,编译器可以更好地进行优化,例如消除函数的局部变量、减少内存访问等,进一步提高程序的执行效率。

2.4 inline替换#define

对2.2的例子进行替换:

inline int DOUBLE(int x) {
    return x + x;
}

ok,替换很简单,但又出现个问题,我现在相对不同类型实现DOUBLE操作呢?

最终版template inline替换

如你所想template可以完成上述效果:

template<typename T>
inline T DOUBLE(T x) {
    return x + x;
}

打完收工!

附录

  • C++编译器记号表:C++ 编译器的记号表(Symbol Table)是一个数据结构,用于存储程序中定义的标识符(identifier)及其相关信息。标识符是指变量、函数、类、命名空间、枚举等各种程序实体的名称,每个标识符都有其自己的属性和值。
    记号表通常是一个符号表(Symbol Table),其中每个条目代表一个标识符,包含了该标识符的名字、类型、作用域、内存位置等属性信息。编译器通过分析程序代码并构建记号表来维护标识符的信息,以便在后续的编译、优化和链接过程中使用。
    **在编译过程中,编译器通过记号表来检查程序中的语法和语义错误,以及优化程序的性能。**同时,在链接过程中,编译器还需要根据记号表中的信息来生成目标代码,并将不同源文件中的同名标识符进行解析和合并。
    总之,记号表是 C++ 编译器重要的数据结构之一,它提供了一个高效的方式来维护程序中定义的标识符及其相关信息,保证了程序的正确性和性能。
  • 在定义宏时,应该注意以下几点:1. 宏名应该以大写字母命名,以便于和其他标识符区分。2. 宏值可以是任意的表达式,但应该保证表达式的正确性和可读性。3. 宏定义应该尽量避免副作用和代码,以免引入不必要的问题。4. 在宏定义中应该尽量使用括号,以保证运算的优先级。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值