前言

C语言的灵魂在于指针,对于初学者来说,对这个灵魂了解的更大程度上体现在难度上,但是实际上也仅仅是由于不熟练,只要学完过后,看几个例子,跟着写几段代码,就会发现其实指针和数组,循环,分支语法之类的运用难度上差不了很多(虽然数组的本质就是指针)。所以,要对自己有信心!接下来正文开始。


一、指针是什么?

先给出结论: 指针==内存地址
我们经常听到“向内存申请空间”,“栈溢出”,“越界”等词,其实,这些词都是对C语言程序与内存空间之间的操作关系的描述。想要解释清楚这个概念,肯定是离不开内存空间的解释。下面给出一张图,让我们更形象的了解内存空间:
在这里插入图片描述

内存空间的最小操作单位是字节,我们常说的内存32G,64G,128G等等也说的是内存空间,将它们换算成字节,就是图中小格子的总数。每一个小格子都有自己的编号,只是有时候用十六进制表达,有时候用八进制表达,但本质都是一个独一无二的编号——地址,我们说指针指向哪里哪里,说的其实就是指向某个地址,所以说,指针==地址,是可以直接在内存空间上操作的灵魂!


二、指针变量是什么?

我们已经了解指针是什么,再根据C语言严谨的类型规则,很自然的会联想到指针用什么存放?,属于什么类型?接下来,让我们进一步了解存放指针的变量的类型:
还是先给结论:存放指针的也是变量——指针变量指针的类型取决于指针指向的数据的类型,例如:指向整型数据的指针就叫做整型指针······

其实很好理解,我们知道像abcd这样的字符——有char等类型,123——有int等类型,那么内存地址(指针)这个特殊的数据,它不被任何一种已有类型所容纳,所以给它专门制定了新名字——指针变量。
而关于指针类型就更好理解了,指针指向一个地址,这个地址可以存放很多种类型的数据,我们不能把杂七杂八的类型一概而论,因此我也也把指针分出类型,这个类型就是取决于这个指针所制指向的数据类型。

指针变量类型和指针类型的关系

我们常听到的整型指针变量(int*),字符指针变量(char*),实际上指的是该指针变量的操作权限
说人话:指针变量类型和指针类型就好比一辆自动化程度很高的车和一个人,指针变量类型决定这辆车一次可以开多远,指针类型就决定了这个人的承受能力,例如:整型指针管理包括首地址向后的4个字节,而整型指针变量就是启动一次开4字节的小车,如果小车和人的“运行里程”一致,那么这辆载人的车就可以很好的运转。
但是,一旦人和车的“运行里程”不一致了,就会出现一些意外的后果,就像人车“运行里程”不一样的话,总有一方要出事,具体问题这里不过多赘述,想了解的更多的话可以搜索专门讲解的博客


三、指针(变量)的分类

1.1 指针(变量)分类简介

指针的的种类很多,包括但不限于所有普通内置数据类型的指针,特殊的指针还有数组指针,函数指针,自定义类型指针(结构体,联合体,枚举),在文件操作里还会涉及到文件指针(这里预告一下,以后也会写关于C语言文件操作的博客),为防止文章太过臃肿,我们将用法趋同且相对更简单的普通内置数据类型(short,int ,long,char······等等)指针分为一组讲解。

2.1普通内置类型指针(变量)(非自定义)

这类指针是最基本的指针,用法简单,且可以说是一模一样。关于命名,一张图一目了然:

在这里插入图片描述
像其他long int等等的也是一样,总结就是:
指针的命名就是在类型后面+ ,即:类型名+*
就像前文所说,指针类型和指针变量类型犹如人和车,在非特别用法的情况下,统一很重要。它们的“运行里程”和名字里的内置类型一致:
在这里插入图片描述

2.2 void*指针(变量)

void类型,广泛用于泛型编程,它像一个变形金刚,可以接收任意类型“运行里程”的人(指针类型),再来通过强制转换适配这个人(指针类型)的“运行里程”。
例如:库函数qsort使用时,函数传参时就要用到void
类型的指针变量来接收实参,这里给出作者用冒泡排序模拟qsort实现时写的代码,代码如下:

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

