再看C語言

目錄

與Java的差異化

編程范式

跨平臺

編譯過程

包管理

基本類型

內存結構

重點掌握

進制、字節與計算

指針

結構體

關鍵詞

動態內存

模塊化

高級特性

動態鏈接

虛擬內存

打包編譯

并發編程


現在需要參與到存儲軟件開發工作,存儲層比較接近OS系統和硬件,基本都是用C實現,比如數據庫、分布式存儲ceph,之前參與過PostgreSQL內核源碼的學習和改造,也對大型C項目有一定認識,準備系統性的重拾C語言,以此記錄,其實已經學了1周了,這里按照幾個方面來總結加固;

與Java的差異化

C與Java語言及開發應用方面的差異,就好比城市中市政工程和鴿子屋小區工程的差異,首先兩者的目的不同,市政工程是為打造便捷、高效、質量上乘的公共服務(如高速公路、隧道、大橋、電站、商場),鴿子屋小區主要是提供民用的家庭住房,兩者使用的材料就是開發語言的差異,前者需要用穩固/靠譜/昂貴的一流基礎原材料,后者使用較為普通但能快速使用的民用材料,其次前者多為非統一性需求所以可套用的模版很少,后者可以使用大量現成工具(List,Map)和模版(SpringBoot等框架)批量開發,所以C開發的組件都有偏底層/輕量化/高效/穩定/靠譜的代名詞,如nginx,sqlite,redis,PostgreSQL等數據庫,Java開發的組件較重且偏上層,如netty,tomcat,flink,hadoop,kafka等。

編程范式

Java和C有面向過程和面向對象的基本編程范式差異,面向過程基本上都是函數封裝和函數調用的過程,面向對象是針對對象的封裝和抽象,以及啟動后對象被實例化后的互相調用,面向對象其原生就是多作用域的(每個對象是一個作用域),想調用函數必須是持有實例化后的某對象Bean的調用來實現,所以面向對象才有嚴格的設計模式,設計模式最大程度的規范了不同場景下如何針對調用主體需求來組織代碼,保證代碼有序可讀。

而面向過程的代碼基本上就是縷清函數調用鏈的過程了,c大型工程中的調用鏈是非常長的,其注重的是算法的封裝和整合。

跨平臺

Java源代碼編譯為中間字節碼(.class文件)后再基于JVM虛擬機來解決跨平臺問題,在執行的時候才在OS上的JVM內編譯,這時候已經知道運行的底層OS了,JVM也是C寫的,其也被其他語言如scala、kotlin等直接使用。

編譯過程

C開發者必須要關注編譯過程,因為在這個過程還有很多的優化空間,比如內存對齊配置優化,比如靜態庫和動態庫配置等,但Java開發者無須關注其語言的編譯過程,只知道java -c A.java指令會把A.java編譯成A.class,然后啟動時候交給JVM虛擬機,JVM虛擬機在運行時候的加載->驗證->準備->初始化才是比較關注的重要點,因為這里涉及到某個對象的單例和初始化過程,很多框架要在這里做文章。

C的編譯過程包括:預編譯-->編譯-->匯編-->鏈接四個過程,以test.c為例,預編譯完成crtl +c 和crtl+v的代碼替換把頭文件和注釋處理掉并生成test.i文件,編譯就是把test.i編譯成匯編語言test.s,匯編代碼本質是低級的編程語言,還是人類可讀但機器不可讀的語言,匯編把匯編代碼test.s匯編成機器可讀的test.o文件,但其中的函數和變量的內存地址是0000...沒有替換成實際的地址,且每個.c會生成一個.o,不是單個程序入口,鏈接就是完成最后一步的合并和鏈接,比如PostgreSQL編譯后是50M左右大小的二進制文件,這就是其源碼編譯后的大小了。

C的編譯過程是把test.c直接編譯成0/1機器碼,運行時候直接加載機器碼到內存段,所以啟動表較快,Java啟動時候完成的工作比較多所以啟動很慢。

包管理

Java 確實通過中心化包管理器(如 Maven、Gradle)管理依賴,可直接通過配置文件(如pom.xml)引用遠程倉庫(如 Maven Central)中的工具包(如 Hutool),無需手動拷貝源碼,依賴管理高度標準化,這種模式依賴中央倉庫統一維護包版本,支持自動解析依賴鏈,大幅降低集成成本。

C 語言沒有官方統一的中心化包管理器,傳統依賴管理以源碼集成或靜態 / 動態庫引用為主(如拷貝.c/.h源碼,或編譯為.o、.a、.so文件后通過頭文件引用),在編譯時C 語言需手動處理頭文件路徑、庫文件路徑,比如顯式指定鏈接參數(如-lxxx),依賴管理的自動化程度較低。

聽說C現在也有了一些包管理工具;

基本類型

從基本類型設計上的比較能看出兩者語言的不同和設計之別,拋開最最基本的數據類型,Java提供大量可變(String)、封裝性好的容器(List)來支持開發者隨取隨用,C的設計保守出于一個宗旨就是把內存分配和回收的決定權交給開發者,對開發者要求很高;

且Java因為有JVM兜底來實現跨硬件特性其所有類型定長,但C在不同硬件上定義的類型可能不定長。

內存結構

