美文网首页
Effective C++学习笔记(Item2)

Effective C++学习笔记(Item2)

作者: 懒生活 | 来源:发表于2021-08-17 09:18 被阅读0次

    Item2 Prefer consts, enums, and inlines to #defines.

    使用预编译的缺点(#define语句的缺点)

    缺点1

    当你使用如下语句#define PI (3.14),预编译器会把全文出现PI的地方都替换成3.14。对编译器而言,他看不到PI,他看到的只是3.14。如果编译器在编译3.14所在语句发生了错误,编译器的提示语句也只会显示3.1,这对于定位问题很麻烦。上述是作者的意思。但实际情况已经不是这样了,用最新的g++5.4.0尝试下,会发现现在的编译器在发出编译告警中,明确的告诉你发生错误时的语句发生过宏替换,并且会告诉你宏定义在什么地方。先暂时认为作者用的可能是太旧的编译环境,才会有这个提示不全面的问题。

    #define PI ("abc")
    int main()
    {
        int a = PI; return 0;
    }
    

    使用g++ main.cpp -std=c++11编译报错,报错信息很详细,并没有作者说的找不着北的情况。

    缺点2:

    类似缺点1,因为编译器不知道宏定义的符号,所以在调试的时候,调试器不认识宏定义。这个是确实存在的,宏定义是没有办法调试。

    #define SWAP(x,y) \
        x= x+y;\
        y=x-y;\
        x=x-y;
    int main()
    {
        int a = 1,b =2;
        SWAP(a,b); return 0;
    }
    

    针对上述的代码,你是没有办法单步进入宏定义内部进行进一步调试的。

    缺点3:

    有些宏替换会潜藏着风险,尤其是带表达式如++,--之类的宏会让人不省心,经典的例子如下:

    #define MAX(a,b) a>b?a:b
    int main()
    {
        int a= 1, b =2;
    int maxone = MAX(a++,b);
    return 0;
    }
    

    这个例子中预编译器在替换的时候,替换出的结果是a++>b?a++:b,这个应该不是编程人员的本意。

    使用const取代宏定义常量

    使用const常量取代宏定义的好处

    1. 使用const常量就省去了预处理器进行替换的工作。就不会存在编译器不认识常量符号的问题。
    2. const定义的常量一般被编译器放到text区,特殊情况下也会放到data区,但不管放在哪里,只会放一份。反观宏定义,经过预处理器替换之后,在源代码中会出现多份该常量,编译器会在text区存放多份该常量。编译器可没有足够聪明能把这些重复的常量提炼成一个。至于什么是data区,text,bss区这个在引申章节中说明。
    3. const常量可以定义在类里面让该常量只在这个类里面生效,这个是宏定义不具有的封装性。

    使用const常量需要注意的地方

    归纳起来主要有两点

    • 定义指向常量的指针要用两个const
    • const变量的初始化时机

    定义指向常量的指针为什么需要两个const

    正确定义指向指针的常量方式如下const char * const authorName = "Scott Meyers"; 这种写法有两个const限定,一方面限定authorName是常量,编译器会禁止对他的赋值操作。另一方面限定了authorName指向的内容是常量,编译器会禁止诸如authorName[0] = 'x'通过指针篡改字符串的操作。 少任何一个const通常都是你不希望的限定缺失。

    const 变量的初始化时机

    这块作者直接上来介绍static const的初始化。对于一般人是有些难度的。这里这样子总结或许会更好理解:

    1. 对于non static const变量,初始化需要通过初始化列表进行。(一般网上查的资料都这么说,但是如果用c++11,实际上是支持在类声明中直接进行对const成员进行初始化的。而且对于non static const成员不管是什么类型都是可以直接在类中声明时赋值的。并不需要初始化列表的方式。举例如下
    class A
    {
        public:
        const string str = "123";
    }
    int main()
    {
        A a;
        cout<<a.str<<endl;
    }
    

    如果用g++ main.cpp的方式编译,编译器会编译失败,并提示不支持在这种初始化方式。 如果用g++ main.cpp -std=c++11编译是正常通过的。

    1. 对于static const变量如果是整形,支持在类声明时直接初始化。除此之外必须在类外面定义并初始化。通用的编写方法是这样的
    static const string A::CONSTSTR = "hello123";
    class A
    {
        public:
        static const string CONSTSTR;
    }
    int main()
    {
    cout<<A::CONSTSTR<<endl;
    return 0;
    }
    

    使用enum取代宏定义

    enum的使用类似于static const的使用。唯一不同的是编译器保证碰到enum的时候不会分配内存,而是一定只放在text区。
    作者还讲到enum是模板元编程的基础技术。这个在Item48中有讲述。

    使用inline取代宏定义

    inline函数在编译的时候会展开,这样在运行期间也能减少函数的调用开销。相对于宏来说,还更容易调试。

    item2的引申

    可执行文件的结构

    对于任何可执行的文件exe也好,.o文件也好,.out文件也好,在linux下你可以用size a.out的类似命令查看可执行文件a.out的结构组成。
    一个可执行文件至少包括3个区段。text区,data区和bss区。

    • text区存放的是编译后组成程序功能的一系列的可执行命令。
    • data区存放的是初始化后的全局变量,静态变量。这些有初始化的数据,执行文件需要记录下来。
    • bss区存放的是没有初始化的全局数据或静态数据。
      bss区因为没有初始值,为了减小可执行文件的大小,可执行文件可以只记录总共需要的bss区域大小就可以。当操作系统加载可执行文件的时候,只要根据这个大小,分配出空间,然后会自动初始化成0一次。

    可执行文件的加载过程

    在PC机上,可执行文件存放在硬盘,要执行的时候,必须先加载到内存中才能执行。(注意这和单片机系统不一样,单片机的可执行文件存放在ROM中,程序执行的时候可以从ROM中直接开始执行,不需要把所有的可执行代码放到RAM中。)这里只针对PC系统描述。
    加载大致过程:

    • 根据可执行文件上的bss标记(一般在header里面)为bss分配指定的大小。
    • 根据data区分配大小,并把所有初始化值复制到分配的空间上。
    • 把text区拷贝到内存
    • 系统为可执行文件分配栈
    • 系统开始跳转到内存中拷贝到的text区并执行第一条指令。
      系统为每个进程自动分配栈,默认栈大小取决于不同的操作系统。

    相关文章

      网友评论

          本文标题:Effective C++学习笔记(Item2)

          本文链接:https://www.haomeiwen.com/subject/cjubbltx.html