C语言 13 指针

指针可以说是整个 C 语言中最难以理解的部分了。

什么是指针

还记得在前面谈到的通过函数交换两个变量的值吗?

#include <stdio.h>

void swap(int, int);

int main() {
    int a = 10, b = 20;
    swap(a, b);
    printf("a = %d, b = %d", a, b);
}

void swap(int a, int b){
    // 这里对a和b的值进行交换
    int tmp = a;   
    a = b;
    b = tmp;
}

实际上这种写法是错误的,因为交换的并非是真正的 a 和 b,而是函数中的局部变量。

那么有没有办法能够直接对函数外部的变量进行操作呢?这就需要指针的帮助了。

程序中使用的变量实际上都是在内存中创建的,每个变量都会被保存在内存的某一个位置上(具体哪个位置由系统分配),所有的变量在对应的内存位置上都有一个地址(地址是独一无二的),可以通过这个地址寻找到这个变量本体,比如 int 占据 4 字节,因此 int 类型变量的地址就是这 4 个字节的起始地址,后面 32 个 bit 位全部都是用于存放此变量的值的。

这里的0x是十六进制的表示形式(10 - 15 用字母 A - F 表示)

如果能够知道变量的内存地址,那么无论身在何处,都可以通过地址找到这个变量了。

而指针的作用,就是专门用来保存这个内存地址的。

来看看如何创建一个指针变量用于保存变量的内存地址:

#include <stdio.h>

int main() {
    int a = 10;
    // 指针类型需要与变量的类型相同,且后面需要添加一个*符号(注意这里不是乘法运算),表示是对于类型的指针
    // 这里的&并不是进行按位与运算,而是取地址操作,也就是拿到变量a的地址
    int* p = &a;                         
    // 地址使用%p表示
    printf("a在内存中的地址为:%p", p);  
}
a在内存中的地址为:00000000005ffe84

可以看到,通过取地址操作&,将变量 a 的地址保存到了一个地址变量p中。

拿到指针之后,就可以很轻松地获取指针所指地址上的值:

#include <stdio.h>

int main() {
    int a = 666;
    int* p = &a;
    // 可以在指针变量前添加一个*号(间接运算符,也可以叫做解引用运算符)来获取对应地址存储的值
    printf("内存%p上存储的值为:%d", p, *p);  
}
内存00000000005ffe84上存储的值为:666

注意这里访问指针所指向地址的值时,是根据类型来获取的,比如 int 类型占据 4 个字节,那么就读取地址后面 4 个字节的内容作为一个 int 值,如果指针是 char 类型的,那么就只读取地址后面 1 个字节作为 char 类型的值。

同样的,也可以直接像这样去修改对应地址存放的值:

#include <stdio.h>

int main() {
    int a = 666;
    int* p = &a;
    // 通过*来访问对应地址的值,并通过赋值运算对其进行修改
    *p = 999;  
    printf("a的值为:%d", a);
}
a的值为:999

实际上拿到一个变量的地址之后,完全不需要再使用这个变量,而是可以通过它的指针来对其进行各种修改。

因此,现在想要实现对两个变量的值进行交换的函数就很简单了:

#include <stdio.h>

// 这里是两个指针类型的形参,其值为实参传入的地址,
// 虽然依然是值传递,但是这里传递的是地址
// 只要知道地址改变值就很容易了
void swap(int* a, int* b) {
    // 先暂存一下变量a地址上的值
    int tmp = *a;  
    // 将变量b地址上的值赋值给变量a地址上的值
    *a = *b;       
    // 最后将a的值赋值给b地址上的值,这样就成功交换两个变量的值了
    *b = tmp;      
}

int main() {
    int a = 10, b = 20;
    // 只需要把a和b的内存地址给过去就行了,这里取一下地址
    swap(&a, &b);  
    printf("a = %d, b = %d", a, b);
}
a = 20, b = 10

通过地址操作,就轻松实现了使用函数交换两个变量的值了。


了解了指针的相关操作之后,再来看看scanf函数,实际上就很好理解了:

#include <stdio.h>

int main(){
    int a;
    // 这里就是取地址,需要告诉scanf函数变量的地址,这样它才能通过指针访问变量的内存地址,对变量的值进行修改,这也是为什么scanf里面的变量(除数组外)前面都要进行一个取地址操作
    scanf("%d", &a);   
    printf("%d", a);
}

当然,和变量一样,要是不给指针变量赋初始值的话,就不知道指向哪里了,因为指针变量也是变量,存放的对应变量的地址值也在内存中保存,如果不给初始值,那么存放变量地址的这块内存可能在其他地方使用过,这样就不知道初始值是多少了(那么指向的地址可能是一个很危险的地址,随意使用可能导致会出现严重错误),所以一定要记得给个初始值或是将其设定为 NULL,表示空指针,不指向任何内容。