Java的內存管理全權交給JVM,重點關注的是堆Heap區域,這個區域在啟動時候就已經指定并限制了其最大空間,除非是非堆內存溢出,不然不會超過XMx大小,其內的對象通過一些特定的算法來回收內存(引用計算),所以最大的風險來源于實例化的bean是否有可能持續增大,不釋放后造成堆內存溢出,或者線程池溢出造成的非堆內存溢出;

┌───────────────────────────────────────────────────────────┐
│                        JVM進程內存空間                        │
├───────────────────────┬───────────────────────────────────┤
│       線程私有區域       │             線程共享區域            │
├────────────┬──────────┼──────────┬───────────────────────────┤
│ 程序計數器   │ 本地方法棧 │  虛擬機棧   │         堆          │
│ (PC Register) │ (Native Stack) │ (Java Stack) │ (Heap)       │
│ 記錄當前指令地址 │ 調用本地方法時使用 │ 存儲棧幀(局部變量等) │ 對象實例存儲區  │
└────────────┴──────────┼──────────┴───────────────────────────┤│                 ↑│                 │├─────────────────┼───────────────────────┤│    方法區/元空間    │       直接內存       ││ (Method Area/Metaspace) │ (Direct Memory)    ││ 存儲類信息、常量等    │ NIO等操作使用的堆外內存│└───────────────────────────────────────────┘

C語言的內存模型可以分為四個部分:代碼段、數據段、堆和棧,代碼段是只讀的空間,存儲加載到內存的源代碼,數據段存儲全局可共享的靜態變量,內部又分為bss和數據段,堆就是os內存了,棧是局部方法使用的空間。

┌───────────────────────────────────────────────────────────┐
│                        進程虛擬地址空間                        │
├───────────────────────┬───────────────────────────────────┤
│       高地址區域        │             低地址區域            │
├────────────┬──────────┼──────────┬───────────────────────────┤
│  內核空間   │  命令行參數/環境變量  │                        │
│ (Kernel)   │ (Command Line Args)  │       代碼段(.text)      │
│            │                      │  存儲可執行代碼和只讀數據  │
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │      只讀數據段(.rodata)    │
│            │          │          │  存儲字符串常量等只讀數據  │
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │      數據段(.data)         │
│            │          │          │  存儲已初始化全局/靜態變量  │
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │      BSS段(.bss)           │
│            │          │          │  存儲未初始化全局/靜態變量  │
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │       堆(Heap)            │
│            │          │          │  動態內存分配區域(向上增長)│
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │       棧(Stack)           │
│            │          │          │  存儲局部變量和函數調用    │
│            │          │          │  (向下增長)                │
├────────────┼──────────┼──────────┼───────────────────────────┤
│            │          │          │  棧保護區域(Guard Page)    │
│            │          │          │  防止棧溢出攻擊            │
└────────────┴──────────┴──────────┴───────────────────────────┘

這里要理解為啥Java內存模型中有針對線程內存和共享的規劃而C沒有呢,因為C在語言層面不支持多線程,需要引用外部庫來實現,但Java從設計之初就支持多線程:包括JVM層面的線程資源和區域共享規劃、包括語言層面的線程間通信與主內存同步(如volatile、synchronized)、還有一些線程間通信的高級用法?,Java多線程是在OS基礎上進行了一些高度抽象并提供給語言API使用,隔離了底層如Windows和Linux的差異化細節,所以這里C內存模型只有主進程的模型,而沒有多線程下的規劃,因為每個OS的線程庫本身是不同的。

重點掌握

上面從Java與C比較來理解C的某些特性,單從編程語言的語法分析C是比Java簡單的,因為其基本類型少、高級API少、依賴的成熟框架少,熟悉其基本語法使用后就可以針對某個特定領域開發了,但是特定領域與其他領域之間的差距是非常大的。下面是需要重點掌握的一些其他特性。

進制、字節與計算

計算機CPU只認0/1所以計算機中數據/代碼都是二進制存儲的,進制(Base)是指計數系統中使用的基數,即每個數位的權重由該基數的冪次決定,常見的進制包括:二進制基數為2(0b或0B開頭),使用0和1表示,八進制基數為8(0開頭)使用0-7表示,十進制基數為10使用0-9表示,十六進制基數為16(0x開頭)使用0-9和A-F(或a-f)表示。

字節是最小的尋址單元,每個字節都對應一個存儲器地址,稱為字節地址,CPU 可通過該地址對字節進行讀寫等操作。

基本數據類型的字節大小并非固定不變,而是取決于編譯器和目標平臺(如 32 位或 64 位系統),但有相對大小關系(如short <= int <= long),具體實現由編譯器決定:

char:1 字節(固定為 1,由sizeof(char)定義)
short:至少 2 字節(常見 2 字節)
int:至少 2 字節(32 位系統通常 4 字節,64 位系統可能 4 或 8 字節)
long:至少 4 字節(32 位系統 4 字節,64 位系統 8 字節)

除了最小的字節單位,還有很多其他大小的單位,這里要記住1G是1字節乘以2的30次方,所以要理解為啥32位系統只能使用最大2的2次方也就是4G內存。

1KB(Kilobyte,千字節) = 1024B
1MB(Megabyte,兆字節) = 1024KB
1GB(Gigabyte,吉字節) = 1024MB
1TB(Terabyte,太字節) ?= 1024GB
1PB(Petabyte,拍字節) = 1024TB

