#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操作一起解析时,这是很正常的操作。)
向量,通常希望所有成员的析构函数都被调用。
#2 楼
使用
log
计算容量非常可疑。它强制客户端将可执行文件与-lm
链接。我根本不会理会Log
:只需根据需要将capacity
加倍。应该排除构造函数中重复的代码。
使用
std::copy()
代替循环是可以的(实际上是更好的选择) 。pop_back()
必须销毁buffer[size - 1]
对象。clear()
必须delete [] buffer
。编辑:它必须销毁缓冲区中的所有内容。缓冲区本身可能会停留。感谢Loki Astari 所有访问器(
begin
,end
,front
,back
,operator[]
)也必须具有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_type
,Vector
和size_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
评论
\ $ \ 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