3. C++類的成員變數和成員函式
類的成員變數和普通的變數一樣,從格式上基本沒多區別。
唯一需要注意是他們的責任是不同的,成員變數是對物件負責的,在類中,使用範圍由類決定,而普通變數則沒有這個說法。
類的成員函式也和普通函式一樣,都有返回值和形參。
它與普通函式的區別是:成員函式是一個類的成員,出現在類中,它的作用範圍由類來決定;而普通函式是獨立的,作用範圍是全域性的,或位於某個名稱空間內。
這裡不同變數和不同的函式我們後面會做一個系統的解釋和分析。
下面還是之前的例程,成員函式類內宣告,類外定義。
ps:早期版本的C++成員變數宣告時不可以對其初始化,後期C++ 11,可以進行成員變數初始化賦值。
#include <iostream> using namespace std; //類通常定義在函式外面 class Person{ public: //類包含的變數 char *name; int age; //類內宣告 void say(); }; //類外定義 void Person::say(){ cout << name << "的年齡是" << age << endl; } int main(){ //建立物件 Person p; p.name = "豆豆"; p.age = 16; p.say(); return 0; } void Person::say()
::被稱為作用域運算子或作用域限定符,用來連線類名和函式名,指明當前函式屬於哪個類,成員函式在類外定義時必須使用作用域限定符。
注意:在引入了類的型別後,我們再引數和返回值會有更多的選擇
#include <iostream> using namespace std; class Car{ public: string name; string color; int wheel; public: void run(); }; void Car::run() { cout << color << "的" << name << "在跑..." << endl; } class CarFatory{ public: string name; string address; string tel; public: Car* repair(Car *c); }; Car* CarFatory::repair(Car *c) { if(c->wheel < 4){ c->wheel = 4; cout << c->name << "車,修好了" << endl; } return c; } int main(){ Car *c = new Car(); c->name = "保時捷"; c->color = "紅色"; c->run();//車在跑 c->wheel = 3;//跑著跑著車輪子掉了了,壞了 CarFatory *f = new CarFatory(); Car *newCar = f->repair(c);//修車 cout << "車有" << newCar->wheel << "個輪子" << endl; return 0; }
在一個類中成員中變數和函式可以分為多種形態
我們先看下成員變數的部分
在C中我們經常遇到這幾個混亂的變數
區域性變數:在一個函式內部定義的變數(包括函式形參)是區域性變數,儲存在棧記憶體,在函式結束後自動銷燬。
全域性變數:在函式體外定義的變數,可以為本原始檔中其它函式所公用,有效範圍為從定義變數的位置開始到本原始檔結束,這種型別的變數就稱為“全域性變數”。全域性變數儲存在靜態儲存區域(靜態記憶體)。
ps:全域性變數可以被同一工程專案中其他檔案用extern聲明後呼叫,對其每次進行修改都會被儲存。
靜態變數又分為:靜態全域性變數和靜態區域性變數
靜態全域性變數:在原先的全域性變數前面加上了static進行修飾。儲存在靜態儲存區。跟全域性變數最大的不同在於,靜態全域性變數不能被其他原始檔使用,只能被本原始檔使用,對其每次進行修改都會被儲存。
靜態區域性變數:在原先的區域性變數前面加上了static進行修飾。儲存在靜態儲存區內,等到整個程式結束才會被銷燬,但是它的作用域依然在函式體內部。
ps:靜態區域性變數一般實際中沒有太大作用,所以這裡我們瞭解下就可以。
需要重點關注的幾個部分:
1、靜態成員變數
class Person{ public: void show(); public: static int height; //靜態成員變數 private: char *name; int age; }; 靜態成員變數屬於類,不屬於某個具體的物件,即使建立多個物件,也只為height分配一份記憶體,所有物件使用的都是這份記憶體中的資料。 當某個物件修改了height,也會影響到其他物件。 注意: 1、靜態成員變數必須在類宣告的外部初始化,而且只能在類體外進行。
int Person::height = 0;
初始化時可以賦初值,也可以不賦值。如果不賦值,那麼會被預設初始化為 0。 全域性資料區的變數都有預設的初始值 0,而動態資料區(堆區、棧區)變數的預設值是不確定的,一般認為是垃圾值。 2、靜態成員變數的記憶體既不是在宣告類時分配,也不是在建立物件時分配,而是在類外初始化時分配。 3、一個類中可以有一個或多個靜態成員變數,所有的物件都共享這些靜態成員變數。 4、靜態成員變數和普通靜態變數一樣,都在記憶體分割槽中的全域性資料區分配記憶體,程式結束時才釋放。 5、靜態成員變數不隨物件的建立而分配記憶體,也不隨物件的銷燬而釋放記憶體。而普通成員變數在物件建立時分配記憶體,在物件銷燬時釋放記憶體。 6、靜態成員變數既可以通過物件名訪問,也可以通過類名訪問。 關於靜態成員變數的訪問方式:
//通過類類訪問 static 成員變數
Person::height= 180;
//通過物件來訪問 static 成員變數
Person p;
p.height= 20;
//通過物件指標來訪問 static 成員變數
Person *p = new Student();
p->height= 190;
----- 2、靜態成員函式 C++中成員函式也是可以宣告為靜態成員函式的,靜態成員函式只能訪問靜態成員。 編譯器在編譯一個普通成員函式時,會隱式地增加一個形參 this,並把當前物件的地址賦值給 this,所以普通成員函式只能在建立物件後通過物件來呼叫,因為它需要當前物件的地址。而靜態成員函式可以通過類來直接呼叫,編譯器不會為它增加形參 this,它不需要當前物件的地址,所以不管有沒有建立物件,都可以呼叫靜態成員函式。 靜態成員函式沒有 this 指標,無法在函式體內部訪問某個物件,所以不能呼叫普通成員函式,只能呼叫靜態成員函式。 靜態成員函式與普通成員函式的根本區別在於:普通成員函式有 this 指標,可以訪問類中的任意成員;而靜態成員函式沒有 this 指標,只能訪問靜態成員(包括靜態成員變數和靜態成員函式)。
include <iostream>
using namespace std;
class Person{
public:
void show();
public: //宣告靜態成員函式
static int getAge();
static double getSalary();
private:
static int age;
static double salary;
private:
char *name;
};
int Person::age = 20;
double Person::salary = 5000.0;
void Person::show(){
cout<< name <<"的年齡是"<< age <<",工資是"<< salary <<endl;
}
//定義靜態成員函式
int Person::getAge(){
return age;
}
double Person::getSalary(){
return salary;
}
int main(){
int age = Person::getAge();
float salary = Person::getSalary();
cout<<"年齡"<< age <<"的員工工資是"<< salary <<endl;
return 0;
}
----- 3、空類的預設成員函式 關於C++成員函式這是我們比較關注的 讓我們看一下空類中都有什麼樣的成員函式,編譯器會為空類提供哪些預設成員函式?分別有什麼樣的功能呢? 空類,宣告時編譯器不會生成任何成員函式,對於空類,編譯器不會生成任何的成員函式,只會生成1個位元組的佔位符。(在Linux下,是4個位元組) C++空類編譯器自動生成的6個成員函式: 一個預設的建構函式 一個拷貝建構函式 一個解構函式 一個賦值運算子 兩個取址運算子。
class Empty
{
public:
Empty(); //預設建構函式
Empty(const Empty &rhs); //拷貝建構函式
~Empty(); //解構函式
Empty& operator=(const Empty &rhs); //賦值運算子
Empty* operator&(); //取址運算子
const Empty* operator&() const; //取址運算子(const版本)
};
使用時的呼叫情況:
Empty *e = new Empty(); //預設建構函式
delete e; //解構函式
Empty e1; //預設建構函式
Empty e2(e1); //拷貝建構函式
e2 = e1; //賦值運算子
Empty *pe1 = &e1; //取址運算子(非const)
const Empty *pe2 = &e2; //取址運算子(const)
C++編譯器對這些函式的實現:
inline Empty::Empty() //預設建構函式
{
}
inline Empty::~Empty() //解構函式
{
}
inline Empty *Empty::operator&() //取址運算子(非const)
{
return this;
}
inline const Empty *Empty::operator&() const //取址運算子(const)
{
return this;
}
inline Empty::Empty(const Empty &rhs) //拷貝建構函式
{
//對類的非靜態資料成員進行以"成員為單位"逐一拷貝構造
//固定型別的物件拷貝構造是從源物件到目標物件的"逐位"拷貝
}
inline Empty& Empty::operator=(const Empty &rhs) //賦值運算子
{
//對類的非靜態資料成員進行以"成員為單位"逐一賦值
//固定型別的物件賦值是從源物件到目標物件的"逐位"賦值。
}
m是類C中的一個型別為T的非靜態成員變數,若C沒有宣告拷貝建構函式(賦值運算子), m將會通過T的拷貝建構函式(賦值運算子)被拷貝構造(賦值);該規則遞迴應用到m的資料成員,直到找到一個拷貝建構函式(賦值運算子)或固定型別(例如:int、double、指標等)為止。 這些函式我們後續會依次進行講解 ---- 4、建構函式 建構函式(Constructor): 它的名字和類名相同,沒有返回值,不需要使用者顯式呼叫(使用者也不能呼叫),而是在建立物件時自動執行。 格式: 宣告:
類名(引數列表);
類外定義:
類名 :: 類名(引數列表) : 建構函式的初始化列表{ 函式體 }
通過建構函式可以在建立物件的同時,對物件的成員變數(屬性)進行初始化,這樣就簡化了建立物件後再賦值屬性值的過程。
include <iostream>
using namespace std;
class Student{
private:
char *name;
int age;
float score;
public:
//宣告建構函式
Student(char *name, int age, float score);
//宣告普通成員函式
void show();
};
//定義建構函式
Student::Student(char *name, int age, float score){
this->name = name;
this->age = age;
this->score = score;
}
//定義普通成員函式
void Student::show(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
int main(){
//建立物件時向建構函式傳參
Student stu("豆豆", 20, 93.0);
stu.show();
//建立物件時向建構函式傳參
Student *pstu = new Student("哈哈", 21, 96.0);
pstu->show();
return 0;
}
 建構函式的一項重要功能是對成員變數進行初始化,為了達到這個目的,可以在建構函式的函式體中對成員變數一一賦值,還可以採用初始化列表,從而使程式碼更加簡潔。
include <iostream>
using namespace std;
class Student{
private:
char *name;
int age;
float score;
public:
Student(char *name, int age, float score);
void show();
};
//採用初始化列表
Student::Student(char *name, int age, float score): name(name), age(age), score(score){
//TODO:
}
void Student::show(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
int main(){
Student stu("豆豆", 20, 93.0);
stu.show();
Student *pstu = new Student("哈哈", 21, 96.0);
pstu->show();
return 0;
}
注意:
1、建構函式必須是 public 屬性的,否則建立物件時無法呼叫。
2、建構函式沒有返回值。
3、函式體中不能有 return 語句。
4、使用建構函式初始化列表並沒有效率上的優勢,僅僅是書寫方便。
5、初始化列表可以用於全部成員變數,也可以只用於部分成員變數。
6、成員變數的初始化順序與初始化列表中列出的變數的順序無關,它只與成員變數在類中宣告的順序有關。
#include <iostream> using namespace std; class Demo{ private: int a; int b; public: Demo(int b1); void show(); }; Demo::Demo(int b1): b(b1), a(b){ } void Demo::show(){ cout<< a <<", "<< b <<endl; } int main(){ Demo obj(100); obj.show(); return 0; }

image.png
上面程式初始化列表等價於
Demo::Demo(int b1): m_b(b1), m_a(b){ a = b; b = b1; }
給 a 賦值時,b 還未被初始化,它的值是不確定的,所以輸出的 a 的值是一個奇怪的數字;
obj 在棧上分配記憶體,成員變數的初始值是不確定的。
好像感覺初始化列表除了簡潔沒有其他作用,實則不然,初始化 const 成員變數的唯一方法就是使用初始化列表。
class Array{ private: const int len; int *arr; public: Array(int len); }; //必須使用初始化列表來初始化 len Array::Array(int len): len(len){ arr = new int[len]; }
預設建構函式
如果使用者自己沒有定義建構函式,那麼編譯器會自動生成一個預設的建構函式,只是這個建構函式的函式體是空的,也沒有形參,也不執行任何操作。
Student(){}
一個類必須有建構函式,要麼使用者自己定義,要麼編譯器自動生成。
一旦使用者自己定義了建構函式,不管有幾個,也不管形參如何,編譯器都不再自動生成。
注意:最後需要注意的一點是,呼叫沒有引數的建構函式也可以省略括號。
在棧上建立物件可以寫作Student stu()或Student stu
在堆上建立物件可以寫作Student *pstu = new Student()或Student *pstu = new Student
它們一樣都會呼叫建構函式 Student()。
5、建構函式的過載
說到建構函式過載,我們就需要說一下過載的概念了。
函式過載是一種特殊情況,C++允許在同一作用域中宣告幾個類似的同名函式,這些同名函式的形參列表(引數個數,型別,順序)必須不同,
常用來處理實現功能類似資料型別不同的問題。
//全域性的函式過載 int get(); int get(int a); int get(float a); int get(int a, int b); class Calculate{ private: int a; int b; public: //建構函式過載 Calculate(); Calculate(int a); Calculate(int a, int b); //成員函式過載 void sum(); //int sum();//不是過載,與返回值無關 void sum(int a, int b); void sum(int a, int b, int c); void sum(double a, double b); }; void Calculate::sum(){} //int Calculate::sum(){} void Calculate::sum(int a, int b){} void Calculate::sum(int a, int b, int c){} void Calculate::sum(double a, double b){}
後續我們還會繼續討論過載。
6、解構函式
解構函式也是一種特殊的成員函式,沒有返回值,不需要程式設計師顯式呼叫,而是在銷燬物件時自動執行。
建構函式的名字和類名相同,而解構函式的名字是在類名前面加一個~符號。
注意:
1、解構函式沒有引數,不能被過載
2、一個類只能有一個解構函式。
3、如果使用者沒有定義解構函式,編譯器會自動生成一個預設的解構函式。
#include <iostream> using namespace std; /* 封裝一個數組類來看delete的作用 */ class Array{ public: Array(int len); //建構函式 ~Array(); //解構函式 public: void input(); //輸入陣列元素函式 void out(); //顯示陣列元素函式 private: int* getElement(int i); //獲取第i個元素的指標 private: const int len; //陣列的長度 int *arr; //陣列指標 int *p; //指向陣列元素的指標 }; Array::Array(int len): len(len){ //使用初始化列表來給len賦值 if(len > 0){ arr = new int[len]; //動態記憶體申請一個塊用於陣列的記憶體 } else{ arr = NULL; } } Array::~Array(){ delete[] arr; //釋放記憶體 } void Array::input(){ for(int i = 0; p = getElement(i); i++){ cin>>*getElement(i); } } void Array::out(){ for(int i = 0; p = getElement(i); i++){ if(i == len - 1){ cout<<*getElement(i)<<endl; } else{ cout<<*getElement(i)<<", "; } } } int * Array::getElement(int i){ if(!arr || i < 0 || i >= len){ return NULL; } else{ return arr + i; } } int main(){ int n; cout<<"輸入陣列的長度: "; cin>>n; Array *parr = new Array(n); //建立一個有n個元素的陣列物件 //輸入陣列元素 cout<<"請輸入 "<<n<<" 個元素: "; parr->input(); //輸出陣列元素 cout<<"陣列內元素是: "; parr->out(); //刪除陣列(物件) delete parr; return 0; }

image.png
注意:
1、new 分配記憶體時會呼叫建構函式。
2、delete 釋放記憶體時會呼叫解構函式。
3、建構函式和解構函式對於類來說是不可或缺的。
解構函式的呼叫時機
解構函式在物件被銷燬時呼叫,而物件的銷燬時機與它所在的記憶體區域有關。
在所有函式之外建立的物件是全域性物件,它和全域性變數類似,位於記憶體分割槽中的全域性資料區,程式在結束執行時會呼叫這些物件的解構函式。
在函式內部建立的物件是區域性物件,它和區域性變數類似,位於棧區,函式執行結束時會呼叫這些物件的解構函式。
new 建立的物件位於堆區,通過 delete 刪除時才會呼叫解構函式;如果沒有 delete,解構函式就不會被執行。
#include <iostream> #include <string> using namespace std; class Test{ public: Test(string s); ~Test(); private: string s; }; Test::Test(string s): s(s){ cout<<this->s<<"建構函式呼叫"<<endl; } Test::~Test(){ cout<<s<<"解構函式呼叫"<<endl; } void function(){ //區域性物件 Test obj1("物件1"); } //全域性物件 Test obj2("物件2"); int main(){ function(); //區域性物件 Test obj3("物件3"); //new建立的物件 Test *pobj4 = new Test("物件4"); return 0; }

image.png
7、拷貝建構函式(複製建構函式)
拷貝建構函式是一種特殊的建構函式,具有單個形參,該形參(常用const修飾)是對該類型別的引用。
當定義一個新物件並用一個同類型的物件對它進行初始化時,將顯示使用複製建構函式。
當該型別的物件傳遞給函式或從函式返回該型別的物件時,將隱式呼叫複製建構函式。
C++支援兩種初始化形式:
複製初始化int a = 5; 直接初始化int a(5);
對於其他型別沒有什麼區別,對於類型別直接初始化直接呼叫實參匹配的建構函式,複製初始化總是呼叫複製建構函式,也就是說:
A x(2); //直接初始化,呼叫建構函式 A y = x; //複製初始化,呼叫複製建構函式
必須定義複製建構函式的情況:
只包含類型別成員或內建型別(但不是指標型別)成員的類,無須顯式地定義複製建構函式也可以複製;
有的類有一個數據成員是指標,或者是有成員表示在建構函式中分配的其他資源,這兩種情況下都必須定義複製建構函式。
什麼情況使用複製建構函式:
類的物件需要拷貝時,拷貝建構函式將會被呼叫。以下情況都會呼叫拷貝建構函式:
(1)一個物件以值傳遞的方式傳入函式體
(2)一個物件以值傳遞的方式從函式返回
(3)一個物件需要通過另外一個物件進行初始化。
深拷貝和淺拷貝:
淺拷貝,指的是在物件複製時,只對物件中的資料成員進行簡單的賦值,預設拷貝建構函式執行的也是淺拷貝。
在“深拷貝”的情況下,對於物件中動態成員,就不能僅僅簡單地賦值了,而應該重新動態分配空間
深拷貝:如果一個類擁有資源,當這個類的物件發生複製過程的時候,資源重新分配,這個過程就是重新動態分配空間
如果沒有自定義拷貝建構函式,則系統會建立預設的拷貝建構函式,但系統建立的預設複製建構函式只會執行“淺拷貝”,即將被拷貝物件的資料成員的值一一賦值給新建立的物件;
若該類的資料成員中有指標成員,則會使得新的物件的指標所指向的地址與被拷貝物件的指標所指向的地址相同,delete該指標時則會導致兩次重複delete而出錯。
下面是示例:
#include <iostream> #include <string.h> using namespace std; class Person { public : // 建構函式 Person(char * pN); // 系統建立的預設複製建構函式,只做位模式拷貝 Person(Person & p); ~Person(); private : char* pName; }; Person::Person(char * pN){ cout << "建構函式被呼叫"<<endl; pName = new char[strlen(pN) + 1]; //在堆中開闢一個記憶體塊存放pN所指的字串 if(pName != NULL) { //如果pName不是空指標,則把形參指標pN所指的字串複製給它 strcpy(pName ,pN); } } Person::Person(Person &p){ //使兩個字串指標指向同一地址位置 pName = p.pName; } Person::~Person(){ delete pName; cout << "解構函式被呼叫"<<endl; } int main( ) { /*p1和p2的指標都指向了同一個地址 函式結束析構時 同一個地址被delete兩次 */ Person p1("豆豆"); Person p2(p1); return 0; }

image.png
// 下面自己設計複製建構函式,實現“深拷貝”,即不讓指標指向同一地址,而是重新申請一塊記憶體給新的物件的指標資料成員 Person::Person(Person & p) { // 用運算子new為新物件的指標資料成員分配空間 pName = new char[strlen(p.pName)+ 1]; if(pName) { // 複製內容 strcpy(pName ,p.pName); } // 則新建立的物件的pName與原物件chs的pName不再指向同一地址了 }