?計算機中的計算參考:計算機中的計算

指針

指針提供了直接操作內存地址的可能,指針的本質是變量,它在通過int *p = &a的定義時候就已經說明自己是個指向int類型的指針,無論指向什么類型的指針都是默認也是占4個字節(32系統中)

系統架構	    指針大小	示例平臺
32 位系統	4 字節	Windows 98/XP、嵌入式系統(如 ARM Cortex-M3)
64 位系統	8 字節	主流桌面系統(Windows 10/11、Linux、macOS)
16 位系統	2 字節	歷史系統(如 DOS、早期單片機)

我們做個測試我本機是64位的OS那么:

#include <stdio.h>
int main() {printf("char*: %zu 字節\n", sizeof(char*));printf("int*: %zu 字節\n", sizeof(int*));printf("void*: %zu 字節\n", sizeof(void*));  // 通用指針類型return 0;
}
輸出
char*: 8 字節
int*: 8 字節
void*: 8 字節

?以下為例:

#include <stdio.h>int main() {// 定義一個整型變量int a = 10;// 定義一個指向整型的指針變量,指向 a 的地址int *p = &a;// 打印 a 的地址和值printf("a 的地址是: %p\n", (void*)&a);printf("a 的值是: %d\n", a);// 打印指針 p 的地址、值(即 a 的地址)、以及 *p 的值(即 a 的值)printf("p 的地址是: %p\n", (void*)&p);printf("p 的值(即 a 的地址)是: %p\n", (void*)p);printf("*p 的值(即 a 的值)是: %d\n", *p);// 修改 a 的值a = 20;printf("修改后,*p 的值是: %d\n", *p);// 修改 *p 的值(等價于修改 a 的值)*p = 30;printf("修改后,a 的值是: %d\n", a);// 字符串示例char str[] = "abc";  // 字符數組char *ptr = str;     // 指針指向數組第一個元素printf("\n字符串示例:\n");printf("str 的地址是: %p\n", (void*)str);printf("ptr 的地址是: %p\n", (void*)&ptr);printf("ptr 的值(即 str 的地址)是: %p\n", (void*)ptr);printf("*ptr 的值(即第一個字符 'a')是: %c\n", *ptr);// 遍歷字符串printf("字符串內容: ");for (int i = 0; i < 3; i++) {printf("%c ", *(ptr + i));}printf("\n");// 二級指針int x = 10;int *y = &x;int **zz = &y; // pp是二級指針printf("%d", **zz); // 輸出10return 0;
}

輸出:

a 的地址是: 00000000005FFE8C
a 的值是: 10
p 的地址是: 00000000005FFE80
p 的值(即 a 的地址)是: 00000000005FFE8C
*p 的值(即 a 的值)是: 10
修改后,*p 的值是: 20
修改后,a 的值是: 30字符串示例:
str 的地址是: 00000000005FFE7C
ptr 的地址是: 00000000005FFE70
ptr 的值(即 str 的地址)是: 00000000005FFE7C
*ptr 的值(即第一個字符 'a')是: a
字符串內容: a b c
10

結構體

把結構體理解成Java中只有屬性沒有方法的類定義是合理的,不過在Java中最佳時間是把屬性設置成private,并暴露public的方法,而C中結構體是沒有類似約束的,結構一般是全局定義,每個.c文件都能使用它,一般結構體是結合指針使用的,也就是內存中傳遞的并不是實例化后的結構體,而是實例化后的結構體對應的指針地址,并通過struct->a來訪問a字段,用個簡單的程序測試用法,沒啥說的:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 1. 基礎結構體定義
struct Point {int x;int y;
};// 2. 使用typedef創建結構體別名
typedef struct {char name[20];int age;float score;
} Student;// 3. 嵌套結構體
typedef struct {int year;int month;int day;
} Date;typedef struct {Student student;  // 嵌套Student結構體Date birthday;    // 嵌套Date結構體char *address;    // 指針成員
} Person;// 測試函數:打印Point結構體信息
void printPoint(struct Point p) {printf("Point: (%d, %d)\n", p.x, p.y);
}// 測試函數:通過指針修改Student信息
void updateStudent(Student *s, float newScore) {s->score = newScore;
}int main() {printf("===== C語言結構體特性測試 =====\n\n");// ===== 1. 結構體初始化方式 =====printf("=== 1. 結構體初始化 ===\n");// 1.1 基礎結構體初始化struct Point p1 = {10, 20};           // 順序初始化struct Point p2 = {.y=50, .x=30};     // 指定成員初始化(C99)struct Point p3;p3.x = 100; p3.y = 200;              // 成員賦值初始化printf("p1: (%d, %d)\n", p1.x, p1.y);printf("p2: (%d, %d)\n", p2.x, p2.y);printf("p3: (%d, %d)\n", p3.x, p3.y);// 1.2 嵌套結構體初始化Person person = {.student = {"Alice", 20, 95.5f},.birthday = {2003, 5, 15},.address = "北京市朝陽區"};printf("Person: %s, %d歲, 分數=%.1f\n", person.student.name, person.student.age, person.student.score);printf("生日: %d-%d-%d\n", person.birthday.year, person.birthday.month, person.birthday.day);printf("地址: %s\n", person.address);// ===== 2. 結構體指針與尋址 =====printf("\n=== 2. 結構體指針與尋址 ===\n");// 2.1 結構體變量的地址printf("p1的地址: %p\n", &p1);printf("p1.x的地址: %p\n", &p1.x);printf("p1.y的地址: %p\n", &p1.y);// 2.2 指針訪問結構體成員struct Point *pPtr = &p1;printf("通過指針訪問: (%d, %d)\n", pPtr->x, pPtr->y);// 2.3 動態分配結構體Student *sPtr = (Student*)malloc(sizeof(Student));if (sPtr != NULL) {strcpy(sPtr->name, "Bob");sPtr->age = 18;sPtr->score = 89.0f;printf("動態分配的Student: %s, %.1f分\n", sPtr->name, sPtr->score);free(sPtr);  // 釋放內存}// ===== 3. 結構體作為函數參數 =====printf("\n=== 3. 結構體作為函數參數 ===\n");printPoint(p2);  // 值傳遞(拷貝結構體)Student s = {"Charlie", 22, 92.5f};printf("更新前分數: %.1f\n", s.score);updateStudent(&s, 96.0f);  // 指針傳遞(修改原結構體)printf("更新后分數: %.1f\n", s.score);// ===== 4. 結構體數組 =====printf("\n=== 4. 結構體數組 ===\n");Student class[3] = {{"Alice", 20, 95.5f},{"Bob", 18, 89.0f},{"Charlie", 22, 92.5f}};for (int i = 0; i < 3; i++) {printf("學生%d: %s, %d歲\n", i+1, class[i].name, class[i].age);}// ===== 5. 內存對齊 =====printf("\n=== 5. 內存對齊 ===\n");// 5.2 內存對齊測試struct {char a;     // 1字節int b;      // 4字節(需4字節對齊)short c;    // 2字節} alignTest;struct {int b;      // 4字節(需4字節對齊)char a;     // 1字節short c;    // 2字節} alignTest2;printf("對齊測試結構體大小: %zu字節\n", sizeof(alignTest));  // 通常為12字節printf("對齊測試結構體大小: %zu字節\n", sizeof(alignTest2));  // 通常為12字節return 0;
}

