在之前,我写过一篇:
通过自定义string类型来理解运算符重载。
在文章末尾,我提出了一个问题:
那么如何能够实现不泄露内存,并且提高效率呢?
那么,今天就来看看如何解决这个问题。
首先是对string类型的一个简单实现,代码如下:
class CMyString
{
public:
CMyString(const char* str = nullptr)
{
cout << "CMyString(const char*)" << endl;
if (str != nullptr)
{
_pstr = new char[strlen(str) + 1];
strcpy(_pstr, str);
}
else
{
_pstr = new char[1];
*_pstr = '\0';
}
}
~CMyString()
{
cout << "~CMyString" << endl;
delete[] _pstr;
_pstr = nullptr;
}
CMyString(const CMyString& str)
{
cout << "CMyString(const CMyString&)" << endl;
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
CMyString& operator=(const CMyString& src)
{
cout << "operator=(const CMyString&)" << endl;
if (this == &src)
return *this;
delete[]_pstr;
_pstr = new char[strlen(src._pstr) + 1];
strcpy(_pstr, src._pstr);
return *this;
}
const char* c_str()const {
return _pstr; }
private:
char* _pstr;
};
};
注意:
在vs2017
、vs2019
这样版本较新的编译器,使用strcpy()
函数一般都会报错:
对于vs2017
来说,可以直接在项目
->属性
:
C/C++
-> 常规
-> SDL检查
-> 改为否
之后点击应用
-> 确定
就好了。
但是对于vs2019
来说,上述方法并不起作用:
博主经过实践,发现可以用如下方法解决:
在文件开始添加宏定义:
#pragma warning(disable:4996)
这样,就可以使用strcpy()
了!
之前的博客提到过,对于正常的逻辑实现,这个代码是没有问题的,但是涉及到对象的优化
,
(可以移步到我的这篇博客:C++对象优化)
或者换句话说,涉及到提高代码的效率,就会出现问题:
同样地,我们使用如下代码:
CMyString GetString(CMyString& str)
{
const char* pstr = str.c_str();
CMyString tmpstr(pstr);
return tmpstr;
}
int main()
{
CMyString str1("aaaaaaaaaaaaaaaaaaaaaaaaa");
CMyString str2;
str2 = GetString(str1);
cout << str2.c_str() << endl;
return 0;
}
可能直接从代码运行结果来看并不太直观,我们来画图分析:
解读:
首先第一步、第二步很好理解,都是调用了构造函数进行对象的构造;
接着实参到形参
因为使用引用传递
,所以不会产生新对象;
进入GetString()
函数,首先构造局部对象tmpstr
;
接着就执行return
语句,由于局部对象不能带出局部作用域,所以为了能够带出局部对象的值
,需要在主函数栈帧上构建一个临时对象,从局部对象到临时对象调用了拷贝构造函数
;
关键点:
这个时候我们的代码的逻辑是:
只要执行拷贝构造函数
,就会依据原对象
的尺寸大小来开辟一个新的空间
,然后将原来的数据依次拷贝(strcpy)进新的对象空间
。
这样的逻辑本身没有什么问题,但是一般自定义的类型都会在堆上
开辟一定的空间,并且,这个空间中的数据可能很多。
执行一次拷贝构造,可能需要大量的内存开辟
和数据复制
。
最关键的,规模如此庞大的内存开辟
和数据复制
完成后,原来的对象就析构了!
那你早说啊!你把你的资源直接给我不就好了?
浪费这么多感情和精力干嘛?
同理,在主函数的栈帧上构建的临时对象给str2
进行赋值的时候,也同样需要大量的内存开辟
和数据复制
,拷贝完成后,临时对象也就析构掉了。
所以,对于这种涉及到临时对象
的构造和赋值时,我们不能使用常规的逻辑:
将我的资源复制一份给你
;
而应该转换逻辑:
将我的资源转移给你
(因为反正我也不用了即将析构掉
)
这样的话,代码的效率会有很大的提升!
那么如何解决上述的问题呢?
其实通过分析我们已经知道,问题就出在临时对象
上面。因为拿正常的逻辑去对临时对象
操作,会有大量资源的消耗。
为了解决这个问题,我们需要区分普通对象
和临时对象
;
那么体现在自定义类型中就是左值引用
和右值引用
的区别了。
我们可以首先来看看什么是右值。
要理解右值,可以从左值开始。
百度百科对于左值和右值的概念为:
左值(lvalue)和右值(rvalue)最先来源于编译。在C语言中表示位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。
左值
:指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以作为一个左值。
右值
:指的是引用了一个存储在某个内存地址里的“数据”。
从上数定义我们可以简单概括为:
左值:有内存、有名字;
右值:没内存或者没名字(临时量
)。
所以我们上面说的临时对象
就是一个右值
。
需要注意的一点是:
右值引用变量本身也是一个左值
!
对于这句话的理解是:
一个右值(临时量),是没有名字的;
一个引用,就是相当于给变量起了别名
;
右值和引用一结合,就有了名字、有了内存;
那么也就变为了左值
。
这一点特别重要:在后面move
和forward
应用的时候需要特别注意!
接下来我们就可以在原来CMyString
类型里添加带有右值引用
参数的成员方法(主要是拷贝构造
和赋值重载
):
// 带右值引用参数的拷贝构造
CMyString(CMyString &&str) // str引用的就是一个临时对象
{
cout << "CMyString(CMyString&&)" << endl;
_pstr= str._pstr;
str._pstr= nullptr;
}
// 带右值引用参数的赋值重载函数
CMyString& operator=(CMyString &&str) // 临时对象
{
cout << "operator=(CMyString&&)" << endl;
if (this == &str)
return *this;
delete[]_pstr;
_pstr= str._pstr;
str._pstr = nullptr;
return *this;
}
修改代码之后再次运行,结果如下:
我们可以看到,这一次,匹配的都是右值引用参数的成员方法了!
了解了右值引用之后,我们发现,右值引用对于解决涉及临时对象大量内存开辟及数据拷贝的问题有着很好的应用。
所以,我们之前遗留的问题:
CMyString
的+
运算符重载函数就可以解决了:
CMyString operator+(const CMyString &lhs,
const CMyString &rhs)
{
CMyString tmpStr;
tmpStr.mptr = new char[strlen(lhs.mptr) + strlen(rhs.mptr) + 1];
strcpy(tmpStr.mptr, lhs.mptr);
strcat(tmpStr.mptr, rhs.mptr);
return tmpStr;
}
注意:
在类外定义+
运算符重载函数的时候,需要在类里面定义一个友元函数
:
class CMyString
{
private:
friend CMyString operator+(const CMyString &lhs,
const CMyString &rhs);
};
这样的话,在涉及临时对象
的拷贝构造
和赋值重载
,都将匹配到带有右值引用参数
的成员方法。
这样就可以避免大量开辟内存
和数据拷贝
了!
代码的效率得到了极大的提高!