[c++] c++ 中的顺序(构造,析构,初始化列表,继承)

对象构造的时候,对象成员变量的初始化顺序是什么样的 ?

派生类构造的时候,先构造基类还是先构造派生类 ?

构造函数中的初始化列表,初始化的顺序是列表的顺序吗 ?

析构的时候,析构的顺序是什么样的 ?

本文通过实例代码记录上述场景下的顺序。先说结论:

(1)全局对象以及静态全局对象,在 main 函数调用之前创建

(2)构造时,先构造类中的成员变量,再调用构造函数

(3)类中成员变量的构造顺序是变量在类中声明的顺序

(4)初始化列表中,成员变量的初始化顺序也是变量在类中声明的顺序

(5)对于派生类来说,先构造基类,再构造派生类

(6)对于多重继承的场景,基类构造的顺序是继承列表的顺序

(7)多重继承中如果有虚拟继承,那么先构造虚拟基类

(8)析构的顺序与构造的顺序是相反的

1 静态变量在 main 函数调用之前创建

main 函数是我们应用中第一个执行的函数,但却不是系统加载应用时第一个调用的函数。在 main 函数正式运行前,最基础的这个应用的环境变量如果需要配置则要进行配置;应用入参如果有需要也应该把入参放到规定的位置;对于全局变量和静态全局变量,也是在 main 函数初始化之前构造的。

#include <iostream>
#include <string>
#include <unistd.h>class Test1 {
public:Test1() {std::cout << "Test1()" << std::endl;}~Test1() {std::cout << "~Test1()" << std::endl;}
};class Test2 {
public:Test2() {std::cout << "Test2()" << std::endl;}~Test2() {std::cout << "~Test2()" << std::endl;}private:static Test1 t1;
};Test1 Test2::t1;Test1 t1;
static Test2 t2;void Do() {std::cout << "Do()" << std::endl;static Test1 t1;
}int main() {std::cout << "main" << std::endl;sleep(2);static Test1 t1;Do();Do();return 0;
}

运行结果如下,从结果中还可以看出,对于局部静态变量来说,是在函数第一次调用的时候构造的。这也体现了懒加载的思想,局部静态变量在第一次函数调用之前,根本就没有用,直接构造这个对象是浪费时间和内存资源。

 
使用 gdb 对 Test1 的构造函数设置断点,可以看到 Test1 的调用栈。

2 构造顺序和析构顺序

对于一个没有继承其它类的类来说,构造顺序有以下几点:

(1)构造的时候先构造类的成员变量,再调用构造函数。这也可以理解,符合我们的使用习惯,因为构造函数中可以使用这个对象中的成员变量,所以调用构造函数的时候,成员变量需要初始化好。

(2)成员变量的构造顺序(也叫初始化顺序)是变量在类中声明的顺序。可以猜测,编译器编译的时候是直接按顺序解析类中的成员变量,然后按这个顺序进行初始化的。这样也符合我们的使用习惯,在使用变量的时候,后声明的变量,可能会使用到之前声明的变量,所以从上向下进行初始化。

(3)成员变量的构造顺序和变量的权限是没有关系的,只有一个影响因素,那就是变量在类中的声明顺序。

静态成员是属于类的,不是属于对象的。

静态成员变量在进程中只有一份,被所有对象所共享。

静态成员在程序启动的时候就会创建,而不是在构造对象的时候再创建。

析构顺序,是构造顺序的逆:

(1)先调用析构函数,再析构类中的成员变量

(2)成员变量的析构顺序与变量在类中声明的顺序是相反的

如下是验证代码,代码中有 3 个类 Test1 ~ Test3。另外有 3 个类 Test4 ~ Test5,这 3 个类中有 3 个成员变量,分别是 Test1 ~ Test3,这 3 个变量的声明顺序是不一样的,访问权限也是不一样的。通过这个代码可以观察构造顺序和析构顺序。

