c++: 构造函数(constructor)与各种眼花缭乱的初始化(initialization)

本文是作为下一篇文章的前置。首发地址:http://blog.csdn.net/madongchunqiu/article/details/22325357

【注:灰色文字,不耐可略过。】

话说我有一个总结技术用的小本本,里面记载了一些自己总结的结论。好处是本本很薄,句子都很短,因此信息量很大,复习回顾时很有用处;坏处是句子短了难免无法描述清楚所有的已知条件,所以如果不是自己,别人看起来估计会觉得都是一些漏洞百出的话语。

可惜这篇文章和下篇文章,偏偏是短小篇幅无法容纳的,因此犹豫再三,还是准备把这篇文章放在博客里,而不记录进我的小本本了。

写博客的麻烦在于,由于是面向大众的,难免谨小慎微,10个字一句话能让自己看懂的,偏偏要写出三句话每句100字,才觉得方方面面俱到且阅读流畅无障碍。啰哩啰嗦的让自己看了都觉得烦躁。。。


先说说自己对c/c++的看法(因为关系到为什么写了10多年代码才回头来写这种文章的理由):c就像是一套运动服,轻便耐用、灵活明快,很讨喜;而c++的升级就仿佛要把运动服改造成上可”九天揽月“的航天服,下可”五洋捉鳖“的潜水服,同时还要能让运动员穿了能提高成绩!左手抓着对内存直接操作的便利不肯放手,右手又弄出一堆或时尚或便捷(此处代指优化)的功能--附加上一揽子的隐式行为,心太大了,太大了。比如前些时研究了下c++11中的rvalue和move constructor,我想吐槽:又一个华丽丽的大补丁,为了“两手都要抓、两手都要硬”,这自找了多少麻烦事哟。。。相较而言,objective-c虽然是c的subset,然而脱离得彻底些,少了掣肘,故而华丽的新生了(当然主要归功于apple硬件产品的成功和提携)。

正由于c++标准中规定的一些列隐式行为,所以才有了这篇文章。

不是我想裹筋(土话,“跟某某杠上了”“纠缠于某件事情”的意思),实在是若不整理一下,我怕哪天我就走火入魔从此不敢写c++代码了。


这篇文章主要是看看c++创建对象时,①到底是调用的哪个构造函数 ②其中编译器隐式行为有哪些。

本文的结论在MAC OS X 10.9 + XCode 5.0.1中验证。

(据称Xcode从4.2开始支持C++11。本测试采用新建的OS X Application (Command Line Tools -> C++) Project,C++ Language Dialect的默认选项是“GNU++11”)


前置定义/条件

进入之前,先澄清一下文字定义和本文讨论的范畴:

1. 默认构造函数(default constructor):默认构造函数不是"一个"函数,它表示一种调用方式,即生成一个对象时不需要参数。因此默认构造函数可能是:①若用户不定义构造函数时,编译器生成的隐式构造函数;②用户定义的不带任何参数的构造函数;③用户定义的带参数的,且参数有默认值的(一般说成“带默认参数的”)构造函数;

2. 本文不讨论global/static的相关情况;

3. 本文暂不涉及operator=,甚至move constructor。


情况一:让编译器生成隐式的构造函数(implicit constructor)和拷贝构造函数

case 1: use the implicitly defined constructor and copy constructor

class A
{
  public:
    int a;
};

