C语言——指针详解

什么是指针:

    1、指针是一种数据类型,使用它可以定义指针变量,简称指针。

    2、指针变量中存储的是整数,这种整数表示的是内存地址(便签的序号)。

    3、可以根据指针变量中存储的内存地址去访问对应的内存。

如何使用指针:

    定义指针变量:

        类型* 指针变量名;

        1、由于指针变量的使用方法与普通变量不同,所以要从名字加以区别,一般以p结尾。

        2、指针变量的类型决定使用访问内存时的字节数,指针变量中存储的是一个内存块的第一个字节的地址。

            int* p; // 用p访问内存时一次访问4字节

            short* p; // 用p访问内存时一次访问2字节

            double* p; // 用p访问内存时一次访问8字节

        3、指针变量的默认值与普通变量一样是随机的(野指针),为了安全一般定义指针变量时要初始化,如果不知道赋什么值可以初始化为NULL(空指针)。

        4、指针变量不能连续定义:

            int* p1,p2; // p1是指针变量,p2是int类型变量

            int *p1,*p2; // p1、p2都是指针变量


指针变量赋值:

    指针变量 = malloc(size); // 向malloc申请一块size字节的堆内存,malloc会返回这个内存块的首地址

    指针变量 = &普通变量; // 计算出普通变量的内存块首地址赋值给指针变量

    把一个合法内存地址赋值给指针变量,该步骤也叫引用或绑定,如果赋值是非法内存地址,此时也不会出错。

    指针变量 = 指针变量; // 必须类型相同

解引用:

    *指针变量; // 根据指针变量中存储的内存地址去访问对应的内存块,具体访问多少个字节,由指针变量的类型决定,如果之前赋值的是非法的内存地址,此时会出现段错误。

    int num;

    int* p = #

    *p <=> num; // *p 等价于 num

指针解引用时产生段错误的原因:

    1、指针变量中存储的内存地址是非法的,也就是不在map文件的内存范围内。

    2、指针变量存储的是text内存段的地址,并尝试修改该内存段的内容。

    3、指针变量存储NULL地址,空指针只要解引用就会产生段错误。


什么情况下使用指针:

    1、函数之间共享变量。

        由于全局变量在使用期间无法释放,所以会造成内存浪费,并且会与其它全局标识符有命名冲突,因此全局变量尽量少最好不用,而使用指针就可以让函数之间共享变量。

        函数之间共享变量可以让调用者返回多个数据,例如:

            int scanf(const char *format, ...);

            // 调用scanf时需要返回多个数据,1、终端输入的数据 2、成功或失败的状态


2、提高函数的传参数效率

    由于C语言函数传参是单向值传递,就是一个变量给另一个变量赋值,也就是把一个变量内存中的内容,拷贝给另一个变量。

    当变量的字节数较多时,这种单向值传递比较耗时,而传递变量的地址,只需要拷贝4个字节的数据,这样就可以大大节约函数的传时间。

    注意:使用指针提高函数的传参效率时,变量就有了被破坏的风险,可以使用 const 类型* 指针 进行保护变量。

3、由堆内存无法取名字(无法用变量名与堆内存建立联系),所以使用堆内存必须与指针变量配合。

总结:指针是一种非常灵活、功能强大的技术手段,但同时也非常危险,能不用就不用。


使用指针要注意的问题:

    空指针:

        指针变量的值是NULL,这种指针变量叫空指针,只要对空指针解引用就会产生段错误,操作系统规则NULL地址不能访问。

        返回值是指针类型的函数,如果返回NULL则表示该函数指针出错,所以NULL也是函数出错的标志。


如何避免空指针产生的段错误:

    勤快,对来历不明的指针解引用前要进行判断。

    1、函数的参数

    2、函数的返回值

    if(!p)

    {//在当前系统下,NULL就是0地址,但不是所有系统的NULL都是0,在当前系统下可以用,但不能做通用、跨平台,并且容易让人误会p是布尔类型变量。

    }

    if(NULL == p)   (更推荐)

    {//既容易阅读,也能跨平台、通用

    }

