概述
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);
void fun(int a = 20) { cout << "你好" << endl; } int main() { fun(); return 0; }
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;(錯誤)
(2)指標常量: 在指標定義語句的指標名前加const, 表示指標本身是常量。 int a; int* const p=&a; 定義時必須初始化 指標值p不可以修改,指標指向的內容可以修改。 即p是常量,不可以作為左值進行運算,*p可以修改,p不可以修改
int *const q=&c; // 錯誤(不允許外界提供修改常量的漏洞)
(3)常量指標: 在指標的定義型別前加const,表示指向的物件是常量。 如const int *p 或 int 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 int (100);//動態分配一個整數並初始化
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指標
#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:物件導向程式設計(實現)
構造、解構函式
構造、解構函式
建構函式:
- 建構函式是特殊的類成員函式。
- C++規定與類同名的成員函式是建構函式,在該類的物件建立時,自動被呼叫。
- 建構函式負責物件的初始化 可擁有多個引數。
- 可以過載。
- 建構函式不返回具體的值,不指定函式的返回型別。
- 可內建定義,也可以在類外定義。
- 通常是Public的
解構函式:
- 解構函式也是一個特殊的成員函式;
- 作用與建構函式相反;
- 在物件的生命期結束時自動被呼叫。
- 名字是類名前加一個~;
- 不返回任何值;沒有函式型別,沒有函式引數,因此不能被過載。
- 一個類可以由多個建構函式,只能有一個解構函式。
- 可內建定義,也可以在類外定義。
- 用於清理和釋放資源的工作。
- 通常是Public的
#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;}
關於無名物件的構造與使用:
建構函式: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;
} //賦值運算子:將一個物件為另一個已經存在的物件賦值
編譯器提供預設的拷貝建構函式和賦值運算子的運作機制。
就是將物件中的每個資料成員拷貝到目標物件相應的資料成員中。
若類的資料成員中有指向動態分配空間的指標,通常定義拷貝建構函式,此時,應過載賦值運算子。
實現深拷貝
++/--運算子過載
型別轉換運算子
型別轉換函式的作用是將一個類的物件轉換成另一型別的資料
型別轉換運算子宣告的形式:
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)(執行期關係) 動物需要呼吸氧氣 形式:區域性變數、方法的引數或者對靜態方法的呼叫
設計原則
“高內聚低耦合”
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();