由浅入深剖析 C语言 指针

195 阅读12分钟

由浅入深剖析 C语言 指针

指针的声明

类型 *变量名 = 实体地址, 例子:int *p = &number; 表示 变量名 p 指向 实体number 的地址

int main(){
    int *p; // 声明变量时 *号表示 声明的变量为指针类型
    int number = 10;
    p = &number; // &号表示 取实体变量的地址(还包含了number 占用存储空间的大小 信息); 变量名 p 指向 实体number 的地址
    return 0;
}

图片.png

指针的两大重要要素

首地址、占用存储空间的大小。

任何变量,记录存储,都必须 有的两个重要信息:首地址、占用存储空间的大小

void testAddress(){
    // TODO 一:内存地址
    /**
     * CPU:算数逻辑单元(对数据进行运算),控制单元(协调硬件活动电路),寄存器组(用于临时存储数据)
     * 寄存器组(用于临时存储数据,有限的,如果鱼的记忆只有七秒,那他还不到七秒)
     * 所以需要出现 运行内存(只要真正CPU需要用的时候,运算的时候,才会从 运存 到 寄存器组,平时大部分情况下 都在运存中)
     *                   等CPU运算完成后,再把结果 放入到 运存中(寄存器组 到 运存)
     * 结论:运存才是数据中心大本营,寄存器组 仅仅是 CPU在运算时 临时存储区域而已哦
     */
    /**
     * 运存:
     * [][][][][][][][] 8个为一组 = 1字节,以这种形式保存到 运存中的
     * 每一个 1字节 都会有一个编号,一个房间,而此编号,就是 【内存地址】
     */

    printf("数据类型 char,占用字节大小:%d\n", sizeof(char)); // 1字节
    printf("数据类型 short,占用字节大小:%d\n", sizeof(short)); // 2字节
    printf("数据类型 int,占用字节大小:%d\n", sizeof(int)); // 4字节

    // 运行在 arm64手机设备上是8字节,  若是运行在32位Windows上是4字节,64位Windows上是8字节
    printf("数据类型 long,占用字节大小:%d\n", sizeof(long));
    printf("数据类型 long long,占用字节大小:%d\n", sizeof(long long)); // 8字节
    printf("数据类型 float,占用字节大小:%d\n", sizeof(float)); // 4字节
    printf("数据类型 double,占用字节大小:%d\n", sizeof(double)); // 8字节

    // 以上打印,我们发现,只有char是一个房间,其他的 都有多个房间,那么多个房间如何管理?
    // 答:无论他几个字节,只需第一个字节的首地址 即可

    // 以上任何变量,记录存储,都必须 两个重要信息, 以int为例也一样?
    // 答:①.此变量的首地址,用第一个字节记录,   ②.字节1 字节2 字节3 字节4 代表 占用存储空间的大小 为 4个字节
    int arr[2] = {1,2};
    int *pInt = arr;
    printf("指针pInt首地址:%u, 下一个地址:%u, 步长:%d\n", pInt, pInt+1, sizeof(int));
}

& 取 数据对象 地址的时候 会取得两个重要信息:首地址、占用存储空间的大小

void testPointerDataType(){
    // TODO 二:指针数据类型
    /**
     * &数据对象  &i
     *  & 获取两个重要级信息:
     *      信息一:获取数据对象的首地址。
     *      信息二:获取数据对象所需的存储空间大小
     */

    // 一个变量,两个要素,
    // 要素一:num1变量的第一个字节 房间1 记录了内存地址 首地址 2000H
    // 要素二:num1变量占用四个字节空间存储大小。
    int num1 = 100;
    printf("首地址:%u, 占用存储空间的大小:%lu", &num1, sizeof(int));
}

指针类型

void testPointerTypeSpace(int *pn, char *pc); //先声明(方便后面的函数调用),后实现

