C语言指针笔记

C 语言指针笔记

目录

  1. 前言
  2. 基础部分
    1. 指针的作用
    2. 指针的声明
    3. 指针的使用
  3. 一级指针
    1. 指向变量的指针
    2. const指针
    3. 指向函数的指针
  4. 二级指针
    1. 指向指针的指针
    2. 指针与数组
    3. 话外: 为什么不能用二级指针直接指向二维数组

前言

🔼

指针 可以说是 C语言的灵魂,最巧妙的地方. 不明白,不理解指针 那就是等于没学C语言.
指针这玩意说难也不难,主要是细节问题.比如最常见的, 指针数组和数组指针、指针常数和常数指针、指针函数和函数指针. 刚学完指针还好,时间一久,听到之这些东西很难短时间内反应过来
指针这块 用的多的 就是字符串了, 其他的用的都比较少,所以久而久之总是忘,所以干脆写篇博客加深印象,也便于日后回顾
我才不承认是我自己,搞混了概念,重新学了一遍,想和人分享没人听才写的博客,绝对不是!

基础部分

🔼
写在前面: 指针是一种变量,地址是一种数据.
大概的介绍一下 什么是指针, 指针的作用, 以及指针的基本操作
PS: 以下假设环境均为 64位 1904 Win10 vscode gcc8.1.0

指针的作用

🔼

我先说明作用后面慢慢分析.
作用:

  1. 数据共享更加便捷,打破共享壁垒
  2. 以更精简的方式引用大的数据结构
  3. 利用指针可以直接操纵内存地址(在MCU,嵌入式开发上体会会很深刻)
  4. 利用指针, 能在程序执行过程中预留新的内存空间(当然在没有MMU的单片机中,无法实现,能写内存管理的大佬除外)

先抛开指针,让我们来想想, 变量的作用是什么? 使内存空间易于管理.
好, 为什么内存空间难管理,因为内存标号(内存地址)往往都是一串16进制数,难于记忆,所以对内存地址取个名字,便于记忆
局部变量都有一定的局限性,所谓的作用域, 全局变量虽然没有局限性,但大量的全局变量会浪费许多内存.
然而很多时候我们需要在作用域外来修改内存上的值(例如,两数交换),数据唯一不变的只有地址,所以想要实现数据共享,只能通过 传递地址 来实现了,指针——一种特殊的变量,就应运而生了.(学过汇编后, 会对指针的数据共享理解更加深刻)

第二个作用, 很大程度上体现在函数传参和字符串上,思考一个问题,如果我有 一个结构体,如下:

struct RxBufferTypeDef{
    int size;
    int read;
    int write;
    int *buffer;
};

里面有 3个整型变量, 一个指针变量,一个int变量4字节 ,一个指针变量8字节, 一个结构体 20 个字节大小, 如果 一个函数调用时传递的是整个结构体,那么就相当于在内存中又开辟了 20 字节大小空间,用于实现对传入结构体的复制, 而且不能对结构体的成员进行修改, 如果传递结构体指针,大小恒为 8 字节,而且可以对结构体进行修改, 整个程序中只占用20字节,在内存和运行速度上,比之直接传结构体更为方便.

大多数情况下, 可以看到程序使用的内存是通过显式声明分配给变量的内存(也就是静态内存分配). 这一点对于节省计算机内存是有帮助的, 因为计算机可以提前为需要的变量分配内存. 但是在很多应用场合中, 可能程序运行时不清楚到底需要多少内存, 这时候可以使用指针, 让程序在运行时获得新的内存空间(实际上应该就是动态内存分配malloc, calloc), 并让指针指向这一内存更为方便.

指针的声明

🔼

语法: 数据类型 * 指针名称 = 初始地址;
示例:

int main(void)
{
    int i_variable = 10;
    double d_variable = 1.1;

    // 声明一个 指向整型的指针,指向i_variable
    int *ptr_a = &i_variable; 
    //声明一个 指向整型的指针,指向d_variable
    int *ptr_b = &d_variable; 

    return 0;
}

指针的使用

🔼

两个和指针息息相关的运算符:

  • & 是 C语言中的取地址符号,用于获取地址
  • * 是 C语言中的解引用的符号,用于获取地址上的值

操作:

  • 修改指向的地址;
    语法: ptr_b = &a;
  • 修改指向地址上的值;
    语法: *ptr_b = 100;
  • 所有指针都之和 处理位数以及 编译器相关,一般来说 是 8字节或者 4字节,比较特殊的 51单片机是12位

