C++小記二

字符串相關

C風格字符串:

1、C風格字符串是字符串末尾有一個\0也即null結束符的字符串。字符串字面值就是C風格的字符串,C++就是從C上面繼承來的。
2、儘管 C++ 支持 C 風格字符串,但不應該在 C++ 程序中使用這個類型。C 風格字符串常常帶來許多錯誤,是導致大量安全問題的根源。網絡程序中大量的安全漏洞都源於與使用C 風格字符串和數組相關的缺陷。現代 C++ 程序不應使用 C 風格字符串。
3、使用C的字符串處理庫函數時,傳遞的必須是這樣的C風格的字符串。要時刻注意後面有個null字符。比如下面的代碼
char ca[] = {‘C’, ‘+’, ‘+’}; // not null-terminated
cout << strlen(ca) << endl; // disaster: ca isn’t null-terminated
標準庫函數 strlen 總是假定其參數字符串以 null 字符結束,當調用該標準庫函數時,系統將會從實參 ca 指向的內存空間開始一直搜索結束符,直到恰好遇到 null 爲止。strlen 返回這一段內存空間中總共有多少個字符,無論如何這個數值不可能是正確的。
在要求 C 風格字符串的地方不可直接使用標準庫 string 類型對象。
這樣的就有null結束符:
char *ca = “abcd”;或者char ca= {“abcd”};
C的幾個常見字符串處理函數。#include<string.h> 中 strlen,strcpy,strncpy,strcmp,比較 strcat拼接
4、C++的string有個方法c_str(),返回C風格的字符串,即以null結尾的字符指針。但此函數有可能獲取失敗

C++的字符串

兩個相鄰的僅由空格、製表符或換行符分開的字符串字面值(或寬字符串字面值),可連接成一個新字符串字面值。這使得多行書寫長字符串字面值變得簡單:
// concatenated long string literal
std::cout << "a multi-line "
"string literal "
“using concatenation”
65
<< std::endl;

字符串內存相關

C++的存儲區裏面還可以細劃分出一個常量存儲區,其中就保存了字符串常量。如const char *ps =“aaa”; “aaa”就在裏面,此時p指向這個地址。這時候要注意的是ps指向的內存一定不能修改,不然會引起嚴重錯誤。即使用字符串常量直接賦值的都得用常量處理。

動態分配內存:

1、與數組變量不同,動態分配的數組將一直存在,即使在局部分配的,直到程序顯式釋放它爲止。分配的空間在自由存儲區或 堆中。C 語言程序使用一對標準庫函數malloc 和 free 在自由存儲區中分配存儲空間,而 C++ 語言則使用 new 和delete
C++的
分配數組: int* p = new int[n];
釋放數組: delete [] 表達式釋放指針所指向的數組空間:delete [] pia;

malloc的使用,只需要傳入分配的長度,其它不用傳。它返回然後將其強制轉換成需要類型的指針即可。
順便,釋放內存時爲什麼知道大小,應該分配的時候,在分配內存的開始位置加了一個額外的空間存儲分配的長度。

2、對於動態分配的數組,其元素只能初始化爲元素類型的默認值,而不能像數組變量一樣,用初始化列表爲數組元素提供各不相同的初值。小點,new xxx 後面可以加上一個(),在調用系統默認構造函數的情況下,會先將基本數據類型和指針類型賦0,對成員對象遞歸調用.

3、 有時需要使用只讀數組 ,也就是程序從數組中讀取數值 ,但是程序不向數組中寫數值 。在這種情況下聲明並初始化數組時 ,建議使用關鍵字const 。
const int days [ MONTHS ] = { 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 31 , 30 , 31 , 30 , 31 } ;

4、使用數組初始化 vector 對象,必須指出用於初始化式的第一個元素以及數組最後一個元素的下一位置的地址:vector ivec(int_arr, int_arr + arr_size);
5、動態分配多維數組,注意第二維及以上只能使用編譯時常量表達式。

動態分配內存需要注意的幾點,目前至少3點

1、動態分配出的內存使用前首先要檢查是否爲NULL,NULL表示分配失敗
2、分配之後的內存必須釋放,使用free或者delete
3、釋放之後的指針必須置爲NULL,不然野指針

