劍指offer (01):賦值運算子函式 (C++ 實現)
阿新 • • 發佈:2019-02-18
1 題目
如下為型別 CMystring
的宣告,請為該型別新增賦值運算子函式。
class CMyString
{
public:
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
private:
char* m_pData;
};
2 題解
這道題就是一道過載賦值運算子為成員函式的題。需要考慮以下幾點:
- 是否把返回值的型別宣告為該型別的引用(
CMystring& ...
),並在函式結束後返回例項的引用(*this
- 是否把傳入的引數的型別宣告為常量引用(
const CMystring &str
)。如果傳入的不是引用而是例項,那麼從形參到實參會呼叫一次複製建構函式。把引數宣告為引用可以避免這樣的無謂消耗,提高效率。同時,我們在函式內不會修改傳入的例項的狀態,因此應該加上const
關鍵字。 - 是否釋放例項自身已有的記憶體。若未在分配空間前釋放自身已有的空間,將會造成記憶體洩漏。
- 判斷傳入的引數和當前的例項(
*this
)是不是同一個例項。如果是,則不賦值。如果不判斷,在釋放自身記憶體時,也會刪除原有的記憶體,就再也找不到賦值的內容。
2.1 經典解法,初級程式設計師
CMyString& CMyString::operator=(const CMystring& str)
{
// 判斷是否為自身
if(this == &str)
return *this;
// 賦值前先刪除原有內容
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
注意:
這樣做乍一看很不錯,但是在分配記憶體之前先用 delete
釋放了例項 m_pData
的記憶體。此時,若記憶體不足導致new char
丟擲異常,則 m_pData
就是一個空指標,容易導致程式崩潰,也丟失了我們自身原來的內容。
要想解決這個問題,有兩個辦法:
- 先
new
分配新內容,再delete
釋放已有內容。這樣及時分配記憶體失敗,亦可以保留原先的例項不會被修改。 - 更好的辦法是用賦值的例項建立一個臨時例項,再交換臨時例項和原來的例項。
2.2 高階解法,考慮異常安全性
CMyString & CMyString::operator=(const CMyString & str)
{
if (this != &str)
{
// 建立臨時例項,相當於把要賦值的內容也傳入了臨時變數!
CMyString strTemp(str);
// 然後交換指向,新換舊,舊換新, 完成賦值。
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
由於strTemp
是一個區域性變數,當程式執行到if
的外面時,也就出了改變數的作用域,就會自動呼叫 strTemp
的解構函式, 釋放掉原有的記憶體。
同時我們在 CMystring
的建構函式裡用 new
分配記憶體。若記憶體不足丟擲諸如 bad_alloc 異常,但我們還沒有修改原來例項的狀態,這也就保證了異常安全性。
3 完整程式碼
#define _CRT_SECURE_NO_WARNINGS // 否則報不安全,VS2015
#include <cstdio>
#include <cstring>
class CMyString
{
public:
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
CMyString& operator = (const CMyString& str); // 過載運算子 =
void Print();
private:
char* m_pData;
};
CMyString::CMyString(char * pData)
{
if (pData == nullptr) // 判斷是否為空指標
{
m_pData = new char[1];
m_pData[0] = '\0';
}
else
{
int length = strlen(pData);
m_pData = new char[length + 1];
strcpy(m_pData, pData);
}
}
CMyString::CMyString(const CMyString & str)
{
int length = strlen(str.m_pData);
m_pData = new char[length + 1];
strcpy(m_pData, str.m_pData);
}
CMyString::~CMyString()
{
delete[] m_pData;
}
/*
// 初級做法,未考慮記憶體不足時的異常安全性
CMyString& CMyString::operator=(const CMystring& str)
{
// 判斷是否為自身
if(this == &str)
return *this;
// 賦值前先刪除原有內容
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
*/
// 高階做法 A = B
CMyString & CMyString::operator=(const CMyString & str)
{
if (this != &str)
{
// 建立臨時例項,相當於把要賦值的內容也傳入了臨時變數!
CMyString strTemp(str);
// 然後交換指向,新換舊,舊換新, 完成賦值。
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
void CMyString::Print()
{
printf("%s", m_pData);
}
// ============================= 測試用例 =================================
// 正常賦值
void Test1()
{
printf("Test1 begins:\n");
char* text = "Hello world";
CMyString str1(text);
CMyString str2;
str2 = str1;
printf("The expected result is: %s.\n", text);
printf("The actual result is: ");
str2.Print();
printf(".\n");
}
// 賦值給自己
void Test2()
{
printf("Test2 begins:\n");
char* text = "Hello world";
CMyString str1(text);
str1 = str1;
printf("The expected result is: %s.\n", text);
printf("The actual result is: ");
str1.Print();
printf(".\n");
}
// 連續賦值
void Test3()
{
printf("Test3 begins:\n");
char* text = "Hello world";
CMyString str1(text);
CMyString str2, str3;
str3 = str2 = str1;
printf("The expected result is: %s.\n", text);
printf("The actual result is: ");
str2.Print();
printf(".\n");
printf("The expected result is: %s.\n", text);
printf("The actual result is: ");
str3.Print();
printf(".\n");
}
int main(int argc, char* argv[])
{
Test1();
Test2();
Test3();
return 0;
}