
以🎈主页传送门****:良木生香
🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》
🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离

目录
[NRVO & URVO:](#NRVO & URVO:)
前言:在上一篇文章中,我们学习了类和对象最核心的部分---类的默认成员函数。它们分别是:构造函数,析构函数,拷贝构造函数,赋值运算符重载,const成员函数以及取地址运算符重载,这六个函数是在程序员不显式写时编译器会自动生成的函数,那么今天我们就对他们剩下的边角料进行收尾
一、再谈构造函数
我们在学习构造函数时,对对象进行初始化是在构造函数体内完成的,现在我们要学习另外一种方法------初始化列表。他的格式是这样子的:初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
cpp
#include<iostream>
using namespace std;
class Date {
public:
//构造函数
Date(int year, int month, int day):
_year(year),
_month(month),
_day(day)
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2026, 4, 3);
return 0;
}
在初始化列表中,如_year(year),是不能用等号'='的,并且每个成员只能出现一次,从语法理解上,可以理解为初始化列表是每个成员变量定义与初始化的地方。这时候肯定有同学要问了,为什么初始化列表是成员变量定义和初始化的地方?那之前写的构造函数不是用来初始化的吗?
当然,之前写的确实起到初始化的作用,但是与初始化列表有着本质的区别:
初始化列表:对成员变量进行定义和初始化
之前写的构造函数:虽然名义上起到了初始化的作用,但是本质上是赋值操作。
ok,既然现在我们了解了初始化列表与之前的函数体的关系之后,那我们来想想,为什么本贾尼博士要发明这么一个玩意?我直接在函数体内部进行初始化不就行了?
因为有些成员函数只能在初始化列表进行赋值:引用成员变量、const成员变量,没有默认构造的类类型变量,以上三种只能在初始化列表中进行定义和初始化。
而且我们还说,所有变量都尽量在初始化列表中进行初始化,因为不在初始化列表初始化的变量,最终在编译的时候还是会走初始化列表的。如果这个成员变量在声明的时候给了缺省值,那编译器就会用所给的缺省值进行初始化。如果变量没有缺省值,那么编译器对于那些没有显式写在初始化列表的内置类型成员是否初始化就取决于编译器了,C++并未对此做出规定;对于没有显式写在初始化列表的自定义类型成员,则会直接调用其默认构造函数,如果没有默认构造函数,则会报错。
这段文字读起来确实是比较难理解,那么我们通过代码来感受一下:
场景1:成员变量显式初始化:
对于内置类型的成员变量,如果有传进来的值,那么就用传进来的值进行初始化,如果没有传进来的值,那就初始化成0。
对于自定义类型的变量,对自己调用它的默认构造函数
cpp
#include<iostream>
using namespace std;
// 1. 先写一个【自定义类型】
class Time {
public:
// 这就是 Time 的 默认构造函数
Time() {
cout << ">>>> Time 默认构造函数调用,完成了对Time类型的初始化" << endl;
_hour = 0;
_minute = 0;
}
void show() {
cout << "时间:" << _hour << ":" << _minute << endl;
}
private:
int _hour;
int _minute;
};
class Date {
public:
Date(int year, int month, int day)
:_year(year), // 显式初始化
_month(month),
_day(day)
// 注意:
// 初始化列表里【没有写 Time 类型的 _t】
// 但编译器仍然会自动初始化它!
{
cout << "成员变量_year的初始化值为:" << _year << endl;
cout << "成员变量_month的初始化值为:" << _month << endl;
cout << "成员变量_day的初始化值为:" << _day << endl;
_t.show(); // 打印自定义类型,验证它已经被构造好了
}
private:
int _year = 1; // 有缺省值
int _month; // 无缺省值
int _day; // 无缺省值
Time _t;
};
int main() {
Date d1(2026, 4, 3);
return 0;
}
运行结果为:

