指針是C語言的靈魂,也是許多初學者的「拦路虎」。本文將系統梳理指針的核心概念,從變量的生命週期與作用域切入,層層遞進至指針算術運算與const修飾符的進階用法,並附帶完整可運行的代碼示例,助你建立扎實的指針思維。
理解指針的前提是搞清變量的「生存空間」:
static):存儲在全局數據區,生命週期贯穿整個程序,但作用域仍局限於函數內部。觀察下面的代碼,體會局部變量如何「遮蔽」全局變量:
#include <stdio.h>
/* 全局變量 x */
int x = 1000;
void f1(int a) {
a = 10; // 參數 a 是局部變量
auto int x = 20; // 局部變量 x(auto 可省略)
static int y = 30; // 靜態局部變量 y
y++; x++;
printf("in f1(), a=%d, x=%d, y=%d\n", a, x, y);
}
void f2() {
printf("in f2(), x=%d\n", x); // 訪問全局 x
x = 2000; // 修改全局 x
}
void f3() {
printf("in f3(), x=%d\n", x); // 訪問全局 x
}
int main() {
int x = 30, b = 100; // main 的局部 x 隱藏全局 x
f1(b); f1(b); // 連續調用兩次 f1
printf("in main(), b=%d, x=%d\n", b, x);
f2(); f3(); // 觀察全局 x 的變化
return 0;
}
in f1(), a=10, x=21, y=31 // 首次調用:x=20→21, y=30→31
in f1(), a=10, x=21, y=32 // 二次調用:x 重新初始化為 20→21,y 保持 31→32
in main(), b=100, x=30 // main 的局部 x 未被 f1 影響
in f2(), x=1000 // 全局 x 初始值
in f3(), x=2000 // 被 f2 修改後的全局 x
核心結論:
static 讓局部變量「不死」,跨函數調用保持值。&(取地址):獲取變量在內存中的編號。*(解引用/間接訪問):根據地址找到內存單元並讀寫數據。#include <stdio.h>
int main() {
int a = 10;
int *p = &a; // p 存儲 a 的地址
printf("p = %p\n", p); // 輸出地址
printf("&a = %p\n", &a); // 驗證地址一致
printf("a = %d\n", a); // 直接訪問值
printf("*p = %d\n", *p); // 通過指針間接訪問值
return 0;
}
記憶口訣:定義時 int *p 表示「*p 是 int」,使用時 *p 表示「取 p 指向的內容」。
#include <stdio.h>
/* 失敗版:值傳遞 */
void swap(int a, int b) {
int tmp = a; a = b; b = tmp; // 僅交換副本
}
/* 成功版:地址傳遞 */
void swap1(int *a, int *b) {
int tmp = *a; *a = *b; *b = tmp; // 交換實參內存中的值
}
int main() {
int x = 10, y = 20;
printf("初始: x=%d, y=%d\n", x, y);
swap(x, y);
printf("swap 後: x=%d, y=%d\n", x, y); // 無變化
swap1(&x, &y);
printf("swap1 後: x=%d, y=%d\n", x, y); // 成功交換
return 0;
}
C 函數只能返回一個值,但通過指針參數可實現多輸出:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void maxAndMin(int data[], int size, int *max, int *min) {
*max = *min = data[0];
for (int i = 1; i < size; i++) {
if (data[i] > *max) *max = data[i];
if (data[i] < *min) *min = data[i];
}
}
int main() {
int data[10];
srand((unsigned)time(NULL));
for (int i = 0; i < 10; i++) data[i] = rand() % 100;
int max, min;
maxAndMin(data, 10, &max, &min); // 傳遞地址接收結果
printf("最大值: %d, 最小值: %d\n", max, min);
return 0;
}
指針加減運算不是簡單的字節增減,而是按所指類型大小跳躍:
#include <stdio.h>
int main() {
int a; int *pa = &a; printf("int: pa=%p, pa+1=%p (差 %ld 字節)\n", pa, pa+1, (char*)(pa+1)-(char*)pa);
short s; short *ps = &s; printf("short: ps=%p, ps+1=%p (差 %ld 字節)\n", ps, ps+1, (char*)(ps+1)-(char*)ps);
long l; long *pl = &l; printf("long: pl=%p, pl+1=%p (差 %ld 字節)\n", pl, pl+1, (char*)(pl+1)-(char*)pl);
char c; char *pc = &c; printf("char: pc=%p, pc+1=%p (差 %ld 字節)\n", pc, pc+1, (char*)(pc+1)-(char*)pc);
float f; float *pf = &f; printf("float: pf=%p, pf+1=%p (差 %ld 字節)\n", pf, pf+1, (char*)(pf+1)-(char*)pf);
double d;double *pd = &d; printf("double:pd=%p, pd+1=%p (差 %ld 字節)\n", pd, pd+1, (char*)(pd+1)-(char*)pd);
return 0;
}
規律總結:p + 1 等價於 (char*)p + sizeof(*p)。這是數組遍歷高效的根本原因——指針自動按元素大小步進。
| 聲明形式 | 含義 | 可修改指針? | 可修改所指值? |
|---|---|---|---|
const int *p / int const *p | 指向常量的指針 | ✅ 是 | ❌ 否 |
int *const p | 常量指針(指針本身不可變) | ❌ 否 | ✅ 是 |
const int *const p | 指向常量的常量指針 | ❌ 否 | ❌ 否 |
#include <stdio.h>
int main() {
int i1 = 5, i2 = 10;
const int *p1 = &i2; // *p1 不可改,p1 可改
// *p1 = 200; // 編譯錯誤
p1 = &i1; // 合法
int *const p2 = &i2; // p2 不可改,*p2 可改
*p2 = 200; // 合法,i2 變為 200
// p2 = &i1; // 編譯錯誤
printf("i2 = %d\n", i2); // 輸出 200
return 0;
}
arr 在大多數上下文中轉換為首元素地址(&arr[0])。sizeof(arr) 返回整個數組大小,&arr 是數組指針(類型 int (*)[N])。arr = p 非法)。int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d %d %d\n", arr[i], *(arr+i), *(p+i)); // 完全等價
// 甚至 i[arr] 也合法(編譯器轉為 *(i+arr)),但請勿在生產代碼使用
}
printf("*p++ = %d\n", *p++); // 等價於 printf("%d\n", *p); p++;
printf("(*p)++ = %d\n", (*p)++); // 先取值打印,再對所指值 +1
所有示例均已整合為獨立可編譯的文件,建議逐個編譯運行,觀察內存地址變化與輸出結果,構建直觀的指針心智模型。
下一站預告:C語言指針進階——二級指針、void*、函數指針與回調機制,解鎖框架設計的核心鑰匙。