1. 程式人生 > >十二、典型問題分析

十二、典型問題分析

png 什麽 當前 交換 編譯 image alt 推薦 析構函數

問題1:創建異常對象時的空指針問題

技術分享圖片

創建一個空指針異常對象,意味著這會調用父類的構造函數Exception(0),然後調用init(0, NULL,0),然後調用m_message = strdup(0)

/* Duplicate S, returning an identical malloc‘d string.  */
char * __strdup (const char *s)
{
  size_t len = strlen (s) + 1;
  void *new = malloc (len);

  if (new == NULL)
    return NULL;

  return (char *) memcpy (new, s, len);
}

缺陷:沒有處理參數為空指針的情況,默認為參數不能為空。

參數為空指針的情況應該合法,空指針作為字符串的一個特殊值,是有意義的,如果要復制的字符串是一個空指針,只需要返回一個空指針就可以了,

m_message = strdup(message);
// 改為
m_message = (message ? strdup(message) : NULL);
// 在外部對message為空的情況進行了處理

改進之後增強了代碼的健壯性

問題2:單鏈表LinkList中的數據元素刪除,異常安全性問題

class Test : public Object
{
    int m_id;
public:
    Test(int id = 0)
    {
        m_id = id;
    }

    ~Test()
    {
        if( m_id == 1 )
        {
            throw m_id;
        }
    }
};

int main()
{
    LinkList<Test> list;
    Test t0(0), t1(1), t2(2);
    
    try
    {
        list.insert(t0);
        list.insert(t1);    // t1 在析構時拋出異常
        list.insert(t2);
        
        list.remove(1);
    }
    catch(int e)
    {
        cout << e << endl;
        cout << list.length() << endl;
    }

    return 0;
}

析構函數中拋出是一個不推薦的操作,但是強制這樣做之後,要保證單鏈表對象list的合法性,這叫異常安全性。list.remove(1)刪除下表為1的對象的時候,即刪除t1對象的時候,肯定會調用t1的析構函數,從而拋出異常,那麽期望的結果就是list.length()長度變為2,因為刪除了一個元素t1。但是結果是程序直接崩潰,原因是QT使用的編譯器所使用的g++編譯器實現細節問題,不允許在析構函數中拋出異常,這個異常無法被捕捉。

使用vs之後,發現程序有輸出:1 3,之後崩潰,過程如下:

  • vs中允許析構函數拋出異常,可以捕捉,故list.remove(1)之後會產生異常並被捕捉,e的信息就是m_id
    值為1,故輸出1
  • 然後打印list.length(),值為3,意為著單鏈表的狀態和我們期望的不一樣,這裏就是隱藏的問題,remove()函數沒有考慮異常安全性

查看remove()的代碼:

bool remove(int i)      // O(n)
{
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        current->next = toDel->next;

        destroy(toDel);
        m_length--;
    }
    return ret;
}

發現在實現這個函數的時候,是先destroy(toDel)之後,再進行長度的m_length--,這裏就不夠異常安全,因為在destroy之後,就進入了異常,不會進行長度運算,修改代碼,交換兩條代碼的位置:

bool remove(int i)
{
...    
        m_length--;
        destroy(toDel);
...
}

同樣的,clear()函數也會有問題,在destroy之後再將m_length清0,同樣的問題存在,也會導致單鏈表的狀態混亂

void clear()        // O(n)
{
    // 釋放每一個結點
    while(m_header.next)
    {
        Node* toDel = m_header.next;
        m_header.next = toDel->next;
        //delete toDel;
        destroy(toDel);
    }
    m_length = 0;
}

改進之後:

void clear()        // O(n)
{
    // 釋放每一個結點
    while(m_header.next)
    {
        Node* toDel = m_header.next;
        m_header.next = toDel->next;
        // 做完指針操作之後,就意味著對應的數據元素已經從單鏈表中剝離出來的,長度應該--
        m_length--;

        //delete toDel;
        destroy(toDel);
    }       
}

問題3:LinkList中遍歷操作與刪除操作的混合使用

LinkList<int> list;

for (int i = 0; i<5; i++)
{
    list.insert(i);
}

for (list.move(0); !list.end(); list.next())
{
    if (list.current() == 3)
    {
        list.remove(list.find(list.current()));
        // 刪除成功後,list.current()的返回值是什麽
        cout << list.current() << endl;
    }
}

for (int i = 0; i<list.length(); i++)
{
    cout << list.get(i) << endl;
}

分析:

技術分享圖片

bool remove(int i)      // O(n)
{
    // 註意i的範圍
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        current->next = toDel->next;

        //delete toDel;       
        m_length--;
        destroy(toDel);
    }
    return ret;
}

遍歷之後current()指向3,刪除該元素之後,current()的指向不明,故出現了隨機數,改進:再remove中對m_current進行重新定位

bool remove(int i)      // O(n)
{
    // 註意i的範圍
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        // 對m_current進行處理,移動到下一個位置
        if (m_current == toDel)
        {
            m_current = toDel->next;
        }
        current->next = toDel->next;
        //delete toDel;       
        m_length--;
        destroy(toDel);
    }
    return ret;
}

