第 10 章 · 指针与函数

函数与指针两个方向:指针进函数(改外部变量、省拷贝)、从函数返回指针(别返回局部变量)。

9.1 C 只有值传递

C 里所有参数都是按值传递:函数拿到的是实参的一份拷贝。想让函数改外部的变量,只能传它的地址

void bad_swap(int a, int b) { int t = a; a = b; b = t; }   // 改了副本,无效
void swap(int *a, int *b)  { int t = *a; *a = *b; *b = t; } // 改的是指向的内存

int x = 1, y = 2;
swap(&x, &y);    // 传地址,x、y 真的交换了

心法(同第 7 章):要改谁,就传谁的地址。intint*,改指针传 T**

9.2 传指针的两大好处

  1. 让函数修改调用者的变量(上面的 swapscanf("%d", &x)、出参模式)。
  2. 避免拷贝大对象:传结构体指针,而不是整个结构体(详见第 12 章)。
void fill(int *a, int n, int v) {   // 数组/大缓冲区都走指针
    for (int i = 0; i < n; i++) a[i] = v;
}

9.3 返回指针:千万别返回局部变量地址

局部变量随函数返回栈帧销毁,返回它的地址即悬空指针:

// 返回局部变量地址:函数返回后 s 已不存在
char *bad(void) {
    char s[64] = "hello";
    return s;
}

// 更隐蔽:返回局部数组元素地址
int *bad2(void) {
    int a[10] = {0};
    return a;      // a 在栈上,返回即失效
}

能安全返回的三种指针

  1. 指向静态存储期的对象(static / 字符串字面量 / 全局):
char *ok1(void) { static char s[64] = "hello"; return s; }
char *ok2(void) { return "hello"; }   // 字面量在只读段,永存
  1. 指向上的对象(调用者负责 free):
char *ok3(void) {
    char *s = malloc(64);
    strcpy(s, "hello");
    return s;      // 堆内存由调用者释放
}
  1. 调用者传入的指针(原样返回,或返回其内部位置):
char *ok4(char *buf) { strcpy(buf, "hello"); return buf; }

9.4 函数内判断「指针是否可写」

字符串字面量不可写、char[] 可写,但进了函数都变成 char* 无法区分。解决办法:const 声明只读意图,把「会不会改」写进签名:

void show(const char *s);                    // 只读,可传字面量
void upper(char *s);                          // 会写,调用者必须传可写数组

9.5 指针作为「多返回值」

C 函数只能返回一个值,多返回值靠指针出参

// 求商和余数,两个结果通过指针带出
int divmod(int a, int b, int *rem) {
    *rem = a % b;
    return a / b;
}

int rem;
int q = divmod(17, 5, &rem);   // q=3, rem=2

9.6 与汇编 / 其他语言对照

  • 汇编:指针传参就是把地址放进寄存器/栈swap 的两个 int* 对应两次 mov [rsi] 类间接寻址,零拷贝、极快。
  • Java:对象是引用传递(引用按值传),基本类型按值;没有「返回局部变量地址」的问题,因为对象都在堆、有 GC。
  • C++:引用参数 void f(int& x) 是「传指针」的语法糖,写法更清爽,代价是调用处看不出是否会被改(swap(x,y) 不显式取地址)。
  • Python:不可变对象(int/str)看起来按值,可变对象(list)按引用;return a, b 直接打包成元组,天然多返回值。

① C 只有值传递,改外部变量靠传地址;② 传指针省拷贝、支持出参;③ 返回指针只返回静态/堆/入参地址,绝不返回局部变量地址。