-
類的基本思想是數據抽象
data abstraction
和封裝encapsulation
。數據抽象是一種依賴于接口interface
和實現implementation
分離的編程技術 -
在類中,由類的設計者負責考慮類的實現過程,使用該類的程序員只需要抽象地思考類型做了什么,而無須了解類型的工作細節
-
執行加法和IO的函數應該定義成普通函數,執行復合賦值運算的函數應該是成員函數
-
成員函數的聲明必須在類的內部,它的定義則既可以在類的內部也可以在類的外部
-
成員函數通過一個名為
this
的額外的隱式參數來訪問調用它的對象。當我們調用一個成員函數時,用請求該函數的對象地址初始化該常量指針this
。因此我們不能自定義名為this
的參數或者變量 -
緊隨參數列表之后的
const
關鍵字修改隱式this
指針為指向類類型常量版本的常量指針(默認不是指向常量的)。這意味著如果一個成員函數是普通函數(函數參數列表后沒有const
),則一個常量類對象無法調用該函數(無法給this
指針初始化) -
如果成員函數內部對對象的其他成員沒有修改,則盡可能將成員函數聲明為常量成員函數,緊跟在函數參數列表后面的
const
表示this
是一個指向常量的常量指針。將函數聲明為常量成員函數有利于提高函數的靈活性,令常量對象仍然可以調用該函數 -
常量對象,以及常量對象的引用或者指針都只能調用常量成員函數
-
類本身就是一個作用域,編譯器分兩部處理:首先編譯成員的聲明,然后才輪到成員函數體。成員函數體可以隨意使用類中的成員而無須在意這些成員出現的次序
-
類外部定義的成員的名字必須包含它所屬的類名,而且返回類型、參數列表和函數名都得與類內部的聲明保持一致
double Sales_data::avg_price() const {}
-
當我們定義函數類似于某個內置運算符時,應該令函數的行為盡量模仿這個運算符
-
如果函數在概念上屬于類但是不定義在類中,則它一般應該與類聲明在同一個頭文件中
-
默認情況下,拷貝類的對象其實是拷貝對象的數據成員
-
構造函數沒有返回類型,構造函數不能被聲明為
const
,當我們創建類的一個const
對象時,直到構造函數完成初始化過程對象才真正取得常量屬性 -
如果我們的類沒有顯式地定義構造函數,則編譯器會為我們隱式定義一個默認構造函數。默認構造函數沒有任何實際參數。這個編譯器創建的構造函數又稱作合成的默認構造函數
- 如果存在類內的初始值,用它來初始化成員
- 否則,默認初始化該成員
-
不能依賴默認構造函數:
- 一旦我們定義了構造函數(不論是否有參),除非我們自己定義一個默認構造函數(無參),否則類將沒有默認構造函數。只有當類沒有聲明任何構造函數的時候編譯器才會自動生成默認構造函數
- 如果定義在塊中的內置類型或復合類型(數組和指針)的對象被默認初始化,則其值是未定義的。除非他們都有類內初始值
- 如果類中包含一個其他類,而且這個類沒有默認構造函數,那么編譯器將無法初始化該成員
-
如果函數定義在類內部,則函數默認是內聯的,如果在外部,則默認是不內聯的
-
在C++11新標準中,我們可以通過在參數列表后面寫上
=default
來要求編譯器生成構造函數(在類外仍然可以) -
在構造函數中可以使用構造函數初始值列表對部分成員進行初始化,在初始值列表初始化后用類內初始化對成員進行初始化,剩下的執行默認初始化,然后再執行構造函數函數體中的內容
-
如果我們不定義拷貝、賦值和析構操作,則編譯器會會替我們合成。一般來說,編譯器生成的版本將對對象中的每個成員執行拷貝、賦值和銷毀操作
- 拷貝:初始化變量、以值的方式傳遞或返回一個對象
- 賦值:使用賦值運算符
- 銷毀:當對象不再存在時銷毀
-
每個訪問說明符指定了接下來成員的訪問級別,其有效范圍直到出現下一個訪問說明符或者到達類的結尾為止
- 定義在
public
說明符之后的成員在整個程序內可以被訪問,定義類的接口 - 定義在
private
說明符之后的成員可以被類的成員函數訪問,但是不能被使用該類的代碼訪問
- 定義在
-
class
和struct
的唯一區別:默認訪問權限不同,struct
的默認訪問權限是public
,class
的默認訪問權限是private
-
類可以允許其他類或者函數訪問它的非公有成員,方法是令其他類或者函數成為它的友元。如果類想把一個函數作為它的友元,只需要增加一條以
friend
關鍵字開始的函數聲明語句即可。友元聲明只能出現在類定義的內部,但是具體位置不限。一般來說,最好在類定義開始或結束前的位置集中聲明友元 -
友元的聲明僅僅指定了訪問的權限,而非一個通常意義上的函數聲明,我們必須在友元聲明之外再專門對函數進行一次聲明(一些編譯器允許不再次聲明,不過為了能夠讓所有的編譯器成功運行,最好還是加上)。為了使友元對類的用戶可見,我們通常把友元的聲明與類本身放置在同一個頭文件中。
-
除了定義數據和函數成員外,類還可以定義某種類型在類中的別名,由類定義的類型名字和其他成員一樣存在訪問限制
class Screen { public:typedef std::string::size_type pos;//using pos = std::string::size; }
用來定義類型的成員必須先定義后使用,這一點與普通成員有所區別。因此類型成員通常出現在類開始的地方
-
如果我們希望無論如何都能夠修改某個類的數據成員,即使該對象是
const
,我們可以在變量的聲明中加入mutable
關鍵字,稱作可變數據成員。可以推測,一個可變數據成員無論如何都不是const
的。 -
當我們提供類內初始值時,必須以符號
=
或者花括號表示,不能用圓括號 -
如果想要在成員函數中返回對象本身,則返回類型應該為引用,返回值為
*this
-
一個
const
成員函數如果以引用的形式返回*this
,則應該返回一個常量引用 -
我們可以通過成員函數是否是
const
對函數進行重載,其原因如同函數的指針參數是否指向const
可以進行重載一樣。如果一個函數既有const
版本,又有非const
版本,則對于常量對象僅僅可以調用const
版本,對于非常量對象非const
版本顯然是一個更好的匹配 -
在類內使用一些小函數不會增加運行時的額外開銷,相反還會給開發帶來很多好處
class Screen {public:Screen &display(std::ostream &os) {do_display(os); return *this;}const Screen &display(std::ostream &os) const {do_display(os); return *this;}void do_display(std::ostream &os) const {os << contents;} }
-
每個類定義了唯一的類型,對于兩個類來說,即使他們的成語完全一樣,這兩個類也是完全不同的類型
-
在C語言中要求類類型定義對象時加上
class
或struct
,但是C++中不必要 -
我們也可以僅僅聲明類而不定義它,這種聲明有時被稱作前向聲明,在定義之前是一個不完全類型:可以定義指向這種類型的指針或者引用,也可以聲明(不能定義)以不完全類型作為參數或者返回類型的函數。因此,一個類的名字出現過以后,就被認為是聲明過了,允許包含指向它自身類型的引用或者指針
-
如果一個類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。友元關系不具有傳遞性
-
需要注意的是,友元的聲明僅僅影響的是該函數的訪問權限,因此并不能看作聲明,為了完成對該友元的調用,必須在其作用域內對友元進行聲明。友元的作用域和類的聲明是同一級別的
struct X {friend void f() {} //友元函數可以定義在類的內部,但是此時在類X的作用域中是不可見的X() { f(); } //錯誤,f()不可見void g();void h(); } void X::g() { f(); } //錯誤,f()不可見 void f(); //對f進行聲明,此時對X可見 void X::h() { f(); } //正確
需要注意的是,有的編譯器對上面沒有這種限制
-
定義在外部的成員函數的返回類型不在類的作用域內
-
類的定義分兩步處理:
- 順序編譯成員的聲明
- 指導類全部可見后編譯函數體
-
一般來說,內層作用域可以重新定義外層作用域名字,即使該名字已經在內層作用域中使用過,而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表某一種類型,則類不能重新定義該名字(一些編譯器忽略這種錯誤)
-
類型名的定義通常出現在類的開始處,這樣保證所有使用該類型的成員都出現在類型之后
-
盡管全局變量有可能被覆蓋掉,但是我們可以使用
::name
來獲取對應的全局變量 -
當成員定義在類的外部時,名字查找的最后一步不僅要考慮類定義之前的全局作用域中的聲明,還要考慮在成員函數定義之前的全局作用域中的聲明
-
如果沒有在構造函數的初始值列表中顯式地初始化成員,而且該成員沒有類內初始值,則該成員將在構造函數體之前執行默認初始化。有時候有的變量無法進行默認初始化(如一些沒有默認構造函數的類對象,以及引用或者常量)
-
在構造函數初始值中每個成員只能出現一次。構造函數初始值列表只用于說明初始化成員的值,而不限定初始化具體執行順序,成員的初始化順序與他們在類定義中的出現順序一致,構造函數初始值列表中初始值的前后位置不會影響實際的初始化順序。如果使用某些成員初始化其他成員,則初始化的順序就比較重要,不過這種做法不是一個好的編程習慣
-
如果一個構造函數為所有參數都提供了默認實參,則它實際上也定義了默認構造函數
-
我們可以使用委托構造函數,委托其他構造函數進行構造
class X {X(int _a, int _b, int _c):a(_a),b(_b),c(_c){}X():X(0, 0, 0){}X(int _a):X(_a, 0, 0){}X{int _a, int _b):X(_a, _b, 0){} };
委托構造函數會先執行被委托構造函數的初始化列表,然后再執行函數體
-
當對象被默認初始化或值初始化時自動執行默認構造函數
-
默認初始化在以下情況下發生:
- 當我們在塊作用域內不適用任何初始值定義的一個非靜態變量或者數組時
- 當一個類本身含有類類型的成員且使用合成的默認構造函數時
- 當類類型的成員沒有在構造函數初始值列表中顯式地初始化時
-
值初始化在以下情況下發生:
- 在數組初始化的過程中如果我們提供的初始值的數量小于數組的大小時
- 當我們不使用初始值定義一個局部靜態變量時
- 當我們通過書寫形如
t()
的表達式顯式地請求值初始化時
-
在實際中,如果提供了其他構造函數,最好也提供一個默認的構造函數
-
如果想定義一個使用默認構造函數進行初始化的對象,正確的方法是去掉對象名后的空的括號對
sales_data obj(); //聲明了一個函數 sales_data obj1; //聲明了一個對象,使用默認構造函數進行初始化
-
能通過一個實參調用的構造函數定義了一條從構造函數參數類型向類類型隱式轉換的規則,但是這種轉換只允許一步
-
我們可以通過將構造函數聲明為
explicit
阻止隱式轉換,只對一個實參的構造函數有效。只能在類內聲明構造函數時使用explicit
關鍵字,在類外定義時不應重復。explicit
構造函數只能用于直接初始化 -
我們可以使用
explicit
構造函數進行強制類型轉換item.combine(static_cast<sales_data>(cin));
const char * -> string
的構造函數不是explicit
的- 接收一個容量參數的
vector
構造函數是explicit
的 - 需要注意的是**
explicit
阻止的隱式轉換是構造函數參數類型轉換為類類型的轉換**,而不是構造函數參數之間的轉換 - 允許臨時量存在的地方才允許隱式類型轉換
- 聚合類使得用戶可以直接訪問其成員,因此具有特殊的初始化語法形式,當一個類滿足如下條件時,我們說它是聚合的
- 所有成員都是
public
的 - 沒有定義任何構造函數
- 沒有類內初始值
- 沒有基類,也沒有
virtual
函數
例如:
我們可以提供一個花括號括起來的成員初始值列表用來初始化,當然順序要和聲明順序一致。struct Data {int i; string s; }; Data v1 = {0, "1"};
- 所有成員都是
-
數據成員那都是字面值類型的聚合類是字面值常量類。或者滿足以下要求:
- 數據成員必須是字面值類型
- 類必須至少含有一個
constexpr
構造函數 - 如果某個成員有類內初始值,內置類型必須是一條常量表達式,類類型必須使用自己的
constexpr
構造函數 - 類必須使用析構函數的默認定義
-
我們可以通過在成員的聲明之前加上關鍵字
static
使得其與類關聯在一起。類的靜態成員存在于任何對象之外,對象中不包含任何與靜態數據成員有關的數據。因為靜態函數成員中沒有this
指針),靜態成員啊還是那話不能聲明成const
,也不能調用非靜態成員 -
可以使用作用域運算符直接訪問靜態成員。雖然靜態成員不屬于某個對象,但是我們還是可以使用類的對象、引用或者指針訪問靜態成員。成員函數不通過作用域運算符就能直接訪問靜態成員
-
靜態成員的
static
關鍵字只能在類內部出現(同explicit
),不能在類外重復使用 -
靜態成員不能由類的構造函數初始化,靜態數據成員定義在任何函數之外,因此一旦被定義就存在于程序的整個生命周期中。一般來說,我們不能在類的內部初始化靜態成員,相反的,必須在類的外部定義和初始化每個靜態成員 。
-
通常情況下,類的靜態成員不應該在類的內部初始化。然而我們可以為靜態成員提供
const
整數類型的類內初始值,不過要求靜態成員必須是字面值常量類型的constexpr
。這樣的靜態常量可以用在所有適合用于常量表達式的地方。即使該成員在類內已經初始化,還是應該在類外定義一下該成員,而且不能再指定一個初始值了。這樣做的好處是能夠在類的外部使用該靜態成員。void func(const int &x) {cout << "x:" << x << endl; } struct A {static const int a;static constexpr int aa = 6;static vector<int> v; }; vector<int> A::v(aa); const int A::a = 5; int main() {//const A a;//a.test();cout << A::v.size() << endl; func(A::a);return 0; }
-
靜態成員能夠用于某些場景而普通成員不能:
- 靜態數據成員可以是不完全類型,特別的,靜態數據成員可以是它所屬的類類型,而非靜態數據成員則收到限制,只能聲明成它所屬類的指針或引用
- 靜態成員和普通成員的另外一個區別是我們可以使用靜態成員為默認實參