【问题标题】:Using user defined types with std::vector and std::sort使用带有 std::vector 和 std::sort 的用户定义类型
【发布时间】:2020-02-03 15:22:55
【问题描述】:

我一直在尝试将用户定义的类型(或类)与 C++ 标准库容器 - 向量一起使用。我希望能够使用内置的 C++ 类型、int、float、string 等的向量来做我喜欢做的通常的事情。但是使用我自己定义的类型。我编写了一个使用box 类的小示例程序来尝试了解发生了什么。

这是该类的代码:

class box {

private:
    float *lengthPtr;
    std::string *widthPtr;

public:

    box(float a, std::string b) {
        std::cout<<"Constructor called" << '\n';
        lengthPtr = new float;
        *lengthPtr = a;

        widthPtr = new std::string;
        *widthPtr = b;     
    } 
    // copy constructor 
    box(const box &obj){
        std::cout<< "User defined copy constructor called" << '\n';
        lengthPtr = new float;
        *lengthPtr = obj.check_length();

        widthPtr = new std::string;
        *widthPtr = obj.check_width();

    }
    // copy assignment operator
    box& operator=(const box &that) {
        std::cout<< "Copy assignment operator called";            

            float *localLen = new float;
            *localLen = that.check_length(); 
            delete[] lengthPtr; 
            lengthPtr = localLen;

            std::string *localWid = new std::string;
            *localWid = that.check_width();
            delete[] widthPtr;
            widthPtr = localWid;            

        return *this;
    }

    ~box() {
        std::cout << "User defined destructor called." << '\n';
        delete lengthPtr;
        delete widthPtr;
    }

    float check_length () const {
        return *lengthPtr;
    }

    std::string check_width() const{
        return *widthPtr;
    }

    void set_legnth(const float len) {
        *lengthPtr = len;
    }
    void set_width(const std::string str) {
        *widthPtr = str;      
    }

    void print_box_info(){  
        std::cout << *lengthPtr << " " << *widthPtr << '\n';
    }
};

我希望能够做的两件事主要是:

  1. 使用 .push_back() 将任意数量的我的用户定义类型 (box) 的新元素添加到向量中。

  2. 存储元素后,我想使用std::sort 和用户定义的比较函数对它们进行排序。

这是我用来测试我的两个目标的主要功能:

int main() {
    srand(time(NULL));
    int i = 0;
    std::vector<box> boxes;

    while (i<25) {
        int x = rand()%100+1;
        std::cout<< "x = " << x << '\n';

        if ( i < 5)        
            boxes.push_back(box(x, "name"));
        if ( i > 4 && i < 12)
            boxes.push_back(box(x, "Agg"));
        if ( i > 11 && i < 20 )
            boxes.push_back(box(x, "Cragg"));
        if (i>19)
            boxes.push_back(box(x, "Lagg"));

        std::cout << "Added the new box to the collection." << '\n';

        i++;  
    }
    for(unsigned int j = 0; j<boxes.size(); j++) {
            boxes[j].print_box_info();
    }
    std::sort(boxes.begin(), boxes.end(), type_is_less);
}

到目前为止,我编写的代码似乎能够完成目标 1。运行程序后,while 循环之后的 for 循环打印存储在我的框向量中的 25 个框的信息。但是,当我尝试使用 std::sorttype_is_less() 函数对我的盒子进行排序时:

bool type_is_less(const box &a, const box &b) {
    std::cout<<"In type is less." << '\n';
    std::string A = a.check_width();
    std::string B = b.check_width();

    std::cout<< "Comparing box a, width = "  << A << '\n';
    std::cout<< "with box b, width = " << B << '\n'; 
    bool val = A<B;
    std::cout << "Returning " << val <<'\n' <<'\n'; 
    return A<B; 
}

我遇到了分段错误,但我不确定错误来自何处。用户定义的复制构造函数似乎是 seg 故障发生之前调用的最终函数。在push_back() 中似乎可以使用复制构造函数,但在std::sort 中会导致问题?

