
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️C++系列个人专栏:
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
目录
0.1概要&序論
大家新年快乐!这里是此方,本篇是类和对象的最终篇下 ,本文将详细介绍类和对象剩余的全部概念****同时揭示对象拷贝过程中编译器为你偷偷做了哪些优化。内容干货满满!「此方」です。让我们现在开始吧!
一,类型转换
1.1类型转换的定义
官方定义
类型转换(Type Conversion)类型转换又名隐式类型转换,是指在 C++ 程序中,将一个表达式的类型转换为另一种类型的过程 。该过程可以由语言规则隐式地自动完成 ,也可以由程序员通过显式转换语法 明确指定。类型转换的结果是一个具有目标类型的值或对象,其值由源类型的值按照 C++ 语言定义的转换规则产生。
1.2类型转换的使用
1.2.1常见隐式类型转换
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
: _a1(a){}
private:
int _a1;
};
int main(){
A aa2 = 1;
return 0;
}
代码中:直接将1赋值给A类型对象,实际上发生了隐式类型转换。该隐式类型转换的发生实际上是基于A类类型对象存在一个单参数构造函数。1利用这个单参数构造函数构造生成了临时对象再拷贝构造给对象aa2。
1.2.2C++11新增特性
如果这个类支持一个n参数构造函数,那么就传递一个n元值集合,就像这样:
cpp
class A
{
public:
A(int a,int b)
: _a1(a)
,_a(b){}
private:
int _a1;
int _a2;
};
int main(){
A aa2 = {1,2};
return 0;
}
1.2.3类型转换的局限
但是这种类型转换并不是所有类型都支持:如下,我们将一个常量字符串传递给A类类型变量aa2,实际上会发生报错。
cpp
class A
{
public:
A(int a)
: _a1(a){}
private:
int _a1;
};
int main(){
A aa2 = "abcde";
return 0;
}
1.3类型转换的意义
1.3.1节省代码量
cpp
A aa3(3);
st.Push(aa3);
//类型转换
st.Push(3);
**对比这两者。显然,适当使用类型转换能减少代码量。**前面的方法你还要定义一个对象再插入,后面你直接传递一个3。

1.3.2编译器的优化
到这里,一定会有读者问到:类型转换增加了构造临时对象和拷贝构造的过程 ,岂不是影响了运行效率?这点下文会详细介绍:编译器对拷贝的优化: 编译器会把拷贝构造优化掉,和二为一,直接使用3进行构造。
如何证明是编译器优化了而不是本就如此?

临时对象是具有常性的,直接给临时对象一个非常性的别名会报编译错误(如图)所以的确是先使用类型转换转换成一个临时对象再拷贝构造。
1.4explicit关键字
1.3.1官方定义
explicit用于修饰构造函数或类型转换函数, 表示该函数不能用于隐式类型转换,只能在需要显式指定的上下文中被调用。
1.3.2使用方式
cpp
class A {
public:
explicit A(int x) {}
};
A a1 = 10; // ❌ 编译错误
如上代码,编译器发生报错,说明不可以使用explicit修饰的构造函数去进行类型转换。
二,static成员
2.1static成员的定义
- 用static修饰的成员变量,称之为静态成员变量 ,静态成员变量一定要在类外进行初始化。
- 静态成员变量为同一个类的所有对象所共享 ,不属于某个具体的对象,不存在于对象中 ,存放在静态区。
2.2static成员的使用
静态对象不可以给缺省值 。因为缺省值是给初始化列表用的,静态成员变量是不走初始化列表的。
cpp
private:
// 类里面声明
static int _scount;
2.3static成员的初始化
static对象可以认为是一个全局变量 ,但是在访问上受到类域和域访问操作符的限制。初始化方法如下:
cpp
private:
// 类里面声明
static int _scount;
};
// 类外面初始化
int A::_scount = 0;
Tips:静态成员的访问与初始化
读者看到这里一定会发出疑问 :static修饰的变量_scount不是在private的作用域中吗?为什么可以在类外初始化?一句话结论:"定义(初始化)" ≠ "访问 "。private影响的是不可以在类外面访问成员变量而不是能否在类外面定义。
2.4sistic成员函数
用 static 修饰的成员函数,称之为静态成员函数 。静态成员函数没有 this 指针 。在静态成员函数中,可以访问其他的静态成员 ,但是不能访问非静态成员,因为静态成员函数没有 this 指针。
- 非静态的成员函数 ,可以访问任意的静态成员变量和静态成员函数。
- 而静态的成员函数 ,只能访问任意的静态成员变量和静态成员函数。
突破类域就可以访问静态成员 ,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。(前提是不被private限制)
2.5静态计数器
如何利用静态函数和静态成员变量来实现查找一共有多少个对象已经被创建?
**非常简单:利用静态成员变量的声明周期,**在每一个构造和拷贝狗杂函数中放一个该静态成员变量的++。生命周期还在的:就在析构里面加给--。

