学废 C 语言——指针

指针是 C 语言的基本类型之一。因为某个红书的缘故,指针作为一个“教材难点”撂倒了无数大学新生,但 C 语言的使用不可能绕开指针,甚至我可以断言不会用指针就不要用 C。我希望这篇文章能帮你搞懂指针这个概念。

函数处理参数的方式

我们首先写一个改变指定变量的函数:

void add(double a, double b, double result)
{
    result = a + b;
}

加上一个 main 函数:

#include <stdio.h>
#include <stdlib.h>

void add(double a, double b, double result)
{
    result = a + b;
}

int main(int argc, char **argv)
{
    if (argc != 3) return EXIT_FAILURE;

    char *end;
    double a = strtod(argv[1], &end);
    if (*end) return EXIT_FAILURE;
    double b = strtod(argv[2], &end);
    if (*end) return EXIT_FAILURE;

    double result = 0;
    add(a, b, result);
    printf("%lf\n", result);
    return EXIT_SUCCESS;
}

运行:

$ gcc -o add add.c
$ add 1 1
0.0

令人纳闷的情况出现了:无论我们给 add 程序什么数字,它都会一成不变的把 0 输出出来。为什么?当你使用一个函数的时候,它是不会直接操作你给的变量的。使用函数时,C 语言会把函数得到的所有参数复制到新的变量中,改变这些新的变量当然不会让用它的地方的变量变一丝一毫。

指针入场

要想改变一个函数外的,不是全局变量的变量,需要用到指针。

指针是一个比较特殊的类型。和整数类型代表整数,浮点类型代表实数不同,指针代表的是一个其它的变量。通过指针,可以改变指针指向的变量的值。

当需要一个指向某个类型的指针变量的时候,会在类型之后加一个 * 号:

    double number;
    double *pointer;

C 语言有两个操作指针用的基本符号。符号 & 可以用来得到指向某个变量的指针:

    double *pointer = &number;

而符号 * 可以用来得到指针指向的变量的值,并改变它:

    double value = *pointer; // 读取变量
    *pointer += 1; // 改变那个变量
    // value += 1; // 改变读取出来的值改不了那个变量

用这两个符号就能通过指针改变其它变量了。

#include <stdio.h>
#include <stdlib.h>

void add(double a, double b, double *result)
{
    *result = a + b; // 修改指针指向的变量
}

int main(int argc, char **argv)
{
    if (argc != 3) return EXIT_FAILURE;

    char *end;
    double a = strtod(argv[1], &end);
    if (*end) return EXIT_FAILURE;
    double b = strtod(argv[2], &end);
    if (*end) return EXIT_FAILURE;

    double result = 0;
    add(a, b, &result); // 取指针
    printf("%lf\n", result);
    return EXIT_SUCCESS;
}

指针与数组

C 语言不止可以用来处理单单一个数,很多时候我们需要用数组和结构体来处理不止一个数的数据,而这些时候经常需要用到指针。

譬如一段文字。在 C 语言里,会用一个字符数组来存一段文字。把这个数组传递给函数时会有两个问题:一段文字的长度很多都会超过存储一个数字所需要的长度,直接复制那段文字给函数会让内存爆炸。而文字的长度也是可长可短,难以控制。

然而 C 语言自己就把这些问题解决好了。很多时候,C 语言会自动取一个指向数组内容的指针,用指针来完成对数组的操作。和大小不确定,有可能会非常大的数组不一样,指针和普通的数字一样大,这样一来处理数组的函数只需要用非常小的,大小确定的空间,就能完成对数组的一个操作。比如一个复制字符串用的函数:

extern char *strcpy(char dest[], const char src[]);

C 语言不会把字符串本身丢进函数,而是会取一个指向字符串内容的指针,把那个指针交给它,让它通过指针完成字符串的复制。

extern char *strcpy(char *dest, const char *src);