另外注意,但是程序一複雜起來,這個工作是比較難做的。解決辦法有負責機制,誰創建的誰刪除,其它類不要管。還有一智能指針,也叫共享指針,類似於Java的自動刪除不用的堆上的對象。

多維數組:

嚴格地說,C/C++ 中沒有多維數組,通常所指的多維數組其實就是一維數組通過序號計算變來的:
多維數組初始化,有多種方式,比如
int ia[3][4] = {{ 0 } , { 4 } , { 8 } };
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
指針的數組與數組的指針:
int *ip[4]; // array of pointers to int,指針的數組
指向數組的指針:
int ia[3][4]; // array of size 3, each element is an array of ints of size 4
int (*ip)[4] = ia; // ip points to an array of 4 ints
ip = &ia[2]; // ia[2] is an array of 4 ints
ip是指向一維數組的指針,即存放一維數組的地址,所以ip可以直接用二維數組ia賦值,用一維數組ia[2]賦值時要取址

表達式

注意,除法或者取餘運算,如果只有一個操作數爲負數,這兩種操作的結果絕對值值取決於機器;求模結果的符號也取決於機器。
21 % -5; // machine-dependent: result is 1 or -4
21 / -5; // machine-dependent: result -4 or -5

位操作符操縱的整數的類型可以是有符號的也可以是無符號的。**如果操作數爲負數,則位操作符如何處理其操作數的符號位依賴於機器。**於是它們的應用可能不同:在一個應用環境中實現的程序可能無法用於另一應用環境。
對於右移操作符(>>),如果操作數是有符號數,則插入符號位的副本或者 0 值,如何選擇需依據具體的實現而定。bitset 類比整型值上的低級位操作更容易使用。

輸入輸出標準庫(IO library)分別重載的 >> 和 <<,與該操作符的內置類型版本有相同的優先級和結合性。

自增運算 ++a和a++

只有在必要時才使用後置操作符。因爲前置操作需要做的工作更少,只需加 1 後返回加 1 後的結果即可。而後置操作符則必須先保存操作數原來的值,以便返回未加 1 之前的值作爲操作的結果。(C primer原話,但是沒想明白爲什麼)。對於 int 型對象和指針,編譯器可優化掉這項額外工作。但是對於更多的複雜迭代器類型,這種額外工作可能會花費更大的代價。因此,養成使用前置操作這個好習慣,就不必操心性能差異的問題。
iter++
等效於
(iter++)。子表達式 iter++ 使 iter 加 1,然後返回 iter 原值的副本作爲該表達式的結果。

函數

形參和實參

形參就是指函數定義頭裏面放的參數,因爲雖然有定義,但每調用函數前內面的參數無效,所以叫做形參。後面調用時傳遞過去的參數就叫做實參。

數組的值傳遞

數組值傳遞,傳遞的是數組地址,不是複製數組,相當於傳遞了指針。然後實參數組的長度可以大於形參的,只要不引起越界就行了,也就是說實參長度不能小於形參。

內聯函數

1、內聯函數的定義很簡單,在函數前面加上inline即可。 但是編譯器並不保證函數一定變成內聯函數調用。當函數比較複雜等情況時。另外,非內聯函數編譯器也有可能變成內聯函數用。目前編譯器對內聯函數的處理十分智能了,手動聲明inline已經不太需要了。
2、注意多處調用內聯函數,則會在多處插入代碼,如果調用位置太多會加大編譯後代碼長度,要注意一下。
2、C++類的定義裏面定義的函數不管有沒有加上incline,編譯器都會默認它是加了inline的函數。如果在內外定義,那麼在定義或者聲明任一位置聲明inline即可變成內聯函數。但是,最終是否真正編譯成內聯函數,需要看編譯器的智能優化。

參數帶默認值

帶默認值的參數必須放在參數列表的最後面,按道理說放在其他位置編譯器也可以處理的,不過應該是那樣用起來太亂了,所以就規定只能放在後面了吧。
另外,要注意的的,函數的多個聲明和定義之間,只能有一處帶有默認值,否則是錯的。
調用的時候,不能跳過有默認值的參數傳參,,只能依次傳參,這也是爲了避免複雜性吧。
引用型參數默認值不能用變量賦值,可以用全局變量等的應用賦值。

