1

一、什么是指针

1. 地址与指针

在程序中定义了一个变量,编译时系统会给这个变量分配存储单元,同时根据变量的数据类型,分配一定长度的空间。内存区的每一个字节都有一个编号,这就是“地址”。由于通过地址就可以找到所需的变量单元,可以说,地址指向该变量单元。由此,将地址形象地称为指针

C 语言对不同的数据类型分配不同大小的存储单元,且不同数据类型的存储方式是不一样的。因此,即使给了一个地址,也无法保证能正确存取所需要的信息。为了能正确存取一个数据,出路需要位置信息,还需要该数据的类型信息。C 语言的地址包括位置信息(地址编号,或称纯地址)和它所指向的数据的类型信息,或者说它是“带类型的地址”。

2. 直接访问与间接访问

在程序中,一般是通过变量名来引用变量的值的,如:

scanf("%d", &n);
printf("%d\n", n);

这种直接按变量名进行的访问称为直接访问

还可以采用另一种称为间接访问的方式,即将一个变量 n 的地址存放在另一个变量 n_pointer 中,然后通过变量 n_pointer 来找到变量 n 的地址,从而访问该变量。

如果我们要对变量 n 赋值,现在就有了两种方法:

  1. 直接访问:根据变量名直接向变量 n 赋值,由于变量名与变量地址有一一对应关系,因此就按照此地址直接对变量 n 的存储单元进行访问。
  2. 间接访问:先找到存放变量 n 地址的变量 n_pointer,从中得到变量 n 的地址,从而找到变量 n 的存储单元,对它进行访问。

如果一个变量专门用来存放另一变量的地址,则称为指针变量。上述的 n_pointer 就是一个指针变量。指针变量就是地址变量,用来存放地址。

二、指针变量

存放地址的变量就是指针变量,它用来指向另一个对象(如变量、数组、函数等)。

1. 定义指针变量

定义指针变量的一般形式为:

类型名* 指针变量名;

定义指针是必须指定其基本类型,该类型表示此指针可以指向的变量的类型。因此,指针类型是基本数据类型派生出来的类型,不能离开基本类型而独立存在。

定义指针的同时也可以对其进行初始化,如:

int* a;
char* b = &c;

定义指针时要注意:

  • 指针前面的 * 表示该变量为指针型变量,指针变量名为 a 而不是 *a
  • 一个变量的指针的含义包括两方面:存储单元编号表示的地址和它指向的存储单元的数据类型。
  • 指针变量只能存放地址,不能将一个整数赋值给指针变量。

2. 引用指针变量

引用指针变量有 3 种情况:

  1. 给指针变量赋值:

    p = &a;
  2. 引用指针变量指向的变量:

    *p = 1;
    printf("%d", *p);
  3. 引用指针变量的值:

    printf("%o", p);  // 以八进制输出指针变量的值(即指向的变量的地址)

要熟练掌握两个有关运算符:

  1. &:取地址运算符。
  2. *:指针运算符(或称为“间接访问运算符”)。

3. 指针变量作为函数参数

将一个变量的地址传到一个函数中可以使用指针变量作为函数参数。

#include <stdio.h>

int main() {
    void swap(int* x, int* y);
    int a, b, *p1, *p2;
    scanf("%d %d", &a, &b);
    p1 = &a;
    p2 = &b;
    if (a < b)
        swap(p1, p2);
    printf("max = %d, min = %d\n", a, b);
    return 0;
}

