Skip to content

指针

在 C 语言中,指针是一种特殊的变量类型,它存储了一个内存地址,即某个变量在内存中的位置。可以通过指针访问该变量,并对其进行操作。

定义和声明

定义指针需要使用 * 运算符,如

c
int *p; //定义一个整型指针 p
char *q; //定义一个字符型指针 q

声明指针则需要使用“&”运算符取得变量的地址:

c
int a = 10;
int *p = &a; //将 a 的地址赋值给 p

解引用

使用 * 运算符解引用一个指针,可以访问该指针所指向的值。

c
#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;
    printf("%d", *p); // 输出10
}

指针操作示例

c
#include <stdio.h>

int main() {
    int a = 5;
    int b = 20;
    int *p;  // 定义指向整型变量的指针
    p = &a;  // 将指针指向变量 a 的地址
    printf("The value of a is %p.\n", p);  // 打印指针指向的内存地址
    printf("The value of a is %d.\n", *p); // 通过指针访问变量 a 的值
    *p = 10;
    printf("The value of a is %d.\n", *p); // 通过指针改变 a 的值,并访问
    printf("The value of a is %d.\n", a);  // 访问变量 a 的值,a 的值随着指针的操作也发生了变化

    p = &b;
    printf("The value of a is %d.\n", *p); // 通过该表指针的地址,新的地址中存储的值变成了 b 的值
    printf("The value of a is %d.\n", a);  // 访问变量 a 的值,发现 a 的值没有变化, 因为指针已经解绑了指向 a 的地址
}

指针与 const

const 关键字可以用于指针的不同位置,从而影响指针的使用方式。

指针常量

指针常量表示指针本身是个常量,一旦被赋值,其指向的地址不可修改,但是可以修改地址中存储的值。便于记忆,我们观察到常量指针符号在前面,关键字在后面,因此按顺序读叫做指针常量。

c
数据类型 * const 指针变量名;

TIP

  • 指针是个常量;
  • 指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
  • 指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化。
  • 使用前要初始化
c
#include <stdio.h>

int main(){
    int a = 10, b= 5;
    int * const p = &a;
    p = &b; // error: assignment of read-only variable 'p'
    *p = 20;
}

常量指针

常量指针表示指针指向的数据是被 const 修饰的变量。 便于记忆,我们观察到常量关键字在前面,指针符号在后面,因此按顺序读叫做常量指针。

c
const 指针类型 * 指针变量名;
指针类型 const * 指针变量名;

TIP

  • 常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
  • 常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
  • 指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址。
c
#include <stdio.h>

int main(){
    const int * p;
    int a = 10;
    p = &a;
    *p = 20; // Read-only variable is not assignable

    int const * q;
    q = &a;
    *q = 20; // Read-only variable is not assignable
    
    int b = 5;
    // 指针可以再次指向别的地址
    p = &b;
    q = &b;
}

指向常量的常指针

c
const 数据类型 * const 指针变量;
数据类型 const * const 指针变量;

常量指针不允许修改指针地址中存储的常量; 指针常量不允许将指针本身指向别处; 而指向常量的常指针则代表指针本身和常量都不允许被修改。

c
int a = 10, b= 5;
const int * const p = &a; // p 是一个指向常量整数的常量指针,即指针和指向的对象都是常量
*p = 20;                  // 错误:不能通过 p 修改 a 的值,因为指针指向的是常量
p = &b;                   // 错误:指针是常量,无法改变指向的地址

多级指针

多级指针是指指向一个或多个指针的指针,通常使用多个星号来表示不同级别的指针。例如,以下代码定义了一个指向指针的指针:

c
int **ppi;

这里使用两个星号,表示 ppi 是一个指向指针的指针,可以用于访问指针指向的变量或指针指向的指针指向的变量。

c
#include <stdio.h>

int main()
{
    int num = 10;
    int *p1 = &num;
    int **p2 = &p1;
    int ***p3 = &p2;

    printf("num = %d\n", num);     
    printf("*p1 = %d\n", *p1);     // num 地址对应的值
    printf("**p2 = %d\n", **p2);   // 指针 *p1 对应的地址的值
    printf("***p3 = %d\n", ***p3); // 指针 **p2 对应的指针的地址的值

    return 0;
}

这里定义了一个指向整型数据的多级指针 p3,它依次指向指向指针的指针 p2,指向指针 p1,最终指向变量 num。通过 * 操作符可以访问各级指针指向的数据。

指针与数组

当一个数组被声明时,它会在内存中占据一段连续的空间。因此,可以使用数组名来获取第一个元素的地址,并通过加上偏移量来获取后面元素的地址。

c
#include <stdio.h>

int main() {
    int arr[5] = {1, 3, 5, 7, 9};
    int *p = &arr[0]; // p 指向 arr[0], 也可以写作 int *p = arr; 因为 arr 代表的就是数组的首地址
    printf("%d\n", *(p+2)); // 输出 5
}

上面是一维数组,二维数组其实也是类似的,需要注意的是内存没有二位的概念,对于二位数组的存储就是按照顺序存储的

c
#include <stdio.h>