#include <iostream>
#include <string>class Test1 {
public:Test1() {std::cout << "Test1()" << std::endl;}~Test1() {std::cout << "~Test1()" << std::endl;}
};class Test2 {
public:Test2() {std::cout << "Test2()" << std::endl;}~Test2() {std::cout << "~Test2()" << std::endl;}
};class Test3 {
public:Test3() {std::cout << "Test3()" << std::endl;}~Test3() {std::cout << "~Test3()" << std::endl;}
};class Test4 {
public:Test4() {std::cout << "Test4()" << std::endl;}~Test4() {std::cout << "~Test4()" << std::endl;}Test1 test1;Test2 test2;Test3 test3;
};class Test5 {
public:Test5() {std::cout << "Test5()" << std::endl;}~Test5() {std::cout << "~Test5()" << std::endl;}Test3 test3;Test2 test2;Test1 test1;
};class Test6 {
public:Test6() {std::cout << "Test6()" << std::endl;}~Test6() {std::cout << "~Test6()" << std::endl;}private:Test3 test3;Test2 test2;public:Test1 test1;
};int main() {std::cout << "t4 --------" << std::endl;Test4 t4;std::cout << "t5 --------" << std::endl;Test5 t5;std::cout << "t6 --------" << std::endl;Test6 t6;std::cout << "----------------" << std::endl;return 0;
}

运行结果如下:

如下代码,运行结果如下,从运行结果也可以看出来,在调用构造函数的时候,打印 a_ 是 10,这说明在调用构造函数的时候,成员变量已经完成了初始化。

#include <iostream>
#include <string>class Test {
public:Test() {std::cout << "Test(), a_ = " << a_ << std::endl;a_ = 100;std::cout << "a_ = " << a_ << std::endl;}~Test() {std::cout << "~Test()" << std::endl;}int a_ = 10;
};int main() {Test t;return 0;
}

3 初始化列表

3.1 初始化列表初始化顺序

初始化列表的初始化顺序有以下几点需要注意:

(1)初始化列表中变量初始化的顺序,不是按照列表的顺序,仍然是按照成员变量在类中声明的顺序。

(2)初始化列表中的初始化也是在调用构造函数之前完成。

(3)如果类中的成员变量有的在初始化列表中进行初始化,有的不在初始化列表中进行初始化,整体的初始化顺序仍然是按照变量在类中声明的顺序。

(4)初始化列表中初始化的变量,不会重复初始化。也就是说不会在初始化列表初始化之前初始化一次,到初始化列表中再初始化一次。

如下代码中 Test3 初始化列表中的顺序和变量在类中声明的顺序不一样,变量的构造顺序仍然按照变量在类中声明的顺序初始化;Test5 中,初始化列表中只初始化了 test1,test1 和没有放到初始化列表中的 test2 和 tes3 的相对顺序仍然是它们在类中声明的顺序。

#include <iostream>
#include <string>class Test1 {
public:Test1() {std::cout << "Test1()" << std::endl;}~Test1() {std::cout << "~Test1()" << std::endl;}
};class Test2 {
public:Test2() {std::cout << "Test2()" << std::endl;}~Test2() {std::cout << "~Test2()" << std::endl;}
};class Test3 {
public:Test3() {std::cout << "Test3()" << std::endl;}~Test3() {std::cout << "~Test3()" << std::endl;}
};class Test4 {
public:Test4() : test3(), test1(), test2() {std::cout << "Test4()" << std::endl;}~Test4() {std::cout << "~Test4()" << std::endl;}Test1 test1;Test2 test2;Test3 test3;
};class Test5 {
public:Test5() : test1() {std::cout << "Test5()" << std::endl;}~Test5() {std::cout << "~Test5()" << std::endl;}Test3 test3;Test2 test2;Test1 test1;
};class Test6 {
public:Test6() : test1(), test2(), test3() {std::cout << "Test6()" << std::endl;}~Test6() {std::cout << "~Test6()" << std::endl;}private:Test3 test3;Test2 test2;public:Test1 test1;
};int main() {std::cout << "t4 --------" << std::endl;Test4 t4;std::cout << "t5 --------" << std::endl;Test5 t5;std::cout << "t6 --------" << std::endl;Test6 t6;std::cout << "----------------" << std::endl;return 0;
}

运行结果如下:

