在C语言中,指针的的使用的非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码。

相关语法

语句 含义
type *ptr 声明一个名为 ptr 的 type 类型指针;
*ptr ptr指针所指向的位置的值;
*(ptr + i) ptr 指针所指向的地址加 i 后所保存的值;
&thing thing的地址
type *ptr = &thing 声明一个类型为 type 的指针,并将 thing 的地址赋给它
ptr++ ptr 指针所指向的地址加一

指针与地址

&

一元运算符 & 可用于取一个对象的地址,因此,下列语句:

1
p = &c;

将把 c 的地址赋给变量 p,我们称 p 为“指向”c的指针。地址运算符 & 只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register类型的变量。

*

一元运算符 * 是间接寻址或间接引用运算符 。当它作用于指针时,将访问指针所指向的对象。我们在这里假定 x 与 y 是整数,而 ip 是指向 int 类型的指针 ,下面的代码段说明了如何在程序中声明指针以及如何使用运算符 &*

1
2
3
4
5
6
int x = 1, y = 2, z[10];
int *ip; /* ip is a pointer to int */
ip = &x; /* ip now points to x */
y = *ip; /* y is now 1 */
*ip = 0; /* x is now 0 */
ip = &z[0]; /* ip now points to z[0] */
不管指针指向什么类型的数据,指针本身占4个字节的空间。

指针的声明

1
int *ip;

这样声明是为了便于记忆。该声明语句表明表达式 *ip 的结果是 int 类型。这种声明变量的语法与声明该变量所在表达式的语法类似。

我们应该注意,指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型。(一个例外情况是:指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身)

如果指针ip指向整型变量,那么在 x 可以出现的任何上下文中都可以使用*ip,因此,语句

1
*ip = *ip + 10;

将把 *ip 的值增加10。

优先级

一元运算符 *& 的优先级比算数运算符的优先级高,因此,赋值语句

1
y = *ip + 1

将把 *ip 指向的对象的值取出并加1,然后再将结果赋值给y,而下列赋值语句:

1
*ip += 1

则将 ip 指向的对象的值加1,它等同于

1
++*ip

1
(*ip)++

语句的执行结果。语句(**ip)++中的圆括号是必需的,否则,该表达式将对ip进行加1运算,而不是对ip指向的对象进行加1运算,这是因为,类似于*和++这样的一元运算符遵循从右至左的结合顺序。

指针也是变量

最后说明一点,由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方法使用。例如,如果 iq 是另一个指向整型的指针,那么语句:

1
iq = ip

将把 ip 中的值拷贝到 iq 中,这样,指针 iq 也将指向 ip 指向的对象。

指针与函数参数

由于 C 语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中变量的值。可以使主调程序将指向所要交换的变量的指针传递给被调用函数。由于一元运算符 & 用来取变量的地址,这样 &a 就是一个指向变量 a 的指针。

示例

swap函数的所有参数都声明为指针,并且通过这些指针来间接访问它们指向的操作数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void swap(int *px, int *py) /* interchange *px and *py */
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}

void main()
{
int a=2, b=3;
printf("交换前:a=%d,b=%d\n",a,b);
swap(&a,&b);
printf("交换后:a=%d,b=%d\n",a,b);
}

指针参数使得被调用函数能够访问和修改主调函数中对象的值。

指针与数组

指针保存数组元素的地址

指针可以保存数组元素的地址,例如:

1
2
3
int a[10];
int *pa = &a[0];
pa++;

在取数组元素时用数组名和用指针的语法一样,但如果把数组名做左值使用,和指针就有区别了。例如pa++是合法的,但a++就不合法,pa = a + 1是合法的,但a = pa + 1就不合法。

数组名做右值时转换成指向首元素的指针,但做左值仍然表示整个数组的存储空间,而不是首元素的存储空间,数组名做左值还有一点特殊之处:不支持++、赋值这些运算符,但支持取地址运算符&,所以&a是合法的。

指针代替数组作为参数

函数原型中的[ ]表示指针而不表示数组,例如:

1
2
3
4
void func(int a[])
{
...
}

等价于:

1
2
3
4
void func(int *a)
{
...
}

参数写成指针形式还是数组形式对编译器来说没区别,都表示这个参数是指针,之所以规定两种形式是为了给读代码的人提供有用的信息,如果这个参数指向一个元素,通常写成指针的形式,如果这个参数指向一串元素中的首元素,则经常写成数组的形式。

指针与字符串数组

字符串字面值类似于数组名,做右值使用时自动转换成指向首元素的指针,这种指针应该是const char *型。我们知道printf函数原型的第一个参数是const char *型,可以把char *或const char *指针传给它,所以下面这些调用都是合法的:

1
2
3
4
5
6
7
const char *p = "abcd";
const char str1[5] = "abcd";
char str2[5] = "abcd";
printf(p);
printf(str1);
printf(str2);
printf("abcd");

指针类型的数组

数组中的每个元素可以是基本类型,也可以复合类型,因此也可以是指针类型。例如定义一个数组a由10个元素组成,每个元素都是int *指针:

1
int *a[10];

这称为指针数组。它可以拆成两句:

1
2
typedef int *t;
t a[10];

我们知道main函数的标准原型应该是int main(int argc, char *argv[]);。argc是命令行参数的个数。而argv是一个指向指针的指针,为什么不是指针数组呢?因为前面讲过,函数原型中的[ ]表示指针而不表示数组,等价于char **argv。那为什么要写成char *argv[]而不写成char **argv呢?这样写给读代码的人提供了有用信息,argv不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *指针,指向一个命令行参数字符串。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[])
{
int i;
for(i = 0; i < argc; i++)
printf("argv[%d]=%s\n", i, argv[i]);
return 0;
}

