在前面的例程中,我們對成員數據的初始化,都是在函數體中進行的,但有些情況下這種初始化的方法是行不通的,例如:
#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,甚至可能是一個任意隨機的任意地址的值。
這又是爲什麼呢?
成員對象初始化的次序完全不受它們在初始化表中次序的影響,只由成員對象在類中聲明的次序決定。這是因爲類的聲明是唯一的,而類的構造函數可以有多個,因此會有多個不同次序的初始化表。如果成員對象按照初始化表的次序進行構造,這將導致析構函數無法得到唯一的逆序。