本文是作为下一篇文章的前置。首发地址: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。
case 1: use the implicitly defined constructor and copy constructor
class A
{
public:
int a;
};
生成对象 | 对象内容 | 解释 |
A a; | 987 | 默认初始化(default initialization) 调用隐式(编译器生成的)默认构造函数,A::a没有得到任何初始值 (注:stack区有可能碰巧为零,因此是如下验证的:连续调用两次下面的函数)
|
A* pa = new A; | 123 | 默认初始化(default initialization) 调用隐式(编译器生成的)默认构造函数,A::a没有得到任何初始值 (注:heap运行前大概被清零了,因此是如下验证的)
|
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) |
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++的统一处理还是做得到位的。
生成基本数据 | 数据内容 | 解释 |
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; | ? | 未初始化 |
开启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