野指针:

    指针变量的的值是不确定的、随机的,这种指针变量叫野指针,对这种指针变量解引用可能会产生 段错误、脏数据、一切正常 等情况,由于这类错误具有隐藏性、潜伏性、随机性,所以它比空指针的危害更大。

如何避免野指针产生的危害:

    由于野指针无法判断,所以只能不制造野指针才能避免使用野指针。

    1、定义指针变量初始化。

    2、不返回局部变量的地址。

    3、当堆内存释放后,与它配合的指针变量要及时的赋值为NULL。


指针的进步值:

    就是指针类型字节数,也就指针+1时前进的字节数。

指针的运算:

    前提:指针变量中存储的就是代表内存编号的整数,理论上整型数据能使用的运算符,指针都可以使用,但由于这些整数代表内存编号且类型不同,所以只有个别的指针运算才有意义。

    指针+整数 = 代表地址的整数+进步值*整数

    指针-整数 = 代表地址的整数-进步值*整数

    指针加减一个整数,相当于指针前后移动。


指针-指针 = (代表地址的整数-代表地址的整数)/进步值

必须类型相同的指针才能进行相减,否则会有编译错误。

指针减指针可以计算出两个指针之间相隔多少个元素。

short* p1 = NULL;

p1 += 4;

printf("%p",p1);  // 4

int* p2 = NULL;

p2 += 8;

printf("%p",p2) // 8

printf("%d\n",p2-p1); // 4


指针与const配合使用:

    指针与const配合有五种写法,三种功能

    const int* p = &num; // 保护指向的目标内存不修改

    int const * p = &num; // 同上

    *p = 6666; // 编译错误


int* const p = &num; // 保护指针变量不被修改

p = NULL; // 编译错误

const int* const p = &num; //既保护目标又保护指针

int const * const p = &num;  // 同上

*p = 6666; // 编译错误

p = NULL;  // 编译错误


通用指针:

    所谓的通用指针就是void类型的指针,它能与任意类型的指针进行转换,在设计函数时有一些不确定调用者传递的指针是什么类型就可以使用void类型的指针作为函数的参数。

    注意:void类型的指针进步值是1,且不能直接解引用,必须先转换成其它类型才能解引用。

    void clear_memory(void* p,size_t n)

    {

        if(NULL == p)

            return;

        char* arr = p;

        for(int i=0; i<n; i++)

        {

            *(arr+i) = 0;

        }

    }

指针与数组名:

    数组名其实就是个常量地址,类型 数组名[n],那么数组名就是 类型* 的地址。

        float arr[10];

        arr 的类型就是 float*


当我们使用数组作为函数的参数时,函数的形参就是指针变量,所以它的长度信息才丢失,需要额外增加一个参数传递数组的长度。

    void show_arr(int arr[],size_t len)

    {

        printf("%d\n",sizeof(arr)); // 4

        printf("%d\n",sizeof(arr)/sizeof(arr[0])); // 1

        while(arr < arr+len)

        {c

            printf("%d ",*arr++); // ++运算级别高于解引用

        }

    }

arr[n] <=> *(arr+n) 8877这两种语法就是等价的,如果指针变量指向一块连续内存,指针变量可以当数组使用。

指针与数组名的相同点:

    1、它们都是地址,代表一块连接的内存。

    3、它们都可以使用*,[]进行解引用,遍历一块连续内存。

指针与数组名的不同点:

    1、指针是变量而数组名是常量

    2、指针变量有自己的存储空间用于存储内存地址,而数组名没有,它就是地址。

    3、指针变量与目标内存是指向关系,而数组名与目标内存是映射关系。


