• 【C++】类和对象 — 编译器对连续构造的优化 + 内部类(补充篇)


    📖前言

    本章将会对类和对象中用类构造对象过程中出现的特殊情况分析讲解,因为有可能代码在不同的编译器上执行,不同编译器对特殊情况的处理不一样,可能会在不同的平台上的结果都不尽相同,所以本章仅为补充内容,供大家参考

    注: 本篇博文代码运行的环境为VS编译器系列的2019版本。


    1. 匿名对象

    • 我们之前用类创造对象的时候格式都是:类名 对象名;
    • 现在提供一种新的方法,所创造出来的对象叫做匿名对象,格式是:类名();

    代码如下:

    class A
    {
    public:
    	A()
    	{
    		cout << "A()" << endl;
    	}
    
    	A(const A& aa)
    	{
    		cout << "const A& aa" << endl;
    	}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    
    private:
    	int _a;
    	int _b;
    };
    
    int main()
    {
    	A aa;
    	cout << endl;
    
    	A();
    	cout << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    构造函数,拷贝构造,析构函数等都只是打印了一下,方便观察。

    运行结果:
    在这里插入图片描述
    特点:

    • 如图所见匿名对象的声明周期只有该匿名对象所在的那一行
    • 该匿名对象所在的行执行结束之后,就会立刻调用析构函数销毁

    2. 单参数的构造函数与explicit的使用

    2.1 隐式类型转换

    构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

    class Date
    {
    public:
    	Date(int year)
    		:_year(year)
    	{}
    private:
    	int _year;
    };
    
    int main()
    {
    	Date d1(2022);//构造
    
    	//隐式类型的转换
    	Date d2 = 2022; //构造 + 拷贝构造 -> 优化 合二为一
    	//中间会生成临时变量
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • Date d1(2022); - 普通的构造
    • Date d2 = 2022; - 隐式类型转换
    • 相当于用2022构造了一个临时对象或者匿名对象(具有常性),再用临时对象拷贝构造一个d2

    • 本来是先构造再拷贝构造Date(2022) —> Date d2(Date(2022)) ,但是结果是编译器对其进行了优化,直接优化成直接构造

    • 构造 + 拷贝构造 —> 优化 合二为一

    如下图所示,两种构造结果是一样的:

    在这里插入图片描述
    这两种构造的区别是:

    • 第一个是直接构造
    • 第二个是隐式类型转换

    标准并没有规定,取决于编译器自身的行为.

    2.2 explicit的使用:

    单参构造函数,没有使用explicit修饰,具有类型转换作用
    explicit修饰构造函数,禁止类型转换—explicit去掉之后,代码可以通过编译

    class Date
    {
    public:
    	explicit Date(int year, int month = 1, int day = 1)
    	: _year(year)
    	, _month(month)
    	, _day(day)
    	{}
    	
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1(2022);
    	d1 = 2023;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    用explicit修饰构造函数,将会禁止构造函数的隐式转换。


    3. 编译器对连续构造的优化

    3.1 正常的构造和拷贝构造:

    下面程序的运行结果是什么:

    class Weight
    {
    public:
    	Weight()
    	{
    		cout << "Weight()" << endl;
    	}
    
    	Weight(const Weight& w)
    	{
    		cout << "Weight(cosnt Weight& w)" << endl;
    	}
    
    	~Weight()
    	{
    		cout << "~Weight()" << endl;
    	}
    };
    
    Weight f(Weight u)
    {
    	Weight v(u);
    	Weight w = v;
    
    	return w;
    }
    
    int main()
    {
    	Weight x;
    	f(x);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    公布答案:
    在这里插入图片描述
    解释:

    • f函数是传值传参,实参拷贝给形参,所以要调用拷贝构造
    • 还未示例化出来的对象调用赋值重载的时候不是调用赋值重载,而是调用拷贝构造
    • f函数是传值返回,不是传引用返回,所以函数f返回的时候会生成一个临时变量用来存放w对象的拷贝,这里又要调用一次拷贝构造
    • f(x)是一个临时变量,也可以看做是匿名对象,生命周期就那一行,执行完就结束调用其析构函数

    3.2 编译器优化之后的构造和拷贝构造:

    单纯为了传参,弄一个匿名对象

    int main()
    {
    	//单纯为了传参,弄一个匿名对象
    	f(Weight());
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    公布答案:
    在这里插入图片描述

    • 编译器在一个步骤里面,一个调用表达式中
    • 连续步骤的构造 + 拷贝构造,或者拷贝构造 + 拷贝构造

    这时候有两种情况:

    • 构造 + 拷贝构造 (传统)
    • 直接构造 (优化之后)
    • 胆大的编译器可能就会优化,合二为一,直接构造
    • 但是C++标准中并没有规定编译器要不要优化,胆肥一点的编译器就会做,现在新的编译器胆都比较肥,都会做。

    知识关联:

    和之前的那个隐式类型转换一样:Date d2 = 2022,本来应该是先用2022构造一个匿名对象,再用这个匿名对象去拷贝构造一样,但是最后被编译器优化,直接用2022去构造对象了

    下面程序拷贝构造的次数是多少:

    class Weight
    {
    public:
    	Weight()
    	{
    		cout << "Weight()" << endl;
    	}
    
    	Weight(const Weight& w)
    	{
    		cout << "Weight(cosnt Weight& w)" << endl;
    	}
    
    	Weight& operator=(const Weight& w)
    	{
    		cout << "Weight& operator=(const Weight& w)" << endl;
    		return *this;
    	}
    
    	~Weight()
    	{
    		cout << "~Weight()" << endl;
    	}
    };
    
    Weight f(Weight u)
    {
    	Weight v(u);
    	Weight w = v;
    
    	return w;
    }
    
    int main()
    {
    	Weight x;
    	Weight ret = f(x);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    公布答案:
    在这里插入图片描述
    解释如下:

    按照传统,中规中矩的,一步一步来构造拷贝构造,拷贝构造应该如下图所示:
    在这里插入图片描述
    在这里编译器也会有优化:

    • 4 和 5 两步会被编译器优化直接合并成一步,相当于w直接构造ret去了

    如下代码就能将优化阻断掉:

    int main()
    {
    	Weight x;
    	Weight ret;
    	ret = f(x);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    公布答案:
    在这里插入图片描述
    这就是中规中矩的,一步一步构造拷贝构造的来,还调用了赋值重载。

    解释如下:
    在这里插入图片描述
    上述红点标出的均为拷贝构造。
    《深度探索C++对象模型》中有提到

    3.3 终极一题:

    下面程序拷贝构造的次数是多少:

    class Widget
    {
    public:
    	Widget()
    	{
    		cout << "Widget()" << endl;
    	}
    	
    	Widget(const Widget& w)
    	{
    		cout << "Widget(cosnt Widget& w)" << endl;
    	}
    	
    	Widget& operator=(const Widget& w)
    	{
    		cout << "Widget& operator=(const Widget& w)" << endl;
    		return *this;
    	}
    	
    	~Widget()
    	{
    		cout << "~Widget()" << endl;
    	}
    };
    
    Widget f(Widget u)
    {
    	Widget v(u);
    	Widget w = v;
    	
    	return w;
    }
    
    int main()
    {
    	Widget x;
    	Widget y = f(f(x));
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    公布答案:

    在这里插入图片描述
    解释如下:
    这就是中规中矩的,一步一步构造拷贝构造的来。
    在这里插入图片描述
    优化:

    • 4 和 5合二为一
    • 8 和 9合二为一

    所以总共调用7次拷贝构造。


    4. 友元类和内部类

    4.1 友元类:

    在一些情况下面,是需要对类的私有数据,保护数据进行访问的
    不提供友元的话,也可以有其他的方式,在类里面写一个函数将想要的值返回的形式带出去(java喜欢这种方式)

    C++中提出友元的概念:

    前倾回顾,友元函数:
    如下代码:

    class Date
    {
    	//友元函数 - 我是你的朋友就能访问你的私有
    	friend ostream& operator<<(ostream& out, const Date& d);
    public:
    	//双操作数的运算符,第一个数是左操作数,第二个数是右操作数
    	//ostream& operator<<(ostream& out)
    	
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    ostream& operator<<(ostream& out, const Date& d)
    {
    	//通过对象去访问私有
    	out << d._year << "/" <<  d._month  << "/" << d._day << endl;
    	return out;
    }
    
    int main()
    {
    	Date d;
    	//d << cout;
    
    	cout << d;//这样更性形象
    	//为了更形象,且保留可读性,就要写成全局的
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    详情参考上一篇博客: 👉 传送门


    友元类:

    • 友元类的特性:
    • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员(或者保护)。
    • 友元关系是单向的,不具有交换性。
    • 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
    • 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
    • 友元关系不能传递:
    • 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
    • 友元关系不能继承,在继承位置再给大家详细介绍。
    //前置声明告诉编译器Date是个类的名字
    class Date;
    
    class Time
    {
    	friend void Func(const Date& d, const Time& t);
    
    	//友元类
    	friend class Date;
    	//声明日期类为时间类的友元类
    	//则在日期类中就直接访问Time类中的私有成员变量
    public:
    	//构造函数
    	Time(int hour = 0, int minute = 0, int second = 0)
    		: _hour(hour)
    		, _minute(minute)
    		, _second(second)
    	{}
    
    	//拷贝构造
    	Time(const Time& T)
    	{
    
    	}
    private:
    	int _hour;
    	int _minute;
    	int _second;
    };
    
    //友元函数放到类里面的任意位置
    //一个函数也可以是多个类的友元
    
    class Date
    {
    	friend void Func(const Date& d, const Time& t);
    public:
    	Date(int year = 1900, int month = 1, int day = 1)
    		: _year(year)
    		, _month(month)
    		, _day(day)
    	{}
    
    	void SetTimeOfDate(int hour, int minute, int second)
    	{
    		//直接访问时间类私有的成员变量 - 在Time类外面访问不了
    		_t._hour = hour;
    		_t._minute = minute;
    		_t._second = second;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    	Time _t;
    };
    
    //谁想访问我,谁就要变成我的友元
    
    
    void Func(const Date& d, const Time& t)
    {
    	//既要访问Date的私有,又要访问Time的私有
    	cout << d._year << endl;
    	cout << t._hour << endl;
    }
    
    int main()
    {
    	Date d;
    	Time t;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    建议:
    友元不建议多用,相当于把访问限定开了一个口子,不到万不得已不要用。


    4.2 内部类:

    概念:

    如果一个类定义在另一个类的内部,这个内部类就叫做内部类。 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

    注意:

    内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

    特性:

    1. 内部类可以定义在外部类的public、protected、private都是可以的。
    2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象 / 类名。
    3. sizeof(外部类) = 外部类,和内部类没有任何关系。
    class A 
    {
    private:
    	//不算大小
    	static int k;
    	int h;
    public:
    	//A类里面没有一个B对象
    	
    	//内部类
    	//B天生就是A的友元
    	class B 
    	{
    	public:
    		void foo(const A& a)
    		{
    			cout << k << endl;//OK
    			cout << a.h << endl;//OK
    		}
    	private:
    		int _b;
    	};
    
    	A不是B的友元
    	//void Print(const B& b)
    	//{
    	//	b._b = 0;
    	//}
    };
    
    int A::k = 1;
    
    int main()
    {
    	//用B类型定义一个对象 - 去A这个类域里面找B这个类型
    	//将内部类放在私有里面这个类就是专属的类,在类外面用不了
    	A::B b;
    	b.foo(A());
    
    	//算这个类型的本质是算,这个类型定义的对象的大小
    	cout << sizeof(A) << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    公布答案:

    在这里插入图片描述
    sizeof(A)讲解:

    • A类里面没有一个B对象,虽然是内部类
    • static 的成员变量也不算大小,因为计算大小算的是,类创造对象的大小,而static在静态区,不算在类所创造的对象里面,类创造的任何对象都能访问,指定类域也能访问
    • 所以只有一个int的成员
    • 所以大小是4
  • 相关阅读:
    MySQL 实践篇 —— 主从复制
    责任链模式auto-pipeline工具使用及源码解析
    不花钱几分钟让你的站点也支持https
    LeetCode39 组合总和
    2、一起探讨MySQL 安装
    linux 下 rm 为什么要这么写?
    刷题笔记day10-栈和队列01
    BuildApkPlugin 自动化编译打包
    【Linux篇<Day15>】——三分钟教会你如何搭建web网站
    MyBatis 学习(七)之 缓存
  • 原文地址:https://blog.csdn.net/m0_63059866/article/details/126572911