void swap(int* x, int* y) {
    int temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

上面的程序通过指针实现了对输入的两个整数按大小输出。可以发现,在调用函数 swap() 之后,变量 ab 会发生交换。通常情况下,由于“单向传递“的值传递方式,形参值的改变不能使实参的值随之改变。为了使函数中改变了的变量的值能够被主函数 main() 所用,应该使用指针变量作为函数参数。函数执行过程中使指针变量所指向的变量值发生变化,函数调用结束后,这些变化依然保留了下来。

如果想通过函数调用得到 n 个要改变的值,可以这样做:

  1. 在主调函数中设置 n 个变量,用 n 个指针指向它们。
  2. 设计一个有 n 个指针形参的函数,在函数中改变转这 n 个形参的值。
  3. 在主调函数中调用这个函数,将这 n 个指针变量作为实参传递给该函数。
  4. 执行函数时,通过形参指针变量改变它们所指向的 n 个变量的值。
  5. 主调函数中就可以使用这些改变了值的变量。

需要注意,不能企图通过改变指针形参的值来使指针实参的值改变,因为实参变量与形参变量之间值的传递时单向的,不可能通过执行调用函数来改变实参指针变量的值,但是可以改变实参指针变量所指变量的值。例如,将上面例子中的 swap() 函数换为下面的函数是达不到要求的:

void swap(int* x, int* y) {
    int* temp;
    temp = x;
    x = y;
    y = temp;
}

注意:函数的调用可以且最多可以得到一个返回值,而使用指针变量做参数,可以得到多个变化了的值。要善于利用指针法。

三、通过指针引用数组

1. 数组元素的指针

一个数组包含若干元素,每个元素在内存中都有一个相应的地址,指针变量可以指向变量,也可以指向数组中的元素。数组元素的指针就是数组元素的地址。

int a[5] = {1, 2, 3, 4, 5};
int* p;
p = &a[0];

引用数组元素可以使用下标法,也可以使用指针法。指针法占用的内存更少,运行速度快,能提高目标程序质量。

C 语言中数组名(不包括形参数组名)代表数组中首元素的地址,因此下面两个语句等效:

p = &a[0];
p = a;

2. 引用数组元素时指针的运算

指针即地址,对指针进行赋值运算是没问题的,但对指针进行乘和除的运算是没有意义的。在一定条件下,允许对指针进行加和减的运算。

在指针已指向一个数组元素时,可以对指针进行以下运算:

  • 加一个整数:p + 1
  • 减一个整数:p - 1
  • 自加运算:p++++p
  • 自减运算:p----p
  • 两个指针相减:p1 - p2

分别说明如下:

  • 如果指针变量 p 已经指向数组中的一个元素,则 p + 1 指向同一数组的下一个元素,p - 1 指向同一数组的上一个元素。注意:p + 1 不是简单地将 p 的值加 1,而是在 p 的值(地址)上加上一个数组元素所占用的字节数,从而使 p 指向下一个元素。
  • 如果 p 的初值为 &a[0],则 p + ia + i 都指向数组 a 序号为 i 的元素。
  • *(p + i)*(a + i)a[i] 三者等价。
  • *(p++) 是先取 *p 的值,然后使 p 加 1.*(++p) 是先使 p 加 1,再取 *p 的值。
  • 如果指针变量 p1p2 都指向同一数组中的元素,则 p1 - p2 的结果是两个地址之差除以数组元素的长度(占用的字节数)。即利用 p1 - p2 就可以hi到它们所指元素的相对距离。

注意:

  • 两个地址不能相加,p1 + p2 是无实际意义的。
  • [] 实际是变址运算符a[i] 表示按照 a + i 计算地址,然后找出此地址单元中的值。

3. 通过指针引用数组元素

综上,引用一个数组元素可以有两种方法:

  1. 下标法:a[i]
  2. 指针法:*(a + i)*(p + i)

需要注意的是:

  • 可以通过改变指针变量的值指向不同的元素,但要注意指针变量的当前值。
  • p++ 不能使用 a++ 代替。因为数组名 a 代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。
  • 指向数组元素的指针变量可以带下标,如 p[i]。因为程序编译时,对下标的处理方式是转换为地址,对 p[i] 处理为 *(p + i)。使用 p[i] 时必须先弄清楚 p 的当前值是什么,否则容易和 a[i] 混淆。

4. 用数组名做函数参数

07 - 函数中,介绍了使用数组名作为函数的参数。当用数组名做参数时,如果形参数组中各元素的值发生了变化,实参数组元素的值随之变化。这是因为是实参数组名代表该数组首元素的地址,而形参是用来接收从实参传过来的数组首元素地址的,因此形参应该是一个指针变量。实际上,C 编译都是将形参数组名作为指针变量来处理的。

下面的两种写法时等效的:

fun(int arr[], int n)
fun(int* arr, int n)

注意:实参数组名代表一个固定的地址,或者说时指针常量,但形参数组名并不是固定的地址,而是按照指针常量处理。

5. 通过指针引用多维数组

指针变量也可以指向多维数组中的元素,但其概念和使用方法要比一维数组复杂。以下面的二维数组为例:

int a[2][3] = {{1, 3, 5}, {2, 4, 6}};

从二维数组的角度来看,a 代表二维数组首元素的地址,但现在的首元素是由 3 个整型元素所组成的一维数组,因此现在的 a 代表的是首行 a[0] 的起始地址,a + 1 代表下一行 a[1] 的起始地址。

a[0]a[1] 都是一维数组名,数组名代表数组首元素的地址,因此 a[0] 代表一维数组 a[0] 中第 0 列元素的地址,即 &a[0][0]。同理,a[1] 的值是 a[1][0]

由前可知,a[i]*(a + i) 等价,因此a[i] + j 也和 *(a + i) + j 等价,两者都是 &a[i][j]

表示形式含义
a二维数组名,指向一维数组 a[0] 的起始地址
a[i]*(a + i)*ai 行 0 列元素地址
a + 1&a[i]i 行起始地址
a[i] + j*(a + i) + j&a[i][j]i 行 j 列元素 a[i][j] 的地址
*(a[i] + j)*(*(a + i) + j)a[i][j]i 行 j 列元素 a[i][j] 的值

如前所述,C 语言的地址信息中既包含位置信息,也包含它所指向的数据的类型信息。a 是二维数组名,是二维数组首行起始地址;a[0] 是一维数组名,是一维数组其实元素的地址。两者的纯地址相同,但基类型不同,前者是一维数组,后者是整型数据。

如果要用一个指针变量 p 来指向此一维数组,应该这样定义:

int (*p)[3];
// 表示 pt 指向 4 个整型元素组成的一维数组

注意:要注意指针变量的类型,int (*p)[3]p 的类型不是 int * 型,而是 int (*)[3] 型,p 被定义为指向一维数组的指针变量,一维数组有 3 个元素,因此 p 的基类型是一维数组。

四、通过指针引用字符串

1. 字符串的引用方式

C 程序中,字符串是存放在字符数组中的,要引用一个字符串,可以有以下两种方法:

  1. 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中的一个字符,也可以通过数组名和格式声明 %s 输出该字符串。
  2. 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
#include<stdio.h>

int main() {
    char* string = "Hello World!";
    printf("%s\n", string);
    return 0;
}

// Hello World!

上面的程序没有定义字符数组,只定义了一个 char * 型的指针变量,并用一个字符串常量对其进行初始化。C 语言对字符串常量是按照字符数组来处理的,但这个字符数组没有名字,因此不能使用数组名来引用,只能通过指针变量来引用。

对字符指针变量初始化,实际上是把字符串的第一个元素的地址(即存放字符串的字符数组的首元素地址)赋值给指针变量,使之指向字符串的第一个字符。

#include <stdio.h>

int main() {
    char a[] = "Hello World!", b[20], *p1, *p2;
    p1 = a;
    p2 = b;
    for (; *p1 != '\0'; p1++, p2++)
        *p2 = *p1;
    *p2 = '\0';
    printf("String a is: %s\n", a);
    printf("String b is: %s\n", b);
    return 0;
}

// String a is: Hello World!
// String b is: Hello World!

2. 字符指针做函数参数

要把一个字符串从一个函数传递到另一个函数,可以用地址传递的方法,即用字符数组名做参数,也可以用字符指针变量做参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以引用改变后的字符串。

#include <stdio.h>

int main() {
    void copy_string(char from[], char to[]);
    char a[] = "Hello";
    char b[] = "World";
    printf("a: %s\tb: %s\n", a, b);
    copy_string(a, b);
    printf("a: %s\tb: %s\n", a, b);
    return 0;
}

void copy_string(char from[], char to[]) {
    int i = 0;
    while (from[i] != '\0') {
        to[i] = from[i];
        i++;
    }
    to[i] = '\0';
}

// a: Hello        b: World
// a: Hello        b: Hello

上面的程序使用字符数组名作为函数实参,若要用字符指针变量做实参,可以将第 8 行代码改为下面两行:

char *from = a, *to = b;
copy_string(from, to);

3. 字符指针变量和字符数组的比较

用字符数组和字符指针变量都能实现字符串的储存和运算,但二者之间是有区别的:

  1. 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第 1 个字符的地址),绝不是将字符串放到字符指针变量中。
  2. 可以对字符指针变量赋值,但不能对数组名赋值。
  3. 编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元。
  4. 指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。
  5. 字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。
  6. 用指针变量指向]一个格式字符串,可以用它代替 printf 函数中的格式字符串。因此只要改变指针变量所指向的字符串,就可以改变输入输出发格式。这种 printf 函数被称为可变格式输出函数
