最小的 C++ STL 向量实现问题

Posted

技术标签:

【中文标题】最小的 C++ STL 向量实现问题【英文标题】:Minimal C++ STL Vector Implementation Problems 【发布时间】:2011-10-13 11:20:42 【问题描述】:

我遇到了一个技术问题,这真的让我很困惑。我提前道歉,因为我可能没有提供相关细节;我还不知道为什么会出错,而且包含我正在使用的所有代码是过分的。

我正在处理一个使用 C++ STL 的大型程序。我正在将此代码移动到一个非常敏感的环境,没有标准的 clib 或 STL 实现;它将重新定义 malloc/free/new/delete 等...为此,我需要用我自己的简化实现替换 std:: 部分。我从 std::vector 开始。现在它在标准生态系统中运行,所以它是 GNU libc 和 STL。唯一改变的是这个向量类。

当我使用替换的类执行程序时,它会出现段错误。我已经通过 GDB 进行了处理,发现程序将使用下标运算符从向量中请求一个对象。当对象引用被返回时,一个方法被调用并且程序段错误。似乎找不到此方法并最终出现在 GDB 的 main_arena() 中。对象的类型是继承类。

我真的完全不确定这里有什么问题。我很想提供更多细节,但我不确定我还能提供什么。我只能假设我的向量实现有问题,因为程序中没有任何其他内容被更改。也许有一些明显的事情我在这里做错了,我根本没有看到。

我正在使用:g++ (GCC) 4.4.5 20110214 (Red Hat 4.4.5-6)

非常感谢任何反馈/建议!

#ifndef _MYSTL_VECTOR_H_
#define _MYSTL_VECTOR_H_

#include <stdlib.h>
#include <assert.h>

typedef unsigned int uint;

namespace mystl

    /******************
      VECTOR
    ********************/


    template <typename T>
    class vector 
    

        private:

            uint _size;
            uint _reserved;
            T *storage;

            void init_vector(uint reserve)
            
                if (reserve == 0)
                
                    _reserved = 0;
                    return;
                

                storage = (T*)malloc(sizeof(T)*reserve);
                assert(storage);

                _reserved = reserve;
            

        public:
            vector()
            
                    // std::cerr << "default constructor " << this << std::endl;
                    storage = NULL;
                    _size = 0;
                    _reserved = 0;
            

            vector(const vector<T> &other)
            
                // std::cerr << "copy constructor " << this << std::endl;

                storage = NULL;
                _size = 0;
                _reserved = 0;


                init_vector(other.size());
                _size = other.size();

                for (uint i=0; i<other.size(); i++)
                
                    storage[i] = T(other[i]);
                
            

            vector(uint init_num, const T& init_value)
            
                    // std::cerr << "special constructor1 " << this << std::endl;

                        storage = NULL;
                        _size = 0;
                        _reserved = 0;

                      init_vector(init_num);

                      for (size_t i=0; i<init_num; i++)
                      
                          push_back(init_value);
                      
            

            vector(uint init_num)
            
                    // std::cerr << "special constructor2 " << this << std::endl;

                        storage = NULL;
                        _size = 0;
                        _reserved = 0;

                      init_vector(init_num);
            

            void reserve(uint new_size) 
               
                if (new_size > _reserved) 
                

                    storage = (T*)realloc(storage, sizeof(T)*new_size);
                    assert(storage);

                    _reserved = new_size;
                
            

            void push_back(const T &item) 
            
                if (_size >= _reserved) 
                
                    if (_reserved == 0) _reserved=1;
                    reserve(_reserved*2);
                

                storage[_size] = T(item);
                _size++;
            

            uint size() const
            
                return _size;
            

            ~vector()
            
                if (_reserved)
                
                    free(storage);
                    storage = NULL;
                    _reserved = 0;
                    _size = 0;
                
            

            // this is for read only
            const T& operator[] (unsigned i) const
            
                // do bounds check...
                if (i >= _size || i < 0)
                
                    assert(false);
                
                return storage[i];
            


            T& operator[] (unsigned i)
            
                // do bounds check...
                if (i >= _size || i < 0)
                
                    assert(false);
                
                return storage[i];
            

            // overload = operator
            const vector<T>& operator= (const vector<T>& x)
            
                // check for self
                if (this != &x)
                   
                    _reserved = 0;
                    _size = 0;
                    storage = NULL;

                    init_vector( x.size() );

                    for(uint i=0; i<x.size(); i++)
                    
                        storage[i] = T(x[i]);
                    

                    _size = x.size();
                

                return *this;
            

            uint begin() const
            
                return 0;
            

            void insert(uint pos, const T& value)
            
                push_back(value);
                if (size() == 1)
                
                          return;
                
                for (size_t i=size()-2; i>=pos&& i>=0 ; i--)
                
                    storage[i+1] = storage[i];
                
                storage[pos] = value;
            

            void erase(uint erase_index)
            
                if (erase_index >= _size) 
                
                    return;
                
                //scoot everyone down by one
                for (uint i=erase_index; i<_size; i++)
                
                    storage[i] = storage[i+1];
                
                _size--;
            


            void erase(uint start, uint end)
            

                if (start > end)
                
                    assert(false);
                

                if (end > _size)
                    end = _size;

                for (uint i=start; i<end; i++)
                
                    erase(start);
                

                assert(false);
            

            void clear()
            
                erase(0,_size);
            

        bool empty() const
        
            return _size == 0;
        

    ; //class vector