函數重載

函數名相同,參數類型或者個數不同即可重載。還有const修飾符也可以重載。

函數棧大致原理

函數會放到棧裏面。一層一層疊加。調用方法是,首先在棧上分配形參的內存,賦值,然後放入返回地址,再放入局部變量等等。棧包含棧指針,還包含指向當前函數底部的幀指針,由兩個特殊的寄存器保存。由上可見分配內存時按一定的規律分配的。然後根據用幀指針或者棧指針加減地址差即可定位參數,局部變量的地址,然後用該地址生成命令,執行操作。當函數執行完之後,如果有返回值,計算完成的返回值會存儲到一個特殊的寄存器中。最後原來存入的返回地址取出來,放到PC中,跳轉到改地址處,需要返回值,就讀取寄存器中的返回值進行操作。
如果返回值是對象,C++爲了避免返回時對象復製造成的性能開銷,採取了一些措施。即主調函數將自己的用於接受返回值的對象的地址傳遞給被調函數,這個地址位於主調函數的幀棧中。被調函數在這個地址上創建對象。而不是將對象創建在自己的幀棧上,然後複製到主調函數的幀棧裏面。

類型的區分

unsigned int 和 int 類型是不同的,同樣的操作比如賦值,得到的結果可能是不同的。但是內存裏面只存儲了兩者的二進制值的數據,並沒有存儲他們是什麼類型,代碼運行的時候怎麼分辨它們的類型的了。原來一直不明白,原來它們是編譯器區分的,因爲聲明瞭類型,編譯器編譯的時候對不同的類型,採取的就是不同的操作,即什麼的代碼就不同,所以不用在內存裏面再特別存儲一下這個數據是什麼類型。而且在內存中存下類型代碼也不科學,類型太多,且會變,那樣就不好弄了。

面向對象

抽象,對具體問題進行概括,抽出一類對象的公共性質並加以描述的過程。
封裝,就是將抽象得到的數據和行爲結合,形成一個有機的整體,也就是將數據與操作數據的代碼進行有機的結合,形成一個整體-類,然後只對外暴露需要的數據和接口,與外界進行交互。
多態:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。C++的多態分爲靜態(編譯)多態和運行時多態。靜態多態主要包含函數重載,類模板參數(泛型編程)。動態多態即虛函數使用。

類定義

在類中可以只進行函數聲明 ,函數的定義可以寫在類外面。實現的時候指明類的名稱即可。返回值 類名::函數(…) {…}

每個對象的內存空間只需要存屬於對象的數據成員,方法和靜態數據都不需要存到對象的內存空間中 。

類成員變量的初始化問題

按照C++ 11標準,首先,不應該在類內部初始化任何成員!!無論是否爲靜態、是否爲常量、是否爲int等!!統統不建議在類內初始化,因爲本質上類只是聲明,並不分配內存。
但是,C++又是什麼兼容之類的,搞一堆例外情況,特殊規則。非靜態的變量,非靜態的常量變量都可以在類內部初始化。

靜態成員變量初始化

靜態成員變量和全局變量都會在程序開始運行之前,即main函數運行之前初始化。
靜態的字面常量,使用constexpr 修飾,可以在類內部初始化。其他的靜態數據,都不能。
即非常量靜態成員數據和非字面值靜態常量必須在類外部初始化。注意這個。

構造函數和析構函數

沒有手動定義構造函數時,C++編譯器會爲類創建一個默認構造函數。沒有參數,函數體也爲空。當定義了構造函數時,不管何種形式,則編譯器不會在生成默認構造函數。這和Java一樣的咯。注意答問題的時候最好稱作無參構造函數,更科學一些,因爲自定義了之後某種程度上不能叫默認構造函數。構造函數也可以是內聯函數。
複製構造函數,定義的方式是構造函數,然後形參定義爲本類對象的引用即可。如果沒有定義構造函數,編譯器會自動生成複製構造函數,複製構造函數會將每個數據成員都複製過去。
調用情形 1、創建對象時,Point b(a); 2、使用=號賦值時 3、函數調用值傳遞時 4、函數返回時。
通常情況下函數的返回值在被調用方法的方法棧幀裏面,要在調用方法裏面的到返回對象,就需要複製。注意一點,賦值構造函數是十分常用的,所以裏面不要做任何多餘的操作。
析構函數 不接受任何參數,類名字前面加上~作爲函數名即可。