關鍵詞

C語言默認是全局作用域,所以它的關鍵詞并不多,其中static extern 與作用域需要重點掌握,static可以用于修飾局部變量和全局變量,被static修飾的局部變量有以下特性:

存儲在靜態存儲區(而非棧上),程序運行期間僅初始化一次;

函數調用結束后變量值不會丟失,下次調用時保留上次的值;

但其作用域沒變仍限于函數內部,外部不可訪問。

static修復的全局變量僅在定義的文件內可見,無法通過extern在其他文件中引用,避免命名沖突(不同文件可定義同名的static全局變量)通過一個例子掌握:

# include <stdio.h>
static int global_var = 10;  // 僅在當前的.c可見
static void helper() {  // 僅在當前.c內部可見printf("Helper function\n");
}
// 其他的.c文件
//extern int global_var;  // 錯誤!無法引用當前.c的static變量
//extern void helper();  // 錯誤!無法引用當前.c的static函數
void counter() {static int count = 0;  // 僅初始化一次count++;printf("Count: %d\n", count);
}
int main() {counter();  // 輸出1counter();  // 輸出2(保留上次的值)helper();return 0;
}

再說extern關鍵詞,extern用于告訴編譯器變量 / 函數在其他文件中定義,無需分配內存,鏈接時會查找實際定義的位置,被extern修飾的變量不能初始化(extern int x = 10;是錯誤的,這會變成定義),extern允許多個源文件訪問同一全局變量或函數,因為默認情況下,函數聲明隱式包含extern,這從某個層面來說C語言是不是最早的函數第一公民的編程語言呢?

// extern-1.c 中
#include <stdio.h>
int global_x = 10;  // 定義全局變量
int main()
{int local_x = 20;  // 定義局部變量printf("local_x = %d, global_x = %d\n", local_x, global_x);return 0;
}// extern_2.c 中
#include <stdio.h>
extern int global_x;  // 聲明外部變量
void print_global() {printf("global_x: %d\n", global_x);
}
extern int add(int a, int b);  // 顯式聲明外部函數
int add2(int a, int b);        // 默認情況下,函數聲明隱式包含extern
int main() {print_global();return 0;
}

再說static和extern搭配的最佳實踐:

1、static extern 不可以修飾同一變量,因為static 要求變量僅在文件內可見,而 extern 要求變量在外部可見,二者矛盾;

2、文件內部的static 變量被同一文件的 extern 函數訪問(外部可調用該函數間接操作 static 變量),來實現類似于面向對象的private屬性+public方法的效果

// utils.c
static int counter = 0;  // 僅在utils.c可見
static extern int x;  // 錯誤!沖突的修飾符extern void increment() {  // 外部可調用counter++;
}// main.c
extern void increment();  // 聲明外部函數
int main() {increment();  // 間接操作utils.c的static變量return 0;
}

動態內存

動態內存也就是把內存的動態開辟和管理交給開發者,主要是幾個函數:

  • malloc函數分配未初始化的內存塊,比如malloc(5 * sizeof(int)) 申請 5 個 int 的空間。
  • calloc函數分配并自動初始化為 0 的內存塊,適合需要初始化的場景(如數組清零),比如calloc(5, sizeof(int)) 申請并初始化 5 個 int 的空間。
  • realloc函數調整已分配內存的大小,如果原內存后有足夠的空間,直接擴展;否則會分配新內存并復制數據,必須先檢查返回值(可能失敗),并將新指針賦給原指針前先保存到臨時變量。
  • free函數釋放動態分配的內存,釋放后必須將指針置為 NULL,避免懸空指針(dangling pointer)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {// 1. 動態開辟初始內存(使用 malloc)int *arr = (int *)malloc(5 * sizeof(int)); // 申請5個int的空間if (arr == NULL) {printf("Memory allocation failed!\n");return 1; // 處理內存分配失敗}// 2. 初始化內存(使用 calloc 或手動初始化)// 選項1: 使用 calloc 自動初始化為0// int *arr = (int *)calloc(5, sizeof(int));// 選項2: 手動初始化(使用 malloc 后)for (int i = 0; i < 5; i++) {arr[i] = i + 1; // 填充數據}// 3. 打印初始數組printf("Initial array:\n");for (int i = 0; i < 5; i++) {printf("%d ", arr[i]);}printf("\n");// 4. 調整內存大小(使用 realloc)int newSize = 10;int *newArr = (int *)realloc(arr, newSize * sizeof(int)); // 擴展到10個intif (newArr == NULL) {printf("Memory reallocation failed!\n");free(arr); // 保留原內存以防萬一return 1;}arr = newArr; // 更新指針// 5. 填充新擴展的內存for (int i = 5; i < newSize; i++) {arr[i] = i + 1; // 繼續填充新元素}// 6. 打印調整后的數組printf("Resized array:\n");for (int i = 0; i < newSize; i++) {printf("%d ", arr[i]);}printf("\n");// 7. 釋放內存并防止野指針free(arr);arr = NULL; // 置空指針,避免懸空指針return 0;
}

模塊化

C 語言模塊化的核心思想是分離接口與實現,通過頭文件暴露公共接口,源文件封裝內部細節,這就比較抽象了,類似于Java的接口和實現類的編程思想,同名的.c文件最好有個.h文件,兩者是聲明和實現的關系,同時入口main.c依賴于其他的.c源文件。

頭文件(.h)和源文件(.c)是實現模塊化編程的核心,合理劃分和組織它們能顯著提高代碼的可維護性、可復用性和可擴展性。

.h頭文件用于暴露模塊的公共接口,包括:函數原型、全局變量聲明(使用extern)、結構體 / 枚舉 / 聯合體定義、宏定義和類型別名(typedef),相對應的.c源文件包含模塊的具體實現,包括函數體、全局變量定義、靜態變量 / 函數(模塊內部使用)

 math_utils.h 內容
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// 函數原型
int add(int a, int b);
int subtract(int a, int b);
// 結構體定義
typedef struct {int x;int y;
} Point;
// 宏定義
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#endif // MATH_UTILS_Hmath_utils.c 內容
#include "math_utils.h"
// 函數實現
int add(int a, int b) {return a + b;
}
int subtract(int a, int b) {return a - b;
}
// 靜態函數(僅在本文件可見)
static int multiply(int a, int b) {return a * b;
}

多個源文件的設計標準是遵從單一職責原則,避免出現Java那種循環依賴問題,不是太大的C工程一般是多個源文件放在一起,很大的項目才會在src中分目錄,如果分目錄可能涉及到Makefile文件的配置了,后續可以注意下:

project/
├── include/          # 存放所有頭文件
│   └── math_utils.h
├── src/              # 存放所有源文件
│   └── math_utils.c
│   └── main.c
└── Makefile          # 編譯腳本

高級特性

高級特性涉及到一些優化和運行態的更底層原理,以及一些高級API調用,其中有些概念是比較抽象但是腦海中一定要有的概念,比如虛擬內存,

動態鏈接

動態鏈接相對于靜態鏈接說的,Windows下的dll和Linux下的.so文件都是動態鏈接庫,可以熱更新而不影響主程序,比如升級linux下的libgp.so肯定不也不會影響PostgreSQL數據庫進程。

動態鏈接是在運行時才將程序與共享庫(.so/.dll)鏈接,可執行文件main僅包含對共享庫的引用,不包含庫的實際代碼。

# 編譯源文件
gcc -c main.c -o main.o
# 動態鏈接(假設libmylib.so是動態庫)
gcc main.o -L. -lmylib -o myapp  # 與靜態鏈接命令相同
# 運行時需確保系統能找到libmylib.so
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH  # Linux臨時設置庫路徑
./myapp

靜態鏈接是指在編譯時就將所有依賴的庫代碼直接復制到最終的可執行文件中,鏈接完成后不依賴外部庫運行;

# 編譯源文件
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
# 靜態鏈接(假設libmylib.a是靜態庫)
gcc main.o utils.o -L. -lmylib -o myapp  # -L指定庫路徑,-l指定庫名

如何創建靜態和動態庫:

# 編譯源文件為目標文件
gcc -c utils.c -o utils.o
# 創建靜態庫
ar rcs libutils.a utils.o  # rcs參數:替換、創建、寫入索引# 編譯時添加-fPIC(生成位置無關代碼)
gcc -fPIC -c utils.c -o utils.o
# 創建動態庫
gcc -shared -o libutils.so utils.o  # -shared指定生成共享庫

