参考测试代码1:
#include<iostream>
using namespace std;
class A
{
public:
A(int iniI,int iniJ,int iniK):i(iniI),j(i),k(iniK){}
int i;
int j;
int k;
};
int main(){
A a = A(3,4,5);
cout << a.i << endl;
cout << a.j << endl;
cout << a.k << endl;
}
初始化列表中,用被赋值3的i来赋值j,j打印出来也是3.
参考测试代码2:
#include<iostream>
using namespace std;
class A
{
public:
A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){}
int i;
int j;
int k;
};
int main(){
A a = A(3,4,5);
cout << a.i << endl;
cout << a.j << endl;
cout << a.k << endl;
}
初始化列表中先初始化j,再用j初始化i,
这次就不能遂愿了。
因为初始化列表映射(请允许我这样称呼它)形参和成员的顺序并不是真实的赋值变量的顺序,可以理解为只是简单的无顺序罗列。
这个大家都知道,我今天想知道的是,为什么会这样,什么机制决定这个初始化列表中成员的初始化是按成员声明的先后顺序执行的。
其实本文想探讨的问题就是——C++的初始化列表这个功能,是怎样整个映射到编译、运行过程的。
![](/qrcode.jpg)
提起过程,我突然联想到了scanf,或者说是cin,这些是可以给变量赋值的,但是这个是什么时候完成的?当然是运行时,这个是动态的赋值了。
所以,假设初始化列表的初始化也是运行时,不用假设,其实就是,类对象就可以运行后动态生成(但是也应该有编译时就定义好的吧?没有吗?main之外的全局对象?)
那么假如初始化列表都是运行时的事了,而类成员的顺序是编译时就确定好的,所以初始化列表对变量的初始化顺序要遵循事先定义好的顺序,也就是变量声明顺序!
但是,为什么一定要遵顼这个顺序呢?举个反例:
同样动态,我先声明个i,再声明个j,先cin给j赋值,再cin给i赋值行不行?顺序不还是可以人为改变么?所以动态这个理由说不通!
或许,就只是一种巧合,一种约定罢了。
“因为内存地址是顺序来的,所以我们初始化它也顺便按这个顺序来吧?”——编者蒙
补充:
经过汇编调试,因为地址顺序是这样的吧,也许,总之编译器就先给i赋值,另一现象是,用j给i赋值,i和k初始值都不是0,j偏偏是0!!
这是一个很费解的问题,一个面试题就是这样的,本来你认为初始化之前,j的值是乱的,一如i与k,可是它就是0,然后给i赋值0。。。
他们打了一个马虎眼,写了个0,但是我们都知道这个j(0)里的0给不到i,所以说答案i是乱的。。。
但是面试官显然对编译器抠的过于细了,也许只是基于gcc或者g++?总之,自己实际操作后,j的野值它就是0。面试官期望的结果是这个。假如你是个傻子,答个0,也对了吧?瞎猫碰死耗子?
但是现在要探讨这个机理,为什么j就刚好是0,如果用k来初始化i,k就不是0啊,见第三种。
class A
{
public:
//两种初始化结果一样
// A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){}
//别看前者没有j(0),因为j的默认值就是0,所以这个题的结果是i为0
// A(int iniI,int iniJ,int iniK):j(0),i(j),k(iniK){}
//换个k试试,看是巧合还是编译器有意优化
A(int iniI,int iniJ,int iniK):j(iniJ),i(k),k(iniK){}
int i;
int j;
int k;
};
有网友说那就是无规律可循的巧合,我就不信邪,肯定是有规律的,当时忘了这道题是怎么说的,算没算我错,我就假设这个题有更高明的解释。一定要找出来!
换个方向探索:
class A
{
public:
//额外加个变量看看有无变化
A(int iniI,int iniJ,int iniK):j(iniJ),i(l),k(iniK),l(2){}
int i;
int j;
int k;
int l;
};
看到一个可能是规律的现象,k变0了,k是倒数第2个。
(gdb) print a.i
$1 = 3097936
(gdb) print a.j
$2 = 134513646
(gdb) print a.k
$3 = 0
(gdb) print a.l
$4 = 5636084
可能和对象有关,可能和地址有关,因为现在k的地址刚好是之前j的地址,0xbffff5f8。是这个地址特殊吗?
(gdb) print &a.l
$8 = (int *) 0xbffff5fc
(gdb) print &a.k
$9 = (int *) 0xbffff5f8
(gdb) print $ebp
$10 = (void *) 0xbffff618
栈底都是0xbffff618。
对象是保证高地址不超过0xbffff5fc,这个l的地址是之前k的地址,栈底对齐的。但是这也不是栈的底部,只是变量能达到的位置吧,可能被其他东西(我在main之外定义了一个非静态的对象,这占用了空间)占用了更高地址(栈底地址高)。总之,就是这个0xbffff5fc地址的内容是0。还是什么?
清除多余对象变量的声明,再编译调试:
(gdb) print &a
$1 = (A *) 0xbffff600
(gdb) print $ebp
$2 = (void *) 0xbffff618
(gdb) print &a.l
$3 = (int *) 0xbffff60c
(gdb) print &a.k
$4 = (int *) 0xbffff608
(gdb) print a.l
$5 = 5636084
(gdb) print a.k
$6 = 134514699
(gdb) print a.j
$7 = 134514160
(gdb) print a.i
$8 = 134514688
打印发现,因为没存到地址0xbffff5fc了,所以没“0”值了。那个地址0xbffff5fc也没规定非得给对象的倒数第二个参数。完全是巧合,野值就是野值,你被忽悠后,自己又刚好遇到巧合,就更质疑里边有什么特殊机制。其实是没有的!!!!
好吧,我被面试的忽悠了,东方X信的C++题就是闹着玩的!!!!!!!!!你们不是吹毛求疵,是鸡蛋里挑骨头!!!
=====================================================================================================
还没完:关于类对象的初始化到底算哪个阶段的问题,或者说里边的成员变量初始化到底算哪个阶段,发现一个比较有意思的事情。
同样用上边那种”错误“代码:
#include<iostream>
using namespace std;
class A
{
public:
A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){}
int i;
int j;
int k;
};
int main(){
cout<<"hello"<<endl;
A a = A(3,4,5);
A a2 = A(3,4,5);
cout << a.i << endl;
cout << a2.i << endl;
}
运行:
[root@j cpp]# ./a.out
hello
134514699
5636084
[root@j cpp]# ./a.out
hello
134514699
3817460
[root@j cpp]# ./a.out
hello
134514699
5636084
可以看到,每次运行,用j上的“野”值给i赋值,结果都是固定的,想要不一样必须重新编译一次才行。(
也不一定,也许是巧合,见下边的int例子)所以说,至少这些初始化之前的值是固定的。
仔细一想,也没什么不可理解的,对象新申请了一片内存,又没初始化,赶上什么是什么呗!
只是为什么每次运行值都固定,是因为这块区域的申请与划分什么的,都定在编译阶段了吗?可以这样理解吗?
其实编译器和内存申请这学得还是不精,这里边的内存指的是虚存么?你不可能这个程序老能分配到固定的内存区域,并且那块的值也固定吧。
这个问题是通用的,比如随便声明定义一个int变量,不定义,每次运行值也是一样的。
int i不赋值一定固定吗?
g++ initializeList.cpp
[root@j cpp]# ./a.out
i:3817460
[root@j cpp]# ./a.out
i:3817460
[root@j cpp]# ./a.out
i:5636084
[root@j cpp]# ./a.out
i:3817460
[root@j cpp]# ./a.out
i:3817460
其实有例外:运行几次,有一次值就不一样了。多运行几次,还有。但是类对象的成员值运行多次就不变化,这也是巧合,或者类和普通类型不一样?
这样一来,对编译原理和内存分配这块就更费解了。
====================================================================================================
结论:也许某些书有参考答案,有进一步的说明,比如网友推荐的Effective C++,还有编译原理,或者自己用编译器分解编译过程来研究,待考察。