C 語言 - 儲存類、修飾語
儲存類:變量在 RAM 中會開闢一塊空間來做存取,並且 記憶體被分為 Heap 堆、Stack 棧、.data、.bass、.text ... 等等
int v1 = 10; // 以初始化,存在 .data 段
// 以下兩個初始化為 0,或是尚未初始化,放在 .bss 段
int v2 = 0;
int v3;
int main() {
int v4 = 5; // Stack
int* v5 = (int *)malloc(sizeof(int)); // Heap
free(v5);
return 0;
}
● 可以參考 程式 & 記憶體 篇章
Linux 記憶體概念
● 基礎段分類概念這裡不說明 (請參考 程式 & 記憶體),特別說 Linux 中 C 應用程式的記憶體概念,這邊以常見的「外部裝置(也就是文件)」、「內核」映射為例
A. 文件映射區
● 文件是外部硬體提供,而 Linux 會將文件先讀取到核心 (Kernel) 中,在透過記憶體映射的方式,映射到目標應域的 RAM 中 (虛擬記憶體上)
如此設計的原因是因為加快訪問速度,因為外部裝置的速度是最慢的
● 應用程序在操縱文件時,其實就是在操縱 Kernel 映射到當前 Process 的記憶體區塊
可參考 檔案映射 - mmap 動態拓展 文章,該文章有做小實驗
B. 內核映射區:將內核 (Kernel) 映射到應用記憶體中
● 每個應用進程都存活在獨立的進程空間中,每個進程有 0 ~ 3G 的使用者記憶體空間、1G 的內核空間 (這記憶體空間可能並非連續,這使用到了 虛擬記憶體技術)
這裡的虛擬內存量,是以 32 位元系統為例
auto - 修飾局部變數
● auto 關鍵字在 C 語言中,唯一的功能就是 修飾局部變數;表示該變量是自動局部變量 (依樣分配在 Stack 上)
既然分配在 Stack 上,代表部初始化,其 Value 就是隨機的
平時在定義局部變量時只是省略個 auto 關鍵字,並且 auto 不可以使用在全局變量
● auto 也會自動推導,目前使用的區域變數類型 (沒定義類型的話會警告,但仍可編譯成功)
void auto_test() {
auto int apple = 10;
auto book = 200; // 自動推斷
auto car = "car"; // 自動推斷
printf("Apple: %d\n", apple);
printf("Book: %d\n", book);
printf("Car: %s\n", car);
}
static - 靜態變量
● static 關鍵字在 C 語言中有兩種用法,但這兩種用法沒相關,個代表了不同的意思
A. 修飾 局部變數:透過 static 關鍵字修飾,該變量的儲存位置就不再是 Stack,也就是說該變量的數值會一直存在 !
● 已定義初始化:存在 .data
● 未定義初始化:存在 .bss
void static_local_test(int init) {
static int a = 100; // .data
static int b; // .bss
int c; // stack
if(init != 0) {
b = 0;
c = 0;
}
int bookcase[1000] = {0}; // 擾亂 Stack
printf("last time b value: %d\n", b++);
printf("last time c value: %d\n\n", c++);
}
int main(void) {
static_local_test(1);
static_local_test(0);
static_local_test(0);
return 0;
}
B. 修飾 全域變數:這有關於多檔案的鏈結,使用 static 描述的變數、函數,都不可以被其他檔案使用,只能在內部使用
鏈結時再說明
register - 提高變數讀寫
● register 簡單來說就是要求編譯器將變數放置到 CPU 等級的 Register 做存取(當然,這並不一定全部被的變數都會被放置到 Register 中)
一般沒有聲明的變量是規劃在 RAM 記憶體空間
register 使用起來跟一般變量一樣
void register_test() {
register int a = 100; // 規劃到棧存器
int b = 200; // 規劃到 RAM
printf("a: %d\n", a);
printf("b: %d\n\n", b);
}
● 並非一定安排的到暫存器,畢竟 CPU 棧存器數量有限制,所以需慎用
extern - 跨文件訪問
● 我們知道 C 語言是以文件為單位來做編譯,如果要跨文件使用變量、函數,就需要使用 extern 關鍵字來修飾 (只能修飾全局變量)
● 這裡要注意一件事… 定義、宣告 兩者個差異:
● 宣告:僅是告訴編譯器,有一個 符號
● 定義:同時有宣告的意義,並加上為該符號 在記憶體中佔(耗費)一個位置
// storage.c
int var = 10; // 宣告 + 定義
// ------------------------------------------------------
// extern_test.c
extern int var; // 宣告
void test_extern() {
var += 10;
printf("extern val: %d\n", var);
}
● 不需要再使用
#include "storage.c",因為 extern 會去全局 (整個應用) 中尋找相對應的符號
volatile - 易變
● 使用 volatile 關鍵字有以下特點
● 變量由外部修改:最常見的就是在中斷時改變變量數值,其次還有,在 mutli thread 時修改數值,硬體修改數值... 等等
● 寫入相同數值時:不會再對記憶體寫入,使用 volatile 修飾變量後,就算是同樣的數值也會寫入
int a = 10;
a = 10; // 由於是相同數值,所以會「被編譯器優化」
a = 10;
a = 10;
a = 10;
a = 10;
● volatile 就是告訴編譯器不要隨意進行優化,使用方式如下
void test_volatile() {
volatile int a, b, c;
a = 3;
b = a;
c = b; // 如果沒修飾,可能被編譯器寫成 c = b = a = 3;
printf("a: %d\n", a);
printf("b: %d\n", b);
printf("c: %d\n", c);
}
restrict - 限制
在 C99 之後才出現
gcc 編譯可以使用-std=c99來指定編譯版本
● restrict 關鍵字是 用於限制、約束 指標,目的是為了讓編譯器更好的優化
簡單來說,在範圍內,修飾的指標不會被參考
void test_restrict() {
int a = 10;
int* restrict aPtr;
int* restrict bPtr;
aPtr = &a;
bPtr = &a;
printf("aPtr: %d\n", *aPtr);
printf("bPtr: %d\n", *bPtr);
// 再次參考被 `restrict` 修飾的指標就會被警告 (仍編得過
int **aPtr2 = &aPtr;
int **bPtr2 = &bPtr;
printf("aPtr2: %d\n", **aPtr2);
printf("bPtr2: %d\n", **bPtr2);
}
C 語言 - 作用域
作用域的重點在 {} 符號 (if、while、for... 都有),只要變量一進入該符號就開始生命,離開就結束生命週期
作用域 - 區域變量
● 相同符號是可以內蓋外
#include <stdio.h>
int main(void) {
int apple = 10;
{
int apple = 1; // 內部 symbol 會覆蓋外部 symbol
apple += 1000;
printf("Inner Apple price: %d\n", apple);
}
printf("Outer Apple price: %d\n", apple);
return 0;
}
C 語言- 程式生命週期
生命週期
● 開始:運行中分配變量空間,排斥其他變量操控
● 結束:回收變量空間,其他變量可用
Stack、Heap
A. Stack 跟 作用域 相當有關,生在作用域內,一離開作用域就死亡
void my_stack() {
int a = 10; // a 被規劃
for(int b = 0; b < = 20; b++) { // b 被規劃
a += b;
}
// b 結束生命週期
printf("a: %d\n", a);
} // a 結束生命週期
B. Heap 生命週期是使用者手動管理,生命週期從 malloc ~ free 之間,可以跨 Function,因為它的記憶體分配在 Heap 上,而非 Stack 上
int* alloc_int(int init) {
// 生命週期開始
int *ptr = (int*) malloc(sizeof(int));
*ptr = init;
return ptr;
}
int main(void) {
int *a = NULL;
a = alloc_int(9876);
printf("a: %d\n", *a);
free(a); // 生命週期結束
return 0;
}
.data & .bss - 全局變量
操作不慎可能導致 OOM or 記憶體洩漏
● .data & .bss 用來描述全局變量,並且它們的 生命周期是永久 (與應用同進退)
● 至於其他段 .text & .rodata 也是生命周期永久
函數 & Stack
● 函數有幾個特點
A. 入參建議不要超過 4 個,超過建議使用 struct 包裹起來(或是使用指標)
否則可能會造成 stack 的負擔
B. 傳入參數大小也不建議過大,避免超過 Stack Size,較大參數建議使用 Pointer 傳遞
C. 編譯完後函數 會存在 elf 中的 .text 段
● 可以使用
readelf指令查看
readelf -S Hello.out
函數 - 入棧 Stack
● 函數的入棧是按照順序的,如下面程式的入棧順序就是
● Stack 棧是由核心棧存器
SP來管控,相關的核心棧存器還有 鏈結LR、計數器PC
A. main 函數返回地址
B. main 函數
C. 遇到 subtraction 函數,保存 subtraction 函數完成後的返回地址
D. subtraction 函數入棧,這裡又細分為,參數由右到左入棧
int subtraction(int a, int b) {
return a - b;
}
int main(void) {
int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數
}
Stack 概念圖如下
C 語言 - 鏈結檔案
鏈結:鏈結在 C 語言中主要就是在鏈結個個檔案的匯編結果 (.o),也就是將各個獨立的二進為檔鏈結,形成一個可執行檔;
● 編譯以文件為單位,鏈結以工程應用為單位
鏈結屬性
● C 語言中的鏈結有三種屬性
A. 外鏈結:使用 extern 修飾的全局變量、函數 都屬於外鏈結部分
extern int a;
void printA() {
printf("A: %d\n", a);
}
● 這在大型專案中容易會有重名的問題
B. 內鏈結:使用 static 修飾的函數、全局 static 變量 (不包含局部 static)
static int count = 10;
static void _cal_val(int *const val) {
*val = *val + 100;
}
●
static可以解決部分函數、變量重名的問題;static修飾過後即便其他檔案有,也不會相互衝突
C. 無鏈結:局部變量、auto 修飾、局部靜態變量
void my_local() {
int a = 10;
auto b = "Hello";
static c = 200;
}
更多的 C 語言相關文章
關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言
C 語言基礎
● C 語言基礎:有關於到 C 語言的「語言基礎、細節」
編譯器、系統開念
● 編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節
C 語言與系統開發
● C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用