對於存的C結構體或者類似結構體的類,還可以使用如下方式初始化。 結構體名 對象名={x,x,x,x} 成員變量依次賦值

初始化列表

格式爲
類名::類名(參數):成員對象(…),成員對象(…),… {類的初始化};
但是注意,初始化的順序不是構造函數這裏的順序,而是按類定義中出現的順序

首先要明確一個東西,和Java的不同,C++裏面成員對象在執行構造函數體之前,就已經初始化了,構造函數體裏面不是初始化,而是再次賦值。初始化發生在創建對象時和初始化列表這兩處。
需要在初始化列表中初始化的目前知道的有3類情況:
1、常量,自然的,不能再次賦值,必須在初始化列表裏面
1、引用,同上
2、沒有默認構造函數的。同樣,系統無法默認初始化,需要在初始化列表裏面寫好。
3、父類沒有默認構造函數。

編譯器不能自動初始化的對象都必須用上面的形式在初始化列表中初始化。3類:沒有默認構造函數的,引用,常量。
析構函數調用成員對象的析構函數順序與初始化的順序反向。自然的。
拷貝構造函數同樣的要加初始化列表。
前向聲明
C++規定變量使用前必須先聲明,對於類也是。C++好像沒有類的聲明。但類有一種特殊情況,假設A類裏面使用了B,B類裏面使用了A。這時無論將哪個定義寫在前面,都是編譯錯誤的。所以,引入的類的聲明。叫做類的前向聲明。使用方法 在前面加上class XXX;即可。 不過這個聲明只相當於半個聲明,它有如下限制。

  1. 前向聲明的類不能定義對象。
  2. 可以用於定義指向這個類型的指針和引用。
  3. 用於申明使用該類型作爲形參或返回類型的函數

構造函數用於轉型

構造函數還可以充當類型轉換方法的作用。正如int(1.1)。使用A a(b); 即可把b對象轉換爲A對象。然後有了這個構造函數之後,各種對應的轉型操作都能用了,轉型操作A a =(A)b以及static_cast<A>(b);等。最後甚至不需要顯式的表明轉換,直接用b賦值或者傳參給A類的對象進行隱式默認轉換。
但是因爲這樣的靈活性,可能使用的時候會出現問題,因此C++ 規定,在這樣的構造函數的聲明處,前面加上explicit來禁止隱式默認轉換。

聯合體union

union應用於這樣一種情形,即數據成員只能有一個賦值存在,一個賦值存在之後,另外的就不能存在了。
union A{
int a;
float b;
bool c;
}
"我:這個也是兼容C,加了一點類的東西,但又不是完全的類,四不像,不如不加。"其成員對象不能有自定義的構造函數,析構函數,拷貝構造函數。union不支持繼承和多態。使用的,union的名字也可以不要,然後直接使用union內部的變量名進行訪問,就向外部的括號不存在一樣。

位域

有些數據值的範圍有限,比如只有0-8的值,佔用3個二進制位,但是一般的數據類型,包括bool,枚舉等,佔用空間都比這個大,bool佔用一個字節,枚舉是int,大得多。C裏面通過位域來處理這種情況。也就是位域裏面的數據最小單位不是字節,而是位。通過如下方法聲明即可。
struct bs
{int a:8;int b:2;int c:6;}; 後面的數字表示佔用的位數。其中成員還可以是enum等。

作用域域生存週期

命名空間

可以在最外層套上一個
namespace XXX{

}
這樣,裏面的代碼就在命名空間XXX裏面了。然後使用XXX::xx 訪問即可。使用using namespace XXX 之後就可以省略前面的東西。
命名空間裏的量可以全局使用,相當於全局變量。
作用域大小關係可以簡單寫爲:命名空間 {類{局部=函數等}}
注意變量頭文件中用了命名空間包起來,cpp中仍然要用命名空間包起來。