如图,题目要求计算累加和,但是**不得使用迭代(限制循环),递归(无法设置结束条件),公式(限制乘法运算)**三大常用方法。
cpp
class Sum {
public:
Sum() {
++cnt;
sum += cnt;
}
static int GetSum() {
return sum;
}
private:
static int cnt;
static int sum;
};
int Sum::cnt = 0;
int Sum::sum = 0;
首先,我们创建构造函数并确保每一次调用构造函数的时候静态变量cnt就会++,sum每次都会加上cnt,基于两者静态变量的性质,可以持续累加实现目的。
cpp
int main() {
int n;
cin >> n;
Sum* p = new Sum[n];
cout << Sum::GetSum() << endl;
delete[] p;
return 0;
}
利用get函数法调用这个静态成员变量 。**使用new创建n个变量(后面会讲)**实现连续调用n此构造函数,每一次构造都是一次累加操作。
理解上述机制后,可进一步分析对象生命周期。
题目:
设已经有 A、B、C、D 4 个类的定义,程序中 A、B、C、D 构造函数调用顺序为?( )
设已经有 A、B、C、D 4 个类的定义,程序中 A、B、C、D 析构函数调用顺序为?( )
cppC c; int main(){ A a; B b; static D d; return 0; }
- 第一题:CABD,静态变量是在第一次运行到这个地方的时候才会初始化。只有全局的静态才会在main函数之前初始化。
- 第二题:BADC后定义先析构。但是局部变量先析构,静态变量后析构。
三,友元
3.1友元的定义
友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类。在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
cpp
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
3.2友元函数
外部友元函数可访问类的私有和保护成员 ,友元函数仅仅是一种声明,他不是类的成员函数。 友元函数可以在类内部的任何地方声明,不受类访问限定符限制。一个函数可以是多个类的友元函数。
Tips:少数特殊情况

如图,我们有一个函数同时是A类和B类的友元函数,但是基于编译器的向上查找原则,当编译器运行到A类中的友元函数声明时,在查找B这个类的时候会出现报错。因此我们必须在整个程序的开头添加一个B类型的声明。class B
3.3友元类
友元类是一种通过friend声明,被授予访问另一个类的私有和保护成员权限的类。其成员函数都可以是另一个类的友元函数。

如上图,我们在类A中加入一个B类的友元声明,然后我们的B类中的函数就视为A类的友元函数。可以直接访问A类的成员变量。
3.4友元的特性
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
- 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
- 友元增加了程序的耦合度,破坏了封装,不建议多用。
四,内部类
4.1内部类的定义
如果一个类定义在另一个类的内部,这个类就叫做内部类 。内部类是一个独立的类 ,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。在计算外部类的时候不包含内部类。
cpp
#include<iostream>
using namespace std;
class A{
private:
static int _k;
int _h = 1;
public:
class B{ // B默认就是A的友元
public:
void foo(const A& a){
cout << _k << endl;
cout << a._h << endl;
}
private:
int _b = 1;
};
};
cpp
int main(){
cout << sizeof(A) << endl;
A::B b; // 红框标注部分
return 0;
}
内部类必须指定类域搜索,如果内部类私有,则该内部类是该外部类的专属类。B受到A的限制,但是****B不是A 的成员。
4.2内部类的特性
内部类本质也是一种封装 ,当A类跟B类紧密关联,A类实现出来主要就是给B类使用 ,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
- 内部类默认是外部类的友元类 ,就是说:A的变量B可以随便使用。
- 内部类可以多层嵌套,但是一般不建议。
- 相对而言C++ 不大喜欢使用内部类,Java比较喜欢。
- 内部类与友元类,前者可以看作是从属但又相互独立关系,后者可以看作是朋友关系。
五,匿名对象
5.1匿名对象的定义
5.1.1官方定义
A temporary object is an object created by a prvalue, which is not bound to a reference and has its lifetime limited to the evaluation of the full-expression in which it was created.(临时对象是由 prvalue 表达式创建、且未绑定到任何引用的对象,其生命周期限制在创建它的完整表达式的求值过程中。)
5.1.2通俗解释和使用
匿名对象就是一个用构造函数临时创建的没有名字的对象,用完马上销毁。
cpp
int main(){
A aa1; //有名对象
A aa2();// 不能这么定义对象,因为编译器无法识别这是函数声明还是对象定义
A(); // 不传参匿名对象
A(1); //传参匿名对象
}
没有设置名称只在后面加一对()(有参数传递参数),这样就完成了匿名对象的创建。
cpp
//有名对象
Solution st;
cout << st.Sum_Solution(10) << endl;
// 匿名对象
cout << Solution().Sum_Solution(10) << endl;
如上代码,可见采用匿名对象在一些一次性的对象参数传递的时候可以节约代码量。
5.2匿名对象的特性
- 匿名对象的声明周期只有一行,匿名对象就是一次性的,后面会讲const引用会延长它的声明周期。
- 匿名对象 ≠ 没有 this,匿名对象只是没有名字,但它依然是一个完整的对象。成员函数是否有 this,只取决于它是不是非静态成员函数,和对象有没有名字完全无关。
- 匿名对象会调用构造函数和析构函数会开辟空间。
六,编译器在拷贝时做的优化
6.1编译器拷贝优化概述
现代编译器会为了尽可能提高程序的效率 ,在不影响正确性的情况下 会尽可能减少一些传参和传参过程中可以省略的拷贝。
如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。 当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝 会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化。
6.2拷贝优化一:类型转换
**测试类(一个简易的验证机器)**如果我们调用了哪个函数,构造还是拷贝构造,我们可以通过打印结果判断出来
cpp
#include<iostream>
using namespace std;
class test{
public:
test(int a, int b)
:_a(a)
, _b(b){
cout << "test::test(int a, int b)" << endl;
}
~test(){
_a = 0;
_b = 0;
cout << "test::~test()" << endl;
}
test(test& test1){
_a = test1._a;
_b = test1._b;
cout << "test::test(test& test1)" << endl;
}
private:
int _a;
int _b;
};
测试类型转换:如下
cpp
test testA = { 1,2 };