void swap( char*p1,char* p2 ,size_t width) {
    //以字节为单位,一个一个字节交换
    for (int i = 0; i < width; i++) {
        char tmp = *(p1 + i);
        *(p1 + i) = *(p2 + i);
        *(p2 + i) = tmp;
    }


}

int compare_int(const void* p1,const void* p2) {

    return *((int*)p1) - *((int*)p2);
}
int compare_char(const void* p1, const void* p2) {

    return *((char*)p1) - *((char*)p2);
}

void bubble_qsort(const void* p,const size_t sz,const size_t width,int (*cmp)(void*,void*) ) {
    //外层表示循环层数
    for (int i = 0; i < sz - 1; i++) {
        //内层实现排序逻辑
        for (int j=0; j < sz - 1; j++) {
            //强转成char*指针,最小权限才能适应所有类型数据
            if (cmp((char*)p + j * width, (char*)p + (j + 1) * width) > 0) {
                //实现交换逻辑
                swap((char*)p + j * width, (char*)p + (j + 1) * width,width);

            }
        }

    }

}
int main() {

    //整型类型数据模拟排序
    int arr[] = { 8,6,9,5,3,4,2,1,7,0 };
    size_t sz = sizeof(arr) / sizeof(arr[0]);
   bubble_qsort(arr,sz,sizeof(int),compare_int);
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");


    //字符类型数据模拟排序

    //不算\0
    char arr1[] = "gdfebca";
    size_t sz1 = strlen(arr1);
    bubble_qsort(arr1, sz1, sizeof(char), compare_char);
    for (int i = 0; i < sz1; i++) {
        printf("%c ", arr1[i]);
    }

    printf("\n");

    //算上\0                                                         \0的ascll值是0,会排在最前面
    bubble_qsort(arr1, sz1 + 1, sizeof(char), compare_char);
    for (int i = 0; i < sz1 + 1; i++) {
        printf("%c ", arr1[i]);
    }
  
    return 0;
}

2.3函数指针(变量)

函数去掉函数名,参数名也可以省略,但参数类型必须保留,就是该函数的类型,还是以上文冒泡排序模拟qsort实现的代码为例,

//这个函数用于完成凉凉比大小的逻辑
int compare_int(const void* p1,const void* p2) {

    return *((int*)p1) - *((int*)p2);
}

这个函数类型就是: int ( void* , void* )
以这个函数为例,它的函数指针是:int (*name)( void* , void* )
                                   name--是名字,可以自己命名
对于函数指针类型,也是一样的,
将函数指针去掉函数名就是函数指针类型:int (*)( void* , void* )


2.3.1 函数指针进阶实践——转移表实现计算器代码

通过函数指针的解引用,我们可以直接访问到该函数,根据这个特点,再利用数组的下标,如果我们将函数指针放在一个数组里面(函数指针数组),我们就可以0~n的简单下标转换快速的调用函数,这种方法就叫做转移表
接下来给出一个题目:用转移表实现有加减乘除和退出功能的计算器。

先给出我的方案:

#include<stdio.h>

int add(int x, int y) {
    return x + y;
}
int sub(int x, int y) {
    return x - y;
}
int mul(int x, int y) {
    return x * y;
}
int div(int x, int y) {
    return x / y;
}

