1. 程式人生 > 實用技巧 >36, 經典問題解析三

36, 經典問題解析三

1.關於賦值的疑問

什麼時候需要過載賦值操作符?

淺拷貝不夠用這時候需要自定義深拷貝

編譯器是否提供預設的賦值操作符?

(1)回答

  ①編譯器每個類預設提供過載了賦值操作符-------可以給同一型別的類物件相互賦值

  ②預設的賦值操作符僅完成淺拷貝

  ③當需要進行深拷貝必須過載賦值操作符(賦值操作符一樣)

  ④賦值操作符與拷貝建構函式相同的存在意義

程式設計說明:編譯器會預設提供過載賦值操作符------僅僅完成淺拷貝

 1 #include<iostream>
 2 #include<string>
 3 
 4 using namespace std;
5 6 //編譯器會預設提供過載賦值操作符------僅僅完成淺拷貝 7 8 class Test 9 { 10 int* m_pointer; //指標成員 11 public: 12 Test() 13 { 14 m_pointer = NULL; 15 } 16 Test(int i) 17 { 18 m_pointer = new int(i); //指向堆空間的4個位元組 19 } 20 21 void print() 22 { 23 cout << "
m_pointer=" << hex << m_pointer << endl; 24 } 25 26 ~Test() 27 { 28 delete m_pointer; 29 } 30 }; 31 32 int main() 33 { 34 Test t1=1; //t1物件的內部的成員指標指向堆空間的一片記憶體,記憶體裡面的值設定為1 35 Test t2; //t2物件指標指向空 36 37 t2 = t1; //t1物件賦值t2 產生記憶體錯誤 38 //
t1 t2指向相同堆空間,main()結束之前摧毀t1 t2兩個物件,釋放呼叫兩次解構函式delete m_pointer;--------不合法的記憶體操作 39 40 41 42 t1.print(); 43 t2.print(); 44 45 return 0; 46 }
 1 #include<iostream>             
 2 #include<string>
 3 
 4 using namespace std;
 5 
 6 //什麼時候進行深拷貝----要實現深拷貝就要過載賦值操作符
 7 
 8 //類裡有成員m_pointer指標指代了外部資源,淺拷貝不夠用這時候需要自定義深拷貝和過載賦值操作符,有必要的時候自定義拷貝建構函式
 9 
10 class Test
11 {
12     int* m_pointer;  //指標成員  
13 
14 public:
15     Test()
16     {
17         m_pointer = NULL;
18     }
19     Test(int i)
20     {
21         m_pointer = new int(i); //指向堆空間的4個位元組
22     }
23 
24 
25 
26     //自定義深拷貝建構函式
27     Test(const Test& obj)    
28     {
29         m_pointer = new int(*obj.m_pointer); //1,堆空間申請一片記憶體,大小由引數物件決定,記憶體代表int型別的值,
30                                              //2,取出引數物件obj的m_pointer指標所指向的堆空間的整型值,賦值到新申請的空間內
31     }
32     
33 
34 
35     //過載賦值操作符  注意4點:  1,返回值型別一定是引用Test&為了連續賦值   2,引數是const引用型別
36     //                              3,賦值操作符不是自賦值a=a,要避免賦值,通過This判斷,this指向當前物件的地址和引數地址不同才進行賦值操作(深拷貝)
37     //                             4,返回當前物件地址*this
38 
39     Test& operator=(const Test& obj)      
40     {       
41         //進行賦值操作
42         if (this != &obj)                   
43         { 
44             delete m_pointer;  //深拷貝之前將當前的指標刪除
45             m_pointer = new int(*obj.m_pointer);
46         }
47 
48         return *this;                  
49     }
50 
51     void print()
52     {
53         cout << "m_pointer=" << hex << m_pointer << endl;
54     }
55 
56     ~Test()
57     {
58         delete m_pointer;
59     }
60 };
61 
62 int main()
63 {
64     Test t1=1;  //t1物件的內部的成員指標指向堆空間的一片記憶體,記憶體裡面的值設定為1
65     Test t2;    //t2物件指標指向空
66 
67     t2 = t1;    //利用賦值操作符過載函式
68                 
69                 // t1物件的m_pointer指標和t2物件的m_pointer指標所指向的堆空間不一樣
70 
71     t2 = t2;    //沒意義,只是支援 所以要避免自賦值    if (this != &obj)    
72 
73     t1.print();
74     t2.print();
75 
76     return 0;
77 }