场景2:成员变量未显式初始化
对于未显式初始化的内置成员变量,会优先查看声明时有没有缺省值,如果有那就用缺省值进行初始化,如果没有那就是随机值,像下面代码中的_minth和_day,他们两个的值就是随机值
对于未显式初始化的自定义类型变量,那就会回到该变量的类域中寻找其对应的默认构造函数,如果没有默认构造函数,那编译器就会报错。
cpp
#include<iostream>
using namespace std;
//自定义类型变量
class Time {
public:
//自定义类型的非构造函数
Time(int hour=1, int minute = 1)
//此处没写Time的初始化列表
{
//_hour = hour;
//_minute = minute; //如果把这两行注释去掉,那就会报错,因为在Date中找不到_t的默认
cout << "_hour和_minute的初始化分别为:" << _hour << " " << _minute << endl;
}
private:
int _hour = 2;
int _minute = 2;
};
class Date {
public:
//构造函数
Date(int& a)
//三个成员变量都没有显式出现在初始化列表中
:_temp(1),
_a_in_main(a)
{
cout << "_year的初始化值为:" << _year << endl;
cout << "_month的初始化值为:" << _month << endl;
cout << "_day的初始化值为:" << _day << endl;
cout << "_tmep的初始化值为:" << _temp << endl;
cout << "_a_in_main的初始值为:" << _a_in_main << endl;
}
private:
int _year = 1;//声明时有缺省值
int _month;//声明时没有缺省值
int _day;
Time _t; //自定义类型成员变量
const int _temp;//const成员变量
int& _a_in_main;
};
int main() {
int a = 2;
Date d1(a);
return 0;
}
运行结果如下:

总结下来就是下面这张图:

