C++知識點大匯總


概述

1、1980年 貝爾實驗室 Bjanre Stroustrup(比雅尼·斯特勞斯特魯普)對C改進與擴充 最初稱為“帶類的C”,(c with classes). 1983年正式命名為C++

2、

  • C++是C的改進與擴充。
  • C++包括C的全部屬性、特征、優點,是在C的基礎上的改進與擴充。
  • C++包括過程性語言和類部分。
  • C++是混合型語言,即是過程型的,又是面向對象型的。

3、“面向過程”是一種以事件為中心的編程思想。功能分解、行為抽象抽象編程。

4、面向對象程序設計的基本特征:

(1)對象:

  • 數據屬性(靜態)、行為屬性(動態);
  • 實現封裝與數據隱藏;
  • 對象是對類的實例化。

(2)繼承:派生其他類

(3)多態:同一個操作在不同的類上有着不同行為

5、編譯過程可分為三個子過程:預處理過程、編譯過程、連接過程

函數

內聯函數

內聯函數也稱內嵌或內置函數,它的語法格式與普通函數一樣,只是在函數原型函數定義標題頭之前加上關鍵字inline。

  inline int isNumber (char); inline int isNumber (char ch) { return(ch>='0' &&ch<='9')?1:0; }

使用內聯函數可以省去函數調用所需的建立棧內存環境,進行參數傳遞,產生程序轉移的時間開銷。內聯函數應是使用頻率高,代碼卻很短的函數。

 

內聯函數的函數體限制:

  • 內聯函數中,不能含有switch和while。不能存在任何形式的循環語句
  • 遞歸函數不能用來做內聯函數。
  • 內聯函數中不能說明數組。 否則,按普通函數調用那樣產生調用代碼。 (內聯函數是建議性,不是指令性)
  • 內聯函數只適合於1-5行的小函數
  • 類結構中所有在類內部定義的函數,都是內聯函數。
  • 內聯函數中不能有過多的條件判斷語句
  • 不能對函數進行取址操作

重載函數

實現函數重載的條件:

  • 同一個作用域
  • 參數個數不同
  • 參數類型不同
  • 參數順序不同

匹配重載函數的順序:

(1)嚴格匹配

(2)內部轉換(相容類型匹配)

(3)通過用戶定義的轉換尋找求一個匹配。

C++用名字粉碎(name mangling)(名字細分、名字壓軋)的方法來改變函數名。

返回值類型不能夠作為重載依據(區分、細分重載)

默認參數的函數

1、當又有聲明又有定義時,定義中不允許默認參數。 若只有定義,則默認參數才可出現在函數定義中。

 

#include <iostream>
using namespace std; void fun(int a = 20); void fun(int a = 20)      //函數定義處不允許默認參數,應改為 void fun(int a){};
{ cout << "你好" << endl; } int main() { fun(); return 0; }

 

 

 

#include <iostream>
using namespace std; void fun(int);   //函數聲明
int main(){ fun(); return 0; } void fun(int a = 20) //函數定義處不允許默認參數,應改為 void fun(int a){};
{ cout << "你好" << endl; }

2、一個函數中可以有多個默認參數,默認參數應從右至左逐漸定義,當調用函數時,從左向右匹配參數。

void foo(int a, int b = 0, bool c);    //fail,b不是最后一參數,只要有一個參數是默認參數,它后面的所有參數都必須是默認參數

3、默認值可以是全局變量、全局常量、函數。不可以是局部變量。因為默認值是在編譯時確定的,必須是靜態確定的。

4、默認參數不能用於細分重載函數。

void func(int,int); void func(int=3,int=4); //redefinition of 'void fun(int, int)'
void func(int); void func(int,int=4); //call of overloaded 'fun(int)' is ambiguous

5、函數的定義和聲明中,都可以省略形參名

void print (int ,int); void print(int a,int){ cout<<a<<endl; } void func(){ print(1,2); }

程序運行時的內存布局

代碼區:存放程序的執行代碼(各個函數的代碼塊)

全局變量區:放程序的全局數據和靜態數據

堆區:存放程序的動態數據malloc free new delete

棧區:存放程序的局部數據(各個函數中的數據)

指針和引用 

指針運算符

“ * ”稱為指針運算符(間接訪問運算符),表示指針所指向的變量的值。一元運算符

“ & ”稱為取地址運算符,用來得到一個對象的地址。一元運算符。

數組名是個指針常量int *const p,指針自身的值是一個常量,不可改變

void 指針與NULL指針值

(1)void

  • void 指針(void *p) 空類型指針 不指向任何類型,僅僅是一個地址。
  • 不能進行指針運算,也不能進行間接引用。
  • 其他類型指針可以賦值給空類型指針
  • 空類型指針經顯示轉換后方可賦給其他指針。

(2)NULL

  • NULL是空指針,不指向任何地方。
  • 任何類型的指針都可以賦該值。

指針和常量

(1)使用const 說明常量:int const x=2; 或const int x=2;

常量定義時,應被初始化。 const int i;(錯誤)

 

 

構造函數初始化時必須采用初始化列表一共有幾種情況,

 

  1. 需要初始化的數據成員是對象(繼承時調用基類構造函數)
  2. 需要初始化const修飾的類成員
  3. 需要初始化引用成員數據
  4. 類中含有對象成員,但其類型中,沒有無參構造函數,必須在初始化列表中指明相應的有參構造函數。
  5. 基類中缺少無參構造函數,必須在初始化列表中指明基類的有參構造函數。

 

(2)指針常量: 在指針定義語句的指針名前加const, 表示指針本身是常量。 int a; int* const p=&a; 定義時必須初始化 指針值p不可以修改,指針指向的內容可以修改。 即p是常量,不可以作為左值進行運算,*p可以修改,p不可以修改

 int *const q=&c;    // 錯誤(不允許外界提供修改常量的漏洞)

(3)常量指針: 在指針的定義類型前加const,表示指向的對象是常量。 如const int *pint const *p; 均可。 以上定義表明,*p是常量,不能將*p作為左值進行操作。 定義指向常量的指針時,定義時不必須先初始化*p不可以修改,p可以修改

(4)指向常量的指針常量(常量指針常量) 形式: const int *const p=&a; 定義時必須初始化 p與*p都是常量。他們都不能作為左值進行操作

(5)常量的特殊用法:int f(int b) const;

(6)重載和const形參

void f(int* p); void f(const int* cp); //有效重載,是不是指向const對象
void f(int* p ); void f(int * const pc); //無效重載,重定義不能基於指針本身是否const 實現重載

(7)new和delete進行動態內存分配和釋放

int *p; p=new  int100);//動態分配一個整數並初始化
delete p; int *p;p=new  int[10]; //分配一個含有10個整數的整形數組   delete[ ] p; //刪除這個數組

使用new較之使用malloc()有以下的幾個優點:

  • new自動計算要分配類型的大小,不使用sizeof運算符,比較省事,可以避免錯誤。
  • 自動地返回正確的指針類型,不用進行強制指針類型轉換。
  • 可以用new對分配的對象進行初始化。
  • 不用使用頭文件聲明(malloc.h),更簡潔。

new操作符若申請成功,返回首單元地址;否則返回NULL值。

函數指針和指針函數

(1)指針函數:返回指針值的函數。

int* f(int a);

(2)函數指針:指向函數地址(程序區)的指針。與函數名等價的指針。函數名是指向函數的指針常量

//類型 (*變量名) (形參表); //()的優先級大於*
int f1(int n);  int (*pf1)(int n);  pf1=f1; //pf1是與f1等價的函數指針

(3)通過函數指針來調用函數:

int add(int a,int b){return a+b;}  int main()    {    int (*p)(int,int);    p=add;                        //p是與add等價的函數指針 
    cout<<add(3,5);   cout<<(*p)(3,5);     //四種調用形式效果等價
    cout<<p(3,5);    cout<<(*add)(3,5); return 0;   }   //結果:8888 

(4)函數指針作函數形參:

例:計算以0.10為步長,特定范圍內的三角函數之和。