問題分析:

      

t1物件賦值t2     產生記憶體錯誤--------分析:
t1 t2指向相同堆空間,main()結束之前摧毀t1 t2兩個物件,釋放呼叫兩次解構函式delete m_pointer;--------不合法的記憶體操作
結論:一旦進行深拷貝,那麼拷貝建構函式和賦值操作符過載就要自定義

一般性原則過載賦值操作符,必然需要實現深拷貝!!!也要進行拷貝構造的自定義

整形陣列類:

    拷貝構造在private內,外部無法呼叫拷貝建構函式,也就是使用了二階構造不允許出現拷貝建構函式,賦值操作允許,可以進行賦值操作符過載

 1 #ifndef _INTARRAY_H_
 2 #define _INTARRAY_H_
 3 
 4 class IntArray
 5 {
 6 private:
 7     int m_length;
 8     int* m_pointer;
 9     
10     IntArray(int len);
11     IntArray(const IntArray& obj);
12     bool construct();
13 public:
14     static IntArray* NewInstance(int length); 
15     int length();
16     bool get(int index, int& value);
17     bool set(int index ,int value);
18     int& operator [] (int index);
19     IntArray& operator = (const IntArray& obj); 賦值操作符過載
20     IntArray& self();
21     ~IntArray();
22 };
23 
24 #endif
25 #endif
  1 #include "IntArray.h"
  2 
  3 IntArray::IntArray(int len)
  4 {
  5     m_length = len;
  6 }
  7 
  8 bool IntArray::construct()
  9 {
 10     bool ret = true;
 11     
 12     m_pointer = new int[m_length];
 13     
 14     if( m_pointer )
 15     {
 16         for(int i=0; i<m_length; i++)
 17         {
 18             m_pointer[i] = 0;
 19         }
 20     }
 21     else
 22     {
 23         ret = false;
 24     }
 25     
 26     return ret;
 27 }
 28 
 29 IntArray* IntArray::NewInstance(int length) 
 30 {
 31     IntArray* ret = new IntArray(length);
 32     
 33     if( !(ret && ret->construct()) ) 
 34     {
 35         delete ret;
 36         ret = 0;
 37     }
 38         
 39     return ret;
 40 }
 41 
 42 int IntArray::length()
 43 {
 44     return m_length;
 45 }
 46 
 47 bool IntArray::get(int index, int& value)
 48 {
 49     bool ret = (0 <= index) && (index < length());
 50     
 51     if( ret )
 52     {
 53         value = m_pointer[index];
 54     }
 55     
 56     return ret;
 57 }
 58 
 59 bool IntArray::set(int index, int value)
 60 {
 61     bool ret = (0 <= index) && (index < length());
 62     
 63     if( ret )
 64     {
 65         m_pointer[index] = value;
 66     }
 67     
 68     return ret;
 69 }
 70 
 71 int& IntArray::operator [] (int index)
 72 {
 73     return m_pointer[index];
 74 }
 75 
 76 IntArray& IntArray::operator = (const IntArray& obj)
 77 {
 78     if( this != &obj )   //拷貝開始
 79     {
 80         int* pointer = new int[obj.m_length];  //申請堆空間記憶體,大小由引數物件決定
 81         
 82         if( pointer )  //申請成功
 83         {
 84             for(int i=0; i<obj.m_length; i++)  //複製,遍歷引數物件所指的堆空間
 85             {
 86                 pointer[i] = obj.m_pointer[i];  //引數物件所指堆空間的內容元素,全部複製到pointer所指的堆空間----以上都是淺拷貝
 87             }
 88             //下面是深拷貝
 89             m_length = obj.m_length;  //第一步:更新長度
 90             delete[] m_pointer;    //第二步:釋放原有的空間
 91             m_pointer = pointer;    //第三步:深拷貝的關鍵-----將當前操作的pointer指標所指向的空間,賦值給m_pointer成員
 92         }
 93     }
 94     
 95     return *this;
 96 }
 97 
 98 IntArray& IntArray::self()
 99 {
100     return *this;
101 }
102 
103 IntArray::~IntArray()
104 {
105     delete[]m_pointer;
106 }

//main.cpp

#include <iostream>
#include "IntArray.h" 

using namespace std; 