#include <stdio.h>

int main(){
    int* a = NULL;
}

接着来看看const类型的指针,这种指针比较特殊:

#include <stdio.h>

int main() {
    int a = 9, b = 10;
    const int* p = &a;
    // 报错,因为被const标记的指针,所指地址上的值不允许发生修改
    *p = 20;
    // 但是指针指向的地址是可以发生改变的
    p = &b;
}

再来看另一种情况:

#include <stdio.h>

int main() {
    int a = 9, b = 10;
    // const关键字被放在了类型后面
    int* const p = &a;
    // 允许修改所指地址上的值
    *p = 20;
    // 报错,不允许修改指针存储的地址值,其实就是反过来了
    p = &b;
}

当然也可以双管齐下:

#include <stdio.h>

int main(){
    int a = 9, b = 10;
    const int * const p = &a;
    *p = 20;   //两个都直接报错,都不让改了
    p = &b;
}

指针与数组

前面介绍了指针的基本使用,来回顾一个问题,为什么数组可以原身在函数之间进行传递呢?

先说结论,数组表示法实际上是在变相地使用指针,甚至可以理解为数组变量其实就是一个指针变量,它存放的就是数组中第一个元素的起始地址

为什么这么说?

#include <stdio.h>

int main() {
    char str[] = "Hello World!";
    // 为什么能直接把数组作为地址赋值给指针变量
    char* p = str;  
    // 还能正常使用,打印出第一个字符
    printf("%c", *p);  
}
H

还能这样玩:

int main() {
    char str[] = "Hello World!";
    char* p = str;
    // 还可以像在使用数组一样用指针
    printf("%c", p[1]);
}
e

怎么数组和指针还能这样混着用呢?先来看看数组在内存中是如何存放的:

数组在内存中是一块连续的空间,所以为什么声明数组一定要明确类型和大小,因为这一块连续的内存空间生成后就固定了。

而数组变量实际上存放的就是首元素的地址,而实际上之前一直使用的都是数组表示法来操作数组,这样可以很方便地对内存中的各个元素值进行操作:

int main(){
    char str[] = "Hello World!";
    // 直接在中括号中输入对应的下标就能访问对应位置上的数组了
    printf("%c", str[0]);   
}

而实际上str表示的就是数组的首地址,所以完全可以将其赋值给一个指针变量,因为指针变量也是存放的地址:

char str[] = "Hello World!";
// 直接把str代表的首元素地址给到p
char* p = str;   

而使用指针后,实际上可以使用另一种表示法来操作数组,这种表示法叫做指针表示法

#include <stdio.h>

int main() {
    char str[] = "Hello World!";
    char* p = str;
    // 通过指针也可以表示对应位置上的值
    printf("第一个元素值为:%c,第二个元素值为:%c", *p, *(p + 1));
}
第一个元素值为:H,第二个元素值为:e

比如现在需要表示数组中的第二个元素:

  • 数组表示法:str[1]
  • 指针表示法:*(p+1)

虽然写法不同,但是他们表示的意义是完全相同的,都代表了数组中的第二个元素,其中指针表示法使用了p+1的形式表示第二个元素,这里的+1操作并不是让地址+1,而是让地址+ 一倍的对应类型大小,也就是说地址后移一个char 的长度,所以正好指向了第二个元素,然后通过*取到对应的值(注意这种操作仅对数组是有意义的,如果是普通的变量,虽然也可以获得后一个 char 的长度的数据,但是毫无意义)

这两种表示法都可以对内存中存放的数组内容进行操作,只是写法不同罢了,所以数组和指针混用也就不奇怪了。

了解了这些东西之后,再来看看下面的各个表达式分别代表什么:

#include <stdio.h>

int main() {
    char str[] = "Hello World!";
    char* p = str;
    // 数组的第一个元素
    printf("*p的值:%c\n", *p);   
    // 数组的第一个元素的地址
    printf("p的值:%p\n", p);
    // 肯定是真,因为都是数组首元素地址
    printf("p == str的值:%d\n", p == str);   
    // 因为str就是首元素的地址,所以这里对地址加*就代表第一个元素,使用的是指针表示法
    printf("*str的值:%c\n", *str);    
    // 这里得到的实际上还是首元素的地址
    printf("&str[0]的值:%p\n", &str[0]);   
    // 代表第二个元素
    printf("*(p + 1)的值:%c\n", *(p + 1));   
    // 第二个元素的内存地址
    printf("p + 1的值:%p\n", p + 1);    
    // 注意*的优先级比+要高,所以这里代表的是首元素的值+1,得到字符'I'
    printf("*p + 1的值:%c\n", *p + 1);    
}
*p的值:H
p的值:00000000005ffe7b
p == str的值:1
*str的值:H
&str[0]的值:00000000005ffe7b
*(p + 1)的值:e
p + 1的值:00000000005ffe7c
*p + 1的值:I

