在程序中,各种作用域的对象很多,有些对象还包含在别的对象中,还有些对象早在main()函数开始之前就已经建立了。创建对象的唯一途径是调用构造函数。构造函数是一段程序代码,所以构造对象的先后顺序不同,直接影响程序执行的先后顺序,导致不同的运行结果。C++给构造对象的顺序做了专门的规定。
1. 局部对象
局部和静态对象是指块作用域(局部作用域)和文件作用域的对象。他们声明的顺序与他们在程序中出现的顺序是一致的。例如,下面的程序是测试局部对象与局部静态对象在不同的情况下的创建顺序:
//================================= //测试局部对象的创建顺序 //================================= #include<iostream> using namespace std; class A{ public: A(){ cout<<"A->";} }; //---------------------------------------------------------- class B{ public: B(){ cout<<"B->";} }; //---------------------------------------------------------- class C{ public: C(){ cout<<"A->";} }; //---------------------------------------------------------- void func() { cout<<"\nfunc:"; A a; static B b; C c; } //---------------------------------------------------------- int main() { cout<<"mian: "; for(int i = 1; i <= 2; ++i) { for(int j = 1; j <= 2; ++j) { if(i == 2) C c; else A a; } B b; } func(); func(); } //---------------------------------------------------------- 输出结果: main: A->A->B->C->C->B-> func: A->B->C-> func: A->C->
在C中,所有的局部变量(没有对象)都是在函数开始执行时统一创建的,创建的顺序是根据变量在程序中按语句行出现的顺序。而C++中却不同,他是根据运行中定义对象的顺序来决定对象创建的顺序。而且,静态对象只创建一次。
2. 全局对象
和全局变量一样,所有全局对象在主函数启动之前,全部已被构造。
因为构造过程是程序语句的执行过程,所以,可以想象在程序启动之前,已经有程序语句在哪里被执行过了。
全局对象的创建还涉及调试的问题。调试总是从main函数的位置开始,因此在开始捕捉错误之前,错误可能已经产生,甚至导致程序无法继续运行,你还没得到程序的控制,程序就死了。
有两种方法解决这个问题:一是将全局对象县作为局部对象来调试;二是在所有怀疑出错的地方,添加上输出信息。
构造全局对象不像局部对象那么简单,全局对象没有明确的控制流来表明其顺序。因为全局对象还有多个源文件之间的协调问题,多文件的程序只有等到程序连接之后相互之间的关系确立,但程序文件之间的关系并不等于对象创建顺序的确定,事实上全局对象的创建顺序是编译器编造出来的,因而不同的编译器做法不同。
一旦main函数开始启动,则顺序是可控的,但全局对象并不依赖于从main函数之后的运行顺序,他早在main运行之前就创建完毕了。所以,全局对象的创建顺序在标准的C++中没有规定,一切根据编译器的内在特性而定。例如:下面的代码,在student.h的基础上,两个cpp文件中分别创建了一个全局对象:
//---------------------------------------------------------- //student.h //---------------------------------------------------------- #include<iostream> using namespace std; class Student{ const int id; public: Student(int d):id(d){ cout<<"student\n"; } void print(){ cout<<"id"<<"\n"; } }; //---------------------------------------------------------- class Tutor { Student s; public: Tutor(Student &st):s(st){ cout<<"tutor\n";s.print(); } }; //---------------------------------------------------------- //测试程序 //Student1.cpp //---------------------------------------------------------- #include<Student.h> extern Student ra; //---------------------------------------------------------- Tutor je(ra); int main() {} //---------------------------------------------------------- //测试程序 //Student2.cpp //---------------------------------------------------------- #include<Student.h> Student ra(18); //---------------------------------------------------------- 输出结果: tutor 0 student
从运行结果看出,Tutor对象je中的学生号并非想象中的18,而是0。说明Tutor对象je先于ra而创建。也许,在另一次编译连接和运行中,是Student对象限于Tutor对象创建
为了避免编译器实现中的不确定问题,应尽量不要设置全局对象,这是程序设计重用级安全要诀,更不要让让全局对象之间互相依赖。
3. 成员对象
成员对象一起在类中声明的顺序构造。例如:
//---------------------------------------------------------- //测试成员对象的创建顺序 //---------------------------------------------------------- #include<iostream> using namespace std; class A{ public: A(int x){ cout<<"A:"<<x<<"->"; } }; //---------------------------------------------------------- class B{ public: B(int x){ cout<<"B:"<<x<<"->"; } }; //---------------------------------------------------------- class C{ A a; B b; public: C(int x,int y):b(x),a(y){ cout<<"C\n"; } }; //---------------------------------------------------------- int main() { C c(15,9); } 输出结果: A:9->B:15->C
4. 构造位置
(1)放在全局数据区
全局对象、常对象、静态对象都放在全局数据区的位置。要想让对象的生命期与运行的程序等寿命,只能放在全局数据区。
(2)放在栈区
在函数中定义局部对象,则随着被调用函数的返回而析构。它具有局部作用域。
(3)放在动态内存区
及放在堆区,用new申请的控件都在动态内存区。例如:
vector<int>* ap = new vector<int>[100];
即从堆区申请的100个整型数的向量控件给向量指针ap,释放,用:
deletep[]ap;