构造函数(构造器)
综述:
这是一个类似于“专题”性质的文章,会涉及构造函数(Java中叫做构造器)的各种知识和问题。
但“太过基础”的内容不再赘述(比如构造函数有什么用,构造函数的基础语法等等)。
对于c++掌握足够好的人来说,这篇文章也太过基础,可以出门去隔壁了。
本文主要目的是深入构造函数,想方设法理解是怎么运行的,出现各种问题的原因等等,希望能够“知其然,也知其所以然”。
c++和Java是主要语言,随着学习进度推进,也可能加入其它语言的内容。现阶段c++和Java将是主要内容。
为了避免混乱,文章以语言作为分类,必要时会在不同语言之间作对比。
一. C++
1. 单继承下构造函数调用顺序:
“先执行基类构造函数,再执行派生类构造函数”
这种说法标准,但不精确,更不具体。倘若同时编写了默认构造函数和非默认构造函数,如此笼统地解释将会产生非常大的问题。
当建立了某个类的一个对象时,编译器自然会先去找这个类的构造函数而不管是否有继承。
但是编译器实际上“知道”继承关系,在进入构造函数后的第一件事就是先去找基类的构造函数。
构造函数也是函数,“众所周知”,函数的相互调用遵循栈“后进先出”的原则。
因此“先基类后派生类”的说法实际上并不是说构造函数“调用”的次序,而是指从最后的结果上来看,确实是基类构造函数首先执行完成。
注意这里的用词是“执行完成”,意思是此时派生类构造函数的函数体语句还没有执行,但函数已经压栈了。
为了验证这个说法来看一个例子:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "default Base constructor." << endl;
}
Base(int i)
{
cout << "Base constructor" << endl;
}
};
class derive:public Base
{
public:
derive()
{
cout << "default derive constructor" << endl;
}
derive(int i)
{
cout << "derive constructor" << endl;
}
};
class re_derive:public derive
{
public:
re_derive()
{
cout << "default re-derive constructor." << endl;
}
re_derive(int i):derive(10)
{
cout << "re-derive constructor." << endl;
}
};
int main()
{
re_derive a;
return 0;
}
运行结果很显然是这样的:default Base constructor
default derive constructor
default re-derive constructor
很多人和很多练习认为:从结果证明了构造函数调用是从基类逐渐到派生类。但个人认为这种看法只浮于结果表面,根本就不清楚调用过程。
要想完整了解调用顺序,一定要单步执行,通过堆栈窗口看压栈和出栈的顺序。
在VS2015环境下通过F10和F11单步执行,逐步观察,调用堆栈窗口显示如下:
可以看到,基类构造函数是最后一个压栈的,并不是被先调用,但确实是第一个执行完成出栈的。
2.构造函数对成员初始化问题:初始化列表 与 函数体内赋值
先明确两件事:
1.默认构造函数:指没有形参的构造函数。
2.合成默认构造函数:编译器为你合成的默认构造函数。
如果在类中没有编写任何构造函数(无论是有参数的还是没有参数的都没有自行编写),则编译器会为你合成一个无参数的默认构造函数来执行默认初始化。除此之外,编译器不会为你合成默认构造函数。
初始化列表是先于函数体执行的。为方便起见只考虑一次继承,实际执行顺序分以下两种情况:
1.如果派生类构造函数初始化列表没有显式调用基类构造函数:
-->派生类构造函数压栈
-->基类默认构造函数压栈并执行函数体(如果基类存在非默认构造函数且不存在默认构造函数,则会报错)
-->基类构造函数退栈
-->按照成员变量定义的顺序依次根据初始化列表进行初始化*
-->执行构造函数函数体
-->派生类构造函数退栈
2.如果派生类构造函数初始化列表显式地调用基类构造函数:
-->派生类构造函数压栈
-->按照派生类构造函数初始化列表中为基类构造函数提供的实参调用相应的基类构造函数,并执行函数体
-->基类构造函数退栈
-->按照成员变量定义的顺序根据初始化列表依次进行初始化*
-->执行构造函数函数体
-->派生类构造函数退栈
*:在VS下确实如此,但在GCC下却不是这样,具体GCC是如何执行暂时没有研究。欢迎喜欢较真的同学来帮忙补充。
为了验证上述说法,可以单步执行下面的代码,观察执行顺序:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "default Base constructor." << endl;
}
Base(int i)
{
cout << "Base constructor" << endl;
}
};
class derive:public Base
{
public:
derive()
{
cout << "default derive constructor" << endl;
}
derive(int i):n(0),i(n+1),Base(i)
{
//Base::Base(i);//注1:这是错误的做法 编译能通过是因为构造函数是静态的 但实际上这句并不是执行main函数中定义的对象的构造函数
cout << "derive constructor" << endl;
cout << n << " " << this->i << endl;
}
private:
int i;
int n;
};
int main()
{
derive a(10);
return 0;
}
上面所述的执行顺序隐含了很多问题:
1.基类的构造函数确实会先于函数体和初始化列表对于成员的初始化执行。
2.如果需要执行非默认的基类构造函数,必须在派生类构造函数的初始化列表显式地调用基类构造函数,而不是在构造函数函数体中调用(其实这是一种错误的做法,见上面代码中的注1)。
3.如果未在派生类初始化列表中显式调用基类构造函数或干脆未使用初始化列表,则会调用基类的默认构造函数。
4.第3条中,如果基类定义了非默认构造函数而没定义默认构造函数,按照这一节一开始所述,编译器并不会为你合成默认构造函数,因此没有默认构造函数可用,编译器会报错。
5.初始化列表中书写成员的先后顺序并不代表初始化顺序,总是先调用基类的构造函数,然后按照成员变量声明的先后顺序依次初始化。
对于上面的5个问题可以概括来说:
基类构造函数先在初始化列表寻找调用,没有就执行默认
而后先完成初始化列表,后函数体
初始化列表中按照成员声明的顺序依次初始化。
----未完待续