局部全局變量

靜態對象,定義在作用域內的對象是全局變量,定義在文件最外層的對象是全局變量,局部作用域裏面比如函數聲明爲static的對象是全局變量。類的成員對象的生存期和Java類似。注意局部裏面的全局變量,在第一次進入局部的時候初始化。它具有全局壽命,但是隻能局部可見

類的靜態成員

類的一般的靜態成員在類內部只能進行引用性定義,不能進行實際的定義和初始化,實際的聲明需要在命名空間的作用域進行定義。因爲C++中只有這樣才能給他分配靜態量的空間。貌似是什麼靜態變量讓全部對象訪問,必須先於對象定義,所以要放在類外面。特別的是靜態字面常量可以在類內部定義和初始化,因爲編譯器確定嘛。
也因爲這個,即使是私有的靜態對象,在定義的時候也能直接在外部訪問。靜態函數可以在類內部定義。
一般用類名::標識符 訪問類的靜態成員

友元函數和友元類

友元函數或者友元類即在一個類中將另一個類或者函數在類中聲明一下,並且在前面加上friend即可。然後該類或者函數就能訪問本類的私有和保護成員。友元類內部的函數都是友元函數。
友元函數聲明 friend xx(xxx);
友元類聲明 friend class xxx;
聲明B是A的友元函數或者友元類的時候,需要B已經聲明瞭才行,不然是失敗的,注意。有些情況下編譯器會處理,但有些不會。
注意友元關係不能傳遞 不能繼承 並且是單向的

C++的文件組織

C++的文件可以分爲3部分,聲明的.h頭文件,實現的.cpp文件,調用的.cpp文件,一般就是main函數所在文件。C++編譯時將每個cpp文件分別編譯,然後鏈接接在一起。其中系統文件已經編譯好,只需要鏈接即可。

頭文件

頭文件能放的內容

一般的放在頭文件中的代碼不能要求分配空間,因爲如果多個文件包含此頭文件,就是產生重複分配而出錯,一般函數定義也不能,因爲函數需要分配代碼空間)。常見的有如下內容:
類聲明,外部變量、外部函數的聲明,字面量常量聲明,內聯函數聲明(變成代碼替換,正好要重複)

所以不應該含有一般變量或函數的定義。

在實踐中,**用常量表達式初始化的常量,大部分的編譯器在編譯時都會用相應的常量表達式替換這些 const 變量的任何使用。**所以,在實踐中不會有任何存儲空間用於存儲用常量表達式初始化的 const 變量。如果 const 變量不是用常量表達式初始化,那麼它就不應該在頭文件中定義。相反,和其他的變量一樣,該 const 變量應該在一個源文件中定義並初始化。應在頭文件中爲它添加 extern 聲明,以使其能被多個文件共享。

總是使用完全限定的標準庫名字:在頭文件中。理由是頭文件的內容會被預處理器複製到程序中。用 #include 包含文件時,相當於頭文件中的文本將成爲我們編寫的文件的一部分。如果在頭文件中放置 using 聲明,就相當於在包含該頭文件 using 的每個程序中都放置了同一 using,不論該程序是否需要 using 聲明。
通常,頭文件中應該只定義確實必要的東西。

標準的C++庫

標準的C++庫主要包含以下幾類代碼 io類 容器內與ADT(抽象數據類) 存儲管理類 算法 錯誤處理 運行環境支持
都在std命名空間下

編譯預處理

編譯預處理是指在開始編譯文件之前對工程進行文本上的處理。語法格式:所有預處理指令都以#開頭,每條預處理指令單獨佔用一行,不用分號結束,預處理指令可以根據需要出現在程序的任何位置。
#include指令
#include的作用就是就後面文件的文本包含到本文件中,形成一個文件

#define和#undefine指令
這個就是繼承C的宏定義指令

條件編譯指令

