C++ review(2)
C++ review (1)
C++ review (2)
C++ review (3)
一、記憶體分區模型
1. 程式運行前
在程式編譯後,生成了 exe 可執行程式,未執行該程式前分爲三個區域
(1) 代碼區:共享和只讀的
(2) 全局區:全局變量、靜態變量、
(3) 常量區:常量、const 修飾的全局常量、字符串常量
2. 程式運行後
(1) 棧區 (stack):由編譯器自動分配釋放,存放函數的參數值、局部變量等1
2
3
4
5
6
7
8
9
10
11int* func ()
{
int a = 10;
return &a;
}
int main () {
int *p = func ();
cout << *p << endl; // 報錯
return 0;
}
(2) 堆區 (heap):堆區數據由管理員開闢和釋放(利用 new 和 delete)1
2
3
4
5
6
7
8
9
10
11int* func ()
{
int* a = new int (10);
return a;
}
int main () {
int *p = func ();
cout << *p << endl; // 正確
return 0;
}
3.new 和 delete 操作符
1 | int *p = new int; |
在這段程式中,new 運算子會配置 int 需要的空間,並傳回該空間的位址,可以使用指標 p 來儲存位址,這段程式只配置空間但不初始空間的值。想在配置完成後指定儲存值,可以如下:1
int *p = new int (100);
這段程式在配置空間之後,會將空間中的儲存值設定為 100。
如果想配置連續個指定型態的空間,可以如下:1
int *p = new int [1000];
配置後的空間資料是未知的,[] 中指定的長度可以是來自於運算式,不必是編譯時期就得決定的值,這個值必須自行儲存下來,因為沒有任何方式,可以從 p 得知到底配置的長度是多少。因此上面的方式,會被用來克服陣列大小必須事先決定的問題,也就是可以用來動態地配置連續空間,並當成陣列來操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14int main ()
{
int size = 0;
cout << " 輸出長度:" << endl;
cin >> size;
int *arr = new int [size](0);
cout << " 指定元素:" << endl;
for (int i=0; i<size; i++)
{
cout << "arr [" << i << "] =";
cin >> arr [i];
}
}
二、引用
1. 引用的基本使用
1 | int a = 10; |
2. 引用注意事項
引用必須初始化
引用在初始化後,不可以改變
3. 引用做函數參數
1 | // 地址傳遞 |
通過引用參數產生的效果和按地址傳遞是一樣的,引用的語法更清楚簡單。
4. 引用做函數返回值
(1) 當函數返回引用類型時,沒有複製返回值,返回的是對象本身。
1 | // 這裡參數用到常量引用,後面會講到 |
(2) 在函數的參數中,包含引用或指針,需要被返回的參數
1 | int& abc (int a, int b, int c, int& result) |
(3) 不能返回局部對象的引用
當函數執行完之後,將釋放分配給局部對象的存儲空間,此時,對局部對象的引用就會指向不確定的空間。
(4) 引用返回一個左值
(5) 如果不希望返回的對象被修改,可以返回 const 引用
5. 引用的本質
引用在 C++ 内部實現是一個指針常量,指針常量本身不可改,這也説明了爲什麽引用不可更改
C++ 推薦引用,因爲語法方便,引用的本質是指針常量,但是所有的指針操作編譯器幫忙做了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void func (int& ref)
{
ref = 100; //ref 是引用,轉換成 *ref = 100;
}
int main ()
{
int a = 10;
int& b = a; // 自動轉換成 int* const b = &a;
b = 20; // 自動轉換成 *b = 20;
cout << "a:" << a <<endl;
cout << "b:" << b << endl;
func (a); // 在函數中轉換成 int* const ref = &a;
return 0;
}
6. 常量引用
函數中常用常量引用防止誤修改實參,例如:1
2
3
4const string& shorterString (const string& s1, const string& s2)
{
return s1.size < s2.size ? s1 : s2;
}
常量引用的初始化1
const int& ref = 10; // 引用本身需要一個合法空間,10 是沒有地址的,因此這列錯誤
三、函數進階
四、類和對象
C++ 面向對象的三大特性:封裝、繼承、多態
1. 封裝
(1) 封裝的意義
(2) struct 和 class 的區別
struct 默認權限為公有
class 默認權限為私有
(3) 成員屬性設置為私有
將所有成員屬性設置為私有,可以自己控制讀寫權限
對於寫權限,可以檢測用戶輸入數據的有效性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41//point.h
using namespace std;
class Point
{
public:
void setX (int x);
int getX ();
void setY (int y);
int getY ();
private:
int m_X;
int m_Y;
};
//point.cpp
void Point:setX (int x)
{
m_X = x;
}
int Point:getX ()
{
return m_X;
}
void setY (int y)
{
m_X = y;
}
int getY ()
{
return m_Y;
}
2. 對象的初始化和清理
(1) 構造函數和析構函數
(a) 如果數據成員包含指針,在析構函數中要 delete 掉
(b) 析構函數調用時機
如果對象是動態變量,則當執行完定義該對象的程式區塊時,將調用該對象的析構函數;
如果對象是靜態變量 (外部、靜態、靜態外部、來自命名空間),則在程式結束時調用對象的析構函數;
如果對象是用 new 創建的,則僅當顯式使用 delete 刪除對象時,其析構函數才會被調用1
2
3Time *a = new Time ();
...
delete a;
(2) 構造函數的分類和調用
1 | // 調用無參構造函數 |
(3) 複製構造函數調用時機
(a) 使用一個已經創建完畢的對象來初始化一個新對象1
2
3
4
5Person man (100);
Person newman1 (man); // 調用複製構造函數
Person newman2 = man; // 調用複製構造函數
Person newman3;
newman3 = man1; // 這是賦值,不是調用複製構造函數
(b) 值傳遞的方式給函數參數傳值
(c) 以值方式返回局部對象1
2
3
4
5Time Time::max (const Time &t1, const Time &t2)
{
return t1;
// 值方式返回,需要創建一份 t1 對象的副本 (調用複製構造函數),效率比較低
}1
2
3
4
5// 因爲在參數中聲明 t1 為 const 類型的,所以返回值也必須聲明為 const
const Time& Time::max (const Time &t1, const Time &t2)
{
return t1;
}
(4) 構造函數調用規則
如果用戶定義有參構造函數,C++ 不再提供默認無參構造函數,但是會提供默認複製函數
如果用戶定義複製構造函數,C++ 不會再提供其他構造函數
(5) 淺複製和深複製
淺複製:簡單的賦值複製操作
深複製:在堆區重新申請空間,進行複製操作1
2
3
4
5
6
7Person (const Person& p)
{
// 如果不利用深複製在堆區創建新空間,會導致淺複製帶來的重複釋放堆區的問題
// 因爲如果單純的進行指針賦值,兩個不同對象的指針成員會指向同一個空間
age = p.age;
height = new int (*p.height);
}
(6) 初始化列表
1 | Person (int a, int b, int c):m_A (a), m_B (b), m_C (c) {}; |
(7) 類對象作爲類成員
當類中成員是其他類對象時,我們稱該成員為對象成員
構造的順序是:先調用對象成員的構造,再調用本類構造;析構順序與構造相反
(8) 靜態成員
非整型 / 枚舉型 const 的靜態屬性,都必須在實現檔案 (.cpp) 中進行初始化,且不能在成員函數中
整型 / 枚舉型 const 的靜態屬性,才可以且必須在聲明檔案 (.h) 中初始化 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//Time.h
using namespace std;
class Time
{
private:
int hours;
int minutes;
int seconds;
static int count;
public:
Time ();
Time (int h = 0, int m = 0, int s = 0);
...1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//Time.cpp
int Time::count = 0;// 初始化時不用加 static
Time::Time ()
{
hours = minutes = seconds = 0;
count++;
}
Time::Time (int h, int m, int s)
{
if (h < 0 || h > 24 || m > 60 || m < 0 || s > 60 || s < 0)
{
cout << " 初始化參數輸入有誤,程式終止!" << endl;
abort (); // 終止程式執行,直接從調用的地方跳出
}
hours = h;
minutes = m;
seconds = s;
count++;
}
(a) 靜態成員變量
所有對象共享一份數據
在編譯階段分配記憶體
類内聲明,類外初始化
(b) 靜態成員函數
所有對象共享同一個函數
靜態成員函數只能訪問靜態成員變量(因爲調用靜態函數時,不會有 this 指針)
3.C++ 對象模型和 this 指針
(1) 成員變量和成員函數分開存儲
非靜態成員變量占對象空間;靜態成員變量不占對象空間
非靜態成員函數不占對象空間,所有對象共享一個函數實例;靜態成員函數不占對象空間
(2) this 指針的概念
所有同類型的對象共享一個函數實例,那如何區分是哪個對象調用該函數呢?
this 指針指向被調用的成員函數所屬的對象
(a) 當形參和成員變量同名時,可以用 this 指針來區分,隱含在每一個非靜態成員函數内,不須定義,可以直接使用
(b) 在類的非靜態函數中返回對象本身,可以使用 return *this;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 訪問對象就可以連續調用
class Person
{
public:
Person (int age)
{
this->age = age;
}
Person& PersonAddPerson (Person p)
{
this->age = p.age;
return *this;
}
};
int main ()
{
Person p1 (20);
Person p2 (10);
p2.PersonAddPerson (p1).PersonAddPerson (p1).PersonAddPerson (p1);
cout << "p2.age =" << p2.age << endl;
}
(3) 空指針訪問成員函數
空指針可以訪問屬性是 public 的成員函數,但是函數中不可以用到 this 指針1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std;
class B {
public:
void foo () { cout << "B foo" << endl; }
};
int main ()
{
B *somenull = NULL;
somenull->foo ();
return 0;
}
(4) const 修飾成員函數
(a) 常函數
若成員函數後加 const,我們稱這個函數為常函數
常函數不可以修改成員屬性
但如果成員屬性在聲明時加關鍵字 mutable,在常函數中依然可以修改
(b) 常對象
聲明對象前加 const 稱爲常對象
常對象只能調用常函數
4. 友元
友元就是讓類外某些特殊的函數或類訪問另一個類中的私有成員
(1) 全局函數做友元
(2) 類做友元
(3) 成員函數做友元
5. 運算符重載
(1) 加號運算符重載
(a) 利用成員函數實現加號重載1
2
3
4
5
6
7
8
9
10
11Person 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;
}
Person p3 = p1.opertor+(p2);
// 可以簡化成 (使用編譯器提供的名稱):
Person p3 = p1 + p2;
(b) 利用全局函數重載加號1
2
3
4
5
6
7
8
9
10
11Person operator+ (Person &p1, Person &p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
Person p3 = operator+ (p1, p2);
// 可以簡化成:
Person p3 = p1 + p2;
(c) 全局函數又實現一次函數重載1
2
3
4
5
6
7Person operator+ (const Person &p1, int val)
{
Person temp;
temp.m_A = p1.m_A + val;
temp.m_B = p2.m_A + val;
return temp;
}
(2) 左移運算符重載
1 | int a = 10; |
(a) 利用成員函數重載 (不會使用)1
2
3
4
5void operator<<(Person &p);
//p.operator (p) 不是我們想要的效果
void operator<<(cout);
//p.operator (p) 簡化后可以寫成 p << cout 但是我們想要的結果是 cout << p
(b) 利用全局函數實現左移重載1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//cout 這個對象全局只能有一個,所以要用引用的,不能創建一個新的
//operator<<(cout, p) 簡化成 cout << p
// 返回 cout 類型,才可以連續輸出
ostream& operator<<(ostream& out, Person& p)
{
out << "m_A =" << p.m_A << ", m_B =" << p.m_B;
return out;
}
// 如果 m_A 和 m_B 是私有成員屬性,就要把重載函數設定成友元
class Person
{
friend ostream& operator<<(ostream& out, Person& p);
public:
...
}
(3) 遞增運算符重載
1 | class MyInteger |
(4) 賦值運算符重載
C++ 編譯器至少給一個類添加 4 個函數
(a) 默認構造函數(無參,函數體為空)
(b) 默認析構函數(無參,函數體爲空)
(c) 默認複製構造函數,對屬性進行值拷貝
(d) 賦值運算符 operator=,對屬性進行值拷貝
如果類中有屬性指向堆區,做賦值操作時也會出現深淺拷貝1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41class Person
{
public:
Person (int age)
{
// 將年齡數據開闢到堆區
m_Age = new int (age);
}
// 重載賦值運算符,要返回自身,才可以做連續賦值
Person& operator= (Person &p)
{
// 編譯器提供的函數是淺拷貝
//m_Age = p.m_Age;
// 應先判斷是否有屬性在堆區,如果有先釋放乾净,然後再深拷貝
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
// 深拷貝,會造成兩個不同的對象的屬性成員指針指向同一個空間
m_Age = new int (*p.m_Age);
// 返回自身
return *this;
}
~Person ()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
// 指向年齡的指針
int *m_Age;
}
(5) 關係運算符重載
1 | class Person |
(6) 函數調用運算符重載
由於重載之後使用的方式非常像函數的調用,因此稱爲仿函數
仿函數沒有固定寫法,非常靈活1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class MyPrint
{
public:
void operator ()(string next)
{
cout << text << endl;
}
};
class MyAdd
{
public:
int operator ()(int v1, int v2)
{
return v1+v2;
}
};
int main ()
{
MyPrint myFunc;
myFunc ("hello world");
MyAdd add;
int ret = add (10, 10);
cout << ret << endl;
// 匿名函數對象,當前列用完了即被釋放
cout << MyAdd ()(100, 100) << endl;
}
6. 繼承
繼承可以減少重複的程式塊
class A : public B
A 類稱爲子類 或 派生類
B 類稱爲父類 或 基類
派生類中的成員,包含兩大部分:
一類是從基類繼承過來的,一類是自己增加的成員
從基類繼承過來的表現其共性,而新增的成員體現其個性
(1) 繼承的基本語法
以簡易版頁面顯示爲例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45class BasePage
{
public:
void header ()
{
cout << " 頁面、公開課、登錄、注冊...(公共頭部)" << endl;
}
void footer ()
{
cout << " 幫助中心、交流合作、站内地圖...(公共底部)" << endl;
}
void left ()
{
cout << "Java,Python,C++...(公共分類列表)" << endl;
}
};
class Java : public BasePage
{
public:
void content ()
{
cout << "JAVA 學科影片 " << endl;
}
};
class Python : public BasePage
{
public:
void content ()
{
cout << "Python 學科影片 " << endl;
}
};
class C++ : public BasePage
{
public:
void content ()
{
cout << "C++ 學科影片 " << endl;
}
};
(2) 繼承方式
(3) 繼承中的對象模型
父類中所有非靜態成員屬性都會被子類繼承下去
父類中私有成員屬性是被編譯器給隱藏了,因此是訪問不到的,但是確實被繼承了
Q: 怎麽得知?
(a) cout << sizeof (Son) << endl;
(b) 利用工具查看
打開 Visual Studio 的 Developer Command Prompt
定位到當前.cpp 檔案的槽,cd 進入檔案所在位置
然後輸入 cl /d1 reportSingleClassLayout 查看的類名 所屬檔案名
(4) 繼承中構造和析構順序
先調用父類構造函數,再調用子類構造函數,析構順序與構造相反
(5) 繼承中同名成員的處理方式
子類對象可以直接訪問到子類中同名成員
子類對象加作用域可以訪問到父類中同名成員
當子類與父類擁有同名的成員函數,子類會隱藏父類中所有版本的同名成員函數 (包含重載),加作用域可以訪問
(6) 繼承中同名靜態成員的處理方式
同名靜態成員處理方式和非靜態成員處理方式一樣,只不過有兩種訪問方式 (通過對象 和 通過類名)
(7) 多繼承語法
C++ 允許一個類繼承多個類
多繼承可能會引發父類中有同名成員出現,需要加作用域區分
C++ 實際開發中不建議用多繼承1
2
3
4class Son : public Base2, public Base1
{
...
}
(8) 菱形繼承
(a) 菱形繼承的概念
兩個派生類繼承同一個基類
又有個類同時繼承這兩個派生類
這種繼承被稱爲菱形繼承,或者鑽石繼承
(b) 舉例
羊繼承了動物的數據,駝也繼承了動物的數據,草泥馬多繼承了羊和駝的數據
(c) 問題
草泥馬繼承自動物的數據就有了兩份,當草泥馬使用數據的時候就會產生歧義
子類繼承了兩份相同的數據,導致資源浪費以及毫無意義
(d) 解決
利用虛繼承可以解決菱形繼承的問題,相當於共享數據1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Animal
{
public:
int m_Age;
}
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
int main ()
{
SheepTuo st;
st.Sheep::m_Age =
}