我实现了一个简单的类似矢量的结构。我将不胜感激所有与代码,样式,流程,camelCase与下划线等有关的批评。

#1 楼

您的代码类似于C ++ 03(即,没有移动构造函数或移动赋值运算符)。您绝对应该考虑更新您的类以使其具有移动意识。

关于接口的第一件事是赋值运算符与构造函数并不接近。由于这些是高度链接的,因此我希望将赋值运算符放置在非常靠近构造函数的位置(请参阅三/五规则)。您没有违反任何规则(但这是故意还是偶然?)。这些规则非常复杂,以至于标识符_的前缀对于用户空间代码来说是个坏主意。其他人建议像_这样的前缀,我个人认为这是过时的建议。只要命名明确,就不会有问题。

我个人的标识符约定是,任何可以作为对象的对象都以小写字母开头。任何类型都是以大写字母开头。其他人将不同意此约定,因为该标准未遵循该约定,这可能导致其他冲突。

这是我的规则中的例外之一。

    typedef T* Iterator;


您可能应该使用小写的m_迭代器。这是因为很多算法都将使用该类型。

良好的开始。

(和i类型)成为完全兼容的容器。

如果缓冲区的类型为T,您将很难进行这项工作。每次扩展缓冲区中的所有元素时,缓冲区将使用const_iterator构造函数初始化。对于T,这不是问题。但是,如果int具有非平凡的构造函数,那么您将付出沉重的价格来初始化可能永远都不会使用的元素。没有构造函数。

    Iterator begin();
    Iterator end();
    T& front();
    T& back();
    T & operator[](unsigned int index);


最好使用初始化列表。

T* buffer;


再次。使用初始化程序列表。

char* buffer;


您真的是想将T成员分配给缓冲区。这不是_size吗?

template<class T>
Vector<T>::Vector() {
    _capacity = 0;
    _size = 0;
    buffer = 0;
    Log = 0;
}

// This will also warn about out of order initialization.
// Not a big thing but it keeps your thinking logical.

template<class T>
Vector<T>::Vector()
    : _size(0)
    , _capacity(0)
    , Log(0)
    // Personally I would always allocate some space for the buffer
    // even if the size is zero. This way I do not have to worry about
    // the special case of buffer ever being a NULL pointer.
    //
    // Having a special case of NULL will affect all the methods and make
    // the code much more complex. Reduce code complexity means easier to
    // maintain code.
    //
    , buffer(new char[sizeof(T) * capacity])
    // Note this assumes buffer is (char* as described above)
{}


还可以使用内部算法为您提供帮助。

template<class T>
Vector<T>::Vector(const Vector<T> & v) {
    _size = v._size;
    Log = v.Log;
    _capacity = v._capacity;


再次使用初始化列表。

    buffer = new T[_size];

    for (unsigned int i = 0; i < _size; i++)
        buffer[i] = v.buffer[i];
}


这似乎是一个常数。我只是将其设为该类的常量,然后手动计算一次。对我来说,使用这些数学函数似乎有点过大。

template<class T>
Vector<T>::Vector(Vector<T> const& v)
 // Note. I put cost here   ^^^^^   most of the time it does not matter.
 // There is one corner case where it does.
    : _size(v._size)
    , _capacity(v._capacity)
    , Log(v.Log)
    , buffer(new char[sizeof(T) * _capacity])
{
    for (unsigned int i = 0; i < _size; i++)
    {
        // Now you can use placement new to copy the elements from
        // source into the current vector.
        new (buffer + sizeof(T) * i) T(v[i]);
    }
}


个人这样的班轮。

template<class T>
Vector<T>::Vector(unsigned int size) {
    _size = size;


我只是直接插入类中。

@ liv902已解决赋值运算符问题。

    Log = ceil(log((double) size) / log(2.0));
    _capacity = 1 << Log;
    buffer = new T[_capacity];
}


基本上,在确保无法抛出任何欺骗并使您的对象处于令人迷恋的状态之前,请不要更改对象的状态。这意味着您的分配应分三个阶段进行。


将src类的状态复制到临时对象中。
这是因为进行复制可能会引发异常,因此您不应将对象置于可能无效的状态。
用临时值交换对象的状态。
交换是一种例外安全操作。
现在您可以销毁旧对象状态(处于临时值中)。
这也可能引发异常,但是您的对象将处于一致状态。

