2015-09-07 64 views
3

的C++專家& d語言的創造者Walter Bright說:對象切片如何導致內存損壞?

切割問題是嚴重的,因爲它可以導致內存 腐敗,這是很難保證程序不 遭受它。要設計出語言,支持 繼承的類應該只能通過引用(而不能通過值)訪問。 D編程語言具有此屬性。

如果有人通過給出C++示例來解釋對象切片問題導致內存損壞的情況,那會更好嗎?以及D語言如何解決這個問題?

+5

你爲什麼不問[在他的回答這個職位的人](http://stackoverflow.com/questions/274626/what-is-object-slicing)?或閱讀評論。 – juanchopanza

+0

@ juanchopanza:有什麼問題如果我問獨立的問題?如果將來有人出現同樣的問題,這將在未來和易於搜索中有用。 – Destructor

+0

這是完全錯誤的。這個問題與鏈接的問題不同。 – Destructor

回答

2

以下簡單的小C++程序及其輸出顯示了切片問題以及爲什麼會導致內存損壞。

使用像D和Java和C#這樣的語言,通過引用句柄訪問變量。這意味着所有關於變量的信息都與參考句柄相關聯。使用C++時,有關變量的信息是編譯完成時編譯器狀態的一部分。打開C++運行時類型信息(RTTI)可以提供一種在運行時查看對象類型的機制,但它並不能真正幫助解決切片問題。

基本上,C++爲了擠出更多的速度移除的安全網。

C++編譯器有一套使用的規則,所以如果某個類沒有提供特定的方法,例如複製構造函數或賦值運算符,編譯器將盡其所能創建自己的默認版本。編譯器也有它使用的規則,所以如果一個特定的方法不可用,那麼它會尋找另一種創建代碼的方式來表達源語句的含義。

有時編譯器太有幫助,結果變得危險。

在此示例中,有兩個類,levelOne是基類,levelTwo是派生類。它使用虛擬析構函數,以便指向基類對象的指針也將清理對象的派生類部分。

在輸出中,我們看到將派生類分配給基類會導致切片以及何時調用析構函數,只調用基類的析構函數而不調用派生類的析構函數。

沒有被調用派生類的析構函數的結果意味着派生的對象所擁有的任何資源可能無法正確釋放。

這是簡單的程序。

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

class levelOne 
{ 
public: 
    levelOne(int i = 1) : iLevel(i) { iMyId = iId++; std::cout << " levelOne construct " << iMyId << std::endl; } 
    virtual ~levelOne() { std::cout << " levelOne destruct " << iMyId << " iLevel = " << iLevel << std::endl; } 

    int iLevel; 
    int iMyId; 

    static int iId; 
}; 

int levelOne::iId = 1; 

class levelTwo : public levelOne 
{ 
public: 
    levelTwo(int i = 2) : levelOne(i) { jLevel = 2; iMyTwoId = iTwoId++; std::cout << "  levelTwo construct " << iMyId << ", " << iMyTwoId << std::endl; } 
    virtual ~levelTwo() { std::cout << "  levelTwo destruct " << iMyId << ", " << iMyTwoId << " iLevel = " << iLevel << " jLevel = " << jLevel << std::endl; } 

    int jLevel; 
    int iMyTwoId; 

    static int iTwoId; 
}; 

int levelTwo::iTwoId = 101; 


int _tmain(int argc, _TCHAR* argv[]) 
{ 
    levelOne one; 
    levelTwo two; 

    std::cout << "Create LevelOne and assign to it a LevelTwo" << std::endl; 
    levelOne aa;  // create a levelOne object 
    aa = two;  // assign to the levelOne object a levelTwo object 

    std::cout << "Create LevelTwo and assign to it a LevelOne pointer then delete it" << std::endl; 
    levelOne *pOne = new levelTwo; 
    delete pOne; 

    std::cout << "Exit program." << std::endl; 
    return 0; 
} 

輸出顯示用其ID的pOne = new levelTwo;創建的對象是4命中兩者levelTwolevelOne析妥善處理對象的破壞。

然而levelTwo對象two的分配到levelOne對象aa導致切片,因爲默認的賦值運算符,它只是做了內存拷貝,使用這樣當對象aa的析構函數被調用時,只有析構函數執行levelOne意味着派生類擁有的任何資源都不會被釋放。

那麼其他兩個對象正確銷燬,因爲他們都走出去的範圍爲程序結束。讀這個日誌記住,析構函數是按照與構造相反的順序調用的。

levelOne construct 1 
    levelOne construct 2 
    levelTwo construct 2, 101 
Create LevelOne and assign to it a LevelTwo 
    levelOne construct 3 
Create LevelTwo and assign to it a LevelOne pointer then delete it 
    levelOne construct 4 
    levelTwo construct 4, 102 
    levelTwo destruct 4, 102 iLevel = 2 jLevel = 2 
    levelOne destruct 4 iLevel = 2 
Exit program. 
    levelOne destruct 2 iLevel = 2 
    levelTwo destruct 2, 101 iLevel = 2 jLevel = 2 
    levelOne destruct 2 iLevel = 2 
    levelOne destruct 1 iLevel = 1 
+0

將LevelTwo分配給LevelOne後,LevelTwo仍然存在並被正確地破壞。 LevelOne只有共享部分的成員變量,這就是複製的所有內容(否則會損壞堆棧)。所以你給的例子其實很好。維基百科有一個[更好的切片問題示例](https://en.wikipedia.org/wiki/Object_slicing) – dgel

2

繼承的一個方面是難以模型能很好地是,有一些情況下,它是說有用:

  1. 一個T應該分配給U類型的變量。
  2. 一個*T應該分配給一個*U
  3. 一個const *T應該分配給一個const *U

但C++使得它們之間沒有區別。 Java和C#避免只提供第二語義問題(這是不可能有持有類對象實例變量,而這些語言不使用指針符號,所有的類類型的變量是隱含在別處存儲對象的引用)。在C++中,然而,有聲明沒有簡單的形式,其簡單地允許在不與第一,第二或第三種形式中,也沒有任何的方式來區分「指針的東西,其可以被存儲在U類型的變量」從「指針其中包含U的所有虛擬和非虛擬成員「。這將有可能對語言的類型系統進行「嚴格的」和「非嚴格的」指針類型之間的區別,並允許U類的虛方法來指定:

  1. 它必須由被覆蓋不能存儲在U類型的變量的任何類型,並且...

  2. 在該方法中,this應類型的如U strict *,和提領U strict *類型的變量應該產生U strict類型的右值,這應該可以指定爲U類型之一,即使類型的右值3210不會。

C++提供沒有這樣的區別,然而,這意味着沒有辦法需要一個指向東西,可以被存儲在U類型的變量方法之間進行區分,與那些需要的東西,有同樣的成員。

2

考慮

class Account 
{ 
    char *name = new char[16]; 

    public: virtual ~Account() { delete[] name; } 
    public: virtual void sayHello() { std::cout << "Hello Base\n"; } 

}; 

class BankAccount : public Account 
{ 
    private: char *bankName = new char[16]; 
    public: virtual ~BankAccount() override { delete[] bankName; } 
    public: virtual void sayHello() override { std::cout << "Hello Derived\n"; } 

}; 

int main() 
{ 
    BankAccount d; 

    Account a1 = d; // slicing 
    Account& a2 = d; // no slicing 

    a1.sayHello(); // Hello Base 
    a2.sayHello(); // Hello Derived 

} 

這裏a1會泄漏bankNameAccount::~Account,而不是BankAccount::~BankAccount,運行,因爲它沒有辦法來調用多態行爲。至於爲什麼它是如此特別,它已被很好地解釋here