【複習】C++面向對象基礎1


C++面向對象基礎

根據侯捷老師的C++面向基礎課程總結而來,內容經過博主適當擴充。(僅作爲複習用,最好有C基礎或者C++部分基礎)

1.class分類

  • class without pointer members

  • class with pointer members

將c++的類分作不帶指針和帶有指針兩種,他們的主要區別是

  • 帶有指針,即數據是通過指針間接訪問的,所以在拷貝構造時需要處理這個數據。(決定了你拷貝的多個對象的某個數據成員,是多個指針變量指向同一個內存區域,還是每個對象的指針成員指向的不同存儲區域。)
  • 帶有指針的類,通常需要自己處理指針相關的內存區域,如初始化和析構函數,以免內存泄漏。
  • 而不帶指針的類不需要手動管理內存,這和Java中的類會比較像。

這種劃分有一個比較簡單的想法是,C++的每個class對象的大小應該是已知固定的,這樣在創建對象分配內存時編譯器知道該分多少內存。這時考慮一下,字符串,每個字符串長度不一樣,那麼字符串類怎麼確定長度。所以採用指針,指針變量的存儲空間是固定的,只需要讓不同對象中的指針成員指向不同具體存儲數據的空間,那麼就可以解決變長的數據問題了。


2.C++歷史簡述

  • B語言
  • C語言
  • C++(早期是c with class)

C++版本

  • c++ 98 (1.0)
  • c++ 03
  • c++ 11(2.0)
  • c++14
  • c++17 (建議擴充學習c++17新特性)

c++包含:

  • 語言部分,描述C++語法
  • C++標準庫,常用功能的實現與封裝。

3.C與C++的區別

這裏只討論數據和內存模型,不涉及具體的語法區別。

  • C:數據和函數獨立,不關聯。
  • C++:通過class將數據和函數組織(綁定)在一起,它考慮到實際上所有的函數都是在處理某些特定的數據,所以將他們綁定在一起,而不需要與其他無關的數據耦合。

class的好處只要具體編寫過面向過程編程和麪向對象編程就能體會到。不同的面嚮對象語言的思想是一樣的,有句經典的話叫

萬物皆對象。

不管是抽象的還是具體的概念都可以是對象。(在很久以前剛接觸面向對象編程的時候很不解,面向對象怎麼就方便了,怎麼就減少複雜性了,每一個面向過程需要編寫的函數在面向對象裏面還不是一樣要寫成員函數的實現嗎?事實上,這種結構思想上的改變在小程序還體現不出來,但當工程量大起來的時候,就是一盤散沙和一堆磚塊的區別。)

補充:C++如何綁定成員變量和成員函數:https://www.cnblogs.com/alone-striver/p/7875741.html

每個對象所佔用的存儲空間只是該對象的數據部分(虛函數指針和虛基類指針也屬於數據部分)所佔用的存儲空間,而不包括函數代碼所佔用的存儲空間。

C++程序的內存格局通常分爲四個區:全局數據區(data area),代碼區(code area),棧區(stack area),堆區(heap area)(即自由存儲區)

  • 全局數據區存放全局變量,靜態數據和常量;
  • 所有類成員函數和非成員函數代碼存放在代碼區
  • 爲運行函數而分配的局部變量、函數參數、返回數據、返回地址等存放在棧區;(所以在無退出條件的遞歸函數調用時會發生棧溢出錯誤.)
  • 餘下的空間都被稱爲堆區。

類成員函數是被放在代碼區,而類的靜態成員變量在類定義時就已經在全局數據區分配了內存,因而它是屬於類的。對於非靜態成員變量,我們是在類的實例化過程中(構造對象)纔在棧區或者堆區爲其分配內存,是爲每個對象生成一個拷貝,所以它是屬於對象的。