#endif // _MYSTL_VECTOR_H_

【问题讨论】:

你能给我们一个简单的main() 来复制段错误吗? 这看起来躲不过if (i &gt;= _size || i &lt; 0)unsigned i怎么会小于0? 不幸的是,我无法用最小化的代码复制故障。我想在我创建对象、将它们存储在向量中并再次访问它们之间发生了一些事情。我再玩一会儿。 【参考方案1】:

哇!

    您的赋值运算符也会泄漏内存。

    因为你正在使用 malloc/release 构造函数到你的类型 T 将不会被调用,因此你不能将你的向量用于除了最微不足道的对象之外的任何东西。

编辑:

今天早上我有点无聊:试试这个

#include <stdlib.h> // For NULL
#include <new>      // Because you need placement new

// Because you are avoiding std::
// An implementation of swap
template<typename T>
void swap(T& lhs,T& rhs)

    T   tmp = lhs;
    lhs = rhs;
    rhs = tmp;



template <typename T>
class vector

    private:
        unsigned int dataSize;
        unsigned int reserved;
        T*           data;

    public:
        ~vector()
        
            for(unsigned int loop = 0; loop < dataSize; ++loop)
            
                // Because we use placement new we must explicitly destroy all members.
                data[loop].~T();
            
            free(data);
        
        vector()
            : dataSize(0)
            , reserved(10)
            , data(NULL)
        
            reserve(reserved);
        

        vector(const vector<T> &other)
            : dataSize(0)
            , reserved(other.dataSize)
            , data(NULL)
        
            reserve(reserved);
            dataSize = reserved;
            for(unsigned int loop;loop < dataSize;++loop)
            
                // Because we are using malloc/free
                // We need to use placement new to add items to the data
                // This way they are constructed in place
                new (&data[loop]) T(other.data[loop]);
            
        

        vector(unsigned int init_num)
            : dataSize(0)
            , reserved(init_num)
            , data(NULL)
        
            reserve(reserved);
            dataSize = reserved;
            for(unsigned int loop;loop < dataSize;++loop)
            
                // See above
                new (&data[loop]) T();
            
        

        const vector<T>& operator= (vector<T> x)
        
            // use copy and swap idiom.
            // Note the pass by value to initiate the copy
            swap(dataSize, x.dataSize);
            swap(reserved, x.rserved);
            swap(data,     x.data);

            return *this;
        

        void reserve(unsigned int new_size)
        
            if (new_size < reserved)
                return;
            

            T*  newData = (T*)malloc(sizeof(T) * new_size);
            if (!newData)
                throw int(2);
            

            for(unsigned int loop = 0; loop < dataSize; ++loop)
            
                // Use placement new to copy the data
                new (&newData[loop]) T(data[loop]);
            
            swap(data, newData);
            reserved    = new_size;

            for(unsigned int loop = 0; loop < dataSize; ++loop)
            
                // Call the destructor on old data before freeing the container.
                // Remember we just did a swap.
                newData[loop].~T();
            
            free(newData);
        

        void push_back(const T &item)
        
            if (dataSize == reserved)
            
                reserve(reserved * 2);
            
            // Place the item in the container
            new (&data[dataSize++]) T(item);
        

        unsigned int  size() const  return dataSize;
        bool          empty() const return dataSize == 0;

        // Operator[] should NOT check the value of i
        // Add a method called at() that does check i
        const T& operator[] (unsigned i) const      return data[i];
        T&       operator[] (unsigned i)            return data[i];

        void insert(unsigned int pos, const T& value)
        
            if (pos >= dataSize)          throw int(1);

            if (dataSize == reserved)
            
                    reserve(reserved * 2);
            
            // Move the last item (which needs to be constructed correctly)
            if (dataSize != 0)
            
                new (&data[dataSize])  T(data[dataSize-1]);
            
            for(unsigned int loop = dataSize - 1; loop > pos; --loop)
            
                data[loop]  = data[loop-1];
            
            ++dataSize;

            // All items have been moved up.
            // Put value in its place
            data[pos]   = value;
        

        void clear()                                         erase(0, dataSize);
        void erase(unsigned int erase_index)                 erase(erase_index,erase_index+1);
        void erase(unsigned int start, unsigned int end)    /* end NOT inclusive so => [start, end) */
        
            if (end > dataSize)
               end     = dataSize;
            
            if (start > end)
               start   = end;
            
            unsigned int dst    = start;
            unsigned int src    = end;
            for(;(src < dataSize) && (dst < end);++dst, ++src)
            
                // Move Elements down;
                data[dst] = data[src];
            
            unsigned int count = start - end;
            for(;count != 0; --count)
            
                // Remove old Elements
                --dataSize;
                // Remember we need to manually call the destructor
                data[dataSize].~T();
            
        
        unsigned int begin() const  return 0;


