黃祥陞 @ Sprout 2023 C/C++語法班
Modified from @ Sprout 2022
問:請幫我建立一個陣列,裡面放 0 到 9
答:...
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
問:再幫我加 10 到 19 進去好了
答:
/* same code above */
for (int i = 10; i < 20; i++) {
arr[i] = i; // ??
}
沒辦法在寫程式時知道陣列要多大,怎麼辦?
int n;
std::cin >> n;
int arr[n];
這樣有什麼問題?
試試看輸入 100000000
[3] 70787 segmentation fault
簡單了解 stack 與 heap
小結論:一般程式執行時用 stack 為主,但
stack 的空間比較小
int n;
std::cin >> n;
int arr[n];
用 new 來跟 heap 要一塊記憶體
用 delete 來把記憶體還給 heap
int* ptr = new int; // get memory from heap
// we can use the memory now
*ptr = 3;
std::cout << *ptr << '\n';
delete ptr; // return memory to heap
ptr = nullptr; // why do this?
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
圖示解說(記憶體位置純數虛構)
0x10
0x18
0x20
0x28
0x30
0x38
int* ptr = new int;
0xa0 |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
delete ptr;
0xa0 |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
ptr = nullptr;
nullptr |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
dangling pointer: 指向已經還給 heap 的記憶體的 pointer
int* ptr = new int;
*ptr = 7;
delete ptr; // now ptr is a dangling pointer
std::cout << *ptr; // undefined behavior
delete ptr; // undefined behavior
謎之聲:還不簡單?delete 之後就不要再用就好啦!
使用注意事項之一
dangling pointer 之沒這麼簡單
int* ptr = new int;
*ptr = 7;
int* ptr2 = ptr;
delete ptr; // now ptr is a dangling pointer
ptr = nullptr; // ptr is not dangling anymore
std::cout << *ptr2; // but ptr2 is dangling
delete ptr2; // double deletion here
使用動態記憶體時要特別小心!
memory leak: 沒有還給 heap 的記憶體
int n = 5;
int* ptr = new int;
ptr = &n;
使用注意事項之二
有什麼問題??
5 |
---|
0x10(n)
0x14
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
int n = 5;
5 | 0xa0 |
---|
0x10(n)
0x14(ptr)
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
int* ptr = new int;
5 | 0x10 |
---|
0x10(n)
0x14(ptr)
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
ptr = &n;
memory leak: 沒有還給 heap 的記憶體
int n = 5;
int* ptr = new int;
ptr = &n;
使用注意事項之二
從 heap 要來的記憶體(上頁的 0xa0)沒有還回去,
也還不回去了(想想看為什麼)
註:當程式結束執行後記憶體還是會被系統回收,但是執行期間這支程式會一直佔據這塊還不回去的記憶體
memory leak 之另一個例子
void leak_memory() {
int* ptr = new int;
}
int main() {
leak_memory();
}
使用注意事項之二
有看出哪裡 leak 了嗎?:)
小結論:有借有還,再借不難
new 出一個新陣列
int* arr = new int[10]; // get an array from heap
// use it like a normal array
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
delete[] arr; // release the memory
注意:陣列的 delete 後面要加上 []
題外話之想想看:delete 沒有寫大小,程式怎麼知道要還多少回去?
小複習:arr[i] 相當於 *(arr + i) 哦
new 出一個新陣列
int n;
std::cin >> n;
int* arr = new int[n]; // get an array from heap
delete[] arr; // release the memory
這時候用變數就沒什麼問題囉!
再試試看 10000000 應該不會有問題了
小實驗:heap 通常比 stack 大上不少,試試看 n 可以到多大才會出現空間不夠的錯誤?
進階:動態二維陣列
int n, m;
std::cin >> n >> m;
int** arr = new int*[n];
for (int i = 0; i < n; i++) {
arr[i] = new int[m];
}
// now we can use arr as an array of size (n * m)
arr[0][0] = 0; // like this
for (int i = 0; i < n; i++) {
delete[] arr[i]; // notice we delete arr[i] first
}
delete[] arr; // and then delete arr
Heap 的記憶體在 delete 之前都一直可以用
int* stack_memory() {
int n = 5;
return &n;
}
int* heap_memory() {
int* ptr = new int;
*ptr = 5;
return ptr;
}
int main() {
int* s_ptr = stack_memory(); // NOT OK!
int* h_ptr = heap_memory(); // OK!
delete h_ptr; // remember to return memory
return 0;
}
謎之聲:好像教完動態陣列了?
才沒有,這只是小試身手!
先來回想一下 Struct
Struct 讓我們可以把許多資料有意義地組合起來
Class 怎麼做?
struct Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
};
class Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
};
第一個 Class 誕生了
Class 的語法其實跟 Struct 一模一樣,兩個可以互相轉換
member functions
除了資料之外,Class 也可以有 member function
#include <cmath>
class Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
玩玩看 Class
#include <iostream>
/* Definition of Shop Class here */
int main() {
Shop shop;
shop.pos_x = 10;
shop.pos_y = 20;
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
main.cpp:17:10: error: 'pos_x' is a private member of 'Shop'
shop.pos_x = 10;
^
main.cpp:5:7: note: implicitly declared private here
int pos_x;
^
main.cpp:18:10: error: 'pos_y' is a private member of 'Shop'
shop.pos_y = 20;
^
main.cpp:6:7: note: implicitly declared private here
int pos_y;
^
main.cpp:19:23: error: 'DistanceToOrigin' is a private member of 'Shop'
std::cout << shop.DistanceToOrigin() << '\n';
^
main.cpp:10:10: note: implicitly declared private here
double DistanceToOrigin() {
^
3 errors generated.
編譯失敗... 為什麼??
Class 的 member variable 跟 function 可以是 public 或 private
Class 預設所有東西都是 private
private vs public
回頭看一次剛剛的 main
#include <iostream>
/* Definition of Shop Class here */
int main() {
Shop shop;
shop.pos_x = 10;
shop.pos_y = 20;
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
main 並不是 Shop 的 member function,所以不能存取 Shop 的 private member
設定 private/public
#include <cmath>
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
一般 C++ Program: 以 function 為基礎
char s[10] = "hello";
std::cout << strlen(s) << '\n';
OOP C++ Program: 以物件為基礎
std::string s = "hello";
std::cout << s.length() << '\n';
Constructor 是一個特別的 function
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
Shop() {
std::cout << "A shop is created\n";
}
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
試試看
int main() {
Shop shop; // create a shop object
return 0;
}
Output:
A shop is created
初始化物件
Constructor 最常見的用途是用來初始化這個物件,例如 member variable
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
Shop(int x, int y) {
pos_x = x;
pos_y = y;
net_income = 10000;
type = false;
}
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
試試看
int main() {
Shop shop(3, 4); // create a shop object
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
Output:
5
原本的 constructor 呢?
int main() {
Shop shop; // create a shop object
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
Output:
main.cpp: In function 'int main()':
main.cpp:23:10: error: no matching function for call to 'Shop::Shop()'
23 | Shop shop;
| ^~~~
發生什麼事?
當你沒有定義 constructor 時,C++ 會自動提供 default constructor,不吃任何參數
當你定義了自己的 constructor 之後,C++ 就不再提供 default constructor 了
你可以定義很多 constructor,但他們的參數型態必須不同
Dive Deeper
如果 class 裡有 const 該怎麼初始化呢?
class Shop {
private:
const int pos_x;
const int pos_y;
public:
Shop(int x, int y) {
pos_x = x;
pos_y = y;
}
};
試著執行看看!
為什麼編譯錯誤?
上頁的作法其實類似於以下的 code
int x = 0;
int y = 0;
const int pos_x;
const int pos_y;
pos_x = x;
pos_y = y;
但是 const 變數一旦宣告之後就不能更改了!
那麼應該怎麼做呢?
我們想要達成以下的效果
int x = 0;
int y = 0;
const int pos_x = x;
const int pos_y = y;
class Shop {
private:
const int pos_x;
const int pos_y;
public:
Shop(int x, int y) : pos_x(x), pos_y(y) {}
};
Constructor 可以這樣寫
通常建議用第二種做法
Destructor 是一個特別的 function
class Shop {
private:
int pos_x;
int pos_y;
public:
~Shop() {
std::cout << "We are closed.\n";
}
};
何時被呼叫?
void out_of_scope() {
Shop shop;
} // will call destructor
int main() {
out_of_scope();
return 0;
}
Out of Scope
delete
void free_from_heap() {
auto new_shop = new Shop;
delete new_shop; // will call destructor
}
int main() {
free_from_heap();
return 0;
}
1. 支援不同型態
DynamicArray<int> int_arr;
DynamicArray<std::string> string_arr;
2. 把新的元素加到最後面
DynamicArray<int> arr; // empty
arr.push_back(0); // 0
arr.push_back(1); // 0 1
3. 像一般陣列一樣存取元素
DynamicArray<int> arr;
arr.push_back(0);
std::cout << arr[0]; // print 0
arr[0] = 1;
std::cout << arr[0]; // print 1
4. 取得陣列裡有幾個元素
DynamicArray<int> arr;
std::cout << arr.size(); // print 0
class DynamicArray {
size_t size_;
size_t capacity_;
int* arr_;
public:
DynamicArray();
size_t size();
void push_back(int value);
~DynamicArray();
};
先以 int 為例
設計說明
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
容量從 1 開始
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
一開始沒有任何元素
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
實際取得 capacity_ 大小的空間
void push_back(int value) {
arr_[size_++] = value;
}
一般情況下沒有問題,但如果 arr_ 的空間不夠了呢?
從 heap 拿更多的空間,但要拿多少呢?
常見的做法是拿一塊相當於現在容量 1.5 或是 2 倍的空間。這裡我們使用 2 倍
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
取得一塊更大的空間
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把舊的元素搬到新的空間裡
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把舊的空間還給 heap
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把 arr_ 指向新的空間
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
更新現在的容量
void push_back(int value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
動態陣列最重要的功能:在空間不夠時自動增長,使用者只需要像操作一般陣列就好
class DynamicArray {
size_t size_;
size_t capacity_;
int* arr_;
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
public:
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
size_t size();
void push_back(int value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
~DynamicArray();
};
最簡單的啦!
size_t size() {
return size_;
}
看起來好冗,為什麼不直接把 size_ 變成 public?
當 size_ 變成 public 時,外界就可以自由操作,例如
DynamicArray arr; // empty
arr.size_ = 2; // messing up
維持 member variable 是 private 確保只有自己可以去更改
要如何用 [] 來操作動態陣列?
[] 其實是一個 operator,跟 + 或是 = 一樣
我們需要提供 [] 的操作,稱為 operator overloading
直接上語法
int& operator [](int index) {
return arr_[index];
}
現在跟一般陣列一樣可以用 [] 來存取元素囉
在消失前,還得先善後
~DynamicArray() {
delete[] arr_;
}
永遠記得要把借來的記憶體還回去
現在加入 template 來支援不同
template <typename T>
class DynamicArray {
size_t capacity_;
size_t size_;
const int growth_rate_ = 2;
T* arr_;
// ...
現在加入 template 來支援不同
void grow() {
T* temp = new T[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
T& operator [](int index) { return arr_[index]; }
void push_back(T value) {
if (size_ == capacity_) {
// ...
template <typename T>
class DynamicArray {
size_t capacity_;
size_t size_;
const int growth_rate_ = 2;
T* arr_;
void grow() {
T* temp = new T[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
public:
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new T[capacity_];
}
T& operator [](int index) { return arr_[index]; }
size_t size() { return size_; }
size_t capacity() { return capacity_; }
void push_back(T value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
~DynamicArray() { delete[] arr_; }
};
成品
int main() {
DynamicArray<int> arr;
for (int i = 0; i < 10; i++) {
arr.push_back(i);
}
for (int i = 0; i < arr.size(); i++) {
arr[i]++;
std::cout << arr[i] << ' ';
}
return 0;
}
登登~動態陣列
Output
1 2 3 4 5 6 7 8 9 10