char *format;
format = "a=%d, b=%f\n";
printf(format, a, b);

// 相当于:
printf("a=%d, b=%f\n", a, b);

五、指向函数的指针

1. 函数的指针

如果在程序中定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配段存储空间。这段内存空间有一个起始地址,也称为函数的入口地址。每次调用函数时都从该地址入口开始执行此段函数代码。函数名就是函数的指针,它代表函数的起始地址。调用函数时,从函数名得到函数的起始地址,并执行函数代码。

可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。例如:

int (*p)(int, int);

定义 p 是一个指向函数的指针变量,它可以指向函数类型为整型且有两个整型参数的函数。此时,指针变量 p 的类型用 int(*)(int, int) 表示。

2. 函数指针变量的定义与使用

调用一个函数,除了使用函数名调用外,还可以通过指向函数的指针变量调用。定义指向函数的指针变量的一般形式为:

类型名 (*指针变量名)(函数参数表列);

注意:

  1. 定义指针变量时指定的类型名和函数参数表列必须和指向的函数一致。
  2. 对指向函数的指针变量进行算术运算是无意义的。

将函数赋值给函数指针变量时,只需给出函数名而不必给出参数。用函数指针变量调用函数,只需将 (*指针变量名) 代替函数名即可。

用函数名调用函数,只能调用所指定的一个函数,而使用指针变量调用函数比较灵活,可以根据不同情况先后调用不同函数。

