--- title: '記憶體 & 連結 & 名稱空間' disqus: kyleAlien --- 記憶體 & 連結 & 名稱空間 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: [TOC] ## 獨立編譯 * C/C++ 鼓勵將函數原件放置於獨立檔案中,在 **修改其中一個檔案後編譯只需做到 ==連結== 就可以使用** * 大部分的 C++ 環境提供額外的功能幫忙管理程式 > UNIX & Linux 系統提供 make 程式 : 它記錄程式與哪些檔案相關,以及檔案的**最後修改時間** :+1: ### include 檔案 * 如果有多個檔案要同一個結構、Class,要將結構宣告放至每個檔案中,這樣容易導致錯誤,並且不易管理 (如果要修正結構,就必須改所有宣告的地方),這時就可使用 #include * 以下是標頭檔中**常存在**的內容 (大部分放置在標頭檔中) | 表示 | 內容 | 補充 | | -------- | -------- | -------- | | void print(); | 函數**原型** | Non | | #define MSG 1 | 定義符號 | Non | | const int i = 0 | 常數定義 | Non | | struct Name_T | 結構**宣告** | 只限至於宣告而非定義 | | class Hello {} | 類別宣告 | Non | | <template typename T\> class Hello{} | 樣板宣告 | Non | | inline void print() {} | 內嵌函數 | 可以存在實體內容 | * include 時使用的符號會影響到 C++ 編譯時期如何找到檔案;**使用正確的符號可以減少編譯所花費的時間** | 符號 | 表示 | 尋找 | | -------- | -------- | -------- | | ==""== | #include "coordin.h" | 工作(Project)目錄 | | ==<>== | #include <string.h> | 標準標頭檔 | ### include 注意事項 * **不要 include 原始碼**檔案 ( `.c`、`.cpp` 檔案),會**導致多重定義**,所以 **不要將==函數定義== & ==變數宣告==**,寫在標頭檔中 > 假設定義函數在標頭檔中,程式 include 兩個檔案中,會導致兩個 .c 中會存在兩分定義,下圖表示此錯誤 > ![](https://i.imgur.com/QvAJGGQ.png) ```cpp= ifndef COORDIN_H_ #define COORDIN_H_ #include <iostream> #define MSG 1 const int i = 0; inline void print() { std::cout<< "Hello World" + MSG << std::endl; } /* Err: 不可定義函數在標頭檔中 int a = 20; void print() { std::cout<< "Hello World" + MSG << std::endl; } */ #endif /* COORDIN_H_ */ ``` **--實做--** > 以下是在標頭檔中**定義**,編譯其間被抓出錯誤,**多重定義** `multiple definition of print()` > ![](https://i.imgur.com/ox0Lvjs.png) ### include 標頭檔管理 * 為了避免多重匯入檔案,可以使用**兩個方法** | 方法 | 解釋 | | -------- | -------- | | #ifndef、#endif | 使用預編譯判斷 define | | #pragma once | pragma 也是預編譯,但是由各家 chip 廠商,各自實現 (也就是不一定會有) | * 以保證可行的方法來實現,使用 #ifndef、#endif,其實它無法避免成式匯入兩次,但**可以保證第二次匯入時 ++不會被編譯++ (==第二次匯入為空==)**,直接跳至 #endif 之後 > ![](https://i.imgur.com/ycvI3cx.png) ### Compiler 編譯 * **編譯後的檔案 C++ 使用翻譯單元 (translation unit) 來==取代== 原本檔案** :::info * 多重涵式連結失敗? 修改相同的編譯器 **不同編譯器編譯原始碼 or 函式庫時產生的二進制檔案不盡相同**,所以有可能 **在 Linked 階段不能很好的連接**,所以這時只要使用同個編譯器編碼全部就可進行連接 ::: * C++ 使用的翻譯單元 (translation unit),名稱取名為 檔案 (file);也就是說 **C++ 編譯單位是檔案 file**,這可以包留較大的一般性 (其他平台都有這個單元) ## 儲存期間 **儲存期間**會影響到程式如何跨檔案共用,而 **C++ 有四種儲存期間** | 儲存期間 | 解釋 | 代表 | | -------- | -------- | -------- | | ==自動== 儲存期間 | 當函數進入==函數區塊==時會**自動產生** | 區域變數 | | ==靜態== 儲存期間 | 使用關鍵字 static 包覆 or 區塊以外的變數,在程式執行期間他們會存在 | 內部 static、外部 static、全域變數 | | ==執行緒== 儲存期間 | 其變數的生命週期同執行它的 thread 生命週期 | thread | | ==動態== 儲存期間 | 生命週期由 new 至 delete,並==存於堆區== | `new`/`delete`、`malloc`/`free` | ### 記憶體區塊 | 記憶體區塊 | 補充 | | -------- | -------- | | 自由 區塊 | 使用 Stack | | 動態 區塊 | Heap 區塊 | | 靜態 區塊 | 內部還會再細分 `data`(靜態數據)、`text`(程式) | ### 範疇 (Scope) * 變數的 **可見範圍** ```java= static d = 20; namespace { int c = 30; } int b = 20; // 變數 b 的可見範圍是整個檔案中 int main() { /** * 變數 a 的可見範圍僅有 main() function 的區塊中 */ int a = 10; return 0; } ``` | 範疇類形 | 意義 | 範例代表 | | -------- | -------- | -------- | | 區域範疇 | **區塊** 間才知道它的存在 (區塊代表大括號 {} 之間) | a (區域變數) | | 全域範疇 | 也稱為 **檔案範疇**,整個檔案都知道其存在 | b (全域變數) | | 類別範疇 | 宣告在 **類別**中的成員 | [**參考**](https://hackmd.io/32uk2K2ZSUKoCKYlidy-Dg?both#%E9%A1%9E%E5%88%A5%E7%AF%84%E7%96%87) | | 名稱空間範疇 | 宣告在 **名稱空間** 的成員,不再該空間的成員,就不具有該變數 | c (namespace) | ### 連結性 (Linked) * 描述不同元素在**不同區塊是否能共用** ```java= static d = 20; namespace { int c = 30; } int b = 20; // 變數 b 的可見範圍是整個檔案中 int main() { /** * 變數 a 的可見範圍僅有 main() function 的區塊中 */ int a = 10; return 0; } ``` | 連結性 | 意義 | 範例代表 | | -------- | -------- | -------- | | 無連結 | Func 內部一般變數 | a | | 內部連結 | 檔案內部 static 變數 | d | | 外部連結 | 檔案內部一般變數 | b |ja ## 自動儲存期間 ### 自動儲存 * 變數的**記憶體配置是在,進入區塊內時才會產生**,一離開區塊就自動釋放記憶體 * 進入區塊時才會配置變數,**範疇從==宣告時==就已經存在** ```cpp= int a = 0; int main() { int a = 10; //"1. " { std::cout << a << std::endl; } std::cout << a << std::endl; //"2. " return 0; } ``` 1. 內部變數 a 會 **==暫時覆蓋==外部範疇的 a**,直至大括弧結束 2. 一來開區快就會離開覆蓋,變回外部範疇 > ![](https://i.imgur.com/mvG4ja1.png) ### 自動變數的初始化 * 當 **定義出變數後並不代表已初始化了**,**++自動變數必須要靠指定才能初始化++,尚未初始化則會是不確定值** ```cpp= #define MY_VALUE 20 void local() { int a; // indeterminate 不定 int b = 0; // 初始化 int c = MY_VALUE - 1; // 藉由 constant value 初始化 } ``` ### 自動變數 & 堆疊 * 進一步了解編譯器是如何處理自動變數 (區域變數) 1. Stack 撥出一塊記憶體 2. 對**自動變數進行堆疊 (Stack、FILO)**,此堆疊大小可改變 3. 離開區塊後變數也從堆疊上移除 * 而它的實現是同於 [**Java 的資料結構 Stack**](https://hackmd.io/Cq9VpmG7RoORzqPKvAdHuA#動態-Stack),並不會真正去清掉其值,而是透過兩個指標 (rear & last) 的移動去儲存值,所以**移除時也指是移動指標,==舊的資料並不會被清除==** > ![](https://i.imgur.com/QqOgPzR.png) ### 修飾符號 register * **==變數存入 CPU 暫存器==,而不是內存記憶體內** * **register 只能用來 ++修飾自動變數++**,**==目的==是為了==更快速地做出存取動作==**,使用 register 的原因有如下 1. 用來**強調該變數是自動變數** 2. **更重要的原因**是,使用 register 存入暫存器 **==避免使用該關鍵字的代碼失效==** ## 靜態儲存時間 :::success **靜態變數的重點在於連結性 (Linked)** ::: * 靜態儲存變數不會隨著 RunTime Work 改變,它的生命週期是整個程式的開始 & 結束,**靜態變數不會自動式放**,它跟 Class 對象的生命週期沒有關係 ```cpp= static d = 50; //"3. " int c = 30; //"2. " int main() { static int a; //"1. " int b = 20; return 0; } ``` 1. 靜態期間,無連結 2. 靜態期間,**外部連結**,所以**一般變數就是靜態變數,==差別在連結性==** 3. 靜態期間,內部連結 (檔案內才可使用) ### 靜態變數特性 * **它有特殊的記憶體儲存區塊**,不像自由儲存變數,它**並不使用堆疊 (Stack) 儲存** * 靜態變數若無指定初始化,**編譯器會將靜態變數++自動初始化為 0++**,稱為 ==0 的初始化== 以下有 `自動變數`、`靜態變數` 的比較 | 儲存方式 | 期間 | ==範疇== | ==連結性== | 宣告方式 | 初始化 | | -------- | -------- | -------- | -- | -- | -- | | 自動 Stack | 自動 | 區塊 | 無 | 變數宣告在區塊中 (auto 隨意) Ex: {int a; auto int b} | 指定初始化 | | CPU 暫存器 | 自動 | 區塊 | 無 | 必須使用關鍵字 register Ex: {register int a;} | 指定初始化 | | 靜態記憶體區塊 | 靜態 | 區塊 | 無 | 必須使用關鍵字 static Ex: {static int a;} | 0 的初始化 | | 靜態記憶體區塊 | 靜態 | 檔案 | ==內部== | static **宣告在所有函數外** Ex: static int a; | 0 的初始化 | | 靜態記憶體區塊 | 靜態 | 檔案 | ==外部== | **宣告在所有函數外** Ex: int a; | 0 的初始化 | :::info 靜態初始化為 0 或是尚未初始化就會放在 `.bss` 區塊,否則大部分放在 `.data` 區塊 ::: ```cpp= static int apple2 = 10; // 內部連結 int apple = 10; // 外部連結 void local_field(){ int apple = 10; // 無連結 register int banana = 11; // 無連結 static int coconut = 12; // 無連結 } ``` ### 靜態初始化 :::info 靜態並不是說 static,而是在編譯期間就決定的程式儲存的區域、連結 ::: * 一開始皆是由 0 初始化,之後會分為靜`態初始化`、`動態初始化` * 靜態初始化 : 經由**常數運算式初始化** **(==編譯期間==解析運算式)** * 動態初始化 : 當下沒有足夠資訊可初始化,就以動態初始化 **(==連結期間==函數呼叫)** ```cpp= #include <iostream> #include <cmath> int main() { int x; //"1. " int y = 5; //"2. " int z = 10 * 10; const double pi = 4.0 * atan(1.0); //"3. " std::cout << pi << std::endl; return 0; } ``` 1. 宣告 x 出來後並無指定,為 0 的初始化 2. 宣告 y、z 出來後,解析運算式指定初始化值 3. 由於 pi 變數牽扯到 **`atan` 函數**,會導至**初始化推遲到連結期間,程式運行期間才完成,稱為動態初始化** ### 靜態期間、外部連結 * 這種變數宣告在所有函式之外,又稱為 **外部變數 (external variable)**、**全域變數** * 單一定義 one definition rule (odf),**變數的定義只能有一次** > C++ 變數的宣告: > > 1. **==定義==宣告 (defining declaration)**,==能夠初始化並配置空間==,不用使用 extern > > > > 2. **==參考==宣告 (referencing declaratoin)**,這種宣告 ==不提供== **初始化**,一定要使用 extern ```cpp= #include <iostream> extern int a = 10; // "1. "Warning: 定義初始化 & 參考 int b = 20; // 定義初始化 extern int c; // "2. "Err: 參考,但沒有定義,編譯器會報錯 int main() { std::cout << "a : " << a << std::endl; std::cout << "b : " << b << std::endl; std::cout << "c : " << c << std::endl; return 0; } ``` 1. extern 如果做為內部使用的參數就不是必需的 2. 參考宣告沒問題,但是如果沒有定義 (參考來源),就會報錯 > 變數 c **未定義就被參考,會報錯** > ![reference link](https://i.imgur.com/TrbHpp1.png) * extern 範例 1. 定義外部連結檔案 `a` ```cpp= #include <iostream> extern int a = 10; // 不必 Extern int b = 20; void testExtern(); // TestExtern.cpp 函數宣告 int main() { std::cout << "a : " << a << std::endl; std::cout << "b : " << b << std::endl; testExtern(); std::cout << "a : " << a << std::endl; std::cout << "b : " << b << std::endl; return 0; } ``` 2. 宣告外部連結,`a`、`b` Symbol 必須在另外一個檔案中定義 ```cpp= #include <iostream> extern int a; extern int b; void testExtern() { a = 1; b = 2; std::cout << "testExtern, a : " << a << std::endl; std::cout << "testExtern, b : " << b << std::endl; } ``` * 兩個原始檔 (.c檔),**不用標頭檔 (.h) 連結就可以有關係**,只要有宣告就可呼叫函數 * 定義宣告就不需要 extern,如同上面範例的變數 b :::info **遮蔽、範疇運算子** ::: * 如果全域變數與區域變數同名,可以使用範疇運算子 `::` 來指定全域變數 ```cpp= #include <iostream> // a、b 會在外部 (另一個檔案) 定義 extern int a; extern int b; void testExtern() { //extern int a; // 可不必,因為外部以參考 a auto a = 0; //"1. " std::cout << "區域自動變數, a : " << a << std::endl; //"2. " std::cout << "全域靜態變數, ::a : " << ::a << std::endl; ::b = 90; std::cout << "全域靜態變數,,改 ::b : " << ::b << std::endl; } ``` 1. 同名自動變數 & **外部全域變數碰撞,會暫時遮蔽全域變數**,這時內部呼叫 a 就是區域自動變數 0 2. 使用範籌運算子 **==::==**,**表示獲取全域版本的變數** > ![reference link](https://i.imgur.com/v3PlRhq.png) ### 靜態期間、內部連結 * 使用 **static 修飾** 變數,該變數就是內部連結的靜態變數,並且該變數記憶體就不存在 Stack 區塊,而是存在靜態區 * 靜態變數會覆蓋 Override 全域變數,自動變數又會 Override 靜態變數 > 優先權 : 自動 (區域) 變數 > 靜態變數 > 全域變數 :::info **未來內部連結的靜態儲存會被,匿名 namespace 取代** ::: 1. 全域變數、外部連結 ```java= #include <iostream> int a = 10; int b = 20; void testExtern(); int main() { std::cout << "a : " << a << ", addr: " << &a << "\n"; std::cout << "b : " << b << ", addr: " << &a << "\n"; testExtern(); std::cout << "a : " << a << ", addr: " << &a << "\n"; std::cout << "b : " << b << ", addr: " << &a << "\n"; return 0; } ``` 2. 全域變數、內部連結 (static) ```java= #include <iostream> extern int a; // 在另外一個檔案定義 static int b = 99; void testExtern() { ::a = 20; std::cout << "外部連結靜態, a : " << a << ", addr: " << &a << "\n"; b = 50; std::cout << "內部連結靜態, b : " << b << ", addr: " << &b << "\n"; int b = 999; std::cout << "自動變數, b : " << b << ", addr: " << &b << "\n"; } ``` **--實做--** > ![](https://i.imgur.com/TbGKVNZ.png) ### 靜態期間、無連結 - 函數中的 Static 變數 * 在 **區塊內使用 static 修飾變數,會使成是內部的變數具有靜態儲存期間**,但是又因為在 **區塊內所以是無連接**,在 **離開區塊** 時又 **++不會釋放靜態變數的記憶體++** * 靜態區塊 (static) 變數的 ==初始化只會有一次== ```cpp= int main() { std::cout << "main call 1 time" << "\n"; testStatic(); std::cout << "main call 1 time" << "\n"; testStatic(); std::cout << "main call 1 time" << "\n"; testStatic(); return 0; } void testStatic() { static int x = 0; //"2. " 靜態初始化,離開函數後,也不會釋放 x += 10; std::cout << "x value: " << x << ", addr: " << &x <<"\n"; /* int x = 20; //"1. " std::cout << "x : " << x << &x <<"\n"; */ } ``` 1. **自動變數 & 靜態區域變數,兩者都使無連結,所以取同名會相互衝突** 2. static 變數只會初始化一次,**預設也會自己 0 初始化** > ![](https://i.imgur.com/FmE90xi.png) ### 指示字、修飾字 * 指示字 : **儲存類別指示字**;修飾字 : **常數可變修飾字** | 描述 | 類型 | 功能 | 補充 | | -------- | -------- | -------- | - | | auto | 指示字 | 自動變數 & 自動推導 | | | rigister | 指示字 | 存入暫存器、防失效 | | | static | 指示字 | 存入靜態記憶體、內部連結 | | | extern | 指示字 | 存入靜態記憶體、外部連結 | | | const | **修飾字** | 定義後不可在被修改 | | | volatile | **修飾字** | **禁止編譯器優化** | | | thread_local | 指示字 | 生命週期同 thread | 可與 extern、static 一起使用 | | mutable | 指示字 | 使被 const 修飾過的變數可改變 | | 1. **volatile** 解釋 > 編譯器對於程式碼覺得多餘的部分會進行優化 ```cpp= int a = 0; volatile int b = 0; //"1. " a = 10; // first time a = 10; // second time //"2." b = 10; // first time b = 10; // second time ``` * 對 a 指定了 10 兩次,編譯器會覺得是多餘的程式碼,會進行優化省略第二次的指定 (不會更動到記憶體) * 同樣是對 b 指定了兩次,但是**差別在於,==每次指定的值都會存入記憶體中==** 2. **const 儲存類別** > 1. **const 會修改全域變數 (一般全域) 為區域性 ==內部連結 ?==**,**C++ 會將 const 定義是為使用了 static** (**可使用 extern 可抓取到**) > > 2. const 資料需要初始化 > > 3. **如果是全域性的 const 變數,則會產生衝突** :::warning * **const 修飾全域的變數,連結性 ?** 如果 const 修飾全域的變數,這也代表了它有外部連接的特性 (外部連接) ! 外部檔案如果要使用就必要用 `extern` 關鍵字取得 ::: ```cpp= ...Test.cpp 檔案 #include <iostream> const char* say = "Hello World"; //static char* say = "Hello World"; // Same like upper void testExternConst(); int main() { testExternConst(); return 0; } // ----------------------------------------------------------------- ...TestExtern.cpp 檔案 #include <iostream> extern char* say; // 可以使用 extern 去抓取到內部連結 const 常數 //const char* say = "Hello World"; 衝突 ~~~~~!!!! (外部連結) void testExternConst() { std::cout << "testExternConst, " <<say << "\n"; } ``` 3. **mutable** 範例:使某一個 const 的變數轉為可改變數 ```cpp= #include <iostream> struct MyStruct{ char name[10]; mutable int age; //"使不可修改的變數可以修改" int c; }; // m 結構內容不可改 const MyStruct m = {"Kyle", 20}; int main() { std::cout << "Original name: " << m.name << "\n"; std::cout << "Original age: " << m.age << "\n"; m.age = 21; std::cout << "Change age: " << m.age << "\n"; return 0; } ``` ### 函數 & 連結性 * **儲存期間** : **所有的函數都有==靜態儲存期間==**,只要程式執行就會一直存在,值到整個程式結束 * **連結性** : 預設為 ==**外部連結**==,表示可跨檔案共用 (當然你可以使用 `static` 將函數、變數限制在檔案內) ```cpp= ...Test.cpp 檔案 #include <iostream> const char* say = "Hello World"; extern void testExternConst(); // "1. " extern 可加可不加 void showPrint(); // "2. " 自動為外部連結 static void testStaticMethod(); //"3. " int main() { testExternConst(); showPrint(); testStaticMethod(); return 0; } ``` 1. 函式宣告,代表此函式存在於別的檔案,可加 **extern,也可不加 extern** 2. 可看到同時宣告了兩個同樣的函式 showPrint(),在 main 中的是外部連結函式,全域中只能有一個 showPrint() 3. 在其他檔案中定義宣告靜態內部連結函式,在 main 中是無發呼叫的,因為**靜態內部連結其他檔案無法連接到** ```cpp= void showPrint() { std::cout << "Hello World in main" << std::endl; } ...TestExtern.cpp 檔案 #include <iostream> extern char* say; static void showPrint(); // "4. " //const char* say = "Hello World"; void testExternConst() { std::cout << "testExternConst, " <<say << "\n"; showPrint(); } static void showPrint() { std::cout << "Hello World in testExternConst" << std::endl; } static void testStaticMethod() { std::cout << "Hello World in testExternConst" << std::endl; } ``` 4. 在其他檔案中的 showPrint(),**使用 ==static 表示為這是內部函數==,==內部連結會覆蓋 (Override) 外部連結==**,導致 testExternConst 在呼叫 showPrint() 是呼叫到內部連結 **--實做--** > 圖中可以看出同樣函數 `showPrint` 使用 static 修飾,就變成了內部連結函數,`testStaticMethod` 則無法呼叫到外部定義函數 (在其他檔案中) > > ![](https://i.imgur.com/DTGEG42.png) ### 語言連結、多檔案連結 - 函數簽名 * **既然是靜態儲存**,對於 **每一個==函數==都需要有不同的符號名稱**,C/C++ 處理的方法也不同,看以下兩個函數 ```cpp= void showPrint(); void showPrint(String); ``` 1. **C 的語言連結**:**不關心參數**,因此上面兩個函數都**相同翻譯**,這也就是為何 C 語言中不可以有函數重載 | 函數原型 | C 函數簽名 | | - | - | | void showPrint() | \_showPrint | | void showPrint(String) | \_showPrint | 2. **C++ 的語言連結** :**關心參數**,因此上面兩個函數都 **不同翻譯** | 函數原型 | C++ 函數簽名 | | - | - | | void showPrint() | \_showPrint | | void showPrint(String) | \_showPrint_S | * 分辨 C/C++ 函數:如果 C 語言與 C++ 聯合開發 (使用 C++ 編譯器),同時都有一個同名函式,這時可以使用 **==唯一指示字== 來區分** :::success C++ 可以調用 C,但 C 不能調用 C++ ::: 1. 指定使用 C 函數 ```cpp= #include <stdio.h> void hello(int times) { for(int i = 0; i < times; i++) { printf("C ~ Hello World.\n"); // C 語言實現 } } // -------------------------------------------------------- #include <iostream> using namespace std; // "C" 代表唯一指示字 extern "C" void hello(int); int main() { hello(3); return 0; } ``` > ![](https://i.imgur.com/NFtBjED.png) 2. 指定使用 C++ 函數 (預設) :::warning * C++/C 不可同時指定同一個函數名稱,會導致衝突 ```cpp= extern "C" void hello(int); extern "C++" void hello(int); ``` > ![](https://i.imgur.com/cQTrI43.png) ::: ```cpp= #include <iostream> using namespace std; void hello(int times) { for(int i = 0; i < times; i++) { cout << "C++ ~ Hello World." << endl; } } // -------------------------------------------------------- #include <iostream> using namespace std; // "C++" 代表唯一指示字 (其實也可以省略,因為預設就是 c++) extern "C++" void hello(int); int main() { hello(3); return 0; } ``` > ![](https://i.imgur.com/A4HvOOY.png) ### 決定使用函數 1. 指示語 `static` 會在該檔案找尋函數 2. 外部連結 (一般函數) 會檢查所有檔案 (`#include "..."`),如果找到**兩個定義**,編譯器會報錯 **(外部連結只能有一個定義)** 3. 都找不到會搜尋函式庫 (`#include <...>`),**如果定義了與函式庫相同名稱了函式,編譯器會先使用內部函式定義** (C++ 其實會禁止) ```cpp= #include <iostream> #include <cmath> // cmath 內函式原型 double sqrt(double); double sqrt(double d) { std::cout << "Define in main" << std::endl; return 0.00; } int main() { double r = sqrt(5.00); std::cout << "sqrt: " << r << std::endl; return 0; } ``` 從結果可以看出同樣式定義了函式庫中的 `sqrt()`,編譯器取用自己定義的 `sqrt()`,而不是使用了標準函式庫中的函數 (內外覆蓋外部定義) **--實作--** > ![](https://i.imgur.com/pTZ10wk.png) ## 動態儲存時間 * 動態記憶體分配的記憶體區塊,在 **堆區 (Heap)**,分配出的記憶體 ==**不受範疇、連結結規範**==,**可以函數 ^1.^ 配置,並在函數內 ^2.^ 釋放** * **分配出的指標位子,會跟隨 delete 一起消失**,但是接收的指標不會消失 (只有內容消失),所以記得要將指標覆值為 nullptr :::info **new 失敗時會丟出,==std::bad_alloc==** ::: | 語言 | 動態分配方法 | 記憶體區塊 | | -------- | -------- | -------- | | C | malloc() / free() | 堆區 | | C | calloc() / free() | 堆區 | | C | ralloc() / free() | 堆區 | | C++ | new / delete | 堆區 | | C++ | new[] / delete[] | 堆區 | ```cpp= ...Test.cpp 檔案 (main) #include <iostream> extern void freePtr(); static void location(); float *a; //"1. " int main() { float c = 30.0; std::cout << "靜態儲存, b 分配位置: " << &b << "\n"; std::cout << "自動儲存, c 分配位置: " << &c << "\n"; location(); freePtr(); return 0; } static void location() { a = new float[20]; //"2. " if(a != nullptr) { std::cout << "動態儲存,a 分配位置: " << a << "\n"; } else { std::cout << "動態儲存,a 分配失敗" << "\n"; } } // ------------------------------------------------------------ ... TestFile.cpp (其他檔案) #include <iostream> extern float *a; void freePtr() { delete(a); //"3. " if(a != nullptr) { std::cout << "動態 a 釋放 sucess" << std::endl; } else { std::cout << "動態 a 釋放 fail" << std::endl; } } ``` 1. 因為 a 宣告為靜態外部指標,其他檔案可以透過 extern 取得該指標 2. 透過 new 動態產生區塊,指標 a 的生命週期與 new/delect 相同 3. 可看出 **指標 a 的內容可在不同檔案中釋放** (只要誰有辦法取得該指標就可以),**但是指標 a 並沒有消失 (還是在靜態儲存區)** > ![](https://i.imgur.com/95nthED.png) ### new 運算子初始化 * 當配置一個動態空間後,**如果未初始化其值式隨機的** 單個值,可以透過括弧 `()` 初始化 (也可以使用 `{}` ),但如果是 `array`、`struct` 需要透過大括弧 `{}` 初始化 ```cpp= #include <iostream> struct Mystruct { char a; short b; int c; }; int *single; float *array; Mystruct *mStruct; int main() { std::cout<< "single address: " << single << std::endl; // 1. single = new int{2}; array = new float[20]{10.9, 1.0, 20.3}; mStruct = new Mystruct { .a = 1, .b = 10, .c = 100 }; std::cout<< "single value: " << *single << std::endl; std::cout<< "array[1] value: " << array[1] << std::endl; std::cout<< "mStruct#c value: " << mStruct->c << std::endl; delete (single); if(single == nullptr) { std::cout<< "指標 single \"內容\"釋放\n"; } else { std::cout<< "After delete single is not null: " << *single << std::endl; } // 2. 靜態指標 if(single != nullptr) std::cout<< "指標 single 仍在\n"; return 0; } ``` 1. **透過動態分配出來的記憶體,都是使用指標接收**,並將該指標存入靜態指標中 2. delete 只刪除指標內容,但是**靜態儲存的指標是無法被刪除的** > **delete 後指標位子 ==(堆中的位子) 仍然在==,但是其值就會不見** > > ![](https://i.imgur.com/JpGViZj.png) ### new 運算子、原型 * new 它屬於運算符號,先看它的 operator 原型,**size_t 為所需的位元組** | 操作 | 原型 | | -------- | -------- | | new() | void* opeartor new(std::size_t); | | new[]() | void* opeartor new[\](std::size_t); | ```cpp= // 概念程式 int *pi = new int; // 同上 int *pi = new(sizeof(int)); // ----------------------------------------------------- int *pArray = new int{40}; int *pArray = new(40 * sizeof(int)); // 可翻譯成這個結果 ``` ## 定位放置 new 運算子 * 一般來說使用 `new` 運算子,程式會尋找一塊足夠大的位子分配 (足夠容納要存放的資料),而其實 **new 也是可以 ==指定== 其記憶體位置** :::info 一般來說透過 new 來創建的對象,會自動分配記憶體位置 ::: > 型態 宣告 = new (指定位子) 型態; ```cpp= #include <iostream> #include <new> using namespace std; struct MyStruct { char a[10]; short b; int c; }; struct MyStruct *ms1; struct MyStruct *ms2; char buffer[100]; int main() { ms1 = new MyStruct; // 將 `ms2` 指定位子到 `buffer` 的位子 ms2 = new(buffer) MyStruct; cout<< "ms1 addr: " << ms1 << "\n" << endl; cout<< "buffer addr: " << &buffer << "\n"; cout<< "ms2 addr: " << ms2 << "\n"; } ``` **--實作--** > ![](https://i.imgur.com/75izrLA.png) ## 名稱空間 * 當專案大後,**靜態全域儲存就很容易發生衝突的狀況 (函式、變數取名衝突)**,而**解決這個問題的方法就是 namespace** | 用語 | 解釋 | | -------- | -------- | | 宣告區域 | 有關於連結性,**該變數可以被誰使用** | | 可能範疇 | 有關於範疇,也就是變數的作用域,被宣告出來在結束前就稱為可能範疇,**直接使用到的 ==區塊== 稱為範疇**,被遮蔽時就不是範疇 | | **名稱空間** | **每一個變數皆有一個持有者,不同持有者可以有相同名稱的變數** | 下圖可看到被區塊遮擋的 a 就不是範疇,被自由變數遮蔽的 temp 也不是範疇 > ![](https://i.imgur.com/3a4b3RX.png) :::warning * 可放置於全域空間,但 **不可以放置於區塊內部** (不可放置於 Function 中) > ![](https://i.imgur.com/z6u19NJ.png) ::: ### using 宣告 & using 指令 * using 關鍵字可以使整個命名空間可用,在 using 之後不必在指定命名空間,使用方式也分為以下兩種,代表了不同功能 | 功能 | using 使用範例 | 意義 | | -------- | -------- | -------- | | 宣告 | using std::cout | 宣告之後使用,引入 ==**特定**== 名稱空間的變數,就只需用 cout 即可 | | 指令 | using namespace | 指令之後使用,就可以使用 namespace 的**所有**函數、變數...,引入 ==**全部**== 名稱空間的變數 | :::success using 宣告會比 using 指令更安全點 ::: * **using 宣告、指令** ```cpp= #include <iostream> static void UsingDeclare(); static void UsingCommand(); namespace MySpace{ char a; short b; int c; } namespace AlienSpace { char a; short b; int c; } int a; // 0 的初始化 int main() { std::cout << "Hello World" << std::endl; UsingDeclare(); UsingCommand(); return 0; } static void UsingDeclare() { // using 宣告 using std::cout; using std::endl; using MySpace::a; //"2. " //using AlienSpace::a; //"1. " 模糊 using MySpace::b; using MySpace::c; cout << "Hello World, UsingDeclare" << endl; a = 'a'; b = 20; c = 30; cout << "a: " << a << "\nb: " << b << "\nc: " << c << endl; } static void UsingCommand() { //using 指令 using namespace std; using namespace MySpace; cout << "Hello World, UsingCommand" << endl; cout << "External, ::a: " << ::a //"3. " << "\nb: " << b << "\nc: " << c << endl; } ``` 1. 雖然 AlienSpace & MySpace 是不同空間 (相同變數名稱也不會衝突),但是 using 宣告 a 就一樣了,這樣會產生模糊 2. 使用 **using 宣告會遮蔽外部連結**的靜態變數,**不會產生錯誤,外部靜態變數會被遮蔽** 3. **using 指令中的變數與外部會產生衝突**,會導致編譯**錯誤**,要使用**外部變數**要 ==**使用範疇運算子 `::`**== 就可取得檔案變數 > ![](https://i.imgur.com/fGFc7VD.png) * using 可以放置在 function 內 (離開函數就失效),也可放置在 Gobal 區塊,但如果 Function 內有與 Gobal 相同的變數名稱,就需要 使用範疇運算子指定 ```cpp= #include <iostream> namespace A { const char* name; int age; } namespace B { const char* name; int age; } using namespace A; int main() { using namespace B; // 由於名稱重複,所以需要使用範疇運算子指定 B::name = "Alien"; B::age = 10; std::cout << B::name << ", " << B::age << std::endl; return 0; } ``` > ![](https://i.imgur.com/FY11oLN.png) ### 名稱空間特色 1. namespace **巢狀空間** ```cpp= #include <iostream> namespace KyleSpace { const char* name; int age; } namespace MySpace{ const char* name; int age; namespace AlienSpace { //巢狀 const char* name; int age; } using namespace KyleSpace; } int main() { using std::cout; using std::endl; MySpace::AlienSpace::name = "Alien"; MySpace::AlienSpace::age = 23; MySpace::name = "MySpace"; MySpace::age = 26; cout << "Alien name = " << MySpace::AlienSpace::name //"1." << "\n Alien age = " << MySpace::AlienSpace::age <<endl; cout << "MySpace name = " //"2. " << MySpace::name // 與 KyleSpace name 衝突,取用 MySpace name << "\n MySpace age = " << MySpace::age <<endl; // 與 KyleSpace age 衝突,取用 MySpace age return 0; } ``` 1. 可有巢狀的命名空間,要指定也可巢狀指定 2. 當**巢狀空間內產生命名衝突時,以最外部的命名會覆蓋內部命名**,導致 `using namespace Kyle` 沒有被指定到 **--實作--** > ![](https://i.imgur.com/0mWZBPm.png) 2. **無名空間** : **特點無法使用 using 指定**,拿來取代內部連結靜態變數 (也就是全域變數),不同檔案之間可以定義同內容的變數 > 全域變數、無名空間兩者不能同時存在重複的名稱,否則會造成模糊,無法判定 ```cpp= #include <iostream> // static char* name; // 被 namespace 取代 // static int age; extern void namespace5(); namespace { const char* name; int age; } int main() { using std::cout; using std::endl; name = "michelle"; age = 13; cout << "name = " << name << "\n age = " << age <<endl; return 0; } // -------------------------------------------- 另一個檔案 #include <iostream> namespace { const char* name; int age; } void namespace5() { ::name = "namespace5"; ::age = 123; std::cout << ::name << ", " << ::age << std::endl; } ``` **--實作--** > ![](https://i.imgur.com/ymPuD7O.png) 3. 直接從已有 namespace 設定新 namespace (簡化 namespace) ```cpp= #include <iostream> namespace Book { char* name; int page; } namespace redefine = Book; int main() { // namespace redefine = Book; 定義在 Function 內也是可以的 ~ using namespace std; redefine::name = "Cpp"; redefine::page = 2110; cout << "Book name: " << redefine::name << ", page:" << redefine::page << endl; return 0; } ``` > ![](https://i.imgur.com/zfhS00S.png) ### namespace 重複定義 * 名稱空間可以重複定義,但 **重複的名稱空間內不能有相同的變數宣告** ```cpp= #include <iostream> namespace x{ int a; } namespace x { int x; // ok~ // int a; // fail~ 名稱重複 redefinition } int main() { using std::cout; using std::endl; x::a = 10; x::x = 123; cout << "x::a = " << x::a << "\n x::x = " << x::x <<endl; return 0; } ``` > ![](https://i.imgur.com/AieaL0l.png) ### 名稱空間 - 整理 | 命名空間種類 | 功能 | 補充 | | -------- | -------- | -------- | | ==**已命名空間**== | 取代外部連結函數、變數 | | | ==**匿名命名空間**== | 取代內部靜態變數 | static 關鍵字 | * using 宣告比起 using 指令安全,因為單個匯入比較不會有名稱衝突問題 * **名稱空間最大的用意在於,幫助我們==簡化大型程式專案的管理==** ## Appendix & FAQ :::info ::: ###### tags: `C++`