#include <iostream>
using namespace std; int sum(int a, int b){ return a + b; } int sub(int a, int b){ return a - b; } int get(int (*p)(int a, int b), int m, int n){ return p(m, n); } int main(){ cout << "sum=" << get(sum, 3, 3) << endl; cout << "sub=" << get(sub, 3, 3) << endl; return 0; } //sum=6 //sub=0

(5)用typedef 來簡化函數指針

typedef int (*FUN)(int a,int b); int f(int,int); FUN funp=f; //FUN 不是指針變量,是指針類型名。
typedef  int FUNC(int,int); //先定義函數類型
FUNC* funp=f;     

例子:

#include <iostream>
using namespace std; int sum(int a, int b){ return a + b; } int sub(int a, int b){ return a - b; } int get(int (*p)(int a, int b), int m, int n){ return p(m, n); } typedef int (*Func)(int a, int b); typedef int Func2(int a, int b); int main(){ Func func = sum; Func2 *func2 = sub; cout << "sum=" << get(func, 3, 3) << endl; cout << "sub=" << get(func2, 3, 3) << endl; return 0; }

(6)函數的返回類型可以是函數指針

 typedef int (*SIG) (); typedef void (*SIGARG) (); SIG signal (int,SIGARG);

引用

(1)為一個變量、函數等對象規定一個別名,該別名稱為引用。

(2)聲明引用:

 int& ir=i;            //定義引用ir作為對象i的別名

聲明引用,不為之分配內存空間。

(3)引用必須初始化。引用一旦被聲明則不能再修改. 

int j,k; int &s=j;     int &s=k;  //(錯誤)
int & i;                  //錯誤
extern int&  r3           //ok,r3在別處初始化
void &a=3;//error //void 本質不是一個類型,只是在語法上相當於一個類型,沒有該類型的對象。

(4)形參和實參結合規則:  

形參為引用時,凡遇到形參(引用)的地方,全部用實參(對象)來代替。

可讀性比指針傳遞好(與傳值方式的調用可讀性相同,性能卻強於傳值方式)

可使用引用傳遞從函數返回多個值(指針和引用都可以)

(5)引用和指針的關系 

指針是個變量,可再賦值; 而引用建立時必須進行初始化並且決不會再關聯其它不同的變量。

指針操縱兩個實體(指針值、指向的值);引用只能操縱一個實體。

引用在內部用指針實現 ,被看成是指針常量,不能操作自身的地址值,只能訪問所指向的實體。

 

實際上“引用”可以做的任何事情“指針”也都能夠做,為什么還要“引用”?

答案是:“用適當的工具做恰如其分的工作”。 指針能夠毫無約束地操作內存中的東西,盡管指針功能強大,但是非常危險。 引用是指針出於安全考慮的替代品。 高級編程多用引用,低級編程多用指針,也是基於安全考慮。

 

在以下情況下你應該使用指針:

一是你考慮到存在不指向任何對象的可能(在這種情況下,你能夠設置指針為空)

二是你需要能夠在不同的時刻指向不同的對象(在這種情況下,你能改變指針的指向)。

如果總是指向一個對象並且一旦指向一個對象后就不會改變指向,那么你應該使用引用。

(6)用const 限定引用

int  i;   const int& p=i;

不能通過引用對目標變量的值進行修改,保證了引用的安全性。

注意:c++不分變量的const引用,和const變量的引用,因為引用本身就不能重新賦值,使它指向另一個變量。 即沒有const int const &a=1, 只有const int &a=1

(7)引用的使用-用引用返回值

float& f2(float r){ t=3.14*r*r; return(t); }

用引用返回一個函數值的最大好處是:在內存中不產生被返回值的副本

別返回一個局部對象的引用

(8)引用的使用-函數調用作為左值

練習:

char  a[ ]="abcdefghij"; char *q=a; int *p=(int *)a; while(*q) *q++=*q+1;  //++運算符優先於* 等價於*(q++)=*q+1;  *a=bcdefghijk
p+=2; //char的大小是1字節,int的大小是4字節,p移動了8字節
printf(
"%s",p); //sizeof(char)=1 //sizeof(int)=4
//答案是jk

 類

從結構到類

C++采用“類”來支持對象,同類對象實體抽象出其共性,形成類(數據類型),類封裝了數據與處理數據的過程(函數)

C++的類中既能包含數據成員,又包含函數成員或稱成員函數

一般而言,類中的數據成員在定義類的時候是不能初始化的。

class class_name_identifier { public:         //訪問控制符
 公共數據成員聲明語句; 公共函數成員聲明語句; private: 私有數據成員聲明語句; 私有函數成員聲明語句; protected : 保護數據成員聲明語句; 保護函數成員聲明語句; };  

C++中類與結構的區別: 默認情況下,class定義的成員是private的,而struct 定義的成員是public的。

對象的引用以及返回對象的函數

void add (Student &b){ //函數體
} Student get(){ //函數體 
 Student tmp;
 return tmp
}

公有&保護&私有成員

訪問權限 類內成員函數或者數據成員 派生類 類外 友元函數
public yes yes yes yes
protected yes yes no yes
private yes no no yes

成員函數&非成員函數

成員函數屬於類,成員函數定義是類設計的一部分, 其作用域是類作用域.,而普通函數一般為全局函數

成員函數的操作主體是對象,使用時通過捆綁對象來行使其職責,,而普通函數被調用時沒有操作主體

重載成員函數

在類內重載成員函數與普通函數重載一樣

定義成員函數

(1)在類的內部定義成員函數

class A{ private: int a; public: void show(){ cout << "haha" << endl; } };

::是作用域區分符、作用域運算符、名字空間引導符

單獨用::表示全局變量和函數

#include <iostream>
using namespace std; int month = 10; void show(){ cout << "show" << endl; } class A{ public: void show(){ ::show(); //調用全局函數
 } }; int main(){ int month = 5; cout << "局部month=" << month << endl; cout << "全局month=" << ::month << endl; A a; a.show(); return 0; }

(2)在類之后定義成員函數

class A{ private: int a; public: void show(); }; void A::show(){ cout << "haha" << endl; }

程序結構

(1)文件結構

1、一般情況下, c++的類定義和成員函數定義分離

 

< >:用於c++提供的頭文件,一般存放在c++系統目錄中的include 子目錄下

“ ”:首先在當前文件所在的目錄中進行搜索,找不到,再按標准方式進行搜索.

2、類定義和使用的分離

成員函數的實現和類外定義不要一起放在頭文件中

外部鏈接:函數定義,可以在其他文件中使用。不能重復定義。

內部鏈接:只能在自己的程序中使用,可以跨文件重復定義。如:類型定義、全局常量、inline函數、模板定義等。

3、頭文件衛士

Student.h文件

#ifndef STUDENT #define STUDENT
class Student{ public: void p(){ cout<<score; } float score; protected: char name; int age; }; #endif

頭文件衛士保證頭文件在一個程序文件中只被定義一次

(2)c++程序中的函數的組織方式

頭文件的使用:使函數調用免於聲明

// a1.h a1.cpp提供的資源
void f1(); // a2.h  a2.cpp提供的資源
void p(); // a3.h  a3.cpp提供的資源
void g1(); void g2(); void f2(); void h(); // s.cpp
#include”a2.h” #include”a3.h” void s() { if(…){ p(); g1(); }else{ g2(); h(); } }

 

成員函數的顯式內聯

class Date { int year, month, day; void  set ( int y, int m, int d ) {        // 默認內聯
        year=y; month=m; day=d; } bool isLeapYear ( ); }; inline bool  Date::isLeapYear ( )  // 顯式內聯
{     return ! ( year%400 ) || !(year%4) &&( year%100); } //類體外定義一目了然 增強可讀性 //必須將類定義和成員函數的定義都放在同一個頭文件或者同一個源文件中,否則編譯時無法置換。

this 指針

this是個指針常量

this是對當前對象的引用

this 指針 是一個隱含於每一個類的成員函數中的特殊指針(包括析構函數和構造函數),它用於指向正在被成員函數操作的對象。

程序中可顯式使用this指針