示例1:

int main(void)
{
    int i_variable1 = 10;
    int i_variable2 = 20;
    double d_variable = 1.1;

    // 声明一个 指向整型的指针,指向i_variable1
    int *ptr_a = &i_variable1; 
    //声明一个 指向整型的指针,指向d_variable
    int *ptr_b = &d_variable; 

    // 打印指针大小
    printf("sizeof(ptr_a) = %d\n",sizeof(ptr_a));
    printf("sizeof(ptr_b) = %d\n",sizeof(ptr_b));

    // 打印 i_variable 指向的地址上的值
    printf("ptr_a = %d\n", *ptr_a);

    ptr_a = &i_variable2; // 改变 ptr_a 的指向
    printf("ptr_a = %p\n", *ptr_a);

    *ptr_a = 100;
    printf("i_variable1 = %d\n", i_variable1); // 
    printf("i_variable2 = %d\n", i_variable2);
    printf("ptr_a = %d\n", *ptr_a);

    return 0;
}

空指针

🔼

  • 定义: 指针变量指向内存中编号为0的空间
  • 用途: 初始化指针变量,不知道指哪里,就先指向这里
  • 注意: 空指针指向的内存是不可以访问的
  • 示例:
int main(void)
{

	//指针变量p指向内存地址编号为0的空间
	int * p = NULL;

	//访问空指针报错 
	//内存编号0 ~255为系统占用内存, 不允许用户访问
	printf("0x%x", p);
	printf("%d", *p);

	return 0;
}

野指针

🔼

  • 定义: 指针变量指向非法的内存空间
    非法空间是指,指针指向系统和程序协商后可访问空间之外的地址
    alt 野指针

  • 示例:

int main(void) 
{

	//指针变量p指向内存地址编号为0x1100的空间
	int * p = (int *)0x1100;

	//访问野指针报错 
	printf("%d", *p);

	return 0;
}

一级指针

🔼

简单来说就是只有 一个 * 运算符的指针变量,大多数的一维指针会用于指向结构体、数组、字符串...

指向变量的指针

🔼

指向变量的语法,如上面指针的基本内容所述,只要存在数据类型,都可以定义指针
这说一个比较特殊的 指针——void指针,众所周知, void 是一种空类型,那么,正常思路下指向 void 的指针, 就是指向空的指针,简称除了NULL啥都不能指.
其实不然, 正所谓 万物皆虚, 万事皆允(we are assassins!), void 指针啥都能指, 最简单的例子就是 内置qsort的回调函数cmp(const void *a, const void *b),咱们总不可能开两个空数组吧(滑稽)
其实无论什么数据,在存储器上都是高低电平,只是读取的方式不同,才有了不同的数据类型, void指针正是依靠了这个特性, 通过强制类型转换来实现,任意传参.
其实无论什么指针,都可以强制转化,用于传递地址,只是因为指针只存储地址, 且数据的意义只与读取方式相关所以才可以进行相互转换
PS: 个人建议,如果要传入不知道什么类型的数据时可以考虑以 void * 作为参数, 不到万不得已,不要使用其他指针来实现类型转换

示例

typedef struct _Nodes
{
    int id;
    char *str;
} MyStruct;

int main(void)
{
    int a = 10;
    double d = 1.1;
    MyStruct node1 = {1, "This is a test about void pointer"};
    void *v_ptr = &a;

    // 转化位 int
    printf("====================int=======================\n");
    printf("a = %d\n", a);
    printf("&a = 0x%x\n", &a);
    printf("*v_ptr = %d\n", *(int *)v_ptr);
    printf("v_ptr = 0x%x\n", v_ptr);

    // 转化位 double
    v_ptr = &d;
    printf("====================double=======================\n");
    printf("d = %.2lf\n", d);
    printf("&d = 0x%x\n", &d);
    printf("*v_ptr = %.2lf\n", *(double *)v_ptr);
    printf("v_ptr = 0x%x\n", v_ptr);

    // 转化为 结构体
    v_ptr = &node1;
    printf("====================structer=======================\n");
    printf("node.id = %d\n", node1.id);
    printf("node.str = %s\n", node1.str);
    printf("&d = 0x%x\n", &node1);
    printf("(*v_ptr).id = %d\n", (*(MyStruct *)v_ptr).id);
    printf("(*v_ptr).str = %s\n", (*(MyStruct *)v_ptr).str);
    printf("v_ptr = 0x%x\n", v_ptr);

    return 0;
}

