Everything you need to know about pointers in C
指针的定义:
指针就是内存地址。
--------------------------------------------------------------------------------
开始:
定义一个变量foo:
int foo;
这个变量占据一定的内存空间。在目前主流的英特尔处理器中,它占据4个字节(因为int是4字节宽的)。
现在我们来定义另一个变量。
int *foo_ptr = &foo;
foo_ptr被定义为指向int的指针。我们初始化它指向 foo。
正如我说的, foo占据一定的内存。它在内存中的位置就是它的地址。 &foo是 foo的地址(&是取地址运算符)。
把每个变量想象成一个盒子,因此 foo是一个 sizeof(int)大小的盒子。这个盒子的位置就是它的地址。当你访问这个地址时,实际上你访问的是它指向的盒子中的内容。
不管类型如何,所有的变量都是如此。实际上,从语法角度来说,没有“指针变量”这种东西:所有的变量都是相同的。但是,变量有不同的类型。foo的类型是 int,foo_ptr的类型是int * (因此, “指针变量”实际就是“指针类型的变量”)。
这里的关键是指针不是变量!指向foo的指针实际是foo_ptr中的内容。你可以在 foo_ptr盒子中放另一个指针,并且这个盒子仍然是 foo_ptr。但是它不再指向foo。
顺便说一下,指针也有类型。foo_ptr的类型是int,所以它是一个 “整形指针”, int **的类型是 int * (它指向一个指向int型的指针)。
--------------------------------------------------------------------------------
声明语法:
在一行里声明两个指针变量的明显的方式是:
int* ptr_a, ptr_b;
•如果变量是指向int的指针,它的类型就是int *,
•一个单行声明可以声明同类型的多个变量,这些变量以逗号分隔 (ptr_a, ptr_b),
•所以,你可以在int *后面跟以逗号分隔的ptr_a, ptr_b来定义多个int型指针变量。
这样,ptr_b的类型是什么呢? int *,对吗?
错了!
ptr_b 的类型是 int。它不是一个指针。
当一个类型跟着多个声明时,C语言的语法忽略指针的*。如果你将 ptr_a和ptr_b的声明分成多个声明,会得到
int *ptr_a;
int ptr_b;
将它看做为每一个变量赋一个基类型 (int)外加一个间接地级别,这个级别由*号表示(ptr_b的级别是0; ptr_a的是1)。
以一种清晰地方式进行单行声明是可能的。改进之后为:
int *ptr_a, ptr_b;
注意*号的位置变了。现在它紧靠 ptr_a的右边。这是一种微妙的关联。
把非指针变量放在前面更加清晰:
int ptr_b, *ptr_a;
最清晰地方式是把每个声明写在一行,但这会占据很多的空白空间。根据你自己的判读去选择。
最后,我应该指出,你这样做就好了:
int *ptr_a, *ptr_b;
这就没什么错误了。
有些情况下, C语言允许变量名和*用0或多层括号包围:
int ((not_a_pointer)), (*ptr_a), (((*ptr_b)));
除了声明函数指针,这没有什么用处。
--------------------------------------------------------------------------------
赋值和指针:
现在, 你如何把int变量赋值给指针呢?方法是很明显的:
foo_ptr = 42;
但这是错误的。
任何对指针变量的直接赋值都会改变变量中的地址,而不是那个地址中的值。在这个例子中, foo_ptr的新值是 42。但我们不知道它指向什么东西.。试图访问这个地址可能导致系统崩溃。
那么你如何访问一个指针中的值呢?你必须对它去引用。
--------------------------------------------------------------------------------
取值:
int bar = *foo_ptr;
在这个声明中,取值操作符 (前缀的 *,不要和乘号混了)取得存储在地址中的值 (这也被称作“加载”操作)。
也可以写一个取值表达式 (C的方式是:取值表达式是一个左值,也就是说在赋值号的左边):
*foo_ptr = 42; //把foo设置为42(这也被称作 “存储”操作)。
--------------------------------------------------------------------------------
数组:
这儿是一个包含三个int类型的数组:
int array[] = { 45, 67, 89 };
注意我们用了[]这个记号,因为我们是声明一个数组。int *array在这儿是非法的,编译器不允许我们用{ 45, 67, 89 }初始化它。
数组变量是一个更大的盒子:存储三个int型整数。
C语言的一个特点是,在大多数情况下,当你再次使用这个数组名时,实际是使用指向数组第一个元素的指针( &array[0])。这被称为“退化”:数组退化为一个指针。在大多数情况下,使用数组就像数组被声明为一个指针一样。
当然,也有些情况是不等同的。一种情况是为数组名本身赋值——这是非法的。
另一种情况是把数组名传给sizeof操作符,结果是数组本身的大小,而不是一个指针的大小。这表明你是在操作一个数组,而不仅仅是指针。
但是在大多数情况下,数组表达式的作用就像指针表达式一样。
因此, 例如你想把一个数组传给printf,你是做不到的。当你把数组作为实参传给一个函数时,实际上你传递的是指向数组第一个元素的指针,因为这时候数组退化为一个指针。你只能传给printf这个指针,而不能是整个数组(printf没法打印一个数组的原因: printf需要你告诉它数组元素的类型以及总共有多少个元素,这样的话格式化字符串和参数列表都会变得混乱)。
退化是隐式的&-- array == &array == &array[0]。在英语中,这些表达式分别读作“数组”、“指向数组的指针”、“指向数组第一个元素的指针”(下标运算符 []的优先级比取地址运算符&的优先级高)。但在C中,这三个表达式的意思相同。(如果数组实际上是一个指针变量时,这三个式子的意思就不尽相同,因为指针变量的地址不同于它保存的地址--因此,中间的式子&array不同于其他两个。只有当数组确实是数组时,它们才严格相等)。
--------------------------------------------------------------------------------
指针算术运算(1==4):
我们想打印出数组的三个元素,
int *array_ptr = array;
printf(" first element: %i\n", *(array_ptr++));
printf("second element: %i\n", *(array_ptr++));
printf(" third element: %i\n", *array_ptr);
输出为:
first element: 45
second element: 67
third element: 89
以防你不熟悉++操作符,我这里给出它的意思:变量增加1,等价于+= 1。
但这儿我们到底做了什么呢?
这是与指针类型有关的。这儿指针的类型是int。当你从指针中加上或减去一个常数,内存地址实际增加或减少的是这个常数乘以指针类型的大小.在这个增加三次的例子中,内存地址每次都增加1* sizeof(int)的大小。
顺便说一下, 尽管sizeof(void) 是非法的,但空指针可以增加或减少 1 byte。
如果你还怀疑 1 == 4的话:想想我先前提到的,在目前的英特尔处理器中int占4个字节.所以,在这样一台机器上,从一个int型指针中加 1或减 1 都会改变4个字节。因此, 1 == 4. (程序员的幽默)。
--------------------------------------------------------------------------------
下标:
printf("%i\n", array[0]);
会发生什么呢?
结果会出现:
45
你可能已经算出来了。但这和指针有什么关系呢?
这是C语言的另一个秘密。下标操作符 (即 array[0]中的[])与数组没有任何关系。
的确,这是它最通常的用法。但是记住,在大多数情况下,数组会退化为指针。这是其中的一种情况:你传过去的不是数组,而是一个指针。
作为证据,我运行下面的代码:
int array[] = { 45, 67, 89 };
int *array_ptr = &array[1];
printf("%i\n", array_ptr[1]);
结果是:89
这可能有点难以理解,解释如下:
数组名是数组首元素的地址;array_ptr被赋值为&array[1],所以它指向数组的第二个元素,也就是说array_ptr[1]等价于array[2](array_ptr开始于数组的第二个元素,所以它的第二个元素array_ptr[1]是数组的第三个元素)。
此外,你可能注意到,由于第一个元素的宽度是sizeof(int)字节,所以第二个元素的地址是数组的开始向前sizeof(int)个字节。没错,你是对的:array[1]等价于*(array + 1)。(记住从指针增加或减去一个数等于增减这个数乘以指针类型的大小,因此指针加1就是指针值加sizeof(int)字节)。
--------------------------------------------------------------------------------
结构体和共同体:
C语言中两个更有趣的类型是结构体和共同体。你可以使用struct关键字构造一个结构体,用union关键字构造一个共同体。
这些类型的确切定义超出了本文的范畴。额外说一句,一个结构体或共同体的声明就像这样:
struct foo {
size_t size;
char name[64];
int answer_to_ultimate_question;
unsigned shoe_size;
};
语句块中的每个声明叫做结构体的成员。共同体也有成员,但是它们的用法不同。可以像这样访问一个成员:
struct foo my_foo;
my_foo.size = sizeof(struct foo);
表达式my_foo.size表示my_foo的大小。
如果有一个指向结构体的指针你会怎么做呢?
//One way to do it
(*foo_ptr).size = new_size;
但是对于这个问题还有一个更好的办法:指向成员操作符。
//Yummy
foo_ptr->size = new_size;
不幸的是,它看起来不像多重间接地址那样好。
//Icky
(*foo_ptr_ptr)->size = new_size;//One way
(**foo_ptr_ptr).size = new_size; //or another
Pascal 在这方面做得更好。它的取值运算符只一个前缀的 ^:。
{ Yummy }
foo_ptr_ptr^^.size := new_size;
(别抱怨啦,C是一门更好的语言)
--------------------------------------------------------------------------------
多重间接地址:
我想更深入一点的介绍多重间接地址。
考虑下面的代码:
int a = 3;
int *b = &a;
int **c = &b;
int ***d = &c;
下面是这些指针的值和谁相等:
*d == c; //去引用一个 (int ***)你可以得到一个(int **) (3 - 1 = 2)
**d == *c == b; //两次去引用一个 (int ***) ,或者一次去引用 (int **),你可以得到一个(int *) (3 - 2 = 1; 2 - 1 = 1)
***d == **c == *b == a == 3; //三次去引用一个(int ***),或者两次去引用 (int **),或者一次去引用 (int *) , 你可以得到一个 int (3 - 3 = 0; 2 - 2 = 0; 1 - 1 = 0)
所以,&操作符可以认为添加一个*(我称它为增加指针级别),而*、 ->和[]操作符可以认为取出一个*(减少指针级别)。
--------------------------------------------------------------------------------
指针和 const
当涉及指针时,const关键字的用法有些不同。下面两种声明是等价的:
const int *ptr_a;
int const *ptr_a;
但是,这两种却不等价:
int const *ptr_a;
int *const ptr_b;
第一个例子里,指针指向的值是常量,你不能赋值:*ptr_a = 42。第二个例子里,指针本身是常量,你可以改变*ptr_b,(译者注:即指针指向的值),但你不能改变指针本身。
--------------------------------------------------------------------------------
函数指针:
注意: 这里的语法看起来很奇怪。的确,它是很多人感到困惑,甚至一些C专家。
取得一个函数的地址也是有可能的。而且,就像数组,使用函数名时他也退化为一个指针。所以,如果你想取得函数 strcpy的地址,你可以用strcpy或者&strcpy(很明显&strcpy[0]不可以)。
你可以用函数调用操作符调用一个函数,它的左边是一个函数指针。
在这个例子中,我们传递dst和src作为参数,strcpy是调用的函数(也就是函数指针)。
enum { str_length = 18U }; //Remember the NUL terminator!
char src[str_length] = "This is a string.", dst[str_length];
strcpy(dst, src);//The function call operator in action (notice the function pointer on the left side)
有一种特殊的语法来声明函数指针类型。
char *strcpy(char *dst, const char *src); //An ordinary function declaration, for reference
char *(*strcpy_ptr)(char *dst, const char *src);//Pointer to strcpy-like function
strcpy_ptr = strcpy;
strcpy_ptr = &strcpy; //This works too
//strcpy_ptr = &strcpy[0]; //But not this
注意上面的声明中*strcpy_ptr旁边的括号,它分开了表明返回类型是(char *)的*和表明变量指针级别的*(*strcpy_ptr —一级指针,指向函数)。
此外,就像一个正规的函数声明,这里形参名是可选的:
char *(*strcpy_ptr_noparams)(char *, const char *) = strcpy_ptr;//Parameter names removed — still the same type
指向strcpy的指针类型是 char *(*)(char *, const char *);
你可能已经注意到上面的声明少了变量名。你可以用它做强制转换,例如:
strcpy_ptr = (char *(*)(char *dst, const char *src))my_strcpy;
正如你所希望的,指向函数指针的指针在括号里有两个*:
char *(**strcpy_ptr_ptr)(char *, const char *) = &strcpy_ptr;
我们也可以有函数指针的数组:
char *(*strcpies[3])(char *, const char *) = { strcpy, strcpy, strcpy };
char *(*strcpies[])(char *, const char *) = { strcpy, strcpy, strcpy };//Array size is optional, same as ever
strcpies[0](dst, src);
以C99的标准来说,下面是一个病态的声明."这个声明声明了一个返回值是int的无参函数f,一个返回值是int型指针的无参函数fip和指向一个返回值为int的无参函数的函数指针pfi"。(6.7.5.3[16])
int f(void), *fip(), (*pfi)();
换句话说,上面的声明等价于下面三个声明:
int f(void);
int *fip(); //Function returning int pointer
int (*pfi)();//Pointer to function returning int
如果你感到心力憔悴了,振作起来吧……
函数指针甚至可以是函数的返回值。这部分确实比较难理解,所以开动你的脑筋,不要被打击到吧!
为了解释这部分内容,我会把你已经学到的声明语法做个总结。首先是声明一个指针变量:
char *ptr;
这个声明告诉可我们指针类型(char),指针级别(*)和变量名(ptr),后两个可以放在括号里:
char (*ptr);
如果我们用跟着一组参数的名字替换第一个声明里的变量名会发生什么?
char *strcpy(char *dst, const char *src);
哈,一个函数声明。
我们可以移除表明指针级别的*,但是记住这个函数里的*是函数返回值类型的一部分。所以如果我们加上表示指针级别的*(使用括号):
char *(*strcpy_ptr)(char *dst, const char *src);
一个函数指针变量!
但是等等,如果这是一个变量,并且第一个声明也是一个变量,那么在这个声明里我们可不可以用一个名字加一个参数集合来代替变量名呢?
当然可以!并且结果是一个返回值为函数指针的函数声明。
char *(*get_strcpy_ptr(void))(char *dst, const char *src);
记住函数指针的类型没有参数,而且返回的int是int (*)(void)。
因此这个函数的返回值类型是char *(*)(char *, const char *) (再说一次,里面的*表明是指针,外面的*是指向的函数的返回值的一部分)。可能你还记得,这也是strcpy_ptr的返回类型。
所以,这个函数,调用时没有参数,将会返回一个指向strcpy那样的函数的指针:
strcpy_ptr = get_strcpy_ptr();
由于函数指针的语法太难理解,大多数开发者都会用typedefs去抽象化它们:
typedef char *(*strcpy_funcptr)(char *, const char *);
strcpy_funcptr strcpy_ptr = strcpy;
strcpy_funcptr get_strcpy_ptr(void);
--------------------------------------------------------------------------------
字符串(为什么没这种东西?)
C语言中没有字符串这种类型。
现在你会有两个问题:
1.如果没有这种类型的话,为什么我总是看到参考“C strings”这样的话呢?
2.这和指针又有什么关系?
事实是,C字符串是一个想象的概念(除了字符串表面量)。没有字符串类型,C字符串实际上是字符数组:
char str[] = "I am the Walrus";
这个数组有16字节长:其中15个用于存储“I am the Walrus”,再加上一个NUL结束符(值为0)。换句话说,str[15](最后一个元素)是0。这个字符代表着“string”的结束。
惯用法是扩展C以使其拥有字符串类型。但实际只是:一个惯用法。除非它得到这样的支持:
•前面提及的字符串字面量语法
•字符串库
string.h中的函数用于字符串操作。但是没有字符串类型,这怎么可能呢?
为什么不能呢,通过指针就可以。
这儿是一个简单函数strlen的一种实现,它返回字符串的长度(不包括NUL结束符):
size_t strlen(const char *str) { //Note the pointer syntax here
size_t len = 0U;
while(*(str++)) ++len;
return len;
}
注意指针算数运算和去引用运算的使用。这是由于,尽管有函数名,这里却没有“string”,只有指向其中至少一个字符的指针,最后一个字符是0。
下面是另一种可能的实现:
size_t strlen(const char *str) {
size_t i;
for(i = 0U; str[i]; ++i);
//When the loop exits, i is the length of the string
return i;
}
这里使用了下标。正如我们先前知道的,它也是用的指针(不是数组,当然也不是字符串)。