條件編譯指令可以限定程序的某些內容在滿足一定條件的情況下才參與編譯。
作用:1、可以利用條件編譯在不同條件下產生不同的代碼2、可以防止頭文件被重複包含。一個頭文件被多次包含進同一源文件也不稀奇。我們必須保證多次包含同一頭文件不會引起該頭文件定義的類和對象被多次定義。使得頭文件安全的通用做法,是使用預處理器定義 頭文件保護符。即預處理器變量。
條件編譯語句幾種形式
第一種
#if xxx
程序段
#elif xxx
程序段
#else
程序段
#endif

第二種
#ifdef xxx
程序段
#else
程序段
#endif

第三種
#ifndef xxx
#define xxx
#endif

編譯過程簡介:

編譯

前面說了,編譯是對一個個源文件單獨編譯的,編譯之後生成目標文件。目標文件主要存放程序運行時需要放在內存中的內容,這些內容包含兩大類-代碼和數據。代碼段裏面存放的是函數的代碼,包括外部函數的代碼,以及內成員函數的代碼。
數據段存放的是定義了的全局數據,包括初始化過的數據和未初始化過的數據。其中初始化的數據需要保存他們佔用空間的大小和初值,而未初始化的數據只需要保存佔用空間的大小,不需要保存初值。局部變量的數據不用存,運行的時候放到棧裏面或者堆裏面。但是這樣的目標文件還是不完整的。除了定義了的靜態變量,聲明瞭但是沒定義的靜態變量並沒有存放進來。目標文件中好包含一個稱爲符號表的東西,符號表中一列存放的是變量或者函數的符號,另一列存放它們的地址。如果是隻有聲明的符號,那麼單個目標文件裏面,第二列的內容就爲空,因爲沒有定義,當鏈接之後,可以在其它文件中找到那個地址,然後就可以加上去了。
其中有一個細節,因爲函數的重載,但是符號表裏面只有符號,沒有參數關係,所以編譯器會使用一定規則,結合參數信息等,對每個函數名加上一段區分的標識符,得到一個新的函數名,從而能夠區分重載函數。
目標文件裏面主要包含了這3個部分,還有一些其它部分,暫時不討論。

鏈接

鏈接就是將所有目標文件合併到一起,然後目標文件裏面的各個部分也合併到一起。除了用戶源程序生成的文件之外,還有運行庫需要鏈接,就將庫文件的目標文件鏈接起來。運行庫包含程序的引導代碼,即執行main函數前執行一些初始化工作。鏈接之後符號表就不必包含在可執行文件中了,因爲所有符號地址已知,直接用地址替代符號即可。一些特殊情況,如調試時會包含符號表到可執行文件中。

函數指針

由上面知,函數是一段代碼,會存放到靜態區的內存中,調用函數的時候就是當前執行地址的跳轉。那麼可以將函數的地址用指針保存,然後加以使用,就是函數指針了。和一般的指針取變量值不一樣,函數指針就是代碼跳轉咯。
聲明函數指針

返回值類型 (*函數指針名字)(形參類型列表)

是用的時候直接將函數名賦值給它既可,不用帶參數了,也不用帶括號。然後不用帶取址符號。函數名本身具有地址的性質?
對於定義函數指針需要多次定義時,由於類型書寫比較複雜,可以使用typedef創建別名,之後可以跟簡潔的使用。格式爲:
typeded 返回值類型 (*名字FP) (參數類型列表)
此後就能用FP直接定義這種函數指針了。

類成員的指針

包括類成員變量和成員函數。這種指針的定義和使用的語法格式和普通的指針又不一樣。需要在*前面加上類名或對象名,之所以這樣,爲了區分類名或者對象名前面加*吧。
定義和賦值時 *前面加上 類名::

定義和初始化
類型 類名:: *指針名 = &類名::非靜態公有數據成員      如  int Point:: * px = &Point::x
返回值類型 (類名::*指針名)( 參數表)= &類名::成員函數名int (Point::*getX)() = &Point::getX;

上面這樣的定義和初始化的原理是指針指向的地址實際上是成員在類內部的偏移地址,並不是絕對的地址。
使用的時候,利用.運算在前面拼接上一個對象,相當於加上了這個偏移地址,於是指針就指向了該對象的該成員。
格式:

對象.*數據成員指針
對象指針—>*數據成員指針

這個就是利用底層原理實現了一個比較繞的東西,直接隊成員取址一樣的可以訪問。