生成对象 对象内容 解释
A a; 987 默认初始化(default initialization)
调用隐式(编译器生成的)默认构造函数,A::a没有得到任何初始值
(注:stack区有可能碰巧为零,因此是如下验证的:连续调用两次下面的函数)
void reStack()
{
    A a;
    cout << a.a; //第一次为0;第二次为987
    a.a = 987;
}
A* pa = new A; 123 默认初始化(default initialization)
调用隐式(编译器生成的)默认构造函数,A::a没有得到任何初始值
(注:heap运行前大概被清零了,因此是如下验证的)
A* pa = new A; //此时的值可能为0
pa->a = 123;
delete pa;
pa = new A; //此时的值为123
A* pa; ? 指针未初始化
A a2 = A(); 0 数值初始化(value initialization)
没有用户定义的构造函数,零值初始化(zero initialization)
(之后是否调用隐式(编译器生成的)默认构造函数?情况很复杂,参考[1]中说不调用)
A* pa2 = new A(); 0 数值初始化(value initialization)
没有用户定义的构造函数,零值初始化(zero initialization)
(之后是否调用隐式(编译器生成的)默认构造函数?情况很复杂,参考[1]中说不调用)
A a3(a); 987 拷贝初始化(copy initialization)
调用隐式(编译器生成的)拷贝构造函数,因此a2的内容与a相同
A a4 = a2; 0 拷贝初始化(copy initialization)
调用隐式(编译器生成的)拷贝构造函数,因此a2的内容与a相同
A a5(); Warning:这是函数声明 无论看多少次,还是会落入陷阱的题目。。。
A a6 = 3; Error:无法通过编译 程序内没有将int转变成A的方法(参见情况二表格中的a6)


情况二:手动编写带默认参数的构造函数(user-defined constructor)和拷贝构造函数

case 2: use the user defined constructor (with default arguments) and copy constructor

class A
{
  public:
    A(int a_=1):a(a_) // 构造函数带参数,因此可以进行(隐式[可用explicit禁之])类型转换
    {                 // 参数有默认值,因此可作为默认构造函数
        cout << "ctor " << a << endl;
    }

    A(const A& rhs):a(rhs.a)
    {
        cout << "copy ctor " << a << endl;
    }

  private:
    int a;
}
【注:这里仅考虑构造函数的调用问题,别用rule of three(甚至c++11中可能会冒出的rule of four)来裹筋哟!】

生成对象 对象内容 解释
A a; ctor 1 默认初始化(default initialization)
调用默认构造函数(此处为用户定义,带默认参数的)
A* pa = new A; ctor 1 默认初始化(default initialization)
调用默认构造函数(此处为用户定义,带默认参数的)
A* pa; ? 指针未初始化
A a2 = A(); ctor 1 数值初始化(value initialization)
由于用户定义了构造函数,因此默认构造函数(此处为用户定义,带默认参数的)被调用
A* pa2 = new A(); ctor 1 数值初始化(value initialization)
由于用户定义了构造函数,因此默认构造函数(此处为用户定义,带默认参数的)被调用
A a3(3); ctor 3 直接初始化(direct initialization)
调用用户定义的构造函数
A a4 = A(4); ctor 4 直接初始化(direct initialization)
调用用户定义的构造函数
A* pa5 = new A(5); ctor 5 直接初始化(direct initialization)
调用用户定义的构造函数
A a6 = 6; ctor 6 隐式类型转换(implicit conversions)
调用单参数的用户定义的构造函数(single-argument constructor)
我本以为会是先调用用户定义的构造函数进行隐式类型转换,然后再调用copy ctor或者甚至operator=(已经私下验证没有调用operator=)。
A a7(a3); copy ctor 3 拷贝初始化(copy initialization)
调用拷贝构造函数
(注:这里为了打印输出定义了拷贝构造函数。若不定义,隐式的拷贝构造函数行为相同)
A a8 = a4; copy ctor 4 拷贝初始化(copy initialization)
调用拷贝构造函数
(注:这里为了打印输出定义了拷贝构造函数。若不定义,隐式的拷贝构造函数行为相同)
A a9(); Warning:这是函数声明 无论看多少次,还是会落入陷阱的题目。。。
【注:c++11还引入了一下花括号初始化(brace-init)的写法,如:A a{},若写在上面,会更加崩溃吧。。。】


扩展:基本数据类型

上面的结论可以直接用在基本数据类型上。从这点看,c++的统一处理还是做得到位的。