我尝试在每行之间使用std::cout 消息调试复制构造函数,并且复制构造函数的每一行似乎都在执行时不会导致段错误。一旦复制构造函数完成执行,seg 错误似乎就会出现。我的控制台输出的尾部在下面(//我使用'//'插入了cmets):

将新盒子添加到集合中。

3 个名字

//...

//...

// 程序打印每个框的 2 个信息点

61 Lagg // 这是最终的盒子信息打印。

输入类型较少。 比较框 a,宽度 = 名称 带框 b,宽度 = Cragg 返回 0

输入类型较少。 比较框 a,宽度 = 名称 带框 b,宽度 = Lagg 返回 0

输入类型较少。 比较框 a,宽度 = Cragg 带框 b,宽度 = Lagg 返回 1

调用用户定义的拷贝构造函数

分段错误(核心转储)

这里有一些移动部件,我不确定如何找出我的代码的哪一部分行为不正确。一切似乎都指向用户定义的复制构造函数是罪魁祸首,但我不确定如何调整它。任何建议将不胜感激。

我尚未调查的一个悬而未决的问题是,我是否可以定义一个与此类似的类,但使用非指针变量lengthPtrwidthPtr,并且仍然具有相同的功能。

【问题讨论】:

  • 您的复制赋值运算符不防范自赋值。修复它,看看您的问题是否仍然存在。
  • 如果你传递给delete [] 一些你没有从new [] 得到的东西,行为是不确定的。
  • 使用复制/swap:box&amp; operator=(const box &amp;that) { box temp(that); std::swap(widthPtr, temp.widthPtr); std::swap(lengthPtr, temp.lengthPtr); return *this; }
  • 复制/交换修复它的原因是因为它绕过了您在尝试重新创建副本时所犯的所有错误。比如错误使用delete[]而不是delete,不检查自赋值等问题。如果您有一个正常工作的、非错误的复制构造函数和析构函数,那么在赋值运算符中使用它们。 See this
  • 我宁愿(在这种情况下)只是将成员声明为对象。如果要使用指针,请考虑使用 std::unique_ptr 或 std::shared_ptr。

标签: c++ class


【解决方案1】:

实际上,您的 box 类不需要使用任何指针,因为成员可能只是非指针类型。

但我们假设您这样做是出于实验目的:您的 box 赋值运算符有几个问题:

  1. 使用了错误的delete... 形式(应该是delete,而不是delete[])。
  2. 没有检查box 实例的自分配。
  3. 如果 new std::string 引发异常时出现问题,则您已通过更改 lengthPtr 损坏了您的对象。

对于 1),修复很简单,并且根据您的测试程序,将解决崩溃问题:

  box& operator=(const box& that) {
        std::cout << "Copy assignment operator called";

        float* localLen = new float;
        *localLen = that.check_length();
        delete lengthPtr;  // Correct form of `delete`
        lengthPtr = localLen;

        std::string* localWid = new std::string;
        *localWid = that.check_width();
        delete widthPtr; // Correct form of `delete`
        widthPtr = localWid;

        return *this;
    }

但是,如果要完成 box 对象的自分配,您的代码将导致未定义的行为。

对于 2),在尝试重新创建副本之前需要进行检查:

  box& operator=(const box& that) 
  {
        std::cout << "Copy assignment operator called";

        // check if attempting to assign to myself.  If so, just return
        if ( &that == this )
           return *this;

        float* localLen = new float;
        *localLen = that.check_length();
        delete lengthPtr;  // Correct form of `delete`
        lengthPtr = localLen;

        std::string* localWid = new std::string;
        *localWid = that.check_width();
        delete widthPtr; // Correct form of `delete`
        widthPtr = localWid;

        return *this;
    }

对于 3),请注意,使用 newnew 有可能(即使是远程的)抛出 std::bad_alloc 异常。如果发生这种情况,并且它发生在new std::string 行上,那么您将损坏您的box 对象,因为lengthPtr 已过早更改。

同样,您的示例将是非常罕见的 new 失败,但如果我们要分配几百万个 std::string 使用对 new std::string [x] 的调用,则可能会发生相同的情况。

为避免因动态内存分配失败而损坏对象,您应该在对对象本身进行任何更改之前预先分配所有需要的内存,并检查每个分配(第一个分配除外)是否引发异常。然后如果抛出异常,你必须回滚之前成功分配的内存。

这是一个例子:

box& operator=(const box& that) 
{
    std::cout << "Copy assignment operator called";
    if ( &that == this )
       return *this;

    // Allocate everything first
    float* localLen = new float;  // If this throws, we'll exit anyway.  No harm
    std::string* localWid = nullptr;  
    try 
    {
        localWid = new std::string;  // If this throws exception, need to rollback previous allocation and get out
    }
    catch (std::bad_alloc& e)
    {
       delete localLen;  // rollback previous allocation and rethrow
       throw e;
    }

    // Everything is ok, now make changes
    *localLen = that.check_length();
    delete lengthPtr;
    delete widthPtr;
    lengthPtr = localLen;
    widthPtr = localWid;
    return *this;
}

总的来说,对于一个正确工作的赋值运算符来说,这是很多工作。

好消息是,只要您有一个有效的副本构造函数和析构函数,就有一种更容易编写代码的技术可以解决所有提到的问题。该技术是copy / swap idiom

 box& operator=(const box& that) 
 {
    std::cout << "Copy assignment operator called";
    box temp(that);
    std::swap(lengthPtr, temp.lengthPtr);
    std::swap(widthPtr, temp.widthPtr);
    return *this;
 } 

不需要进行自赋值检查(即使它可以用于优化目的),也不需要检查new 是否抛出,因为没有真正完成对new 的调用(如果出现问题,创建temp 会自动将我们排除在外)。

【讨论】:

    【解决方案2】:

    分段错误的原因在于您的box&amp; operator=(const box &amp;that) 函数。

    在调试的时候发现了这个错误——

    ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new vs operator delete [])

    lengthPtrwidthPtr 不是使用 new[] 语法创建的。因此,当您尝试使用 delete[] 删除时,您会遇到分段错误。

    要从代码中删除分段错误,只需将构造函数中的 delete[] 替换为 delete 并使用赋值运算符实现即可。

    请也检查这个答案 - delete vs delete[] operators in C++

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-02
      • 1970-01-01
      • 1970-01-01
      • 2016-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多