您当前的位置: 首页 > 学无止境 > 心得笔记 网站首页心得笔记
第36讲-副本构造器
发布时间:2021-05-21 17:08:35编辑:雪饮阅读()
逐位賦值引出的問題
把一个对象赋值给一个类型与之相同的变量。
编译器将生成必要的代码把“源”对象各属性的值分别赋值给“目标”对象的对应成员。这种赋值行为称之为逐位赋值(bitwise copy)。
这种行为在绝大多数数场合都没有问题,但如果某些成员变量时指针的话,那么问题来了:对象成员进行逐位复制的结果是你将拥有两个一模一样的实例,而这两个副本里的同名指针会指向相同的地址。
于是乎,当删除其中一个对象时,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,就会出问题。空指针。
老對象被幹掉,其指針類型成員也被幹掉,但是這個成員由於是指針,所以此時新的對象正在用的指針就是一個空指針。
[那聪明的程序员们这时候可能会说“如果我在第二个副本同事也删除指针,不就行了吗?”]
這句引自小甲魚,其實我覺得他這個舉例本身就有問題
本來目的就是爲了拷貝對象,你僅僅刪除第二個副本的指針是沒有多少意義的。
正確的做法應該是拿到第一個對象的指針所指向的值,然後第二個對象新增一個指針來接收這個值。
重載賦值操作符解決逐位賦值問題
最常見對象變量被賦值都是通過賦值操作符的,所以賦值的具體操作都在賦值操作符内部。
所以對賦值操作符重載是必須的,對於這個問題的解決來説。
一個具體的實例如:
#include <iostream>
#include <string>
using namespace std;
class MyClass
{
public:
MyClass(int *p);
~MyClass();
//重载赋值符
MyClass &operator = (const MyClass &rhs);
void print();
private:
//解引用
int *ptr;
};
MyClass::MyClass(int *p)
{
ptr = p;
}
MyClass::~MyClass()
{
delete ptr;
}
MyClass &MyClass::operator = (const MyClass &rhs)
{
//赋值号两边为不同对象,做处理
if(this != &rhs)
{
delete ptr;
//為ptr創建一個新的指針
ptr = new int;
//左邊解引用就相當於左邊直接為指針的指向側,那麽這裏的意思就是為指針的指向側賦值
*ptr = *rhs.ptr;
cout << "赋值号两边为不同对象,做处理!\n";
}
//赋值号两边为同个对象,不做处理
else
{
cout << "赋值号两边为同个对象,不做处理!\n";
}
return *this;
}
void MyClass::print()
{
cout << *ptr << endl;
}
int main()
{
MyClass obj1(new int(1));
MyClass obj2(new int(2));
obj1.print();
obj2.print();
/*没起作用*/
obj1 = obj1;
obj1.print();
obj2.print();
/*起作用*/
obj2 = obj1;
obj1.print();
obj2.print();
return 0;
}
編譯運行結果如:
1
2
赋值号两边为同个对象,不做处理!
1
2
赋值号两边为不同对象,做处理!
1
1
Process returned 0 (0x0) execution time : 0.034 s
Press any key to continue.
上面實例中關於:
MyClass &operator = (const MyClass &rhs)
这个方法所预期的输入参数应该是一个MyClass类型的,不可改变的引用。
因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为它创建另外一个副本。如果是非引用那麽MyClass rhs相當於又實例化了一個實例出來,和普通形參一樣是以拷貝的副本存在的。
運算符重載的精簡
不喜歡小甲魚寫的運算符重載,他寫的代碼太囉嗦了,又是定義又是聲明的。
這裏重新改寫為聲明定義一體化的
#include <iostream>
#include <string>
using namespace std;
class MyClass
{
public:
MyClass(int *p)
{
ptr = p;
};
~MyClass()
{
delete ptr;
};
MyClass operator = (const MyClass &rhs)
{
if(this != &rhs)
{
delete ptr;
ptr = new int;
*ptr = *rhs.ptr;
cout << "赋值号两边为不同对象,做处理!\n";
}
else
{
cout << "赋值号两边为同个对象,不做处理!\n";
}
return *this;
};
void print()
{
cout << *ptr << endl;
}
private:
int *ptr;
};
int main()
{
MyClass obj1(new int(1));
MyClass obj2(new int(2));
obj1.print();
obj2.print();
/*没起作用*/
obj1 = obj1;
obj1.print();
obj2.print();
/*起作用*/
obj2 = obj1;
obj1.print();
obj2.print();
return 0;
}
編譯運行同樣沒有問題:
1
2
赋值号两边为同个对象,不做处理!
1
2
赋值号两边为不同对象,做处理!
1
1
Process returned 0 (0x0) execution time : 0.170 s
Press any key to continue.
在這裏爲了更好的説明,還可以繼續簡化為如下程序:
#include <iostream>
#include <string>
using namespace std;
class MyClass
{
public:
MyClass(int *p)
{
ptr = p;
};
~MyClass()
{
delete ptr;
};
MyClass operator = (const MyClass rhs)
{
delete ptr;
ptr = new int;
*ptr = *rhs.ptr;
return *this;
};
void print()
{
cout << *ptr << endl;
}
private:
int *ptr;
};
int main()
{
MyClass obj1(new int(1));
MyClass obj2(new int(2));
obj1.print();
obj2.print();
cout<<"\n起作用\n";
/*起作用*/
obj2 = obj1;
obj1.print();
obj2.print();
return 0;
}
這個程序編譯運行也是沒有問題的:
1
2
起作用
1
1
Process returned 0 (0x0) execution time : 0.175 s
Press any key to continue.
這次簡化最重要的是這個:
MyClass operator = (const MyClass rhs)
這裏形參rhs沒有使用引用,關於這裏不使用引用有一個危害
这时实参传值给形参会创建一个临时变量,调用复制构造函数将实参复制给形参(MyClass rhs)
相當於浪費了一點内存,不過這裏影響不大。
但是這裏的成員變量如果不是一個標量,比如字符串那麽有如:
Myclass &operator=(const Myclass &ele) {
delete[] str;
str = new char[strlen(ele.str) + 1];
strcpy(str, ele.str);
return *this;
}
这时实参传值给形参会创建一个临时变量,调用复制构造函数将实参复制给形参,实参的str与形参的str指向同一块内存。所以,当赋值函数完成时,会清除函数的所占的内存,形参就会被清除,形参的str指向的内存也就会被清除。
所以形參引用比較穩妥。
觀察上面的程序,不知不覺,重載運算符的返回值類型也是被寫成了非引用(&)類型了。
但是發現程序並沒有報錯。
這和上面形參一樣,若是字符串類型的成員,則返回時候還是會創建臨時的一個對象,則對象創建完畢后函數躰内内存都被清除,則返回類型中還是帶走的只是指針,指針指向一個空的内存了。
進一步説明了函數返回時候也是以副本返回的。
跳過賦值重載的運算符問題
如果將剛才初始化和交換賦值的語句修改如:
MyClass obj1;
MyClass obj2=obj1;
区别很细微,刚才是先创建两个对象,然后再把obj1赋值给obj2。
现在是先创建一个实例obj1,然后再创建实例Obj2的同时用obj1的值对它进行初始化。
虽然看起来好像一样,但编译器确生成完全不同的代码:编译器将在MyClass类里寻找一个副本构造器(copy constructor),如果找不到,它会自行创建一个。
换句话说,如果遇到上面这样的代码,即使已经在这个类里面重载了赋值操作符,暗藏着隐患的“逐位复制”行为还是发生的,也就是说这种情况下=操作符的重载仍然没有起作用。
想要躲开这个隐患,还需要亲自定义一个副本构造器,而不是让系统帮我们生成。
那麽一個具體的解決該問題的實例如:
#include <iostream>
#include <string>
class MyClass
{
public:
MyClass(int *p)
{
std::cout<<"進入主構造器\n";
ptr=p;
std::cout<<"離開主構造器\n";
};
//副本構造器
MyClass(const MyClass &rhs){
std::cout<<"進入副本構造器\n";
if(this!=&rhs){
ptr=new int;
*ptr=*rhs.ptr;
std::cout<<"副本構造器 賦值號兩邊為不同對象 做處理\n";
}
else{
std::cout<<"副本構造器 賦值號兩邊為相同對象 不做處理\n";
}
std::cout<<"離開副本構造器\n";
};
~MyClass(){
std::cout<<"進入析構器\n";
delete ptr;
std::cout<<"離開析構器\n";
};
MyClass &operator=(const MyClass &rhs){
std::cout<<"進入賦值語句重載\n";
if(this!=&rhs){
//如果不存在就刪除可能會有問題
if(ptr){
delete ptr;
}
ptr=new int;
*ptr=*rhs.ptr;
std::cout<<"副本構造器 賦值號兩邊為不同對象 做處理\n";
}
else{
std::cout<<"賦值號兩邊為同個對象,不做處理!\n";
}
std::cout<<"離開賦值語句重載!\n";
return *this;
};
void print()
{
std::cout<<*ptr<<std::endl;
};
private:
int *ptr;
};
int main(){
MyClass obj1(new int(1));
MyClass obj2(new int(2));
obj2 = obj1;
obj1.print();
obj2.print();
std::cout << "-----------------------\n";
MyClass obj3(new int(3));
std::cout<<"obj3創建完成\n";
MyClass obj4=obj3;
std::cout<<"obj4被obj3賦值完成\n";
obj3.print();
obj4.print();
std::cout << "-----------------------\n";
MyClass obj5(new int(5));
obj5 = obj5;
obj5.print();
return 0;
}
編譯並運行結果如:
在這個例子中副本構造器中的一個判斷
if(this!=&rhs){
ptr=new int;
*ptr=*rhs.ptr;
std::cout<<"副本構造器 賦值號兩邊為不同對象 做處理\n";
}
關於這個判斷我個人理解,一個個人想法是,覺得是多餘的,可以直接執行這個if語句塊裏面的語句的。
因爲能觸發副本構造器的就只有MyClass obj2=obj1(這裏obj1是已經實例化出來的),只有這種情況下才會觸發副本構造器,而在這種情況下,副本構造器接收到的參數只能是obj1,試想下你怎樣才能讓MyClass obj2=obj2成立?沒有這種情況吧。那麽這種情況MyClass obj2=obj1的時候,傳入副本構造的obj1很顯然和obj2(副本構造裏面的this)是不同的,所以說這個判斷是多餘的。
不過這裏只做個人看法。
关键字词:副本構造器
上一篇:第35讲-从函数或方法返回内存
下一篇:第37讲-高级强制类型转换
相关文章
-
无相关信息