#include <stdio.h>

int main() {
    int max(int x, int y);
    int (*p)(int, int);
    p = max;
    int a, b, c;
    scanf("%d %d", &a, &b);
    c = (*p)(a, b);  // 等价于:c = max(a, b)
    printf("max = %d\n", c);
    return 0;
}

int max(int x, int y) {
    if (x > y)
        return x;
    else
        return y;
}

3. 用指向函数的指针做函数参数

指向函数的指针变量的一个重要用途就是把函数的入口地址作为参数传递给其他函数,这样就能在被调用的函数中使用实参函数。

使用指向函数的指针变量做形参可以实现多个函数在不同情况下被主调函数使用,而主调函数无需做任何修改。这种方法符合结构化程序设计方法原则。

void fun(int (*x)(int), int (*y)(int, int)) {
    int a, b, i=3, j=5;
    a = (*x)(i);
    b = (*y)(i, j);
}

六、返回指针值的函数

一个函数可以返回一个整型值、字符值等,也可以返回指针型的数据,即地址。定义返回指针值的函数的一般形式为:

类型名* 函数名(参数表列);

如定义 int *a(int x, int y);,调用后可以得到一个 int * 型指针,即整型数据的地址。

七、指针数组和多重指针

1. 指针数组

元素均为指针类型数据的数组称为指针数组。其定义方法为:

类型名 *(数组名)[数组长度];

指针数组比较和是用来指向若干个字符串,时字符串的处理更加方便灵活。下面的程序实现了若干个字符串按字母顺序由小到大输出:

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

int main() {
    void sort(char* name[], int n);
    void print(char* name[], int n);
    char* name[] = {"Follew me", "C-language", "408"};
    int n = 3;
    sort(name, n);
    print(name, n);
    return 0;
}

void sort(char* name[], int n) {
    char* temp;
    int i, j, k;
    for (i = 0; i < n - 1; i++) {
        k = i;
        for (j = i + 1; j < n; j++) {
            if (strcmp(name[k], name[j]) > 0)
                k = j;
        }
        if (k != i) {
            temp = name[i];
            name[i] = name[k];
            name[k] = temp;
        }
    }
}

void print(char* name[], int n) {
    int i;
    for (i = 0; i < n; i++)
        printf("%s\n", name[i]);
}

// 408
// C-language
// Follew me

2. 指向指针数据的指针变量

指向指针数据的指针变量简称为指向指针的指针。其定义方法为:

类型名** 变量名;

* 运算符的结合顺序时从右到左,因此 **p 相当于 *(*p)

#include <stdio.h>

int main() {
    char* name[] = {"Follew me", "C-language", "408"};
    char** p;
    int i;
    for (i = 0; i < 3; i++) {
        p = name + i;
        printf("%s\n", *p);
    }
    return 0;
}

// Follew me
// C-language
// 408

3. 指针数组做 main 函数的形参

指针数组的一个重要应用是作为 main 函数的形参。以往的程序中,main 函数的第一行一般写为 int main()int main(void),表示 main 函数没有参数。实际上,某些情况下,main 函数可以有参数,即:

int main(int argc, char* argv[])

其中,agrcargvmain 函数的形参,它们是程序的命令行参数。argc(argument count 缩写)意为参数个数,argv(argument vector 缩写)意为参数向量,是一个 * char 指针数组,数组中每一个元素指向命令行中的一个字符串的首字母。

注意:如果用带参数的 main 函数,第一个形参必须是 int 型,用来接收形参个数,第二个形参必须是字符指针数组,用来接收操作系统命令行传来的字符串中首字符的地址。