所以不难理解,为什么printf函数的第一个参数是const char*了,实际上就是需要传入一个字符串而已,只不过这里采用的是指针表示法而已。

当然指针也可以进行自增和自减操作,比如:

#include <stdio.h>

int main() {
    char str[] = "Hello World!";
    char* p = str;
    // 自增后相当于指针指向了第二个元素的地址
    p++;
    // 所以这里打印的就是第二个元素的值了
    printf("%c", *p);
}
e

一维数组看完了,再来看看二维数组,那么二维数组在内存中是如何表示的呢?

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

这是一个2x3的二维数组,其中存放了两个能够容纳三个元素的数组,在内存中,是这样的:

所以也可以使用指针来进行访问:

#include <stdio.h>

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
    // 因为是二维数组,注意这里要指向第一个元素,需要降一个维度才能正确给到指针
    int* p = arr[0];  
    // 同理如果这里是arr[1]的话那么就表示指向二维数组中第二个数组的首元素
    // 实际上这两种访问形式都是一样的
    printf("%d = %d", *(p + 4), arr[1][1]);  
}
5 = 5

多级指针

实际上指针本身也是一个变量,它存放的是目标的地址,但是它本身作为一个变量,也要将地址信息保存到内存中,所以,实际上当有指针之后:

实际上,还可以继续创建一个指向指针变量地址的指针,甚至可以创建更多级(比如指向指针的指针的指针)

比如现在要创建一个指向指针的指针:

落实到代码中:

#include <stdio.h>

int main() {
    int a = 20;
    // 指向普通变量的指针
    int* p = &a;
    // 因为现在要指向一个int *类型的变量,所以类型为int* 再加一个*
    // 指向指针的指针(二级指针)
    int** pp = &p;
    // 指向指针的指针的指针(三级指针)
    int*** ppp = &pp;
    // 使用一次*表示二级指针指向的指针变量,继续使用一次*会继续解析成指针变量所指的普通变量
    printf("p = %p, a = %d", *pp, **pp);  
}
p = 00000000005ffe84, a = 20

本质其实就是一个套娃而已,只要把各个层次分清楚,实际上还是很好理解的。

特别提醒: 一级指针可以操作一维数组,那么二级指针是否可以操作二维数组呢?不能!因为二级指针的含义都不一样了,它是表示指针的指针,而不是表示某个元素的指针了。下面会认识数组指针,准确的说它才更贴近于二维数组的形式。

指针数组与数组指针

前面了解了指针的一些基本操作,包括它与数组的一些关系。接着来看指针数组和数组指针,这两词语看着就容易搞混,不过哪个词在后面就哪个,先来看指针数组,虽然名字很像数组指针,但是它本质上是一个数组,不过这个数组是用于存放指针的数组。

#include <stdio.h>

int main() {
    int a, b, c;
    // 可以看到,实际上本质还是数组,只不过存的都是地址
    int* arr[3] = {&a, &b, &c};
    // []运算符的优先级更高,所以这里先通过[0]取出地址,然后再使用*将值赋值到对应的地址上
    *arr[0] = 999;  
    printf("%d", a);
}

当然也可以用二级指针变量来得到指针数组的首元素地址:

#include <stdio.h>

int main(){
    int * p[3];   //因为数组内全是指针
    int ** pp = p;  //所以可以直接使用指向指针的指针来指向数组中的第一个指针元素
}

实际上指针数组还是很好理解的,那么数组指针呢?可以看到指针在后,说明本质是一个指针,不过这个指针比较特殊,它是一个指向数组的指针(注意它的目标是整个数组,和之前认识的指针不同,之前认识的指针是指向某种类型变量的指针)

数组指针表示指向整个数组:

// 注意这里需要将*p括起来,因为[]的优先级更高
int (*p)[3];   

注意它的目标是整个数组,而不是普通的指针那样指向的是数组的首个元素:

int arr[3] = {111, 222, 333};
// 直接对整个数组再取一次地址(因为数组指针代表的是整个数组的地址,虽然和普通指针一样都是指向首元素地址,但是意义不同)
int (*p)[3] = &arr;  

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