示例2:
这一部分是标准库调用示例:

int cmp(const void *a, const void *b)
{
    return (*(int *)a) > (*(int *)b) ? 1 : -1;
}

void travelArray(int *arr, int n);

int main(void)
{
    int n;
    // 获取数组长度
    scanf("%d", &n);
    // 动态开辟数组
    int *arr = (int *)malloc(sizeof(int) * n);

    // 读入数据
    for (int i = 0; i < n; i++)
    {
        scanf("%d", arr + i);
    }
    
    travelArray(arr, n);
    qsort(arr, n, sizeof(arr[0]), cmp);
    travelArray(arr, n);

    return 0;
}
/**
 * @brief 遍历数组
 * 
 * @param arr   数组首地址
 * @param n     数组大小 
 */
void travelArray(int *arr, int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    putchar('\n');
}

const指针

🔼

这就是一个经典的问题了, ** **, const 修饰不同 的地方,指针的效果就不一样,const 一共有三种修饰方式:

  • 第一种: const int* ptr = &a; 常量指针,可以改变指向方向
  • 第二种: int* const ptr = &a; 指针常量,可以改变地址上的值
  • 第三种: const int* const ptr = &a; 上面两种的结合体,可以称为指针常数

顾名思义,常量指针, 指向常量的指针,指向的是常量,指针不是常量,可以改变指向的地址,但是不能改变指向的值
指针常量, 指针自己是一个常量, 指向的不一定是常量,所以可以改变所指向地址上的值,不能改变指向的地址
指针常数,这个就不多说了,啥都改不了,指向的是常数

示例:

int main(void)
{
    int a = 10, b = 20;
    int *pa = &a;
    // 常量指针
    const int *cpa = pa;
    // 指针常量
    int *const pca = pa;
    // 指针常数
    const int *const cpca = pa;

    // cout << "a = " << a << endl;
    printf("pa = 0x%x\n", pa);
    printf("*cpa = %d\n", *cpa);
    printf("*pca = %d\n", *pca);
    printf("*cpca = %d\n", *cpca);

    puts("================修改cpa指向==================\n");

    cpa = &b; // 正确, 修改常量指针指向的地址
    // pca = &b; // 错误, 修改指针指常量向的地址
    printf("&b = 0x%x\n", &b);
    printf("cpa = 0x%x\n", cpa);
    printf("pca = 0x%x\n", pca);

    cpa = &a;
    puts("=================修改pca指向的值===============\n");
    // *cpa = 90;   // 错误, 修改常量指针的指向的变量的值
    *pca = 100; // 正确, 修改指针常量指向的值
    printf("*pa = %d\n", *pa);
    printf("*cpa = %d\n", *cpa);
    printf("*pca = %d\n", *pca);

    // cpca = &b;   // 错误, 双const 啥都不能改
    // *cpca = 90;  // 错误, 双const 啥都不能改

    return 0;
}

指向函数的指针

话外: 为什么不能用二级指针直接指向二维数组

  1. 什么是函数指针:
    和变量类似,如果在程序中定义了一个函数, 那么在编译时系统就会为这个函数代码分配一段存储空间, 这段存储空间的首地址称为这个函数的地址,在debug时,我们会在主栈中看到,被压入的函数的地址. 而且函数名表示的就是这个地址. 既然是地址我们就可以定义一个指针变量来存放, 这个指针变量就叫作函数指针变量, 简称函数指针.
  2. 函数指针的声明:
    函数指针与其他指针声明方式不同, 正如前面所说, 指针只能指向一种数据类型, 所以 函数指针的声明有些复杂,大致格式如下:
    返回数据类型 (* 指针名称)(参数列表);
    例如: int (*funPtr)(int *, int *);
    这个语句就定义了一个指向函数的指针变量 funPtr. 首先它是一个指针变量, 所以要有一个 * , 即(*p); 其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数; 后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数. 所以合起来这个语句的意思就是: 定义了一个指针变量 funPtr, 该指针变量可以指向返回值类型为 int 型, 且有两个整型参数的函数. funPtr 的类型为 int(*)(int, int)
  3. 如何用函数指针调用函数
    int Func(int x);   /*声明一个函数*/
    int (*pFunc) (int x);  /*定义一个函数指针*/
    pFunc = Func;          /*将Func函数的首地址赋给指针变量p*/
    
    PS: 函数指针 没有 ++-- 的操作
  • 示例
    #include <stdio.h>
    
    int Max(int x, int y); //函数声明
    
    int main(void)
    {
        int a = 0, b = 0, c = 0;
        int (*p)(int, int); //定义一个函数指针
    
        //把函数Max赋给指针变量p, 使p指向Max函数
        p = Max;
    
        printf("please enter a and b:");
        scanf("%d%d", &a, &b);
    
        //通过函数指针调用Max函数
        c = (*p)(a, b);
        printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
    
        return 0;
    }
    /**
     * @brief 比较连个数大小
     * 
     * @param x 比较数 x
     * @param y 比较数 y
     * @retval int 最大值
     */
    int Max(int x, int y) //定义Max函数
    {
        return x > y ? x : y;
    }
    