總結:

  • 類的靜態成員變量:全局數據區,屬於類,每個類一份。
  • 類的非靜態成員變量:棧區,屬於類的每一個對象實體,每個對象獨立一份,互不干擾。
  • 類的成員函數:代碼區。屬於類。每個該類的對象都會調用到這同一份成員函數代碼。(關於虛函數和靜態成員函數後續關於三大OOP操作再詳細討論,TODO

4.“善變的”struct

  • C中的struct VS C++中的struct

    • C語言中的struct是用戶自定義數據類型,它是沒有權限設置的,它只能是一些變量的集合體,雖然可以封裝數據卻不可以隱藏數據,而且成員不可以是函數。
    • C++語言中的struct是抽象數據類型(ADT),它支持成員函數的定義,同時他增加了訪問權限,它的成員函數默認訪問權限是public。
    • C語言中的struct是沒有繼承關係的,而C++的struct卻有豐富的繼承關係。
  • C++中的struct VS C++中的class

    • 如果沒有多態和虛擬繼承,在C++中,struct和class的存取效率完全相同,存取class的數據成員與非虛函數效率和struct完全相同,不管該數據成員是定義在基類還是派生類。

      class的數據成員在內存中的佈局不一定是數據成員的聲明順序,C++只保證處於同一個access section的數據成員按照聲明順序排列。

    • 對於成員的默認訪問權限,class是private,struct是public。

    • 默認繼承權限不同,class繼承默認是private繼承,而struct默認是public繼承。需要注意的是,class可繼承class,也可繼承struct。struct可繼承struct,也可繼承class。默認的繼承方式取決於子類是struct還是class

    • 定義模版參數,使用typename,也可以使用class。但是不能使用struct。

在編寫C++代碼時,強烈建議使用 class 來定義類,而使用 struct 來定義結構體,這樣做語義更加明確。


5.防衛式頭文件聲明

#ifndef __HEAD_FILE_NAME__
#define __HEAD_FILE_NAME__

// 頭文件內容
// ...

#endif

原因:由於C++不允許重複定義,所以在include頭文件的時候,可能出現不同文件交叉include,帶來重複定義問題。所以這個模式保證了每個頭文件只會被include一次。

具體的含義:

  • #ifndef:if no define,如果沒有定義xxx。
  • #define:那麼定義xxx
  • #endif:end if,結束該if塊。

這是一個比較正統的頭文件編寫模板!


6.一個class的例子

// complex.h
#ifndef __COMPLEX__
#define __COMPLEX__

#include <cmath>

// 前置聲明:forward declarations
class ostream;
class complex;
complex&
__doapl (complex* ths, const complex& r);

// 類的聲明:class declarations
class complex
{
...
};

// 類的定義:class definition
complex::function ...

    
#endif

說明:

  • 前置聲明:前置聲明作用是告訴編譯器下文中會用到一些標識符,他們是有具體含義的,不要因爲你不知道而給我報錯了。其實這裏的前置聲明作用還不明顯,更明顯的作用可查看一個交叉引用的struct定義的例子(就是說定義struct B的時候有成員類型是struct A,定義strcut A的時候,裏面有成員類型是struct B,這時該如何編碼):

    https://blog.csdn.net/f290131665/article/details/17678851

  • 類的聲明:向編譯器描述你的類有哪些數據成員變量,有哪些函數(也可在聲明的時候就實現了該函數——默認是inline模式)。

  • 類的定義:成員函數的實現。


7.C++模板和Java泛型的區別

Java中的泛型是“類型擦除”,源碼編譯成字節碼時,參數類型消除。

// 編譯前:代碼
	Vector<String> vector = new Vector<String>();
	vector.add(new String("hello"));
	String str = vector.get(0);

// 編譯後:實際
Vector vector = new Vector();
vector.add(new String("hello"));
String str = (String)vector.get(0);

看到,Java的泛型世界上是強制類型轉換。虛擬機常量池中只有一個方法,以及方法字節碼中一些多出來的cast指令,泛型參數在class文件裏是不存在。Java中虛擬機中沒有泛型,只有基本類型和類類型,泛型會被擦除,一般會修改爲Object

除了Java,所有實現在jvm上的語言,如果能與java兼容的(互相調用之類的),那麼他必定也是這種泛型實現方式,例如kotlin,groovy

C++的模板則是創建多個具有不同參數的方法,所以最後的可執行文件中,也必定包含了多個方法。這一現象被稱爲“模板代碼膨脹”。

// C++
template <typename T> T max(T a,T b){
    return a>b?a:b;
}
// 可以編譯,在實際調用max的時候,會判斷傳入的參數T是否支持>操作符,如果不支持纔會報錯

// Java
 static <T extends Comparable<T>>T max(T a,T b){
        return a.compareTo(b)>0?a:b;
    }
// 編譯出錯,因爲不能確定a是否有compareTo方法,這需要在編譯階段就確定好類型。一個解決方案就是採用Java中的泛型的類型上限約束,如下:
 static <T extends Comparable<T>>T max(T a,T b){
        return a.compareTo(b)>0?a:b;
    }

具體就可以想象成, Java的泛型只有一個方法,所以它的類型必須符合嚴格的要求,而C++有多個互不干涉的方法,每個方法有自己的類型要求。Java的泛型檢查在編譯期,運行時沒有泛型。C++的泛型在編譯器運行時都存在。

C++: MyClass不會與MyClass共享靜態變量。然而,兩個MyClass實例則會共享靜態變量。

Java:MyClass類的靜態變量會由所有MyClass實例共享,無論類型參數相與否.

  • C++模板可以使用int等基本數據類型。Java則不行,必須轉而使用Integer
  • Java中,可以將模板的類型參數限定爲某種特定類型。例如,你可能會使用泛型實現A,並規定參數必須擴展自B。也就是上文提到的泛型類型限定。
  • Java中,類型參數(即MyClass中的Foo)不能用於靜態方法和變量,因爲他們會被MyClass和MyClass共享。但在C++中,這些類是不同的,類型參數可以用於靜態方法和靜態變量。
  • 在Java中,不管類型參數是什麼,MyClass的所有實例都是同一類型。類型參數會在運行時被抹去。而C++中,參數類型不同,實例類型也不同

8.inline與宏的區別及其優缺點

#define TABLE_MULTI(x) (x*x)

因爲函數的調用必須要將程序執行的順序轉移到函數所存放在內存中的某個地址,將函數的程序內容執行完後,再返回到轉去執行該函數前的地方。這種轉移操作要求在轉去執行前要保存現場並記憶執行的地址,轉回後要恢復現場,並按原來保存地址繼續執行。因此,函數調用要有一定的時間和空間方面的開銷,於是將影響其效率。即,函數調用會在棧中進行“轉場”,有內存(空間+時間)開銷。

而宏是編譯時的代碼展開,“實際”代碼量會增長,同時,宏會存在一些歧義。如上述宏定義:TABLE_MULTI(10),結果返回100,是正確的。如果我們用TABLE_MULTI(10+10)去調用的話,我們期望的結果是400,而宏的調用結果是(10+10*10+10),結果是120.

// 避免方法:參數用括號括起來
#define TABLE_MULTI(x) ((x)*(x))

但此時若用TABLE_MULTI(a++)調用,希望得到(a+1)(a+1)(a+1)*(a+1),但結果(a++)(a++)(a++)*(a++)

內聯函數

  • 任何在class的聲明部分內定義實現的函數都自動被認爲是內聯函數。

  • 內聯函數必須是和函數體申明在一起,纔有效。像這樣的申明inline Tablefunction(int I)是沒有效果的,編譯器只是把函數作爲普通的函數申明,我們必須定義函數體。

    inline tablefunction(int I) {return I*I};
    
  • 內聯函數只是一種對編譯器的建議,建議把該函數編譯成“內聯”的方式,實際在編譯過程中,編譯器根據該函數的具體複雜度決定最終它能否被編譯成一個真正的內聯函數。

區別

  • 內聯函數和宏的區別在於,宏是由預處理器對宏進行替代,而內聯函數是通過編譯器控制來實現的。而且內聯函數是真正的函數,只是在需要用到的時候,內聯函數像宏一樣的展開,所以取消了函數的參數壓棧,減少了調用的開銷。你可以象調用函數一樣來調用內聯函數,而不必擔心會產生於處理宏的一些問題。

  • 內聯函數與帶參數宏定義的另一個區別是,內聯函數的參數類型和返回值類型在聲明中都有明確的指定;而帶參數宏定義的參數沒有類型的概念,只有在宏展開以後,才由編譯器檢查語法,這就存在很多的安全隱患。

  • inline是指嵌入代碼,就是在調用函數的地方不是跳轉,而是把代碼直接寫到那裏去。可以加快程序運行的速度,因爲不需要中斷調用.

  • 預處理器可以刪除註釋、包含其他文件以及執行宏替代。所以宏是預處理處理,inline是編譯器處理。


9.構造函數初始化

初始化列表與函數體初始化的區別

class complex
{
public:
    // 1.通過initialization list初始化成員變量
    complex (double r = 0, double i = 0): re (r), im (i) 
    { }
    
    // 2.通過assignments,賦值成員變量(方法1,2只能選1個,這裏爲了說明,兩個都寫出來了。)
    complex (double r = 0, double i = 0)
	{ re = r; im = i; }
};

  • initialization list初始化成員變量,僅在構造函數中可以這樣寫。
  • 上面的代碼中可見,構造函數不能有返回值,函數名和class name相同。參數推薦使用傳引用參數。參數可以有默認值,但有默認值的參數必須放在參數列表的後面。
  • 推薦使用方法1的構造函數,因爲它在創建成員變量的時候直接初始化到目標值。而方法二分兩步,先隨機初始化(與編譯器有關,可能不是隨機,可能爲0初始化),然後再通過賦值語句賦值,所以效率低了一點。
// 初始化列表應該按聲明的順序,不要輕易改變其順序。
class CMyClass {   
CMyClass(int x, int y);   
int m_x;   
int m_y;   
};   
  
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)   
{   
  
}

