我无法找到以下代码中的错误,我写过[尽管不是出于任何目的]。
#include < iostream >
#include < cstdlib >
using namespace std;
class Base{
public:
Base(){cout << "Base class constructor" << endl;}
void funv() {};
~Base(){cout << "Base class destructor" << endl;} ;
};
class Derived:public Base{
public:
char *ch;
Derived():ch(new char[6]()){}
~Derived(){
cout << "before" << endl;
delete [] ch;
ch = NULL;
cout << "after" << endl;
}
};
int main(){
Derived * ptr = new Derived;
//memcpy(ptr -> ch,"ar\0",4); // Works when class Derived is derved from base and also when not derived from base
ptr -> ch = const_cast < char* >("ar0"); // Works only when class Derived is not derived from class Base
cout << ptr -> ch[1] << endl;
ptr -> funv();
delete ptr;
return 0;
}
我已经对可疑的代码行进行了评论。
我正在使用sun Studio 12。
答案 0 :(得分:2)
这是未定义的行为。无论你是否派生,它都会引起任何问题。
当您将const char*
分配给char*
时,如下所示:
ptr -> ch = const_cast < char* >("ar0");
这意味着,您要分配一个在非堆段中定义的字符串(主要在数据段中)。只应在堆段上分配delete
个内存。
此外,执行上面的赋值语句,它将ch
之前指向的泄漏内存。避免此类问题的一种方法是将变量声明为
private: char* const ch;
只要您尝试为ch
分配内容,就会出现编译错误。这样它就会让你编写包装器来分配ch
,你可以在那里处理释放。
答案 1 :(得分:1)
我认为你在理解Undefine Behavior的含义方面存在严重问题。
未定义的行为意味着,如名称所示,“未定义”。这意味着你不知道会发生什么。实际上,段错误是你可以期待的最好的东西......但不幸的是,C ++中90%的UB只是沉默。
应用程序将正常运行,甚至可以为您提供所期望的结果。一切都会好的......直到演示日当前在一百个人面前,应用程序会严重崩溃只是为了让你在youtube上惊呆了的脸。
未定义的行为是通过实验学习C或C ++是一个坏主意的原因之一。当你在C ++中出错时,语言不会对你有所帮助......沉默的假设基本上是你不会犯任何错误......如果你正在学习语言,这当然是一个非常难以匹配的要求
未定义的行为也是在C或C ++中编写时对测试套件寄予太多希望的原因也是一个坏主意。在这些语言中(以及其他存在UB的语言),编写代码时没有太多思考(因此让错误进入),然后希望以后删除它们是一种非常愚蠢的方法。虽然首先节省思考时间并且稍后用调试时间交换它的想法一般来说IMO是一种糟糕的方法(删除bug的成本/努力总是会更高),在允许UB的语言中这是一个真正的自杀,因为bug可以隐藏也是在非确定性行为背后。
显然,我不是说测试是一个坏主意......它当然是一个伟大的(如果你想保持重构的可能性,基本上是必须的)但只有如果它是在编写代码时,不要以此为借口降低你的注意力。
你基本上应该避免混淆什么是错误(即UB)与崩溃(即段错误)。崩溃是朋友......你希望尽可能多的崩溃,因为崩溃是一个错误信号。例如,要添加更多可能的崩溃点,您应该使用断言...段错误只是环境自动放置的断言,但您需要更多。当你遇到崩溃然后你知道有一个错误,你可以开始寻找它。不幸的是,当你没有崩溃时,并不意味着没有错误......它只是意味着你没有为你准备的陷阱中出现任何错误。
一段代码可以编译好(零错误和零警告),它可以通过整个测试套件......但仍然可能同时不正确。当然,即使在更高级别的语言中也是如此,其中已经确定为避免UB而支付的性能价格是值得运行时检查(例如Java)但是更高的逻辑级别。由于非确定性,C和C ++中的UB使事情变得更加困难。
在您的代码中,UB被触发,因为您正在通过调用delete[]
(字符串文字)获得的指针上调用new ... []
。这可能会或可能不会产生段错误。
您的代码还有一些其他合法但非常有问题的部分:
基类析构函数不是虚拟的。你没有使用指向base的指针来销毁派生对象,所以这是合法的,但不过这是一个坏主意。如果要派生一个类,那么析构函数应该是虚拟的。
派生类定义了析构函数,但没有复制构造函数,也没有赋值运算符。 These three methods should always go together:要么不定义它们,要么定义(或至少声明)它们中的所有三个。原因是这三种方法是由编译器自动创建的,如果它们不存在,并且自动生成的代码在一种情况下是不正确的,而在其他情况下是不可能的。例如,如果有人使用Derived *der2 = new Derived(*der1);
制作派生对象的副本,那么指向动态分配的内存的指针将被复制并可能被解除分配两次(对于被销毁的两个对象中的每一个);如果使用*der2 = *der1;
进行赋值,则除了内存泄漏之外,还会有潜在的双重释放。禁止复制构造函数和类的赋值可能是有意义的,但在这种情况下,常见的习惯用法是将这些方法声明为私有并使它们不被实现(如果其他人试图使用它们,这会产生编译错误,以及链接错误,如果方法错误地使用它们。)
答案 2 :(得分:-1)
memcpy(ptr -> ch,"ar\0",4);
memcpy
将源缓冲区中的内容复制到指针ptr->ch
指向的目标缓冲区。
ptr -> ch = const_cast < char* >("ar0");
您将值重新分配给指针本身。这很危险,因为您无法再在堆上获取原始缓冲区。那是内存泄漏。此外,在析构函数中删除此指针,该指针现在不指向堆上的缓冲区。这是未定义的。