二级指针

🔼
与 一级指针类似, 需要两次 * 操作才能得到最顶层值 的 指针变量, 最常见的就是 字符串数组. 一级指针往往比较简单, 维度一升高 后就开始变得复杂起来
三者之间的关系如图, 手残, 将就一下
alt
在内存中的图示
alt

指向指针的指针

🔼

指针可以指向一份普通类型的数据, 例如 int、double、char 等, 也可以指向一份指针类型的数据, 例如 int *double *char * 等, 所以就有了指向指针的指针
上面图片的关系用 C 语言来描述就是

int a = 10;
int *ptr_a = &a;
int **pptr_a = &ptr_a;

指针变量也是一种变量, 也会占用存储空间, 也可以使用&获取它的地址.C语言不限制指针的级数, 每增加一级指针, 在定义指针变量时就得增加一个星号*. p1 是一级指针, 指向普通类型的数据, 定义时有一个*; p2 是二级指针, 指向一级指针 p1, 定义时有两个*.

那么,为什么要有二级指针呢?
先来看一下这段代码:
有两个变量a,b,指针 q,q指向a, 我们想让q指向b,在函数里面实现.
这里贴一下用于测试的主函数

#include <stdio.h>

int a = 10;
int b = 100;
int *q;

void func(int *p);

int main(void)
{
    printf("&a=0x%x, &b=0x%x, &q=0x%x\n", &a, &b, &q); // 1
    q = &a;

    printf("*q=%d, q=0x%x, &q=0x%x\n", *q, q, &q); // 2
    func(q);
    printf("*q=%d, q=0x%x, &q=0x%x\n", *q, q, &q); // 5

    return 0;
}
  • 用 一级指针 实现:
    void func(int *p)
    {
        printf("func: &p=0x%x, p=%d\n", &p, p); // 3
        p = &b; // 让指针 p 指向 b;
        printf("func: &p=0x%x, p=%d\n", &p, p); // 4
    }
    

看起来 在逻辑上代码没有什么问题, 但是众所周知,程序执行后 *q 不等于100,为什么呢?
来简单看一下, 测试输出的结果

&a=0x403010, &b=0x403014, &q=0x407970
*q=10, q=0x403010, &q=0x407970
func: &p=0x61fe00, p=4206608  
func: &p=0x61fe00, p=4206612  
*q=10, q=0x403010, &q=0x407970

来分析一下输出:

  • 注释 1: a, b, q都有一个地址.
  • 注释 2: q 指向 a, q的值发生了变化, 地址是固定的
  • 注释 3: 进入函数后的参数p的地址跟q不一样了. 这是因为在函数调用时,为了保障原数据不变对其进行了拷贝, 也就是说 pq 不是同一个指针, 但是 他们指向的地址是相同的,都指向 &a(0x403010)
  • 注释 4: p 指向 b, 这时候 p 的值发生了变化
  • 注释 5: 回到主函数后, 函数栈释放, p 也就丢失了, q 也不会有任何变化.

结论:
编译器会对函数的每个参数制作临时副本, 指针参数p的副本是 q, 编译器使 p = q (但是 &p != &q ,也就是他们并不在同一块内存地址, 只是他们的内容一样, 都是a的地址). 如果函数体内的程序修改了p的内容(比如在这里它指向b). 在本例中, p申请了新的内存, 只是把 p所指的内存地址改变了(变成了b的地址,但是q指向的内存地址没有影响), 所以在这里并不影响函数外的指针q.

其实, 这就是 所谓的 传值调用传地址调用, 这两个概念就是一个抽象概念, value 和 address 是相对的, 对于指针变量来说, 传值 也是地址, 传地址也是地址,只不过前者是传递指向的地址,后者是传递本身的地址.

