对象使用背后调用的方法
#include<iostream>
using namespace std;
class Test
{
public:
Test(int a = 10):ma(a) {cout<<"Test(int)"<<endl;}
~Test(){cout<<"~Test()"<<endl;}
Test(const Test& t):ma(t.ma) {cout<<"Test(const Test& t)"<<endl;}
Test& operator=(const Test& t)
{
cout<<"operator="<<endl;
ma = t.ma;
return *this;
}
private:
int ma;
};
int main()
{
Test t1; //构造
Test t2(t1); //拷贝构造
Test t3 = t1; //拷贝构造
//Test(20) 显式生成临时对象 生存周期:所在的语句
Test t4 = Test(20);
/* 调用过程? 先调用构造生成临时对象,再调用拷贝构造,析构? */
return 0;
}
输出结果:
从输出结果我们可以看到,C++编译器对于对象构造做出的优化:用临时对象生成新对象的时候,临时对象就不产生了,直接构造新对象就可以了,即 Test t4 = Test(20) 等价于 Test t4(20)
int main()
{
Test t1;
Test t2(t1);
Test t3 = t1;
//Test(20)
Test t4 = Test(20);
t4 = t1; //拷贝构造
t4 = Test(30);
return 0;
}
用临时对象给已有对象赋值会先创建临时对象,再调用赋值运算符重载
显式 or 隐式 生成临时对象:
int main()
{
Test t4;
//显式生成临时对象
t4 = Test(30);
t4 = (Test)30;
//隐式生成临时对象
t4 = 30;
return 0;
}
指针 引用 赋值临时对象:
int main()
{
Test *p = &Test(30);
const Test &ref = Test(40);
return 0;
}
无法对临时对象取地址,临时对象的生命周期只在所在语句,出语句临时对象就会被析构
引用是变量的别名,可以用临时对象赋值给常对象
示例:
#include<iostream>
using namespace std;
class Test
{
public:
Test(int a = 5,int b = 5): ma(a), mb(b)
{
cout << "Test(int,int)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
Test(const Test &src): ma(src.ma), mb(src.mb)
{
cout << "Test(const Test &)" << endl;
}
void operator=(const Test &src)
{
ma = src.ma;
mb = src.mb;
cout << "operator=" << endl;
}
private:
int ma;
int mb;
};
Test t1(10, 20); // 1.Test(int,int)
int main()
{
Test t2(20,20); // 3.Test(int,int)
Test t3 = t2; // 4.Test(const Test &)
static Test t4 = Test(30, 40); // 5.Test(int,int)
t2 = Test(40, 50); // 6.Test(int,int) operator= ~Test()
t2 = (Test)(50, 60); // 7.Test(int,int) operator= ~Test()
t2 = 60; // 8.Test(int,int) operator= ~Test()
Test *p1 = new Test(70, 80); // 9.Test(int,int)
Test *p2 = new Test[2]; // 10.Test(int,int) Test(int,int)
const Test &p3 = Test(90, 100); // 11.Test(int,int)
delete p1; // 12.~Test() free(p1)
delete[] p2; // 13.~Test() ~Test() free(p2)
}
Test t5(100,100); // 2.Test(int,int)
运行一下~
函数调用过程中对象背后调用的方法
参考以下示例代码,分析其函数调用过程
#include<iostream>
using namespace std;
class Test
{
public:
Test(int a = 10):a_(a)
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
Test(const Test& t):a_(t.a_)
{
cout << "Test(const Test& t)" << endl;
}
void operator=(const Test& t)
{
cout << "operator=(const Test& t)" << endl;
a_ = t.a_;
}
int getA() const { return a_; }
private:
int a_;
};
Test GetObj(Test t)
{
int val = t.getA();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
首先main函数里的 t1、t2 调用构造函数,GetObj(t1) 调用 t1 给 GetObj函数初始化形参Test t,接下来tmp调用构造函数,return tmp 构造了一个临时对象,GetObj函数结束,进行局部对象析构,先对tmp进行析构,然后对形参 t 进行析构;赋值运算符重载,使用临时对象对 t2 赋值,接着临时对象析构,t2、t1析构
三条对象优化的规则
- 函数参数传递过程中,对象优先按引用传递,不要按值传递
- 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
- 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
下面通过代码示例的方式,分析逐条规则的含义:
函数参数传递过程中,对象优先按引用传递,不要按值传递
Test GetObj(Test &t)
{
int val = t.getA();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
Test GetObj(Test &t)
{
int val = t.getA();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
第一段代码 Test 函数形参采用的是对象的值传递,第二段采用的是引用传递,从输出结果来看,采用引用传递的方式输出少了 一段拷贝赋值运算符重载;这是由于采用值传递会先拷贝一份传入参数的数据,用于构建 Test 中的临时对象,如果采用引用传递,可以避免此操作的开销
函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
Test GetObj(Test &t)
{
int val = t.getA();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
Test GetObj(Test &t)
{
int val = t.getA();
return Test(val);
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
在 Test 函数编写过程中,先定义临时对象在返回会先调用构造函数,再调用析构;而将临时对象放在 return 中定义,编译器会用构造的临时对象去拷贝构造新对象,会直接构造新对象,优化了临时对象产生的问题
接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
Test GetObj(Test &t)
{
int val = t.getA();
return Test(val);
}
int main()
{
Test t1;
Test t2;
t2 = GetObj(t1);
return 0;
}
Test GetObj(Test &t)
{
int val = t.getA();
return Test(val);
}
int main()
{
Test t1;
Test t2 = GetObj(t1);
return 0;
}
对比分析可以看到编译器以同样地方式进一步作出优化,临时对象不生成了,直接调用两次构造、析构后返回
CMyString代码问题
#include<iostream>
#include<cstring>
using namespace std;
class CMyString
{
public:
CMyString();
CMyString(const char* str)
{
cout << "CMyString(const char* str)" << 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& str)" << endl;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
}
CMyString& operator=(const CMyString& str)
{
cout << "operator=" << 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 GetMyString(CMyString &str)
{
const char* temp = str.c_str();
CMyString tempStr(temp);
return tempStr;
}
int main()
{
CMyString str("hello");
CMyString str2;
str2 = GetMyString(str);
cout << str2.c_str() << endl;
return 0;
}
当GetMyString函数中调用构造函数生成临时对象tempStr时,会开辟一块堆上内存复制 str 中 mptr 的内容,因为GetMyString函数结束,临时对象要析构,函数栈帧回退,故 tempStr 临时对象无法返回,此时返回的方式是通过调用拷贝构造,在main函数栈帧上开辟新的内存,创建对象来返回;然后main函数中 str2 调用赋值运算符重载复制GetMyString函数返回的内容(会开辟内存),参考上图,这样就会导致内存频繁开辟释放,如果对象复杂就会造成极大的资源浪费!!!
添加带右值引用参数的拷贝构造和赋值函数
int main()
{
//左值:有内存、有名字 右值:没名字(临时量)、没内存
int a = 10; //定义一个左值
int &b = a; //左值引用
//int &&c = a; //无法将右值引用绑定到左值
//int &c = 10; //无法将左值引用绑定到右值
/*
int tmp = 10;
const int &c = tmp;
*/
const int &c = 10;
/*
int tmp = 20;
const int &&d = tmp;
*/
int &&d = 20; //右值引用
CMyString &&e = CMyString("aaa"); //右值引用 临时对象
//int &&f = d; //无法用右值引用 引用一个右值引用变量
int &f = d; //一个右值引用变量,本身是一个左值
return 0;
}
CString添加带右值引用参数的拷贝构造和赋值运算符重载函数
//带左值引用参数的拷贝构造
CMyString(const CMyString &str)
{
cout << "CMyString(const CMyString&)" <<endl;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
}
//带右值引用参数的拷贝构造
CMyString(CMyString &&str) //str引用的是一个临时对象
{
cout << "CMyString(CMyString&&)" <<endl;
mptr = str.mptr;
str.mptr = nullptr;
}
//带左值引用的赋值运算符重载函数
CMyString& operator=(const CMyString& str)
{
cout << "operator=(const CMyString& str)" << 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=(CMyString&& str)" << endl;
if (this == &str)
{
return *this;
}
delete[] mptr;
mtpr = str.mptr;
str.mptr = nullptr;
return *this;
}
更改临时对象的mptr指针
operator+函数实现过程
CMyString operator+(const CMyString& str1, const CMyString& str2)
{
char *ptmp = new char[strlen(str1.mptr) + strlen(str2.mptr) + 1];
strcpy(ptmp, str1.mptr);
strcat(ptmp, str2.mptr);
CMyString tmpStr(ptmp);
delete []ptmp;
return tmpStr; //CMyString& operator=(CMyString&& str)
}
int main()
{
CMyString str1 = "hello";
CMyString str2 = "world";
CMyString str3 = str1 + str2;
cout << str3 << endl;
return 0;
}
这段代码实现了对于 “+” 的赋值运算符重载,首先开辟一块内存,之后调用构造函数,释放开辟的内存,最后返回对象;同样,这样的操作在赋值运算 “+” 的过程中,开辟新的内存用于临时存储指针内容,又马上释放,增加了开销,造成内存浪费,为了避免此问题,作出如下优化:
CMyString operator+(const CMyString& str1, const CMyString& str2)
{
CMyString temp;
temp.mptr = new char[strlen(str1.mptr) + strlen(str2.mptr) + 1];
strcpy(temp.mptr, str1.mptr);
strcat(temp.mptr, str2.mptr);
return temp;
}
int main()
{
CMyString str1 = "hello";
CMyString str2 = "world";
CMyString str3 = str1 + str2;
cout << str3 << endl;
return 0;
}