#include <iostream>
using namespace std; class A{ public: int age = 0; void add(){ this->age++; } A &p(){ return *this; } }; int main(){ A a; (a.p().age)++; cout << a.age; return 0; }    //1

類的封裝

首先是數據與算法(操作)結合,構成一個不可分割的整體。

其次是在這個整體中一些成員是保護的,他們被有效地屏蔽,以防外界的干擾和誤操作。

另一些成員是公共的,他們作為接口提供給外界使用。

保護(私有)成員protected(private):

(1)保護類的內部數據不被肆意侵犯

(2)是類對它本身內部實現的維護負責,因為只有類自己才能訪問該類的保護數據,所以對一切保護數據的維護只有靠類自己。

(3)限制類與外部世界的接口。 保護成員對使用者不可見。

(4)減少類與其它代碼的關聯程度。類的功能是獨立的,不依賴於應用程序的運行環境。

公共成員(公共接口、對外接口)(public):

需要讓外界調用的成員函數指定為公共的,外界通過公共的函數來實現對數據的操作。

屏蔽類的內部實現

對類內所有的private修飾的數據提供get和set接口給外界,避免外界對數據直接進行操作。在java里叫javabean類

class A{ private: int age = 0; public: int getAge(){ return this->age; } void setAge(int age){ this->age=age; } };(2)類的作用域

作用域

(1)變量的作用域-{}內或其后的全部內容

(2)類的作用域

包括類定義作用域類實現作用域

類定義作用域:一個類的成員函數對同一類的數據成員具有無限制的訪問權。 私有成員和受保護成員只能被類內部的成員函數訪問; 公有成員是類提供給外部的接口, 可以在類外部被訪問. 這種技術實現了信息的隱藏和封裝.

類的實現作用域:通過類的對象體現,對象有全局對象、局部對象等作用范圍。

可見性:

  • 當內層的變量和外層的變量同名時,在內層里,外層的變量暫時地失去了可見性。
  • 不能在同一作用范圍內有同名變量

類名允許與其它變量名或函數名相同

1)如果一個非類名隱藏了類名,則類名通過加前綴即可

