乐趣区

关于c:C程序设计-08-指针

一、什么是指针

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)*a i 行 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 程序设计(第五版)》

退出移动版