背景

  事情是这样的:在股票的自动化交易软件开发过程中,我们使用了万得的一套股票行情获取API,伴随着API还提供了一个示例程序,我们在开发过程中借用了部分示例程序中的代码,其中就包括一个配置读取类:ConfigSettings,这个类负责从配置文件中读入软件配置参数,并把参数的值保存在成员变量中,以供后面程序进行参数读取。

  但是除了原来支持的参数之外,我们又在配置文件中增加了新的配置参数,是股票代码和其初始持仓,我们需要从配置文件中读取股票代码和其初始持仓,于是我们便想到了在原来的配置读取类(ConfigSettings)基础上进行拓展,拓展思路也很简单,在原来的类中增加一个成员变量用于保存股票代码和其对应持仓,我们采用了标准库里的unordered_map(hash table),键就是string类型的股票代码,值就是该股票代码对应的初始持仓,后续程序想要获取持仓信息只需要读取该map即可。

  添加上读取股票代码和对应持仓部分的代码后,测试运行时程序却出现了崩溃现象(runtime error),使用断点调试,发现错误出现在向map中添加新的键值对的代码处:

1
2
3
4
std::istringstream record(line);
record >> stockcode; //读取股票代码
record >> position; //读取股票对应持仓量
mapStockInitialPosition[stockcode] = position; //atoi(position.c_str()); //初始化持仓量

  运行完第4行代码后,程序就会崩溃退出,经过反复检查,都没有发现代码有什么逻辑问题,一个很让人摸不着头脑的BUG。

查找问题

  为了确定问题出现在哪里,我们将类的成员mapStockInitialPosition变为static类型并在类外进行初始化,程序编译后运行正常。类的static成员变量与普通成员变量的区别就是存储区域不一样,static成员存储在程序的全局数据区,是类的所有对象共用的变量。既然将普通成员变量换成了static成员变量错误就消失了,说明map作为普通成员变量时遭到了破坏,所以导致了后面对map进行操作时出现了错误。我们检查ConfigSettings类的构造函数,发现了如下代码:

1
2
3
4
5
6
//配置读取类的构造函数
ConfigSettings::ConfigSettings()
{
memset(this, 0, sizeof(*this));
RestoreStatus();
}

  从代码中我们看出,ConfigSettings的构造函数使用了C语言中常用的memset()函数进行类成员的初始化,memset()函数将对象的所有内存都初始化为0,这在ConfigSettings类是一个POD类型(Plain Old Data)时是没有问题的,因为POD类型的二进制内容可以随便复制、移动、重置而不会改变POD类型对象的值。ConfigSettings成员都是POD类型(int,char,float…),而且ConfigSettings类既不存在虚继承也不存在虚函数,所以可以对ConfigSettings使用memset()函数进行初始化。更多C++ POD类型的相关信息请参考这篇博客:C++ POD(Plain Old Data)类型

  但是后面我们对ConfigSettings类进行拓展时加入了unordered_map类型的成员变量,但是unordered_map这种标准库容器不是POD类型,这就导致ConfigSettings也不是一个POD类型,所以当使用memset()函数进行初始化时会对对象造成破坏,unordered_map底层是哈希表,其中必然包括各种指针,当使用memeset将对象所有内存初始化为0的时候,unordered_map中的各种指针必然就变成了null_ptr,整个结构已经被完全破坏了,所以会后面对unordered_map进行操作的时候出现崩溃的现象也就不难理解了。

解决办法

  1. 重新写ConfigSettings的构造函数,避免使用memset()函数初始化对象。
  2. 将成员变量mapStockInitialPosition设置为static类型,可以避免在memset()时被破坏。

总结

  每次踩坑都要反思和总结,才能避免以后再次掉进同一个坑里,这次这个问题虽然看似不复杂,但是依然耗费了一两个小时去解决它。虽然花费了时间,但是收获也是有的,为了解决这个问题,我对POD类型的理解更深了,也对C语言与C++之间的区别更加了解。为了避免这种错误再次出现,在以后的编程中需要遵循下面的原则:

  • 在以后自己写C++程序时,初始化对象使用C++语言支持的构造函数进行初始化,避免使用从C语言继承而来的memset()等类似函数进行初始化。
  • 在修改或者拓展一段别人写的代码时,一定要仔细查看整段代码并大体理解代码运行过程,避免盲目修改源代码,导致后续程序运行错误。