一、預處理指令
預處理指令在編譯前執行,除了#include
,還有以下常用指令:
1.?#define
?宏定義
無參宏:定義常量或代碼片段,編譯時直接替換(無類型檢查)。
#define PI 3.1415926 // 定義常量
#define MAX(a,b) (a > b ? a : b) // 定義簡單邏輯(注意括號,避免運算優先級問題)
注意:宏替換是 “文本替換”,可能導致副作用,例如?
MAX(a++, b++)
?會導致a
或b
多自增一次。帶參宏與函數的區別:
特性 帶參宏 函數 執行時機 編譯前替換 運行時調用 開銷 無調用開銷(代碼膨脹) 有棧幀開銷 類型檢查 無 有 返回值 無(替換結果直接使用) 有明確返回值類型
2. 條件編譯
用于控制代碼是否參與編譯,常用于跨平臺開發或調試。
#define DEBUG 1 // 定義宏DEBUG#ifdef DEBUG // 如果定義了DEBUG,則編譯以下代碼printf("調試信息:變量x的值為%d\n", x);
#else // 否則編譯以下代碼// 無調試信息
#endif#ifndef _HEADER_H // 如果未定義_HEADER_H(防止頭文件重復包含)
#define _HEADER_H
// 頭文件內容
#endif
二、數據類型進階
1. 類型修飾符
short
/long
:修飾整數長度(short int
?通常 2 字節,long int
?通常 4/8 字節,取決于系統)。signed
/unsigned
:signed int
?可表示正負(默認),unsigned int
?只表示非負(范圍更大)。
unsigned int num = 10; // 范圍:0 ~ 4294967295(32位系統)
signed char c = -12; // 范圍:-128 ~ 127(8位)
2. 自定義類型
結構體(
struct
):組合不同類型數據,用于描述復雜對象(如學生、坐標)。
// 定義結構體類型,此時的student是一種自定義的新數據類型,像int一樣
struct Student {char name[20]; // 姓名int age; // 年齡float score; // 成績
};int main() {// 聲明結構體變量并初始化struct Student stu = {"張三", 18, 90.5f};// 訪問成員(用.運算符)printf("姓名:%s,年齡:%d\n", stu.name, stu.age);// 結構體指針(用->運算符訪問成員)struct Student *p = &stu;p->score = 95.0f; // 等價于 (*p).score = 95.0freturn 0;
}
- 枚舉(
enum
):定義命名的整數常量,提高代碼可讀性。
enum Weekday {MON, // 默認為0TUE, // 1WED=5, // 顯式賦值5THU // 6(自動遞增)
};int main() {enum Weekday day = WED;printf("%d\n", day); // 輸出5return 0;
}
- 共用體(
union
):所有成員共享同一塊內存(大小為最大成員的大小),用于節省空間。
union Data {int i;float f;char c;
}; // 大小為4字節(float和int通常4字節)int main() {union Data d;d.i = 10;printf("d.i = %d, d.f = %f\n", d.i, d.f); // f的值會混亂(內存被i覆蓋)return 0;
}
3. 結構體的高級用法
- 結構體嵌套與自引用(鏈表基礎):結構體可嵌套其他結構體,自引用(包含自身類型的指針)是實現鏈表、樹等數據結構的核心。
示例:單向鏈表節點
#include <stdio.h>
#include <stdlib.h>// 結構體自引用(必須用指針,否則會無限遞歸定義)
struct Node {int data; // 數據域struct Node *next; // 指針域:指向 next 節點
};// 創建新節點
struct Node* createNode(int data) {struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = data;newNode->next = NULL; // 初始指向NULLreturn newNode;
}int main() {// 創建3個節點并鏈接struct Node *n1 = createNode(10);struct Node *n2 = createNode(20);struct Node *n3 = createNode(30);n1->next = n2; // n1 指向 n2n2->next = n3; // n2 指向 n3// 遍歷鏈表struct Node *current = n1;while (current != NULL) {printf("%d ", current->data); // 輸出:10 20 30current = current->next;}// 釋放鏈表內存(從首節點依次釋放)current = n1;while (current != NULL) {struct Node *temp = current;current = current->next;free(temp);}return 0;
}
三、函數與指針深入
1. 函數參數與返回值
傳值調用:函數接收參數的副本,修改副本不影響原變量。
void swap(int a, int b) {int temp = a;a = b;b = temp; // 僅修改副本,原變量不變
}
- 傳址調用:通過指針傳遞變量地址,函數可修改原變量。
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp; // 直接修改原變量
}int main() {int x=3, y=5;swap(&x, &y); // 傳遞地址printf("x=%d, y=%d\n", x, y); // 輸出x=5, y=3return 0;
}
- 指針函數(返回指針的函數):返回值為指針(需注意:不能返回局部變量的地址,因其生命周期隨函數結束而結束)。
int* getStaticPtr() {static int num = 10; // static變量生命周期為整個程序,地址有效return #
}
- 函數指針(指向函數的指針):函數指針存儲函數的地址,可實現 “回調函數”(將函數作為參數傳遞),是 C 語言實現多態的重要方式。
#include <stdio.h>// 加法函數
int add(int a, int b) { return a + b; }// 乘法函數
int multiply(int a, int b) { return a * b; }// 函數指針作為參數(回調函數)
int calculate(int a, int b, int (*func)(int, int)) {return func(a, b); // 調用傳入的函數
}int main() {int x=3, y=4;// 用函數指針調用不同函數,實現不同邏輯printf("加法結果:%d\n", calculate(x, y, add)); // 7printf("乘法結果:%d\n", calculate(x, y, multiply)); // 12return 0;
}
2. 函數遞歸
函數自身調用自身,需滿足終止條件和遞歸關系(如階乘、斐波那契數列)。
// 計算n的階乘:n! = n * (n-1)!,終止條件n=1時返回1
int factorial(int n) {if (n == 1) return 1; // 終止條件return n * factorial(n-1); // 遞歸關系
}
注意:遞歸深度過大會導致棧溢出(棧內存有限),復雜場景建議用循環替代。
3. 指針深入
二級指針(指針的指針):用于處理 “指針的集合”(如動態二維數組、指針數組的修改)。
示例:動態創建二維數組(用二級指針)
#include <stdio.h>
#include <stdlib.h>int main() {int rows = 2, cols = 3;int **arr; // 二級指針:指向int*類型的指針// 第一步:分配存放指針的內存(rows個int*)arr = (int**)malloc(rows * sizeof(int*));// 第二步:為每個指針分配數組內存for (int i=0; i<rows; i++) {arr[i] = (int*)malloc(cols * sizeof(int));}// 賦值arr[0][0] = 1; arr[0][1] = 2; arr[0][2] = 3;arr[1][0] = 4; arr[1][1] = 5; arr[1][2] = 6;// 打印for (int i=0; i<rows; i++) {for (int j=0; j<cols; j++) {printf("%d ", arr[i][j]);}printf("\n");}// 釋放內存(先釋放內層,再釋放外層)for (int i=0; i<rows; i++) {free(arr[i]);}free(arr);return 0;
}
- const 修飾的指針:區分 “指向 const 的指針” 和 “const 指針”,避免意外修改數據。
#include <stdio.h>int main() {int a = 10, b = 20;// 1. 指向const的指針(不能通過指針修改指向的值,但指針可指向其他地址)const int *p1 = &a;// *p1 = 30; // 錯誤:不能修改指向的值p1 = &b; // 正確:可指向其他地址// 2. const指針(指針本身不能修改指向,但可修改指向的值)int *const p2 = &a;*p2 = 30; // 正確:可修改指向的值// p2 = &b; // 錯誤:指針本身是const,不能改指向// 3. 指向const的const指針(既不能改指向,也不能改值)const int *const p3 = &a;return 0;
}
四、數組與指針深入
1. 數組與指針的關系
數組名本質是首元素地址(常量指針,不可修改),可通過指針訪問數組元素。
int arr[5] = {1,2,3,4,5};
int *p = arr; // 等價于 p = &arr[0]// 訪問arr[2]的三種方式
printf("%d\n", arr[2]); // 數組下標
printf("%d\n", *(arr+2)); // 數組名+偏移量
printf("%d\n", *(p+2)); // 指針+偏移量
2. 字符數組與字符串
字符串是以'\0'
(ASCII 值 0)結尾的字符數組,printf
和strlen
等函數通過'\0'
判斷結束。
char str1[] = "hello"; // 自動添加'\0',長度為6(h e l l o \0)
char str2[] = {'h','i','\0'}; // 必須顯式添加'\0',否則不是字符串// 字符串處理函數(需包含<string.h>)
printf("長度:%d\n", strlen(str1)); // 輸出5(不包含'\0')
char dest[20];
strcpy(dest, str1); // 復制字符串(注意dest容量足夠)
strcat(dest, " world"); // 拼接字符串
3. 指針數組與數組指針
指針數組:數組元素是指針(如存儲多個字符串的地址)。
char *strs[] = {"apple", "banana", "cherry"}; // 每個元素是字符串常量的地址
for (int i=0; i<3; i++) {printf("%s\n", strs[i]); // 輸出三個字符串
}
- 數組指針:指向整個數組的指針(需指定數組長度)。
int arr[3] = {1,2,3};
int (*p)[3] = &arr; // p指向包含3個int的數組
printf("%d\n", (*p)[1]); // 輸出2(訪問數組第2個元素)
也就是說,指針可以指向數組的某個元素,也可以指向整個數組,也可以作為元素本身構成數組。當指針直接指向數組名的時候,存儲的實際上是數組的第一個元素的地址。
4. 多維數組的指針訪問
二維數組在內存中是連續存儲的(按行優先排列),可通過指針靈活訪問,而不僅限于arr[i][j]
的形式。
示例:用指針遍歷二維數組
#include <stdio.h>int main() {int arr[2][3] = {{1,2,3}, {4,5,6}};int *p = &arr[0][0]; // 指向首元素的指針(或直接用 int *p = arr[0];)// 遍歷整個二維數組(共2×3=6個元素)for (int i=0; i<2*3; i++) {printf("%d ", *(p+i)); // 輸出:1 2 3 4 5 6}return 0;
}
- 二維數組名
arr
是數組指針(類型為int (*)[3]
),指向第一行的整個數組,arr+1
會跳過一整行(3 個 int)。 - 易錯點:
arr[i][j]
等價于*(*(arr+i)+j)
,需注意指針類型匹配。
五、內存管理
C 語言需手動管理內存,通過stdlib.h
中的函數操作堆內存(堆內存需手動申請和釋放,否則內存泄漏)。
1. 動態內存分配
malloc(size)
:分配size
字節的內存,返回void*
(需強制類型轉換),失敗返回NULL
。calloc(n, size)
:分配n
個size
字節的內存,初始化為 0。realloc(ptr, new_size)
:調整已分配內存的大小,可能移動內存塊。
2. 內存釋放
free(ptr)
:釋放malloc
/calloc
/realloc
分配的內存,釋放后ptr
應置為NULL
(避免野指針)。
示例:
#include <stdio.h>
#include <stdlib.h>int main() {// 分配10個int的內存(40字節)int *arr = (int*)malloc(10 * sizeof(int));if (arr == NULL) { // 檢查分配是否成功printf("內存分配失敗\n");return 1;}// 使用內存for (int i=0; i<10; i++) {arr[i] = i;}// 重新分配為20個int的內存int *new_arr = (int*)realloc(arr, 20 * sizeof(int));if (new_arr != NULL) {arr = new_arr; // 重新分配成功,更新指針}// 釋放內存free(arr);arr = NULL; // 避免野指針return 0;
}
六、文件操作
通過stdio.h
中的函數讀寫文件,核心是文件指針(FILE*
)。
1. 文件打開與關閉
fopen(filename, mode)
:打開文件,mode
為打開模式("r"
讀,"w"
寫,"a"
追加等),失敗返回NULL
。fclose(fp)
:關閉文件,必須調用(否則可能丟失數據)。
2. 文件讀寫
- 字符級:
fgetc(fp)
(讀一個字符)、fputc(c, fp)
(寫一個字符)。 - 字符串級:
fgets(buf, size, fp)
(讀一行)、fputs(str, fp)
(寫字符串)。 - 格式化:
fscanf(fp, format, ...)
、fprintf(fp, format, ...)
。
#include <stdio.h>int main() {// 寫文件FILE *fp = fopen("test.txt", "w"); // 以寫模式打開if (fp == NULL) {printf("文件打開失敗\n");return 1;}fprintf(fp, "Hello, File!\n"); // 寫入內容fclose(fp); // 關閉文件// 讀文件fp = fopen("test.txt", "r");char buf[100];fgets(buf, 100, fp); // 讀取一行printf("讀取內容:%s", buf); // 輸出Hello, File!fclose(fp);return 0;
}
七、其他的一些名詞
- 野指針:訪問已釋放或未初始化的指針(可能崩潰)。
- 數組越界:訪問超出數組長度的元素(可能修改其他內存)。
- 內存泄漏:未用
free
釋放堆內存(長期運行的程序會耗盡內存)。