指针数组:

    就是由指针组成的数组,它的身份是数组,成员是指针。

    类型* 数组名[数组长度];

    使用它能构造出不规则的二维数组:

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

    int arr2[8] = {8,1,2,3,4,5,6,7};

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

    int arr4[9] = {9,1,2,3,4,5,6,7,8};

    int arr5[3] = {3,1,2};


    int* arr[5] = {arr1,arr2,arr3,arr4,arr5};

    for(int i=0; i<5; i++)

    {

        for(int j=1; j<arr[i][0]; j++)

        {

            printf("%d ",arr[i][j]);

        }

        printf("\n");

    }


    // 堆内存构造出不规则的二维数据

    int lens[5] = {5,6,7,8,9};

    int* arr[5];

    for(int i=0; i<5; i++)

    {

        arr[i] = malloc(sizeof(int)*lens[i]);

        arr[i][0] = lens[i];

    }

    for(int i=0; i<5; i++)

    {

        for(int j=1; j<arr[i][0]; j++)

        {

            printf("%d ",arr[i][j]);

        }

        printf("\n");

    }


    // 使用指针数组指向若干个字符串字面值

    char* arr[] = {"hello1","hello2","hello3"};

数组指针:

    专门指向数组的指针,它的进步值是整个数组的字符数。

    类型 (*指针变量名) [长度];

    arr与&arr的值相同,但类型不同,arr 类型是int*,&arr 类型是 int (*)[n]


    使用它可以把一块连续的内存当作二维数组使用:

    int arr[20];

    int (*p)[5] = (void*)arr;


    for(int i=0; i<4; i++)

    {

        for(int j=0; j<5; j++)

        {

            printf("%d ",p[i][j]);

        }

        printf("\n");

    }

指针、数组名、二维数组名、数组指针:

    int arr[7];

    int* p <=> arr; // 一维数组的数组名是普通指针

    int (*p)[7] <=> &arr;

    *p <=> arr; // 数组指针解一次引用就变成的指针


    arr[i] <=> *(arr+i);


    int arr1[3][4];

    int (*p)[4] <=> arr1; // 二维数组的数组名是数组指针

    int (*p)[3][4] <=> &arr1;

    arr1[i][j] <=> *(*(arr1+i)+j)

    int* p <=> *arr1;

指针函数:

    返回值是指针类型的函数就叫作指针函数,如果指针函数的返回值是NULL/0xffffffff,则表示指针函数执行出错,指针函数的返回值一定要先判断再解引用。

函数指针:

    前提:函数就一段代码,这段代码会被翻译成二进制指令存储在代码段中,函数名就是个地址,表示函数的二进制指令在代码段的位置。

    什么是函数指针:就是专门指向函数的指针变量,使用函数指针可以把函数像数据一样在函数之间进行传递。

    注意:函数既是一段数据,也是一种类型。

    如何定义函数指针:

        1、照抄函数声明。

        2、用小括号包含函数名

        3、在函数名的前面加*,并在函数名末尾添加_fp。


    使用函数:

        1、用函数名给函数指针赋值。

        2、函数指针(实参) 就可以调用函数了。


        int func(int,int);


        int (*fp)(int,int) = &func;

        int (*fp)(int,int) = func;


        sizeof(func);  // 结果是1

        sizeof(fp);    // 结果是4


    函数指针的作用在大多数情况下用于实现回调模式,先实现的代码调用后实现的函数,例如:

        void qsort(void *base, size_t nmemb, size_t size,

                int (*compar)(const void *, const void *));

        该函数是早期标准库程序实现的,它无法知道如何判定元素的大小,所以就留一个函数指针的参数,让我们提供一个自己实现的对数组元素进行比较函数。

二级指针:

    专指向普通指针的指针变量,它里面存储的是指针变量的地址。

    普通指针变量有三项用处:

        1、跨函数共享变量

        2、提高函数的传参效率

        3、配合堆内存使用

    注意:二级指针只有一个用处,就是跨函数共享指针变量。

    类型** 指针变量名pp = &普通指针变量;

    int num = 1234;

    int* p = &num;

    int** pp = &p;


    pp <=> &p;

    *pp <=> p <=> &num;

    **pp <=> *p <=> num;

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容