class Sample {//…..};
 void fun(int Sample) { class Sample s;  //定義一個對象要加前綴
 Sample++; //…}

 2)如果一個類名隱藏了非類名,則用一般作用域規則即可

int s=0; void func() { class s{//…};
 s a: ::s=3;}  // class作用域到此結束
 int g=s; //在函數中定義的類稱為局部類,局部類的成員函數必須在類定義內部定義

生命期(生存期)

指一個對象產生后,存活時間的度量。

在生存期內,對象保持它的狀態。

作用域與生命期不盡相同。

整個程序的生命期:全局(靜態)數據

靜態生命期:靜態局部數據

局部生命期:

動態生命期:由new申請到內存空間之后,該空間實體開始有效,一直到delete釋放該內存空間

名字空間

名字空間的作用是建立一些互相分隔的作用域,把一些全局實體分隔開來,以免產生名字沖突。

避免不同人編寫的程序組合時會遇到名字沖突的危險

可以包含:變量&對象;常量;函數定義; 類型(結構)定義;名字空間的定義

不能包含:預處理命令

可以開放定義;可以{}外定義

#include <iostream>
using namespace std; namespace name { int a = 20; void show() { cout << "I'm a namespace" << endl; } namespace another { int b = 30; } } int main() { cout << name::a << endl; name::show(); cout << name::another::b << endl; return 0; }
//20
//I'm a namespace
//30

標准c++庫的所有標識符都是在一個名為std的名字空間定義的。

using namespace std; //在std中定義和聲明所有標識符都可以作為全局量來使用。

面向對象基本概念

對象:物理實體在計算機邏輯中的映射和體現

類:同種對象的集合與抽象

實體:現實世界中需要描述的對象

面向對象:

就是力圖從實際問題中抽象出封裝了數據和操作的 對象,通過定義對象的各種屬性來描述他們的特征和功能,通過接口的定義描述他們的地位及與其它對象的關系,最終形成一個廣泛聯系的可理解、 可擴充、可維護、更接近於問題本來面目的動態對象模型系統。

OOA:面向對象分析(干什么)

OOD:面向對象設計(怎樣干)

OOP:面向對象編程(實現)

構造、析構函數

構造、析構函數

構造函數:

  1. 構造函數是特殊的類成員函數。
  2. C++規定與類同名的成員函數是構造函數,在該類的對象創建時,自動被調用
  3. 構造函數負責對象的初始化 可擁有多個參數。
  4. 可以重載
  5. 構造函數不返回具體的值,不指定函數的返回類型。
  6. 可內置定義,也可以在類外定義。
  7. 通常是Public

析構函數:

  1. 析構函數也是一個特殊的成員函數;
  2. 作用與構造函數相反;
  3. 在對象的生命期結束時自動被調用
  4. 名字是類名前加一個~
  5. 不返回任何值;沒有函數類型,沒有函數參數,因此不能被重載
  6. 一個類可以由多個構造函數,只能有一個析構函數
  7. 可內置定義,也可以在類外定義。
  8. 用於清理和釋放資源的工作。
  9. 通常是Public的
  10. 析構函數並不是銷毀對象的,只是釋放構造函數在構造時初始化的資源(主要包括堆上分配內存等)
#include <iostream>
using namespace std; class A{ private: int a; public: A(){ cout << "Default constructor called" << endl; } A(int a){ this->a = a; cout << "Constructor called" << endl; } ~A(){ cout << "Destructor called" << endl; } }; int main(){ A a; A b(1); return 0; }

//Default constructor called
//Constructor called
//Destructor called
//Destructor called

組合類

#include <iostream>
using namespace std; class Student{ public: Student(){ cout << "Student constructor called" << endl; } ~Student(){ cout << "Student destructor called" << endl; } }; class Teacher{ public: Teacher(){ cout << "Teacher construtor called" << endl; } ~Teacher(){ cout << "Teacher destrctor called" << endl; } }; class Manager{ private: Student student; Teacher teacher; public: Manager(){ cout << "Manager constructor called" << endl; } ~Manager(){ cout << "Manager destructor called" << endl; } }; int main(){ Manager manager; return 0; }

//Student constructor called
//Teacher construtor called
//Manager constructor called
//Manager destructor called
//Teacher destrctor called
//Student destructor called

//注意執行順序,組合類的對象成員先初始化,調用構造函數,然后是本類初始化,調用構造函數,析構順序相反
#include <iostream>
using namespace std; class Student{ public: Student(){ cout << "Student constructor called" << endl; } ~Student(){ cout << "Student destructor called" << endl; } }; class Teacher{ public: Teacher(){ cout << "Teacher construtor called" << endl; } ~Teacher(){ cout << "Teacher destrctor called" << endl; } }; class Temp{ public: Temp(){ cout << "Temp constructor called" << endl; } ~Temp(){ cout << "Temp destructor called" << endl; } }; class Manager : public Temp{ private: Student student; Teacher teacher; public: Manager(){ cout << "Manager constructor called" << endl; } ~Manager(){ cout << "Manager destructor called" << endl; } }; int main(){ Manager manager; return 0; }

//Temp constructor called
//Student constructor called
//Teacher construtor called
//Manager constructor called
//Manager destructor called
//Teacher destrctor called
//Student destructor called
//Temp destructor called

當一個類既是組合類又是派生類,它在創建對象時,系統對構造函數的調用順序有相應的規定:

最先調用基類的構造函數,初始化基類的數據成員;

然后調用子對象所在類的構造函數,初始化子對象的數據成員;

最后調用本類的構造函數,初始化新增數據成員。

#include <iostream>
using namespace std; class Student{ public: Student(){ cout << "Student constructor called" << endl; } ~Student(){ cout << "Student destructor called" << endl; } }; class Teacher{ public: Teacher(){ cout << "Teacher construtor called" << endl; } ~Teacher(){ cout << "Teacher destrctor called" << endl; } }; class Temp { public: Temp(){ cout << "Temp constructor called" << endl; } ~Temp(){ cout << "Temp destructor called" << endl; } }; class Temp2{ public: Temp2(){ cout << "Temp2 constructor called" << endl; } ~Temp2(){ cout << "Temp2 destructor called" << endl; } }; class Manager{ private: Student student; Teacher teacher; public: Manager(){ Temp temp;  //局部對象Temp Temp2 temp2; //局部對象Temp2 cout << "Manager constructor called" << endl; } ~Manager(){ cout << "Manager destructor called" << endl; } }; int main(){ Manager manager; return 0; } /* Student constructor called Teacher construtor called Temp constructor called Temp2 constructor called Manager constructor called Temp2 destructor called Temp destructor called Manager destructor called Teacher destrctor called Student destructor called */

如果類內有局部臨時對象:

則先初始化類內子對象,再初始化局部臨時對象,最后初始化本類對象。

析構時先調用局部臨時對象的析構函數,且多個局部臨時對象的構造和析構順序相反

 

引用應依附於另一個獨立的變量,等待初始化。

常量數據成員是僅僅初始化一次,其值便不再改變的數據成員。

二者只能借助冒號語法初始化。

#include <iostream>
using namespace std; class A { private: const int a;  //常數據成員 int &b; //引用 public: A(int a, int b) : a(a), b(b) { cout << "A constructor called" << endl; }
 /*A(int a,int b){ //錯誤
    this->a=a;
    this->b=b;
 }*/ };
int main() { A a(10, 20); return 0; }

對象構造順序

局部和靜態局部對象(靜態生命期)以文本定義順序為順序 (類成員屬於此種情況)

靜態對象在首次定義時構造一次;程序結束析構

全局對象在main之前構造;程序結束時析構

全局對象如果分布在不同文件中,則構造順序隨機

常對象和常成員函數

常對象:數據成員值在對象的整個生存期間內不能被改變。

即常對象定義是必須進行初始化,而且不能被更改。

聲明的語法形式: const 類名 對象名 或者 類名 const 對象名

#include <iostreamusing namespace std; class A{ private: const int a; int &b; public: A(int a, int b) : a(a), b(b){} void show() const; //構成重載
  void show()        //如果不是常對象,則優先調用普通函數
 { cout << "normal show" << endl; } }; void A::show() const //類外實現常成員函數時不可省略const
{ cout << "const show" << endl; } int main(){ A a(10, 20); a.show(); //normal show
 const A b(10,20);  b.show(); //const show
return 0; }

沒有常構造函數和常析構函數,常對象和普通對象都調用同一構造函數和析構函數。

為對象申請動態空間

int *p=new int; delete p; int *q=new int[10]; delete []q;

函數內部的申請空間要及時釋放,否則容易造成內存重復申請和內存迷失(內存泄漏)

對象數組不能通過參數傳遞初始化。要么默認構造函數,要么構造函數有默認參數。

拷貝構造函數

一個已知對象構造(初始化)另一對象

類名 (類名& 形參); 或者是 類名 ( const 類名& 形參);

Student (Student& s);或者是Student ( const Student& s);

一旦提供了拷貝構造函數,就不在提供默認構造函數

#include <iostream>
using namespace std; class B{ private: int a; public: B(const B &b) {} }; int main(){ B b; return 0; } //no matching function for call to 'B::B()'

淺拷貝:

創建q時, 對象p被復制給了q, 但資源未復制, 使得p和q指向 同一個資源, 這稱為淺拷貝。

可能會破壞該堆及自由內存表

深拷貝:

復制指針的同時,開辟同樣的空間,把空間的數據復制,讓指針分別指向各自的內存空間

class aa { public: aa(){f=new char[10];} aa(aa const & s){ f=new   char[10];  //開辟空間
  strcpy(f,s.f);} //數據復制
~aa(){delete [ ]f;} char * f; }; int main() {aa p; strcpy(p.f,"Computer"); cout<<p.f<<endl; aa q(p); cout<<q.f<<endl; strcpy(p.f,”Software”); cout<<p.f<<q.f<<endl; return 0;}

 

如果派生類調用拷貝構造函數且基類含有拷貝構造函數,則基類拷貝構造函數也會被調用

#include <iostream>
using namespace std; class Data { public: Data(int x = 0) { Data::x = x; cout << "Data()" << endl; } Data(const Data &) { cout << "Data(const Data&)" << endl; } ~Data() { cout << "~Data()" << endl; } private: int x; }; class Base { public: Base() { cout << "Base()" << endl; } Base(const Base &) { cout << "Base(const Base&)" << endl; } virtual ~Base() {} }; class Child : public Base { public: Child() { cout << "Child()" << endl; } virtual ~Child() { cout << "~Child()" << endl; } private: Data d2; }; int main() { Child c1; Child c2(c1); return 0; } /* Base() Data() Child() Base(const Base&) Data(const Data&) ~Child() ~Data() ~Child() ~Data() */

 

關於無名對象的構造與使用:

構造函數:Student(int i){ }

 Student(9);// 無名對象 ,正確
  Student *p=&Student(8);//錯誤,error: taking address of temporary
  Student const &q=Student(7);//正確,無名對象將在它的引用離開作用域時銷毀
  Student i(6); i = Student(5);//正確
 Student &q=Student(5); //錯誤cannot bind non-const lvalue reference of type 'Student&' to an rvalue of type 'Student'

構造函數用於類型轉換

class aa { public: aa(int a=1){id=a;cout<<“構造”<<endl;} aa (aa & s){id = s.id;cout<<“拷貝”;} int & Getid(){return(id);} private: int id; }; int main() { aa m; //“構造”
   aa n(m);     //“拷貝”
   aa o = m;    //“拷貝” 和aa o(m)等價
   aa s = 9;    //“構造” 和aa s(9)等價
   aa t;        //“構造”
   t = 9;       // t=aa(9);“構造”、 賦值運算
   return 0; } //隱式的類型轉換,可以將其他類型轉換成類類型

轉換不能太復雜,不允許多參數,不允許間接轉換

class aa { public: aa(int a=1){id=a;} int id; }; class bb { public: bb(int a=2){id=a;} int id; }; void m(aa a) {cout<<a.id<<endl;} void m(bb a) {cout<<a.id<<endl;} int main() { m(9);  //存在二義性; m(bb(9));
     return 0; }//轉換不能太復雜,不允許多參數,不允許間接轉換

explicit可以禁止隱式類型轉換

explicit A(int a){this->a=a} A a=3;   //錯誤error: conversion from 'int' to non-scalar type 'A' requested
A a(3);   //正確

 友元

(1)聲明的位置既可在public區,也可在protected區。友元函數雖然是在類內進行聲明,但它不是該類的成員函數,不屬於任何類

(2)在類外定義友元函數,與普通函數的定義一樣,一般與類的成員函數放在一起,以便類重用時,一起提供其友元函數。

(3)友元函數是能訪問類的所有成員的普通函數,一個函數可以是多個類的友元函數,只需在各個類中分別聲明。

(4)友元能使程序精煉,提高程序的效率。

(5)友元破壞了類的封裝,使用時,要權衡利弊,在數據共享與信息隱藏之間選擇一個平衡點。

(6)友元類的所有成員函數都可視為該類的友元函數,能存取該類的私有成員和保護成員。

(7)友元關系不具有對稱性。 友元關系不具有傳遞性。如果類B是類A的友元類,類C是類B的友元類,這並不隱含類C是類A的友元類

#include <iostream> #include <string>
using namespace std; class Building; //友元類
class MyFriend { public: //友元成員函數
  void LookAtBedRoom(Building &building); void PlayInBedRoom(Building &building); }; class Building { //全局函數做友元函數
  friend void CleanBedRoom(Building &building); #if 1
  //成員函數做友元函數
  friend void MyFriend::LookAtBedRoom(Building &building); friend void MyFriend::PlayInBedRoom(Building &building); #else
  //友元類
  friend class MyFriend; #endif
public: Building(); public: string mSittingRoom; private: string mBedroom; }; void MyFriend::LookAtBedRoom(Building &building) { cout << "我的朋友參觀" << building.mBedroom << endl; } void MyFriend::PlayInBedRoom(Building &building) { cout << "我的朋友玩耍在" << building.mBedroom << endl; } //友元全局函數
void CleanBedRoom(Building &building) { cout << "友元全局函數訪問" << building.mBedroom << endl; } Building::Building() { this->mSittingRoom = "客廳"; this->mBedroom = "卧室"; } int main() { Building building; MyFriend myfriend; CleanBedRoom(building); myfriend.LookAtBedRoom(building); myfriend.PlayInBedRoom(building); system("pause"); return EXIT_SUCCESS; } /* 友元全局函數訪問卧室 我的朋友參觀卧室 我的朋友玩耍在卧室 */

靜態成員

前面沒有static ,類外初始化

int StudentID::ID=0;

(1) 不管一個類的對象有多少個,其靜態數據成員也只有一個,由這些對象所共享,可被任何一個對象所訪問。

(2) 在一個類的對象空間內,不包含靜態成員的空間,所以靜態成員所占空間不會隨着對象的產生而分配,或隨着對象的消失而回收。

(3) 靜態數據成員的存儲空間的分配是在程序一開始運行時就被分配。並不是在程序運行過程中在某一函數內分配空間和初始化。

(4) 靜態數據成員的初始化語句,既不屬於任何類,也不屬於包括主函數在內的任何函數,靜態數據成員初始化語句最好在類的實現部分定義

(5) 對於在類的public部分說明的靜態數據成員,可以不使用成員函數而直接訪問,既使未定義類的對象,同樣也可以直接訪問,但在使用時也必須用類名指明所屬的類。 而private和protected部分的靜態成員只能通過類的成員函數訪問。

(6) 不允許不指明對象訪問非靜態數據成員;不允許使用this

(7)可以用const限定靜態成員嗎?

可以限定形態數據成員,不可以限定靜態成員函數

class A{ public: const static int a; static void getA() const{ //編譯報錯 return a; } }; //靜態成員函數上不允許使用類型限定符
class A{ private: int t = 10; public: const static int a; static int getT(){ return t; } }; //error: invalid use of member 'A::t' in static member function非靜態成員引用必須與特定對象相對

單例模式

懶漢式:

#include <iostream>
using namespace std; class A{ private: A() {} //構造函數私有化 A(const A &a) {} static A *instance; public: void show(){ cout << "hello" << endl; } static A *getInstance(){ if (instance == NULL){ instance = new A; } return instance; } }; A *A::instance = NULL;  //靜態數據成員類外初始化 int main(){ A *instance = A::getInstance(); instance->show(); return 0; } //如果寫A a會報錯

運算符重載

格式

使用運算符重載的一般格式為:

類型名 operator 運算符(形參表) {函數體}

其中operator是關鍵字,類型名為重載運算符的返回類型,即運算結果類型。

(1)在C++中幾乎所有的運算符( 除“.”(成員選擇符)、“.*”(成員對象選擇符)、“->*(成員指針選擇符)”、“::”、“?:”、“size of”外)都可以被重載。 只能重載         C++中已有的運算符,不允許創建新的運算符.

(2) 運算符的重載既不會改變原運算符的優先級和結合性。

(3) 至少有一個操作對象是自定義類型,參數都是基本類型時不能重載.

(4) 不提倡改變參數個數、運算符含義。

(5) 重載運算符的函數不能有默認的參數。

(6) 運算符重載時參數個數不可以超過原來數目

 

賦值運算符重載

拷貝構造函數和賦值操作符都是用來拷貝一個類的對象給另一個同類型的對象。

void fn(A a) { A na=a; //拷貝構造函數:將一個對象拷貝到另一個新對象
    A b; 
   b=a; } //賦值運算符:將一個對象為另一個已經存在的對象賦值

編譯器提供默認的拷貝構造函數和賦值運算符的運作機制。

就是將對象中的每個數據成員拷貝到目標對象相應的數據成員中。

若類的數據成員中有指向動態分配空間的指針,通常定義拷貝構造函數,此時,應重載賦值運算符。

實現深拷貝

 

類外顯式調用賦值函數

#include <iostream>
using namespace std; class A { public: int num; A(int n) : num(n) {} }; int main() { A a(3); A b(0);  b.operator=(a); cout << b.num; return 0; } //3

 

++/--運算符重載

 

 

 

 

類型轉換運算符

類型轉換函數的作用是將一個類的對象轉換成另一類型的數據

 

類型轉換運算符聲明的形式:

operator 類型名();

沒有參數,沒有返回類型,(其返回類型由函數名字指定)但是函數體中必須包含return語句。

只能作為成員函數。

同一個類中不能定義多個轉換運算符重載函數.

#include <iostream>
using namespace std; class aa{ float a; float b; public: float &aaa() { return (a); } float &bbb() { return (b); } operator float(); }; aa::operator float() { return (a); } int main(){ aa a, b; a.aaa() = 1; a.bbb() = 2; b.aaa() = 3; b.bbb() = 4;              //int(3.5)
  cout << float(a) << endl; //a.operator float();
  cout << 10 + a << endl; cout << a + b << endl; cout << a << endl; }

類和友元

重載<<運算符

class Person{ friend ostream& operator<<(ostream& os, Person& person); public: Person(int id,int age){ mID = id; mAge = age; } private: int mID; int mAge; }; ostream& operator<<(ostream& os, Person& person){ os << "ID:" << person.mID << " Age:" << person.mAge; return os; } //全局函數友元
class Person { public: Person(int id, int age) { mID = id; mAge = age; } ostream &operator<<(ostream &os) { os << "ID:" << mID << " Age:" << mAge; return os; } private: int mID; int mAge; }; //另一種:在類內重載<<,不需友元

繼承

class 派生類名:繼承方式 基類名 { 派生類中的新成員 } 基類(父類)、派生類(子類)
class Master :public Student

派生類繼承了基類的除了構造函數、析構函數、拷貝構造函數和賦值運算符重載函數之外的所有成員,因此派生類對象由兩部分組成:一部分是由基類繼承的成員,另一部分是派生類新增加的自己特有的成員。

 

當類的繼承方式為公有繼承時,基類的公有和保護成員的訪問屬性在派生類中不變,而基類的私有成員不可訪問。

直接基類:直接參與派生出某類的基類。間接基類:基類的基類,甚至更高層的基類。

 

繼承的本質實際上就是由上到下完全的復制;但是在對內可見性上做了手腳,對外可見性則沒有改變。

c++提供了類的繼承機制,解決了軟件的復用問題。

 

賦值兼容規則是指需要基類對象的任何地方都可以使用公有派生類的對象來替代。替代之后,派生類對象就可以作為基類的對象使用,但只能使用從基類繼承的成員。

 

里氏代換原則: (LSP-Liskov Substitution Principle) 在軟件里面,把基類都替換成它的子類,程序的行為沒有變化。

 

使用時還應注意:基類指針指向派生類對象時,只能通過基類指針訪問派生類中從基類繼承來的成員,不能訪問派生類中的其它成員。

 

不允許將基類的對象賦值給派生類的對象

 

當類的繼承方式為保護繼承時,基類的公有和保護成員都以保護成員身份出現在派生類中,而基類的私有成員不可訪問。

當類的繼承方式為私有繼承時,基類的公有和保護成員都以私有成員身份出現在派生類中,而基類的私有成員不可訪問。

 

protected繼承和private繼承得到的類都不是子類 “凡是父類對象可以出現的地方可以用子類對象代替”,不再適用

#include <iostream>
using namespace std; class Parent{}; class Child : protected Parent{}; int main(){ Child child; Parent &parent = child; return 0; } //error: 'Parent' is an inaccessible base of 'Child' //不允許對不可訪問的基類 "Parent" 進行轉換

 

繼承類型省略時默認為私有繼承

 

派生類中初始化基類數據成員,不可以調用基類的構造函數,可以使用初始化成員列表

#include <iostream>
using namespace std; class Parent{ private: int a; public: Parent(int a){ this->a = a; } }; class Child : protected Parent{ public: Child(int a) : Parent(a) //正確
 { //Parent(a)錯誤no matching function for call to 'Parent::Parent()
 } }; int main(){ return 0; }

 

派生類構造函數執行的一般順序是:

(1)基類構造函數,

(2)派生類對象成員類的構造函數(如果有的話)。

(3)派生類構造函數體中的內容。

析構函數的執行順序相反

嵌套類

在一個類中定義的類稱為嵌套類,定義嵌套類的類稱為外圍類。定義嵌套類的目的在於隱藏類名,減少全局的標識符,從而限制用戶能否使用該類建立對象。這樣可以提高類的抽象能力,並且強調了兩個類(外圍類和嵌套類)之間的主從關系。 

class A{ int a; public: class B{ int b; public: B(int i); }; void f(){ pb->b=5; //error}
    B* pb; }; A::B::B(int i):b(i){}  //成員可以在體外定義
int main(){ A a; A::B b(10); }

嵌套類中說明的成員不是外圍類中對象的成員,反之亦然。

嵌套類的成員函數對外圍類的成員沒有訪問權,反之亦然。

嵌套類僅僅是語法上的嵌入。 在嵌套類中說明的友元對外圍類的成員沒有訪問權。

繼承和組合的關系

組合是“has a”關系的模型。 汽車=方向盤+輪子+車窗+……

繼承是"is a"關系模型。是為了產生子類型。讓開發人員設計“kind of”關系的模型。 鳥類->老鷹

組合和繼承不是絕對的。組合可以用繼承來實現,繼承也可以由組合來實現。

 

避免繼承帶來的重負:繼承是C++中第二緊密的耦合關系,僅次於友元關系。緊密的耦合是一種不良現象,應該盡量避免。因此,應該用組合代替繼承,除非知道后者確實對設計有好處。人們曾經過度地使用繼承,即使是有經驗的程序員也會如此。軟件工程的一條明智原則,就是盡量減少耦合:如果一種關系不只有一種表達方式,那么就 應該用最可行的最弱關系。考慮到繼承關系幾乎是C++中所能表達的最強關系,因此只有在沒有更弱的等價代替選擇時,才適合使用。如果用組合就能表示類的關系,那么應該優先使用。

所以更加嚴格的繼承規則應當是:若在邏輯上B是A的“一種”,並且A的所有功能和屬性對B而言都有意義,則允許B繼承A的功能和屬性。

多重繼承和虛基類

若一個派生類具有兩個或兩個以上基類,這種繼承稱為多重繼承。

class M_p:public Master,public Phd{}

多繼承派生類構造函數執行順序是:

(1)    所有基類的構造函數;多個基類構造函數的執行順序取決於定義派生類時所指定的順序,與派生類構造函數中所定義的成員初始化列表的參數順序無關。

(2)    對象成員(如果有的話)的構造函數;

(3)    派生類本身構造函數的函數代碼。

 

虛基類

格式如下:

class 派生類名:virtual public 基類名 { //聲明派生類成員 };

這時,從不同的路徑繼承過來的同名數據成員在內存中就只有一個拷貝,同一個函數名也只有一個映射。

當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次,即基類成員只保留一次

 

C++規定,虛基類子對象是由最后派生類的構造函數通過調用虛基類的構造函數進行初始化的。

如果一個派生類有一個直接或間接的虛基類,那么派生類的構造函數的成員初始化列表中必須列出虛基類構造函數的調用.

如果沒有列出,則表示使用該虛基類的缺省構造函數來初始化派生類對象中的虛基類子對象。

 

多繼承的構造順序:

(1)任何虛擬基類的構造函數按照他們被繼承的順序構造

(2)任何非虛擬基類的構造函數按照他們被繼承的順序構造

(3)任何成員對象的構造函數按照他們被聲明的順序構造

(4)類自己的構造函數

 

例子:

(1)如果不用虛繼承,用多繼承:

#include <iostream>
using namespace std; class Grandfather { public: Grandfather() { cout << "Grandfather constructor called" << endl; } protected:
  int a = 0; void fun() {} }; class FatherA : public Grandfather { public: FatherA() { cout << "FatherA constructor called" << endl; } void show() { cout << "FatherA" << endl; } }; class FatherB : public Grandfather { public: FatherB() { cout << "FatherB constructor called" << endl; } void show() { cout << "FatherB" << endl; } }; class Son : public FatherA, public FatherB { public: Son() { cout << "Son constructor called" << endl; } void show() { cout << "Son" << endl; } }; int main() { // Grandfather grandfather; // FatherA fatherA; // FatherB fatherB;
 Son son; cout << "sizeof(Son)=" << sizeof(son) << endl; // cout << "sizeof(Grandfather)=" << sizeof(grandfather) << endl; // cout << "sizeof(fatherA)=" << sizeof(fatherA) << endl; // cout << "sizeof(fatherB)=" << sizeof(fatherB) << endl;
  return 0; } /* Grandfather constructor called //間接基類構造函數調用了兩次 FatherA constructor called Grandfather constructor called FatherB constructor called Son constructor called sizeof(Son)=8 //Son類中繼承了兩個間接基類的數據成員 */

(2)使用虛繼承#include <iostream>using namespace std;

class Grandfather { public: Grandfather() { cout << "Grandfather constructor called" << endl; } protected: int a = 0; void fun() {} }; class FatherA : virtual public Grandfather { public: FatherA() { cout << "FatherA constructor called" << endl; } void show() { cout << "FatherA" << endl; } }; class FatherB : virtual public Grandfather //虛繼承 { public: FatherB() { cout << "FatherB constructor called" << endl; } void show() { cout << "FatherB" << endl; } }; class Son : public FatherA, public FatherB //虛繼承 { public: Son() { cout << "Son constructor called" << endl; } void show() { cout << "Son" << endl; } }; int main() { // Grandfather grandfather; // FatherA fatherA; // FatherB fatherB;  Son son; cout << "sizeof(Son)=" << sizeof(son) << endl; // cout << "sizeof(Grandfather)=" << sizeof(grandfather) << endl; // cout << "sizeof(fatherA)=" << sizeof(fatherA) << endl; // cout << "sizeof(fatherB)=" << sizeof(fatherB) << endl; return 0; } /* Grandfather constructor called //間接基類構造函數只調用了一次 FatherA constructor called FatherB constructor called Son constructor called sizeof(Son)=24 //注意這里Song的內存大小,(間接基類的int num占4,兩個直接基類的兩個虛函數表占8+8=16,然后根據字節對齊規則,總內存20擴大到8的
倍數也就是24
*/

多態

虛函數

虛函數的使用方法:

(1)在基類用virtual聲明成員函數為虛函數

(2)在派生類中重新定義此函數,要求函數名函數的參數個數類型全部與基類的虛函數相同,並根據派生類的需要重新定義函數體

(3)定義一個指向基類對象的指針變量,並使它指向同一類族中的某一對象。

(4)通過該指針變量調用此虛函數,此時調用的就是指針變量指向的對象的同名函數。 c++規定,當一個成員函數被聲明為虛函數后,其派生類中原型相同的函數都自動成為虛函數 。派生類沒有對基類的虛函數重新定義,則派生類繼承其直接基類的虛函數。

 

類型轉換:

static_cast:靜態轉型,必須是相關類型,非多態類層次的祖孫互易,void*轉換任何類型

dynamic_cast:動態轉型,專門針對有虛函數的繼承結構,將基類指針或引用轉換成想要的子類指針或引用

const_cast:常量轉型,去掉常量性的轉換。

使用:

dynamic_cast<Derived*>(p); static_cast<Car*>(p); const char* max(const char*,const char*); char* p=const_cast<char*>(max(“hello”,”good”));

 

虛函數的實用意義:從基類繼承來的某些成員函數不完全適應派生類的需要,允許其派生類中對該函數重新定義,賦予它新的功能,當基類的這些成員函數聲明為虛函數后,可以通過指向其基類的指針指向同一類族中不同類的對象,從而調用其同名的函數。

由虛函數實現的多態性是:同一類族中不同類的對象,對同一函數調用作出不同的響應。

 

多態的實現:

聯編(編聯、束定、綁定)(binding):就是把一個標識符名和一個存儲地址聯系在一起的過程。將一個函數調用鏈接上相應於函數體的代碼,這一過程就是函數聯編。

靜態聯編:出現在運行前的聯編(在編譯時完成),也稱為早期聯編。

動態聯編:聯編工作在程序運行階段完成的情況。在編譯、連接過程中無法解決的聯編問題,要等到程序開始運行之后再來確定。 也稱為滯后聯編。

 

虛函數的工作機理:

Virtual出現則每個類增加一個虛函數表保存類的虛函數 。凡有虛函數的類均維護一個虛函數表 ,實例化每個對象中會增加一個指針指向虛函數表(對象大小會有變化).。虛函數調用時不需要確定對象類型,通過該指針即可找到所要鏈接函數

 

多態注意事項:

非成員、靜態成員、內聯函數不能是虛函數

構造函數、賦值運算符函數不能是虛函數 

析構函數經常定義成虛函數 delete p;

 

多態實現深拷貝的例子:

class Student{ int move_tel; public:   int id; virtual Student * n(){ return(new Student(*this));} }; class Master :public Student{ string topic; public:  //方法略
 Student * n(){ return(new Master(*this));} }; class Phd:public Student{ string professor; public:  //方法略
Student * n(){ return(new Phd(*this)); } 
};
class DoubleMaster :public Student{ string SecondMajor; public: //方法略 Student * n(){ return(new DoubleMaster (*this));} }; class Member{ Student * m[128]; public: Member( ){ } Member(Member &s){ for(int i=0;i<128;i++) this->m[i] = s.m[i]->n();} Student * &f(int k) {return(m[k]);} }; void main() {Member s; s.f(0)=new Student; s.f(1)=new Master; s.f(2)=new Phd; s.f(3)=new DoubleMaster; s.f(4)=new Phd; Member t(s); } //定義了一個t,並完全拷貝s內容

 

抽象類

純虛函數:在基類中只聲明虛函數而不給出具體的函數定義體,稱此虛函數為純虛函數。

純虛函數的聲明如下: (注:要放在基類的定義體中)

  virtual 函數原型=0;

聲明了純虛函數的類,稱為抽象類(抽象基類)

通過該基類的指針引用就可以調用所有派生類的虛函數,基類的純虛函數只是用於繼承,僅作為一個接口,具體功能在派生類中實現。

 

使用純虛函數時應注意:

(1)抽象類中可以有多個純虛函數。

(2)抽象類也可以定義其他非純虛函數

(3)從抽象類可以派生出具體或抽象類,但不能從具體類派生出抽象類

問題:抽象類需要有構造函數和析構函數嗎?

 

【答】雖然抽象類不能實例化,但是抽象類被繼承之后,它的派生類可以實例化;而派生類在實例化調用構造函數的時候會先調用基類中的構造函數,所以抽象類的構造函數也是可以被調用的,所以抽象類中可以有構造函數。但是注意:C++核心准則C.126:抽象類通常不需要構造函數‍,因為抽象類通常不包含任何需要構造函數初始化的數據。

 

抽象類通常代表一個抽象的概念,它提供一個繼承的出發點。 在一個復雜的類繼承結構中,越上層的類抽象程度越高,有時甚至無法給出某些成員函數的實現,顯然,抽象類是一種特殊的類,它一般處於類繼承結構的較外層。 引入抽象類的目的,主要是為了能將相關類組織在一個類繼承結構中,並通過抽象類來為這些相關類提供統一的操作接口,更好的發揮多態性。

類的六種關系

一、縱向關系:(耦合關系相同)

1、繼承(is-a) 虎是一種動物

2、實現接口(is like a)(接口:是對行為的抽象) 飛機和鳥都會飛

二、橫向關系:(耦合關系漸弱)

1、合成、組合(is a part of)(強擁有,嚴格的部分整體) 鳥和翅膀;生命周期同步。 形式:成員

2、聚合(own a)(弱擁有,A可以包含B,但B不是A的一部分) 大雁和雁群,群體和個體,生命周期不同步 形式:成員,一般為容器

3、關聯(has a) 人有朋友,不是包含關系。企鵝和氣候的關系 形式:成員

4、依賴(use a)(運行期關系) 動物需要呼吸氧氣 形式:局部變量、方法的參數或者對靜態方法的調用

 

其關系強弱(耦合度)為 依賴<關聯<聚合<組合<繼承<實現<繼承

 

能使用組合或聚合就不要使用繼承(降低耦合度)

 

關聯和聚合的區別:主要在語義上,關聯的兩個對象之間一般是平等的,例如你是我的朋友,聚合則一般不是平等的,例如一個公司包含了很多員工,其實現上是差不多的。聚合和組合的區別則在語義和實現上都有差別,組合的兩個對象之間其生命期有很大的關聯,被組合的對象是在組合對象創建的同時或者創建之后創建,在組合對象銷毀之前銷毀。一般來說被組合對象不能脫離組合對象獨立存在,而且也只能屬於一個組合對象,例如一個文檔的版本,必須依賴於文檔的存在,也只能屬於一個文檔。聚合則不一樣,被聚合的對象可以屬於多個聚合對象,例如一個員工可能可以屬於多個公司。

 

我想舉個通俗的例子。
你和你的心臟之間是composition組合關系(心臟只屬於自己)
你和你買的書之間是aggregation聚合關系(書可能是別人的)
你和你的朋友之間是association關聯關系

組合和聚合的區別

聚合關系(Aggregation)體現的是A對象可以包含B對象,但B對象不是A對象的組成部分。具體表現為,如果A由B聚合成,表現為A包含有B的全局對象,但是B對象可以不在A創建的時刻創建。

組合關系(Composition):如果A由B組成,表現為A包含有B的全局對象,並且B對象在A創建的時刻創建。


現實生活中,人和人和手,腳是組合關系,因為當人死亡后人的手也就不復存在了。人和他的電腦是聚合關系

組合實例:

public class Person { private Hand hand; public Person() { hand=new Hand(); } private void go(){ hand.hand(); } public static void main(String[] args) { new Person().go();    //person消亡時,hand也消亡
 } } class Hand{ public void hand(){ System.out.print("hand"); } }

聚合實例:

public class Person2 { private Computer c; public Person2(){ } public Person2(Computer c){ this.c=c; } public void go(){ c.computer(); } public static void main(String[] args) { Computer computer=new Computer(); Person2 person = new Person2(computer); person.go(); //person消亡時,不影響computer
 } } class Computer{ public void computer(){ System.out.print("computer"); } }

 

設計原則

“高內聚低耦合”

1、單一職責原則  

2、開放封閉原則

3、依賴倒轉原則

4、里氏替換原則

5、合成聚合復用原則

6、迪米特法則(最少知識原則)

7、接口隔離原則

終結類

前提:不通過關鍵詞final

思路:構造函數私有則該類不能用來繼承,則為終結類

方法一:靜態成員方法

#include <iostream>
using namespace std; class AA{ private: AA() {} public: static AA *GetAAObj() { return new AA; } //static AA GetAAObj(){ return AA();}
  static void DeleteAAObj(AA *pa) { delete pa; } void show(){ cout << "hehe" << endl; } protected: int _aa; }; class BB : public AA{ }; int main(){ AA::GetAAObj()->show(); //BB b;錯誤:無法引用 "BB" 的默認構造函數 -- 它是已刪除的函數 //note: 'BB::BB()' is implicitly deleted because the default definition would be ill-formed: // error: 'AA::AA()' is private within this context
  return 0; }

方法二:虛擬繼承

(1)典型方法

把基類的構造函數先設為protected,然后在派生類中變為private,使基類不可在被繼承

虛擬繼承的核心:一個基類如果被虛擬繼承,那么在創建它的孫子類的對象時,該基類的構造函數需要單獨被調用。此時,如果該基類的構造函數在孫子類的構造函數中無法訪問,那么就實現了基類的子類不能被繼承。基類 FinalParent,它不定義任何數據成員,這樣任何類從它派生並不會增加任何空間上的開銷。將它的默認構造函數的訪問權限設定為 protected,這樣它自身不能產生任何實例,只能用作基類。

當 FinalClassChild 試圖繼承 FinalClass 的時候,FinalClassChild 的構造函數中需要調用 FinalParent 的構造函數,而 FinalParent 的構造函數在 FinalClass 中已經變成了私有 private,不能被 FinalClassChild 的任何成員函數所訪問,導致編譯錯誤。所以,任何一個類,只要虛擬繼承類 FinalParent,就不能被繼承,從而簡單、高效、安全地實現了終結類。

#include <iostream>
using namespace std; class FinalParent{ protected: FinalParent() {} }; class FinalClass :private virtual FinalParent{ public: FinalClass() {} //其他略,類要實現的功能或者定義的數據成員可以寫在這里
}; class A :public FinalClass{}; int main(){ FinalClass f; A a; //
return 0; } //[Note] 'A::A()' is implicitly deleted because the default definition would be ill-formed: //[Error] 'FinalParent::FinalParent()' is protected

(2)

把基類構造函數設為private,派生類設為友元類,可以調用基類構造,而派生類的派生類無法調用基類構造函數

#include <iostream>
using namespace std; class Vb {
private: Vb() {} //構造函數私有
public: friend class Student; //Student為友元類,可以調用基類構造函數 void show(){ cout<<"hello"<<endl; } }; class Student :virtual public Vb { public: Student(){} }; class A :public Student {}; //A無法調用間接基類的構造函數 int main() { Student s; s.show(); A a; // return 0; }

final和override關鍵字

1、類被final修飾,不能被繼承

class A1 final { }; class B1 : A1 { }; // “B1”: 無法從“A1”繼承,因為它已被聲明為“final”

2、虛函數被final修飾,不能被override

class A1{ virtual void func() final {} }; class B1 : A1{ virtual void func() {} //“A1::func”: 聲明為“final”的函數無法被“B1::func”重寫
};

3、被override修飾后如果父類無對應的虛函數則報錯。override就是編譯器輔助你檢查是否繼承了想要虛繼承的函數

struct A1{    virtual void func(int) {}}; struct B1 : A1{    virtual void func(int) override {} // OK
                        virtual void func(double) override {} //// “B1::func”: 包含重寫說明符“override”的方法沒有重寫任何基類方法
};

模板

 

定義函數模板使用保留字template,

定義格式如下:

template < 模板參數表 >

返回類型 函數名( 函數形式參數表 ) { ... }

模板參數表中的參數可以有多個,可以是類型形參,也可以是表達式形參。多個參數間用逗號間隔。

模板參數若是代表一個類型,模板類型參數形式如下:

class 類型參數名 (或 typename 類型參數名)

函數模板的定義可以看作由兩部分組成, 一是模板類型的定義,template<class 模板形參表>。 二是函數定義,它與普通函數的定義類似。 函數模板只是對函數的描述,編譯系統不為其產生任何執行代碼。

template <class T>      //class可以用typename代替
void swap1(T& a,T& b){ T temp=a; a=b; b=temp; }//函數模板
swap1<int>(a,b);//顯式
    swap1(a,b);//隱式

 

C++ 編譯器在遇到調用用模板方式定義的函數時,會根據調用函數的參數類型構造出一個個具體的函數。這個過程稱為函數模板的實例化(instantiation)。

同一個模板生成的不同的模板函數是不同名字的函數,不是重載函數。函數模板反映的是不同函數的函數族。使用模板實現了代碼重用。

 

模板參數表中的參數可以有多個,多個參數間用逗號間隔。參數值可默認

template<typename T,typename U>  //模板類型聲明不能共享;typename不能節省template<classname T,U> 錯誤
void add(T a,U b) { cout<<a+b<<endl;} void main(){ int x=1,y=2; float s=3.0,t=4.0; add(x,y); add(s,t); add(x,t); add(s,y);//以上四條語句均正確;因為T和U //可以相同也可以不相同
  add<float,int>(s,y); add<float>(s,y); add<,int>(s,y);//錯誤程序員可以僅提供部分模板實參,其余的模板實參仍由編譯器自動推導。且省略掉的實參必須是模板參數表尾部的參數對應的實參。
}

 

優先匹配非模板函數

#include <iostream>
using namespace std; template <class T>
void swap(T &a, T &b){ T tmp = a; a = b; b = tmp; cout << "template function called" << endl; } void swap(int &a, int &b){ int tmp = a; a = b; b = tmp; cout << "non-template function called" << endl; } int main(){ int a = 1, b = 2; swap(a, b); cout << "a=" << a << " b=" << b; return 0; } //non-template function called //a=2 b=1

 

模板的參數是非類型參數

模板非類型參數:則形參的類型是某種具體的數據類型

模板非類型參數表示該參數名代表了一個潛在的值,而該值代表了模板定義中的一個常量。

模板非類型參數被用作一個常量值出現在模板定義的余下部分。 它可以用在要求常量的地方,如 數組聲明中指定數組的大小或作為枚舉常量的初始值.

template <class T, int i>
int find(T a[],T k) { for(int j=0;j<i;j++) if(a[j]==k) return(j); return(-1); } void main(){ int a[10]={0}; cout<<find<int,10>(a,1); }

 

類 模板

 

template <typename T=int>//類模板也允許默認參數
class Stack{ public: void push( T ); T pop( ); private: T *head; }; //與普通類一樣,類模板的成員函數也可以在類模板定義中定義具體實現,這時,該函數是內聯的(inline)。

 

類模板可以有多個類型參數

template <typename T,typename U> //多個類型參數
 class List{… …}; List <NODE,Student>  k;

I/O流

C++語言系統為實現數據的輸入和輸出定義了一個龐大的類庫,它包括的類主要有:

ios:抽象基類

iostream:輸入流類istream,輸出流類ostream,輸入輸出流類iostream;

     對標准輸入設備和標准輸出設備的輸入輸出,簡稱為標准I/O流

fstream:輸入文件流類ifstream,輸出文件流類ofstream,輸入輸出文件流類fstream;

      對在外存磁盤上文件的輸入輸出,簡稱為文件I/O流

Strstream:輸入字符串流類istrstream,輸出字符串流類ostrstream,輸入輸出字符串流類strstream.

sstream:輸入字符串流類istringstream,輸出字符串流類ostringstream,輸入輸出字符串流類stringstream.

     對內存中指定的空間進行輸入輸出。通常指定字符數組string類對象做為存儲空間的輸入輸出,簡稱為串I/O流

 

 

異常處理

(1)框定異常(try語句塊) 將那些有可能產生錯誤的語句放在try塊中

(2)拋擲異常(throw語句) 檢測是否產生異常,若是,則拋擲異常。

(3)捕捉異常,定義異常處理(catch語句塊)將異常處理語句放在catch塊中,以便異常被傳遞過來時就處理它。

C++只理會受監控的異常。 Try塊之后必須緊跟一個或多個catch語句。 Catch括號中只能容納一個形參,與throw拋擲的異常類型匹配時,便捕獲了異常。 避免把正常邏輯淹沒在錯誤處理代碼中,從而使程序更易於閱讀。

 

 

 

異常捕捉的類型匹配之苛刻程度可以和模板的類型匹配媲美,它不允許相容類型的隱式轉換。

對於沒有捕捉到的異常,Abort()進程被調用,從而無條件的中止程序的執行。

 

異常也可以拋出類:

 

class A{ char net[20]; public: virtual  void x(){cout<<"網絡錯誤";} }; class B:public A{ long card; public: void x(){cout<<"網卡錯誤";} }; class C:public B{ int port; public: void x(){cout<<"端口錯誤";} }; void net()         //void net() throw(A,B,C)
{  throw B();} --------------------------------------------------
void net() throw(A,B,C);  //異常申述(異常說明) 聲明和定義要一致
void main() { try{net();} catch(A & s){s.x();}   }

 

如果在函數的聲明中沒有包括異常接口聲明,則此函數可以拋擲任何類型的異常,例如: void net( );

一個不拋擲任何類型異常的函數可以進行如下形式的聲明: void net( ) throw();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM