適合具備 C 語言基礎的 C++ 教程之二
前言
在上一則教程中,通過與 C 語言相比較引出了 C++ 的相關特性,其中就包括函數重載,引用,this 指針,以及在脫離 IDE 編寫 C++ 程序時,所要用到的 Makefile的相關語法。本節所要敘述的是 C++的另外兩個重要的特性,也就是構造函數和析構函數的相關內容,這兩部分內容也是有別于 c語言而存在的,也是 c++的一個重要特性。
構造函數
類的構造函數是類的一種特殊的成員函數,它會在每次創建新的對象的時候執行,構造函數的名稱和類的名稱是完全相同的,并不會返回任何的類型,也不會返回 void。構造函數可以用于為某些成員變量設置初始值。
比方說,我們現在有如下所示的一段代碼:
- #include <iostream>
- using namespace std;
- class Person{
- private:
- char *name;
- int age;
- char *work;
- public:
- Person() {cout << "Person()" << endl;}
- };
- int main(int argc, char **argv)
- {
- Person per;
- return 0;
- }
在主函數中,定義 Person per 的同時,就會自動地調用 Person() 函數,那么不難猜出,執行 test 文件的時候,輸出結果如下:
image-20210113124209248
上述的構造函數并沒有參數,實際上在構造函數是可以具有參數的,具體的看如下所示的代碼:
- #include <iostream>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- public:
- Person(char *name, int age)
- {
- cout << "Person(char *,int)" << endl;
- this->name = name;
- this->age = age;
- }
- Person(){cout << "Person()" << endl;}
- };
- int main(int argc, char **argv)
- {
- Person per;
- Person per2("zhangsan",18);
- return 0;
- }
上述代碼中,定義第一個 Person 實例的時候,就會自動地調用無形參地構造函數,當實例化第二個 Person 類的時候,就會自動地調用有形參地構造函數。
這個時候,運行函數的輸出結果如下所示:
image-20210113125016221
可以看到調用構造函數的順序是和實例化對象的順序是一致的。
構造函數除了可以有形參,也可以有默認的形參,比如說下面這段代碼:
- #include <iostream>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- public:
- Person(char *name, int age, char *work = "none")
- {
- cout << "Person(char *,int)" << endl;
- this->name = name;
- this->age = age;
- this->work = work;
- }
- Person(){cout << "Person()" << endl;}
- void printInfo(void)
- {
- cout << "name =" << name << ",age = "<< age << ",work ="<< work << endl;
- }
- };
- int main(int argc, char **argv)
- {
- Person per;
- Person per2("zhangsan",18);
- Person per3();
- per2.printInfo();
- return 0;
- }
上述代碼中,第一條代碼和第二條代碼創建了兩個 Person 實例,在創建時依次調用構造函數,這里需要注意的是,第三條語句,這條語句看起來像是實例化了一個 per3 對象,但是 per3 括號里并沒有實參,這其實是定義了一個函數,函數的形參為void,返回值為 Person ,并非是一個對象。這里還需要注意的一點是 per2 對象,它在調用構造函數時,形參有一個默認值,所以最終,程序輸出的結果如下所示:
image-20210113131653000
在實例化對象的時候,我們也可以通過定義指針的形式實現,下面代碼是上述代碼的一個改進,并且以指針的形式實例化了對象,代碼如下所示:
- #include <iostream>
- #include <string.h>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person(){cout << "person()" << endl;}
- Person(char *name,int age, char *work)
- {
- cout << "Person(char *,int, char *)" << endl;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->age = age;
- this->work = new char[strlen(work) + 1];
- strcpy(this->work,work);
- }
- void printInfo(void)
- {
- cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl;
- }
- };
- int main(int argc,char *argv)
- {
- Person per("zhangsan",18,"teacher");
- Person per2;
- Person *per4 = new Person;
- Person *per5 = new Person(); /* 這兩種方式定義的效果是一樣的 */
- Person *per6 = new Person[2];
- Person *per7 = new Person("lisi", 18,"doctor");
- per.printInfo();
- per7.printInfo();
- delete per4;
- delete per5;
- delete []per6;
- delete per7;
- }
上述代碼中,使用了new 來分配給對象空間,再分配完之后,系統會自動的進行釋放,或者說是使用手動的方式進行釋放內存,在手動釋放內存的時候,我們采用 delete 的方式來進行釋放,當創建了兩個指針數組的時候,在手動釋放的時候,要在指針變量前面加上 [],在實例化指針對象的時候,也可以帶上參數或者說是不帶參數。下面是上述代碼的運行結果:
image-20210114125841211
析構函數
析構函數的引出
上述我們知道,在函數運行完之后,用 new 分配到的空間才會被釋放掉,那么如果是在函數調用里用 new 獲取到的空間會隨著函數調用的結束而釋放么,我們現在來做這樣一個實驗,把上述中的代碼中的主函數寫成 test()函數,然后在 main() 函數里調用。
代碼如下所示:
- #include <iostream>
- #include <string.h>
- #include <unistd.h>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person(){cout << "person()" << endl;}
- Person(char *name,int age, char *work)
- {
- cout << "Person(char *,int, char *)" << endl;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->age = age;
- this->work = new char[strlen(work) + 1];
- strcpy(this->work,work);
- }
- void printInfo(void)
- {
- //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl;
- }
- };
- void test(void)
- {
- Person per("zhangsan",18,"teacher");
- Person per2;
- Person *per4 = new Person;
- Person *per5 = new Person(); /* 這兩種方式定義的效果是一樣的 */
- Person *per6 = new Person[2];
- Person *per7 = new Person("lisi", 18,"doctor");
- per.printInfo();
- per7->printInfo();
- delete per4;
- delete per5;
- delete []per6;
- delete per7;
- }
- int main(int argc, char **argv)
- {
- for (int i = 0; i < 1000000; i++)
- test();
- cout << "run test end" << endl;
- sleep(10);
- return 0;
- }
這是運行前的空閑內存的大小:
image-20210114133025365
緊接著是函數運行完 100 0000 次的 test 函數之后的空閑內存大小:
image-20210114133140216
然后,是主函數運行完之后,推出主函數之后,空閑的內存剩余量:
image-20210114133241325
總結一下就是,在子函數里用 new 分配給局部變量的空間,具體來說在上述代碼中的體現就是用 new給 this->name分配的空間。也就是在主函數沒有運行完是不會被釋放掉的,也就是說只有在主函數運行完之后,子函數里用 new 分配的空間才會被釋放掉,因此,如果想要在子函數調用完之后就釋放掉用 new 分配的空間,就需要編寫代碼來實現。而這個操作, C++ 提供了析構函數來完成,下面是使用析構函數來進行釋放內存的代碼:
- #include <iostream>
- #include <string.h>
- #include <unistd.h>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person(){cout << "person()" << endl;}
- Person(char *name,int age, char *work)
- {
- cout << "Person(char *,int, char *)" << endl;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->age = age;
- this->work = new char[strlen(work) + 1];
- strcpy(this->work,work);
- }
- ~Person()
- {
- if (this->name)
- delete this->name;
- if (this->work)
- delete this->work;
- }
- void printInfo(void)
- {
- //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl;
- }
- };
- void test(void)
- {
- Person per("zhangsan",18,"teacher");
- Person per2;
- Person *per4 = new Person;
- Person *per5 = new Person(); /* 這兩種方式定義的效果是一樣的 */
- Person *per6 = new Person[2];
- Person *per7 = new Person("lisi", 18,"doctor");
- per.printInfo();
- per7->printInfo();
- delete per4;
- delete per5;
- delete []per6;
- delete per7;
- }
- int main(int argc, char **argv)
- {
- for (int i = 0; i < 1000000; i++)
- test();
- cout << "run test end" << endl;
- sleep(10);
- return 0;
- }
下述就是代碼運行之前,和主函數在休眠的時候的剩余內存的容量,可以看出,剩余內存的容量是一樣的,換句話說,也就是在 test()函數運行完成之后,用 new 分配的空間就已經被釋放掉了,就算執行了 1000000 次也沒有造成內存泄漏。這也說明了我們的析構函數是有作用的。
image-20210115130212394
析構函數在什么地方被調用
上述析構函數的存在避免了內存泄漏,那么析構函數是在什么時候被調用的呢,用一句話描述就是:在實例化對象被銷毀的前一瞬間被調用的,另外還要注意的是構造函數可以有很多個,有參的,無參的構造函數,但是對于析構函數來講,它只有一個,并且它是無參的。具體的來看如下所示的代碼,在剛才那段代碼的基礎上,我們添加一些打印信息,從而推斷我們析構函數調用的位置:
- #include <iostream>
- #include <string.h>
- #include <unistd.h>
- using namespace std;
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person()
- {
- name = NULL;
- work = NULL;
- }
- Person(char *name,int age, char *work)
- {
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->age = age;
- this->work = new char[strlen(work) + 1];
- strcpy(this->work,work);
- }
- ~Person()
- {
- cout << "~Person()" << endl;
- if (this->name)
- {
- delete this->name;
- cout << "The name is:" << name << endl;
- }
- if (this->work)
- {
- delete this->work;
- cout << "The work is:" << work << endl;
- }
- }
- void printInfo(void)
- {
- //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl;
- }
- };
- void test(void)
- {
- Person per("zhangsan",18,"teacher");
- Person *per7 = new Person("lisi", 18,"doctor");
- delete per7;
- }
- int main(int argc, char **argv)
- {
- test();
- return 0;
- }
我們來看輸出的結果:
image-20210115132418481
通過上面的輸出結果可以知道,先輸出的是lisi,后輸出的是 zhangsan,而在實例化對象的時候,是先創建的 per 對象,并初始化為 zhangsan,后創建的 per7 對象,并初始化為 lisi,再調用析構函數的時候順序卻是顛倒過來的。因此,總結一下就是:
per 這個實例化對象是在 test()函數執行完之后,再調用的析構函數,而對于 per7對象來說,是在執行 delete per7這條語句之后調用的析構函數,所以也就有了上述的輸出結果。
另外,引出一點,如果我們在上述的代碼中把delete per7這條語句給注釋掉,那么會怎么樣呢,下圖是去掉該語句之后的結果:
image-20210115133215468
我們看到,上述就只執行了 zhangsan的析構函數,并沒有執行lisi的析構函數,這也告訴我們,在使用 new 創建的實例化對象,必須使用 delete 將其釋放掉,如果沒有使用 delete 來將其釋放,那么在系統退出之后,會自動地釋放掉它地內存,但是這個時候是不會調用它地析構函數的。
最后,關于構造函數和析構函數,如果類里沒有實現任何構造函數和析構函數,那么其系統本身會調用一個默認的構造函數和析構函數。那么,除了默認的構造函數和默認的析構函數,還存在一個默認的拷貝構造函數,接下來,來敘述這個拷貝構造函數。
拷貝構造函數
默認拷貝構造函數
我們直接來看這樣一段代碼:
- #include <iostream>
- #include <string.h>
- #include <unistd.h>
- using namespace std;
- class Person {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person() {//cout <<"Pserson()"<<endl;
- name = NULL;
- work = NULL;
- }
- Person(char *name)
- {
- //cout <<"Pserson(char *)"<<endl;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name, name);
- this->work = NULL;
- }
- Person(char *name, int age, char *work = "none")
- {
- //cout <<"Pserson(char*, int)"<<endl;
- this->age = age;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name, name);
- this->work = new char[strlen(work) + 1];
- strcpy(this->work, work);
- }
- ~Person()
- {
- cout << "~Person()"<<endl;
- if (this->name) {
- cout << "name = "<<name<<endl;
- delete this->name;
- }
- if (this->work) {
- cout << "work = "<<work<<endl;
- delete this->work;
- }
- }
- void printInfo(void)
- {
- //printf("name = %s, age = %d, work = %s\n", name, age, work);
- cout<<"name = "<<name<<", age = "<<age<<", work = "<<work<<endl;
- }
- };
- int main(int argc, char **argv)
- {
- Person per("zhangsan", 18);
- Person per2(per);
- per2.printInfo();
- return 0;
- }
在主函數的第二行代碼中,我們可以看到我們創建了一個實例,并且傳入的參數是 per,但是我們看類里面的代碼實現,并沒有發現有一個構造函數的形參為 Person ,那這個時候,會發生什么函數調用呢,實際上是會調用一個系統的默認構造函數,這個默認的構造函數會進行值拷貝,會將 per中的內容拷貝到 per2中去,下圖是這個過程的一個示意圖:
image-20210117015212259.png
通過上圖可以看到,在執行默認的拷貝構造函數的時候,執行的是值拷貝,那么相應的,per 的 name 也就指向了 address1,per2 的 name 同樣也指向了 adress,從而完成了值拷貝的過程,下面是代碼運行的結果:
image-20210117015527675
可以看到,在輸出 per2 的內容的時候,輸出的是 per 的初始化內容,在主函數運行完之后,就要執行析構函數來釋放使用 new 分配的空間,首先是釋放 per 的內容,然后緊接著是釋放 per2的內容,但是在剛剛的敘述中,使用默認構造函數進行拷貝的時候,使用的是值拷貝,從而造成的效果是 per2 的 name 和 work 指向的地址是 per 中的同一塊地址,這樣,在執行析構函數的時候,同一塊內存空間就會被釋放兩次,從而導致錯誤。因此,使用默認的拷貝構造函數存在一定的問題,也就需要我們自己來定義拷貝構造函數,下面介紹自定義的拷貝構造函數。
自定義拷貝構造函數
我們根據在上述代碼的基礎上,修改得到我們自定義的拷貝構造函數如下:
- #include <iostream>
- #include <string.h>
- #include <unistd.h>
- using namespace std;
- class Person {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person() {//cout <<"Pserson()"<<endl;
- name = NULL;
- work = NULL;
- }
- Person(char *name)
- {
- //cout <<"Pserson(char *)"<<endl;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name, name);
- this->work = NULL;
- }
- Person(char *name, int age, char *work = "none")
- {
- cout <<"Pserson(char*, int)"<<endl;
- this->age = age;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name, name);
- this->work = new char[strlen(work) + 1];
- strcpy(this->work, work);
- }
- Person(Person &per)
- {
- cout <<"Pserson(Person &per)"<<endl;
- this->age = per.age;
- this->name = new char[strlen(per.name) + 1];
- strcpy(this->name, per.name);
- this->work = new char[strlen(per.work) + 1];
- strcpy(this->work, per.work);
- }
- ~Person()
- {
- cout << "~Person()"<<endl;
- if (this->name) {
- cout << "name = "<<name<<endl;
- delete this->name;
- }
- if (this->work) {
- cout << "work = "<<work<<endl;
- delete this->work;
- }
- }
- void printInfo(void)
- {
- //printf("name = %s, age = %d, work = %s\n", name, age, work);
- cout<<"name = "<<name<<", age = "<<age<<", work = "<<work<<endl;
- }
- };
- int main(int argc, char **argv)
- {
- Person per("zhangsan", 18);
- Person per2(per);
- per2.printInfo();
- return 0;
- }
上述中,我們編寫了一個拷貝構造函數,函數的形參是 Person 類的引用,然后我們在主函數中傳入 per 實參,程序執行的結果如下圖所示:
image-20210117234707175
通過圖片代碼的運行結果我們也可以知道,在執行主函數的第二行代碼的時候,調用了默認的拷貝構造函數。
對象的構造順序
在上述代碼的基礎上,比如說我們存在如下幾個實例化對象。
- Person per_g("per_g", 10);
- void func(void)
- {
- Person per_func("per_func",11);
- static Person per_func_s("per_func_s",11);
- }
- int main(int argc,char **argv)
- {
- Person per_main("per_main",11);
- static Person person_main_s("person_main_s",11);
- for (int i = 0; i < 2; i++)
- {
- func();
- Person per_for("per_for",i);
- }
- return 0;
- }
緊接著,我們來看上述代碼的執行結果,結果如下圖所示:
image-20210118000045599
通過上述的結果,我們可以得出:
實例化類的構造順序是按照定義的順序進行構造的,全局的實例化對象會在主函數執行前被構造,然后緊接著構造的是在主函數定義的實例化對象 per_main 和 per_main_s,構造的順序不會因為其實例化對象是 static 而發生改變,緊接著就是函數 func里面的 per_func和 per_func_s。在退出 func的時候,會釋放掉 func中的局部變量,這個時候會調用 per_func的析構函數,但是這時是不會釋放掉 func中的 per_func_s,因為它是 static 的,緊接著會構造 per_for對象,當一個 for循析構函數。環執行完畢之后,就會將剛剛那個構造的 per_for對象釋放掉,也就是會調用析構函數。緊接著,我們繼續調用 func函數,在 func函數里面,會執行 per_fun的構造函數,但是不會執行 per_fun_s的構造函數,因為已經構造過了,在最后,主函數運行完畢之后,以此釋放實例化的空間,首先會釋放掉 per_main,然后釋放 per_main_s,緊接著釋放全局變量的空間per_g。
在類里初始化類對象
在剛剛說到的類里面,我們繼續添加新的代碼,同樣的,我們有如下所示的這樣一個類:
- class
- {
- private:
- Person father;
- Person mother;
- int student_id;
- public:
- Student(int id, char *father, char *mother, int father_age = 49, int mother_age = 39) : mother(mother,mother_age),father(father,father_age)
- {
- cout << "Student(int id, char *father, char *mother, int father_age, int mother_age)" << endl;
- }
- };
- int main(int argc, char **argv)
- {
- Student s(100,"Bill","Lisa")
- return 0;
- }
上述代碼運行就會輸出如下所示的信息:
image-20210119131136755
這樣的操作,就會首先調用的是 father的構造函數,然后,緊接著再調用的是 mother的構造函數,然后,才是調用的 Student的構造函數,在主函數執行完畢之后,執行析構函數的順序又和剛剛的相反。
小結
上述便是關于 C++比較核心的兩個概念,構造函數以及析構函數兩大特性,除了講述了兩大特性的基本概念之外,也敘述了為什么要適用析構函數,以及析構函數調用的位置,同時也敘述了拷貝構造函數的相關內容。在本節的末尾也講述了構造的順序以及析構的順序,最后,給出了一種在類里面初始化類對象的一種方法。
本文轉載自微信公眾號「 wenzi嵌入式軟件」,可以通過以下二維碼關注。轉載本文請聯系 wenzi嵌入式軟件公眾號。