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
11
int* 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
11
int* 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
14
int 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
2
int a = 10;
int &b = a;

2. 引用注意事項

引用必須初始化
引用在初始化後,不可以改變

3. 引用做函數參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 地址傳遞
void mySwap01 (int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}

// 引用傳遞
void mySwap02 (int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}

通過引用參數產生的效果和按地址傳遞是一樣的,引用的語法更清楚簡單。

4. 引用做函數返回值

(1) 當函數返回引用類型時,沒有複製返回值,返回的是對象本身。
1
2
3
4
5
// 這裡參數用到常量引用,後面會講到
const string& shorterString (const string& s1, const string& s2)
{
return s1.size < s2.size ? s1 : s2;
}
(2) 在函數的參數中,包含引用或指針,需要被返回的參數
1
2
3
4
5
6
7
8
9
10
11
12
int& abc (int a, int b, int c, int& result)
{
result = a + b + c;
return result;
}

// 可以改寫成:
int& abc (int a, int b, int c, int* result)
{
result = a + b + c;
return result;
}
(3) 不能返回局部對象的引用

當函數執行完之後,將釋放分配給局部對象的存儲空間,此時,對局部對象的引用就會指向不確定的空間。

(4) 引用返回一個左值
(5) 如果不希望返回的對象被修改,可以返回 const 引用

5. 引用的本質

引用在 C++ 内部實現是一個指針常量,指針常量本身不可改,這也説明了爲什麽引用不可更改
C++ 推薦引用,因爲語法方便,引用的本質是指針常量,但是所有的指針操作編譯器幫忙做了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void 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
4
const 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
#pragma once
#include <iostream>
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
#include "point.h"

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
3
Time *a = new Time ();
...
delete a;

(2) 構造函數的分類和調用
1
2
3
4
5
6
7
8
9
10
// 調用無參構造函數
Person p;
// 調用有參構造函數
Person p1 (10); // 刮號法
// 顯式法
Person p2 = Person (10);
Person p3 = Person (p2);
// 隱式轉換法
Person p4 = 10;
Person p5 = p4;
(3) 複製構造函數調用時機

(a) 使用一個已經創建完畢的對象來初始化一個新對象

1
2
3
4
5
Person man (100);
Person newman1 (man); // 調用複製構造函數
Person newman2 = man; // 調用複製構造函數
Person newman3;
newman3 = man1; // 這是賦值,不是調用複製構造函數

(b) 值傳遞的方式給函數參數傳值
(c) 以值方式返回局部對象
1
2
3
4
5
Time 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
7
Person (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
#ifndef TIME_H_
#define TIME_H_

#include <iostream>
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
#include "Time.h"
#include <cstdlib> //abort () 函數

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
#include <iostream>

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
11
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;
}

Person p3 = p1.opertor+(p2);
// 可以簡化成 (使用編譯器提供的名稱):
Person p3 = p1 + p2;

(b) 利用全局函數重載加號
1
2
3
4
5
6
7
8
9
10
11
Person 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
7
Person 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
2
3
4
5
6
7
int a = 10;
cout << a << endl;

Person p;
p.m_A = 10;
p.m_B = 20;
cout << p << endl; // 不知道你有什麽屬性成員 // 運算符沒有和操作數匹配

(a) 利用成員函數重載 (不會使用)

1
2
3
4
5
void 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
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
class MyInteger
{
friend ostream& operator<< (ostream& out, MyInteger myint);

public:
MyInteger ()
{
m_Num = 0;
}

// 返回引用是爲了對同一個數據進行遞增操作 ++(++a)
MyInteger& operator++ ()
{
// 先自增
m_Num ++;
// 再返回
return *this;
}

//int 代表占位參數,可以用於區分前置和後置
// 後置要返回值,不能返回引用,不然就是返回局部對象的引用,會出錯
MyInterger operator+ (int)
{
MyInteger temp = *this;
m_Num++;
return temp;
}
private:
int m_Num;
};

ostream& operator<< (ostream& out, MyInteger myint)
{
out << myint.m_Num;
return out;
}
(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
41
class 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
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
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;

}
};
(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
30
class 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
45
class 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
4
class Son : public Base2, public Base1
{
...
}

(8) 菱形繼承

(a) 菱形繼承的概念
兩個派生類繼承同一個基類
又有個類同時繼承這兩個派生類
這種繼承被稱爲菱形繼承,或者鑽石繼承
(b) 舉例
羊繼承了動物的數據,駝也繼承了動物的數據,草泥馬多繼承了羊和駝的數據
(c) 問題
草泥馬繼承自動物的數據就有了兩份,當草泥馬使用數據的時候就會產生歧義
子類繼承了兩份相同的數據,導致資源浪費以及毫無意義
(d) 解決
利用虛繼承可以解決菱形繼承的問題,相當於共享數據

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class 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 =
}

7. 多態

五、檔案操作