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 目前的构造函数执行顺序
- 开辟内存空间.
- 按照成员变量声明的顺序开始构造成员变量.
- 进入函数体, 执行语句.
二. 成员变量如何被构造
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_;
};
主要做了几个改动
- 给
Collar添加了一个带参构造函数. 便于和缺省构造函数进行区分. - 添加一个
拷贝构造函数.
// todo 还没有解释 - 添加一个
拷贝赋值运算符.
拷贝赋值运算符其实就是我们常用的"="(更准确的说是"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_;
};
主要做了以下改动:
- 修改了Dog自身的构造函数声明, 添加了一个参数.
- 在构造函数的函数体内, 将参数
collar赋值给成员变量collar_. - 由于本构造函数内, 会调用其他函数, 所以我们在函数体内最上方和最下方都打印了一条日志, 便于分析函数调用链.
修改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:
- 带参构造
- 缺省构造
- 拷贝赋值运算符
- 析构"缺省构造"
- 析构"带参构造"
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_;
};
主要做了以下改动:
- 在
Dog构造函数中, 添加初始化列表, 直接用myCollar来初始化collar_. - 既然
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.
三 构造函数执行顺序
- 开辟内存空间.
- 按照成员变量声明的顺序开始构造成员变量.
- 如果成员变量在初始化列表中, 就会执行该变量类型的拷贝构造函数.
- 如果成员变量没有在初始化列表中, 就会执行该变量类型的缺省构造函数.
- 进入函数体, 执行语句.
C++ 构造函数的执行过程(一) 无继承的更多相关文章
- JavaScript高级 面向对象(13)--构造函数的执行过程
说明(2017-4-2 21:50:45) 一.构造函数是干什么用的: 1. 初始化数据的. 2. 在js给对象添加属性用的,初始化属性值用. 二.创建对象的过程: 1. 代码:var p = new ...
- C#类继承中构造函数的执行序列
不知道大家在使用继承的过程中有木有遇到过调用构造函数时没有按照我们预期的那样执行呢?一般情况下,出现这样的问题往往是因为类继承结构中的某个基类没有被正确实例化,或者没有正确给基类构造函数提供信息,如果 ...
- (无)webservice执行过程深入理解
前面我们搞了1,2个DEMO,基本对webservice服务发布,调用 ,执行 有一定的了解. 今天的话,我们再系统的梳理下webservice执行过程. 首先我们在webservice服务器端开发w ...
- 反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) C#中缓存的使用 C#操作redis WPF 控件库——可拖动选项卡的TabControl 【Bootstrap系列】详解Bootstrap-table AutoFac event 和delegate的分别 常见的异步方式async 和 await C# Task用法 c#源码的执行过程
反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) 背景介绍: 为了平衡社区成员的贡献和索取,一起帮引入了帮帮币.当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮 ...
- 从编译,执行过程理解c#
上节我们说过C#所开发的程序源代码并不是编译成能够直接在操作系统上执行的二进制代码.与Java类似,它被编译成为中间代码,然后通过.NET Framework的虚拟机——被称之为通用语言运行时(CLR ...
- ASP.NET Web API 过滤器创建、执行过程(一)
ASP.NET Web API 过滤器创建.执行过程(一) 前言 在上一篇中我们讲到控制器的执行过程系列,这个系列要搁置一段时间了,因为在控制器执行的过程中包含的信息都是要单独的用一个系列来描述的,就 ...
- ASP.NET Web API 控制器执行过程(一)
ASP.NET Web API 控制器执行过程(一) 前言 前面两篇讲解了控制器的创建过程,只是从框架源码的角度去简单的了解,在控制器创建过后所执行的过程也是尤为重要的,本篇就来简单的说明一下控制器在 ...
- 通过源码了解ASP.NET MVC 几种Filter的执行过程
一.前言 之前也阅读过MVC的源码,并了解过各个模块的运行原理和执行过程,但都没有形成文章(所以也忘得特别快),总感觉分析源码是大神的工作,而且很多人觉得平时根本不需要知道这些,会用就行了.其实阅读源 ...
- JavaWeb之 Servlet执行过程 与 生命周期
Servlet的概念 什么是Servlet呢? Java中有一个叫Servlet的接口,如果一个普通的类实现了这个接口,这个类就是一个Servlet.Servlet下有一个实现类叫HttpServle ...
随机推荐
- 讨厌的Permission denied:adb访问手机目录时,怎么处理Permission denied问题
故事背景 手机某app出现了无响应,我想找到手机anr日志 但我只知道在data目录的某个目录里有个tra**的文件里有anr日志 具体的我真忘了,所以想要进入data中用ls查看一下 结果就出现了讨 ...
- jenkins自动化部署项目5 -- 系统管理-系统设置ssh配置
[系统管理]-[系统设置] 如果应用服务(前端后台)要部署在linux服务器上,我选择的是用ssh 为了jenkins登录远程登录linux服务器可以免密登录,先配置公钥和私钥: 我是在windows ...
- FlagCounter被封杀?自己实现一个简单的多国访客计数器
起因 前段时间发现博客右边的FlagCounter计数器突然没了,又看到了博客园封杀了FlagCounter的消息,有点摸不着头脑.于是上FlagCounter的网站上看了一眼,发现最近出现的来自新国 ...
- 【Python笔记】Python变量类型
Python 变量类型 变量存储在内存中的值.这就意味着在创建变量时会在内存中开辟一个空间. 基于变量的数据类型,解释器会分配指定内存,并决定什么数据可以被存储在内存中. 因此,变量可以指定不同的数据 ...
- 从SpringMVC获取用户信息谈起
Github地址:https://github.com/andyslin/spring-ext 编译.运行环境:JDK 8 + Maven 3 + IDEA + Lombok spring-boot: ...
- 一个简单的MyBatis项目
1.log4j.properties,我们把它设为debug级别,以便于调试.生产环境可以设为INFO,本项目放在src下面: # Global logging configuration log4j ...
- zookeeper 都有哪些使用场景?
面试题 zookeeper 都有哪些使用场景? 面试官心理分析 现在聊的 topic 是分布式系统,面试官跟你聊完了 dubbo 相关的一些问题之后,已经确认你对分布式服务框架/RPC框架基本都有一些 ...
- gulp简单使用
1.安装gulp,由于某些在下不能解决的原因,故使用gulp 3.9.1版本 安装命令: npm install gulp@3.9.1 注意不要直接使用 : npm install gulp 安装,直 ...
- selenium实现百度图片爬取
因为是百度图片是瀑布流ajax异步上传的数据,所以这里用到抓包工具来抓取链接(fiddler) 好了直接上代码, from selenium import webdriver from seleniu ...
- Java 学习笔记之 Error和Exception的联系
Error和Exception的联系: Error和Exception的联系 继承结构:Error和Exception都是继承于Throwable,RuntimeException继承自Excepti ...