🎊 剖析深拷贝与浅拷贝,探究重载返回引用还是对象

剖析深拷贝与浅拷贝,探究重载返回引用还是对象

剖析深拷贝与浅拷贝,探究重载返回引用还是对象

导论今天在研究STL源码中发现这么一段有意义的代码:

代码语言:javascript代码运行次数:0运行复制// 重载前置++操作符 ++i

_Self& operator++() _GLIBCXX_NOEXCEPT

{

_M_node = _M_node->_M_next;

return *this;

}

// 重载后置++操作符 i++

_Self operator++(int) _GLIBCXX_NOEXCEPT

{

_Self __tmp = *this; // 记录原值 *调用的是拷贝构造函数

_M_node = _M_node->_M_next; // 进行操作

return __tmp; // 返回原值

}上述分别是前置++重载操作符与后置++操作符重载,可以有个疑惑,为何前置返回的是引用而后置返回的是对象呢?

这里先给出解释,后面一步步分析!

总结:不妨尝试一下,连续的前置++不会出问题,但是连续的后置++就报错了~

对于STL源码设计也是考虑了模仿内置类型的行为,后置的++需要返回增加之前的对象,不需要返回新对象,所以直接不返回对象的引用.

前置的++返回的是增加后的对象,这个对象是需要保留的,不是临时对象,返回引用就不需要拷贝对象,效率高.

上面这句话说的真的稀里糊涂的,第一次看到这句话,肯定一脸懵逼,实际上将上述话差分开就是解决两个问题:

(1) 深拷贝和浅拷贝?

(2) c++中有些重载运算符为什么要返回引用?

为了理解第(2)点,我们需要知道什么是深拷贝,什么是浅拷贝,分配内存是在堆上,还是在栈上!

1.深拷贝和浅拷贝这里先阐述一下C++对象中的两个概念,分别是拷贝操作与赋值操作!

拷贝操作如果对象在申明的同时马上进行的初始化操作,则称之为拷贝运算

代码语言:javascript代码运行次数:0运行复制String A("hello");

String B=A;此时其实际调用的是B(A)这样的浅拷贝操作。调用的是String类的拷贝构造函数!

赋值操作如果对象在申明之后,在进行的赋值运算,我们称之为赋值运算

代码语言:javascript代码运行次数:0运行复制String A("hello");

String B;

B=A; 此时实际调用的类的缺省赋值函数B.operator=(A),调用的是=操作符重载函数.

不管是浅拷贝还是赋值运算,其都有缺省的定义。也就是说,即使我们不overload这两种operation,仍然可以运行。那么,我们到底需不需要overload这两种operation 呢?答案就是:一般,我们我们需要手动编写析构函数的类,都需要overload 拷贝函数和赋值运算符。

现在有如下例子:

代码语言:javascript代码运行次数:0运行复制using namespace std;

class String

{

private:

char *str;

int len;

public:

String(const char* s);//构造函数声明

void show()

{

cout << "value = " << str << endl;

}

/*copy construct*/

String(const String& other)

{

len = other.len;

str = new char[len + 1];

strcpy(str, other.str);

cout << "copy construct" << endl;

}

};

String::String(const char* s)//构造函数定义

{

len = strlen(s);

str = new char[len + 1];

strcpy(str, s);

cout<<"construct"<

}调用:

代码语言:javascript代码运行次数:0运行复制String str1("abc");

String str2("123");

str1.show();

str2.show();如果在上述调用后面加一句:

代码语言:javascript代码运行次数:0运行复制str2=str1此时的内存分配如下图所示:

因此,对于缺省的赋值运算,如果对象域内没有heap上的空间,其不会产生任何问题。但是,如果对象域内需要申请heap上的空间,那么在析构对象的时候,就会连续两次释放heap上的同一块内存区域,从而导致异常。

因此,对于对象的域在heap上分配内存的情况,我们必须重载赋值运算符。当对象间进行拷贝的时候,我们必须让不同对象的成员域指向其不同的heap地址--如果成员域属于heap的话。

其余代码一致,在上述加上运算符=重载函数:

代码语言:javascript代码运行次数:0运行复制String& String::operator=(const String &other)//运算符重载

{

cout<<"= operator"<

if (this == &other)

return *this;

// return;

delete[] str;

len = other.len;

str = new char[len + 1];

strcpy(str, other.str);

return *this;

// return;

}此时内存分配示意图如下:

这样,在对象str1,str2退出相应的作用域,其调用相应的析构函数,然后释放分别属于不同heap空间的内存,程序正常结束。

上述的运算符重载就是深拷贝!我们也可以这样重载赋值运算符 void operator=(A &a);即不返回任何值。如果这样的话,他将不支持客户链式赋值 ,例如a=b=c will be prohibited!

上述另一个写法:

代码语言:javascript代码运行次数:0运行复制void String::operator=(const String &other)//运算符重载

{

cout << "= operator" << endl;

if (str!=NULL)

delete[] str;

len = other.len;

str = new char[len + 1];

strcpy(str, other.str);

// return;

}后面调用如果是str3=str2=str1,就会出现下面错误:

代码语言:javascript代码运行次数:0运行复制error: no match for ‘operator=’ (operand types are ‘String’ and ‘void’) str3 = str2 = str1;//str3.operator=(str1.operator=(str2))从上可以看出,赋值运算符和拷贝函数很相似。只不过赋值函数最好有返回值(进行链式赋值),返回也最好是对象的引用, 而拷贝函数不需要返回任何。同时,赋值函数首先要释放掉对象自身的堆空间(如果需要的话),然后进行其他的operation.而拷贝函数不需要如此,因为对象此时还没有分配堆空间。

