在前面的例程中,我们对成员数据的初始化,都是在函数体中进行的,但有些情况下这种初始化的方法是行不通的,例如:
#include <iostream>
using namespace std;
class Date{
int da, mo;
const int yr;//const常量
public:
Date(int d, int m, int y) //有参数的构造函数
{
cout << "Have parameter: ";
da = d;
mo = m;
yr = y; //此处有错误
}
void display()
{
cout << "/n" << mo << "-" << da << "-" << yr;
}
};
int main()
{
Date a;
a.display();
getchar();
return 0;
}
在类Data中有一个成员yr是一个const int类型,它是不能在函数体中被重新赋值的。这种情况下我们只有使用另一种特殊的初始化方式——初始化列表。初始化列表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
构造函数初始化列表的使用规则:
如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
例如
class A
{…
A(int x); // A的构造函数
};
class B : public A
{…
B(int x, int y);// B的构造函数
};
B::B(int x, int y)
: A(x) // 在初始化表里调用A的构造函数
{
…
}
类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如
class A
{…
A(void); // 无参数构造函数
A(const A &other); // 拷贝构造函数
A & operate =( const A &other); // 赋值函数
};
class B
{
public:
B(const A &a); // B的构造函数
private:
A m_a; // 成员对象
};
示例9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。
示例9-2 (b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
B::B(const A &a)
: m_a(a)
{
…
} B::B(const A &a)
{
m_a = a;
…
}
示例9-2(a) 成员对象在初始化表中被初始化 示例9-2(b) 成员对象在函数体内被初始化
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。若类F的声明如下:
class F
{
public:
F(int x, int y); // 构造函数
private:
int m_x, m_y;
int m_i, m_j;
}
示例9-2(c)中F的构造函数采用了第一种初始化方式,示例9-2(d)中F的构造函数采用了第二种初始化方式。
F::F(int x, int y)
: m_x(x), m_y(y)
{
m_i = 0;
m_j = 0;
} F::F(int x, int y)
{
m_x = x;
m_y = y;
m_i = 0;
m_j = 0;
}
示例9-2(c) 数据成员在初始化表中被初始化 示例9-2(d) 数据成员在函数体内被初始化
(引自〈〈高质量c++编程指南〉〉)
为了更好地理解构造函数初始化列表的使用规则,我们再来看下面的例子。
前面我们已经说了类的构造函数和析构函数,我们知道一个类的成员可以是另外一个类的对象,构造函数允许带参数,那么我们可能会想到在程序中我们可以这样做:在Student类中把它的teacher成员用带参数的形式调用Student类的构造函数,不必要再在Teacher类中进行操作,由于这一点构想我们把在2.1中提及的程序修改成如下形式:
#include <iostream>
using namespace std;
class Teacher
{
char *director;
public:
Teacher(char *temp)
{
cout << "class Teacher:";
director = new char[10];
strcpy(director, temp);
}
~Teacher()
{
cout << "释放堆区director内存空间/n";
delete[] director;
cin.get();
}
char* GetMember()
{
return director;
}
void Show()
{
cout << "director = " << director << endl;
}
};
class Student
{
int number;
int score;
Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,
//不能在类中使用带参数的构造函数进行初始化 ;
public:
Student()
{
cout << "class Student:";
number = 1;
score = 100;
}
~Student()
{
cout << "释放class Student 内存空间/n";
cin.get();
}
void Show()
{
cout << "/nteacher = " << teacher.GetMember() << endl;
cout << "number = " << number << endl;
cout << "score = " << score << endl;
}
};
int main()
{
Student a;
Teacher b("内存空间");
a.Show();
b.Show();
getchar();
return 0;
}
可是很遗憾,程序不能够被编译成功,为什么呢?
因为:类是一个抽象的概念,并不是一个实体,并不能包含属性值(这里来说也就是构造函数的参数了),只有对象才占有一定的内存空间,含有明确的属性值!
这一个问题是类成员初始化比较尴尬的一个问题,是不是就没有办法解决了呢?呵呵。。。。。。
c++为了解决此问题,有一个很独特的方法,下面我们来看。
对于上面的那个尴尬问题,我们可以在构造函数头的后面加上冒号并指定调用那个类成员的构造函数来解决!
代码如下:
#include <iostream>
using namespace std;
class Teacher
{
char *director;
public:
Teacher(char *temp)
{
cout << "class Teacher:";
director = new char[10];
strcpy(director, temp);
}
~Teacher()
{
cout << "释放堆区director内存空间/n";
delete[] director;
cin.get();
}
char* GetMember()
{
return director;
}
void Show()
{
cout << "director = " << director << endl;
}
};
class Student
{
int number;
int score;
Teacher teacher;
public:
Student(char *temp): teacher(temp) //冒号后指定调用某成员构造函数
{
cout << "class Student:";
number = 1;
score = 100;
}
~Student()
{
cout << "释放class Student 内存空间/n";
cin.get();
}
void Show()
{
cout << "/nteacher = " << teacher.GetMember() << endl;
cout << "number = " << number << endl;
cout << "score = " << score << endl;
}
};
int main()
{
Student a("王大力");
Teacher b("内存空间");
a.Show();
b.Show();
getchar();
return 0;
}
程序将正确运行并输出:
class Teacher:: class Student: class Teacher:
teacher = 王大力
number = 1
score = 100
director = 内存空间
释放堆区director内存空间
释放class Student 内存空间
释放堆区director内存空间
大家可以发现最明显的改变在这里 :Student(char *temp): teacher(temp)
冒号后的teacher就是告诉调用Student类的构造函数的时候把参数传递给成员teacher的Teacher类的构造函数,这样一来我们就成功的在类体外对teacher成员进行了初始化,既方便也高效,这种冒号后指定调用某成员构造函数的方式,可以同时指定多个成员,这一特性使用逗号方式,例如:
Student(char* temp):teacher(temp),abc(temp),def(temp)
由冒号后可指定调用那个类成员的构造函数的特性,使得我们可以给类的常量和引用成员进行初始化成为可能。
我们修改上面的程序,得到如下代码:
#include <iostream>
using namespace std;
class Teacher
{
char *director;
public:
Teacher(char *temp)
{
cout << "class Teacher:";
director = new char[10];
strcpy(director, temp);
}
~Teacher()
{
cout << "释放堆区director内存空间/n";
delete[] director;
cin.get();
}
char* GetMember()
{
return director;
}
void Show()
{
cout << "director = " << director << endl;
}
};
class Student
{
int number;
int score;
Teacher teacher;
int &pk;
const int ps;
public:
Student(char* temp, int &k): teacher(temp), pk(k), ps(10)
{
cout << "class Student:";
number = 1;
score = 100;
}
~Student()
{
cout << "释放class Student 内存空间/n";
cin.get();
}
void Show()
{
cout << "/nteacher = " << teacher.GetMember() << endl;
cout << "number = " << number << endl;
cout << "score = " << score << endl;
cout << "pk = " << pk << endl;
cout << "ps = " << ps << endl;
}
};
int main()
{
char *name = "王大力";
int b = 99;
Student a(name, b);
a.Show();
getchar();
return 0;
}
程序将正确运行并输出:
class Teacher:: class Student: teacher = 王大力
number = 1
score = 100
pk = 99
ps = 10
释放class Student 内存空间
释放堆区director内存空间
改变之处最重要的在这里Student(char* temp, int &k): teacher(temp), pk(k), ps(10)
调用的时候我们使用
Student a(name, b);
我们将b的地址传递给了int &k这个引用,使得Student类的引用成员pk和常量成员ps进行了成功的初始化。
但是细心的人会发现,我们在这里使用的初始化方式并不是在构造函数内进行的,而是在外部进行初始化的。的确,在冒号后和在构造函数括号内的效果是一样的,但和teacher(temp)所不同的是,pk(pk)的括号不是调用函数的意思,而是赋值的意思,我想有些读者可能不清楚新标准的c++对变量的初始化是允许使用括号方式的,int a=10和int a(10)的等价的,但冒号后是不允许使用=方式只允许()括号方式,所以这里只能使用pk(pk)而不能是pk=pk了。 (引自〈〈C++面向对象编程入门〉〉)
结语:
最后一点内容我们来谈谈对象构造的顺序。对象构造的顺序直接关系程序的运行结果,有时候我们写的程序不错,但运行出来的结果却超乎我们的想象,了解c++对对象的构造顺序有助于解决这些问题。
c++规定,所有的全局对象和全局变量一样都在主函数main()之前被构造,函数体内的静态对象则只构造一次,也就是说只在首次进入这个函数的时候进行构造!
代码如下:
#include <iostream>
using namespace std;
class Test{
public:
int kk;
Test(int a)
{
kk = a;
cout << "构造参数a: " << kk << endl;
}
};
void fun_t(int n)
{
static Test a(n); //静态对象
// static Test a = n; //这么写也是对的
cout << "函数传入参数n: " << n << endl;
cout << "对象a的属性kk的值: " << a.kk << endl;
}
Test m(100); //全局对象
int main()
{
fun_t(20);
fun_t(30);
getchar();
return 0;
}
程序将正确运行并输出:
构造参数a: 100
构造参数a: 20
函数传入参数n:20
对象a的属性kk的值: 20
函数传入参数n:30
对象a的属性kk的值: 20
下面我们来看一下,类成员的构造顺序的问题。
先看下面的代码:
#include <iostream>
using namespace std;
class Test{
public:
int pa;
int pb;
Test(int j): pb(j), pa(pb+5)
{
}
};
int main()
{
Test a(10);
cout << a.pa << endl;
cout << a.pb << endl;
getchar();
return 0;
}
程序输出:
7
10
上面的程序在代码上是没有任何问题的,但运行结果却并不如人意。pa并没有得到我们所希望的15,而是7,甚至可能是一个任意随机的任意地址的值。
这又是为什么呢?
成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。