靜態類成員可以像一般的變量或者函數直接定義賦值,賦值的時候加上類名:: 即可

淺拷貝與深拷貝

C++裏面如果存在引用,指針類型的成員變量,使用默認的複製構造函數只能完成淺拷貝,因爲是直接賦值,指針的話就會出現兩個對象指針指向同一個地址。引用類似。這將導致數據修改衝突。更嚴重的是,如果指針指向動態分配對象,兩個對象析構時,都指針所指對象調用析構函數,將導致重複析構,出錯。這種情況下需要自己編寫拷貝構造函數,實現對指針或引用所指對象的深拷貝。

繼承和派生

繼承的定義方式
class xxx: 繼承方式 基類名1, 繼承方式 基類名2, 等等{
}

繼承方式

繼承方式有3種,公有繼承,保護繼承,私有繼承,默認私有繼承
繼承方式規定的繼承類和使用繼承類的外部代碼對改基類成員的訪問權限。
公有繼承:不能訪問基類的私有成員,其他權限不變的放到繼承類中,同Java
私有繼承:不能訪問基類私有成員, 基類的公有和保護成員成繼承類的私有成員。用得較少,因爲對後繼類完全覆蓋了基類成員
保護繼承:不能訪問基類私有成員, 基類的公有和保護成員成繼承類的保護成員.
注意到3種都不能訪問基類私有成員。

構造函數的定義,在函數參數列表後面接上基類構造函數,形式上就和初始化成員變量一樣的,形式如下

D(a,b,c,d):A(a),B(b),C(c),ia(a),ib(b) {...}

和成員變量初始化順序一樣,基類初始化順序也是和構造函數定義順序無關,只按照類定義時的順序來。
初始化順序 = 父類按順序構造—>初始化成員列表按順序—>構造函數

複製構造函數
複製構造函數一樣的在後面加上基類的複製構造函數即可。傳遞實參時直接傳遞子類對象即可,因爲兼容性。

析構函數,析構函數沒有參數,寫起來更簡單,和沒有繼承時的定義方式一樣,只需要~類名。然後析構函數的執行順序是和構造函數完全相反。即 析構構造函數體—>成員列表按反序析構—>父類按反序析構

子類、多個基類之間的變量名區分

1、第一中情形,繼承的多個基類沒有更上層的公共基類,此時,如果子類,多個基類之間變量名重複,如果要訪問某個基類的變量,加上一個作用域分辨符即可。
如 a.Base1::var = xxx; 訪問Base1基類中的變量var
注意繼承類與父類之間不能產生函數重載,只要同名,子類就會覆蓋基類的函數,此時不能直接訪問基類函數。和成員變量類似的,只能通過作用域訪問。

共同基類問題

1、注意C++的多繼承的一個特性是,如果一個類X繼承多個類,如A,B。而這多個類又有共同的基類,如N。繼承相當於把基類的代碼寫到子類中,最終只形成一個類。N的代碼既寫到了A中,又寫到了B中,最終一起寫到X中,形成一個類。此時公共基類N的數據成員不是隻有一份,而是每個繼承它的類中都有一份,成員函數只有一份,但是傳遞進去的this指針需要需要指明是哪個中間類,不然不行
此時,如果訪問這些數據成員的時候,必須指明是那個中間類的作用域進行確定,而且直接用公共基類作用域是不行的。因爲不能確定是哪個中間類中的數據。
繼承的代碼形式是?從最頂層的基類,沿着繼承的路徑,往下把代碼注入到下面的類中,最終注入到當前類中。這樣的嗎?

此時公共基類數據成員,函數成員都不能用了?
2、C++定義了一個語法來消除這種不科學的東西,虛基類繼承,使用虛基類繼承就不產生多份代碼副本。語法格式是在繼承方式前面加上virtual關鍵字,注意寫在繼承公共基類N的A、B的構造函數那兒,不是最終類X構造函數那兒。此時訪問成員就不用再加作用域符號了。但是,沒有副本又有一個問題,中間類A、B如果使用不同的值初始化公共基類N怎麼辦,C++規定,這種情況下只有最遠繼承類,即X對基類的初始化有效,中間類,即A、B的初始化語句忽略。
3、在這個語法下,對象的初始化順序y又加了一條,如果存在虛基類繼承,這首先按順序調用虛基類的初始化,再按順序調用其餘的初始化。