void menu() {
    printf("****** 1.加   2.减 ******\n");
    printf("****** 3.乘   4.除 ******\n");
    printf("******    0.exit    ******\n");
    printf("请输入: ");

}
//void calculator(int (*pf)(int, int)) {
//    printf("请输入两个操作数(x   y)\n ");
//    int x, y;
//    scanf("%d %d", &x, &y);
//    int ret = pf(x, y);                                              
//   printf("结果是:%d\n", ret);
//}
int main() {
    int taggle;
    do {
        menu();
        scanf("%d", &taggle);
        //这里就是函数指针数组,通过输入下标调用函数,即转移表
        int (*pf[5])(int, int) = { 0,add,sub,mul,div };
        //用于检查功能选择合法性              
        if (taggle >= 0 && taggle <= 4) {
            switch (taggle) {                                            
            case 0:
                printf("退出计算器\n");
                break;
            default:
                printf("请输入两个操作数(x 和 y) \n");
                int x, y;
                scanf("%d %d", &x, &y);
                int ret = pf[taggle](x, y);
                printf("结果是:%d\n", ret);
                break;

                /* case 1:
                     calculator(add);
                     break;
                 case 2:
                     calculator(sub);
                     break;
                 case 3:
                     calculator(mul);
                     break;
                 case 4:
                     calculator(div);
                     break;*/
            }
        }
        else
            printf("功能选择错误,请重新输入\n");

    } while (taggle);
    return 0;
}

以上代码给出了两种实现方案,转移表没注释;另一种do···while语句列举多种情况的方案是比较容易想到的,但是不如转移表简介。两种方法都可以实现计算器功能,但是转移表版本,要深入了解。

2.4 数组指针(变量)

像函数一样,数组也有类型,并且数组类型获取方式和数组指针的样式也是一模一样:数组去掉数组名,剩下的就是数组类型

例如:
数组(包括内容):      int arr1[3]={1,2,3}
数组(不看内容):      int arr1[3]
数组类型(去掉数组名): int [3]
数组指针:    int (*name)[3]   //这里name和函数指针的name一样,只表示指针的名字
数组指针类型(数组指针去掉名字):int (*)[3]

2.4.1数组指针实践运用:用数组指针模拟二维数组

同样,了解数组指针后,
我们给出一个小小的实践运用:用数组指针模拟二维数组

#include <stdio.h>

int main() {
    // 1. 定义原始二维数组(M行N列)
    const int ROW = 3;  // 行数
    const int COL = 4;  // 列数
    int arr[ROW][COL] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 2. 定义数组指针,指向二维数组的首行(必须指定列数COL,与二维数组匹配)
    int (*p)[COL] = arr;  // 等价于 p = &arr[0](指向第0行的数组指针)

    // 3. 三种等价访问方式(验证数组指针模拟效果)
    printf("=== 数组指针模拟二维数组 ===\n");
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            // 方式1:最直观,模拟二维数组语法 p[i][j]
            printf("p[%d][%d] = %d\t", i, j, p[i][j]);
            // 方式2:指针偏移 + 下标:*(p[i] + j)(p[i]是第i行的首地址)
            // printf("*(p[%d]+%d) = %d\t", i, j, *(p[i] + j));
            // 方式3:双重指针解引用:*(*(p+i) + j)(p+i指向第i行,解引用后为该行首地址)
            // printf("*(*(p+%d)+%d) = %d\t", i, j, *(*(p + i) + j));
        }
        printf("\n");
    }

    return 0;
}

不过值得注意的是,通过数组指针模拟的二维数组,其元素在内存中的储存并不一定连续,而原始的二维数组在内存中是连续储存的,所以这样的方法并不完全等价与二维数组。

2.5 文件指针(变量)

文件指针就不像其他指针会涉及到类型,它只有一个固定的写法:FILE*name
其中,FILE* 就是文件指针的类型,FILE 是标准库预定义的结构体,封装了文件操作所需的底层信息。

关于文件指针的内容这里不过多赘述,后续更新的关于C语言文件操作的文章将会详细讲解。


总结

首先,感谢大家能看到这里!
然后是对文章内容的总结:这篇博客基本总结了C语言指针这块的大体框架,并给出了两个指针知识的运用实践和相关代码,但是由于篇幅原因,实际上关于指针还有很多细节没有说明,如果大家有兴趣可以去b站上进行学习,也可以去C/C++官方参考手册了解。
这里给出网站:C/C++参考手册(点击即可跳转)

在这里插入图片描述

更多推荐