此技术意味着您提供“强(交易)例外保证”(您可以在google中查找)。

实现上述目的的最简单方法是使用“复制并交换成语”。

删除时来自std :: vector的东西。

template <class T>
bool Vector<T>:: empty() const {
    return _size == 0;
}


该对象的析构函数也被调用。这使您可以将资源释放给非琐碎的资源(例如T是_capacity),而不调用析构函数意味着您有效地保留了对可以释放的对象的引用。

手动调用完全有效对象的析构函数(实际上,在与Placement new操作一起解析时,这是很正常的操作。)
向量,通常希望所有成员的析构函数都被调用。


评论


\ $ \ begingroup \ $
看起来Visual Studio 2012试图通过最大容量(容量+ 50%,所需的最小容量)来增加容量。但是话又说回来,众所周知微软会忽略标准。
\ $ \ endgroup \ $
– jliv902
14年8月19日在20:19

\ $ \ begingroup \ $
@ jliv902:那对我来说大概是1.5倍(所以我建议的1.6倍距离不远)。但我认为该标准并未真正规定。它只是定义了插入(和其他一些)特征,这些特征应摊销固定时间
\ $ \ endgroup \ $
–马丁·约克
2014年8月19日在20:26

\ $ \ begingroup \ $
@LokiAstari:1.5倍增长因子背后的逻辑是,只要增长因子小于黄金分割率,向量从理论上讲就应该(从长远来看)能够重用先前分配的内存,如果该比率大于黄金比率,则新分配将始终比所有先前分配的总和请求更多的内存。例如,请参阅此博客文章以获取进一步的说明。
\ $ \ endgroup \ $
– Mankarse
2014年8月20日在3:07



\ $ \ begingroup \ $
@Mankarse:我(有点)意识到黄金分割的关系。这就是为什么我说1.6黄金比例
\ $ \ endgroup \ $
–马丁·约克
2014年8月20日在3:08



\ $ \ begingroup \ $
@LokiAstari,您应该知道此答案正在meta上讨论。
\ $ \ endgroup \ $
–RubberDuck
15年6月13日在11:33

#2 楼


使用log计算容量非常可疑。它强制客户端将可执行文件与-lm链接。我根本不会理会Log:只需根据需要将capacity加倍。
应该排除构造函数中重复的代码。
使用std::copy()代替循环是可以的(实际上是更好的选择) 。
pop_back()必须销毁buffer[size - 1]对象。
clear()必须delete [] buffer。编辑:它必须销毁缓冲区中的所有内容。缓冲区本身可能会停留。感谢Loki Astari
所有访问器(beginendfrontbackoperator[])也必须具有const版本。尺寸的自然类型是unsigned int

PS:解决注释:

规范说

const T& front() const;
      T& front();


两者都必须实现。这不是约定俗成的问题:您希望您的类有用,也就是说,标准算法应与之配合使用-并且他们希望这些方法。

评论


\ $ \ begingroup \ $
+1您提到的一些要点。我纠正了您建议的所有内容,但日志内容除外,因为STL向量以cplusplus.com/reference/vector/vector相同的方式重新产生向量。主席先生,我应该重新考虑吗?
\ $ \ endgroup \ $
–凯杜尔伊斯兰教
2014年8月19日在17:14



\ $ \ begingroup \ $
是的,向量应该成倍增长,但这不是召唤数学库的原因。具有合理的默认容量并根据需要将其增加一倍还绰绰有余。一个非常重要的准则是不要强迫客户为未要求的客户付款。我认为您应该重新考虑。
\ $ \ endgroup \ $
–vnp
14年8月19日在17:31

\ $ \ begingroup \ $
好的,主席先生:)另一个问题是我要像int&front()const和int&back()const一样返回前后。我可以在那里使用int front()和int back()吗?会违反公约吗?如果是这样,为什么?
\ $ \ endgroup \ $
–凯杜尔伊斯兰教
2014年8月19日在17:34

\ $ \ begingroup \ $
请注意以下事项:clear()必须删除[]缓冲区。它必须删除向量的所有成员。但是您不需要取消分配缓冲区。
\ $ \ endgroup \ $
–马丁·约克
14年8月19日在18:09