如下代码,两个成员变量 i 和 j,先声明的 i 后声明的 j,所以初始化顺序是先 i 后 j。在初始化列表中对 i 进行初始化的时候,j 还没有初始化,所以 i 是一个随机值,j 是 10。

#include <iostream>
#include <string>class Test {
public:Test(int a) : j(a), i(j) {std::cout << "Test(), j = " << j << ", i = " << i << std::endl;}~Test() {std::cout << "~Test()" << std::endl;}void Print() {std::cout << "Print(), j = " << j << ", i = " << i << std::endl;}private:int i;int j;
};int main() {std::cout << "main" << std::endl;Test t(10);t.Print();return 0;
}

 运行结果如下:

3.2 const 变量和引用只能在初始化列表中初始化

(1)const 常量和引用必须在初始化列表中进行初始化,不能在构造函数中初始化

(2)const 常量只能在初始化列表中初始化,构造函数和其它地方不能修改

(3)引用在初始化列表中初始化之后,在其它地方也可以进行修改

#include <iostream>
#include <string>class Test {
public:Test(int a, int &b) : a_(a), b_(b) {std::cout << "Test(), a = " << a_ << ", b = " << b_ << std::endl;// a_ = a; 编译错误b_ = b;}~Test() {std::cout << "~Test()" << std::endl;}void Print() {std::cout << "Print(), a = " << a_ << ", b = " << b_ << std::endl;}private:const int a_;int &b_;
};int main() {int a = 10;int b = 20;Test t(a, b);t.Print();return 0;
};

3.3 基类构造只能在初始化列表中初始化

#include <iostream>
#include <string>class Base {
public:Base(int a, int b) : a_(a), b_(b) {std::cout << "Base(), a = " << a_ << ", b = " << b_ << std::endl;}~Base() {std::cout << "~Base()" << std::endl;}void Print() {std::cout << "Print(), a = " << a_ << ", b_ = " << b_ << std::endl;}public:int a_;private:int b_;
};class Derived : public Base {
public:Derived(int data) : Base(data, data * 2), data_(data) {// 这句,语法没有错,但是不能起到初始化基类的作用// 只是生成了一个基类的临时对象// 初始化基类只能在初始化列表中Base(data * 10, data * 20);std::cout << "Derived()" << std::endl;};~Derived() {std::cout << "~Derived()" << std::endl;}private:int data_;
};int main() {Derived d(10);d.Print();return 0;
}

4 继承

当派生类构造的时候,先构造基类,再构造派生类。这也是符合常识的,派生类是继承基类而来,没有构造出基类对象,何来派生类对象,派生类对象从哪继承。

4.1 单继承

如下代码,Derived 继承了 Base,构造 Derived 的时候,先构造 Base 再构造 Derived。

#include <iostream>
#include <string>
#include <unistd.h>class Base {
public:Base() {std::cout << "Base()" << std::endl;}~Base() {std::cout << "~Base()" << std::endl;}
};class Derived : public Base {
public:Derived() {std::cout << "Derived()" << std::endl;}~Derived() {std::cout << "~Derived()" << std::endl;}
};int main() {std::cout << "main" << std::endl;Derived d;return 0;
}

4.2 多继承

多重继承是一个类可以继承多个类。多重继承意思是一个类需要同时具备多个类的属性,从现实语义上来说,多重继承更像是组合的语义。多继承的语法和语义,让人感觉有点别扭,与常识也是不大符合的,但是 c++ 中支持这种语法。

4.2.1 构造顺序

(1)基类构造的顺序按继承列表的顺序进行构造,而不是按照初始化列表中声明的顺序进行构造

(2)如果有虚拟继承,那么先构造虚拟继承的基类

