指针

指针 属于 C

{other: "%{count} 个练习"}

关于 指针

指针是一种变量或常量,它的值是另一个变量或常量的地址。指针是 C 语言中一个强大但也可能很危险的功能。理解指针可能并不容易。

int i = 5;

当我们给一个变量赋值之后,可以问两个问题。

你是谁?

我是 i。

你有什么?

我有一个整数 5。

还有第三个可以问的问题。

你在哪里?

我在 0x6dfed4。

等等,什么?

变量和常量把它们的值存储在一个内存位置上。这个内存位置称为该变量或常量的地址。

取地址运算符与间接寻址运算符(第 1 部分)

要获取一个变量的地址,我们使用取地址运算符,也就是&。

int i = 5;
int *i_address = &i;

等等,i_address前面的那个*是什么意思?它不就表示乘法吗?

类型后面跟一个*,再跟一个标识符,就是声明一个指向该类型变量的指针的方式。*被称为间接寻址运算符,因为它间接引用了指针所持地址对应的那个值。

好吧,但为什么要专门用一个变量来保存另一个变量的地址呢?

我们一开始为什么会在意一个变量的地址是多少?

嗯,当变量处在数组中时,我们就需要知道它们的位置。

数组与指针

当我们这样创建一个数组时

int my_ints[] = {1, 2};

my_ints的值实际上就是它的地址,而这个地址恰好和它第一个元素的地址相同。它第二个元素的地址会是第一个元素的地址加上该系统中一个int的大小。

为了更清楚地看到这一点,试着运行下面的代码

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

指针运算,以及数组下标为什么从 0 开始

上一个例子展示了数组下标为什么从0开始,而不是从1开始。数组下标会把下标值乘以类型的大小,再加到数组的起始地址上。由于第一个元素与数组的开头对齐,要得到第一个元素,就把类型大小乘以0再加到起始地址上。要得到第二个元素,就把类型大小乘以1再加到起始地址上。如果类型长度为四个字节,那么第二个元素的地址会比第一个元素高四个字节。

这也说明了为什么指针要声明成某个特定的类型。如果指针不知道自己指向的是什么类型,那么在给它加1时就不知道该前进多少字节。给指针加1并不一定意味着给地址加一个字节。它意味着加上1乘以该类型以字节计的长度。

不过,等等,还有呢!你不会以为我们讲完*了吧?

间接寻址运算符(第 2 部分)

既然我们理解了地址在数组下标中的运作方式,那如果我们取不到某个地址上存储的值,又有什么用呢?我们该怎么做呢?要取得某个地址上存储的值,我们使用间接寻址运算符,你可能还记得,它就是*。在这种情况下,间接寻址运算符有时也被称为解引用运算符,因为它对指针所持地址对应的那个值进行解引用。在用于解引用时把它叫作解引用运算符,是为了把它和用于定义指针的间接寻址运算符区分开;但严格来说,这两种用法里的*都叫间接寻址运算符。

拜托!你在逗我吗?*到底有多少种用法?!乘法、声明指针,现在又来个解引用!真是让人糊涂!而且,解引用到底是什么?

下面这个例子展示了如何用间接寻址运算符进行解引用

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

在使用指针运算时,用括号有助于消除意图上的歧义。下面的例子打印出来的结果可能并不是程序员想要的

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

*和++的优先级相同,但它们从右向左求值。在第一行中,my_ptr先自增到第二个元素,然后被解引用。在第二行中,my_ptr(在第一行里已经自增到了第二个元素)先被解引用,然后被解引用得到的值再自增。如果我们把这一行加到上面的例子中

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

在这一行里,我们把指针自增到了数组之外。我们越过了数组的边界,指向内存中接下来的四个字节,指针把这四个字节解释为一个int;但实际上,这些字节可能属于另一个变量,也可能属于几个变量。这四个字节可能是一个double的一部分,也可能是四个char。问题在于,我们根本就不该用指针去看它们。

我们本可以用

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

由于数组下标是用指针实现的,所以同样存在越过数组边界的危险。

而且,由于字符串就是char的数组,指针也可以用于字符串。字符串常常用指针语法来声明,像这样

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

不过,有一个问题。为什么我们要把星号放在标识符旁边?既然它是指向该类型的指针,为什么不把它放在类型旁边呢?因为下面这样

int* my_ptr1, my_ptr2_that_is_really_an_int;

并不会声明两个指向int的指针,而是声明一个指向int的指针和一个int。要声明两个指向int的指针,应该这样写

int *my_ptr1, *my_ptr2;

所以,最好把*放在标识符附近,以尽量减少歧义。或者每行只声明一个标识符。而且,如果你还没看够*,欢迎来到……

指向指针的指针

字符串数组常常像下面这样表示

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

这一点可能不那么明显:这个字符串数组被声明并初始化成一个指向由char数组组成的数组的指针。当这个由char数组组成的数组被传入print_strings()时,签名把它描述成一个指向char的指针的指针。

那么我们怎么知道char **真的是一个指向由指向char数组的指针组成的数组的指针,而不是一个指向单个char的指针的指针呢?

我们并不知道。

这是让指针用起来令人困惑的许多事情之一。下面这段代码也许能编译通过

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

但print_strings()并没有按预期的方式被使用。换一个不同的签名也无济于事。把签名改成这样

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

可能仍然能编译,也仍然会打印出乱码。但传入一个指向单个char的指针的指针,其实毫无价值。当你看到char **时,它多半表示一个指向由指向char数组的指针组成的数组的指针。

我们已经见过把指针传入函数。但为什么要向函数传指针呢?

把指针传给函数

在 C 中,实参是按值传给函数的。这意味着值会从实参复制一份,而这个副本是函数局部的。当函数结束时,这个副本也随之消失。

看看我们尝试在函数内部修改变量的值时会发生什么

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

这是因为,尽管变量名字相同,swap()里的a和b并不是main()里的那个a和b。它们是a和b的值的_副本_。我们能不能改一改swap,让它按我们想要的方式工作?

当然可以!我们可以把a和b的地址作为指针传进去。但是,既然函数会复制指针,那又有什么用呢?有用,因为复制出来的地址仍然是同一个地址。通过解引用这个地址,我们就能访问到main()中定义的那个原始值。

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

现在swap里有好多*,但它现在能按预期工作了。

间接寻址运算符的一种读法是“取……处的值”。所以,*a就是a处的值。这个读法用在字符串上效果如何呢?

指向 char 的指针

我们知道,数组变量的值就是数组开始处的地址。我们也知道,可以把字符串定义成char数组,即char my_string[],或者定义成指向char的指针,即char *。两者有什么不同呢?

在这段代码中,我们向describe_me()传入了一个char *实参。adjective实参被定义成一个char数组,并被函数当作指向char的指针接收。

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

我们能够对adjective做出持久的修改,这说明定义好的char数组可以被当作指向char的指针来接收。

现在,让我们试着把adjective直接定义成指向char的指针。

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

还记得指针既强大又可能很危险吗?但它为什么就不行呢?数组就是一个指针,而指针明明能和char数组配合工作啊!原因是,之前adjective被定义成一个char数组,而它是属于main()的变量。现在,adjective被定义成一个字符串_字面量_,这意味着这些chars被写入了属于整个程序的只读静态内存。它们都是指针,但指向的东西不同。试图修改字符串字面量的值,就像试图修改5这个字面量一样。这是不被允许的。所以,问题其实不在于它是不是指针。问题在于它指向的是什么。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开