例如 swap 函数, 如果参数为 (int a, int b), 那就是传值调用, 因为我们想要交换 ab 的值, 如果仅仅传入值, 那么调用函数产生的副本,也仅仅是 数值与 a b 相同的两个全新变量而已. 我们想要交换两个变量, 就必须要传入地址, 在地址上直接对值进行操作.

上例中, p 对应的是 a b 变量, 我们只传进想要改变的值, 而非传入值所在的地址, 所以 q 并没产生变化. 这时候我们就需要传入, 指针 *q 的地址了, 对应的函数参数类型, 就变量了指向指针的指针, 也就是二级指针.

  • 二级指针操作

    void func(int **p)
    {
        printf("func: &p=0x%x, p=%d\n", &p, p); 
        *p = &b; 
        printf("func: &p=0x%x, p=%d\n", &p, p); 
    }
    

    改动的地方很少.

    因为传了指针 q 的地址(二级指针**p)到函数,所以二级指针拷贝(拷贝的是 p,一级指针中拷贝的是 q,就是指向的地址),(拷贝了指针但是指针内容也就是指针所指向的地址是不变的)所以它还是指向一级指针q (*p = q). 在这里无论拷贝多少次, 它依然指向q, 那么*p = &b;自然的就是 q = &b;了.

  • PS:
    到这里其实我,想说的是其实 一级指针也可以实现二级指针的效果,但是并不推荐,咱们永远不知道这种方法的通用性,
    我们可以 一级指针的调用的时候传入 q 的地址, 然后把赋值语句改为 *p = &b; 也可以得到相应的效果, 因为指针都是 8 字节, 里面进行了一次(隐式)强制类型转换.由于指针指向的类型比较简单, 没有导致数据异常, 所以GUN仅仅是抛出了 warming.

指针与数组

🔼
这里 大概会涉及到几个内容:

  • 指针 与 数组首地址
  • 指针数组 和 指向数组的指针

先声明一下:

  • 指针: 是用于存储 地址的变量
  • 数组: 是一串相同类型变量的
  • 地址: 是一种数据,与指针不同的地方在于,没有数据类型(某种意义上像是指针常量)

先来说一下第一个: 指针 与 数组首地址

在我初学指针的时候一直有一个疑惑就是 int arr[4];int *ptr; 的区别, 因为老师经常说, 数组的首地址等价于指针, 而且 访问数组的时候使用 arr[1]*(arr+1) 的效果是一样的, 我一度就把 一级指针 等价为 一维数组.
直到有一次, 有个小姐姐问我,为什么 sizeof(ptr)算不出数组大小 而 sizeof(arr)可以. 我当时的回答是,因为 arr 是 一个数组 ptr 是一个指针变量.说完我就察觉到不对了, 如果 arr 完全等价于 ptr 那么为什么 sizeof(ptr) 不等于 sizeof(arr), arr = ptr 会报错. 果然啊,教学相长(有点丢脸,错失良机啊)
其实 数组的首地址、变量的取地址 这些得到的都是指针常量(上面说过,传送门), 只能充当右值, 不能作为左值.
ptr 仅仅是一个指针变量,用于存储地址的变量. 对于二维数组,也是如此, int arr[2][3] = {0}; , arr 整个二维数组的首地址, arr[i] 为 每一行一维数组的首地址.
示例:

#include <stdio.h>

int main(void)
{
    int arr[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    int *ptr1 = arr;

    for (int i = 0; i < 9; i++)
    {
        printf("ptr1_val = %d \n", ptr1[i]);
    }

    return 0;
}

接着咱们 来讨论一下: 指针数组和指向数组的指针

这个问题 经常出现在,声明指向二维数组指针上, 经常会有 问题是 int *arr[5]int(*arr)[5] 哪个是指向数组的指针之流.
在说这个问题之前,先来说明一下两个概念:

  • 行指针: 指的是一整行, 不指向具体元素
    声明格式: 数据类型 (*指针名)[长度];
  • 列指针: 指的是一行中某个具体元素(一维指针)
    声明格式: 数据类型 *指针名称;
    示例
#include <stdio.h>

int main(void)
{
    int arr[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    int *ptr1 = arr;        // 声明列指针
    int(*ptr2)[9] = &arr;   // 声明行指针

    for (int i = 0; i < 9; i++)
    {
        printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i], (*ptr2)[i]);
    }

    return 0;
}