对比结果:
|-----------------------|----------------------------|
| 基于逻辑我们所想的 | 编译器优化后实际做的 |
| 一、{1,2}传递给构造函数创建临时变量 | 一,直接用{1,2}传递参数给构造函数构造testA |
| 二、该临时变量传递给testA实现拷贝构造 | / |
| 三、析构 | 二,析构 |
所以i我们可以得出结论:编译器在连续的拷贝工作中会优化掉其中一步构造实现优化。
++以下,我们可以用更多的案例来证明这个结论++
6.3拷贝优化二:值传参

这里我们分有名对象传参和匿名对象传参。以下逐条解释分析:
- 第一个构造函数:通过构造函数构造对象testA。
- 第一个拷贝构造函数:有名对象testA传递参数调用拷贝构造函数,创建临时对象拷贝。
- 第一个析构函数:临时对象销毁。
- 第二个构造函数:编译器优化:将构造匿名对象并拷贝构造传递的过程合并为构造函数。
- 第二给析构函数:匿名对象离开本行声明周期结束。
- 第三个析构函数:程序运行到结束,testA对象声明周期结束。
我们看看编译器哪里优化了:
- **首先:**有名对象传参必须经过拷贝构造,同时这个过程不属于连续的拷贝,所以不能优化。
- **优化的地方:**匿名对象的一个连续的步骤:构造匿名对象+拷贝构造匿名对象优化。

同理的,将类型转换与函数传参结合起来,同样会被优化,因为这是一个连续操作上的两个拷贝操作,必然优化。
6.4拷贝优化三:值返回
Tips:我们先补充一种写代码的方式
cpp
test f2(){
test aa;
return aa;
}
int main(){
f2().Print();
return 0;
}
在函数体内创建一个对象,然后将变量值返回,这种方式在C语言中是万万不可以的,但是在C++中,值返回会拷贝构造一个临时对象,这里正是利用了这个临时对象调用了函数print()。
我们用类似的方式去测试一下:

发现,这里也优化了: 严格来说这里是省略了aa。为什么省略临时对象不行?因为aa出函数作用域就析构了,无法调用print()。看起来省的是拷贝构造,但是实际上是取消了aa的构造。让构造直接构造临时对象。
|---------------------------------------------------------------------------------|--------------------------------------------------|
| 原本应该是怎么样 | 编译器优化的结果是什么 |
| 对象aa创建->值返回调用拷贝构造函数->创建临时对象->aa销毁->临时对象调用函数打印->临时对象销毁(注意析构发生在print()函数这一行) | 省略aa对象的创建和销毁->直接构造临时对象并返回->临时对象调用函数打印->临时对象销毁 |
6.5高度激进的优化操作一
最厉害的来了(这里我们采用VS2022的debug版本)
cpp
A f2(){
A aa(1);
++aa;
return aa;
}
接着上面的测试案例,我们看看将aa++后还能不能实现优化。牛逼的来了:编译器会自定语义分析aa++并实现优化。

6.6高度激进的优化操作二
cpp
test f2(){
test aa();
return aa;
}
int main(){
test testC = f2();
}
如上,该代码本应该执行三步操作,构造对象aa,拷贝构造临时对象传递,拷贝构造初始化给testC,编译器在这里省略了临时对象,将三步和为一步

如果我们把拷贝构造换成赋值运算符重载,这个时候拷贝构造没有进行任何优化,但是release下任然进行了优化。

好了,本期内容就到这里,感谢你的阅读,如果对你由帮助,不要忘记点赞三联哦,我是此方,我们下期再见,拜拜~