int main()
{
    IntArray* a = IntArray::NewInstance(5);//陣列物件
    IntArray* b = IntArray::NewInstance(10); //陣列物件

    if(a && b)//二階構造都成功,賦值操作
    {
        IntArray& array = a->self();

        IntArray& brray = b->self();      

        cout << "array.length() = " << array.length() << endl; //5      

        cout << "brray.length() = " << brray.length() << endl; //10 

        array = brray;  //賦值

        cout << "array.length() = " << array.length() << endl; //10      

        cout << "brray.length() = " << brray.length() << endl; //10       
    }    

    delete a;
    delete b;
  
    return 0;
}

(2)編譯器預設提供的函式

面試題:給一個空類,是不是一個真正意義上的空類???

  編譯器會自動提供四個函式實現

  • 不帶參的建構函式

  • 拷貝建構函式

  • 預設的賦值操作符

  • 解構函式

    

(3)一般原則過載賦值操作符,必然需要實現深拷貝!!!

2.關於string的

s.c_str()-----返回字元指標,代表字串

append----插入字串

#include<iostream>
#include<string>

//不能混合使用c語言和c++

using namespace std;

int main()
{
    string s = "12345";

    //程式試圖在C的方式訪問字串(不建議這樣用!)
    const char* p = s.c_str(); //指標指向------c_str表示C方式的字串記憶體空間

    cout << p << endl;        //12345        p成為了野指標
  //cout << s << endl;        //12345

    s.append("abcde");      //字串插入函式
                            //p成為野指標,因為追加字串,可能
                            //導致堆記憶體的重新分配,從而m_cstr 
                            //指的堆記憶體地址改變了,但p並不知道!

    cout << p << endl;       //    12345--仍然指向舊的地址(野指標) 
    //    cout << s << endl;     //12345abcde

    return 0;
}

問題分析:

    

append()執行結束後,內部的字元指標m_cstr()指向的是一片新的記憶體空間,值為12345abcded,之前的記憶體空間已經釋放,P指向之前的記憶體空間,所以p稱為野指標。

(2)string類內部維護了一個m_length的變數,用於指示字串中字元的個數。當使用C的方式使用string物件時,這個m_length可能不會自動更新

(1)string類內部維護一個指向資料的char*指標(m_cstr),這裡用於存放字串資料的堆空間地址。因字串操作(如複製、合併、追加等),所以這個指標可能在程式執行的過程中發生改變。----------所以一般不要操作此指標

            不要混合使用c語言和c++!!!!!

字串問題2:

#include <iostream>
using namespace std;
 
int main()
{
    const char* p = "12345";  //c還是c++程式碼???

    string s = "";   //使用c++標準庫類  s.length==0

    //保留一定量記憶體以容納一定數量的字元
    s.reserve(10);    //字串物件內部的資料指標指向的堆空間大小設定為10個位元組    s.length ==0;

    //不要使用C語言的方式操作C++中的字串
    for(int i=0; i < 5; i++)
    {
        s[i] = p[i];      //字串賦值----s[i]當作c語言字元陣列使用-----error
                // 注意,雖然此時s物件中的字串記憶體,確實被賦新的值了。但用這種方式賦值,相等於只是通過指標賦值,s.length不,會自動更新,即仍為0 }
  if(!s.empty())   {
    cout<<s<<endl; //空
  }

  for(int i=0;i<5;i++)
  {
    cout<<s[i]<<endl; //12345 說明s[i]=p[i]賦值成功------但是字串為空
  }

cout << s.length() << endl; //0 cout << s.empty() << endl; //1 cout << s << endl; //這裡將不會有任何輸出,因為s.length=0; return 0; }

分析bug出現的地方

      

m_length=0,字串本身不代表12345,還是空串

這樣修改:

 1 #include<iostream>
 2 #include<string>
 3 
 4 //不能混合使用c語言和c++
 5 using namespace std;
 6 
 7 int main()
 8 {
 9     const string p = "12345";
10 
11     string s = "";
12 
13     s = p;   //使用字串物件------拋棄c語言程式設計,採用面對物件思想
14 
15     cout << s << endl; //12345
16 return 0; 17 }

3.小結

(1)在需要進行深拷貝的時候必須過載賦值操作符(以及拷貝構造)

(2)賦值操作符拷貝建構函式同等重要的意義

(3)string通過一個數據空間儲存字元資料

(4)string通過一個成員變數儲存當前字串的長度

(5)C++開發儘量避開C語言中慣用的程式設計思想