void testPointerType(){
    // TODO 三:指针类型

    int n1 = 100;
    int * pN1 = &n1; // pN1 是指针变量(保存 n1 的 首地址 与 所需空间大小,两个重要级 信息)
    // 所以会诞生这样的大白话:pN1指针指向n1,其实就是因为pN1指针存储的值 == n1自己的内存地址

    char sex = 'M';
    char * pSex = &sex; // pSex 是指针变量(保存 sex 的 首地址 与 所需空间大小,两个重要级 信息)
    // 所以会诞生这样的大白话:pSex指针指向sex,其实就是因为pSex指针存储的值 == sex自己的内存地址

    // 类型* 指针变量 = 内存地址;   类型 * 指针变量 = 内存地址;   类型 *指针变量 = 内存地址; 三种方式都是可以的,没有说那种方式就是错误的

    int i1 = 100, i2 = 200, i3 = 300, i4 = 400;
    int * pi1 = &i1;
    int * pi2 = &i2;
    int * pi3 = &i3;
    int * pi4 = &i4;

    printf("pi1存放的内存地址是:%u\n", pi1); // 3756679752 3756679751 3756679750 375667949
    printf("pi2存放的内存地址是:%u\n", pi2); // 3756679748
    printf("pi3存放的内存地址是:%u\n", pi3); // 3756679744
    printf("pi4存放的内存地址是:%u\n", pi4); // 3756679740
    // 为何相差4个字节? 答:因为是从 i1到i4的首地址, 一个i1就有四个字节 第一个字节记录首地址 以此类推
    // 所以再次定论一句话:“指针类型的值 是 目标数据对象 的 首地址”

    // 经过前面的分析,我们已得知(指针存放 目标数据对象的首地址)但是 数据对象 空间大小 存储到哪里?
    // 答:请看下面的代码

    int n = 100;
    int *pn = &n;

    char c = 'A';
    char *pc = &c;

    pn + 1; // 指针运算,为什么可以, 有【空间存储大小】,内部可以计算步长

    // pn = pc;
    // 报错:”无法将 char* 转换为 int*“
    // 大家会思考,不都是属于整形领域么,为什么不可能呢?
    // 答:其实 pn = pc 首地址可以赋值的,首地址赋值是没有任何问题的
    //     报错的原因【指针类型改变,会导致数据长度改变,因此无法正确赋值】
    //     也就是int* 存放 目标数据对象的 存储空间大小4字节,无法修改成1字节

    void * v1 = pc; // 如果你敢使用 void * ,主动丢失【空间存储大小】 所以可以, 直接安安心心的保存 首地址
    void * v2 = pn;

    // v2 + 1; // 就是因为你丢失 【空间存储大小】 没有任何记录了,所以无法计算

    // 【指针最专业 两句话】
    // 第一句话:指针类型 是 通过值 来 保存 目标数据对象 的 首地址        int *p = 值
    // 第二句话:指针类型 是 通过类型本身 来 标记 目标数据对象 的 空间大小 int *p = 值==sizeof(目标数据对象==int)
    // 所以 pn = pc 行不通,就在于 第二句话的严格要求

    testPointerTypeSpace(pn, pc);
}

void testPointerTypeSpace(int *pn, char *pc){ // 实现
    // TODO 五:指针类型占用空间大小
    // 我们知道 char类型占用1字节,int类型占用4字节,代表了 他们的 目标数据对象 占用空间大小 是不同的
    printf("sizeof(int * pn) = %d\n", sizeof pn);
    printf("sizeof(int * pc) = %d\n", sizeof pc);
    // 注意:在Windows的32位环境中,打印的都是4, 在安卓设备 CPU架构指令 arm64位中打印都是8
    // 那么为什么是 4 或 8 呢?
    // 答:因为 int* 或者 char* 等等,他们的目的,都是存放 目标数据对象 的 首地址/空间大小,而自己本身 只是代表指针类型而已
    // 所以此指针类型,的存储访问 用 4个字节 或 8个字节 足矣
}

指针的使用

void testUsePointer(){
    // TODO 四:使用指针
    // *pj (取pj存放的内存地址 的 值)
    // *指针 (根据 指针中存储的(首地址 与 空间大小)找到 目标数据对象, 注意:目标数据对象 就是存放了值 666 888 这种)

    int j = 666; // 自己的内存地址 == 1000H
    int *pj = &j;

    printf("j自己的内存地址:%u\n", &j); // 1000H
    printf("*pj存放的值:%u\n", pj); // 1000H    pj == pj存放的值 等价于 pj存放的 内存地址
    printf("*pj自己的内存地址:%u\n", &pj); // 2000H
    printf("*pj存放的值(j的内存地址)的值:%d\n", *pj); // 根据*pj存放的1000H内存地址 找到 值本身 == 666

    *pj = 888; // 通过 *pj存放的1000H内存地址 找到 666 修改为 888

    printf("*pj存放的值(j的内存地址)的值:%d", *pj); // 根据*pj存放的1000H内存地址 找到 值本身 == 888
}

