添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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

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[4]是NULL,我们也可以这样循环遍历argv:

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

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

在这个例子中我们还看到,如果给程序建立符号链接,然后通过符号链接运行这个程序,就可以得到不同的argv[0]。通常,程序会根据不同的命令行参数做不同的事情,例如 ls -l ls -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
    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);
    }
    }

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

  • pa 指向长度为 3x2 的指针的地址,该指针保存 a 的第二个元素的地址
  • 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->c p->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;
    }
  • 利用struct和函数指针实现面向对象
  •