C++核心程式設計
本階段主要針對C++面向對象程式設計技術做詳細講解,探讨C++中的核心和精髓。
1 記憶體分區模型
C++程式在執行時,将記憶體大方向劃分為4個區域
- 代碼區:存放函數體的二進制代碼,由作業系統進行管理的
- 全局區:存放全局變量和靜态變量以及常量
- 棧區:由編譯器自動配置設定釋放, 存放函數的參數值,局部變量等
- 堆區:由程式員配置設定和釋放,若程式員不釋放,程式結束時由作業系統回收
記憶體四區意義:
不同區域存放的資料,賦予不同的生命周期, 給我們更大的靈活程式設計
1.1 程式運作前
在程式編譯後,生成了exe可執行程式,未執行該程式前分為兩個區域
代碼區:
存放 CPU 執行的機器指令
代碼區是共享的,共享的目的是對于頻繁被執行的程式,隻需要在記憶體中有一份代碼即可
代碼區是隻讀的,使其隻讀的原因是防止程式意外地修改了它的指令
全局區:
全局變量和靜态變量存放在此.
全局區還包含了常量區, 字元串常量和其他常量也存放在此.
該區域的資料在程式結束後由作業系統釋放.
示例:
//全局變量
int g_a = 10;
int g_b = 10;
//全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//局部變量
int a = 10;
int b = 10;
//列印位址
cout << "局部變量a位址為: " << (int)&a << endl;
cout << "局部變量b位址為: " << (int)&b << endl;
cout << "全局變量g_a位址為: " << (int)&g_a << endl;
cout << "全局變量g_b位址為: " << (int)&g_b << endl;
//靜态變量
static int s_a = 10;
static int s_b = 10;
cout << "靜态變量s_a位址為: " << (int)&s_a << endl;
cout << "靜态變量s_b位址為: " << (int)&s_b << endl;
cout << "字元串常量位址為: " << (int)&"hello world" << endl;
cout << "字元串常量位址為: " << (int)&"hello world1" << endl;
cout << "全局常量c_g_a位址為: " << (int)&c_g_a << endl;
cout << "全局常量c_g_b位址為: " << (int)&c_g_b << endl;
const int c_l_a = 10;
const int c_l_b = 10;
cout << "局部常量c_l_a位址為: " << (int)&c_l_a << endl;
cout << "局部常量c_l_b位址為: " << (int)&c_l_b << endl;
system("pause");
return 0;
}
總結:
- C++中在程式運作前分為全局區和代碼區
- 代碼區特點是共享和隻讀
- 全局區中存放全局變量、靜态變量、常量
- 常量區中存放 const修飾的全局常量 和 字元串常量
1.2 程式運作後
棧區:
由編譯器自動配置設定釋放, 存放函數的參數值,局部變量等
注意事項:不要傳回局部變量的位址,棧區開辟的資料由編譯器自動釋放
示例:
int * func()
{
int a = 10;
return &a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
堆區:
由程式員配置設定釋放,若程式員不釋放,程式結束時由作業系統回收
在C++中主要利用new在堆區開辟記憶體
示例:
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
總結:
堆區資料由程式員管理開辟和釋放
堆區資料利用new關鍵字進行開辟記憶體
1.3 new操作符
C++中利用new操作符在堆區開辟資料
堆區開辟的資料,由程式員手動開辟,手動釋放,釋放利用操作符 delete
文法:
new 資料類型
利用new建立的資料,會傳回該資料對應的類型的指針
示例1: 基本文法
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
//利用delete釋放堆區資料
delete p;
//cout << *p << endl; //報錯,釋放的空間不可通路
system("pause");
return 0;
}
示例2:開辟數組
//堆區開辟數組
int main() {
int* arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//釋放數組 delete 後加 []
delete[] arr;
system("pause");
return 0;
}
2 引用
2.1 引用的基本使用
**作用: **給變量起别名
文法:
資料類型 &别名 = 原名
示例:
int main() {
int a = 10;
int &b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
2.2 引用注意事項
- 引用必須初始化
- 引用在初始化後,不可以改變
示例:
int main() {
int a = 10;
int b = 20;
//int &c; //錯誤,引用必須初始化
int &c = a; //一旦初始化後,就不可以更改
c = b; //這是指派操作,不是更改引用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
system("pause");
return 0;
}
2.3 引用做函數參數
**作用:**函數傳參時,可以利用引用的技術讓形參修飾實參
**優點:**可以簡化指針修改實參
示例:
//1. 值傳遞
void mySwap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//2. 位址傳遞
void mySwap02(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//3. 引用傳遞
void mySwap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
mySwap01(a, b);
cout << "a:" << a << " b:" << b << endl;
mySwap02(&a, &b);
cout << "a:" << a << " b:" << b << endl;
mySwap03(a, b);
cout << "a:" << a << " b:" << b << endl;
system("pause");
return 0;
}
總結:通過引用參數産生的效果同按位址傳遞是一樣的。引用的文法更清楚簡單
2.4 引用做函數傳回值
作用:引用是可以作為函數的傳回值存在的
注意:不要傳回局部變量引用
用法:函數調用作為左值
示例:
//傳回局部變量引用int& test01() { int a = 10; //局部變量 return a;}//傳回靜态變量引用int& test02() { static int a = 20; return a;}int main() { //不能傳回局部變量的引用 int& ref = test01(); cout << "ref = " << ref << endl; cout << "ref = " << ref << endl; //如果函數做左值,那麼必須傳回引用 int& ref2 = test02(); cout << "ref2 = " << ref2 << endl; cout << "ref2 = " << ref2 << endl; test02() = 1000; cout << "ref2 = " << ref2 << endl; cout << "ref2 = " << ref2 << endl; system("pause"); return 0;}
2.5 引用的本質
本質:引用的本質在c++内部實作是一個指針常量.
講解示例:
//發現是引用,轉換為 int* const ref = &a;void func(int& ref){ ref = 100; // ref是引用,轉換為*ref = 100}int main(){ int a = 10; //自動轉換為 int* const ref = &a; 指針常量是指針指向不可改,也說明為什麼引用不可更改 int& ref = a; ref = 20; //内部發現ref是引用,自動幫我們轉換為: *ref = 20; cout << "a:" << a << endl; cout << "ref:" << ref << endl; func(a); return 0;}
結論:C++推薦用引用技術,因為文法友善,引用本質是指針常量,但是所有的指針操作編譯器都幫我們做了
2.6 常量引用
**作用:**常量引用主要用來修飾形參,防止誤操作
在函數形參清單中,可以加const修飾形參,防止形參改變實參
示例:
//引用使用的場景,通常用來修飾形參void showValue(const int& v) { //v += 10; cout << v << endl;}int main() { //int& ref = 10; 引用本身需要一個合法的記憶體空間,是以這行錯誤 //加入const就可以了,編譯器優化代碼,int temp = 10; const int& ref = temp; const int& ref = 10; //ref = 100; //加入const後不可以修改變量 cout << ref << endl; //函數中利用常量引用防止誤操作修改實參 int a = 10; showValue(a); system("pause"); return 0;}
3 函數提高
3.1 函數預設參數
在C++中,函數的形參清單中的形參是可以有預設值的。
文法:
傳回值類型 函數名 (參數= 預設值){}
示例:
int func(int a, int b = 10, int c = 10) { return a + b + c;}//1. 如果某個位置參數有預設值,那麼從這個位置往後,從左向右,必須都要有預設值//2. 如果函數聲明有預設值,函數實作的時候就不能有預設參數int func2(int a = 10, int b = 10);int func2(int a, int b) { return a + b;}int main() { cout << "ret = " << func(20, 20) << endl; cout << "ret = " << func(100) << endl; system("pause"); return 0;}
3.2 函數占位參數
C++中函數的形參清單裡可以有占位參數,用來做占位,調用函數時必須填補該位置
文法:
傳回值類型 函數名 (資料類型){}
在現階段函數的占位參數存在意義不大,但是後面的課程中會用到該技術
示例:
//函數占位參數 ,占位參數也可以有預設參數void func(int a, int) { cout << "this is func" << endl;}int main() { func(10,10); //占位參數必須填補 system("pause"); return 0;}
3.3 函數重載
3.3.1 函數重載概述
**作用:**函數名可以相同,提高複用性
函數重載滿足條件:
- 同一個作用域下
- 函數名稱相同
- 函數參數類型不同或者個數不同或者順序不同
注意: 函數的傳回值不可以作為函數重載的條件
示例:
//函數重載需要函數都在同一個作用域下void func(){ cout << "func 的調用!" << endl;}void func(int a){ cout << "func (int a) 的調用!" << endl;}void func(double a){ cout << "func (double a)的調用!" << endl;}void func(int a ,double b){ cout << "func (int a ,double b) 的調用!" << endl;}void func(double a ,int b){ cout << "func (double a ,int b)的調用!" << endl;}//函數傳回值不可以作為函數重載條件//int func(double a, int b)//{// cout << "func (double a ,int b)的調用!" << endl;//}int main() { func(); func(10); func(3.14); func(10,3.14); func(3.14 , 10); system("pause"); return 0;}
3.3.2 函數重載注意事項
- 引用作為重載條件
- 函數重載碰到函數預設參數
示例:
//函數重載注意事項//1、引用作為重載條件void func(int &a){ cout << "func (int &a) 調用 " << endl;}void func(const int &a){ cout << "func (const int &a) 調用 " << endl;}//2、函數重載碰到函數預設參數void func2(int a, int b = 10){ cout << "func2(int a, int b = 10) 調用" << endl;}void func2(int a){ cout << "func2(int a) 調用" << endl;}int main() { int a = 10; func(a); //調用無const func(10);//調用有const //func2(10); //碰到預設參數産生歧義,需要避免 system("pause"); return 0;}
4 類和對象
C++面向對象的三大特性為:封裝、繼承、多态
C++認為萬事萬物都皆為對象,對象上有其屬性和行為
例如:
人可以作為對象,屬性有姓名、年齡、身高、體重…,行為有走、跑、跳、吃飯、唱歌…
車也可以作為對象,屬性有輪胎、方向盤、車燈…,行為有載人、放音樂、放空調…
具有相同性質的對象,我們可以抽象稱為類,人屬于人類,車屬于車類
4.1 封裝
4.1.1 封裝的意義
封裝是C++面向對象三大特性之一
封裝的意義:
- 将屬性和行為作為一個整體,表現生活中的事物
- 将屬性和行為加以權限控制
封裝意義一:
在設計類的時候,屬性和行為寫在一起,表現事物
文法:
class 類名{ 通路權限: 屬性 / 行為 };
**示例1:**設計一個圓類,求圓的周長
示例代碼:
//圓周率const double PI = 3.14;//1、封裝的意義//将屬性和行為作為一個整體,用來表現生活中的事物//封裝一個圓類,求圓的周長//class代表設計一個類,後面跟着的是類名class Circle{public: //通路權限 公共的權限 //屬性 int m_r;//半徑 //行為 //擷取到圓的周長 double calculateZC() { //2 * pi * r //擷取圓的周長 return 2 * PI * m_r; }};int main() { //通過圓類,建立圓的對象 // c1就是一個具體的圓 Circle c1; c1.m_r = 10; //給圓對象的半徑 進行指派操作 //2 * pi * 10 = = 62.8 cout << "圓的周長為: " << c1.calculateZC() << endl; system("pause"); return 0;}
**示例2:**設計一個學生類,屬性有姓名和學号,可以給姓名和學号指派,可以顯示學生的姓名和學号
示例2代碼:
//學生類class Student {public: void setName(string name) { m_name = name; } void setID(int id) { m_id = id; } void showStudent() { cout << "name:" << m_name << " ID:" << m_id << endl; }public: string m_name; int m_id;};int main() { Student stu; stu.setName("德瑪西亞"); stu.setID(250); stu.showStudent(); system("pause"); return 0;}
封裝意義二:
類在設計時,可以把屬性和行為放在不同的權限下,加以控制
通路權限有三種:
- public 公共權限
- protected 保護權限
- private 私有權限
示例:
//三種權限//公共權限 public 類内可以通路 類外可以通路//保護權限 protected 類内可以通路 類外不可以通路//私有權限 private 類内可以通路 類外不可以通路class Person{ //姓名 公共權限public: string m_Name; //汽車 保護權限protected: string m_Car; //銀行卡密碼 私有權限private: int m_Password;public: void func() { m_Name = "張三"; m_Car = "拖拉機"; m_Password = 123456; }};int main() { Person p; p.m_Name = "李四"; //p.m_Car = "奔馳"; //保護權限類外通路不到 //p.m_Password = 123; //私有權限類外通路不到 system("pause"); return 0;}
4.1.2 struct和class差別
在C++中 struct和class唯一的差別就在于 預設的通路權限不同
差別:
- struct 預設權限為公共
- class 預設權限為私有
class C1{ int m_A; //預設是私有權限};struct C2{ int m_A; //預設是公共權限};int main() { C1 c1; c1.m_A = 10; //錯誤,通路權限是私有 C2 c2; c2.m_A = 10; //正确,通路權限是公共 system("pause"); return 0;}
4.1.3 成員屬性設定為私有
**優點1:**将所有成員屬性設定為私有,可以自己控制讀寫權限
**優點2:**對于寫權限,我們可以檢測資料的有效性
示例:
class Person {public: //姓名設定可讀可寫 void setName(string name) { m_Name = name; } string getName() { return m_Name; } //擷取年齡 int getAge() { return m_Age; } //設定年齡 void setAge(int age) { if (age < 0 || age > 150) { cout << "你個老妖精!" << endl; return; } m_Age = age; } //情人設定為隻寫 void setLover(string lover) { m_Lover = lover; }private: string m_Name; //可讀可寫 姓名 int m_Age; //隻讀 年齡 string m_Lover; //隻寫 情人};int main() { Person p; //姓名設定 p.setName("張三"); cout << "姓名: " << p.getName() << endl; //年齡設定 p.setAge(50); cout << "年齡: " << p.getAge() << endl; //情人設定 p.setLover("蒼井"); //cout << "情人: " << p.m_Lover << endl; //隻寫屬性,不可以讀取 system("pause"); return 0;}
練習案例1:設計立方體類
設計立方體類(Cube)
求出立方體的面積和體積
分别用全局函數和成員函數判斷兩個立方體是否相等。
4.2 對象的初始化和清理
- 生活中我們買的電子産品都基本會有出廠設定,在某一天我們不用時候也會删除一些自己資訊資料保證安全
- C++中的面向對象來源于生活,每個對象也都會有初始設定以及 對象銷毀前的清理資料的設定。
4.2.1 構造函數和析構函數
對象的初始化和清理也是兩個非常重要的安全問題
一個對象或者變量沒有初始狀态,對其使用後果是未知
同樣的使用完一個對象或變量,沒有及時清理,也會造成一定的安全問題
c++利用了構造函數和析構函數解決上述問題,這兩個函數将會被編譯器自動調用,完成對象初始化和清理工作。
對象的初始化和清理工作是編譯器強制要我們做的事情,是以如果我們不提供構造和析構,編譯器會提供
編譯器提供的構造函數和析構函數是空實作。
- 構造函數:主要作用在于建立對象時為對象的成員屬性指派,構造函數由編譯器自動調用,無須手動調用。
- 析構函數:主要作用在于對象銷毀前系統自動調用,執行一些清理工作。
構造函數文法:
類名(){}
- 構造函數,沒有傳回值也不寫void
- 函數名稱與類名相同
- 構造函數可以有參數,是以可以發生重載
- 程式在調用對象時候會自動調用構造,無須手動調用,而且隻會調用一次
析構函數文法:
~類名(){}
- 析構函數,沒有傳回值也不寫void
- 函數名稱與類名相同,在名稱前加上符号 ~
- 析構函數不可以有參數,是以不可以發生重載
- 程式在對象銷毀前會自動調用析構,無須手動調用,而且隻會調用一次
class Person{public: //構造函數 Person() { cout << "Person的構造函數調用" << endl; } //析構函數 ~Person() { cout << "Person的析構函數調用" << endl; }};void test01(){ Person p;}int main() { test01(); system("pause"); return 0;}
4.2.2 構造函數的分類及調用
兩種分類方式:
按參數分為: 有參構造和無參構造
按類型分為: 普通構造和拷貝構造
三種調用方式:
括号法
顯示法
隐式轉換法
示例:
//1、構造函數分類// 按照參數分類分為 有參和無參構造 無參又稱為預設構造函數// 按照類型分類分為 普通構造和拷貝構造class Person {public: //無參(預設)構造函數 Person() { cout << "無參構造函數!" << endl; } //有參構造函數 Person(int a) { age = a; cout << "有參構造函數!" << endl; } //拷貝構造函數 Person(const Person& p) { age = p.age; cout << "拷貝構造函數!" << endl; } //析構函數 ~Person() { cout << "析構函數!" << endl; }public: int age;};//2、構造函數的調用//調用無參構造函數void test01() { Person p; //調用無參構造函數}//調用有參的構造函數void test02() { //2.1 括号法,常用 Person p1(10); //注意1:調用無參構造函數不能加括号,如果加了編譯器認為這是一個函數聲明 //Person p2(); //2.2 顯式法 Person p2 = Person(10); Person p3 = Person(p2); //Person(10)單獨寫就是匿名對象 目前行結束之後,馬上析構 //2.3 隐式轉換法 Person p4 = 10; // Person p4 = Person(10); Person p5 = p4; // Person p5 = Person(p4); //注意2:不能利用 拷貝構造函數 初始化匿名對象 編譯器認為是對象聲明 //Person p5(p4);}int main() { test01(); //test02(); system("pause"); return 0;}
4.2.3 拷貝構造函數調用時機
C++中拷貝構造函數調用時機通常有三種情況
- 使用一個已經建立完畢的對象來初始化一個新對象
- 值傳遞的方式給函數參數傳值
- 以值方式傳回局部對象
示例:
class Person {public: Person() { cout << "無參構造函數!" << endl; mAge = 0; } Person(int age) { cout << "有參構造函數!" << endl; mAge = age; } Person(const Person& p) { cout << "拷貝構造函數!" << endl; mAge = p.mAge; } //析構函數在釋放記憶體之前調用 ~Person() { cout << "析構函數!" << endl; }public: int mAge;};//1. 使用一個已經建立完畢的對象來初始化一個新對象void test01() { Person man(100); //p對象已經建立完畢 Person newman(man); //調用拷貝構造函數 Person newman2 = man; //拷貝構造 //Person newman3; //newman3 = man; //不是調用拷貝構造函數,指派操作}//2. 值傳遞的方式給函數參數傳值//相當于Person p1 = p;void doWork(Person p1) {}void test02() { Person p; //無參構造函數 doWork(p);}//3. 以值方式傳回局部對象Person doWork2(){ Person p1; cout << (int *)&p1 << endl; return p1;}void test03(){ Person p = doWork2(); cout << (int *)&p << endl;}int main() { //test01(); //test02(); test03(); system("pause"); return 0;}
4.2.4 構造函數調用規則
預設情況下,c++編譯器至少給一個類添加3個函數
1.預設構造函數(無參,函數體為空)
2.預設析構函數(無參,函數體為空)
3.預設拷貝構造函數,對屬性進行值拷貝
構造函數調用規則如下:
- 如果使用者定義有參構造函數,c++不在提供預設無參構造,但是會提供預設拷貝構造
- 如果使用者定義拷貝構造函數,c++不會再提供其他構造函數
示例:
class Person {public: //無參(預設)構造函數 Person() { cout << "無參構造函數!" << endl; } //有參構造函數 Person(int a) { age = a; cout << "有參構造函數!" << endl; } //拷貝構造函數 Person(const Person& p) { age = p.age; cout << "拷貝構造函數!" << endl; } //析構函數 ~Person() { cout << "析構函數!" << endl; }public: int age;};void test01(){ Person p1(18); //如果不寫拷貝構造,編譯器會自動添加拷貝構造,并且做淺拷貝操作 Person p2(p1); cout << "p2的年齡為: " << p2.age << endl;}void test02(){ //如果使用者提供有參構造,編譯器不會提供預設構造,會提供拷貝構造 Person p1; //此時如果使用者自己沒有提供預設構造,會出錯 Person p2(10); //使用者提供的有參 Person p3(p2); //此時如果使用者沒有提供拷貝構造,編譯器會提供 //如果使用者提供拷貝構造,編譯器不會提供其他構造函數 Person p4; //此時如果使用者自己沒有提供預設構造,會出錯 Person p5(10); //此時如果使用者自己沒有提供有參,會出錯 Person p6(p5); //使用者自己提供拷貝構造}int main() { test01(); system("pause"); return 0;}
4.2.5 深拷貝與淺拷貝
深淺拷貝是面試經典問題,也是常見的一個坑
淺拷貝:簡單的指派拷貝操作
深拷貝:在堆區重新申請空間,進行拷貝操作
示例:
class Person {public: //無參(預設)構造函數 Person() { cout << "無參構造函數!" << endl; } //有參構造函數 Person(int age ,int height) { cout << "有參構造函數!" << endl; m_age = age; m_height = new int(height); } //拷貝構造函數 Person(const Person& p) { cout << "拷貝構造函數!" << endl; //如果不利用深拷貝在堆區建立新記憶體,會導緻淺拷貝帶來的重複釋放堆區問題 m_age = p.m_age; m_height = new int(*p.m_height); } //析構函數 ~Person() { cout << "析構函數!" << endl; if (m_height != NULL) { delete m_height; } }public: int m_age; int* m_height;};void test01(){ Person p1(18, 180); Person p2(p1); cout << "p1的年齡: " << p1.m_age << " 身高: " << *p1.m_height << endl; cout << "p2的年齡: " << p2.m_age << " 身高: " << *p2.m_height << endl;}int main() { test01(); system("pause"); return 0;}
總結:如果屬性有在堆區開辟的,一定要自己提供拷貝構造函數,防止淺拷貝帶來的問題
4.2.6 初始化清單
作用:
C++提供了初始化清單文法,用來初始化屬性
文法:
構造函數():屬性1(值1),屬性2(值2)... {}
示例:
class Person {public: 傳統方式初始化 //Person(int a, int b, int c) { // m_A = a; // m_B = b; // m_C = c; //} //初始化清單方式初始化 Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {} void PrintPerson() { cout << "mA:" << m_A << endl; cout << "mB:" << m_B << endl; cout << "mC:" << m_C << endl; }private: int m_A; int m_B; int m_C;};int main() { Person p(1, 2, 3); p.PrintPerson(); system("pause"); return 0;}
4.2.7 類對象作為類成員
C++類中的成員可以是另一個類的對象,我們稱該成員為 對象成員
例如:
class A {}class B{ A a;}
B類中有對象A作為成員,A為對象成員
那麼當建立B對象時,A與B的構造和析構的順序是誰先誰後?
示例:
class Phone{public: Phone(string name) { m_PhoneName = name; cout << "Phone構造" << endl; } ~Phone() { cout << "Phone析構" << endl; } string m_PhoneName;};class Person{public: //初始化清單可以告訴編譯器調用哪一個構造函數 Person(string name, string pName) :m_Name(name), m_Phone(pName) { cout << "Person構造" << endl; } ~Person() { cout << "Person析構" << endl; } void playGame() { cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手機! " << endl; } string m_Name; Phone m_Phone;};void test01(){ //當類中成員是其他類對象時,我們稱該成員為 對象成員 //構造的順序是 :先調用對象成員的構造,再調用本類構造 //析構順序與構造相反 Person p("張三" , "蘋果X"); p.playGame();}int main() { test01(); system("pause"); return 0;}
4.2.8 靜态成員
靜态成員就是在成員變量和成員函數前加上關鍵字static,稱為靜态成員
靜态成員分為:
- 靜态成員變量
- 所有對象共享同一份資料
- 在編譯階段配置設定記憶體
- 類内聲明,類外初始化
- 靜态成員函數
- 所有對象共享同一個函數
- 靜态成員函數隻能通路靜态成員變量
**示例1 :**靜态成員變量
class Person{ public: static int m_A; //靜态成員變量 //靜态成員變量特點: //1 在編譯階段配置設定記憶體 //2 類内聲明,類外初始化 //3 所有對象共享同一份資料private: static int m_B; //靜态成員變量也是有通路權限的};int Person::m_A = 10;int Person::m_B = 10;void test01(){ //靜态成員變量兩種通路方式 //1、通過對象 Person p1; p1.m_A = 100; cout << "p1.m_A = " << p1.m_A << endl; Person p2; p2.m_A = 200; cout << "p1.m_A = " << p1.m_A << endl; //共享同一份資料 cout << "p2.m_A = " << p2.m_A << endl; //2、通過類名 cout << "m_A = " << Person::m_A << endl; //cout << "m_B = " << Person::m_B << endl; //私有權限通路不到}int main() { test01(); system("pause"); return 0;}
**示例2:**靜态成員函數
class Person{public: //靜态成員函數特點: //1 程式共享一個函數 //2 靜态成員函數隻能通路靜态成員變量 static void func() { cout << "func調用" << endl; m_A = 100; //m_B = 100; //錯誤,不可以通路非靜态成員變量 } static int m_A; //靜态成員變量 int m_B; // private: //靜态成員函數也是有通路權限的 static void func2() { cout << "func2調用" << endl; }};int Person::m_A = 10;void test01(){ //靜态成員變量兩種通路方式 //1、通過對象 Person p1; p1.func(); //2、通過類名 Person::func(); //Person::func2(); //私有權限通路不到}int main() { test01(); system("pause"); return 0;}
4.3 C++對象模型和this指針
4.3.1 成員變量和成員函數分開存儲
在C++中,類内的成員變量和成員函數分開存儲
隻有非靜态成員變量才屬于類的對象上
class Person {public: Person() { mA = 0; } //非靜态成員變量占對象空間 int mA; //靜态成員變量不占對象空間 static int mB; //函數也不占對象空間,所有函數共享一個函數執行個體 void func() { cout << "mA:" << this->mA << endl; } //靜态成員函數也不占對象空間 static void sfunc() { }};int main() { cout << sizeof(Person) << endl; system("pause"); return 0;}
4.3.2 this指針概念
通過4.3.1我們知道在C++中成員變量和成員函數是分開存儲的
每一個非靜态成員函數隻會誕生一份函數執行個體,也就是說多個同類型的對象會共用一塊代碼
那麼問題是:這一塊代碼是如何區分那個對象調用自己的呢?
c++通過提供特殊的對象指針,this指針,解決上述問題。this指針指向被調用的成員函數所屬的對象
this指針是隐含每一個非靜态成員函數内的一種指針
this指針不需要定義,直接使用即可
this指針的用途:
- 當形參和成員變量同名時,可用this指針來區分
- 在類的非靜态成員函數中傳回對象本身,可使用return *this
class Person{public: Person(int age) { //1、當形參和成員變量同名時,可用this指針來區分 this->age = age; } Person& PersonAddPerson(Person p) { this->age += p.age; //傳回對象本身 return *this; } int age;};void test01(){ Person p1(10); cout << "p1.age = " << p1.age << endl; Person p2(10); p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); cout << "p2.age = " << p2.age << endl;}int main() { test01(); system("pause"); return 0;}
4.3.3 空指針通路成員函數
C++中空指針也是可以調用成員函數的,但是也要注意有沒有用到this指針
如果用到this指針,需要加以判斷保證代碼的健壯性
示例:
//空指針通路成員函數class Person {public: void ShowClassName() { cout << "我是Person類!" << endl; } void ShowPerson() { if (this == NULL) { return; } cout << mAge << endl; }public: int mAge;};void test01(){ Person * p = NULL; p->ShowClassName(); //空指針,可以調用成員函數 p->ShowPerson(); //但是如果成員函數中用到了this指針,就不可以了}int main() { test01(); system("pause"); return 0;}
4.3.4 const修飾成員函數
常函數:
- 成員函數後加const後我們稱為這個函數為常函數
- 常函數内不可以修改成員屬性
- 成員屬性聲明時加關鍵字mutable後,在常函數中依然可以修改
常對象:
- 聲明對象前加const稱該對象為常對象
- 常對象隻能調用常函數
示例:
class Person {public: Person() { m_A = 0; m_B = 0; } //this指針的本質是一個指針常量,指針的指向不可修改 //如果想讓指針指向的值也不可以修改,需要聲明常函數 void ShowPerson() const { //const Type* const pointer; //this = NULL; //不能修改指針的指向 Person* const this; //this->mA = 100; //但是this指針指向的對象的資料是可以修改的 //const修飾成員函數,表示指針指向的記憶體空間的資料不能修改,除了mutable修飾的變量 this->m_B = 100; } void MyFunc() const { //mA = 10000; }public: int m_A; mutable int m_B; //可修改 可變的};//const修飾對象 常對象void test01() { const Person person; //常量對象 cout << person.m_A << endl; //person.mA = 100; //常對象不能修改成員變量的值,但是可以通路 person.m_B = 100; //但是常對象可以修改mutable修飾成員變量 //常對象通路成員函數 person.MyFunc(); //常對象不能調用const的函數}int main() { test01(); system("pause"); return 0;}
4.4 友元
生活中你的家有客廳(Public),有你的卧室(Private)
客廳所有來的客人都可以進去,但是你的卧室是私有的,也就是說隻有你能進去
但是呢,你也可以允許你的好閨蜜好基友進去。
在程式裡,有些私有屬性 也想讓類外特殊的一些函數或者類進行通路,就需要用到友元的技術
友元的目的就是讓一個函數或者類 通路另一個類中私有成員
友元的關鍵字為 friend
友元的三種實作
- 全局函數做友元
- 類做友元
- 成員函數做友元
4.4.1 全局函數做友元
class Building{ //告訴編譯器 goodGay全局函數 是 Building類的好朋友,可以通路類中的私有内容 friend void goodGay(Building * building);public: Building() { this->m_SittingRoom = "客廳"; this->m_BedRoom = "卧室"; }public: string m_SittingRoom; //客廳private: string m_BedRoom; //卧室};void goodGay(Building * building){ cout << "好基友正在通路: " << building->m_SittingRoom << endl; cout << "好基友正在通路: " << building->m_BedRoom << endl;}void test01(){ Building b; goodGay(&b);}int main(){ test01(); system("pause"); return 0;}
4.4.2 類做友元
class Building;class goodGay{public: goodGay(); void visit();private: Building *building;};class Building{ //告訴編譯器 goodGay類是Building類的好朋友,可以通路到Building類中私有内容 friend class goodGay;public: Building();public: string m_SittingRoom; //客廳private: string m_BedRoom;//卧室};Building::Building(){ this->m_SittingRoom = "客廳"; this->m_BedRoom = "卧室";}goodGay::goodGay(){ building = new Building;}void goodGay::visit(){ cout << "好基友正在通路" << building->m_SittingRoom << endl; cout << "好基友正在通路" << building->m_BedRoom << endl;}void test01(){ goodGay gg; gg.visit();}int main(){ test01(); system("pause"); return 0;}
4.4.3 成員函數做友元
class Building;class goodGay{public: goodGay(); void visit(); //隻讓visit函數作為Building的好朋友,可以發通路Building中私有内容 void visit2(); private: Building *building;};class Building{ //告訴編譯器 goodGay類中的visit成員函數 是Building好朋友,可以通路私有内容 friend void goodGay::visit();public: Building();public: string m_SittingRoom; //客廳private: string m_BedRoom;//卧室};Building::Building(){ this->m_SittingRoom = "客廳"; this->m_BedRoom = "卧室";}goodGay::goodGay(){ building = new Building;}void goodGay::visit(){ cout << "好基友正在通路" << building->m_SittingRoom << endl; cout << "好基友正在通路" << building->m_BedRoom << endl;}void goodGay::visit2(){ cout << "好基友正在通路" << building->m_SittingRoom << endl; //cout << "好基友正在通路" << building->m_BedRoom << endl;}void test01(){ goodGay gg; gg.visit();}int main(){ test01(); system("pause"); return 0;}
4.5 運算符重載
運算符重載概念:對已有的運算符重新進行定義,賦予其另一種功能,以适應不同的資料類型
4.5.1 加号運算符重載
作用:實作兩個自定義資料類型相加的運算
class Person {public: Person() {}; Person(int a, int b) { this->m_A = a; this->m_B = b; } //成員函數實作 + 号運算符重載 Person operator+(const Person& p) { Person temp; temp.m_A = this->m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; }public: int m_A; int m_B;};//全局函數實作 + 号運算符重載//Person operator+(const Person& p1, const Person& p2) {// Person temp(0, 0);// temp.m_A = p1.m_A + p2.m_A;// temp.m_B = p1.m_B + p2.m_B;// return temp;//}//運算符重載 可以發生函數重載 Person operator+(const Person& p2, int val) { Person temp; temp.m_A = p2.m_A + val; temp.m_B = p2.m_B + val; return temp;}void test() { Person p1(10, 10); Person p2(20, 20); //成員函數方式 Person p3 = p2 + p1; //相當于 p2.operaor+(p1) cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl; Person p4 = p3 + 10; //相當于 operator+(p3,10) cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;}int main() { test(); system("pause"); return 0;}
總結1:對于内置的資料類型的表達式的的運算符是不可能改變的
總結2:不要濫用運算符重載
4.5.2 左移運算符重載
作用:可以輸出自定義資料類型
class Person { friend ostream& operator<<(ostream& out, Person& p);public: Person(int a, int b) { this->m_A = a; this->m_B = b; } //成員函數 實作不了 p << cout 不是我們想要的效果 //void operator<<(Person& p){ //}private: int m_A; int m_B;};//全局函數實作左移重載//ostream對象隻能有一個ostream& operator<<(ostream& out, Person& p) { out << "a:" << p.m_A << " b:" << p.m_B; return out;}void test() { Person p1(10, 20); cout << p1 << "hello world" << endl; //鍊式程式設計}int main() { test(); system("pause"); return 0;}
總結:重載左移運算符配合友元可以實作輸出自定義資料類型
4.5.3 遞增運算符重載
作用: 通過重載遞增運算符,實作自己的整型資料
class MyInteger { friend ostream& operator<<(ostream& out, MyInteger myint);public: MyInteger() { m_Num = 0; } //前置++ MyInteger& operator++() { //先++ m_Num++; //再傳回 return *this; } //後置++ MyInteger operator++(int) { //先傳回 MyInteger temp = *this; //記錄目前本身的值,然後讓本身的值加1,但是傳回的是以前的值,達到先傳回後++; m_Num++; return temp; }private: int m_Num;};ostream& operator<<(ostream& out, MyInteger myint) { out << myint.m_Num; return out;}//前置++ 先++ 再傳回void test01() { MyInteger myInt; cout << ++myInt << endl; cout << myInt << endl;}//後置++ 先傳回 再++void test02() { MyInteger myInt; cout << myInt++ << endl; cout << myInt << endl;}int main() { test01(); //test02(); system("pause"); return 0;}
總結: 前置遞增傳回引用,後置遞增傳回值
4.5.4 指派運算符重載
c++編譯器至少給一個類添加4個函數
- 預設構造函數(無參,函數體為空)
- 預設析構函數(無參,函數體為空)
- 預設拷貝構造函數,對屬性進行值拷貝
- 指派運算符 operator=, 對屬性進行值拷貝
如果類中有屬性指向堆區,做指派操作時也會出現深淺拷貝問題
示例:
class Person{public: Person(int age) { //将年齡資料開辟到堆區 m_Age = new int(age); } //重載指派運算符 Person& operator=(Person &p) { if (m_Age != NULL) { delete m_Age; m_Age = NULL; } //編譯器提供的代碼是淺拷貝 //m_Age = p.m_Age; //提供深拷貝 解決淺拷貝的問題 m_Age = new int(*p.m_Age); //傳回自身 return *this; } ~Person() { if (m_Age != NULL) { delete m_Age; m_Age = NULL; } } //年齡的指針 int *m_Age;};void test01(){ Person p1(18); Person p2(20); Person p3(30); p3 = p2 = p1; //指派操作 cout << "p1的年齡為:" << *p1.m_Age << endl; cout << "p2的年齡為:" << *p2.m_Age << endl; cout << "p3的年齡為:" << *p3.m_Age << endl;}int main() { test01(); //int a = 10; //int b = 20; //int c = 30; //c = b = a; //cout << "a = " << a << endl; //cout << "b = " << b << endl; //cout << "c = " << c << endl; system("pause"); return 0;}
4.5.5 關系運算符重載
**作用:**重載關系運算符,可以讓兩個自定義類型對象進行對比操作
示例:
class Person{public: Person(string name, int age) { this->m_Name = name; this->m_Age = age; }; bool operator==(Person & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return true; } else { return false; } } bool operator!=(Person & p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) { return false; } else { return true; } } string m_Name; int m_Age;};void test01(){ //int a = 0; //int b = 0; Person a("孫悟空", 18); Person b("孫悟空", 18); if (a == b) { cout << "a和b相等" << endl; } else { cout << "a和b不相等" << endl; } if (a != b) { cout << "a和b不相等" << endl; } else { cout << "a和b相等" << endl; }}int main() { test01(); system("pause"); return 0;}
4.5.6 函數調用運算符重載
- 函數調用運算符 () 也可以重載
- 由于重載後使用的方式非常像函數的調用,是以稱為仿函數
- 仿函數沒有固定寫法,非常靈活
示例:
class MyPrint{public: void operator()(string text) { cout << text << endl; }};void test01(){ //重載的()操作符 也稱為仿函數 MyPrint myFunc; myFunc("hello world");}class MyAdd{public: int operator()(int v1, int v2) { return v1 + v2; }};void test02(){ MyAdd add; int ret = add(10, 10); cout << "ret = " << ret << endl; //匿名對象調用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;}int main() { test01(); test02(); system("pause"); return 0;}
4.6 繼承
繼承是面向對象三大特性之一
我們發現,定義這些類時,下級别的成員除了擁有上一級的共性,還有自己的特性。
這個時候我們就可以考慮利用繼承的技術,減少重複代碼
4.6.1 繼承的基本文法
例如我們看到很多網站中,都有公共的頭部,公共的底部,甚至公共的左側清單,隻有中心内容不同
接下來我們分别利用普通寫法和繼承的寫法來實作網頁中的内容,看一下繼承存在的意義以及好處
普通實作:
//Java頁面class Java {public: void header() { cout << "首頁、公開課、登入、注冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站内地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類清單)" << endl; } void content() { cout << "JAVA學科視訊" << endl; }};//Python頁面class Python{public: void header() { cout << "首頁、公開課、登入、注冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站内地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類清單)" << endl; } void content() { cout << "Python學科視訊" << endl; }};//C++頁面class CPP {public: void header() { cout << "首頁、公開課、登入、注冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站内地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類清單)" << endl; } void content() { cout << "C++學科視訊" << endl; }};void test01(){ //Java頁面 cout << "Java下載下傳視訊頁面如下: " << endl; Java ja; ja.header(); ja.footer(); ja.left(); ja.content(); cout << "--------------------" << endl; //Python頁面 cout << "Python下載下傳視訊頁面如下: " << endl; Python py; py.header(); py.footer(); py.left(); py.content(); cout << "--------------------" << endl; //C++頁面 cout << "C++下載下傳視訊頁面如下: " << endl; CPP cp; cp.header(); cp.footer(); cp.left(); cp.content();}int main() { test01(); system("pause"); return 0;}
繼承實作:
//公共頁面class BasePage{public: void header() { cout << "首頁、公開課、登入、注冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站内地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類清單)" << endl; }};//Java頁面class Java : public BasePage{public: void content() { cout << "JAVA學科視訊" << endl; }};//Python頁面class Python : public BasePage{public: void content() { cout << "Python學科視訊" << endl; }};//C++頁面class CPP : public BasePage{public: void content() { cout << "C++學科視訊" << endl; }};void test01(){ //Java頁面 cout << "Java下載下傳視訊頁面如下: " << endl; Java ja; ja.header(); ja.footer(); ja.left(); ja.content(); cout << "--------------------" << endl; //Python頁面 cout << "Python下載下傳視訊頁面如下: " << endl; Python py; py.header(); py.footer(); py.left(); py.content(); cout << "--------------------" << endl; //C++頁面 cout << "C++下載下傳視訊頁面如下: " << endl; CPP cp; cp.header(); cp.footer(); cp.left(); cp.content();}int main() { test01(); system("pause"); return 0;}
總結:
繼承的好處:可以減少重複的代碼
class A : public B;
A 類稱為子類 或 派生類
B 類稱為父類 或 基類
派生類中的成員,包含兩大部分:
一類是從基類繼承過來的,一類是自己增加的成員。
從基類繼承過過來的表現其共性,而新增的成員展現了其個性。
4.6.2 繼承方式
繼承的文法:
class 子類 : 繼承方式 父類
繼承方式一共有三種:
- 公共繼承
- 保護繼承
- 私有繼承
示例:
class Base1{public: int m_A;protected: int m_B;private: int m_C;};//公共繼承class Son1 :public Base1{public: void func() { m_A; //可通路 public權限 m_B; //可通路 protected權限 //m_C; //不可通路 }};void myClass(){ Son1 s1; s1.m_A; //其他類隻能通路到公共權限}//保護繼承class Base2{public: int m_A;protected: int m_B;private: int m_C;};class Son2:protected Base2{public: void func() { m_A; //可通路 protected權限 m_B; //可通路 protected權限 //m_C; //不可通路 }};void myClass2(){ Son2 s; //s.m_A; //不可通路}//私有繼承class Base3{public: int m_A;protected: int m_B;private: int m_C;};class Son3:private Base3{public: void func() { m_A; //可通路 private權限 m_B; //可通路 private權限 //m_C; //不可通路 }};class GrandSon3 :public Son3{public: void func() { //Son3是私有繼承,是以繼承Son3的屬性在GrandSon3中都無法通路到 //m_A; //m_B; //m_C; }};
4.6.3 繼承中的對象模型
**問題:**從父類繼承過來的成員,哪些屬于子類對象中?
示例:
class Base{public: int m_A;protected: int m_B;private: int m_C; //私有成員隻是被隐藏了,但是還是會繼承下去};//公共繼承class Son :public Base{public: int m_D;};void test01(){ cout << "sizeof Son = " << sizeof(Son) << endl;}int main() { test01(); system("pause"); return 0;}
利用工具檢視:
打開工具視窗後,定位到目前CPP檔案的盤符
然後輸入: cl /d1 reportSingleClassLayout檢視的類名 所屬檔案名
結論: 父類中私有成員也是被子類繼承下去了,隻是由編譯器給隐藏後通路不到
4.6.4 繼承中構造和析構順序
子類繼承父類後,當建立子類對象,也會調用父類的構造函數
問題:父類和子類的構造和析構順序是誰先誰後?
示例:
class Base {public: Base() { cout << "Base構造函數!" << endl; } ~Base() { cout << "Base析構函數!" << endl; }};class Son : public Base{public: Son() { cout << "Son構造函數!" << endl; } ~Son() { cout << "Son析構函數!" << endl; }};void test01(){ //繼承中 先調用父類構造函數,再調用子類構造函數,析構順序與構造相反 Son s;}int main() { test01(); system("pause"); return 0;}
總結:繼承中 先調用父類構造函數,再調用子類構造函數,析構順序與構造相反
4.6.5 繼承同名成員處理方式
問題:當子類與父類出現同名的成員,如何通過子類對象,通路到子類或父類中同名的資料呢?
- 通路子類同名成員 直接通路即可
- 通路父類同名成員 需要加作用域
示例:
class Base {public: Base() { m_A = 100; } void func() { cout << "Base - func()調用" << endl; } void func(int a) { cout << "Base - func(int a)調用" << endl; }public: int m_A;};class Son : public Base {public: Son() { m_A = 200; } //當子類與父類擁有同名的成員函數,子類會隐藏父類中所有版本的同名成員函數 //如果想通路父類中被隐藏的同名成員函數,需要加父類的作用域 void func() { cout << "Son - func()調用" << endl; }public: int m_A;};void test01(){ Son s; cout << "Son下的m_A = " << s.m_A << endl; cout << "Base下的m_A = " << s.Base::m_A << endl; s.func(); s.Base::func(); s.Base::func(10);}int main() { test01(); system("pause"); return EXIT_SUCCESS;}
總結:
- 子類對象可以直接通路到子類中同名成員
- 子類對象加作用域可以通路到父類同名成員
- 當子類與父類擁有同名的成員函數,子類會隐藏父類中同名成員函數,加作用域可以通路到父類中同名函數
4.6.6 繼承同名靜态成員處理方式
問題:繼承中同名的靜态成員在子類對象上如何進行通路?
靜态成員和非靜态成員出現同名,處理方式一緻
- 通路子類同名成員 直接通路即可
- 通路父類同名成員 需要加作用域
示例:
class Base {public: static void func() { cout << "Base - static void func()" << endl; } static void func(int a) { cout << "Base - static void func(int a)" << endl; } static int m_A;};int Base::m_A = 100;class Son : public Base {public: static void func() { cout << "Son - static void func()" << endl; } static int m_A;};int Son::m_A = 200;//同名成員屬性void test01(){ //通過對象通路 cout << "通過對象通路: " << endl; Son s; cout << "Son 下 m_A = " << s.m_A << endl; cout << "Base 下 m_A = " << s.Base::m_A << endl; //通過類名通路 cout << "通過類名通路: " << endl; cout << "Son 下 m_A = " << Son::m_A << endl; cout << "Base 下 m_A = " << Son::Base::m_A << endl;}//同名成員函數void test02(){ //通過對象通路 cout << "通過對象通路: " << endl; Son s; s.func(); s.Base::func(); cout << "通過類名通路: " << endl; Son::func(); Son::Base::func(); //出現同名,子類會隐藏掉父類中所有同名成員函數,需要加作作用域通路 Son::Base::func(100);}int main() { //test01(); test02(); system("pause"); return 0;}
總結:同名靜态成員處理方式和非靜态處理方式一樣,隻不過有兩種通路的方式(通過對象 和 通過類名)
4.6.7 多繼承文法
C++允許一個類繼承多個類
文法:
class 子類 :繼承方式 父類1 , 繼承方式 父類2...
多繼承可能會引發父類中有同名成員出現,需要加作用域區分
C++實際開發中不建議用多繼承
示例:
class Base1 {public: Base1() { m_A = 100; }public: int m_A;};class Base2 {public: Base2() { m_A = 200; //開始是m_B 不會出問題,但是改為mA就會出現不明确 }public: int m_A;};//文法:class 子類:繼承方式 父類1 ,繼承方式 父類2 class Son : public Base2, public Base1 {public: Son() { m_C = 300; m_D = 400; }public: int m_C; int m_D;};//多繼承容易産生成員同名的情況//通過使用類名作用域可以區分調用哪一個基類的成員void test01(){ Son s; cout << "sizeof Son = " << sizeof(s) << endl; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl;}int main() { test01(); system("pause"); return 0;}
總結: 多繼承中如果父類中出現了同名情況,子類使用時候要加作用域
4.6.8 菱形繼承
菱形繼承概念:
兩個派生類繼承同一個基類
又有某個類同時繼承者兩個派生類
這種繼承被稱為菱形繼承,或者鑽石繼承
典型的菱形繼承案例:
菱形繼承問題:
羊繼承了動物的資料,駝同樣繼承了動物的資料,當草泥馬使用資料時,就會産生二義性。
草泥馬繼承自動物的資料繼承了兩份,其實我們應該清楚,這份資料我們隻需要一份就可以。
示例:
class Animal{public: int m_Age;};//繼承前加virtual關鍵字後,變為虛繼承//此時公共的父類Animal稱為虛基類class Sheep : virtual public Animal {};class Tuo : virtual public Animal {};class SheepTuo : public Sheep, public Tuo {};void test01(){ SheepTuo st; st.Sheep::m_Age = 100; st.Tuo::m_Age = 200; cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; cout << "st.m_Age = " << st.m_Age << endl;}int main() { test01(); system("pause"); return 0;}
總結:
- 菱形繼承帶來的主要問題是子類繼承兩份相同的資料,導緻資源浪費以及毫無意義
- 利用虛繼承可以解決菱形繼承問題
4.7 多态
4.7.1 多态的基本概念
多态是C++面向對象三大特性之一
多态分為兩類
- 靜态多态: 函數重載 和 運算符重載屬于靜态多态,複用函數名
- 動态多态: 派生類和虛函數實作運作時多态
靜态多态和動态多态差別:
- 靜态多态的函數位址早綁定 - 編譯階段确定函數位址
- 動态多态的函數位址晚綁定 - 運作階段确定函數位址
下面通過案例進行講解多态
class Animal{public: //Speak函數就是虛函數 //函數前面加上virtual關鍵字,變成虛函數,那麼編譯器在編譯的時候就不能确定函數調用了。 virtual void speak() { cout << "動物在說話" << endl; }};class Cat :public Animal{public: void speak() { cout << "小貓在說話" << endl; }};class Dog :public Animal{public: void speak() { cout << "小狗在說話" << endl; }};//我們希望傳入什麼對象,那麼就調用什麼對象的函數//如果函數位址在編譯階段就能确定,那麼靜态聯編//如果函數位址在運作階段才能确定,就是動态聯編void DoSpeak(Animal & animal){ animal.speak();}多态滿足條件: //1、有繼承關系//2、子類重寫父類中的虛函數//多态使用://父類指針或引用指向子類對象void test01(){ Cat cat; DoSpeak(cat); Dog dog; DoSpeak(dog);}int main() { test01(); system("pause"); return 0;}
總結:
多态滿足條件
- 有繼承關系
- 子類重寫父類中的虛函數
多态使用條件
- 父類指針或引用指向子類對象
重寫:函數傳回值類型 函數名 參數清單 完全一緻稱為重寫
4.7.2 多态案例一-電腦類
案例描述:
分别利用普通寫法和多态技術,設計實作兩個操作數進行運算的電腦類
多态的優點:
- 代碼組織結構清晰
- 可讀性強
- 利于前期和後期的擴充以及維護
示例:
//普通實作class Calculator {public: int getResult(string oper) { if (oper == "+") { return m_Num1 + m_Num2; } else if (oper == "-") { return m_Num1 - m_Num2; } else if (oper == "*") { return m_Num1 * m_Num2; } //如果要提供新的運算,需要修改源碼 }public: int m_Num1; int m_Num2;};void test01(){ //普通實作測試 Calculator c; c.m_Num1 = 10; c.m_Num2 = 10; cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl; cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl; cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;}//多态實作//抽象電腦類//多态優點:代碼組織結構清晰,可讀性強,利于前期和後期的擴充以及維護class AbstractCalculator{public : virtual int getResult() { return 0; } int m_Num1; int m_Num2;};//加法電腦class AddCalculator :public AbstractCalculator{public: int getResult() { return m_Num1 + m_Num2; }};//減法電腦class SubCalculator :public AbstractCalculator{public: int getResult() { return m_Num1 - m_Num2; }};//乘法電腦class MulCalculator :public AbstractCalculator{public: int getResult() { return m_Num1 * m_Num2; }};void test02(){ //建立加法電腦 AbstractCalculator *abc = new AddCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; //用完了記得銷毀 //建立減法電腦 abc = new SubCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; //建立乘法電腦 abc = new MulCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc;}int main() { //test01(); test02(); system("pause"); return 0;}
總結:C++開發提倡利用多态設計程式架構,因為多态優點很多
4.7.3 純虛函數和抽象類
在多态中,通常父類中虛函數的實作是毫無意義的,主要都是調用子類重寫的内容
是以可以将虛函數改為純虛函數
純虛函數文法:
virtual 傳回值類型 函數名 (參數清單)= 0 ;
當類中有了純虛函數,這個類也稱為抽象類
抽象類特點:
- 無法執行個體化對象
- 子類必須重寫抽象類中的純虛函數,否則也屬于抽象類
示例:
class Base{public: //純虛函數 //類中隻要有一個純虛函數就稱為抽象類 //抽象類無法執行個體化對象 //子類必須重寫父類中的純虛函數,否則也屬于抽象類 virtual void func() = 0;};class Son :public Base{public: virtual void func() { cout << "func調用" << endl; };};void test01(){ Base * base = NULL; //base = new Base; // 錯誤,抽象類無法執行個體化對象 base = new Son; base->func(); delete base;//記得銷毀}int main() { test01(); system("pause"); return 0;}
4.7.4 多态案例二-制作飲品
案例描述:
制作飲品的大緻流程為:煮水 - 沖泡 - 倒入杯中 - 加入輔料
利用多态技術實作本案例,提供抽象制作飲品基類,提供子類制作咖啡和茶葉
示例:
//抽象制作飲品class AbstractDrinking {public: //燒水 virtual void Boil() = 0; //沖泡 virtual void Brew() = 0; //倒入杯中 virtual void PourInCup() = 0; //加入輔料 virtual void PutSomething() = 0; //規定流程 void MakeDrink() { Boil(); Brew(); PourInCup(); PutSomething(); }};//制作咖啡class Coffee : public AbstractDrinking {public: //燒水 virtual void Boil() { cout << "煮農夫山泉!" << endl; } //沖泡 virtual void Brew() { cout << "沖泡咖啡!" << endl; } //倒入杯中 virtual void PourInCup() { cout << "将咖啡倒入杯中!" << endl; } //加入輔料 virtual void PutSomething() { cout << "加入牛奶!" << endl; }};//制作茶水class Tea : public AbstractDrinking {public: //燒水 virtual void Boil() { cout << "煮自來水!" << endl; } //沖泡 virtual void Brew() { cout << "沖泡茶葉!" << endl; } //倒入杯中 virtual void PourInCup() { cout << "将茶水倒入杯中!" << endl; } //加入輔料 virtual void PutSomething() { cout << "加入枸杞!" << endl; }};//業務函數void DoWork(AbstractDrinking* drink) { drink->MakeDrink(); delete drink;}void test01() { DoWork(new Coffee); cout << "--------------" << endl; DoWork(new Tea);}int main() { test01(); system("pause"); return 0;}
4.7.5 虛析構和純虛析構
多态使用時,如果子類中有屬性開辟到堆區,那麼父類指針在釋放時無法調用到子類的析構代碼
解決方式:将父類中的析構函數改為虛析構或者純虛析構
虛析構和純虛析構共性:
- 可以解決父類指針釋放子類對象
- 都需要有具體的函數實作
虛析構和純虛析構差別:
- 如果是純虛析構,該類屬于抽象類,無法執行個體化對象
虛析構文法:
virtual ~類名(){}
純虛析構文法:
virtual ~類名() = 0;
類名::~類名(){}
示例:
class Animal {public: Animal() { cout << "Animal 構造函數調用!" << endl; } virtual void Speak() = 0; //析構函數加上virtual關鍵字,變成虛析構函數 //virtual ~Animal() //{ // cout << "Animal虛析構函數調用!" << endl; //} virtual ~Animal() = 0;};Animal::~Animal(){ cout << "Animal 純虛析構函數調用!" << endl;}//和包含普通純虛函數的類一樣,包含了純虛析構函數的類也是一個抽象類。不能夠被執行個體化。class Cat : public Animal {public: Cat(string name) { cout << "Cat構造函數調用!" << endl; m_Name = new string(name); } virtual void Speak() { cout << *m_Name << "小貓在說話!" << endl; } ~Cat() { cout << "Cat析構函數調用!" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } }public: string *m_Name;};void test01(){ Animal *animal = new Cat("Tom"); animal->Speak(); //通過父類指針去釋放,會導緻子類對象可能清理不幹淨,造成記憶體洩漏 //怎麼解決?給基類增加一個虛析構函數 //虛析構函數就是用來解決通過父類指針釋放子類對象 delete animal;}int main() { test01(); system("pause"); return 0;}
總結:
1. 虛析構或純虛析構就是用來解決通過父類指針釋放子類對象
2. 如果子類中沒有堆區資料,可以不寫為虛析構或純虛析構
3. 擁有純虛析構函數的類也屬于抽象類
4.7.6 多态案例三-電腦組裝
案例描述:
電腦主要組成部件為 CPU(用于計算),顯示卡(用于顯示),記憶體條(用于存儲)
将每個零件封裝出抽象基類,并且提供不同的廠商生産不同的零件,例如Intel廠商和Lenovo廠商
建立電腦類提供讓電腦工作的函數,并且調用每個零件工作的接口
測試時組裝三台不同的電腦進行工作
示例:
#include<iostream>using namespace std;//抽象CPU類class CPU{public: //抽象的計算函數 virtual void calculate() = 0;};//抽象顯示卡類class VideoCard{public: //抽象的顯示函數 virtual void display() = 0;};//抽象記憶體條類class Memory{public: //抽象的存儲函數 virtual void storage() = 0;};//電腦類class Computer{public: Computer(CPU * cpu, VideoCard * vc, Memory * mem) { m_cpu = cpu; m_vc = vc; m_mem = mem; } //提供工作的函數 void work() { //讓零件工作起來,調用接口 m_cpu->calculate(); m_vc->display(); m_mem->storage(); } //提供析構函數 釋放3個電腦零件 ~Computer() { //釋放CPU零件 if (m_cpu != NULL) { delete m_cpu; m_cpu = NULL; } //釋放顯示卡零件 if (m_vc != NULL) { delete m_vc; m_vc = NULL; } //釋放記憶體條零件 if (m_mem != NULL) { delete m_mem; m_mem = NULL; } }private: CPU * m_cpu; //CPU的零件指針 VideoCard * m_vc; //顯示卡零件指針 Memory * m_mem; //記憶體條零件指針};//具體廠商//Intel廠商class IntelCPU :public CPU{public: virtual void calculate() { cout << "Intel的CPU開始計算了!" << endl; }};class IntelVideoCard :public VideoCard{public: virtual void display() { cout << "Intel的顯示卡開始顯示了!" << endl; }};class IntelMemory :public Memory{public: virtual void storage() { cout << "Intel的記憶體條開始存儲了!" << endl; }};//Lenovo廠商class LenovoCPU :public CPU{public: virtual void calculate() { cout << "Lenovo的CPU開始計算了!" << endl; }};class LenovoVideoCard :public VideoCard{public: virtual void display() { cout << "Lenovo的顯示卡開始顯示了!" << endl; }};class LenovoMemory :public Memory{public: virtual void storage() { cout << "Lenovo的記憶體條開始存儲了!" << endl; }};void test01(){ //第一台電腦零件 CPU * intelCpu = new IntelCPU; VideoCard * intelCard = new IntelVideoCard; Memory * intelMem = new IntelMemory; cout << "第一台電腦開始工作:" << endl; //建立第一台電腦 Computer * computer1 = new Computer(intelCpu, intelCard, intelMem); computer1->work(); delete computer1; cout << "-----------------------" << endl; cout << "第二台電腦開始工作:" << endl; //第二台電腦組裝 Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);; computer2->work(); delete computer2; cout << "-----------------------" << endl; cout << "第三台電腦開始工作:" << endl; //第三台電腦組裝 Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);; computer3->work(); delete computer3;}
5 檔案操作
程式運作時産生的資料都屬于臨時資料,程式一旦運作結束都會被釋放
通過檔案可以将資料持久化
C++中對檔案操作需要包含頭檔案 < fstream >
檔案類型分為兩種:
- 文本檔案- 檔案以文本的ASCII碼形式存儲在計算機中
- 二進制檔案- 檔案以文本的二進制形式存儲在計算機中,使用者一般不能直接讀懂它們
操作檔案的三大類:
- ofstream:寫操作
- ifstream: 讀操作
- fstream : 讀寫操作
5.1文本檔案
5.1.1寫檔案
寫檔案步驟如下:
-
包含頭檔案
#include <fstream>
-
建立流對象
ofstream ofs;
-
打開檔案
ofs.open(“檔案路徑”,打開方式);
-
寫資料
ofs << “寫入的資料”;
-
關閉檔案
ofs.close();
檔案打開方式:
打開方式 | 解釋 |
ios::in | 為讀檔案而打開檔案 |
ios::out | 為寫檔案而打開檔案 |
ios::ate | 初始位置:檔案尾 |
ios::app | 追加方式寫檔案 |
ios::trunc | 如果檔案存在先删除,再建立 |
ios::binary | 二進制方式 |
注意: 檔案打開方式可以配合使用,利用|操作符
**例如:**用二進制方式寫檔案
ios::binary | ios:: out
示例:
#include <fstream>void test01(){ ofstream ofs; ofs.open("test.txt", ios::out); ofs << "姓名:張三" << endl; ofs << "性别:男" << endl; ofs << "年齡:18" << endl; ofs.close();}int main() { test01(); system("pause"); return 0;}
總結:
- 檔案操作必須包含頭檔案 fstream
- 讀檔案可以利用 ofstream ,或者fstream類
- 打開檔案時候需要指定操作檔案的路徑,以及打開方式
- 利用<<可以向檔案中寫資料
- 操作完畢,要關閉檔案
5.1.2讀檔案
讀檔案與寫檔案步驟相似,但是讀取方式相對于比較多
讀檔案步驟如下:
-
包含頭檔案
#include <fstream>
-
建立流對象
ifstream ifs;
-
打開檔案并判斷檔案是否打開成功
ifs.open(“檔案路徑”,打開方式);
-
讀資料
四種方式讀取
-
關閉檔案
ifs.close();
示例:
#include <fstream>#include <string>void test01(){ ifstream ifs; ifs.open("test.txt", ios::in); if (!ifs.is_open()) { cout << "檔案打開失敗" << endl; return; } //第一種方式 //char buf[1024] = { 0 }; //while (ifs >> buf) //{ // cout << buf << endl; //} //第二種 //char buf[1024] = { 0 }; //while (ifs.getline(buf,sizeof(buf))) //{ // cout << buf << endl; //} //第三種 //string buf; //while (getline(ifs, buf)) //{ // cout << buf << endl; //} char c; while ((c = ifs.get()) != EOF) { cout << c; } ifs.close();}int main() { test01(); system("pause"); return 0;}
總結:
- 讀檔案可以利用 ifstream ,或者fstream類
- 利用is_open函數可以判斷檔案是否打開成功
- close 關閉檔案
5.2 二進制檔案
以二進制的方式對檔案進行讀寫操作
打開方式要指定為 ios::binary
5.2.1 寫檔案
二進制方式寫檔案主要利用流對象調用成員函數write
函數原型 :
ostream& write(const char * buffer,int len);
參數解釋:字元指針buffer指向記憶體中一段存儲空間。len是讀寫的位元組數
示例:
#include <fstream>#include <string>class Person{public: char m_Name[64]; int m_Age;};//二進制檔案 寫檔案void test01(){ //1、包含頭檔案 //2、建立輸出流對象 ofstream ofs("person.txt", ios::out | ios::binary); //3、打開檔案 //ofs.open("person.txt", ios::out | ios::binary); Person p = {"張三" , 18}; //4、寫檔案 ofs.write((const char *)&p, sizeof(p)); //5、關閉檔案 ofs.close();}int main() { test01(); system("pause"); return 0;}
總結:
- 檔案輸出流對象 可以通過write函數,以二進制方式寫資料
5.2.2 讀檔案
二進制方式讀檔案主要利用流對象調用成員函數read
函數原型:
istream& read(char *buffer,int len);
#include <fstream>#include <string>class Person{public: char m_Name[64]; int m_Age;};void test01(){ ifstream ifs("person.txt", ios::in | ios::binary); if (!ifs.is_open()) { cout << "檔案打開失敗" << endl; } Person p; ifs.read((char *)&p, sizeof(p)); cout << "姓名: " << p.m_Name << " 年齡: " << p.m_Age << endl;}int main() { test01(); system("pause"); return 0;}
- 檔案輸入流對象 可以通過read函數,以二進制方式讀資料