对象切片如何导致内存损坏?

时间:2015-09-07 14:47:46

标签: c++ inheritance d memory-corruption object-slicing

C ++专家& D语言创建者Walter Bright说:

  

切片问题很严重,因为它可能导致内存   腐败,很难保证一个程序没有   遭受它。要用语言设计它,支持的类   继承只能通过引用访问(而不是通过值)。该   D编程语言具有此属性。

如果有人通过提供一个C ++示例解释它,其中对象切片问题会导致内存损坏,那会更好吗? D语言如何解决这个问题?

3 个答案:

答案 0 :(得分:3)

考虑

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而不是bankName运行时,Account::~Account会泄漏BankAccount::~BankAccount,因为它无法调用多态行为。正如为什么这么具体,它已被大大解释here

答案 1 :(得分: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;
}

输出显示使用pOne = new levelTwo;创建的ID为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

答案 2 :(得分:2)

难以很好地建模的继承方面是,有些情况下说:

  1. T应该可以分配给U类型的变量。
  2. *T应分配给*U
  3. const *T应分配给const *U
  4. 但是C ++没有区别。 Java和C#通过仅提供第二个语义来避免这个问题(它不可能有包含类对象实例的变量;而这些语言不使用指针表示法,所有类型变量都是隐式引用到其他地方存储的对象)。然而,在C ++中,没有简单的声明形式,它只允许第二种或第三种形式而没有第一种形式,也没有任何方法可以区分指向某种可以存储在变量中的东西的指针。输入U&#34; from&#34;指向包含U&#34;的所有虚拟和非虚拟成员的内容。语言类型系统有可能区分&#34; strict&#34;和&#34;非严格的&#34;指针类型,并允许类U的虚方法指定:

    1. 必须覆盖任何无法存储在U类型的变量中的类型,并且......

    2. 在方法中,this的类型应为U strict *,解除引用类型为U strict *的变量时,应生成U strict类型的右值,应该可以分配给U类型之一,即使U类型的右值不是。

    3. 然而,

      C ++没有提供这样的区别,这意味着无法区分需要指向可以存储在U类型的变量中的东西的方法,而不需要具有某些东西的方法。同样的成员。