#include <iostream>
#include <string>
#include <unistd.h>class Base1 {
public:Base1(int a) : a_(a) {std::cout << "Base1() a = " << a_ << std::endl;}~Base1() {std::cout << "~Base1()" << std::endl;}int a_;
};class Base2 {
public:Base2(int a) : a_(a) {std::cout << "Base2() a = " << a_ << std::endl;}~Base2() {std::cout << "~Base2()" << std::endl;}int a_;
};class Base3 {
public:Base3(int a) : a_(a) {std::cout << "Base3() a = " << a_ << std::endl;}~Base3() {std::cout << "~Base3()" << std::endl;}virtual void Do3() {std::cout << "Do3, a = " << a_ << std::endl;}int a_;
};class Base4 {
public:Base4(int a) : a_(a) {std::cout << "Base4() a = " << a_ << std::endl;}~Base4() {std::cout << "~Base4()" << std::endl;}virtual void Do4() {std::cout << "Do4, a = " << a_ << std::endl;}int a_;
};class Derived1 : public Base1, public Base2 {
public:Derived1() : Base2(20), Base1(10) {std::cout << "Derived1()" << std::endl;}~Derived1() {std::cout << "~Derived1()" << std::endl;}
};class Derived2 : public Base1, public Base2 {
public:Derived2() : Base1(100), Base2(200) {std::cout << "Derived2()" << std::endl;}~Derived2() {std::cout << "~Derived2()" << std::endl;}
};class Derived3 : public Base3, public Base4 {
public:Derived3() : Base4(400), Base3(30) {std::cout << "Derived3()" << std::endl;}~Derived3() {std::cout << "~Derived3()" << std::endl;}
};class Derived4 : public Base1, public Base4, public Base3, virtual public Base2 {
public:Derived4() : Base1(1000), Base2(2000), Base3(3000), Base4(4000) {std::cout << "Derived4()" << std::endl;}~Derived4() {std::cout << "~Derived4()" << std::endl;}
};int main() {std::cout << "main" << std::endl;std::cout << "d1 --------" << std::endl;Derived1 d1;std::cout << "d2 --------" << std::endl;Derived2 d2;std::cout << "d3 --------" << std::endl;Derived3 d3;std::cout << "d4 --------" << std::endl;Derived4 d4;return 0;
}

运行结果如下:

(1)Derived1 和 Derived2 都是多继承 Base1 和 Base2,继承列表中顺序是一致的,初始化列表中是不一样的。基类构造顺序都是继承列表的顺序。

(2)Derived4 虚拟继承了 Base2,并且 Base2 在继承声明的最后,构造 Derived4 的时候也是先构造 Base2。

4.2.2 多重继承的二义性以及怎么避免

如下代码,两个基类,Base1 和 Base2,两个基类有两个相同的函数 Speak()。Derived1 类继承自 Base1 和 Base2,当通过 Derived1 对象调用 Speak 的时候,会产生二义性。编译不知道调用的这个 Speak 是 Base1 中的还是 Base2 中的。

消除二义性,可以通过在调用的时候指定是哪个基类中的方法。

#include <iostream>
#include <string>class Base1 {
public:Base1() {std::cout << "Base1()" << std::endl;}~Base1() {std::cout << "~Base1()" << std::endl;}void Speak(std::string str) {std::cout << "Base1 speak: " << str << std::endl;}
};class Base2 {
public:Base2() {std::cout << "Base2()" << std::endl;}~Base2() {std::cout << "~Base2()" << std::endl;}void Speak(std::string str) {std::cout << "Base2 speak: " << str << std::endl;}
};class Derived1 : public Base1, public Base2 {
public:Derived1() {std::cout << "Derived1()" << std::endl;}~Derived1() {std::cout << "Derived1()" << std::endl;}
};int main() {Derived1 d1;// d1.Speak(); // 二义性d1.Base1::Speak("hello");return 0;
}

如下图所示,Derived1 和 Derived2 继承自 Base1,Derived3 继承自 Derived1 和 Derived2。这样当 Derived3 调用 Speak() 的时候就会产生二义性,因为 Speak 在 Derived1 和 Derived2 中各有一份。

有两种方法可以消除这个代码中的二义性,个人更倾向于使用第一种,这样语法清晰。

(1)代码中指定使用哪个类中的函数

d3.Derived1::Speak("hello");

(2)将 Derived1 和 Derived2 改成虚拟继承