\ $ \ begingroup \ $
@LokiAstari该标准未指定扩展的大小。
\ $ \ endgroup \ $
–玉石
2014年8月20日在2:02

#3 楼



使用size_type。您将不得不在代码中更改多行。如果有人使用您的unsigned int类,那么他们也将不得不更改其代码。

定义unsigned long可以使之只需更改一行代码,任何使用size_t类的人都可以更改(假设)根本不需要更改任何代码。

您可以这样定义unsigned long long

typedef unsigned int size_type ;


或者,如果使用C ++ 11:

using size_type = unsigned int ; // C++11 only



注意异常安全性。例如,让我们来看看您的复制分配运算符。

template<class T>
Vector<T>& Vector<T>::operator = (const Vector<T> & v) {
    delete[] buffer; // no throw (can cause undefined behavior)
    _size = v._size; // no throw
    Log = v.Log; // no throw
    _capacity = v._capacity; // no throw
    buffer = new T [_capacity]; // CAN THROW!!
    for (unsigned int i = 0; i < _size; i++) // no throw
        buffer[i] = v.buffer[i]; // no throw
    return *this; // no throw
}


假设我正在像这样使用您的Vector:

Vector <int> v ;
// Initialize v and add some elements
// some other code...

try {
    // copy-assignment operator
    // let's pretend it throws std::bad_alloc()
    v = someOtherVector ;

    // some other code...
}

// some other code...


假设我处理了一个异常,Vector现在将具有错误的size_typeVectorsize_type


您的复制分配运算符不会检查自我分配。

当您到达代码的这一部分时,您的程序将调用未定义的行为:

for (unsigned int i = 0; i < _size; i++)
    buffer[i] = v.buffer[i];


检查请使用复制和交换惯用法来解决此问题。


您的成员变量名称不一致。

您应遵循一个约定。

一种方法:

size_type _size;
size_type _capacity;
size_type _log;
T* _buffer;


另一种方法:

size_type m_size;
size_type m_capacity;
size_type m_log;
T* m_buffer;


还有其他一些约定。只要选择一个并坚持下去,并确保您没有使用任何保留的名称即可。



评论


\ $ \ begingroup \ $
+1提供了非常有用的答案。现在我处于pos N困境中,我应该接受谁的答案? :P
\ $ \ endgroup \ $
–凯杜尔伊斯兰教
2014年8月19日在17:53

\ $ \ begingroup \ $
@KaidulIslam您一个小时前发布了此问题。我通常等待一个星期才能接受答案(仅在代码审查中),但这仅是我一个人。我将对您认为有帮助的所有答案表示赞同,并接受对您最有帮助的答案。
\ $ \ endgroup \ $
– jliv902
2014年8月19日在17:56



\ $ \ begingroup \ $
是的。等待接受答案是一个好主意,因为随着codereview没有确切的答案,将会有更多的意见。但是,此代码段非常小,几乎没有挖掘范围。我将等待 :)
\ $ \ endgroup \ $
–凯杜尔伊斯兰教
2014年8月19日在18:02



\ $ \ begingroup \ $
@ jliv902是否真的需要检查自我分配?自我分配何时真正发生?我怀疑即使在1 / 1,000,000的情况下也会发生这种情况。但是,将始终计算条件分支的额外开销!
\ $ \ endgroup \ $
– Nikos
18-10-22在16:55

#4 楼

每次您第一次删除buffer时,都必须检查_size > 1。如果是这样,则您使用的是delete[],否则使用delete

if(m_size >1)
        delete [] buffor;
        delete buffor;


评论


\ $ \ begingroup \ $
为了清楚起见,我也将delete []放在大括号中,或者至少使用较少的缩进进行删除。
\ $ \ endgroup \ $
– Jamal♦
17年6月24日在18:12

\ $ \ begingroup \ $
@Maciej您确定吗?内存分配有new []。因此,无论其大小为delete []而不是delete均应调用。是不是?
\ $ \ endgroup \ $
– Nikos
18-10-22在16:48



\ $ \ begingroup \ $
这根本不是事实。正如@Nikos所指出的那样,规则是:如果使用new,则简单地使用delete;如果使用new [],则简单地使用delete []。
\ $ \ endgroup \ $
–fgp
19年8月15日在10:18