指针的使用注意事项:空指针、指针赋值、void *指针

    前面的文章,分析了指针的一些概念,可以说指针是C的灵魂,看起来简单,但是想要理解透彻却是相当难,需要大量的练习,不断的巩固,不断的重复才能尽可能的理解指针,这里做一个简单的阶段总结。

1、指针是地址,而不是具体的标量值,这是指针的精髓,不管是一级指针、二级 指针、 整型指针、浮点数指针、结构体指针等等等等所有类型的指针,内容都是个地址,而指针本身当然也是有地址的,而且容易忽略的是,不管这个指针指向的类型多么复杂,比如一个特别大的结构体,它的指针和一个 char型指针的大小本质上是一样的,因为指针就是起始地址。 正式由于这个原因,(void )型指针才能发挥万能指针的作用。

       所谓“地址”的是内存地址,基本上可以认为是虚拟内存地址,程序中不管是变量、函数、进程、线程, 可以说程序的全部, 任意一个元素在内存中都是有对应的地址存在的,只不是有的地址会变化,有的地址不变化而已,一般局部变量的地址会经常改变,因为使用的是“栈”,而函数的开头地址、申请的动态内存地址、静态变量、全局变量的地址则是不变的,不变的地址就意味着我们可以在整个程序中的任何地方,都可以用一定的方式去操作这个指针指向的内容,比如读、写、调用函数等。

2、永远不要使用“野”指针

    前面说了,指针的内容是 地址,是内存地址,一旦你拥有了一个指针,你也就有了权力去通过这个指针名字去访问指针指向的内容了,我们可以打个比方,内存相当于核电站 中控室里的 按钮,指针就是这些 按钮的序号地址(名字),“野”指针(未初始化定义)是没有初始化的,相当于蒙着眼睛去按那些按钮,后果当然也是不确定的,可能运气好, 按到了无关紧要的按钮,假如按到了不该被按的按钮,那后果当然也是不堪设想的,程序员就相当于CPU的上帝,我们一定要清楚的知道程序逻辑是怎么运行的,所以一旦我们定义了一个指针,必须要对指针进行初始化。

3、只要是指针都要初始化

    正如前面所讲,只要是未初始化的指针都是野指针,都不能使用,所以只要是指针都要初始化,不管嵌套多少层,不要漏掉任何一个指针的初始化,比如:

typedef struct{
    int a;
    int b;
    int *p;
}Stype; 

int main
{
    Stype *pdev = NULL;
    pdev = (Stype *)malloc(5*sizeof(Stype));   //第一层指针初始化

    //pdev[0]初始化
    pdev[0].a = 1;
    pdev[0].b = 1;
    pdev[0].p = (int *)malloc(10*sizeof(int));    //第2层指针也要初始化

    //pdev[1]初始化
    ......
    //pdev[2]初始化
    .....
    //pdev[3]初始化
    ......
    //pdev[4]初始化
}

     

4、指针的初始化方式

   所谓指针的初始化,就是给指针赋值,注意这里的“值”指的是“地址”,这个地址必须是固定的,指针是什么类型,就要赋什么类型的地址值,初始化的方式 基本有以下几种:

    ① 直接将相同类型变量赋值,比如 int *p = &a;   其他类型变量也是一样的,前提是a是确定的。

    ② 动态内存分配,比如 int *p = (int *)malloc(10*sizeof(int));  这段代码背后的逻辑是,我们申请了 10个 int 型长度的内存区域。 而p则是起始地址。所以我们可以 通过 p[0]、p[1].....直接访问对应的变量。这里可能会觉得这个例子比较简单,其实其他类型的动态内存也是一样的,比如代码如下:

typedef struct
{
    int a;
    int b;
    int *p;
} Stype;


Stype *pdev = (Stype *)malloc(5*sizeof(Stype));

pdev[0].a = 1;
pdev[0].b = 2;
pev[0].p = (int *)malloc(3*sizeof(int));

      上面的代码理解起来还是有些难度的,但是我们只要牢记两点:指针是地址+指针必须初始化,就能剥茧抽丝,深入理解代码的含义。上面的代码的含义是:pdev是一个结构体指针,这个结构体指针指向了一段内存区域,是这段内存区域的起始地址,这段内存区域又包含了 5 小段内存区域,每小段内存区域的包含了一个整型a,整型吧和一个整型指针。我们可以通过[]操作符,间接操作指针,也就是说pdev[0]是第1小段,pdev[1]是第2小段,以此类推。结构体中包含的整型指针,又被初始化为一小段内存区域,这段内存区域包含了3个整型。至此,我们可以通过p访问n多的信息,上面的代码的实现的结果,在内存中的示意图如下所示:

                                                                指针的使用注意事项:空指针、指针赋值、void *指针_第1张图片   

    这里特别说明一下,如果对于"[ ]"操作符不太熟悉的话,可能会有混淆,不严谨的说,“[ ]”其实是等同于 p + i 的, 也就是所谓的通过下标来访问数据,对于简单的标准变量,比如 int a[10];   int *p = a;,我们获得的是10个变量的起始指针, p[i] 实现的是 直接访问数组中 对应序号 i的值。

   而 如果这个指针是 指针数组,比如说struct a;    struct a *p = (struct a *)malloc(10*sizeof(struct a));  我们相当于获得了10个结构体起始指针,这个时候,我们使用p[i]则是对应第i个指针。访问结构体变量,需要 用 -> 操作符。

5、void 类型指针,只能用于指针传送,不能直接使用。

    void 型指针绝对是一项特别伟大的发明,可以说, 如果没有void 类型指针,那么程序代码写起来工作量会剧增,比如说我们在创建线程时,需要给线程传递参数,线程参数就是一个void型,这样所有的程序员都可以按照自己的需求传递想要的类型指针,打个比方,这就相当于,当我们想要给线程 们在传传递指针参数时,我们把我们想要的参数指针地址的时候,放到一个一个箱子里,这个箱子是一个万能箱,什么都能装,这样就做到了最大的兼容性。所以我们需要将自己的参数指针强制转换为(void *)型。所以说,void类型指针是 传递参数的利器。

    void型指针只能用于指针的传送,不能直接使用,我们传递完void类型指针后,当我们要具体使用它的时候,必须必须必须将该空指针再转换为它原来的类型,否则我们是无法使用该指针的。这一点也是比较容易理解的,试想,我们让cpu去使用一个void类型指针,cpu绝对会一脸懵逼的说:你丫给了我一个箱子,不给我打开看,我用这个箱子能访问锤子地址。所以将空指针再转换为它原来的指针操作就相当于打开这个箱子,让CPU知道这个指针类型和值。程序代码示例:

void test_fun(void *arg)
{
    int *p = (int *)arg;   //必须 转换为原来的类型
    .....
    ....

}

int main(void *arg)
{
    int a[10];
    
    test_fun((void *)a);  // 必须转换为 void *


    return 0;
}

 

你可能感兴趣的:(C/C++)