• C++ 构造函数的执行过程(一) 无继承


    引言

    C++ 构造函数的执行过程(一) 无继承
    本篇介绍了在无继承情况下, C++构造函数的执行过程, 即成员变量的构建先于函数体的执行, 初始化列表的数量和顺序并不对构造函数执行顺序造成任何影响.
    还指出了初始化列表会影响成员变量的构造方式, 分析了为何要尽可能地使用初始化列表.

    关于在继承的情况下, C++构造函数的执行过程, 请期待第二篇.

    本文所依赖的环境如下:

    平台: Windows 10 64位

    编译器: Visual Studio 2019

    一. 构造函数的执行顺序

    1.1 声明一个类

    首先我们声明一个类:

    // Dog.h
    class Dog;
    

    如果我们创建一个该类的实例:

    // main.cpp
    Dog myDog = Dog( );
    

    那么编译器会申请一块内存空间, 并调用Dog的构造函数, 构造这个实例.

    1.2 添加构造函数

    我们一点点补全这个类.

    在这个类中, 添加一个构造函数, 一个析构函数.

    在函数体内, 各打印一条日志, 方便我们在调试的过程中, 知道执行的顺序.

    // Dog.h
    class Dog
    {
    public:
      Dog( )
      {
        std::cout << "Dog构造函数函数体"<< std::endl;
      }
      ~Dog( ) { }
    };
    

    现在再次执行:

    // main.cpp
    std::cout << "Dog构造函数 开始" << std::endl;
    Dog myDog = Dog( );
    std::cout << "Dog构造函数 结束" << std::endl;
    std::cout << "程序即将结束" << std::endl;
    

    程序会打印出日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Dog构造函数函数体
    3. Dog构造函数 结束
    4. 程序即将结束
    

    1.3 添加成员变量

    文明养狗, 每只狗都应该有自己的项圈.

    我们给Dog添加一个项圈collar属性.

    注: 为了方便验证, 我们让collar也是一个类的实例, 原因在于, 我们需要让这个属性在构造的时候, 打印出一条日志, 这样我们才能判断出它是在何时被构造的.

    // Collar.h
    class Collar
    {
    public:
      // 缺省构造函数
      Collar( )
      {
        std::cout << "Collar缺省构造函数" << std::endl;
      }
    };
    

    现在我们在Dog中添加整个成员变量:

    // Dog.h
    class Dog
    {
    public:
      Dog( )
      {
        std::cout << "Dog构造函数函数体<< std::endl;
      }
      ~Dog(){ }
    private:
      Collar collar_;
    };
    

    现在再次执行:

    // main.cpp
      std::cout << "Dog构造函数 开始" << std::endl;
      Dog myDog = Dog(myCollar);
      std::cout << "Dog构造函数 结束" << std::endl;
      std::cout << "程序即将结束" << std::endl;
    

    程序会打印出日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Collar缺省构造函数
    3. Dog构造函数函数体
    4. Dog构造函数 结束
    5. 程序即将结束
    
    目前的结论:

    在创建一个类的实例的时候, 会先构造出它的成员变量, 然后才会执行它的构造函数函数体的语句.

    观察上面的代码, 我们并没有在任何地方, 显式的调用Collar的构造函数, 也就是说:

    编译器帮你完成了Collar构造函数的调用.

    但是, 如果这个类, 不止有一个成员变量, 那么编译器先构造哪个成员变量呢?

    1.4 成员变量的构造顺序

    现在, 我们给狗狗一个玩具.

    // Toy.h
    class Toy
    {
    public:
      // 缺省构造函数
      Toy( )
      {
        std::cout << "Toy缺省构造函数" << std::endl;
      }
    };
    

    Dog添加一个玩具Toy属性.

    // Dog.h
    class Dog
    {
    // 构造和析构与1.3相同, 在此省略
    private:
      Collar collar_;
      Toy toy_;
    };
    

    现在执行程序, 得到日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Collar缺省构造函数
    3. Toy缺省构造函数
    4. Dog构造函数函数体
    5. // 其余日志与1.3相同, 在此省略
    

    可以看到, 我们在class Dog的声明中, 先声明了Collar, 再声明了Toy, 实际执行过程, 就是先调用了Collar缺省构造函数, 再调用了Toy缺省构造函数.

    如果修改为:

    // Dog.h
    class Dog
    {
    // 构造和析构与1.3相同, 在此省略
    private:
      Toy toy_; // 调换了位置
      Collar collar_; // 调换了位置
    };
    

    日志也会变成:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Toy缺省构造函数
    3. Collar缺省构造函数
    4. Dog构造函数函数体
    5. // 其余日志与1.3相同, 在此省略
    
    目前的结论:

    类的成员变量, 是按照类的定义中, 成员变量的声明顺序进行构造的. 且构造都早于类构造函数的函数体.

    1.5 初始化列表的顺序, 不影响成员变量构造顺序

    我们将对初始化列表做3个测试.
     

    测试1: 初始化列表的顺序 和 成员变量声明顺序一致.
    // Dog.h
    class Dog
    {
    public:
      Dog(const Collar& myCollar, const Toy& myToy)
        : collar_(myCollar)
        , toy_(myToy)
      {
        std::cout << "Dog构造函数函数体开始"<< std::endl;
        std::cout << "Dog构造函数函数体结束" << std::endl;
      }
    private:
      Collar collar_;
      Toy toy_;
    };
    

    现在执行程序, 得到日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Collar缺省构造函数
    3. Toy缺省构造函数
    4. Dog构造函数函数体
    5. // 其余日志与1.3相同, 在此省略
    
    测试2: 初始化列表的顺序 和 成员变量声明顺序不一致.
    // Dog.h
    class Dog
    {
    public:
      Dog(const Collar& myCollar, const Toy& myToy)
        : toy_(myToy)
        , collar_(myCollar)
      {
        std::cout << "Dog构造函数函数体开始"<< std::endl;
        std::cout << "Dog构造函数函数体结束" << std::endl;
      }
    private:
      Collar collar_;
      Toy toy_;
    };
    

    现在执行程序, 得到日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Collar缺省构造函数
    3. Toy缺省构造函数
    4. Dog构造函数函数体
    5. // 其余日志与1.3相同, 在此省略
    

    日志没有任何变化.

    测试3: 初始化列表中的数量少于成员变量的数量.
    // Dog.h
    class Dog
    {
    public:
      Dog(const Collar& myCollar, const Toy& myToy)
        : collar_(myCollar)
        // 删除了toy_(myToy)
      {
        std::cout << "Dog构造函数函数体开始"<< std::endl;
        std::cout << "Dog构造函数函数体结束" << std::endl;
      }
    private:
      Collar collar_;
      Toy toy_;
    };
    

    现在执行程序, 得到日志:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Dog构造函数 开始
    2. Collar缺省构造函数
    3. Toy缺省构造函数
    4. Dog构造函数函数体
    5. // 其余日志与1.3相同, 在此省略
    

    日志没有任何变化.

    目前的结论:

    初始化列表的数量和顺序, 均不影响成员变量构造顺序.

    构造顺序仍然是按照类的定义中, 成员变量的声明顺序进行构造的. 且构造都早于类构造函数的函数体.

    1.6 目前的构造函数执行顺序

    1. 开辟内存空间.
    2. 按照成员变量声明的顺序开始构造成员变量.
    3. 进入函数体, 执行语句.

    二. 成员变量如何被构造

    2.1 在构造函数体内, 给成员变量赋值

    现在, 我们显示的指定collar的构造, 给Collar添加另一个构造函数:

    // Collar.h
    class Collar
    {
    public:
      // 缺省构造函数
      Collar( )
      {
        std::cout << "Collar缺省构造函数" << std::endl;
      }
    
      // 含参构造函数
      Collar(std::string color)
      {
        std::cout << "Collar含参构造函数" << std::endl;
        color_ = color;
      }
    
      // 拷贝构造函数, 这里直接使用了const引用, 是出于性能考虑. 如果用值拷贝, 会多构造一个collar出来, 然后再析构它.
      Collar(const Collar& collar)
      {
        std::cout << "Collar拷贝构造函数" << std::endl;
        this->color_ = collar.color_;
      }
    
      // 拷贝赋值运算符
      Collar& operator = (const Collar& collar)
      {
        std::cout << "Collar拷贝赋值运算符" << std::endl;
        this->color_ = collar.color_;
        return *this;
      }
    
      // 析构函数
      ~Collar()
      {
        std::cout << "Collar析构函数" << std::endl;
      }
      
    private:
      std::string color_;
    };
    

    主要做了几个改动

    1. Collar添加了一个带参构造函数. 便于和缺省构造函数进行区分.
    2. 添加一个拷贝构造函数.
      // todo 还没有解释
    3. 添加一个拷贝赋值运算符.
      拷贝赋值运算符其实就是我们常用的"="(更准确的说是"operator ="), 它存在于所有的类中, 当你在执行dog1 = dog2;的时候, 就是调用了这个函数来完成的赋值工作.
      不管你在类的定义中, 有没有定义这个"operator ="函数, 你都可以使用它, 因为编译器已经帮助你自动合成了它.
      C++允许用户自己对"operator ="进行重载, 在这段代码中, 我重载了这个函数, 额外添加了一条日志.

    修改Dog的构造函数:

    // Dog.h
    class Dog
    {
    public:
      Dog(const Collar& myCollar)
      {
        std::cout << "Dog构造函数 函数体开始"<< std::endl;
        // 将参数`collar`赋值给成员变量`collar_`
        collar_= collar;
        std::cout << "Dog构造函数 函数体结束" << std::endl;
      }
      
      ~Dog(){ }
      
    private:
      Collar collar_;
    };
    

    主要做了以下改动:

    1. 修改了Dog自身的构造函数声明, 添加了一个参数.
    2. 在构造函数的函数体内, 将参数collar赋值给成员变量collar_.
    3. 由于本构造函数内, 会调用其他函数, 所以我们在函数体内最上方和最下方都打印了一条日志, 便于分析函数调用链.

    修改main.cpp

      Collar myCollar = Collar("yellow");
      std::cout << "Dog构造函数 开始" << std::endl;
      Dog myDog = Dog(myCollar);
      std::cout << "Dog构造函数 结束" << std::endl;
      std::cout << "程序即将结束" << std::endl;
    

    实际运行后打印的日志如下:

    // 日志, 每行开头的数字序号, 是我手动添加的, 数字后才是真实的日志.
    1. Collar含参构造函数
    2. Dog构造函数开始
    3. ----Collar缺省构造函数
    4. ----Dog构造函数函数体开始
    5. --------Collar拷贝赋值运算符
    6. ----Dog构造函数函数体结束
    7. Dog构造函数结束
    8. 程序即将结束
    9. Collar析构函数"
    10. Collar析构函数"
    

    但是第二行日志指出, 编译器还是帮你完成了Collar缺省构造函数的隐式调用, 并且该调用早于Dog构造函数的调用.

    > 第一条日志, 调用`Collar`的含参构造函数, 构造出一个对象.
    > 第二条日志, 标志着程序开始调用`Dog`构造函数.
    > 第三条日志, 调用成员变量的`Collar`缺省构造函数, 将`collar_`构造出来.
    > 第四条日志, 进入`Dog`的构造函数的函数体.
    > 第五条日志, 调用拷贝赋值运算符, 将参数`myCollar`赋值给成员变量`collar_`;
    > 第六条日志, `Dog`的构造函数的函数体结束.
    > 第七条日志, 标志着`Dog`构造函数彻底结束.
    > 第八条日志, 标志着程序即将结束, 开始进入析构阶段.
    > 第九条日志, 在析构`Dog`实例的过程中, 会析构成员变量`collar_`, 执行`Collar`的析构函数.
    > 第十条日志, 仍然是程序结束阶段, 会析构第一步建立的`myCollar`, 执行`Collar`的析构函数.
    
    总结一下:

    在构造Dog实例的过程中, 总共有5个步骤涉及了Collar:

    1. 带参构造
    2. 缺省构造
    3. 拷贝赋值运算符
    4. 析构"缺省构造"
    5. 析构"带参构造"

    2.2 问题在哪里?

    在刚才总结出的5个步骤中, 第2和3步, 存在浪费.

    现在我们单独看这两步:

    第一步: 先使用缺省构造, 构造出collar_对象.
    这个缺省构造过程中, 如果collar_是一个很复杂的对象, 我们假设它包含了多个成员变量, 且每个成员变量要么是类的对象, 要么是结构体.
    这个缺省构造, 将花费很多时间, 将每一个成员变量正确构造出来, 给它们一个默认值, 记住, 默认值通常都是没用的, 比如是'0'或者'nullptr'.

    紧接着, 进入第二步, 拷贝赋值运算符:
    在这个步骤之前, 我们已经将myCollar作为参数传递了进来, 这个myCollar早就已经构造完成了, 它所有的成员变量的值都是正确的且有意义的, 现在我们把它复制给collar_, 完成对collar_的创建, 其中collar_的默认值, 被一一覆盖.

    现在你可能意识到了问题:

    第一步的默认值完全是多余的!

    我们需要执行第一步的前半部分, 将collar_对象构造出来.
    但是我们不需要第一步的后半部分, 不需要默认值.
    我们直接使用第二步, 将myCollar的值, 拷贝给collar_就行了.

    2.3 使用初始化列表

    我们仅仅对Dog.h进行一些修改:

    // Dog.h
    class Dog
    {
    public:
      Dog(const Collar& myCollar)
        : collar_(myCollar)
      {
        std::cout << "Dog构造函数函数体开始"<< std::endl;
        std::cout << "Dog构造函数函数体结束" << std::endl;
      }
    
      ~Dog(){ }
    
    private:
      Collar collar_;
    };
    

    主要做了以下改动:

    1. Dog构造函数中, 添加初始化列表, 直接用myCollar来初始化collar_.
    2. 既然collar_已经初始化了, 函数体内的拷贝赋值运算符就可以删掉了.

    其他内容保持不变, 执行:

    1. Collar含参构造函数
    2. Dog构造函数开始
    3. Collar拷贝构造函数
    4. Dog构造函数函数体开始
    5. Dog构造函数函数体结束
    6. Dog构造函数结束
    7. 程序即将结束
    8. Collar析构函数"
    9. Collar析构函数"
    

    对比上一次的日志可以发现:

    本次运行使用了初始化列表, Collar拷贝构造函数一个步骤, 替代了上次运行的Collar缺省构造函数+拷贝赋值运算符两个步骤.

    避免了Collar缺省构造, 也就避免了多余的默认值.

    目前的结论:

    对于一个类的成员变量, 一定会在进入该类的构造函数之前构造完成.
    如果成员变量在初始化列表中, 就会执行该变量类型的拷贝构造函数.
    如果成员变量没有在初始化列表中, 就会执行该变量类型的缺省构造函数.

    2.4 尽可能地使用初始化列表

    使用初始化列表, 首要原因是性能问题.

    按照我们刚才的分析, 如果不使用初始化列表, 而是用构造函数函数体来完成初始化, 会额外调用一次缺省构造.

    对于内置类型, 如int, double, 在初始化列表和在构造函数函数体内初始化, 性能差别不是很大, 因为编译器已经进行了优化.

    但是对于类类型, 性能差别可能是巨大的, 数倍的.

    另一个原因是, 有一些情况必须使用初始化列表:

    • 常量成员, 因为常量只能初始化不能赋值, 所以必须放在初始化列表里面.

    • 引用类型, 引用必须在定义的时候初始化, 并且不能重新赋值, 所以也要写在初始化列表里面.

    • 没有默认构造函数的类类型, 因为使用初始化列表可以不必调用缺省构造函数来初始化, 而是直接调用拷贝构造函数初始化.

    注: 对于还不知道具体值的变量, 使用零值或没有具体含义的值, 比如int类型使用0, std::string类型使用"", 指针类型使用nullptr.

    三 构造函数执行顺序

    1. 开辟内存空间.
    2. 按照成员变量声明的顺序开始构造成员变量.
      • 如果成员变量在初始化列表中, 就会执行该变量类型的拷贝构造函数.
      • 如果成员变量没有在初始化列表中, 就会执行该变量类型的缺省构造函数.
    3. 进入函数体, 执行语句.
  • 相关阅读:
    Java NIO与IO
    linux命令
    windows的定时任务设置
    《软硬件接口》课程大纲
    使用SSIS对Dynamics CRM 系统进行数据迁移
    数据库设计中的14个技巧
    背景建模或前景检測之PBAS
    Leetcode 树 Populating Next Right Pointers in Each Node II
    QCon大会上推荐阅读的10本书
    cocos2d-x3.0 Slider
  • 原文地址:https://www.cnblogs.com/silenzio/p/11766609.html
Copyright © 2020-2023  润新知