当一个指针指向一个数组的内容时,可以对这个指针做加减法来访问数组里的其它数据。

    char a[7] = " Hello!"
    printf("%s\n", a); // Hello!
    char *p = a + 1;
    *(p - 1) = "C";
    *p = "i";
    *(p + 2) = "a";
    printf("%s\n", a); // Ciallo!

用来访问数组的符号 [] 同样也可以用来访问并改变指针指向的数组的数据。

    char a[7] = " Hello!"
    printf("%s\n", a); // Hello!
    char *p = a + 1;
    p[-1] = "C";
    p[0] = "i";
    p[1] = "a";
    printf("%s\n", a); // Ciallo!

指针与结构体

和数组一样,结构体也是多个数的组合。和为什么要给函数指向数组内容的指针一样,一个结构体可能会非常大,直接传给函数可能会浪费大量空间,所以一般也会使用指针来传递结构体的数据。

通过指针操作结构体有一个专门的符号:->。这个符号由 -> 组成,形似一个向右的箭头。它的作用和 . 几乎一致,都是用来访问结构体成员的。和通过指针修改一个其它变量一样,通过 -> 可以修改一个外面的结构体的各个成员。

struct foo {
    int value;
};

void set_value(struct foo *foo, int value) {
    foo->value = value;
}

C 语言中可以声明有一个特定名字的结构体,但不声明这个结构体有哪些成员。没有声明成员的结构体不能直接作为值存在,但是指向这种结构体的指针是允许的。

typedef struct foo foo;

foo bar; // 编译错误
foo *baz; // OK

不仅如此,通过这个特性还可以让一个结构体中有指向其它相同类型的结构体的成员,可以用这个特性实现链表和二叉搜索树等数据结构。

typedef struct node node;

struct node {
    int value;
    node *next;
}

函数指针

指针不止可以指向数据,还可以指向函数。与其它指针不同,取函数的指针时,不需要把取指针符号(&)明确写出来:

    int (*ptr_to_printf)(const char *format, ...) = printf;

将函数指针作为函数的参数,就可以只用那一个函数来操作多种不同的数据。标准库自带的排序函数,不仅可以对一段整数数组进行排序:

static int cmp(const void *ap, const void *bp) {
    int a = *(const int *)ap, b = *(const int *)bp;
    return a > b - a < b;
}

/* 某个其它函数... */
    qsort(ints, length, sizeof(int), cmp);

也可以对字符串数组进行排序:

#include <string.h>

static int cmp(const void *ap, const void *bp) {
    return strcmp(ap, bp);
}

/* 某个其它函数... */
    qsort(strings, length, sizeof(char *), cmp);

只要自己的某个结构体能比较,更可以对那个结构体的数组进行排序:

/* my_struct.c */
struct my_struct;

int my_struct_cmp(void *av, void *bv) {
    struct my_struct *a = av, *b = bv;
    /* 比较代码... */
}

/* 其它文件 */
    /* ... */
    qsort(array_of_my_struct, length, sizeof(void *), my_struct_cmp);
    /* ... */

更多的指针

很多时候,指针指向的数据都有具体的类型——整数指针 int * 指向整数 int,字符指针char * 指向字符 char 等等。但是,指针可以不指向某个特定类型的数据。这时候,可以在填指向数据类型的地方填 void,来获得一个 void 指针(void *)。void 指针一般用在泛型编程中,典型的运用案例有标准库的 qsort 排序和 bsearch 二分搜索。

指针也可以套娃。有时候,我们需要用函数让一个指针不再指向一个数据,而是让它指向另一个位置,这时候就可以用到指向指针的指针。下面的链表删除的写法就用到了套娃指针:

node *node_delete(node **node, int value) {
    if (*node == NULL) return NULL;
    if ((*node)->value == value) {
        node *deleted = *node;
        *node = (*node)->next;
        return deleted;
    }
    return node_delete(&((*node)->next), value);
}
posted @ 2024-04-18 19:35  McEndu  阅读(18)  评论(0编辑  收藏  举报