#include <iostream>
#include <string>class Base1 {
public:Base1() {std::cout << "Base1()" << std::endl;}~Base1() {std::cout << "~Base1()" << std::endl;}void Speak(std::string str) {std::cout << "Base1 speak: " << str << std::endl;}
};class Derived1 : virtual public Base1 {
public:Derived1() {std::cout << "Derived1()" << std::endl;}~Derived1() {std::cout << "Derived1()" << std::endl;}
};class Derived2 : virtual public Base1 {
public:Derived2() {std::cout << "Derived2()" << std::endl;}~Derived2() {std::cout << "Derived2()" << std::endl;}
};class Derived3 : public Derived1, public Derived2 {
public:Derived3() {std::cout << "Derived3()" << std::endl;}~Derived3() {std::cout << "Derived3()" << std::endl;}
};int main() {Derived3 d3;d3.Speak("hello"); // 二义性return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/271210.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

静态时序分析:典型与非典型时序路径的约束详解(一)

相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 时序路径是静态时序分析中的一个重要概念&#xff0c;了解时序路径能帮助设计者更好地编写SDC脚本&#xff0c;本文旨在详细介绍时序路径相关内容。 首先给出时序…

【重制版】WSDM 2024 2023时空时序论文总结

&#x1f31f;【紧跟前沿】“时空探索之旅”与你一起探索时空奥秘&#xff01;&#x1f680; 欢迎大家关注时空探索之旅 WSDM 2024于2024年3月4日-3月8日在墨西哥梅里达&#xff08;Mrida, Mxico&#xff09;正在举行。目前官网已经放出了所有被录用论文的表单&#xff08;链接…

实现消息队列(Kafka、ActiveMQ、RabbitMQ和RocketMQ)高可用

概述 单机没有高可用可言&#xff0c;高可用都对集群来说的 要保证消息队列系统&#xff08;如Kafka、ActiveMQ、RabbitMQ和RocketMQ&#xff09;的高可用性&#xff0c;可以采取以下一些通用的措施&#xff1a; 集群部署&#xff1a;将消息队列系统部署为集群&#xff0c;包…

uniapp和vue项目配置多语言,实现前端切换语言

在uniapp中配置多语言功能&#xff0c;实现前端切换语言&#xff0c;可以按照以下步骤进行&#xff1a; 1. 创建语言包 首先&#xff0c;创建一个名为 lang 的目录&#xff0c;并在该目录下为每种支持的语言创建对应的JSON或JS文件。例如&#xff1a; lang/en.js&#xff08…

【Linux】深入理解cd命令

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 基本用法&#xff1a; 相对路径和绝对路径&#xff1a; 特殊符号和快捷方式&#xff1a; 符号链接&#xff1a; 自动补全&…

重装系统后正版office如何安装

前言 重装系统后&#xff0c;正版office如何安装 登录官网 https://www.microsoft.com 下载office https://account.microsoft.com/services

培训机构新助力:教务管理工教务管理新境界:完善流程,高效运营触手可及具

随着科技的不断进步&#xff0c;教育领域正迎来一场革命性的变革。乔拓云教育系统&#xff0c;作为这场变革的引领者&#xff0c;正以其卓越的功能和高效的解决方案&#xff0c;为培训机构带来前所未有的教务管理新篇章。 一、高效排课&#xff0c;让教务管理更轻松 乔拓云教育…

蓝桥杯(3.7)

P1102 A-B 数对 import java.util.Scanner; public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int c sc.nextInt();int[] res new int[n1];for(int i1;i<n;i)res[i] sc.nextInt();int sum 0;for(i…

Redis基础入门

目录 目录 一、认识Redis Redis特征&#xff1a; 二、Redis数据结构介绍 三、Redis的命令 1.Redis通用命令 2.关于String类型的命令 3.关于Hash类型的命令 4.关于List类型的常用命令 5.关于Set类型的常用命令 6.关于SortSet类型的常用命令 四、Redis中的层级关系的key 五…

【RabbitMQ】WorkQueue

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;MQ ⛺️稳中求进&#xff0c;晒太阳 Work Queues Work queues任务模型&#xff0c;简单来说就是让多个消费者绑定到一个队列&#xff0c;共同消费队列中的消息 当消息处理比较耗时的时候&…

【开课】云贝教育2024年3月9日-PostgreSQL中级工程师PGCE认证培训开课啦!

课程介绍 根据学员建议和市场需求,规划和设计了《PostgreSQL CE 认证课程》,本课程以内部原理、实践实战为主&#xff0c;理论与实践相结合。课程包含PG 简介、安装使用、服务管理、体系结构等基础知识。同时结合一线实战案例&#xff0c; 面向 PG 数据库的日常维护管理、服务和…

力扣114. 二叉树展开为链表

Problem: 114. 二叉树展开为链表 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a;借助额外空间 借助一个队列将二叉树先序遍历的节点存入&#xff0c;再取出连接成一个链表 思路2&#xff1a;后序遍历处理 后序遍历&#xff0c;先将左子树拉伸为一个链表&am…

支小蜜校园防欺凌系统如何有效应对学生霸凌?

学生霸凌不仅直接伤害到被霸凌者的身心健康&#xff0c;也对整个校园的和谐氛围构成了威胁。为了应对这一问题&#xff0c;校园防欺凌系统应运而生&#xff0c;成为维护校园安全、保护学生权益的重要工具。那么当校园防欺凌系统面对学生霸凌时&#xff0c;该如何有效应对呢&…

3. springboot中集成部署vue3

1. vue3构建 构建命令 npm run build&#xff0c; 构建的结果在disc目录&#xff1a; 2. springboot集成 2.1 拷贝vue3构建结果到springboot resources/static目录 2.2 springboot pom依赖 添加thymeleaf依赖 <dependency><groupId>org.springframework.boot</…

onnx模型优化利器onnxoptimizer、onnxsim

ONNX性能优化和调试技巧 - 知乎ONNX模型是一种跨平台、跨框架的模型表示格式,允许用户在不同的深度学习框架之间共享模型和数据,从而加速模型开发和部署。然而,在实际应用中,我们通常需要对ONNX模型进行性能优化和调试,以确保其在不同硬件和…https://zhuanlan.zhihu.com/…

golang中go build 后读取配置文件

golang打包后读取配置文件 在用go写代码的时候&#xff0c;为了好用经常使用go build 打包&#xff0c;如果我们用到了配置文件&#xff0c;就总是导致不能找到文件所在位置了出现bug&#xff0c;所以以下代码就解决了这个问题。 核心代码&#xff1a; file, err : exec.Look…

蓝桥杯刷题(一)

一、 import os import sys def dps(s):dp [0] * len(s)dp[0] ord(s[0]) - 96if len(s) 1:return dp[-1]dp[1] max(ord(s[0]) - 96, ord(s[1]) - 96)for i in range(2, len(s)):dp[i] max(dp[i - 1], dp[i - 2] (ord(s[i])) - 96)return dp[-1] s input() print(dps(s))…

Java定时调度

在Java应用程序中&#xff0c;定时调度是一项重要的任务。它允许你安排代码执行的时间&#xff0c;以便在将来的某个时刻自动执行任务。Java提供了多种方式来实现定时调度&#xff0c;其中最常用的是Java的Timer和ScheduledExecutorService。 在本教程中&#xff0c;我们将学习…

高效实用|ChatGPT指令/提示词/prompt/AI指令大全,进阶版

大家好&#xff0c;我是淘小白~ 《高效实用|ChatGPT指令/提示词/prompt/AI指令大全&#xff0c;基础版》整理完了&#xff0c;下面来看下进阶版的吧&#xff01; 如果对你有用记得点赞、关注、收藏哦~ 划走可能找不着了哦~~ 进阶版指令可用于复杂任务和场景&#xff0c;以及…

千帆AppBuilder使用指南-组件中心

应用中心 百度智能云千帆AppBuilder&#xff08;以下简称为AppBuilder&#xff09;应用中心&#xff0c;提供了大量可以立即体验的应用示例&#xff0c;开发者可以在这里搜索感兴趣的应用进行使用。 官方应用&#xff1a;AppBuilder官方提供的应用&#xff0c;可以立即体验应用…