#include <stdio.h>

int main() {
    int arr[3] = {111, 222, 333};
    // 直接对整个数组再取一次地址
    int(*p)[3] = &arr;  
    // 要获取数组中的每个元素,稍微有点麻烦
    printf("%d, %d, %d", *(*p + 0), *(*p + 1), *(*p + 2));  
}
111, 222, 333

注意此时:

  • p代表整个数组的地址
  • *p表示所指向数组中首元素的地址
  • *p + i表示所指向数组中第i个(0 开始)元素的地址(实际上这里的 *p 就是指向首元素的指针)
  • *(*p + i)就是取对应地址上的值了

虽然在处理一维数组上感觉有点麻烦,但是它同样也可以处理二维数组:

#include <stdio.h>

int main() {
    int arr[][3] = {{111, 222, 333}, {444, 555, 666}};
    // 二维数组不需要再取地址了,因为现在维度提升,数组指针指向的是二维数组中的其中一个元素(因为元素本身就是一个数组)
    int (*p)[3] = arr;
    // 现在想要访问第一个数组的第二个元素
    // 因为上面直接指向的就是第一个数组,所以想要获取第一个数组的第二个元素和之前是一模一样的
    printf("%d\n", *(*p + 1));
    // 现在想要获取第二个数组中的最后一个元素
    // 首先*(p + 1)为一个整体,表示第二个数组(因为是数组指针,所以这里 +1 一次性跳一个数组的长度),然后再到外层 +2 表示数组中的第三个元素,最后再取地址,就是第二个数组的第三个元素了
    printf("%d\n", *(*(p + 1) + 2));
    // 当然也可以使用数组表示法
    // 这就是二维数组的用法,甚至可以认为这两个是同一个东西
    printf("%d\n", p[1][2]);
}
222
666
666

指针函数与函数指针

函数可以返回一个指针类型的结果,这种函数就称为指针函数

#include <stdio.h>

// 函数的返回值类型是int*指针类型的
int* test(int* a) {  
    return a;
}

int main() {
    int a = 10;
    // 使用指针去接受函数的返回值
    int* p = test(&a);  
    printf("%d\n", *p);
    // 当然也可以直接把间接运算符在函数调用前面表示直接对返回的地址取地址上的值
    printf("%d\n", *test(&a));  
}
10
10

不过要注意指针函数不要尝试去返回一个局部变量的地址:

#include <stdio.h>

int* test(int a) {
    int i = a;
    // 返回局部变量i的地址
    return &i;
}

int main() {
    // 连续调用两次test函数
    int* p = test(20);  
    test(30);
    // 这里会报错
    printf("%d", *p);
}

为什么会这样呢?因为函数一旦返回,那么其中的局部变量就会全部销毁了,至于这段内存之后又会被怎么去使用,就不得而知了。


接着来看函数指针,实际上指针除了指向一个变量之外,也可以指向一个函数,当然函数指针本身还是一个指针,所以依然是用变量表示,但是它代表的是一个函数的地址(编译时系统会为函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址)

来看看如何定义:

#include <stdio.h>

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

int main() {
    // 类型 (*指针变量名称)(函数参数...)  
    // 注意一定要把*和指针变量名称括起来,不然优先级不够
    int (*p)(int, int) = sum;
    printf("%p", p);
}
00007ff6524713b4

这样就拿到了函数的地址,既然拿到函数的地址,就可以通过函数的指针调用这个函数了:

#include <stdio.h>

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

int main() {
    int (*p)(int, int) = sum;
    // 就像正常使用函数那样,(*p)表示这个函数,后面依然是在小括号里面填上实参
    int result1 = (*p)(1, 2);
    printf("%d\n", result1);
    // 当然也可以直接写函数指针变量名称,效果一样
    int result2 = p(1, 2);
    printf("%d\n", result2);
}
3
3

有了函数指针,就可以编写函数回调了(所谓回调就让别人去调用提供的函数,而不是主动来调别人的函数)

比如现在定义了一个函数,不过这个函数需要参数通过一个处理的逻辑才能正常运行,所以就还要给他一个其他函数的地址:

#include <stdio.h>

// 将函数指针作为参数传入
int sum(int (*p)(int, int), int a, int b) {
    // 函数回调
    return p(a, b);
}

// 这个函数实现了a + b
int sumImpl(int a, int b) {  
    return a + b;
}

int main() {
    // 拿到实现那个函数的地址
    int (*p)(int, int) = sumImpl;  
    printf("%d", sum(p, 10, 20));
}
30
posted @ 2024-09-14 17:13  天航星  阅读(6)  评论(0编辑  收藏  举报