不同指针类型的转换对应关系

void testt(char * pChar);

void testChangePointerType(){
    // TODO 六:不同指针类型的转换对应关系
    int i = 999999;
    int * pInt = &i;

    // 前面说 指针中存储(首地址 与 空间大小) 此时*pChar 能够存储值==首地址, 却不能存储“空间大小”(埋下伏笔:此时空间大小 已丢失)
    char * pChar = (char *) pInt;

    printf("*pInt存储的值:%u\n", pInt); // 值 等于 i变量自己的内存地址
    printf("*pChar存储的值:%u\n", pChar); // 值 等于 i变量自己的内存地址

    // *pInt存储 内存地址 的值 == 999999【因为取值时,是根据 首地址 取 四个字节的数据 从而得到完整int值】目标空间大小4字节 未丢失
    printf("*pInt存储的值的值:%d\n", *pInt);

    // *pChar存储 内存地址 的值 == 63【因为取值时,是根据 首地址 取 一个字节的数据 从而无法得到完整int值】目标空间大小4字节 已丢失
    printf("*pInt存储的值的值:%u\n", *pChar); // 数据精度丢失

    testt(pChar);
}

void testt(char * pChar){
    // TODO 七:《对前面内容的总结》:
    // 内存地址:在C语言中,万物皆地址,任何一个变量 都一定有自己的 内存地址
    // 指针类型:指针类型 保存 首地址 与 空间大小
    // 指针要素:指针存储的值==内存地址,指针自己也有内存地址(因为任何变量都有 自己的内存地址)【这个是指针 最绕的环节,很多开发者 就因为这个不理解 而晕菜】

    int * pInt2 = (int *) pChar; // 是不是可以这样想?pChar记录首地址,有了首地址,啥事都可以做,可以通过首地址 还原之前的空间大小
    printf("*pInt2存储的值的值:%d\n", *pInt2);

    // 既然 指针存放的是地址,为啥要用二级指针存放一级指针的地址,不用一级指针存放另一个一级指针的地址
    // 答:语法不支持

    // 规律 规则:
    int number1 = 100;
    int * pnumb1 = &number1;
    int ** pnumb2 = &pnumb1; // &取出pnumb1自己的内存地址 的 值 == number1的内存地址
    int *** pnumb3 = &pnumb2;
}

函数指针

*定义函数指针:返回值(名称)(参数类型1,参数类型2...)

无论 函数指针 还是 普通指针 本质都是 指针

int add(int num1, int num2){
    printf("num1 + num2 = %d\n", num1+num2);
    return num1+num2;
}

int mins(int num1, int num2){
    printf("num1 - num2 = %d\n", num1-num2);
    return num1-num2;
}

/**
 * 定义函数指针:返回值(*名称)(参数类型1,参数类型2...)
 * @param method 函数指针别名,
 *  int 表示函数的返回值类型
 *  (int,int) 表示函数的形参 的 参数类型
 * @param num1
 * @param num2
 * @return
 */
int methodPointer(int (*method)(int,int), int num1, int num2){
    printf("method address = %p, sizeof = %d, sizeof int = %d\n", method, sizeof(method), sizeof(int));
    return method(num1, num2);
}

/**
 * 数组和函数变量都是表示地址值,array == &array,method == &method
 */
void useMethodPointer(){
    // 方法一:
    int ret = methodPointer(add, 10, 10);
    printf("method add ret = %d\n", ret);
    ret = methodPointer(&mins, 100, 10);
    printf("method mins ret = %d\n", ret);

    //方法二:
//    int (*metName)(int, int) = add;//直接把add函数地址赋值给 *metName指针;(相当于 int i = 10)
    int (*method)(int,int);
    method = &add;
    methodPointer(method, 60, 60);
}

指针数组 与 数组指针

前提:符号优先级:() > [] > *

函数声明的**()** 与 数组声明的**[]** 优先级相同

如果优先级相同:从左往右 依次读取 作为优先级顺序