注意程序名也算一个命令行参数,所以执行./a.out a b c这个命令时,argc是4,argv如下图所示:

argv指针数组
argv指针数组

由于argv[4]是NULL,我们也可以这样循环遍历argv:

1
for(i=0; argv[i] != NULL; i++)

NULL标识着argv的结尾,这个循环碰到NULL就结束,因而不会访问越界,这种用法很形象地称为 Sentinel(哨兵) ,NULL就像一个哨兵守卫着数组的边界。

在这个例子中我们还看到,如果给程序建立符号链接,然后通过符号链接运行这个程序,就可以得到不同的argv[0]。通常,程序会根据不同的命令行参数做不同的事情,例如ls -lls -R打印不同的文件列表,而有些程序会根据不同的argv[0]做不同的事情,例如专门针对嵌入式系统的开源项目Busybox,将各种Linux命令裁剪后集于一身,编译成一个可执行文件busybox,安装时将busybox程序拷到嵌入式系统的/bin目录下,同时在/bin、/sbin、/usr/bin、/usr/sbin等目录下创建很多指向/bin/busybox的符号链接,命名为cp、ls、mv、ifconfig等等,不管执行哪个命令其实最终都是在执行/bin/busybox,它会根据argv[0]来区分不同的命令。

指向数组的指针与多维数组

定义

以下定义一个指向数组的指针,该数组有10个int元素:

1
int (*a)[10];  /* a points to an array of 10 ints */

用 typedef 类型定义可使指向多维数组元素的指针更容易读、写和理解。上面的句子可以拆成两句:

1
2
typedef int int_array[10];
int_array *a;

t代表由10个int组成的数组类型,a则是指向这种类型的指针。

示例

示例1

下面代码分别演示了上述的两种定义方法。

  • 直接定义一个指向多维数组的指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

using namespace std;

int main()
{
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
typedef int int_array[4];
int (*p)[4];

for (p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << endl;
}
}
  • 用 typedef 简化指向多维数组的指针:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
typedef int int_array[4];

for (int_array *p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
printf("%d\n", *q);
}
}
示例2

下列代码演示了使用 pappa 访问数组 a 中的 r 元素。注意理解 pa 和 ppa 所保存的内容的不同:

  1. pa 指向长度为 3x2 的指针的地址,该指针保存 a 的第二个元素的地址
  2. ppa 指向长度为 2 的数组的指针,该指针保存 a[1] 的首元素的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(void)
{
char a[4][3][2] = { { {'a', 'b'}, {'c', 'd'}, {'e', 'f'} },
{ {'g', 'h'}, {'i', 'j'}, {'k', 'l'} },
{ {'m', 'n'}, {'o', 'p'}, {'q', 'r'} },
{ {'s', 't'}, {'u', 'v'}, {'w', 'x'} } };

char (*pa)[2] = &a[1][0];
char (*ppa)[3][2] = &a[1];

putchar(pa[5][1]);
putchar('\n');

putchar(ppa[1][2][1]);
putchar('\n');
return 0;
}

指针与const限定符

const限定符和指针结合起来常见的情况有以下几种。

指向const int型的指针

1
2
const int *a;
int const *a;

这两种写法是一样的,a是一个指向const int型的指针,a所指向的内存单元不可改写,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。

指向 int 型的 const 指针

1
int * const a;

a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。

指向 const int 型的 const 指针

1
int const * const a;

a是一个指向const int型的const指针,因此*a和a都不允许改写。

类型转换

指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,例如:

1
2
char c = 'a';
const char *pc = &c;

但是,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免透过后者意外改写了前者所指向的内存单元,例如对下面的代码编译器会报警告:

1
2
const char c = 'a';
char *pc = &c;

指针与结构体

首先定义一个结构体类型,然后定义这种类型的变量和指针:

1
2
3
4
5
6
struct unit {
char c;
int num;
};
struct unit u;
struct unit *p = &u;

要通过指针p访问结构体成员可以写成(*p).c和(*p).num,为了书写方便,C语言提供了->运算符,也可以写成p->cp->num

指针的指针

指针可以指向基本类型,也可以指向复合类型,因此也可以指向另外一个指针变量,称为指向指针的指针。

1
2
3
int i;
int *pi = &i;
int **ppi = &pi;

这样定义之后,表达式*ppi取pi的值,表达式**ppi取i的值。

很自然地,也可以定义指向“指向指针的指针”的指针,但是很少用到:

1
int ***p;

函数的指针

在C语言中,函数也是一种类型,可以定义指向函数的指针。函数指针存放的就是函数的入口地址(位于.text段)。

声明和初始化

函数指针的声明格式如下:

1
int (*pointer_name)(int a, int b) = func_name;

其中 func_name 是该指针指向的函数的名字。

如果要声明一个返回指针类型变量的函数指针,则可以这么写:

1
char *(*pointer_name)(int awesome_levels) = func_name;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int sort(int a, int b)
{
return a > b ? a : b;
}

int main(void)
{
int (*max)(int a, int b) = sort; /* 声明一个函数指向 sort */
printf("TEST: %d is same as %d.\n", max(2, 3), sort(2, 3));
return 0;
}

函数指针的常见用途是回调函数

函数返回指针

要声明一个返回指针的函数,只需在函数名前加上 *,例如:

1
void *max(void *data[], int num, cmp_t cmp);

实现:

1
2
3
4
5
6
7
8
9
10
void *max(void *data[], int num, cmp_t cmp)
{
int i;
void *temp = data[0];
for (i = 1; i < num; i++){
if(cmp(temp, data[i]) < 0)
temp = data[i];
}
return temp;
}

深入阅读

  1. 利用struct和函数指针实现面向对象

Comments