C++類和對象:初始化列表和static成員深度詳解
- 1. 前言
- 2. 構造函數初始化成員變量的方式
- 2.1 構造函數體內賦值
- 2.2 初始化列表
- 2.2.1 初始化列表的注意事項
- 2.3 初始化列表的初始化順序
- 3. 類的靜態成員
- 3.1 引入
- 3.2 靜態成員變量
- 3.3 靜態成員函數
- 3.4 靜態成員的注意事項
- 3.5 靜態成員的另一個實踐場景
- 4. 常見問題解答
- 4. 總結對比
1. 前言
在C++面向對象編程中,構造函數初始化列表和靜態成員是提升代碼質量與安全性的重要特性。本文深度詳解關于C++構造函數的初始化列表
和C++類中的static成員
,其中static
成員包含static成員變量
和static成員函數
,以及介紹static的相關實踐場景。
2. 構造函數初始化成員變量的方式
2.1 構造函數體內賦值
在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值。
class Date{
public://賦值方式初始化Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
我們可以利用以上構造函數,在創建一個對象的時候對該對象進行初始化。
雖然上述構造函數調用之后,對象中已經
有了一個初始值
,但是不能將其稱為對對象中成員變量的初始化
,構造函數體中的語句只能將其稱為賦初值,,而不能稱作初始化。
因為初始化只能初始化一次,而構造函數體內可以多次賦值。
2.2 初始化列表
初始化列表:以一個冒號開始,接著是一個**以逗號分隔的數據成員列表
**, 每個"成員變量"后面跟一個放在括號中的初始值或表達式。
例如:
class Date{
public://初始化列表初始化Date(int year, int month, int day): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
int main(){Date d1(2025, 2, 18);//傳入的參數會通過初始化列表的方式進行初始化。return 0;
}
2.2.1 初始化列表的注意事項
首先回顧一下往期文章提出的對默認構造函數的理解:
C++類和對象進階:構造函數和析構函數詳解
默認構造函數
,以下三種函數都可以被稱作是默認構造函數。
- 無參構造函數。
- 全缺省構造函數。
- 我們沒寫編譯器默認生成的構造函數。
- 總結來說就是,不需要傳參的構造函數,都屬于是默認構造函數。
- 無參構造函數。沒有參數,因此無需傳參。
- 全缺省構造函數。參數全缺省,不需要傳參。
- 我們沒寫編譯器默認生成的構造函數。編譯器生成的,我們無法顯示調用。自動調用時無需傳參。
注意
:
- 要把初始化列表理解成非靜態成員變量創建(占內存)的地方。類中只是成員變量的聲明
- 無論是否顯式指定初始化列表,都會走一遍初始化列表來對成員變量進行初始化。
(靜態成員變量在類外進行初始化
)
- 每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)
- 類中包含以下成員,必須放在初始化列表位置進行初始化:
- 引用成員變量
- const成員變量
- 自定義類型成員(且該類沒有默認構造函數時)
分析這三類變量的特征:
- 引用成員變量:引用成員變量的初始化是“綁定”過程,而非賦值操作。構造函數體內無法完成初始化(
此時成員已定義,只能賦值
),因此必須在初始化列表(引用變量創建時)初始化
- const成員變量:
const
成員變量的初始化是“定義時賦值”,構造函數體內無法修改其值。const
變量在定義后其值不可修改,因此必須在創建時初始化,也就是必須在初始化列表初始化。 - 自定義類型成員(該類沒有默認構造函數時):編譯器只會調用默認構造函數,若類沒有默認構造函數(即無參構造函數或所有參數均有默認值的構造函數或編譯器自己生成的構造函數),也就說程序員自己寫了構造函數且調用時需要顯式傳參,則必須顯式調用其某個構造函數,該工作在初始化列表中進行。
對比以上三類特殊情形:
變量類型 | 核心特征 | 初始化方式 | 未正確初始化的后果 |
---|---|---|---|
引用成員變量 | 必須綁定對象,不可重新綁定 | 初始化列表中綁定 | 編譯錯誤(未初始化引用) |
const成員變量 | 不可修改,需定義時賦值 | 初始化列表中賦值 | 編譯錯誤(未初始化常量) |
自定義類型成員(無默認構造函數) | 必須顯式調用構造函數 | 初始化列表中調用帶參構造函數 | 編譯錯誤(找不到默認構造函數) |
2.3 初始化列表的初始化順序
成員變量
在類中聲明次序就是其在初始化列表中的初始化順序
,與其在初始化列表中的先后次序無關。
我們來看如下代碼:
class MyClass {
public:MyClass(int init_value): _value1(init_value) // 類中的聲明順序決定初始化順序, _value2(_value1) // _value2先聲明,會先用 _value1 初始化 _value2// 此時 _value1 還未完成初始化,是隨機值{}void Print() const {cout << "_value1: " << _value1<< " _value2: " << _value2 << endl;}
private:int _value2; int _value1;
};
int main() {MyClass obj(5);obj.Print(); // 輸出:_value1: 5 _value2: 隨機值return 0;
}
結論:
- 初始化列表中,變量初始化的順序應該和變量在類中聲明的次序保持一致。
- 盡量使用初始化列表對成員變量進行初始化,因為不管程序員是否使用初始化列表,對于自定義類型成員變量,一定會先試用初始化列表初始化。
- 不能在初始化列表中完成的,在函數體內用語句來完成(如開辟空間后對指針的檢查等)
初始化列表總結:
- ?論是否顯式寫初始化列表,
每個構造函數都有初始化列表
- ?論是否在初始化列表顯式初始化成員變量,
每個成員變量都要?初始化列表初始化
;
3. 類的靜態成員
3.1 引入
設計一個類,計算程序中創建了多少個該類的類對象
#include <iostream>
//設計一個程序,統計當前正在使用的某個對象有多少個
int _scount = 0; //我們可以利用全局變量class A {
public:A() { ++_scount; } //構造函數A(const A& t) { ++_scount; } //拷貝構造函數~A() { --_scount; } //析構函數
};
int main() {cout << __LINE__ << ": " << _scount << endl; // 是 1 ,此處還沒進入Func函數,static 對象還沒創建A aa1;Func(); //3Func(); //3return 0;
}
以上程序確實可以實現統計,但全局變量有極大的缺陷:
void Func() {static A aa2; //局部靜態對象,只會創建一次,不在函數棧幀內,在靜態區cout << __LINE__ << ": " << _scount << endl; //3//全局變量的劣勢:任何地方都可以隨意改變,不安全//_scount++;
}
因此,我們想到了利用類來對計數器進行封裝,并將計數器設置成靜態成員變量。
什么是靜態成員?
- 聲明為
static
的類成員稱為類的靜態成員,用static
修飾的成員變量,稱之為靜態成員變量; - 用
static
修飾的成員函數,稱之為靜態成員函數。靜態成員變量一定要在類外進行初始化。
利用靜態成員實現的類:
class A {
public:A(){ ++_scount;}A(const A& t) { ++_scount; }~A() { --_scount; }
private:static int _scount; //此處只是該成員變量的聲明//類內的靜態成員,相當于用類去封裝全局變量
};
//全局位置,類外定義 類的 static 成員,類內聲明,類外初始化 static 成員定義時不受訪問限定符的限制
int A::_scount = 0;
- 需要尤其注意,靜態成員變量,在類內是聲明,需要在類外進行初始化:
int A::_scount = 0;
這是規定的寫法,通過類作用域限定符來訪問。
3.2 靜態成員變量
private:// 非靜態成員變量 ----- 屬于每一個類對象, 存儲在對象里面int _a1 = 1; //成員變量給缺省值,會自動進入初始化列表int _a2 = 2;// 靜態成員變量 ----- 屬于類,類的每個對象共享,存儲在靜態區, 生命周期是全局的,不能用初始化列表初始化static int _scount;
};
需要注意:靜態成員變量和非靜態成員變量存儲的位置不同。
- 靜態成員變量:屬于類內,類的每個對象共享,存儲在靜態區, 生命周期是全局的,程序運行期間持續存在,不能用初始化列表初始化。
- 非靜態成員變量: 屬于每一個類對象, 存儲在對象里面
3.3 靜態成員函數
靜態成員函數一般是和靜態成員變量成對出現的。
我們在類中添加以下函數方便我們獲取_scount的值
:
public:
static int GetACount() {return _scount;
}
靜態成員函數的特點:
- 沒有this指針。
- 指定類域和訪問限定符就可以訪問。
- 可以直接訪問類內的靜態成員變量
通過指定類域和訪問限定符訪問靜態成員函數。
int main(){//由于靜態成員變量是私有的//可以通過靜態成員函數來訪問靜態成員變量cout << A::GetACount() << endl;return 0;
}
因此我們可以得出:
- 靜態成員函數,不能訪問類內的非靜態成員變量,因為沒有this指針(
沒有傳入調用對象的地址
) 靜態成員函數不能調用非靜態成員函數
,非靜態成員函數的調用需要傳遞this指針,但static
成員函數沒有this指針
3.4 靜態成員的注意事項
- 靜態成員為
所有類對象所共享
,不屬于某個具體的對象,存放在靜態區
靜態成員變量必須在類外定義,定義時不添加static關鍵字
,類中只是聲明。類內聲明,類外初始化。- 類靜態成員(成員變量和成員函數)可用
類名::靜態成員
或者對象.靜態成員
來訪問 - 靜態成員函數
沒有隱藏的this指針,不能用const修飾
,不能訪問任何非靜態成員 - 靜態成員也是類的成員,受
public、protected、private
訪問限定符的限制 - 靜態成員變量:不能是auto推導類型。
- 核心特點:
- 靜態成員函數無this指針,無法訪問非靜態成員
- 靜態成員函數可直接調用無需實例化
3.5 靜態成員的另一個實踐場景
靜態成員函數的妙用
設計一個類,在類外只能在 棧 上創建對象
。
設計一個類,在類外只能在 堆 上創建對象
。
我們可以這樣做
class Obj {
public://通過類作用域限定符來調用函數獲取對象static Obj GetStackObj() {Obj obj;return obj;}static Obj* GetHeapObj() {Obj* obj = new Obj;return obj;}//構造函數私有化,防止直接調用構造函數在棧或堆上創建對象。
private:Obj(){}
private:int _a1 = 1;int _a2 = 2;
};
int main() {//這三種方式都會調用構造函數,我們將構造函數私有化后,就無法再類外創建 堆/棧 上的對象了/*static OBj o1;OBj o2;Obj* o3 = new Obj;*///提供對外的接口//無需創建對象,通過類作用域限定符來調用靜態成員函數。Obj obj_1 = Obj::GetStackObj();Obj* p_obj = Obj::GetHeapObj();return 0;
}
- 實現原理:
- 私有化構造函數
- 通過靜態工廠方法控制對象創建
4. 常見問題解答
Q1:為什么靜態成員變量必須類外初始化?
- 靜態成員不屬于單個對象,類內聲明僅表示存在性,需在程序全局空間進行內存分配
Q2:靜態成員函數能否調用非靜態成員函數?
- 不能。非靜態成員函數隱含this指針參數,而靜態函數無this指針
Q3:如何選擇初始化列表與構造函數體?
- 優先使用初始化列表,特別是對于const/引用成員/無默認構造函數的自定義類型等必須初始化的場景
4. 總結對比
特性 | 初始化列表 | 靜態成員 |
---|---|---|
作用對象 | 對象成員初始化 | 類級別共享數據/操作 |
關鍵優勢 | 處理特殊類型成員初始化 | 減少全局變量使用 |
典型應用場景 | const/引用成員初始化 | 計數器、工具類函數 |
內存管理 | 對象內存空間 | 靜態存儲區 |
以上就是本文的所有內容了,碼字整理不易,歡迎各位大佬在評論區留言交流。