关于C++构造函数: 一些杂谈和一次诡异的bug


先说结论:

  无论何时,都不应该显式的调用构造函数

C++是一门强大的语言 在保留了C本身面向过程的特性(高性能)下 加入的面向对象的元素 以及多态的特性 使得编程风格更加灵活

C++的核心是类 按我的理解 类是一种数据类型 而类的实例是对应类型的变量 例如:

 1 void func (void)
 2 {
 3     //do sth.
 4 }
 5 
 6 class typ
 7 {
 8 public:
 9     long   a;
10     char   b;
11     void (*f)(void);
12     void   e (void)
13     {
14         a = 0;
15         b = 0x00;
16         f = Null;
17     }
18     typ()
19     {
20         a = 1234;
21         b = 0xAB;
22         f = func;
23     }
24 private:
25     char   c;
26     char   d;
27 };
28 
29 int main (void)
30 {
31     typ   A;
32     typ * B = new typ;
33     return 0;
34 }

在以上的代码中定义了一个类typ并生成了两个实例A和*B 其中类名typ是数据类型,而A和*B是typ类型的变量 从底层的角度看 他们分别是栈区和堆区上的一段内存 长度是sizeof(typ)

那么问题来了 sizeof(typ)是多少?

答案是11 3个char类型各占1字节 long类型4字节 函数指针4字节 而函数成员 包括显式定义的 void e(void) 构造函数typ() 以及隐含的析构函数 他们存储在程序区 是不占用数据空间的

实际上 这也是类相较于结构体的优势 后者虽然可以通过函数指针的形式实现类似函数成员的效果 但占地方不说 还很危险——怎么保证被使用的函数指针在被调用前一定装好了值? 结果就是每个结构体在调用前都需要初始化一下 这一步如果出了差错 那程序100%就崩了 不会有任何意外 而类则完美的解决了这个问题:每个类在实例化之后都会自动调用构造函数 而构造函数可以被自由定义 不过 类本身可以包含函数成员 所以也用不着函数指针了

有一点需要注意由于类的封装性 类的成员在类外是不可见的在合法的手段下不可能脱离类的实例而单独调用其成员 就像不可能修改有const属性的变量一样 但我们知道 C/C++作为一种贴近底层的语言 在程序运行期间是没有合法性检查的 所有的类型/权限检查和限制都是编译期间完成的 简而言之 诸如变量类型啊 const之类的修饰符啊 声明一个函数是那个类的成员啊 这些东西都是给编译器看的 他们会在编译期间发挥作用 对还是代码的程序进行一些限制和检查 但一旦编译结束 这些东西就没了 底层不知道什么是数据类型 从底层来看 所谓的变量无非就是一截或长或短的存储器或寄存器空间在32位机上 类型为int/long/char[4]/void* 的变量是一样的 都是占4个字节内存的数据 程序用它存一个长整型数 他就是int/long 用它存4个字符 他就是char[4] 而编译期间的类型检查保证了你不能用一个长整型的空间存4个字符; 而类的成员函数跟普通函数在底层也没什么区别 都是程序区的一段程序 依靠编译期间的权限检查保证了你不能脱离类的实例而单独调用其成员

因此 从实际上来说 所有类的全部成员函数在程序开始运行的那一刻就已经全部就绪了(就像普通的函数一样) 而类的实例却是在被使用的时候才分配内存调用构造函数进行初始化的 因此 使用一些不合法的手段 是可以直接使用类的成员函数的 比如 直接指定该函数的地址 然后使用绝对指针访问

但是但是 这种做法编译器是不认可的 在编译器看来 所有的UB行为都是在作死 所有的非法访问都是在找死(事实上也确实是) 出现了UB 之后的事得看编译器的心情 心情好就提示一下 心情不好就直接放过去 反正程序跑不通 急的人是你

所以一个程序的命运啊 当然要靠程序员的奋斗 但是也要考虑编译器的进程

接下来就是一个我被编译器Gank的故事:

事情是这样的 我写了一小段单片机的程序(IDE:Keil-MDK5 MCU:STM32F1) 用于封装一个无线模块的逻辑层操作

 1 class linker 
 2 {
 3 public:
 4     linker();
 5     linker(linker_address);
 6     linker(linker_address,linker_rcv_cbf);
 7 
 8 private:
 9     linker_address  local;
10     linker_rcv_cbf  rcv;
11     //...很多其他的成员函数和变量
12     volatile enum
13     {
14         linker_stat_UnInit = 0x00 ,// 未初始化(正常不会出现)
15         linker_stat_Free   = 0xA0 ,// 空闲(初始状态 by构造函数)
16         linker_stat_Busy   = 0xB0 ,// 忙碌
17         linker_stat_Stop   = 0xD0 ,// 已停止
18         linker_stat_Error  = 0xEE ,// 出错
19     }   status;
20 };
21 
22 linker::linker(linker_address addr)
23 {
24     linker(addr,linker_receive_callback_default);
25 }
26 linker::linker(linker_address addr,linker_rcv_cbf rcvf)
27 {
28     local  = addr;
29     rcv    = rcvf;
30     status = linker_stat_UnInit;
31 }

在设计中 linker类会被实例化为全局变量 由于全局变量的构造函数早于main函数运行 此时系统还没有初始化 各种外设还没有启动 故linker类的构造函数中不能进行具体的初始化工作 构造函数的目的是记录下实例化时传入的参数 在linker被使用的时候 相关的接口函数会进行初始化

linker_receive_callback_default是一个空函数 作为默认的接收函数使用 这样如果用户不需要接收的话 也不需要自行定义一个接收函数了

1 //实例化
2 linker  local (0x1234ABCD);
1 //或者
2 void Receive (rx_pld * buf)
3 {
4     // ...
5 }
6 
7 linker  local (0x1234ABCD,Receive);