PS: 可以将列指针理解为行指针的具体元素, 行指针理解为列指针的地址

言归正传, 假设 我们需要声明一个指向 int arr[5][9]; 和 一个大小为 4 指向整型指针的数组.

先来解决第一个:

假设 指针名为 ptr

  1. ptr 先是一个指针, 所以第一步是*ptr,
  2. *ptr 指向的是数组,由于 [] 运算优先级比 * 高, 所以 需要加上 (), 即 (*arr)[9]
  3. (*arr)[9] 指向的类型为 int, 得出最终结果 int (*arr)[9] = &arr;

然后是指针数组

设 ptrs 为数组名:

  1. ptrs 先是一个数组, 得到 ptrs[4]
  2. ptrs[4] 的成员是指针, 得到 *ptrs[4]
  3. *ptrs[4] 成员指向的数据类型为 int, 得到 int ptrs[4];

小结: 指向数组的指针格式一般以 (*指针名) 打头, 而指针数组一般以 *数组名 打头

正如上面所说, 一般情况下指向数组的指针格式以 (*指针名) 打头,咱们这行只要是一般,就必然有例外.
由于 数组是在一块连续的内存上定义的 所以 只要找到 第一个元素所在的地址,即数组起始地址,就等级于找到整个数组.
所以就有了 p = &arr[0]p = &arr[0][0]*p = &arr[0][0][0]; 的奇观.
这也就是上面所说的 列指针. 突然就感觉 上面的行指针不香了(滑稽)

示例:

#include <stdio.h>

int main(void)
{
    int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    int *ptr1 = &arr[0][0];
    int(*ptr2)[3] = arr;

    for (int i = 0; i < 9; i++)
    {
        printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i], (*ptr2)[i]);
    }
    printf("======================================================\n");
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("ptr1_val = %d, ptr2_val = %d\n", ptr1[i * 3 + j], ptr2[i][j]);
        }
    }

    return 0;
}

话外: 为什么不能用二级指针直接指向二维数组

🔼

举个例子:

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

    int **pptr = arr; //编译出错, 不能用二级指针直接指向二维数组

    int(*ptrRow)[3] = arr; //对, ptrRow是指向一维数组的指针, 可以指向二维数组

    int *ptrCol = arr[0]; //可以, ptrCol也是一维指针, 可以指向二维数组

理论上一维数组对应一维指针, 例如int arr[3]; int *ptr = arr;
那么二维数组应该也对应于二级指针才对啊.
对于这个问题, 咱们先来看一下二级指针的定义:
二级指针指向一级指针, 一级指针是取一次地址, 二级指针是取两次地址.
就此可以推及到 更高的维度: n级指针是指向n-1级指针的指针, n级指针是取n次地址

现在我们来分析一下
int **pptr = arr; 为什么会出错
首先 **pptr = arr 是等价于 **pptr = &arr[0][0];
那么 *ptr 得到的结果为 1, 如果 再对其进行 * 操作,就会访问到 内存地址 1 上的值, 显然这是不允许的
PS: 二级指针是指向一级指针的, 那么二级指针 pptr 每次移动的大小就是 sizeof(int *) 也就是8个字节, 所以 pptr+1 不是像二维数组 arr+1 那样移动到下一行首地址, 而是移动8个字节.

int **pp=a; 不行. 那 int **pp=&a; 呢?
很遗憾也不行, 原因也是数据类型不一致, 导致地址偏移非法.

凡事总有那么个例外:
例如:

char *str[2] = {"hello","world"};
char **strptrs = str;

本质原因是因为这是一个指针数组, 而非 真正的二维数组, 每一行的首地址 本身即为指针, 符合了两次取地址的要求.而且指针偏移量也为 8 字节, strptrs++偏移正常, 所以 strptrs 才能指向 str

其实这个问题的核心在于 数据类型的不匹配 导致的地址自加异常.
示例

#include <stdio.h>

int main(void)
{
    int arr[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
    int **pptr = arr;
    char *str[4] = {"hello", "world", "C pointer", "G++"};
    char **strptrs = str;

    printf("%c\n", strptrs[0][0]);
    // printf("%d\n", pptr[0][1]);// 异常, 不会打印数据

    return 0;
}

以上都是,个人浅薄的见解,如有不当,欢迎各位大佬们指出

posted @ 2021-03-13 20:16  秦_殇  阅读(138)  评论(0编辑  收藏  举报