; //class vector

【讨论】:

您好,感谢您的反馈。你能详细说明一下擦除问题吗? @Bob,请仔细查看循环条件,尤其是。结束条件检查。 @Bob: 和断言。以及擦除的复杂性。目前是 O(n^2) 也许我错过了什么。您是说擦除执行不力还是语义错误?我同意前者,但我没有看到后者。看起来很奇怪,第二次擦除用擦除(开始)调用第一次擦除,并且开始是一个常量变量。当考虑到第一次擦除的语义时,这是有道理的。 @Bob:不,我想制作 RHS 的副本,这样我就可以使用交换了。这是实现赋值运算符的标准方式。请查收Copy and Swap Idiom【参考方案2】:

根据您当前的内存处理,此向量仅适用于普通的旧数据类型。

要处理所有类型,必须确保对象

实际上是创建的(malloc 不这样做), 已销毁(免费不这样做), 并且您不能使用 realloc 重新分配内存,因为如果将复杂对象按字节复制到另一个位置,则不能保证它们保持有效。

【讨论】:

【参考方案3】:

看起来可以在您的问题中找到答案:“当返回对象引用时,会调用一个方法并且程序会出现段错误。它似乎找不到该方法并最终出现在 GDB 中的 main_arena() 中。对象的类型是继承类。”

您可能将基类实例 T 存储在向量中,但是为从 T 继承的类的实例创建 push_back。在 push_back storage[_size] = T(item); 中,您进行了强制转换(实际上是创建复制构造函数 T: T(const T&)) 项到 T (这可能命名为“类型切割”),然后获取对 T 的引用并使用 T 的虚拟表调用从 T 继承的类的方法,其中该方法尚未定义/抽象。我对吗? 为了使其正常工作,您应该将 T* 放入向量或 shared_ptr/unique_ptr 中,具体取决于您应用于向量元素的所有权条款。 通常在向量中只能存储 POD(Plain Old Data)类型。

【讨论】:

希望三年半后这仍然有用:) 许多序列容器用户不了解 C++ 标准,“奇怪”错误成倍增加。 作为我上面评论的补充:C++ ISO 标准文本:“23.2.3 序列容器序列容器组织了一组有限的对象,所有对象的类型都相同,......” object 在构造时被定义为最派生的对象构造函数调用。即使它被强制转换为基类,它仍然有自己的类型。但是当类型切割发生时,结果对象的类型被更改为基类型,并且类型变得与它被强制转换的基类相同。

以上是关于最小的 C++ STL 向量实现问题的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中为我自己的自定义向量模板类实现迭代器(类似于 STL)[重复]

修改 C++ 算法 STL 中的 make_heap 以作为最小堆工作

c++ stl里的向量vector非常好用,那么它是怎么实现的呢

C++:问题向量 STL

关于 C++ 和 stl 向量的 begin() 的“需要左值作为增量操作数”

C++ STL 中向量的恒定时间交换逻辑