设计里是有其它部分接收数据的 回调函数只是留给用户的拓展 因此我倾向于第一种写法

然后 诡异的情况出现了

编译通过 连警告都没有 非常完美 除了执行结果不对之外...

为了确定问题 我在 linker 的接口函数加入以下内容来报告问题(linker_log类似printf)

 1 int  linker::available(void)
 2 {
 3     if(status == linker_stat_UnInit)
 4     {
 5         linker_log("linker called before init!\r\n");
 6         linker_log("linker Auto init(%d)\r\n",init(local,rcv));
 7     }
 8     
 9     linker_log("linker @[0x%08X] status: 0x%02X \r\n",(int)this,status);
10     
11     //一些操作
12 }
 1 int  linker::init(linker_address addr,linker_rcv_cbf rcvf)
 2 {
 3     linker_log("linker @[0x%08X] init: (0x%08X,0x%08X) \r\n",(int)this,(int)addr,(int)rcvf);
 4     if(linker_address_check(addr)) return 1;
 5     if(linker_rcv_cbf_check(rcvf)) return 2;
 6     if(linker_hw_init      (addr)) 
 7     {
 8         status = linker_stat_Error;
 9         return 3;
10     }
11     //一些操作
12     status = linker_stat_Free;
13     return 0;
14 }

通过串口输出结果

分析结果 首先通过打印的this指针确认这个对象确实是在内存里 也确实正确调用了linker::init 但是明显传入的两个参数不对 这个过程发生在linker::available 里 本来传入的是构造函数记录的实例化参数 存在类的成员变量里

那么 可能的原因大概有两个:

    1.一开始就没存上

    2.在用之前被意外的修改了

开始跟踪调试

从这张图可以看出 linker的构造函数确实被调用了 传入的参数也是正确的(在寄存器R1里)

继续执行

这一步看似也没问题 简易的构造函数linker(linker_address)调用了完整的构造函数 linker(linker_address,linker_rcv_cbf)并传入缺省参数 比较奇怪的是接下来两条语句的断点失效了 这个情况我是第一次遇到 手册上也没有说明 先不管它了

下一步就发现问题了

看红框处 可以发现 明明赋值语句已经执行过了 但成员变量的值没有变 问题就出在这里

于是 回到上一步 仔细观察 发现了异常

如图所示 反汇编结果(中间的天蓝色框)没问题 编译器使用双机器字存储指令同时将保存在R1和R2的内容通过R0基址变址寻址存入[R0+#0]的位置 但在寄存器栏(左侧窗口)就发现问题了可以看到R1与R2传入的参数是没问题的 但R0里的地址(左侧红色框)很奇怪 并不是预期的值(正确的值应该是右侧橘黄色框里的那个)

至此 问题已经找到了 接下来是解决问题

首先确认这个奇怪的值( 0x20002BA4 )是什么

打开映像表

可以看到0x20002BA4 这个地址在堆区里... 很尴尬

显然 这个地址并不是我手动分配的 编译器也不会闲着没事往堆里塞东西 那么 唯一的解释就是 这其实是栈上的数据 栈溢出了 占用了堆的空间

再回去检查是哪一步导致栈溢出了

非常幸运 一眼就能看出来 就在跟踪调试的第二步 下图截取自调试界面的第一二张图片(注:栈是向小方向生长的)

emmm... 0x30B0-0x2BA4=0x050C=1292Byte的堆栈开销

这个数好像很眼熟(local就是linker的实例)

显然 这两步之间 系统在栈上新建了一个实例 由于这个结构本身的大小(1288Byte)超过了栈的深度(1024Byte) 于是一直延伸到了堆空间里

那么在此期间执行的语句是

emmm 就调用个函数怎么这么大的开销?

反汇编结果证实了我的猜测

-------------------------------------------- 问题结束 以下是答案 --------------------------------------------

查找了一些资料 明白了问题所在

我在一个构造函数里调用了另一个构造函数 本意是把构造函数当成一个普通的成员使用了 但这样做是不对的:

 首先 回想下类的实例化过程

    1.分配内存

    2.调用构造函数

 也就是说 在构造函数返回之前 类其实是还没实例化的 此刻调用其成员函数 就像调用一个未初始化的结构体成员一样危险

 其次 由于类还没有实例化 而调用了它的成员函数 所以编译器会自动给你实例化一个对象... 也就是上图中的语句执行的真正行为并不是调用本实例的另一个构造函数 而是自动建立了一个匿名的新实例 并调用了它的构造函数进行初始化 所以一开始的问题中 消失的参数实际上是赋给这个新实例了 由于类的实例本质上是个变量 而这个新变量又是函数的局部变量 因此 系统把它分配在栈上 一旦函数执行完毕 这个临时实例自动析构并退栈 不留一丝痕迹(如果之前没有破坏堆里的数据的话) 说起来 这有点坑 因为这不产生提示 除了类之外 随便一个其他类型的变量定义了不用直接退出肯定都会有警告的

所以说 构造函数吧 没必要的话应该避免显式调用(说实话 我也想不到有什么情况是必要的)

另外 栈溢出了那么多居然程序没有崩溃 果然内存大就是可以为所欲为的

优质内容筛选与推荐>>
1、转:Senparc.Weixin.MP SDK 微信公众平台开发教程(十二):OAuth2.0说明
2、页面居中的一系列问题
3、windows server 2003如何安装IIS,配置IIS,让iis支持aspx(收集)
4、设计联合主键和外键的关系时,报表中的列与现有主键或UNIQUE约束不匹配的错误
5、VS2005 && MasterPage && Form 的一些相关使用记录


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn