刚刚结束近三周的模块重构,搞得比较身心疲惫,尤其是被一些摸不着头脑的小bug弄得很狼狈。在这里稍做总结。
1、慎用无符号整形作为循环变量
一个容器或数组的长度常用无符号整形(通常用size_t)来表示,常常从头到尾遍历容器时也用一个整形(int或size_t)变量来指定元素的位置,这样几乎不会出什么错。但有时我们需要从末尾向开头遍历一个容器,这时循环变量的类型就不能随便用了。看看这段代码:
size_t i, _len = 10;
double *_DAry = new double[ _len ];
for ( i = _len - 1; i >= 0; --i )
{
_DAry[ i ] = i;
}
有时为了省事,将循环变量和容器长度都定义成无符号类型,没有多想就运行这段程序,最终程序会挂掉。仔细一想就会发现,循环终止的条件是数组的第一个元素被赋值后就退出,也就是i递减到-1时。但i被定义为无符号类型,当i递减到0时再--就会变为一个很大的正整数,循环永远无法终止,直到访问到受保护的内存后程序挂掉。
所以倒序遍历一个容器时一定要慎用无符号循环变量,无论怎样遍历容器,选用int型总不会出错。
2、对一个二进制文件同时进行读和覆盖写时容易忽略的
简单举个例子,书店新进了一百本新书,要把书的价格记录到数据库的某个文件(只写):
struct Book
{
double Price; // 定价
double SellPrice; // 售价 = 定价 * 折扣
};
double _Discount = 0.9;
FILE *_Fid = fopen( "BookList.dat", "wb" );
Book _NewBook;
for ( int i = 0; i < 100; ++i )
{
cin >> _NewBook.Price;
_NewBook.SellPrice = _NewBook.Price * _Discount;
fwrite( &_NewBook, sizeof( Book ), 1, _Fid );
}
fclose( _Fid );
_Fid = NULL;
这样就可以把一百本书的价格信息顺序写入文件。
现在需要将记录好的文件打印一遍(只读):
FILE *_Fid = fopen( "BookList.dat", "rb" );
for ( int i = 0; i < 100; ++i )
{
Book _Book;
fread( &_Book, sizeof( Book ), 1, _Fid );
cout << _Book.Price << " " << _Book.SellPrice << endl;
}
fclose( _Fid );
_Fid = NULL;
这样就可以按顺序打印一百本书的价格。
现在书店的折扣改为0.85,需要修改记录的文件(同时读写):
// "r+b"模式允许读取,也允许在流的某个位置进行覆盖写入
FILE *_Fid = fopen( "BookList.dat", "r+b" );
double _NewDiscount = 0.7;
for ( int i = 0; i < 100; ++i )
{
// 读出一本书的记录,重新计算售价
Book _Book;
fread( &_Book, sizeof( Book ), 1, _Fid );
_Book.SellPrice = _Book.Price * _NewDiscount;
// 再定位到这本书在流中的位置,并重新写入,覆盖掉旧的信息
fseek( _Fid, sizeof( Book ) * i, SEEK_SET );
fwrite( &_Book, sizeof( Book ), 1, _Fid );
}
fclose( _Fid );
_Fid = NULL;
我们希望这段代码也能按顺序重新计算每本书的售价,再把信息写回去,似乎没错。但当再次打印信息时,发现价格全乱套了。
只读时,每fread一次流位置都会跳到下一个记录的开头,所以在循环打印每本书信息时,不需要fseek来指定位置,打印完一本会自动跳到下一本的位置;
只写时也不需要关心流的位置,下一条记录会自动追加到前一条的后面,流中的缓冲区涨到一定程度后自动会写入文件,也不会有问题。
同时读写时,当重新计算完售价后再次覆盖写入记录时,理论上,假如fwrite命令可以立刻将这条记录写到文件中的话,那么流的位置也应该跳跃了sizeof(Book )个字节 ,指向了下本书的开始,下一次循环就应该顺理成章的读出下本书的价格。
但实际上fwrite不会立刻写入文件,流位置也不会立刻跳跃,所以下一次循环很可能读到的还是前一本书的价格,更糟糕的甚至读不到正常的价格信息。因为写文件时流中有一个缓冲区的概念,fwrite命令只是把数据放到缓冲区里,当缓冲区饱和后才会写入文件,流位置才会跳跃,上面的程序除了读不到正确位置的信息外,还有可能输出缓冲区写到错误的位置,因为缓冲区准备写到文件时,流位置已经不是当初的位置了。
解决这个问题的办法就是每次fwrite命令后,强制写入文件:
fflush( _Fid );
总之,记住fwrite不会立刻写入文件也不会立刻跳跃流位置就可以了。
3、纯虚析构函数不仅要声明,还必须要定义一下
与一般的成员函数不同,一般的成员函数如果是纯虚的,只要在基类中声明,并且在继承类中实现就可以了。纯虚析构函数必须在基类就定义一下:
class Book
{
public:
virtual ~Book() = 0;
virtual double GetPiece() const = 0;
};
Book::~Book(){}
4、再强调容易忘掉的两点
(1)std::vector的push_back操作,如果是一般对象的话绝对会调用拷贝构造函数,插入的元素和你输入的参数除了值一样外,没有任何联系。除非vector的类型是指针类型;
(2)对于非指针对象,数组元素的赋值(比如_Ary[ 0 ] = obj)会调用类的=操作符重载(Object& Object::operator = (const Object &other )),所以=左边和右边赋值完毕后也没有任何联系;
两种赋值操作都有点类似将参数拷贝(如果没有定义拷贝构造函数或重载=,就直接拷贝内存)再记录的意思,并不是两边都指向同一块内存。
5、切记二进制文件的IO过程中尽量减少fseek的次数,哪怕多几次fwrite或fread。文件流位置的跳跃性能是非常低的。