運算符重載

分 成員函數重載 非成員函數重載
成員函數重載方式:
返回值 operatorXX(參數列表)
注意這個參數列表,第一個運算數就是this,所以不寫到參數列表裏面。對於單目運算符,有左和右之分。如果是在右邊的話,需要在參數的括號里加一個int參數類名進行表明,後面不用加參數名了。
非成員函數重載,因爲沒了this,只需要把第一個參數加進去即可。如果需要訪問類的私有成員,需要將其聲明爲操作類的友元函數即可。

成員函數重載內聚性更好,非成員函數重載在一些特殊情形下有用,1、運算的雙方是系統或第三方類對象,無法在其內部寫成員函數。或者運算的一方是,但是要求第三方對象寫在運算符前面,寫成成員函數則this指針在前面,不滿足, 2、運算過程中需要一方自動轉型,如X+B,需要X自動轉化爲A,變成A+B,而如果運算符重載定義在A內部,需要A調用,用X是不行的。

C++規定,=, [],(),->只能被重載爲成員函數,並且派生類中的=總會隱藏基類中的=。
小點:io操作符,ostream & operator<<(ostream &out, A a) {}記住類名,類名不是cout哦,且通常只能寫成友元函數的形式。

虛函數和多態:

實現多態,這點C++和Java不一樣,Java直接覆蓋即可,C++需要將函數聲明爲virtual,並且要使用基類的引用或者指針訪問,不然將子類對象賦值給通過基類對象、引用等仍然是訪問的基類函數,不能訪問到子類的。注意使用基類的指針訪問只能用p->x的形式,使用(*p).x是不行的,相當於用對象訪問了。

和Java不同,基類的虛函數需要實現,然後通過類限定名仍然可以訪問,即
xx. 基類名::成員函數…

編程好習慣:
需要覆蓋的方法,一般都寫成virtual的,沒寫成的,不要覆蓋,否則引起歧義。(還知道引起歧義,直接規定死,不能覆蓋或者直接覆蓋不行????)

特殊情況(C++一個缺點,搞各種特殊情況,很不簡潔。一個源於它的特性要各種兼顧,就想它的基本點,兼容C,到了C++裏面,仍然這個語法兼容那個語法,那個語法兼容這個語法,第二個,不屏蔽底層,不利用編譯器屏蔽一些底層的東西,全都透露到語言上來,最後導致各種各樣的特殊情況,增加了語言的複雜性,實際上很多沒必要,不過沒辦吧,標準定死了,好像大家都能適應,每人要去該,不用這個C++又不行,只能硬着頭皮學了。C++繼承這一塊語法弄得,相對於Java就想菜鳥寫的幾百行的一團混亂代碼和大神寫的幾十行的簡潔代碼一樣)
如果虛函數參數帶有默認值,子類覆蓋的函數默認值無效。
基類構造函數和析構函數中調用虛函數時,此時是調用基類的,而不是子類的。(搞你妹的特殊,規定死必須用類限定名調用不行????)

隱藏和覆蓋

1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏(注意別與覆蓋)。如果有virtual關鍵字,則覆蓋。
簡述:所有沒有virtual的同名函數都被隱藏,有virtual的完全相同被覆蓋。

將派生類對象賦值給基類時,會調用基類的複製構造函數,最終生成的就是一個基類對象,和派生類沒有關係了。這種操作通常是不太好的,會產生一些問題,比如派生類裏面如果有動態分配的內存,則不能調用派生類的析構函數,內存無法釋放。
對於上面這個問題,通過如下方式可以解決,虛析構函數,構造函數不能定義爲虛函數,析構函數可以。這樣,如果是動態綁定的子類對象,則可以調用到子類的析構函數,完成內存釋放等必要操作。通常的類析構函數都應當寫爲虛析構函數。
基於以上,沒有虛函數的類,一般都不用來繼承。

派生類對象和派生類的指針可以給基類的對象和基類的指針賦值,但反之則不行,已查過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章