通过析构函数删除时崩溃

Posted

技术标签:

【中文标题】通过析构函数删除时崩溃【英文标题】:Crash upon delete through destructor 【发布时间】:2020-03-20 21:59:32 【问题描述】:

在下面的程序中,我打算通过strcpychar* line 的内容从一个对象复制到另一个对象。 然而,当程序结束时,obj2 的析构函数工作正常,但obj 的 dtor 崩溃。 gdb 显示两个对象的 line 的不同地址。

class MyClass 
        public:
                char *line;
                MyClass() 
                        line = 0;
                
                MyClass(const char *s) 
                        line = new char[strlen(s)+1];
                        strcpy(line, s);
                
                ~MyClass() 
                        delete[] line;
                        line = 0;
                
                MyClass &operator=(const MyClass &other) 
                        delete[] line;
                        line = new char[other.len()+1];
                        strcpy(line, other.line);
                        return *this;
                
                int len(void) const return strlen(line);
;

int main() 
        MyClass obj("obj");
        MyClass obj2 = obj;

【问题讨论】:

即使您使用 C 风格的以空字符结尾的字符串,您仍在使用 C++ 进行编程。 你也需要一个拷贝构造函数。 Rule of three 那是因为我被要求通过strcpy在c++中模拟复制字符串 顺便说一句,一旦添加了复制构造函数:MyClass obj1; MyClass obj2 = obj1; 仍然会出现段错误,因为您将调用 strlen(obj1.line),即 strlen(NULL)。和MyClass obj1; obj1.len();一样。 还有未定义的行为:MyClass obj1; obj1.len(); 在空指针上调用 strlen 是未定义的行为。 【参考方案1】:

MyClass obj2 = obj;

你没有任务,你有copy-construction。而且您没有遵循rules of three, five or zero,因为您没有复制构造函数,因此默认生成的只会复制指针。

这意味着在此之后您有两个对象,其line 指针都指向完全相同的内存。这将导致未定义的行为,一旦其中一个对象被破坏,因为它给另一个对象留下了无效的指针。

天真的解决方案是添加一个复制构造函数,它对字符串本身进行深度复制,类似于您的赋值运算符正在执行的操作。

更好的解决方案是使用std::string 代替您的字符串,并遵循零规则。

【讨论】:

【参考方案2】:

您需要创建一个复制构造函数。这必须执行rule of 3/5。您正在创建obj2,这意味着调用了复制构造函数,而不是复制赋值运算符。

因为您没有复制构造函数,所以制作了一个“浅”副本。这意味着line 是按值复制的。因为它是一个指针,所以objobj2 都指向同一个内存。第一个析构函数被调用并很好地擦除了该内存。第二个构造函数被调用并发生双重删除,导致您的分段错误。

class MyClass 
public:
  char *line = nullptr;
  std::size_t size_ = 0;  // Need to know the size at all times, can't 
                          // rely on null character existing
  const std::size_t MAX_SIZE = 256;  // Arbitrarily chosen value
  MyClass()  
  MyClass(const char *s) : size_(strlen(s)) 
    if (size_ > MAX_SIZE) size_ = MAX_SIZE;
    line = new char[size_];
    strncpy(line, s, size_ - 1);  // 'n' versions are better
    line[size_ - 1] = '\0';
  
  MyClass(const MyClass& other) : size_(other.size_)   // Copy constructor
    line = new char[size_ + 1];
    strncpy(line, other.line, size_);
    line[size_] = '\0';
  
  ~MyClass() 
    delete[] line;
    line = nullptr;
  
  MyClass& operator=(const MyClass &other) 
    if (line == other.line) return *this;  // Self-assignment guard
    size_ = other.size_;
    delete[] line;
    line = new char[other.size_ + 1];
    strncpy(line, other.line, size_);
    line[size_] = '\0';
    return *this;
  
  int len(void) const  return size_; 
;

在处理 C 字符串时,绝对不能丢失空字符。问题是它非常容易丢失。您的复制分配运算符中也缺少自我分配保护。这可能会导致你不小心对一个物体产生了核武器。我添加了一个size_ 成员并使用strncpy() 而不是strcpy(),因为在丢失空字符的情况下能够指定最大字符数非常重要。它不会防止损坏,但会减轻损坏。

我确实喜欢使用 Default Member Initialization(从 C++11 开始)和使用构造函数 member initialization list 的其他一些东西。如果您能够使用std::string,那么很多这些都变得不必要了。 C++ 可以是“带有类的 C”,但值得花时间真正探索该语言所提供的功能。

工作复制构造函数和析构函数允许我们做的事情是使用“复制和交换习语”简化我们的复制赋值运算符。

#include <utility>

MyClass& operator=(MyClass tmp)  // Copy by value now
  std::swap(*this, tmp);
  return *this;

Link to explanation.

【讨论】:

更好的解决方案是使用复制和交换习语的实现。 学到了一个新东西,我喜欢它。我再补充一点。 感谢复制使用交换示例

以上是关于通过析构函数删除时崩溃的主要内容,如果未能解决你的问题,请参考以下文章

为啥我在这里的析构函数中删除时创建了一个潜在的流浪指针?

如果使用向量删除叠瓦式自己容器的析构函数中的 char* 成员,则崩溃

析构函数没调用

Qt小部件析构函数通过连接信号间接调用小部件方法,并崩溃

关闭应用程序时 QQuickItem 析构函数/changeListeners 崩溃(Qt 5.6)

调用具有条件变量等待的线程对象的析构函数时会发生啥?