目录
[一. 深剖构造函数](#一. 深剖构造函数)
[二. 类型转换](#二. 类型转换)
[三. static成员](#三. static成员)
[四. 友元](#四. 友元)
[五. 内部类](#五. 内部类)
[六. 匿名函数](#六. 匿名函数)
[七. 对象拷贝时的编译器优化](#七. 对象拷贝时的编译器优化)
一. 深剖构造函数
- 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后面跟⼀个放在括号中的初始值 或表达式。
- 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地方。
- 引用成员变量 ,const成员变量 ,没有默认构造的类类型变量,必须放在初始化列表位置进行初始 化,否则会编译报错。
- C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的 成员使用的。
- 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没 有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构 造会编译错误。
- 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
初始化列表总结:
- 无论是否显示写初始化列表,每个构造函数都有初始化列表
- 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化
不给缺省值时,不同类型的成员变量调用构造函数的情况:
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year = 2025, int month = 10, int day = 1)
://_year(year)
t(1) // 自定义类型会去调用它的构造
, ref(x) // 引用在初始化时,不能引用局部对象,否则函数结束后局部变量销毁,会造成野引用,最好引用外面的一个变量
,_n(20)
// 进入函数体之前,认为成员变量定义的地方在初始化列表,所以那三类必须在初始化列表初始化
{
// 内置类型可以用初始化列表初始化,也可以在函数体内部初始化;也可以既在初始化列表初始化,又在函数体内部初始化(最好别这样)
// 建议都使用初始化列表
_month = month;
_day = day;
}
private:
// 构造函数的初始化顺序与成员变量的声明顺序有关,与初始化列表中的顺序无关
// 调用构造时,先初始化初始化列表中的变量(按照声明顺序),在初始化函数体内部的成员变量
// 这里是成员变量的声明,声明不开空间
int _year;
int _month;
int _day;
// 这三个成员变量都有一个共同特点:必须在定义时初始化
// 因为这些成员变量需要在定义时初始化,所以这时就要用到初始化列表,初始化列表就是它们定义的地方
Time t; // 没有默认构造
int& ref; // 引用
const int _n; // const常量
};
int main()
{
// 对象整体定义,开空间了
// 当创建类的对象时,才会为定义类中的成员变量,为他们分配内存空间
// 初始化分配空间时,是按成员变量声明的顺序分配的
Date d1;
return 0;
}
给缺省值时,不同类型的成员变量调用构造函数的情况:
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
// 尽量在初始化列表初始化,即使没有在初始化列表初始化的成员也会走初始化列表,因为初始化列表是每个成员变量定义的地方
// 必须在初始化列表初始化,但是不一定非要显示初始化,也可以给了缺省值
// 给了缺省值就可以不显示初始化,编译器自己用缺省值在初始化列表初始化,如果显示初始化了就用显示的
Date()
:_year(100)
//, _t(1)
, _x(100)
{
}
private:
// 成员变量的声明,缺省值,不是初始化
// 如果不显示的写构造函数,编译器默认生成就会用这个值
// 如果显示的写构造函数,就用显示初始化的值
// 如果既没有给缺省值,也没有显示初始化就是随机值或0,具体看编译器
int _year = 1;
int _month = 1;
int _day = 1;
const int& ref = 0;
int* ptr = (int*)malloc(40); // 缺省值也可以是malloc表达式
Time _t = 1; // 自定义类型变量也可以给一个缺省值,给一个整型是因为它的构造函数参数类型为整型
const int _x = 10; // 常量类型变量也可以给一个缺省值,给了之后它就会用这个缺省值在初始化列表初始化
};
int main()
{
// 对象整体定义,开空间了
Date d1;
return 0;
}
// 总结:
// 1、一般情况下,建议尽量用初始化列表显示初始化
// 2、如果没有在初始化列表初始化,尽量给缺省值
// 自定义类型有默认构造,会调用它自己的默认构造,不用初始化列表显示初始化
// 除了自定义函数可以调用自己的默认构造的,可以既不显示初始化也没有缺省值,其他的情况必须显示初始化或者给缺省值
// 函数体在做一些检查,或更深层次的初始化等情况使用
class A
{
// 除了自定义函数可以调用自己的默认构造的,可以既不显示初始化也没有缺省值,其他的情况必须显示初始化或者给缺省值
// 函数体在做一些检查,后更深层次的初始化等情况使用
public:
A(int n = 10)
:_a((int*)malloc(sizeof(int) * 10))
, _size(0)
{
// 检查
if (_a == nullptr)
{
perror("malloc fail!");
exit(-1);
}
// 更深层次的初始化,给数组赋值
memset(_a, 0, sizeof(int) * n);
}
private:
int* _a;
int _size;
};
构造函数初始化列表逻辑梳理

总结:在构造函数中使用初始化列表
一、如果不显示的初始化成员变量
1、成员变量类型为内置类型:
a)有缺省值,那么就会用缺省值在初始化列表初始化
b)没有缺省值,那就是随机值
2、成员变量类型为自定义类型
a)有默认构造,有缺省值,传缺省值调用默认构造;有默认构造,没有缺省值,调用默认构造
b)没有有默认构造,有缺省值,传缺省值调用默认构造;没有默认构造,没有缺省值,报错二、显示初始化成员变量
1、成员变量类型为内置类型:
a)有缺省值,但是不会用缺省值,而是用初始化列表中的初始值
b)没有缺省值,就是初始化列表中的初始值2、成员变量类型为自定义类型
a)有默认构造,有缺省值,用初始化列表中的值调用默认构造;有默认构造,没有缺省值,用初始化列表中的值调用默认构造
b)没有默认构造,有缺省值,用初始化列表中的值调用构造;没有默认构造,没有缺省值,用初始化列表中的值调用构造
下面这段代码会打印出什么?
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{
}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
// 初始化列表按照成员变量在类中声明的顺序进行初始化
// 跟成员变量在初始化列表中出现的先后顺序无关
// 建议:声明和初始化列表顺序相同
int _a2 = 2;
int _a1 = 2;
};
int main()
{
// 当创建类的对象时,才会为定义类中的成员变量,为他们分配内存空间
// 初始化分配空间时,是按成员变量声明的顺序分配的
A aa(1);
aa.Print(); // 1和一个随机值
}
二. 类型转换
- C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
- 构造函数前面加explicit就不再支持隐式类型转换。
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
(1)内置类型之间的转换:
// 类型转换
int main()
{
// 有一定的关联才能转换:
//
// <1> 内置类型 - 内置类型 之间转换
// 整型家族(表示的范围不一样)
// 整形和浮点型,都能表示数据大小(表示的方式不一样)
// 整型和指针,指针是地址的编号
// 浮点数和指针之间,不能转换(强制类型转换都不行),没有关联关系
int i = 1;
double d = i; // 隐式类型转换,C规定,相近的类型可以进行隐式类型转换
// 给d时,中间会生成一个double类型的临时变量(临时对象)
const double& ref = i;
// 中间产生的临时变量具有常性,要加const
//int* pi = &i;
//float f = (float)pi; //err
return 0;
}
(2)内置类型转换为类类型对象:
// <2> C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
class A
{
public:
A(int a1)
:_a1(a1)
{
}
private:
int _a1 = 1;
int _a2 = 2;
};
// 引用传参一般都要加上const
void func(const A& aa = 1) // 引用的不是1,是临时对象
{}
class Stack
{
public:
void Push(const A& a)
{}
void Push(const string& str)
{}
private:
};
int main()
{
// 构造
A a1(1);
// 隐式类型转换
// 1构造⼀个A的临时对象,再用这个临时对象拷⻉构造a2
// 编译器遇到连续构造+拷贝构造->优化为直接构造
A a2 = 1;
// 1 可以通过构造函数转换为一个A的对象,这个对象是临时对象,它再拷贝构造给a2
// 为什么1能构造?因为A类的构造函数参数类型为整型,所以 1 能够通过构造函数转换为一个A的对象
const A& ref1 = a1;
const A& ref2 = 1; // 引用的是中间产生的临时对象,临时对象具有常性,要加const
func(1);
func(a1);
func(); // 也可以给缺省值
Stack st1;
A a3(3);
st1.Push(a3);
st1.Push(3); // 3能够通过构造函数转换为一个A的对象(编译器自动生成),这个对象是临时对象,然后这个对象会传给形参
string s1("xxxxx");
st1.Push(s1);
st1.Push("xxxxx");
return 0;
}
多参数情况和explict修饰
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a)
:_a1(a)
{}
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
// 构造
A a1(1);
// 类型转换
// 构造+拷贝构造 优化为-> 直接构造
A a2 = 1;
const A& ref1 = 0;
// 多参数
// 构造
A a3(1, 2);
// 多参数
// 类型转换
// 构造+拷贝构造 优化为-> 直接构造
A a4 = { 1,2 };
const A& ref2 = { 1,2 };
// 虽然编译器是直接构造,但是理论上还是{1,2}构造一个A的临时对象,再拷贝构造给ref2
// 而临时对象具有常性,所以要加const
A a5 = (1, 1); // 不能用小括号,编译器会解析为逗号表达式
// 逗号表达式:它将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值
return 0;
}
(3)类类型之间的转换:
// <3> 类类型的对象(自定义类型)之间也可以隐式类型转换,需要相应的构造函数支持
// 与内资类型转换为类类型类似,构造函数中参数的类型为该类类型,才能隐式类型转换
class A
{
public:
// 构造函数explicit就不再支持隐式类型转换
A(int a1 = 1)
// explicit A(int a1)
:_a1(a1)
{
cout << "A(int a1)" << '\n';
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
A(const A& aa) // 拷贝构造
{
cout << "A(const A& aa)" << '\n';
}
int Get() const
{
return _a1 + _a2;
}
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{
}
private:
int _b = 0;
};
int main()
{
A a1(1);
// 构造函数中参数的类型为该类类型,才能隐式类型转换
// 本质是a1作为参数调用B类的构造函数,转换为B类的对象,然后拷贝构造给b1
// 构造+拷贝构造 优化为-> 直接构造
B b1 = a1;
const B& ref = a1; // 引用的是临时对象,临时对象具有常性,要加const
return 0;
}
三. static成员
-
用static修饰的成员称之为静态成员,static成员包括static成员变量和static成员函数
-
静态成员是属于整个类的,不单独属于某个对象,为所有类对象所共享,不存在对象中,存放在静态区
-
静态成员变量一定要在类外进行初始化
-
静态成员函数没有this指针
-
静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为没有this指针
-
非静态成员函数可以访问任意的静态成员变量和静态成员函数
-
突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 或者 指针->静态成员 来访问静态成员变量和静态成员函数
-
静态成员也是类的成员,受public、protected、private访问限定符的限制
-
静态成员变量不能在声明的位置给缺省值,因为用缺省值实质是,用缺省值在构造函数初始化列表初始化成员变量,而静态成员变量不属于某个对象,不走构造函数初始化列表
class A
{
public:
A(int a = 1)
:_a1(a)
{}void Print1() { // 非静态成员函数可以访问任意的静态成员变量和静态成员函数 cout << _a1 << endl; cout << _a2 << endl; } static void Print2() // 静态成员函数 { // cout << _a1 << endl; // err 不能访问非静态的成员变量 cout << _a2 << endl; }
// private:
int _a1 = 1;
static int _a2; // 静态成员变量,不能给缺省值
};int A::_a2 = 1; // 静态成员变量一定要在类外进行初始化
int main()
{
A a;
A* ptr = nullptr;// 无论是成员变量还是成员函数都受访问限定符的限制,public才能在类外访问,private则不能 // 访问非静态成员函数 a.Print1(); ptr->Print1(); // 这里有点bug,因为是空指针,所以无法访问内部成员变量,这里主要是理解可以通过指针访问成员函数 // A::Print1(); //err 非静态成员函数不能使用这种方法 // 访问静态成员函数 a.Print2(); ptr->Print2(); A::Print2(); // 这个最好 // 访问非静态成员变量 a._a1; ptr->_a1; // // 这里有点bug,因为是空指针,所以无法访问内部成员变量,这里主要是理解可以通过指针访问成员变量 // A::_a1; //err 非静态成员变量不能使用这种方法 // 访问静态成员变量 a._a2; ptr->_a2; A::_a2; return 0;
}
// static定义的成员变量生命周期是全局的(可以看成类专属的全局变量)
class A
{
public:
// A类型的对象通过构造或拷贝构造创建,这两个函数调用了多少次,就创建了多少个对象
A(int a = 1) // 构造
:_a1(a)
,_a2(a)
{
++_count;
}
// 拷贝构造
A(const A& t)
{
++_count;
}// 静态成员函数 // 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针 // 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数 static int GetCount() { // _a1++; // 无法访问非静态的,因为没有this指针 return _count; }
private:
// 声明
int _a1 = 1;
int _a2 = 1;
// public:
// 声明
static int _count; // 这个变量不存放在类中,它存放在静态区
// 可以理解为这个变量是这个类专属的,并且受到类域和访问限定符的限制的全局变量
// 因为它不再对象里面存放,所以它不会走构造函数
// 因此也不能给它缺省值,因为缺省值本质是,初始化列表没有初始化时,用这个值在初始化列表初始化,而初始化列表就是构造函数的一部分
};int A::_count = 0;
int main()
{
A aa1;
cout << sizeof(aa1) << endl; // 8 对象的大小不包括静态成员变量
A* ptr = nullptr;A aa2 = 1; // 这里是直接调用构造 // 因为_count是所有类共享的,每个对象访问的都是同一个 /*cout << ptr->_count << endl; cout << aa1._count << endl; cout << A::_count << endl;*/ // cout << _count << endl; //err // 突破类域和访问限定符就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 或者 指针-> 静态成员 来访问静态成员变量和静态成员函数。 // 只有静态成员函数可以通过类名直接调用 cout << A::GetCount() << endl; cout << aa1.GetCount() << endl; cout << ptr->GetCount() << endl; return 0;
}
求下列代码的构造顺序和析构顺序?
C c;
int main()
{
A a;
B b;
static D d; // 局部static对象,都是在第一次运行到定义的位置时,才初始化
return 0;
}
// 析构时,先析构栈区,在析构静态区,全局对象和局部静态对象都在静态区,析构时也遵循后定义的先析构
// 注:局部对象在离开其作用域时析构(该段代码作用域为main函数)
// 静态局部对象和全局对象在程序完全结束时析构,而不在离开其作用域时(该段代码作用域是main函数),即main函数结束时
// 构造函数的调用顺序为:C A B D
// 析构函数调用的顺序为:B A D C
四. 友元
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类 声明的前面加friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另⼀个类中的私有和保护成员
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元
- 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元
- 有时提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用
(1)友元函数
// 友元函数
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元函数声明
// 可以放在类内部的任意地方
friend void func(const A& aa, const B& bb); // 编译器都是向上找的,找它的声明或定义,而B类定义在A的下面,向上找不到,所以要在前面声明一下
private: // 这个友元函数声明在上定义在两个类的下面,所以两个类的细节都能用到
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元函数声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
// 一个函数可以成为多个类的友元
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
(2)友元类
// 友元类
// 友元类是单向的,比如C是D的友元,D不是C的友元
// D要访问C,D要声明为C的友元
// 可以让他们互相成为友元
// 一个类中的成员函数可以成为另一个类的友元,但是比较麻烦,我们可以直接让这个类成为另一个类的友元
class C
{
friend class D; // 这是友元函数的声明,同时也算是类的前置声明,
// 但是如果用不到类D里面的细节,比如成员变量,声明和定义分离能很好的解决这个问题
public:
void func1(const D& dd)
{
// cout << dd._d1 << endl; // err 要访问dd对象中的成员变量,会向上找D类,但是上面只有声明,没有定义,所以访问不到D类内部的细节
cout << _c1 << endl;
}
void func2(const D& dd)
{
// cout << dd._d2 << endl; // err 访问不到D类内部的细节
// cout << _c2 << endl;
}
private:
int _c1 = 1;
int _c2 = 2;
};
class D
{
friend class C;
public:
void func1(const C& cc)
{
cout << cc._c1 << endl;
cout << _d1 << endl;
}
void func2(const C& cc)
{
cout << cc._c2 << endl;
cout << _d2 << endl;
}
private:
int _d1 = 3;
int _d2 = 4;
};
声明和定义分离解决访问不到类中成员的问题
// 声明和定义分离就能很好解决这个问题
// xxx.h
class C
{
friend class D; // 这是友元函数的声明,同时也算是类的前置声明,
// 但是如果用不到类D里面的细节,比如成员变量,声明和定义分离能很好的解决这个问题
public:
void func1(const D& dd);
void func2(const D& dd);
private:
int _c1 = 1;
int _c2 = 2;
};
class D
{
friend class C;
public:
void func1(const C& cc);
void func2(const C& cc);
private:
int _d1 = 3;
int _d2 = 4;
};
// xxx.cpp
void C::func1(const D& dd)
{
cout << dd._d1 << endl;
cout << _c1 << endl;
}
void C::func2(const D& dd)
{
cout << dd._d2 << endl;
cout << _c2 << endl;
}
void D::func1(const C& cc)
{
cout << cc._c1 << endl;
cout << _d1 << endl;
}
void D::func2(const C& cc)
{
cout << cc._c2 << endl;
cout << _d1 << endl;
}
五. 内部类
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在 全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类
- 内部类默认是外部类的友元类。
- 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
内部类的使用
class A
{
public:
A(int n = 1)
:_a1(n)
, _a2(_a1)
{}
// void fun1(const B& refb) // classB的声明在该函数之后,向上找不到B类
// {}
// 内部类 是一个独立的类,不是A类的成员;受类域和访问限定符的限制
class B // B默认为A的友元,B中可以访问A中的成员
{
public:
B(int m = 1)
:_b1(m)
{}
void func2(const A& refa)
{
cout << refa._a1 << endl;
}
private:
int _b1;
};
//void fun1(const B& refb)
//{
// cout << refb._b1 << endl; // err 可以找到B类,但是A不是B的友元,无法访问B的成员变量,在B中加友元声明才能访问B
//}
private:
int _a1;
int _a2;
};
int main()
{
cout << sizeof(A) << endl; //8 A类中不报包含B类
A::B b;
A aa;
b.func2(aa);
return 0;
}
内部类的应用
class Solution {
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
public:
// 防止多次调用时累加
void clear()
{
_i = 1;
_ret = 0;
}
int Sum_Solution(int n) {
// 变长数组 vs不支持
// Sum arr[n];
Sum* ptr=new Sum[n];
delete[] ptr;
return _ret;
}
~Solution()
{
cout << "~Solution()" << endl;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
int main()
{
Solution s;
cout << s.Sum_Solution(5) << endl; // 15
s.clear();
cout << s.Sum_Solution(3) << endl; // 6
return 0;
}
六. 匿名函数
- 用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的 叫有名对象
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象
匿名对象的使用
class Solution {
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
public:
// 防止多次调用时累加
void clear()
{
_i = 1;
_ret = 0;
}
int Sum_Solution(int n) {
// 变长数组 vs不支持
// Sum arr[n];
Sum* ptr=new Sum[n];
delete[] ptr;
return _ret;
}
~Solution()
{
cout << "~Solution()" << endl;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
void Func(const Solution& s = Solution()) // 给匿名对象缺省值;const引用会延长匿名对象的生命周期
{}
// 匿名对象
// 之前我们定义的对象都叫有名对象
int main()
{
Solution s; // 有名对象
cout << s.Sum_Solution(10) << endl;
s.clear();
// 生命周期只在当前一行,下一行它就销毁了
// Solution(); // 匿名对象
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
cout << Solution().Sum_Solution(10) << endl; // 匿名对象只能在当前行使用
Func(Solution()); // 匿名对象具有常性
const Solution& ref = Solution(); // const引用会延长匿名对象的生命周期
Func(Solution());
Func(s);
Func();
return 0;
}
七. 对象拷贝时的编译器优化
-
现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。
-
如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
-
linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造相关的优化。
// 对象拷贝时的编译器优化
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)
{}//int main()
//{
// // 传值传参
//
// // 构造+拷贝构造 优化-> 构造
// A aa1 = 1;
// cout << "--------------------" << endl;
//
// // 有名对象不优化,直接拷贝
// f1(aa1);
// cout << "--------------------" << endl;
//
// // 原本应该是把1作为参数构造一个临时对象,然后这个临时对象再拷贝构造传给函数,编译器优化为直接构造然后传给函数
// f1(1); // 隐式类型转换
// cout << "--------------------" << endl;
//
// // 同上
// f1(A(1)); // 匿名对象
// cout << "--------------------" << endl;
//
// return 0;
//}A f2()
{
// NRVO(命名返回值优化)
//A aa;
//cout << &aa << endl;
//return aa; // aa在返回时就销毁了,返回的是aa的拷贝// UPVO(未命名返回优化) return A(1); // 匿名对象
}
int main()
{
// 传返回值
A aa1 = f2();
// 不优化,整个过程:
// 1、f2函数定义对象aa,调用构造函数
// 2、aa拷贝构造给临时对象,调用拷贝构造
// 3、aa拷贝后f2函数结束,调用析构函数,析构aa
// 4、临时对象拷贝构造给aa1,调用拷贝构造
// 5、临时对象拷贝后,声明周期也结束了,调用析构函数析构// 优化后: // 可以理解为aa是aa1的引用,aa的改变就会影响aa1 cout << &aa1 << endl; // aa和aa1地址相同 return 0;
}
结语
如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!