問題4:StaticLinkList中數據元素刪除時的效率問題

void destroy(Node* pn)
{
    SNode* space = reinterpret_cast<SNode*>(m_space);
    SNode* spn = dynamic_cast<SNode*>(pn);
    for(int i = 0; i < N; i++)
    {
        if (spn == space + i)
        {
            m_used[i] = 0;
            spn->~SNode();
            // 空間歸還,對象析構,即可跳出循環,沒必要再繼續循環下去,加上break
            break;
        }
    }
}

問題5:StaticLinkList是否需要提供析構函數

一個類是否需要提供析構函數,由資源來決定,如果在類的構造函數中申請了系統資源,就需要提供析構函數,在析構函數中對應地釋放系統資源。這個判斷依據的前提條件是:

所實現的類是一個獨立的類,沒有任何繼承關系

StaticLinkList()
{
    for(int i = 0; i < N; i++)
    {
        m_used[i] = 0;
    }
}
// 從資源的角度看,構造函數只是進行了成員函數的賦值操作,沒有申請系統資源,那麽是不是可以不提供析構函數

但是這裏的StaticLinkList是有繼承關系的

template <typename T>
class LinkList : public List<T>
{
...
    void clear()        // O(n)
    {
        // 釋放每一個結點
        while(m_header.next)
        {
            Node* toDel = m_header.next;
            m_header.next = toDel->next;
            //delete toDel;
            destroy(toDel);
        }
        m_length = 0;
    }
...    
    ~LinkList()
    {
        clear();
    }
...
};

在繼承的類中有析構函數,並且在析構函數中調用了一個虛函數,但是構造函數和析構函數中是不會發生多態的,這個clear()函數就是類中實現的函數。所以對於StaticLinkList來說,父類中提供了clear()函數,但是子類中並沒有提供該函數,所以不管在子類還是父類中調用這個函數,始終調用的都是LinkList中的clear();繼續分析clear()函數,在裏面又調用另外一個虛函數destroy(),父類LinkList中有一個destroy()函數版本,子類StaticLinkList中也有一個destroy()函數版本,這意味著:父類的析構函數被調用的時候,始終調用到的都是父類中的destroy()函數,子類中的destroy()是沒有辦法在析構的時候被調用到的。

int main()
{
    StaticLinkList<int, 10> list;

    for (int i = 0; i<5; i++)
    {
        list.insert(i);
    }

    for (int i = 0; i<list.length(); i++)
    {
        cout << list.get(i) << endl;
    }

    return 0;
}

list對象是一個子類StaticLinkList的對象,於是在主程序結束的時list對象就會被析構,接著就調用到父類的析構函數,從而調用父類中的clear()函數,其中的destroy()函數肯定是父類中的實現,這裏就會有問題了

template <typename T>
class LinkList : public List<T>
{
protected:
    virtual void destroy(Node* pn)
        {
            delete pn;
        }
};

父類的destroy直接delete對應的內存空間,這個內存空間來自於子類creat()函數創建的空間toDel,這個空間是子類中的unsigned char m_space[sizeof(SNode) * N]中的空間,所以對於現在父類的destroy的空間就不是堆空間了,這就會造成程序的不穩定了,因為delete關鍵字只能釋放堆空間,程序的崩潰時間無法預測。子類中所希望的destroy函數並沒有被調用,這種問題在實際工程中不允許出現。

解決辦法:在子類中添加自己的析構函數

~StaticLinkList()
{
    this->clear();
}

調用的還是父類中clear()函數,但是clear調用的destroy函數卻是當前類中的實現,原因是:構造函數和析構函數是不會發生多態的,在構造函數或析構函數中調用的虛函數必然是當前類中實現的版本,不管是直接調用還是間接調用,都是這樣。所以這裏一定會調用到子類中的destroy()函數,斷點調試:

技術分享圖片

發現在父類的clear()函數中調用的確實是子類的destroy()函數,符合預期。

註意:經典問題

構造函數和析構函數中是不會發生多態的,所調用的虛函數都是當前類中實現的版本,不管直接調用還是間接調用

問題6:是否有必要增加多維數組類?

沒有必要

多維數組的本質:數組的數組,本質還是一維數組

二維數組類對象

int main()
{
    DynamicArray< DynamicArray<int> > d;

    d.resize(3);

    for(int i=0; i<d.length(); i++)
    {
        // d[i].resize(3);
        d[i].resize(i + 1); // 不規則二維數組
    }

    for(int i=0; i<d.length(); i++)
    {
        for(int j=0; j<d[i].length(); j++)
        {
            d[i][j] = i + j;
        }
    }

    for(int i=0; i<d.length(); i++)
    {
        for(int j=0; j<d[i].length(); j++)
        {
            cout << d[i][j] << " ";
        }

        cout << endl;
    }

    return 0;
}

十二、典型問題分析