PostgreSQL 的主程序 postgres 是一個靜態鏈接的可執行文件,通過靜態鏈接包含數據庫引擎的核心邏輯(如查詢解析、事務管理、存儲引擎),但PostgreSQL 的擴展功能(如全文搜索、JSON 函數)通過 .so 文件實現;

虛擬內存

當C程序被編譯時,編譯器會為每個函數和全局變量生成一個邏輯地址(虛擬地址)。這些地址基于一個假設的“起始地址0”的連續內存空間,但程序實際加載到物理內存時,操作系統會將其放置到任意的物理地址(取決于當時內存的空閑情況)。

虛擬內存是操作系統提供的一種內存管理技術,它為每個進程都提供一個獨立的、連續的虛擬地址空間,使程序可以使用比實際物理內存更大的地址范圍,虛擬內存的必要性:

   問題	               虛擬內存的解決方案
物理內存碎片化	       虛擬地址連續映射到任意物理頁,避免外部碎片
多進程內存沖突	       進程間地址空間隔離,互不可見
程序大于物理內存	       通過分頁交換(Paging)擴展可用內存
代碼/數據位置不確定	   虛擬地址固定,運行時動態重定位
內存安全漏洞	           權限控制+隔離,防止非法訪問內核或其他進程

    操作系統上的程序(無論使用何種編程語言)都間接依賴虛擬內存,但由于我們可以直接操作虛擬內存指針所以需要了解其基本原理,C 指針存儲的是虛擬地址,需通過操作系統和 MMU 轉換為物理地址。若映射失效(如內存釋放后),訪問該地址會觸發段錯誤。

    虛擬地址 → MMU(內存管理單元) → 頁表(Page Table) → 物理地址

    待深入挖掘;?

    打包編譯

    打包編譯需要通過configure和Makefile來完成,都是autotools工具鏈,configure是configure.ac生成,而configure.ac(或configure.in)使用的是M4 宏語言(GNU M4)來編寫的,configure完成環境監測和配置,并根據Makefile.in或者Makefile.am模版來生成Makefile文件,Makefile直接進行編譯安裝:

    用戶命令                      系統狀態
    ┌───────────────────┐        ┌─────────────────────┐
    │ ./configure       │        │ 1. 檢測編譯器       │
    │ --prefix=/usr     │ ─────? │ 2. 檢查依賴庫       │
    │ --enable-feature  │        │ 3. 生成config.h     │
    └───────────────────┘        │ 4. 生成Makefile     │└─────────────────────┘│▼
    ┌───────────────────┐        ┌─────────────────────┐
    │ make              │        │ 1. 編譯源文件       │
    │                   │ ─────? │ 2. 鏈接目標文件     │
    └───────────────────┘        │ 3. 生成可執行文件   │└─────────────────────┘│▼
    ┌───────────────────┐        ┌─────────────────────┐
    │ make install      │        │ 1. 復制文件到指定路徑│
    │                   │ ─────? │ 2. 創建目錄結構      │
    └───────────────────┘        │ 3. 設置文件權限      │└─────────────────────┘

    C 語言大型項目中Makefile 是自動化構建的核心工具,其設計質量直接影響開發效率和項目可維護性,那么其最佳實踐是啥呢,首先項目必然是按照功能的多模塊拆分,使用清晰的目錄結構:

    project/
    ├── src/              # 源代碼(按模塊拆分:core/, network/, utils/)
    ├── include/          # 公共頭文件(模塊私有頭文件可放在子目錄)
    ├── build/            # 編譯輸出(.o、.a、.so)
    ├── third_party/      # 第三方庫(如 Boost、OpenSSL)
    ├── tests/            # 單元測試代碼
    ├── Makefile          # 根 Makefile
    └── modules/          # 模塊級 Makefile(可選)

    一般大型項目又包括根Makefile和子模塊的Makefile,根 Makefile統籌全局并調用子模塊 Makefile,子模塊 Makefile完成獨立模塊編譯并生成靜態庫(.a)或動態庫(.so),比如根模塊一般有以下用法:

    # 根 Makefile 示例
    SUBDIRS = src/network src/utils tests
    .PHONY: all clean
    all: $(SUBDIRS)$(SUBDIRS):$(MAKE) -C $@clean:for dir in $(SUBDIRS); do $(MAKE) -C $$dir clean; done

    大型C語言會有以.in 結尾的 Makefile 模版文件(如 Makefile.in),它是 GNU Autotools 工具鏈中的核心組成部分,主要用于自動化生成跨平臺兼容的 Makefile,configure 腳本會替換 Makefile.in 中的占位符(如 @CC@ 替換為 gcc),生成適配當前系統的 Makefile;

    Makefile.in 中的變量(如 @CC@、@CFLAGS@)由 configure 根據系統環境動態填充,比如CC = @CC@ ?# 在 Linux 上可能是 "gcc",在 macOS 上可能是 "clang",

    通過變量替換支持不同系統的路徑差異,比如prefix = @prefix@ ?# 默認值為 "/usr/local"

    # Makefile.in 的簡化示例
    # 自動生成的注釋
    # @configure_input@# 變量替換(由 configure 填充)
    CC = @CC@
    CFLAGS = @CFLAGS@
    LDFLAGS = @LDFLAGS@# 項目規則
    bin_PROGRAMS = myprogram
    myprogram_SOURCES = main.c utils.c
    myprogram_LDADD = libmylib.a# 安裝路徑
    prefix = @prefix@
    exec_prefix = @exec_prefix@
    bindir = $(exec_prefix)/bin

    一般大型項目都采用cmake了,CMake 比 Autotools(configure/Makefile)簡化了很多?,但需要編寫?CMakeLists.txt,CMake 通過抽象層直接支持多平臺,同一套 CMakeLists.txt 可生成 Windows(Visual Studio)、Linux(Makefile)、macOS(Xcode)的構建文件,是更加現代化的構建工具。

    并發編程

    在 Linux 下使用 C 語言進行多線程編程,通常依賴于 POSIX 線程庫(pthread),以下示例演示兩個線程對一個全局變量進行遞增操作,并通過互斥鎖確保數據一致性。

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>// 共享變量
    int number = 0;
    // 互斥鎖
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 線程函數
    void* increment_thread(void* arg) {int thread_id = *(int*)arg;for (int i = 0; i < 5; i++) {// 加鎖pthread_mutex_lock(&mutex);number++;printf("Thread %d: number = %d\n", thread_id, number);// 解鎖pthread_mutex_unlock(&mutex);sleep(1);  // 模擬工作時間}return NULL;
    }int main() {pthread_t thread1, thread2;int tid1 = 1, tid2 = 2;// 創建線程if (pthread_create(&thread1, NULL, increment_thread, &tid1) != 0) {perror("Failed to create thread 1");return 1;}if (pthread_create(&thread2, NULL, increment_thread, &tid2) != 0) {perror("Failed to create thread 2");return 1;}// 等待線程結束pthread_join(thread1, NULL);pthread_join(thread2, NULL);printf("Final number: %d\n", number);return 0;
    }

    本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
    如若轉載,請注明出處:http://www.pswp.cn/bicheng/86941.shtml
    繁體地址,請注明出處:http://hk.pswp.cn/bicheng/86941.shtml
    英文地址,請注明出處:http://en.pswp.cn/bicheng/86941.shtml

    如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

    相關文章

    機器學習入門 | 訓練、推理與其他機器學習活動(預處理、測試與評估)

    在訓練階段&#xff0c;訓練算法通過優化目標/損失函數在訓練數據集上的表現&#xff0c;不斷更新模型參數θ。在監督學習場景中&#xff0c;訓練數據集由輸入-標簽對&#xff08;真實輸出值&#xff09;組成。目標函數應當獎勵模型根據訓練輸入成功預測真實輸出的行為&#xf…

    Node.js特訓專欄-實戰進階:11. Redis緩存策略與應用場景

    &#x1f525; 歡迎來到 Node.js 實戰專欄&#xff01;在這里&#xff0c;每一行代碼都是解鎖高性能應用的鑰匙&#xff0c;讓我們一起開啟 Node.js 的奇妙開發之旅&#xff01; Node.js 特訓專欄主頁 專欄內容規劃詳情 Redis 緩存策略與應用場景&#xff1a;從理論到實戰的高…

    【stm32】HAL庫開發——Cube配置基本定時器

    目錄 一、Cube配置基本定時器 1.定時器CubeMX配置介紹 2.定時器中斷控制LED 3.定時器常用函數 4.定時器從模式&#xff08;Reset Mode&#xff09; 5.定時器的從模式&#xff08;Gated Mode&#xff09; 6.定時器的編碼器接口 一、Cube配置基本定時器 1.定時器CubeMX配置…

    nginx反向代理后端服務restful及token處理

    #user nobody; worker_processes 1;#error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info;#pid logs/nginx.pid;events {worker_connections 1024; } #代理mysql服務 stream {upstream mysql_backend {server 192.168…

    正確理解Cola StateMachine不內置事務管理機制

    ? 正確理解&#xff1a;Cola StateMachine 并非“不支持”事務一致性&#xff0c;而是“不內置”事務管理機制 因為&#xff1a; Cola StateMachine 是輕量級、無狀態、不依賴 Spring 的框架&#xff0c;它本身 不綁定任何事務上下文。它不像 Spring StateMachine 那樣自動與…

    AudioTrack使用

    ** AudioTrack ** AudioTrack 是 Android 音頻系統中的核心類&#xff0c;用于播放原始音頻數據&#xff08;PCM&#xff09;或壓縮音頻&#xff08;如 MP3、AAC&#xff09;。它提供了低級別的音頻播放控制&#xff0c;適合需要精細管理的場景&#xff08;如游戲音效、實時音…

    解密:MySQL 的常見存儲引擎

    在數據庫領域&#xff0c;MySQL 作為一款廣受歡迎的關系型數據庫管理系統&#xff0c;提供了多種存儲引擎以滿足不同應用場景的需求。每種存儲引擎都有其獨特的特性、優勢和適用場景。本文將深入探討 MySQL 中幾種常見的存儲引擎&#xff0c;包括 InnoDB、MyISAM、MEMORY 和 AR…

    qt和qtcreator版本關系

    實例展示&#xff1a; 如圖所示的qtcreator是使用qt5.15安裝過程選擇勾選了qtcreator 14.0.2&#xff0c;安裝完成qtcreator版本信息&#xff1a; 安裝過程中選擇了這些構件kits&#xff0c;會自動識別到&#xff1a; 使用qt5.9.9另外安裝的kits&#xff0c;需要手動設置才能識…

    2個任務同時提交到YARN后2個都卡住(CDH)

    文章目錄 問題描述解決方案1、增加資源2、調整ApplicationMaster資源3、關閉YARN調度器的資源搶占4、不使用公平調度器 問題描述 在CDH集群上&#xff0c;同時提交2個任務到YARN后&#xff0c;2個任務都卡住 解決方案 1、增加資源 增加服務器的內存和CPU 2、調整Applicatio…

    web3區塊鏈-ETH以太坊

    一. 以太坊概述 以太坊&#xff08;Ethereum&#xff09;作為區塊鏈技術的代表性項目之一&#xff0c;自2015年發布以來&#xff0c;迅速成為全球區塊鏈行業的核心基礎設施。相比比特幣&#xff0c;以太坊不僅支持點對點的價值轉移&#xff0c;還引入了智能合約&#xff0c;使…

    【智能協同云圖庫】智能協同云圖庫第二彈:用戶管理系統后端設計與接口開發

    用戶管理系統 一、需求分析 對于用戶模塊&#xff0c;通常要具有下列功能&#xff1a; 二、方案設計 &#xff08;一&#xff09;庫表設計 實現用戶模塊的難度不大&#xff0c;在方案設計階段&#xff0c;我們需要確認以下內容&#xff1a; 庫表設計用戶登錄流程如何對用戶權限…

    閑庭信步使用SV搭建圖像測試平臺:第十三課——談談SV的數據類型

    &#xff08;本系列只需要modelsim即可完成數字圖像的處理&#xff0c;每個工程都搭建了全自動化的仿真環境&#xff0c;只需要雙擊top_tb.bat文件就可以完成整個的仿真&#xff0c;大大降低了初學者的門檻&#xff01;&#xff01;&#xff01;&#xff01;如需要該系列的工程…

    前端進階之路-從傳統前端到VUE-JS(第一期-VUE-JS環境配置)(Node-JS環境配置)(Node-JS/npm換源)

    經過前面的傳統前端開發學習后&#xff0c;我們接下來進行前端的VUE-JS框架學習&#xff08;寫這篇文章的時候VUE-JS最新版是VUE3&#xff0c;所以默認為VUE3即可&#xff09; 首先&#xff0c;我們要配置Node-JS環境&#xff0c;雖然我們還不學習Node-JS但是Node-JS可以快速配…

    Requests源碼分析:面試考察角度梳理

    簡單描述執行流程 ?? Q:能簡單描述一下發送一個requests.get(url)請求時,在requests庫內部的主要執行流程嗎?(從調用get方法到收到響應) 入口委托: get() 方法內部調用 requests.request(GET, url)。Session 接管: request() 方法會獲取或隱式創建一個 Session 對象,并…

    航天VR賦能,無人機總測實驗艙開啟高效新篇?

    (一)沉浸式培訓體驗? 在傳統的無人機培訓中&#xff0c;操作人員主要通過理論學習和簡單的模擬操作來掌握技能。但這種方式存在很大局限性&#xff0c;難以讓操作人員真正感受無人機在復雜環境下的運行狀態。而航天 VR 技術引入到 VR 無人機總測實驗艙后&#xff0c;徹底改變了…

    Kotlin 函數與 Lambda 表達式

    今天繼續分享Kotlin學習內容。 目標&#xff1a;掌握函數定義、調用、參數傳遞&#xff0c;以及 Lambda 表達式的基礎用法 1. 函數&#xff1a;Kotlin 的代碼模塊化工具 定義&#xff1a;函數是可重復調用的代碼塊&#xff0c;用于封裝邏輯。 語法&#xff1a; fun 函數名(參…

    [mcp-servers] docs | AI客戶端-MCP服務器-AI 架構

    鏈接&#xff1a;https://github.com/punkpeye/awesome-mcp-servers 服務器調用 相關專欄&#xff1a;實現Json-Rpc docs&#xff1a;精選MCP服務器資源列表 本專欄為精選 模型上下文協議&#xff08;MCP&#xff09;服務器的列表。 MCP 是一種標準協議語言&#xff0c;允許*…

    1688商品發布API:自動化上架與信息同步

    一、1688商品發布API的核心功能與技術架構 1.1 API功能全景 1688商品發布API是1688開放平臺的核心組件之一&#xff0c;支持商品信息的自動化發布、編輯、上下架及庫存同步。其核心功能包括&#xff1a; 商品信息管理&#xff1a;支持商品標題、描述、價格、庫存、SKU&#…

    如何在x86_64 Linux上部署Android Cuttlefish模擬器運行環境

    0 軟硬件環境 x86_64服務器Ubuntu20.04 LTS參考&#xff1a;Cuttlefish 虛擬 Android 設備參考&#xff1a; 筆記&#xff1a;搭建 Cuttlefish 運行環境可以下載編好的android-cuttlefish&#xff1a;android-cuttlefish.tar.gz 1 系統采用Ubuntu20.04 LTS 2 搭建cuttlefish…

    機器學習9——決策樹

    決策樹 Intro 歸納學習&#xff08;Inductive Learning&#xff09;的目標&#xff1a;從訓練數據中學習一般規則&#xff0c;應用于未見過的數據。 決策樹是一個樹形結構&#xff0c;其中&#xff1a; 每個分支節點表示一個屬性上的選擇&#xff08;即決策條件&#xff09;。…