通常 main 函数和其他函数组成一个文件模块,对该文件进行编译和连接得到可执行文件 .exe,执行该文件操作系统就能调用 main 函数,然后由 main 函数调用其他函数,从而完成程序的功能。main 函数是由操作系统调用的,因此其实参也只能由操作系统给出。在操作命令状态下,实参是和执行文件的命令一起给出的。例如,在 DOS、UNIX 或 Linux 等系统的操作命令状态下,命令行中包括了命令名和需要传给 main 函数的参数。命令行的一般形式为:

命令名 参数1 参数2···参数n

假设可执行文件为 file.exe,现在要将两个字符串 abcxyz 作为传送给 main 函数的参数,命令行为:

file abc zyx

需要注意的是,文件名也作为一个参数,即上面的例子中 argc 的值为 3,argv[0] 指向字符串 file 的首字符。

八、动态内存分配与指向它的指针变量

1. 动态内存分配

07 - 函数中介绍过全局变量和局部变量,全局变量分配在内存中的静态存储区,非静态的局部变量(包括形参)分配在内存中的动态存储区,这个存储区是一个称为(stack)的区域。

除此之外,C 语言还允许和建立内存动态分配区域,以存放一些临时用的数据,这些数据不必再程序的声明部分定义,也不必等到函数结束时才释放,而是需要时开辟,不需要时随时释放。这些数据临时存放在一个特殊的自由存储区,称为(heap)区。可以根据需要,像系统申请所需大小的空间。由于未在声明部分定义它们为变量或数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。

2. 建立内存的动态分配

对内存的动态分配通过系统提供的库函数来实现,主要有 mallocallocfreerealloc 这 4 个函数。以上 4 个函数的声明在 stdib.h 头文件中,在用到这些函数时应当用 #include <stdlib.h> 指令把 stdlib.h 头文件包含到程序文件中。

mallo 函数开辟动态存储区

其函数原型为:

void* malloc(unsigned int size);

其作用是在内存的动态存储区中分配一个长度为 size 的连续空间。形参 size 的类型定为无符号整型(不允许为负数)。此函数是一个指针型函数,返回的指针指向该分配域的第一个字节。如:

malloc(100);  // 开辟 100 字节的临时分配域,函数值为其第 1 个字节的地址

注意指针的基类型为 void,即不指向任何类型的数据,只提供一个纯地址。如果此函数未能成功地执行(例如内存空间不足),则返回空指针 NULL

calloc 函数开辟动态存储区

其函数原型为:

void* calloc(unsigned n, unsigned size);

其作用是在内存的动态存储区中分配 n 个长度为 size 的连续空间,这个空间一般比较大,足以保存一个数组。用 calloc 函数可以为一维数组开辟动态存储空间,n 为数组元素个数,每个元素长度为 size。这就是动态数组。函数返回指向所分配域的第一个字节的指针;如果分配不成功,返回 NULL。如:

p = calloc(50,4);  // 开辟 50×4 个字节的临时分配域,把首地址赋给指针变量 p

realloc 函数重新分配动态存储区

其函数原型为:

void* realloc(void* p, unsigned int size);

如果已经通过 malloc 函数或 calloc 函数获得了动态空间,想改变其大小,可以用 recalloc 函数重新分配。用 realloc 函数将 p 所指向的动态空间的大小改变为 size。p 的值不变。如果重分配不成功,返回 NULL。如:

realloc(p,50); // 将 p 所指向的已分配的动态空间改为 50 字节

free 函数释放动态存储区

其函数原型为:

void free(void* p);

其作用是释放指针变量 p 所指向的动态空间,使这部分空间能重新被其他变量使用。p 应是最近一次调用 callocmalloc 函数时得到的函数返回值。如:

free(p);  // 释放指针变量 p 所指向的已分配的动态空间

free 函数无返回值。

3. void 指针类型

C 99 允许使用基类型为 void 的指针类型,即 void* 型变量,它不指向任何类型的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。例如:

注意:不要把“指向 void 类型”理解为能指向“任何的类型”的数据,而应理解为“指向空类型”或“不指向确定的类型”的数据。

由于地址必须包含基类型信息,否则无法实现对数据的存取,因此 void* 型指针所标志的存储单元中是不能储存任何数据的,一般情况下只在调用动态储存分配函数时会使用。


Reference:

谭浩强《C程序设计(第五版)》


GVenusLeo
7 声望0 粉丝

开拓之人 - 应许之地