int main() {
    int arr[2][3] = {{1,3,5},{5,7,9}};
    int * p = arr[1];
    printf("%d", p[2]);
}

对于上述示例,内存存储的格式如下

1,3,5,5,7,9

而指针 p 指向的是二维数组的第二组数的首地址即 arr[1][0], 因此 p[2] 的值就是 7。

指针数组

指针数组,本质上是一个数组,不过这个数组是用于存放指针的数组。

c
#include <stdio.h>

int main()
{
    int num1 = 10, num2 = 20, num3 = 30;
    int *ptr[3] = { &num1, &num2, &num3 };
    
    printf("%d\n", *ptr[0]);   // 输出 10
    printf("%d\n", *ptr[1]);   // 输出 20
    printf("%d\n", *ptr[2]);   // 输出 30
    
    return 0;
}

这里定义了一个包含三个整型指针的指针数组 ptr,并将其初始化为指向 num1、num2 和 num3。通过索引和 * 操作符,我们可以访问各个指针指向的数据。

数组指针

数组指针,本质是一个指针,不过这个指针比较特殊,它是一个指向数组的指针。

c
int arr[3] = {10, 20, 30};
int (*ptr)[3] = &arr; // 直接对整个数组再取一次地址

这里定义了一个包含三个整型元素的数组 arr,并创建了一个指向该数组的指针 ptr。通过 () 操作符将 *ptr 解释为一个包含三个整型元素的数组,并使用 & 操作符获取 arr 的地址。

那么现在已经取到了指向整个数组的指针,如何使用呢?

c
#include <stdio.h>

int main() {
    int arr[3] = {10, 20, 30};
    int (*ptr)[3] = &arr;
    printf("%d, %d, %d", *(*ptr+0), *(*ptr+1), *(*ptr+2));
    return 0;
}

我们来梳理下

  • ptr 代表整个数组的地址
  • *ptr 代表指向数组中首元素的地址
  • *ptr + i 代表指向数组中第 i 个元素的地址
  • *(*ptr + i) 代表指向数组中的第 i 个元素的地址里面的值

那么二维数组如何使用数组指针来操作呢?

c
#include <stdio.h>

int main() {
    int arr[2][3] = {{1,3,5},{5,7,9}};
    int (*p)[3] = arr; // 此时维度提升,数组指针指向的就是二维数组中的一个元素,而这个元素本身就是一个数组
}

取第一个数组中的第 2 个元素

c
printf("%d", *(*p + 1));

取第二个数组中的第 2 个元素, 首先 *(p+1) 为一个整体,表示第二个数组(因为是数组指针,所以这里 +1 一次性跳一个数组的长度),然后再到外层 +1 表示数组中的第二个元素,最后再取地址,就是第二个数组的第二个元素了

c
printf("%d", *(*(p + 1) + 1)); // 

printf("%d", p[1][1]);

指针传参

指针也可以作为参数传递给函数,从而让函数能够直接修改指针所指向的值。

c
#include <stdio.h>

int main() {
    void swap(int *a, int *b) {
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
    
    int main() {
        int x = 1, y = 2;
        swap(&x, &y);
        printf("%d %d", x, y); // 输出2 1
    }
}

指针函数

指针函数是指返回指针的函数,本质是一个函数,返回值是指针类型。

例如,我们定义一个指向字符串类型的指针函数

c
char * get_string() {
    char str[] = "Hello, World!";
    return &str; // 返回局部变量的地址,外部拿到的是个 null,函数一旦返回,那么其中的局部变量就会全部销毁了
}

上述的代码虽然返回了一个指针,但是 C 语言中不支持函数玩不返回局部变量的指针,以此还需要将字符串存储在静态内存中或者动态分配的内存中。

c
#include <stdio.h>

char *get_string() {
    static char str[] = "Hello, World!";
    return str;
    
    /**
    char *str = "Hello, World!";
    return str;
    */
}

int main() {
    char *str_ptr = get_string();
    printf("String: %s\n", str_ptr);   // 输出 "String: Hello, World!"
    return 0;
}

函数指针

函数指针是指向函数的指针,本质是一个指针。通过函数指针,我们可以实现不同函数之间的切换和调用。要定义一个函数指针,需要使用函数类型作为指针类型的基础。

c
int (*func_ptr)(int, int);

上述代码定义了一个指针,指向一个入参为 2 个整形,返回值也为整形的函数指针 func_ptr, 我们就可以使用该函数指针来调用相应的函数

c
func_ptr(a, b);

详细请看下面的代码示例

c
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*p)(int, int) = add;

    int result = (*p)(1, 2);   // (*p)表示 add 函数
    printf("%d\n", result);
    int result2 = p(1, 2);     // 有点类似给函数定义了一个别名
    printf("%d\n", result2);
}

总结

在 C 语言中,指针是一个非常重要的概念。指针是一个变量,其值为另一个变量的地址。使用指针可以访问和修改原变量的值,而指针的算术运算可以对数组进行操作。在使用指针时,需要注意指针的类型和指向的变量或数组的类型。掌握指针的使用方法和注意事项将有助于我们编写高效、稳定的 C 语言程序。

Released under the MIT License.