2.C++中有些重载运算符为什么要返回引用?假设不返回引用,如下面代码:

代码语言:javascript代码运行次数:0运行复制class String

{

private:

char *str;

int len;

public:

String(const char* s);//构造函数声明

String operator=(const String& another);//运算符重载,此时返回的是对象

void show()

{

cout << "value = " << str << endl;

}

/*copy construct*/

String(const String& other)

{

}

~String()

{

delete[] str;

cout << "deconstruct" << endl;

}

};

String::String(const char* s)//构造函数定义

{

len = strlen(s);

str = new char[len + 1];

strcpy(str, s);

cout<<"construct"<

}

String String::operator=(const String &other)//运算符重载

{

cout<<"= operator"<

if (this == &other)

return *this;

// return;

delete[] str;

len = other.len;

str = new char[len + 1];

strcpy(str, other.str);

return *this;

// return;

}

int main()

{

String str1("abc");

String str2("123");

str2=str1;

str1.show();

str2.show();

return 0;

}此时运行报错:

代码语言:javascript代码运行次数:0运行复制free(): invalid size上述出错是因为,操作符=重载返回的不是引用,对于上述的操作符重载返回的是对象,此时对象是临时对象,并且会多调用一次拷贝构造与析构函数,当调用拷贝构造函数的时候,并没有在堆上分配内存,而此时free调的其实就是临时对象,而在后面str1与str2任务完成后,str1会因为前面释放临时对象被free掉了,所以此时进入str1的析构函数,会出现错误!解决这种问题将返回改为引用即可.

对比一下返回对象与返回引用:

第一种:返回对象

代码语言:javascript代码运行次数:0运行复制String String::operator=(const String &other)//运算符重载

{

cout<<"= operator"<

if (this == &other)

return *this;

// return;

delete[] str;

len = other.len;

str = new char[len + 1];

strcpy(str, other.str);

return *this;

// return;

}调用:

代码语言:javascript代码运行次数:0运行复制String str1("abc");

String str2("123");

String str3("456");

(str3 = str2) = str1;

str1.show();

str2.show();

str3.show();输出:

代码语言:javascript代码运行次数:0运行复制construct

construct

construct

= operator

copy construct

= operator

copy construct

deconstruct

deconstruct

value = abc

value = 123

value = 123

deconstruct

deconstruct

deconstruct第二:返回引用:

代码语言:javascript代码运行次数:0运行复制construct

construct

construct

= operator

= operator

value = abc

value = abc

value = abc

deconstruct

deconstruct

deconstruct将上述结果对比放在表格中:

(注:可以往右拖)

construct

construct

construct

construct

construct

construct

= operator

= operator

copy construct

= operator

= operator

value = abc

copy construct

value = abc

deconstruct

value = abc

deconstruct

deconstruct

value = abc

deconstruct

value = 123

deconstruct

value = 123

deconstruct

deconstruct

deconstruct

区别1:会发现使用引用返回后少了四行,原因返回的如果是对象,这个对象是临时对象,返回后会调用一次拷贝构造函数,结束后会调用析构函数,上面使用了两次=,所以第一种情况会多四行.区别2:结果不同,我们期待的结果是将str1也拷贝进str3,可是第一种情况并没有实现这种效果,str3只得到了str2的内容,并没有得到str1的内容,这是因为执行(str3=str2)后,因为返回的是对象(一个临时对象,str3的一个拷贝),不是引用,所以此时str3不在后面的=str1的操作中,而是str1对一个临时对象赋值,所以str3的内容保持不变(等于str2)。总结

那么什么情况下要返回对象的引用呢?

原因有两个:

允许进行连续赋值防止返回对象(返回对象也可以进行连续赋值(常规的情况,如a = b = c,而不是(a = b) = c))的时候调用拷贝构造函数和析构函数导致不必要的开销,降低赋值运算符的效率。最后,我们回到我们最前面解释:

对于STL源码设计也是考虑了模仿内置类型的行为,后置的++需要返回增加之前的对象,不需要返回新对象,所以直接不返回对象的引用.

前置的++返回的是增加后的对象,这个对象是需要保留的,不是临时对象,返回引用就不需要拷贝对象,效率高.

相信大家对这句话认识更加深刻!

学习资料:

https://www.cnblogs.com/winston/archive/2008/06/03/1212700.html

https://www.cnblogs.com/codingmengmeng/p/5871254.html

🎯 相关推荐

计绌方匮的意思、计绌方匮的详细解释
365bet在线网投

计绌方匮的意思、计绌方匮的详细解释

📅 08-01 👀 3407
员工培训要收 PK 金话术 如何制定
365bet在线网投

员工培训要收 PK 金话术 如何制定

📅 08-12 👀 1458
客户分析报告 11
365封号提现了没到账

客户分析报告 11

📅 07-26 👀 5022
word中怎么导出图片的三种方法
完美体育365

word中怎么导出图片的三种方法

📅 07-05 👀 2180
vue中date为什么是函数
365bet在线网投

vue中date为什么是函数

📅 08-03 👀 7973
讫章是什么意思是什么
完美体育365

讫章是什么意思是什么

📅 08-29 👀 7937
节鬼门关几点开 鬼门开和鬼门关时间在何时
完美体育365

节鬼门关几点开 鬼门开和鬼门关时间在何时

📅 08-11 👀 4056
黑莓的种植方法和技术(黑莓树怎么栽)
365bet在线网投

黑莓的种植方法和技术(黑莓树怎么栽)

📅 07-10 👀 2400
手机qq悄悄话在哪里?手机qq找不到悄悄话功能怎么办