你 可能以爲上面的代碼將會首先做m_y=I,然後做m_x=m_y,最後它們有相同的值。但是編譯器先初始化m_x,然後是m_y,,因爲它們是按這樣的順序聲明的。結果是m_x將有一個不可預測的值。


10.函數重載條件

  • 重載判斷:參數類型,參數個數,參數順序,函數名相同。(返回值類型不作爲重載判斷

  • 含有默認參數的重載函數,在調用的時候會發生調用錯誤,編譯器不知道到底該調用哪個函數,此時,可用函數指針解決該問題,或者用namespace解決。

  • C沒有函數重載,但是cpp有函數重載,故cpp兼容c時,需要用到extern關鍵字。(C++的重載函數,編譯器在編譯的時候實際上爲每個函數重新命名了的)

void test(int a,int b=1)
{
	cout<< a << b;
}

void test(int a)
{
	cout << a;
}

int main(int argc, char const *argv[])
{
	// 調用test的時候,若test(2);則不知道如何匹配
	// 此時用namespace或者函數指針解決
	void (*fun1)(int a,int b) = test;
	// 用指針解決的時候,默認參數不再起作用,要顯式傳入參數值
	fun1(2,1);  
	return 0;
}

11.namespace

  • namespace作用:解決類重名、函數重名、變量重名等重名問題。用來給資源(變量、函數)限定一個名稱。

  • namespace內容的訪問類型:namespace中所有的內容都是public的,且不能用private修飾。

  • C沒有namespace關鍵字,cpp纔有命名空間概念。

  • 命名空間的定義:namespace space_name { body…}

  • 命名空間的使用:

    • using namespace space_name ;
    • space_name ::body_name
  • 匿名namespace:匿名namespace,表示可直接引用該space內的內容,而不用space_name ::的方式調用該域內容。其作用和static類似,將資源限定在本文件中使用,外部文件是無法訪問的(叫不上名字)

    // 匿名空間
    namespce {
        char c;
        int i;
        double d;
    }
    
    // 編譯器的等效結果
    namespace __UNIQUE_NAME_ {
        char c;
        int i;
        double d;
    }
    using namespace __UNIQUE_NAME_;
    // 在匿名命名空間中聲明的名稱也將被編譯器轉換,與編譯器爲這個匿名命名空間生成的唯一內部名稱(即這裏的__UNIQUE_NAME_)綁定在一起。還有一點很重要,就是這些名稱具有internal鏈接屬性,這和聲明爲static的全局名稱的鏈接屬性是相同的,即名稱的作用域被限制在當前文件中,無法通過在另外的文件中使用extern聲明來進行鏈接。
    // C++ 新的標準中提倡使用匿名命名空間,而不推薦使用static,因爲static用在不同的地方,涵義不同,容易造成混淆.另外,static不能修飾class。
    
  • 命名空間別名

    namespace name1_long_name{}
    namespace name2 = name1_long_name;//變量賦值的方式爲命名空間取別名
    
  • 擴充命名空間:

    • 再次重定義命名空間(空間名相同)則會擴展原命名空間的內容,而不像變量那樣因重定義而編譯出錯。
    • 但是,在擴展的時候,不能再次重定義變量,不會覆蓋而是發生重定義編譯錯誤。
    • 不建議在命名空間中直接定義函數,而是採用函數指針變量的方式,在命名空間外部定義函數。
  • using作用域

    • using作用只從該語句到本源文件結束。
    • 若using語句在block內部,則該using只作用在該代碼塊中
    • using語句只能在命名空間定義之後的地方使用
    • 一個源文件可用同時使用多個using語句,但是若同時using的多個命名空間有名稱相同的內容,則引用該內容時需要顯式的採用域操作符::方式。
  • C中全局變量與CPP命名空間

    • C中,當函數中局部變量與全局變量重名時,在該函數中無法引用該全局變量
    • CPP中,可用::操作符解決上述情景,可在函數中引用重名的全局變量,::name表示name是全局的變量,而不是該代碼塊中的局部變量
    • 綜上所述,cpp中的::域操作符,若之前有標識符,表示命名空間,無標識符,表示全局變量。
    #include<iostream>
    
    using namespace std;
    
    // 自定義命名空間
    namespace name1 {
    	int a = 10;
    	void print() {
    		cout << "print in space name1 !" << endl;
    	}
    }
    
    // 匿名空間
    namespace {
    	int a = 20;
    	void print() {
    		cout << "print in space none_name !" << endl;
    	}
    }
    
    // 全局命名空間
    int a = 30;
    void print() {
    	cout << "print in globle space !" << endl;
    }
    
    int main() {
    	// 編譯錯誤,a不明確,編譯器不知道是全局空間的a還是匿名空間的a
    	// cout << a << endl;
    	// 同樣錯誤,有多個重載的print, 全局print和<unamed>::print()
    	// print();
    	
    	// 30,調用的是全局的變量
    	cout << ::a << endl;
    	// 同理,調用全局的print函數
    	::print();
    
    	// 10,調用的是name1的變量
    	cout << name1::a << endl;
    	// 同理,調用name1的print函數
    	name1::print();
    
    	// 所以無法訪問到匿名空間中的資源!?
    	return 0;
    }
    

12.const member functions 常成員函數

const與宏常量的區別

  1. const常量有數據類型,而宏常量沒有數據類型

編譯器可以對前者進行類型安全檢查,而對後者只能進行字符替換,沒有安全檢查,並且在字符替換時可能會產生意料不到的錯誤。

  1. 編譯器對二者的調試

有些集成化的調試工具可以對const常量進行調試, 在 c++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

class complex
{
public:
    complex (double r = 0, double i = 0)
    : re (r), im (i) 
    { }
    // const 寫在參數括號之後,無數據修改的成員函數都強烈建議寫成const的。(不寫不報錯,但是使用中會出現問題)
    double real () const { return re; }
    double imag () const { return im; }
private:
    double re, im;
};

13.參數傳遞(value VS. reference)

  • 值傳遞:參數在傳遞過程中,會拷貝一份,若是複雜對象,會帶來很多額外的開銷。所以推薦只在基本數據類型時採用值傳遞。

  • 引用傳遞:reference參數接收的實參可以是顯式地reference也可以不是reference而是value,傳遞者無需知道接受者怎麼接收的,接收者無需知道傳遞者怎麼傳的。引用與指針類似(底層就是指針實現)。它傳遞的是對象的地址,所以在參數傳遞過程中開銷很小。

    inline complex&
    __doapl(complex* ths, const complex& r)
    {
    ...
    return *ths;
    }
    inline complex&
    complex::operator += (const complex& r)
    {
    return __doapl(this,r);
    }
    
  • 常引用傳遞:但由於是地址傳遞,所以引用傳遞往往存在一個問題,有可能在函數體中修改了函數調用處(外部)的實參。這個時候需要根據實際需求來增加const修飾,若要保證這個傳進來的引用的數據不被函數體修改,需要增加const限制。(大多數情況下都是如此)


14.參數返回(value VS. reference)

函數的返回值,同樣需要考慮直接返回值還是返回結果的引用

  • return value:基本數據類型。這種返回會發生數據拷貝。所以自定義類型帶指針一定要實現拷貝構造函數,否則進行函數調用值傳遞時會出現問題。(假設在函數中聲明一個臨時對象A,A中p指針數據new了一個內容。然後函數執行完之後,結果A值傳遞給調用處,B接收A,若只發生淺拷貝,B中的p指針也指向A中的p指針指向的內容,此時,函數調用結束,自動回收臨時對象A,A的析構中自然會delete p,那麼B就會引用到一個被回收的內存,會錯誤。)
  • return reference:顯然返回reference是比較好的,它避免的大規模的拷貝。但是,函數調用返回結果時,往往不能直接返回結果的引用!這是因爲在函數調用內部的變量都是在棧中創建的局部臨時變量(Local variables),它在函數調用後,會被銷燬(空間回收),所以引用無法通過地址訪問到正確的數據。這種情況下不能返回引用!若函數返回值不是局部變量,比如是一個由參數傳入的外部引用,那麼自然就可以返回該引用(若需要的話)。
  • 要想在函數調用中返回reference,可以用new的方式將對象創建在堆中,這樣函數調用結束後,它就不會被自然回收了(new創建對象直接使用堆空間,而局部不用new定義類對象則使用棧空間)。如下列代碼中的get_str2

string & get_str(string & str) {
	return str;
}

string & get_str() {
	string local = "abc";
	cout << "局部變量local的地址:" << &local << endl;
	return local; // 這裏傳遞的時候,可以直接傳對象值,不用顯式寫引用。
}

string & get_str2() {
    // 但這種new的方式容易造成內存泄漏
    // 普通方式創建,使用完後不需要手動釋放,該類析構函數會自動執行。而new申請的對象,則只有調用到delete時再會執行析構函數,如果程序退出而沒有執行delete則會造成內存泄漏。
	string* local = new string("abc");
	cout << "局部變量local的地址:" << local << endl;
	return *local; 
}

int main() {
	string a = string("abc");
	// abc, a address : 0000000C2FAFF7B8
	// abc, get_str(a) address : 0000000C2FAFF7B8
	cout << a << ", a address:" << &a << endl;
	cout << get_str(a) << ", get_str(a) address:" << &(get_str(a)) << endl;

	// 局部變量local的地址:000001D57008F760
	// get_str2() address : 000001D57008F760
	cout << "get_str2() address:" << &(get_str2()) << endl;

	// 局部變量local的地址:0000000C2FAFF638
	// 中止!在調用string c = get_str();會發生一個拷貝構造,訪問到一個被銷燬的內存空間,出現內存錯誤
	string c = get_str();

	return 0;
}

15.友元 friend

  • 關鍵字friend
  • 注意:友元關係只是單向關係,且不具備傳遞性。即我當你是朋友,你不一定當我是朋友;你是我朋友,你的朋友不一定是我朋友
  • 用法:友元類、友元函數
  • 友元函數的實現可以在類外定義,但必須在類內部聲明。友元函數是可以直接訪問類的私有成員的非成員函數。它是定義在類外的普通函數,它不屬於任何類
  • 一定程度上,友元打破了C++的封裝性,給class開闢了一個額外的訪問路徑。

16.訪問控制

封裝(訪問權限針對的是class,而不是單個對象,private修飾的資源只能被本類class的object訪問,而不是說當前object訪問當前object的previate修飾的資源)。直接表現:相同class的各個objects互爲友元

class complex
{
public:
    complex (double r = 0, double i = 0)
    : re (r), im (i) 
    { }
    int func(const complex& param)
    { return param.re + param.im; }
private:
    double re, im;
};

// 使用代碼
{
    complex c1(2,1);
    complex c2;
    c2.func(c1);
    // c2的func中直接訪問c1的private成員。這是可以的,本身private的作用單元就是class,而不是object
}

17.操作符重載

  • 操作符重載不改變優先級、結合性、操作數,能創建新操作符。

  • C++中操作符實際等價與函數調用,其重載操作符函數常規語法如下:
    類型 類名 :: operator 操作符(參數列表)

  • 對於操作符重載需要確定三點:幾元操作符、操作數類型、返回值類型。對應不同的操作符重載函寫法。對於全局函數,其參數個數、類型、順序就是操作數的個數、類型、順序。對於類成員操作符重載函數,可以看作是當前類對象爲該操作符的默認第一個操作數(通過this指針引用這個當前對象)。所以操作符重載又可以分爲兩種寫法:全局函數或者成員函數(同一功能,只能選一種實現)。如下

    class Foo
       {
       public:
       int operator + (int i)
       {
          cout<<"Foo + override " << endl;
          return 0;
       }
     };
    
    // 編譯器將這個函數和Foo的成員函數視爲相同的重載函數,編譯會出錯
    int operator + (Foo a,int b)
    {
        cout << "global + override. " << endl;
        return 0;
    }
    
    // 實際上,這兩個同樣的“+”重載是站在不同角度編寫的同樣意義的函數
    int main(int argc, char *argv[])
    {
        Foo foo;
        int i = foo + 1;
        return 0;
    }
    
  • 但有些重載必須寫成全局函數,如 輸出操作符“<<”。

  • 操作符重載往往要考慮使用習慣,考慮連續操作問題。

  • 特別注意:++、-- 、new 、delete、new [] 、delete [] 重載參數問題。new\delete必須成對重載。new xxx[] 與 new xxx不同,且new xxx[] 通常與delete [] 成對重載。new和delete一定會調用構造、析構函數。malloc、free不會調用析構和構造。即重載new、和delete並不會完全摒棄其原始功能(即使重載了,還是會調用構造析構),僅是干涉(或者說添加)一些自定義的邏輯。一般不建議重載new和delete。

  • 操作符重載基本條件:一定有一個操作數是自定義的C++類,即所有操作數都是基本數據類型的操作符重載是非法的。


18.小結(1)

寫一個類該考慮哪些問題?

  • 有哪些數據?數據應該private保護起來。
  • 有哪些方法?通常方法都是public,公開給外部調用,簡單的方法通常會inline實現。
  • 方法會改變private的數據嗎?如果不改變,那麼該方法應該被修飾爲const。
  • 參數建議傳引用。
  • 引用參數會在函數體內inplace修改嘛?如果不修改傳進來的參數,那麼參數應該是const 的引用傳遞。
  • 函數返回應該是返回value還是返回reference?如果返回的內容是在函數體內創建的local變量,那麼不應該return reference,因爲在函數調用棧中創建的局部變量在函數調用結束後它就會被回收,那麼外部通過這個引用訪問這個空間就會出問題。
  • 操作符重載需要寫成成員函數還是非成員函數?只能選擇其一,且有些特殊的操作符只能用非成員函數重載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章