C++ 专家和 D 语言创造者Walter Bright说:
切片问题很严重,因为它会导致内存损坏,并且很难保证程序不会受到影响。为了在语言之外设计它,支持继承的类应该只能通过引用访问(而不是通过值)。D 编程语言具有此属性。
如果有人通过给出对象切片问题导致内存损坏的 C++ 示例来解释它会更好吗?而D语言是如何解决这个问题的呢?
C++ 专家和 D 语言创造者Walter Bright说:
切片问题很严重,因为它会导致内存损坏,并且很难保证程序不会受到影响。为了在语言之外设计它,支持继承的类应该只能通过引用访问(而不是通过值)。D 编程语言具有此属性。
如果有人通过给出对象切片问题导致内存损坏的 C++ 示例来解释它会更好吗?而D语言是如何解决这个问题的呢?
考虑
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
难以很好地建模的继承的一个方面是,在某些情况下说它很有用:
T
应该可以分配给类型为 的变量U
。*T
应该可以分配给 a *U
。const *T
应该可以分配给 a const *U
。但是 C++ 没有区分它们。Java 和 C# 通过只提供第二种语义来避免这个问题(不可能有保存类对象实例的变量;虽然这些语言不使用指针表示法,但所有类类型变量都是对存储在其他地方的对象的隐式引用)。然而,在 C++ 中,没有简单的声明形式可以简单地允许第二种或第三种形式而没有第一种形式,也没有任何方法可以区分“指向可以存储在类型变量中的某物的U
指针”和“指向某物的指针”其中包含“.”的所有虚拟和非虚拟成员U
。语言的类型系统可以区分“严格”和“非严格”指针类型,U
它必须被任何不能存储在类型变量中的类型覆盖U
,并且...
在方法中,this
应该是 as 类型U strict *
,并且取消引用类型的变量U strict *
应该产生一个类型的右值U strict
,它应该可以分配给一个类型,U
即使类型的右值U
不是。
然而,C++ 没有提供这样的区别,这意味着无法区分需要指向可以存储在类型变量中的东西的指针的方法U
与需要具有相同成员的东西的方法。
以下简单的 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
同时命中levelTwo
和levelOne
析构函数正确处理对象销毁。
但是,将对象分配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