生成基本数据 数据内容 解释
int a; 987 默认初始化(default initialization)
未初始化
注:此987的来源同“情况一”中的相应栏
int* pa = new int; 123 默认初始化(default initialization)
未初始化
注:此123的来源同“情况一”中的相应栏
int* pa; ? 指针未初始化
int a2 = int(); 0 零值初始化(zero initialization)
int* pa2 = new int(); 0 零值初始化(zero initialization)
int a3(3); 3 直接初始化(direct initialization)
int a4 = int(4); 4 直接初始化(direct initialization)
int* pa5 = new int(5); 5 直接初始化(direct initialization)
int a6 = 6; 6 基本数据类型的赋值操作
int a7(a3); 3 拷贝初始化(copy initialization)
int a8 = a4; 4 拷贝初始化(copy initialization)
int a9(); Warning:这是函数声明 无论看多少次,还是会落入陷阱的题目。。。

总结

1. 各种初始化方式(initialization)以及其对应使用的构造函数(constructor)已经详细列在上面的表格中了

2. 标准定义的/编译器的隐式行为有:

    a. 若用户不定义构造函数,隐式生成的构造函数(implicit constructor)不带参数,且不会对基本数据类型(primitive types)做任何初始化动作;(若涉及继承(inheritance)和内含(containment),可以参看下一篇博文)

    b. 大多数时候,采用数值初始化(value initialization)【A()或者A{}】,会有隐式的零值初始化(zero initialization)动作,保证对象空间被清零;

    c. 若用户定义了单参数的构造函数(single-argument constructor),且未标明explicit关键字,则可能会有隐式的类型转换发生;

    d. 同a,会有隐式的拷贝构造函数(copy constructor)和隐式的赋值操作符(operator=),前者本文并未展开,后者未列入讨论行列。(可以参看下一篇博文)


========================== 你可能很感兴趣的分割线 ==========================

========================== 你可能不感兴趣的分割线 ==========================


顺带说说Objective-C的情况。

其实我是比较喜欢[[A alloc] init]这种写法的。内存分配和数据初始化分割开来写,看上去很是清晰明快。同时,Objective-C统一采用对象指针,摈弃了对象对象引用,既保持了高效的特性,又摒除了繁杂的表现形式(当然,有得有失,我这里只提好的方面),赞之。

在ARC(Automatic Reference Counting)之前,retain/release的计数方式挺清晰的,autorelease的引入也有了点垃圾处理的意思(当然,其实是不一样的),让人很是心情愉悦。ARC出来后,引入了更多的隐式处理,就变得不那么可爱了。当然,其实我没用过ARC...所以也不好做更多的判断。

这里可以看出本文作者的偏好:①程序员的掌控力越大越好;②编译器的隐式行为越少越好。前者的争议在于,程序越的掌控力度越大,往往代表着大型项目可能会出现更多问题;后者的争议在于,编译器不隐式做的事情,就需要程序员来做了,这对于某些有洁癖或者认为代码“必须”“简洁”的偏执人士来说,恐怕会很不爽。


//NS_ROOT_CLASS        //如果用了这个,就不必继承自别的基类了
@interface A:NSObject  //但是没有NSObject作为基类,alloc就要自己写了,要不要这么高端呢?
@property (nonatomic) int a;
@end

@implementation A
@synthesize a = a_;
@end

对于没有ARC:

类型 生成 内容 解释
基本数据类型 int i; 987 未初始化
注:此987的验证方法同“情况一”中的相应栏
类中的基本数据类型 A* a = [[A alloc] init]; 0 类中的基本数据类型会被初始化
(注1:清0操作由alloc完成)
(注2:alloc和init继承自NSObject)
对象指针 A* a; ? 未初始化
【注:只有这么短的列表吗?没有各种隐式处理,没有拷贝构造(只有NSCopying protocol),好美】

开启ARC后,最后一列变成

对象指针      A* a;        nil 开启ARC后,会进行初始化(参考[2])
strong, weak, and autoreleasing stack variables are now implicitly initialized with nil

参考

[1] Is there an implicit default constructor in C++?中的这个答案,引用了Standard上的条目,看起来很专业

[1] Transition to ARC


你可能感兴趣的:(c++)