【C++进阶笔记】对象优化(1)
文章目录
1. 类内的函数调用
C++构造对象时可能会调用的函数:构造函数、拷贝构造函数以及赋值重载函数。对象结束作用域时会调用析构函数。
1.1 对象构造时
示例
#include <iostream>
using namespace std;
class Test {
public:
Test(int a = 5, int b = 5) : m_A(a), m_B(b) {
cout << "Test(int, int)" << endl;
}
~Test() {
cout << "~Test()" << endl;
}
Test(const Test& other) : m_A(other.m_A), m_B(other.m_B) {
cout << "Test(&)" << endl;
}
void operator=(const Test& other) {
m_A = other.m_A;
m_B = other.m_B;
cout << "operator=" << endl;
}
int getData() const {
return m_A;
}
private:
int m_A;
int m_B;
};
Test t1(10, 10);
int main() {
Test t2(20, 20);
Test t3 = t2;
static Test t4 = Test(20, 20);
t2 = Test(20, 20);
t2 = (Test)(20, 20);
t2 = 60;
Test* t5 = new Test(20, 20);
Test* t6 = new Test[2];
//Test* t7 = &Test(30, 30);
const Test& t8 = Test(20,20);
delete t5;
delete[] t6;
return 0;
}
- t1是全局变量,最先构造,调用构造函数
t3 = t2
调用的时拷贝构造函数,并不是赋值重载函数,不能简单地认为 ’=‘ 就代表了调用了赋值重载函数。Test t4 = Test(20, 20)
等价于Test t4(20, 20)
, 调用构造函数t2 = Test(20, 20)
, 调用赋值重载函数,在赋值重载函数前会调用构造函数构造临时对象,完成复制后,临时对象调用析构函数。t2 = (Test)(20, 20)
和t2 = 60
, 前者属于强转,(20, 20)等价于20,调用构造函数构造临时对象,然后调用赋值函数;后者将一个整形变量赋值给类对象,编译器同样将整形作为参数调用构造函数构造临时对象,然后调用赋值函数。临时对象都会析构。- t5, t6 属于new出来的对象,调用构造函数
- t7 为非法赋值(在某些编译器可以编译通过)。
Test(30, 30)
先构造临时对象,然后t7作为类指针指向该临时对象,但是临时对象在结束该语句时析构,相对于t7指向了一块被释放的内存(野指针!)。 Test &t8 = Test(20, 20)
类似于上面的t4, 调用构造函数。
1.2 函数(类外的)调用时
讨论类的对象作为参数被调用时所调用的函数。
示例
#include <iostream>
using namespace std;
class Test {
public:
Test(int a = 5) : m_A(a) {
cout << "Test(int)" << endl;
}
~Test() {
cout << "~Test()" << endl;
}
Test(const Test& other) : m_A(other.m_A) {
cout << "Test(&)" << endl;
}
void operator=(const Test& other) {
m_A = other.m_A;
cout << "operator=" << endl;
}
int getData() const {
return m_A;
}
private:
int m_A;
};
Test getObject(Test t) {
int val = t.getData();
Test tmp(val);
cout << "----------------------" << endl;
return tmp;
}
int main() {
Test t1;
Test t2;
t2 = getObject(t1);
return 0;
}
这里我的运行结果也视频中的结果不一样,主要是在函数返回值处,视频中的结果显示调用的拷贝构造来产生局部变量的临时变量。但我的结果中并未调用拷贝构造函数。以下是GPT-4的解释。
GPT-4 : 函数执行到末尾,返回局部对象 tmp。由于返回的是局部对象,通常情况下会调用拷贝构造函数来创建返回值的副本。然而,在C++11及以后的版本中,编译器可能会使用(Named)返回值优化((N)RVO),以避免不必要的拷贝构造调用。一般的g++编译器都是默认开启此优化的。
此外,需要注意的是t2 = getObject(t1);
实参 t1 到形参 t 属于初始化,调用拷贝构造函数构造 t。
2.三条优化准则
2.1 函数参数传递过程中,对象优先按引用传递,不要按值传递
值传递:Test getObject(Test t)
引用传递:Test getObject(Test &t)
使用引用传递优点:调用getObject函数时,从实参到形参不需要调用拷贝构造函数,同样也减少了一次析构函数的调用。引用传递相当于是调用对象的另一个名字,值传递的形参是一个全新的临时变量,需要调用拷贝构造函数去构造。
2.2 函数返回对象时,优先返回一个临时对象,而不要返回定义过的对象
Test tmp(val); return tmp;
上面的两行代码,是通过构造临时对象,并返回临时对象。可将上面两行改写为:
return Test(val);
直接返回一个临时对象,该情况下不会调用任何构造函数或者拷贝构造函数。该return语句的任务是要在main的栈帧上构造一个对象,将此对象赋值给t2。所以我们可以用临时对象拷贝构造一个新对象(1.1中的第3条),这样只会调用一次构造函数,不会产生临时对象后再调用拷贝构造再析构。类似于上面提到的RVO(返回值优化)。
2.3 接收返回值是对象的函数调用时,优先按初始化的方式接收,不要按赋值的方式接收
1.Test t1; Test t2; t2 = getObject(t1);
改写为以下的代码:
2.Test t1; Test t2 = getObject(t1;)
代码2同样是用临时对象拷贝构造一个新对象(1.1中的第3条),临时对象会被优化,只会调用构造函数。代码1属于赋值,必须将存在临时对象才能进行赋值。
通过上面三次优化,最后只调用了两次构造函数和析构函数,效率大大提高。
3. 右值引用、移动语义、完美转发
3.1 CMyString的问题
class CMyString {
public:
CMyString(const char* str = nullptr) {
cout << "CMyString(const char*)" << endl;
if (str != nullptr) {
mptr = new char[strlen(str) + 1];
strcpy(mptr, str);
} else {
mptr = new char[1];
*mptr = '\0';
}
}
~CMyString() {
cout << "~CMyString" << endl;
delete[] mptr;
mptr = nullptr;
}
CMyString(const CMyString& str) {
cout << "CMystring(const CMyString&)" << endl;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
}
CMyString& operator=(const CMyString& str) {
cout << "operator=(const CMyString)" << endl;
if (this == &str) {
return *this;
}
delete[] mptr;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
return *this;
}
const char* c_str() const {
return mptr;
}
private:
char* mptr;
};
CMyString GetString(CMyString& str) {
const char* pstr = str.c_str();
CMyString tmpstr(pstr);
return tmpstr;
}
int main() {
CMyString str1("aaaaaaaaaaaaaaaaaaaa");
CMyString str2;
str2 = GetString(str1);
cout << "----------------" << endl;
return 0;
}
问题:str2 = GetString(str1)
构造临时变量,赋值给str2,然后析构临时变量。假设mptr指向了一段很长的字符串,那么构造临时变量会造成很大的内存消耗,同时,该临时变量只是赋值给了str2,之后面的语句中没有任何作用便析构掉了,这是一种很大的浪费,因此,我们希望能省去构造临时变量的过程。直接将str1的mptr赋值给str2的mptr。(临时对象是使用后即销毁的资源,所以不希望其去占用外部资源)
3.2 优化
3.2.1 右值引用
左值:有名字,有内存(既可以出现在等号左边又可以在等号右边)。
右值:没有名字(临时量),无内存(只能出现在等号右边),举例:常量、函数返回值等。
int a = 10; // a 属于左值
int &b = a; // 左值引用
int &&c = a; // 错误,左值不能赋值给右值引用
int && d = 20; // 右值引用
int &e = d // 一个右值引用变量,本身是一个左值
// 带左值引用参数的拷贝构造
CMyString(const CMyString& str) {
cout << "CMystring(const CMyString&)" << endl;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
}
// 带右值引用参数的拷贝构造
CMyString(CMyString&& str) {
cout << "CMystring(const CMyString&&)" << endl;
mptr = str.mptr;
str.mptr = nullptr;
}
// 带左值引用参数的赋值重载函数
CMyString& operator=(const CMyString& str) {
cout << "operator=(const CMyString&)" << endl;
if (this == &str) {
return *this;
}
delete[] mptr;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
return *this;
}
// 带右值引用参数的赋值重载函数
CMyString& operator=(CMyString&& str) {
cout << "operator=(const CMyString&&)" << endl;
if (this == &str) {
return *this;
}
delete[] mptr;
mptr = str.mptr;
str.mptr = nullptr;
return *this;
}
在CMyString类中添加带右值引用参数的拷贝构造和带右值引用参数的赋值重载函数,str2 = GetString(str1)
,等号右边出现临时变量,编译器优先匹配参数带右值引用的赋值重载函数,可以省去内存的开辟。(相当于深拷贝于浅拷贝的区别)
3.2.2 移动语义
int main() {
CMyString str1("aaaaaaaaaaaaaaaaaaaa");
vector<CMyString> vec;
vec.reserve(10);
cout << "-----------------" << endl;;
vec.push_back(move(str1));
vec.push_back(CMyString("bbb"));
cout << "-----------------" << endl;
return 0;
}
#include <utility>
std::move();
move(str1)
返回的是一个右值引用,调用移动构造。如果str1在之后都不会再使用,可以使用move调用移动构造,使用浅拷贝,减少内存的开辟。
3.2.3 完美转发
对象再经过函数参数传递时,其类型可能会改变,例如从右值变为左值。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
上面的代码与图片来自【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
模板中的 && 不代表右值引用,而是万能引用,其既能接受左值,又能接受右值。模板的万能引用只是提供了能够同时接受左值和右值的能力(包括 const 左值和 const 右值)。如上代码:实参如果传递的是一个左值 a,那么模板实例化出来的就是左值引用 int& t,有的书上也管这个叫引用折叠;实参如果传递的是一个右值 10,那么模板实例化出来的就是 int&& t(const 左值和 const 右值也类似)。
————————————————
版权声明:本文为CSDN博主「春人.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_63115236/article/details/133908793
可以看到结果与我们的预期似乎有出入,常量10以及std::move(a)
都是右值,但是最后打印出来的却是左值引用。上面提到过右值引用本身还是一个左值,变量传递到Fun()
函数时,都是左值引用,我们希望能够变量的类型进行完美的传递,可以使用完美转发std::forward
。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));//进行完美转发
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
上面的代码与图片来自【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
4. 结语
看了施磊的C++进阶课程,重新学了一下右值引用、移动语义和完美转发,也搞懂了之前一些比较模糊的概念。不同的写法,对函数方法的调用也有着巨大的差别,尤其在数值相对较大时,性能差距就会比较明显。