指針是 C 語言的靈魂,也是區分初學者與資深工程師的分水嶺。本文從內存模型出發,系統講解指針算術、自定義內存分配器、字符串操作、指針數組排序、多維數組參數傳遞、函數指針等核心主題,並附帶可編譯運行的完整代碼示例。
p + i 的地址增量 = i * sizeof(*p),自動按元素類型縮放ptrdiff_t),跨數組無意義NULL)alloc/afree 內存池原理:預分配大型靜態字符數組 alloc_buf,維護指針 allocp 指向下一個空閒位置,實現棧式(LIFO)分配釋放。
#define ALLOCSIZE 1000
static char alloc_buf[ALLOCSIZE];
static char *allocp = alloc_buf;
char *alloc(int n) { // 申請 n 字節
if (alloc_buf + ALLOCSIZE - allocp >= n) {
allocp += n;
return allocp - n; // 返回本次分配起始地址
}
return NULL; // 空間不足
}
void afree(char *p) { // 釋放,必須按 LIFO 順序
if (p >= alloc_buf && p < alloc_buf + ALLOCSIZE)
allocp = p;
}
關鍵點:static 限制文件內可見,隱藏實現細節;釋放順序必須與分配順序相反,否則 allocp 回退會導致內存洩漏或覆寫。
| 定義方式 | 內存佈局 | 可修改性 |
|---|---|---|
char *p = "hello"; | 指向只讀區常量區 | 指針可改向,內容不可改 |
char arr[] = "hello"; | 棧上分配,含結束符 \0 | 內容可改,地址固定 |
strcpy/strcmp/strlen// 指針版 strcpy:經典單行寫法
void strcpy_ptr(char *dst, const char *src) {
while (*dst++ = *src++); // 賦值後雙雙後移,遇 \0 停止
}
// 指針版 strcmp
int strcmp_ptr(const char *s, const char *t) {
for (; *s == *t; s++, t++)
if (*s == '\0') return 0;
return *s - *t;
}
// 指針版 strlen
size_t strlen_ptr(const char *s) {
const char *p = s;
while (*p++) ; // 移動到 \0 後一位
return p - s - 1;
}
*p++ = val;(先存後增)val = *--p;(先減後取)不移動文本內容,僅操作指向行首的指針。排序時交換指針而非數據,消除大塊內存拷貝開銷。
#define MAXLINES 100
#define MAXLEN 100
#define ALLOCSIZE 11000
char *lineptr[MAXLINES]; // 指針數組
static char allocbuf[ALLOCSIZE];
static char *allocp = allocbuf;
char *alloc(int n) { /* 同前 */ }
void afree(char *p) { /* 同前 */ }
int get_line(char *s, int lim) { /* 讀一行,返回長度 */ }
int readlines(char *lineptr[], int maxlines) {
int len, nlines = 0;
char line[MAXLEN], *p;
while ((len = get_line(line, MAXLEN)) > 0) {
if (nlines >= maxlines || (p = alloc(len + 1)) == NULL)
return -1;
line[len - 1] = '\0'; // 去換行符
strcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
void writelines(char *lineptr[], int nlines) {
for (int i = 0; i < nlines; i++)
printf("%s\n", lineptr[i]);
}
// 快速排序(指針數組版)
void qsort_ptr(char *v[], int left, int right) {
if (left >= right) return;
swap(v, left, (left + right) / 2);
int last = left;
for (int i = left + 1; i <= right; i++)
if (strcmp(v[i], v[left]) < 0)
swap(v, ++last, i);
swap(v, left, last);
qsort_ptr(v, left, last - 1);
qsort_ptr(v, last + 1, right);
}
static char day_table[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // 平年
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} // 閏年
};
int leap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 月日 -> 年內天數
int day_of_year(int year, int month, int day) {
for (int i = 1; i < month; i++)
day += day_table[leap(year)][i];
return day;
}
// 年內天數 -> 月日(輸出參數用指針)
void date_of_year(int year, int yearday, int *pmonth, int *pday) {
int lp = leap(year);
int i = 1;
while (yearday > day_table[lp][i])
yearday -= day_table[lp][i++];
*pmonth = i;
*pday = yearday;
}
| 聲明形式 | 含義 | 正確性 |
|---|---|---|
void f(int a[2][13]) | 帶行列數 | ✅ |
void f(int a[][13]) | 僅列數,行數退化為指針 | ✅ 推薦 |
void f(int (*a)[13]) | 指向「含 13 個 int 的數組」的指針 | ✅ 本質等價 |
void f(int *a[13]) | 13 個 int* 的數組 | ❌ 錯誤 |
void f(int a[2][]) | 缺列數,無法計算行偏移 | ❌ 錯誤 |
本質:函數接收的是指向首行的指針,編譯器需要知道「一行有多少列」才能正確計算 a[i][j] 的地址偏移。
// 指針數組:存 13 個指針,各指向字符串常量(長度不一)
static char *name_ptr[] = {"Illegal", "January", "February", ...};
// sizeof(name_ptr) = 13 * sizeof(char*) = 104 bytes (64位)
// 二維數組:連續存 13 行 × 20 列 字符
static char name_2d[][20] = {"Illegal", "January", "February", ...};
// sizeof(name_2d) = 13 * 20 = 260 bytes
結論:指針數組適合存儲長度不一的字符串,節省空間;二維數組適合固定寬度數據,訪問速度略快(連續內存、無間接引用)。
// 聲明:指向「接受 int, 返回 int」函數的指針
int (*func_ptr)(int);
// 賦值
int square(int x) { return x * x; }
func_ptr = square;
// 調用
int result = func_ptr(5); // 25
// 回調示例
void apply(int *arr, int n, int (*op)(int)) {
for (int i = 0; i < n; i++) arr[i] = op(arr[i]);
}
*p++ 棧操作慣用語深度理解指針,即掌握了 C 語言操控內存的核心能力。