int * p_arr[10]; // 指针数组,表示数组p_arr 能存放 10个指针
int (* arr_p) [10]; // 数组指针,表示指针arr_p 指向的是 int[10] 的实体数组

无论 数组指针 还是 普通指针 本质都是 指针,以下为 指针数组 与 数组指针 实战代码:

// 指针数组 与 数组指针
void test_pointer_arr(){
    // 元素
    int arr2[5][10] = {
            {1,2,3,4,5,6,7,8,9},
            {11,22,33,44,55,66,77,88,99},
            {111,222,333,444,555,666,777,888,999},
            {1111,2222,3333,4444,5555,6666,7777,8888,9999},
            {11111,22222,33333,44444,55555,66666,77777,88888,99999}
    };
    printf("%d\n", *( *(arr2+2)+5 ) );

    int * pIndex2 = (int*)(arr2 + 2);
    printf("%d\n", *pIndex2);

    // 符号优先级:() > [] > *
    // TODO 指针 数组: 指针为类型,数组为实体
    int * pointArr[5];
    for (int i = 0; i < 5; ++i) {
        *(pointArr+i) = arr2[i];
    }
    for (int i = 0; i < 5; ++i) {
        printf("%d, %d, %d\n", (*(pointArr+i))[0],*( *(pointArr+i) + 1 ),(*(pointArr+i))[2]);
    }
    printf("\n");


    int arr[10] = {0,1,2,3,4,5,6,7,8,9};
    //  TODO 数组 指针: 数组为类型,   指针为实体
    //             类型:int [10]     实体:*pa(pa指针)
    int (*pa) [10]; // *pi一定要加括号否则会被认为 指针数组
    pa = &arr; // 数组 指针pa 指向一维数组 的地址
    printf("%d,%d,%d\n", *(*pa), *( *pa + 1 ), *(*pa+2) ); // *pa: 取得arr的地址; *(*pa): 取 arr元素值

    int (*pda) [10] = arr2;
    printf("pda元素的地址:%u,%u,%u,%u,%u\n", pda,pda+1,pda+2,pda+3,pda+4);
    printf("pda元素首个值:%d,%d,%d,%d,%d\n", *(*pda),*(*(pda+1)),*(*(pda+2)),*(*(pda+3)),*(*(pda+4)));

    // 无论 数组指针 还是 普通指针 本质都是 指针
    // 一维数组arr:10*sizeof(int)=10*4=40,指针pda=8(64位平台)或者 4(32位平台),二维数组arr2: 5*(10*sizeof(int))=5*(10*4)=200
    printf("sizeof(arr)=%d, sizeof(pda)=%d, sizeof(arr2)=%d\n", sizeof(arr), sizeof(pda), sizeof(arr2));
}

目标类型的推导

为什么要使用符号优先级呢?

答:C/C++编译器设计规范: 声明使用 统一化 的设计

前提:符号优先级:() > [] > *

函数声明的**()** 与 数组声明的**[]** 优先级相同

如果优先级相同:从左往右 依次读取 作为优先级顺序

按照符号优先级 () > [] > * 就能推导出所有指针的类型。看下图:

图片.png

void test(){
    // 元素类型  数组名  [元素个数][元素个数]...
       int      arr       [5]   [10];


    // 声明指针
    // 目标类型 *  指针名
        int   *  pInt;
        int*  *  ppInt; // 目标类型为 指针 的 指针


    // 按照符号优先级 () > [] > *    就能推导出所有指针的类型
    //C/C++编译器设计规范:    声明 与 使用 统一化 的设计
    int * (*id)[2]; // 声明  ,指针数组指针,指针 指向的数组 里面存放元素为指针
    int n = * (*id)[2]; // 实现


    int * pInt;    // 指针
    int** ppInt;   // 指针的指针
    int * arr[10];      // [] > *         指针数组
    int (*arrp) [10];   // () > [] > *    数组指针
    int * (*id)[2];     // () > [] > *    指针数组指针
    int * pfun(char *, double);  // []“函数的()相当与[]的优先级” > *       指针函数
    int (*funp)(char *, double); // () > [] > *                        函数指针
    int (*funparr[10])(char *, double); // () > [] > *                  函数指针数组
    int *(*funparr1[10])(char *, double); // 指针函数指针数组
    // 从以上可得结论:名字里越往后的优先级越高,例如:指针“*” 数组“[]” 指针“(*)”,
}