請注意,這一節內容是c++的重點,要特別注意!
我們先說一下什麼是構造函數。
上一個教程我們簡單說了關於類的一些基本內容,對於類對象成員的初始化我們始終是建立成員函數然後手工調用該函數對成員進行賦值的,那麼在c++中對於類來說有沒有更方便的方式能夠在對象創建的時候就自動初始化成員變量呢,這一點對操作保護成員是至關重要的,答案是肯定的。關於c++類成員的初始化,有專門的構造函數來進行自動操作而無需要手工調用,在正式講解之前先看看c++對構造函數的一個基本定義。
1.C++規定,每個類必須有默認的構造函數,沒有構造函數就不能創建對象。
2.若沒有提供任何構造函數,那麼c++提供自動提供一個默認的構造函數,該默認構造函數是一個沒有參數的構造函數,它僅僅負責創建對象而不做任何賦值操作。
3.只要類中提供了任意一個構造函數,那麼c++就不在自動提供默認構造函數。
4.類對象的定義和變量的定義類似,使用默認構造函數創建對象的時候,如果創建的是靜態或者是全局對象,則對象的位模式全部為0,否則將會是隨即的。
我們來看下面的代碼:
#include <iostream>
using namespace std;
class Student
{
public:
Student()//無參數構造函數
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
};
void Student::show()
{
cout<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
cin.get();
}
在類中的定義的和類名相同,並且沒有任何返回類型的Student()就是構造函數,這是一個無參數的構造函數,他在對象創建的時候自動調用,如果去掉Student()函數體內的代碼那麼它和c++的默認提供的構造函數等價的。
構造函數可以帶任意多個的形式參數,這一點和普通函數的特性是一樣的!
下面我們來看一個帶參數的構造函數是如何進行對象的始化操作的。
代碼如下:
#include <iostream>
using namespace std;
class Teacher
{
public:
Teacher(char *input_name)//有參數的構造函數
{
name=new char[10];
//name=input_name;//這樣賦值是錯誤的
strcpy(name,input_name);
}
void show();
protected:
char *name;
};
void Teacher::show()
{
cout<<name<<endl;
}
void main()
{
//Teacher a;//這裡是錯誤的,因為沒有無參數的構造函數
Teacher a("test");
a.show();
cin.get();
}
我們創建了一個帶有字符指針的帶有形參的Teacher(char *input_name)的構造函數,調用它創建對象的使用類名加對象名稱加擴號和擴號內參數的方式調用,這和調用函數有點類似,但意義也有所不同,因為構造函數是為創建對象而設立的,這裡的意義不單純是調用函數,而是創建一個類對象。
一旦類中有了一個帶參數的構造函數而又沒無參數構造函數的時候系統將無法創建不帶參數的對象,所以上面的代碼
Teacher a;
就是錯誤的!!!
這裡還有一處也要注意:
//name=input_name;//這樣賦值是錯誤的
因為name指是指向內存堆區的,如果使用name=input_name;會造成指針指向改變不是指向堆區而是指向棧區,導致在後面調用析構函數delete釋放堆空間出錯!(析構函數的內容我們後面將要介紹)
如果需要調用能夠執行就需要再添加一個沒有參數的構造函數
對上面的代碼改造如下:
#include <iostream>
using namespace std;
class Teacher
{
public:
Teacher(char *input_name)
{
name=new char[10];
//name=input_name;//這樣賦值是錯誤的
strcpy(name,input_name);
}
Teacher()//無參數構造函數,進行函數重載
{
}
void show();
protected:
char *name;
};
void Teacher::show()
{
cout<<name<<endl;
}
void main()
{
Teacher test;
Teacher a("test");
a.show();
cin.get();
}
創建一個無闡述的同名的Teacher()無參數函數,一重載方式區分調用,由於構造函數和普通函數一樣具有重載特性所以編寫程序的人可以給一個類添加任意多個構造函數,來使用不同的參數來進行初始話對象。
現在我們來說一下,一個類對象是另外一類的數據成員的情況,如果有點覺得饒人那麼可以簡單理解成:類成員的定義可以相互嵌套定義,一個類的成員可以用另一個類進行定義聲明。
c++規定如果一個類對象是另外一類的數據成員,那麼在創建對象的時候系統將自動調用那個類的構造函數。
下面我們看一個例子。
代碼如下:
#include <iostream>
using namespace std;
class Teacher
{
public:
Teacher()
{
director = new char[10];
strcpy(director,"王大力");
}
char *show();
protected:
char *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student()
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher;//這個類的成員teacher是用Teacher類進行創建並初始化的
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
Student b[5];
for(int i=0; i<sizeof(b)/sizeof(Student); i++)
{
b[i].show();
}
cin.get();
}
上面代碼中的Student類成員中teacher成員是的定義是用類Teacher進行定義創建的,那麼系統碰到創建代碼的時候就會自動調用Teacher類中的Teacher()構造函數對對象進行初始化工作!
這個例子說明類的分工很明確,只有碰到自己的對象的創建的時候才自己調用自己的構造函數。
一個類可能需要在構造函數內動態分配資源,那麼這些動態開辟的資源就需要在對象不復存在之前被銷毀掉,那麼c++類的析構函數就提供了這個方便。
析構函數的定義:析構函數也是特殊的類成員函數,它沒有返回類型,沒有參數,不能隨意調用,也沒有重載,只有在類對象的生命期結束的時候,由系統自動調用。
析構函數與構造函數最主要大不同就是在於調用期不同,構造函數可以有參數可以重載!
我們前面例子中的Teacher類中就使用new操作符進行了動態堆內存的開辟,由於上面的代碼缺少析構函數,所以在程序結束後,動態開辟的內存空間並沒有隨著程序的結束而小時,如果沒有析構函數在程序結束的時候逐一清除被占用的動態堆空間那麼就會造成內存洩露,使系統內存不斷減少系統效率將大大降低!
那麼我們將如何編寫類的析構函數呢?
析構函數可以的特性是在程序結束的時候逐一調用,那麼正好與構造函數的情況是相反,屬於互逆特性,所以定義析構函數因使用"~"符號(邏輯非運算符),表示它為膩構造函數,加上類名稱來定義。
看如下代碼:
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
Teacher()
{
director = new char[10];
strcpy(director,"王大力");
//director = new string;
// *director="王大力";//string情況賦值
}
~Teacher()
{
cout<<"釋放堆區director內存空間1次";
delete[] director;
cin.get();
}
char *show();
protected:
char *director;
//string *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student()
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher;
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
Student b[5];
for(int i=0; i<sizeof(b)/sizeof(Student); i++)
{
b[i].show();
}
cin.get();
}
上面的代碼中我們為Teacher類添加了一個名為~Teacher()的析構函數用於清空堆內存。
建議大家編譯運行代碼觀察調用情況,程序將在結束前也就是對象生命周期結束的時候自動調用~Teacher()
~Teache()中的delete[] director;就是清除堆內存的代碼,這與我們前面一開始提到的。
name=input_name;//這樣賦值是錯誤的
有直接的關系,因為delete操作符只能清空堆空間而不能清楚桟空間,如果強行清除棧空間內存的話將導致程序崩潰!
前面我們已經簡單的說了類的構造函數和析構函數,我們知道一個類的成員可以是另外一個類的對象,構造函數允許帶參數,那麼我們可能會想到上面的程序我們可以在類中把Student類中的teacher成員用帶參數的形式調用Student類的構造函數,不必要再在Teacher類中進行操作,由於這一點構想我們把程序修改成如下形式:
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
Teacher(char *temp)
{
director = new char[10];
strcpy(director,temp);
}
~Teacher()
{
cout<<"釋放堆區director內存空間1次";
delete[] director;
cin.get();
}
char *show();
protected:
char *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student()
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher("王大力");//錯誤,一個類的成員如果是另外一個類的對象的話,不能在類中使用帶參數的構造函數進行初始化
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a;
a.show();
Student b[5];
for(int i=0; i<sizeof(b)/sizeof(Student); i++)
{
b[i].show();
}
cin.get();
}
可是很遺憾,程序不能夠被編譯成功,為什麼呢?
因為:類是一個抽象的概念,並不是一個實體,並不能包含屬性值(這裡來說也就是構造函數的參數了),只有對象才占有一定的內存空間,含有明確的屬性值!
這一個問題是類成員初始化比較尴尬的一個問題,是不是就沒有辦法解決了呢?呵呵。。。。。。
c++為了解決此問題,有一個很獨特的方法,下一小節我們將介紹。
對於上面的那個"尴尬"問題,我們可以在構造函數頭的後面加上:號並指定調用哪那個類成員的構造函數來解決!
教程寫到這裡的時候對比了很多書籍,發現幾乎所有的書都把這一章節叫做構造類成員,筆者在此覺得有所不妥,因為從讀音上容易混淆概念,所以把這一小節的名稱改為構造類的成員比較合適!
代碼如下:
#include <iostream>
using namespace std;
class Teacher
{
public:
Teacher(char *temp)
{
director = new char[10];
strcpy(director,temp);
}
~Teacher()
{
cout<<"釋放堆區director內存空間1次";
delete[] director;
cin.get();
}
char *show();
protected:
char *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student(char *temp):teacher(temp)
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher;
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl;
}
void main()
{
Student a("王大力");
a.show();
//Student b[5]("王大力"); //這裡這麼用是不對的,數組不能夠使用帶參數的構造函數,以後我們將詳細介紹vector類型
// for(int i=0; i<sizeof(b)/sizeof(Student); i++)
//{
// b[i].show();
//}
cin.get();
}
大家可以發現最明顯的改變在這裡
Student(char *temp):teacher(temp)
冒號後的teacher就是告訴調用Student類的構造函數的時候把參數傳遞給成員teacher的Teacher類的構造函數,這樣一來我們就成功的在類體外對teacher成員進行了初始化,既方便也高效,這種冒號後指定調用某成員構造函數的方式,可以同時制定多個成員,這一特性使用逗號方式,例如:
Student(char *temp):teacher(temp),abc(temp),def(temp)
由冒號後可指定調用哪那個類成員的構造函數的特性,使得我們可以給類的常量和引用成員進行初始化成為可能。
我們修改上面的程序,得到如下代碼:
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
Teacher(char *temp)
{
director = new char[10];
strcpy(director,temp);
}
~Teacher()
{
cout<<"釋放堆區director內存空間1次";
delete[] director;
cin.get();
}
char *show();
protected:
char *director;
};
char *Teacher::show()
{
return director;
}
class Student
{
public:
Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10)
{
number = 1;
score = 100;
}
void show();
protected:
int number;
int score;
Teacher teacher;
int &pk;
const int ps;
};
void Student::show()
{
cout<<teacher.show()<<endl<<number<<endl<<score<<endl<<pk<<endl<<ps<<endl;
}
void main()
{
char *t_name="王大力";
int b=99;
Student a(t_name,b);
a.show();
cin.get();
}
改變之處最重要的在這裡Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10)
調用的時候我們使用
Student a(t_name,b);
我們將b的地址傳遞給了int &pk這個引用,使得Student類的引用成員pk和常量成員ps進行了成功的初始化。
但是細心的人會發現,我們在這裡使用的初始化方式並不是在構造函數內進行的,而是在外部進行初始化的,的確,在冒號後和在構造函數括號內的效果是一樣的,但和teacher(temp)所不同的是,pk(pk)的括號不是調用函數的意思,而是賦值的意思,我想有些讀者可能不清楚新標准的c++對變量的初始化是允許使用括號方式的,int a=10和int a(10)的等價的,但冒號後是不允許使用=方式只允許()括號方式,所以這裡只能使用pk(pk)而不能是pk=pk了。
這一小節的內容是說對象構造的順序的,對象構造的順序直接關系程序的運行結果,有時候我們寫的程序不錯,但運行出來的結果卻超乎我們的想象,了解c++對對象的構造順序有助於解決這些問題。
c++規定,所有的全局對象和全局變量一樣都在主函數main()之前被構造,函數體內的靜態對象則只構造一次,也就是說只在首次進入這個函數的時候進行構造!
代碼如下:
#include <iostream>
#include <string>
using namespace std;
class Test
{
public:
Test(int a)
{
kk=a;
cout<<"構造參數a:"<<a<<endl;
}
public:
int kk;
};
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);
void main()
{
fun_t(20);
fun_t(30);
cin.get();
}
下面我們來看一下,類成員的構造順序的問題。
先看下面的代碼:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int j):pb(j),pa(pb+5)
{
}
public:
int pa;
int pb;
};
void main()
{
Test a(10);
cout<<a.pa<<endl;
cout<<a.pb<<endl;
cin.get();
}
上面的程序在代碼上是沒有任何問題的,但運行結果可能並不如人意。
pa並沒有得到我們所希望的15而是一個隨機的任意地址的值。
這又是為什麼呢?
類成員的構造是按照在類中定義的順序進行的,而不是按照構造函數說明後的冒號順序進行構造的,這一點需要記住!