指针是一种变量或常量,它的值是另一个变量或常量的地址。指针是 C 语言中一个强大但也可能很危险的功能。理解指针可能并不容易。
int i = 5;
当我们给一个变量赋值之后,可以问两个问题。
你是谁?
我是 i。
你有什么?
我有一个整数 5。
还有第三个可以问的问题。
你在哪里?
我在 0x6dfed4。
等等,什么?
变量和常量把它们的值存储在一个内存位置上。这个内存位置称为该变量或常量的地址。
要获取一个变量的地址,我们使用取地址运算符,也就是&。
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开始,而不是从1开始。数组下标会把下标值乘以类型的大小,再加到数组的起始地址上。由于第一个元素与数组的开头对齐,要得到第一个元素,就把类型大小乘以0再加到起始地址上。要得到第二个元素,就把类型大小乘以1再加到起始地址上。如果类型长度为四个字节,那么第二个元素的地址会比第一个元素高四个字节。
这也说明了为什么指针要声明成某个特定的类型。如果指针不知道自己指向的是什么类型,那么在给它加1时就不知道该前进多少字节。给指针加1并不一定意味着给地址加一个字节。它意味着加上1乘以该类型以字节计的长度。
不过,等等,还有呢!你不会以为我们讲完*了吧?
既然我们理解了地址在数组下标中的运作方式,那如果我们取不到某个地址上存储的值,又有什么用呢?我们该怎么做呢?要取得某个地址上存储的值,我们使用间接寻址运算符,你可能还记得,它就是*。在这种情况下,间接寻址运算符有时也被称为解引用运算符,因为它对指针所持地址对应的那个值进行解引用。在用于解引用时把它叫作解引用运算符,是为了把它和用于定义指针的间接寻址运算符区分开;但严格来说,这两种用法里的*都叫间接寻址运算符。
拜托!你在逗我吗?*到底有多少种用法?!乘法、声明指针,现在又来个解引用!真是让人糊涂!而且,解引用到底是什么?
下面这个例子展示了如何用间接寻址运算符进行解引用
#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 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这个字面量一样。这是不被允许的。所以,问题其实不在于它是不是指针。问题在于它指向的是什么。