还有一点是:初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
总结:无论是否显⽰写初始化列表,每个构造函数都有初始化列表;⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要走初始化列表初始化.
现在我们再来思考一个问题:能不能只要初始化列表?不要函数体?
答案是:不能!!!看看下面这个场景:
cpp
#include<iostream>
#include<stdlib.h>
using namespace std;
class A {
public:
//A的构造函数
A(int n = 10)
:_a((int*)malloc(sizeof(int)*n)),
_size(n)
{
if (_a == nullptr) {
cout << "malloc fail" << endl;
}
//如果我想对整个数组进行初始化的话:
memset(_a, 0, sizeof(int) * n);
}
void cout_a() {
for (int i = 0; i < _size; i++) {
cout << *(_a + i) << " ";
}
}
private:
int* _a;
int _size;
};
int main() {
A aa(12);
aa.cout_a();
return 0;
}
假设先在我有一个数组_a,以及记录数组大小的_size,当我在初始化列表中对_a申请空间之后,想要对其进行判空操作,这时候初始化列表就满足不了我了,还是需要用到函数体,甚至对整个数组用memset进行初始化,也是要用到函数体的,所以祖师爷设计这些东西出来都是由原因的,不能抛弃任何一个。
那现在我们来看看这道题目:
在下面这段程序中,输出的结果是多少?
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1
cpp
#include<iostream>
using namespace std;
class A {
public:
A(int a)
:_a1(a),
_a2(_a1)
{
}
void cout_class() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main() {
A aa(1);
aa.cout_class();
return 0;
}
答案是D,因为在C++的规定中,先声明的先进行初始化,后声明的后进行初始化,在这段代码中,_a2先声明,_a1后声明,所以在初始化列表中_a2会先被_a1初始化,但是此时的_a1还没有被初始化,是随机值,所以_a2的值也是随机值。
二、类型转换
类型转换我们在之前也有碰到过,那就是将整型变量转换成浮点型变量:
cpp
int a = 3;
double b = a;//这时候就是类型转换
在这个过程中,会产生一个临时变量作为他们转换的一个桥梁,如果我这个时候想用double的引用呢?这么写可以吗?
cpp
int i=2;
double& ref = i;
当然不行,因为临时变量具有常性(const性质),所以我们就要在ref前加上const:
cpp
从const double& ref = i;
现在,C++支持由内置类型到类类型的转换,也支持内置类型到内置类型的转换,下面我们一个一个来讲解。
2.1、内置类型转换到类类型:
如果我们想将内置类型转换为类类型,就有一个前提:这个类有一个可以用该内置类型调用的构造函数 (通常是「单参数构造函数」,或「多参数但其余参数有默认值」)。
可以看看下面的代码:
cpp
#include <iostream>
using namespace std;
class Date {
public:
// 单参数构造函数:支持 int → Date 隐式转换
Date(int year)
: _year(year), _month(1), _day(1)
{
cout << "Date(int) 构造函数调用" << endl;
}
void Print_date() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 测试函数:接收 Date 对象
void func(Date d) {
d.Print_date();
}
int main() {
// 场景1:直接用 int 给 Date 赋值(隐式转换)
Date d = 2026; // 等价于 Date d = Date(2026); 自动转换
d.Print_date(); // 输出 2026-1-1
// 场景2:用 int 直接传参给 Date 类型的函数(隐式转换)
func(2025); // 自动把 2025 转成 Date 对象,输出 2025-1-1
return 0;
}
代码中将int类型准换为Date类型,就是将int作为参数传入类的构造函数中,其中Date d = 2026,与 func(2025),这两句代码就是隐式类型转换,如果不想使用隐式类型转换,那就加上explicit关键字,这样的话就要显式创建对象,再进行转换:
cpp
Date d = Date(2026)
是用显示类型转换的好处是能让代码更具有健壮性,比如你写func(1000)其实是想传int进去的,但是编译器会误认为你是想进行类型转换的操作。那关键字explicit加在哪里呢?这样子:
cpp
//禁用隐式类型转换
explicit Date(int n):
_year(n),_month(1),_day(1)
{}
//......
2.2、类类型之间的转换:
如果想进行自定义类型之间的转换:目标类有一个「以源类为参数 」的构造函数(或转换构造函数)。
请看下面的代码:
cpp
#include <iostream>
using namespace std;
// 源类:Time
class Time {
public:
Time(int hour = 0, int minute = 0)
: _hour(hour), _minute(minute)
{
}
int GetHour() const { return _hour; }
private:
int _hour;
int _minute;
};
// 目标类:Date
class Date {
public:
// 转换构造函数:支持 Time → Date 隐式转换
Date(const Time& t)
: _year(2026), _month(4), _day(t.GetHour())
{
cout << "Date(const Time&) 转换构造函数调用" << endl;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 测试函数:接收 Date 对象
void func(Date d) {
d.Print();
}
int main() {
Time t(3, 30); // 创建 Time 对象
// 场景1:Time → Date 隐式转换
Date d = t; // 自动调用 Date(const Time&),完成转换
d.Print(); // 输出 2026-4-3
// 场景2:Time 直接传参给 Date 类型的函数(隐式转换)
func(t); // 自动把 t 转成 Date 对象,输出 2026-4-3
return 0;
}
在这段代码中,我们将Time类型变量t作为参数传给了目标类Date,用于初始化Date中的成员_day,这样就实现了Time类型向Date类型的转换。
小贴士:类型转换的本质实际上就是传参数,将一个参数传入另一个类中,从而创建出另一个类的对象。
三、static成员
用static修饰成员变量, 我们称之为静态成员变量。具体的我们通过下面的代码来体会:
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a1 = 1;
int _a2 = 2;
static int _count;
public:
};
int main() {
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
输出的结果是8,可见,static修饰的成员变量并不在类中,我们可以这么理解为:static是一个收到类域和访问限定符限制的全局变量。专门记录这个类下的一些数据,比如创建了几个对象
在private中,我们都说那是成员函数声明的地方,可以给缺省值,但是static修饰的成员变量不能加上缺省值,因为它不会经过初始化列表,我们将其他变量给上缺省值是为了在初始化列表时发挥作用,所以static变量要在全局进行定义和初始化。同时,static修饰的成员变量也不会经过构造函数。
如果static在公有域,就可以通过类域和访问限定符进行访问:
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a1 = 1;
int _a2 = 2;
public:
static int _count;
};
//全局定义和初始化
int A::_count = 0;
int main() {
A aa1;
cout << sizeof(aa1) << endl;
//通过访问限定符进行访问
cout << aa1._count << endl;
//通过类域进行访问
cout << A::_count << endl;
return 0;
}
如果我们在函数中一个空指针对static变量进行范访问,也是,没问题的:
cpp
A* ptr = nullptr;
cout << ptr->_count << endl;//输出结果为0
如果static变量是在私有域,我们可以通过静态函数来访问它,因为静态函数没有this指针:
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a1 = 1;
int _a2 = 2;
static int _count;
public:
static int GetCount() {
return _count;
}
};
//全局定义和初始化
int A::_count = 0;
int main() {
A aa1;
cout << sizeof(aa1) << endl;
A* ptr = nullptr;
cout << ptr->GetCount() << endl;
//通过访问限定符进行访问
cout << aa1.GetCount() << endl;
//通过类域进行访问
cout << A::GetCount() << endl;
return 0;
}
我们也说过了,static变量的重要作用之一就是统计这个类创建了多少个对象,像下面这样:
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a1 = 1;
int _a2 = 2;
static int _count;
public:
A() {
_count++;
}
A(const A& t) {
_count++;
}
static int GetCount() {
return _count;
}
};
int A::_count = 0;
int main() {
A aa1;
A aa2(aa1);
A aa3;
cout << A::GetCount() << endl;
return 0;
}
这时候就能统计出在这段代码中创建的对象为3个。
这里有一道挺有意思的题目:
大家可以去试试
还有两道选择题:

四、友元函数
我们在类和对象(中)的时候wield实现日期类的输出,使用了流运算符重载,流运算符重载就是要以友元函数的形式存在,因为他们需要重载成全局,但是又要访问私有成员,这种我们就将他们称为友元函数/友元类。
一个函数可以成为多个类的友元。具体的可以看下面的代码:
cpp
#include<iostream>
using namespace std;
//需要先声明B,不然A中的友元函数不认识类型B
class B;
class A {
private:
int _a1 = 1;
int _a2 = 2;
public:
//对Func()进行友元声明
friend void Func(const A& a, const B& b);
};
class B {
private:
int _b1 = 1;
int _b2 = 2;
public:
friend void Func(const A& a, const B& b);
};
//全局函数,需要同时输出A和B的成员变量的值
void Func(const A& a, const B& b) {
cout << "A:" << a._a1 << endl;
cout << "B:" << b._b1 << endl;
}
int main() {
A aa;
B bb;
Func(aa, bb);
return 0;
}
当一个类需要用到另一个类时,也可以使用友元函数。
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a1 = 1;
int _a2 = 2;
public:
//在A中声明类B
friend class B;
};
class B {
private:
int _b1 = 1;
int _b2 = 4;
public:
void Func(const A& a) {
cout << a._a1 << endl;
cout << _b1 << endl;
}
};
int main() {
A aa;
B bb;
bb.Func(aa);
return 0;
}
在这段代码中,类型B可以访问类型A,但是类A不能访问类B,这是单向的,除非在类型B中也声明类型A是友元类。
需要注意的点还有:

友元类/友元函数不具有传递性,想要多个关系两两都能访问你,那就要彼此都对对方声明友元函数/友元类。
友元函数/友元类具有耦合性,我们在以后的项目中最好是尽量少使用友元,因为两个区域中的代码相关性越小,维护起来就更加方便,对于需要经常维护的项目的要求是高内聚、低耦合。
五、内部类
内部类的重点:
- 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 内部类默认是外部类的友元类。
- 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。
具体的实现代码如下:
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; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
六、有/匿名对象
下面是有/匿对象的重点知识:
- ⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
- 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象
之前我们定义对象是这样子的:
cpp
#include<iostream>
using namespace std;
class A{
//...
};
int main(){
A aa1; //这个是有名对象
A(); //这个是匿名对象
return 0;
}
使用场景如下:其实就是临时使用一行,一行过后就销毁了:
cpp
#include<iostream>
using namespace std;
class A {
private:
int _a;
public:
A(int a = 0) :
_a(a)
{
cout << _a << endl;
}
~A() {
cout << "~A()" << endl;
}
};
class Solution {
public:
int solution(int n) {
return n;
}
};
int main() {
A aa1;
A();
A(1);
A aa2(2);
int ret = Solution().solution(10);
cout << ret << endl;
return 0;
}
如果是const solution& ret = solution(),会延长solution()的生命周期。
七、对象拷贝时的编译器优化
概念:
1、 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。
2、 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化
3、 linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的⽅式关闭构造相关的优化。
优化:
临时对象比较小,可以存在寄存器。
优化本质就是省略掉之间的临时对象。 
优化展示:

NRVO & URVO:
两者的官网地址:Copy elision - cppreference.com

连续一个表达式步骤中的连续拷贝会进行合并优化。

两组优化前后的对比:
1、

2、
对于编译器的优化,都是提前于规定的。
本段知识点代码如下:
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
// 构造+拷贝构造
A aa1;
f1(aa1);
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
cout << "**************************************************" << endl;
// 传值返回
// 不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
// 无优化 (vs2019 debug)
// 一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象优化为直接构造(vs2022 debug)
f2();
cout << endl;
// 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。(vs2022 debug)
A aa2 = f2();
cout << endl;
// 一个表达式中,开始构造,中间拷贝构造+赋值重载->无法优化(vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝临时对象合并为一个直接构造(vs2022 debug)
aa1 = f2();
cout << endl;
return 0;
}
结语:到这里,我们的C++类和对象就算正式结束了,下一篇文章我们会学习新的内容~~~
那么以上就是本次所有的内容了
文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~~~~
上一篇文章的链接: