一文带你深入浅出C语言指针(初阶)

目录

📌前言

📌1. 指针是什么

📌举个例子

📌指针的意义

📌2. 指针变量

📌解引用 

📌地址对应字节

📌如何编址

📌深入理解编址

📌3. 指针的基本类型

📌意义何在

📌4. 野指针

📌4.1 概念 

📌4.2 野指针成因

📌1. 指针未初始化

📌2. 指针越界访问

📌3. 指针指向的空间未释放

📌4.3 如何规避野指针

📌1. 指针初始化

📌2. 小心指针越界

📌3. 指针指向空间释放及时置NULL

📌4. 避免返回局部变量的地址

📌5. 指针使用之前检查有效性

📌5. 指针运算

📌5.1 指针的关系运算

📌5.2 指针+- 整数        

📌5.3 指针相减 

📌5.4 数组与指针关系

📌指针和数组访问互通

📌指针和数组的区别

📌5.5 指针的强制转化

📌例1

📌 例2

📌6. 关于const修饰下的指针

📌6.1 常量指针

📌6.2 指针常量

📌6.3 小总结

📌6.4 常量指针常量

📌6.5 举个例子

📌7. 二级指针

📌8. 指针数组简介

📌8.1 指针数组是指针还是数组?

📌8.2 用指针数组模拟二维数组

📌敬请期待更好的作品吧~


📌前言

        学习C语言,不得不学到的就是指针,甚至可以这么说:指针是C语言的精髓之所在。

本文就来分享一波作者的C指针学习见解与心得,由于水平有限,难免存在纰漏,读者各取所需即可。

给你点赞,加油加油!

📌1. 指针是什么

指针理解的2个要点:

1. 指针是内存中一个最小存储单元(1byte)的编号,也就是地址。

2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

        指针就是地址,口语中说的指针通常指的是指针变量。

📌举个例子

        常用的例子就是住户的门牌号,小区里的住楼都是以户为单位的,每一户的构造由基本相同,如果没有划分编号的话很有可能就找不到想要找的住户,毕竟家家户户从门外看都是一样的。一旦根据一定依据,比如说这栋楼是A区的,每层的住户给上对应楼层号,再按一定顺序分配号码,像是A213,就是A区楼2楼第13位住户。这样一来想要访问和管理这么多住户中特定的一户就容易的多了。

📌指针的意义

        回答一个问题:为何每间宿舍都要有门牌号呢?结论:提高查找效率和准确度。

        类比到计算机中

        CPU在内存中寻址的基本单位是多大?——字节

        在32位机器下,最多能够识别多大的物理内存?——2^32字节

        既然CPU寻址按照字节寻址,但是内存又很大,所以,内存可以看做众多字节的集合

        其中,每个内存字节空间,相当于一个学生宿舍,字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位。

        每间宿舍都有门牌号就等价于每个字节空间对应的地址,即该空间对应的指针。

        那么,为何要存在指针呢?为了CPU寻址的效率。如果没有,该怎么找在字节空间中的数据呢?只能是按顺序遍历。

📌2. 指针变量

        我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量。存放在指针中的值都被当成地址处理。

        有一个问题,int变量占四个字节,那不就有四个地址吗,变量的地址又是哪一个呢?

        答案是从低到高数第一个地址,因为通过第一个地址,根据变量类型,比如int就沿着从低到高数够四个字节就能把变量的值全覆盖。

📌解引用 

        对于指针变量,可以使用*操作符间接使用其保存地址所指向的变量。

        比如:

        *p完整理解是,取出p中的地址,访问该地址指向的内存单元(空间或者内容)(其实通过指针变量访问,本质是一种间接寻址的方式)。

         知道了指针的本质就是地址,地址就是数据,那么我们可以直接通过地址数据对变量进行访问吗?

        大部分技术书,是落后于行业的。目前主流的编译器和操作系统,为了安全,已经有了很多内存保护的机制。我们目前的windows和Linux都有栈随机化这样的机制来方式黑客对用户数据地址进行预测。当然,还有其他的栈保护机制,比如“金丝雀”技术之类的。

        经过试验,目前vs和Centos7上,使用C语言定义的局部变量,在每次运行的时候,地址都是不同的。经过试验发现,定义全局变量,每次更改代码,地址也会发生变化。

        通过地址直接寻址的方式现在是行不通的,因为地址每次运行时都会随机化,这次是这个地址,下次就是另一个了。所以使用的都是指针解引用间接寻址了。

📌地址对应字节

        经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

bit

byte

kb

mb

gb

        而各类型的变量的大小最小1byte,最大也不过8byte,如果用bit为内存单元分配地址的话太浪费地址了,内存就会很小,而要是用kb之类的为内存单元分配地址的话太浪费空间了,所以用byte是相对更合适的。

📌如何编址

        地址本身是由硬件产生的一串二进制序列,用来唯一标识一块内存空间。

        对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0),电信号转换为数字信号。

那么32根地址线产生的地址就会是

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111

        这里就有2的32次方个地址。

        每个地址标识一个字节,那我们就可以给

(2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)

        4G的空间进行编址。

        在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

        在64位机器上,有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

📌深入理解编址

        首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。

        但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。

        而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。

        不过,我们今天关心一组线,叫做地址总线。

        CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。

        计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

        举个例子:钢琴、吉他上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识。

        硬件编址也是如此

        我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0或1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32中含义,每一种含义都代表一个地址。

        地址信息被下达给内存,在内存内部,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

📌3. 指针的基本类型

        我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?

        准确的说:有的。

        不同类型指针应存放对应类型变量的地址,如:

        char* 类型的指针存放 char 类型变量的地址。

        short* 类型的指针存放 short 类型变量的地址。

        int* 类型的指针存放 int 类型变量的地址

        并且如果int a = 0; 的话,&a就是int型指针,也就是说取地址时会自动根据原变量类型确定指针类型。

📌意义何在

        指针的类型决定了它从地址处访问的内存大小,比如char*就是一个字节,int*就是4个字节等等。

        虽然float*和int*指针访问的都是四个字节,但是不可以混用,比如

        因为float和int存储方式不同,float*和int*在解引用时的读取方式也不同,所以混用可能会出问题。

关于两者存储与读取方式的不同,想了解更多请戳这里跳转阅读:

        要注意:无论是什么类型的指针,本质上都是指针,在同一平台下占用空间大小都相同。

📌4. 野指针

📌4.1 概念 

        概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

        指针给了程序员深入内存的权限,但也有可能打开“潘多拉魔盒”,不经意间造成难以想象的后果,所以要小心使用指针。

📌4.2 野指针成因

📌1. 指针未初始化

(局部变量指针未初始化,默认为随机值)

如int *p;

这时候不要解引用,为什么?

        指针未初始化,其值是一个随机值,不知道解引用后会把值存到何处,这可能没问题,也可能会擦写数据或代码甚至导致程序崩溃也是有可能的。

📌2. 指针越界访问

int main() 
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}

📌3. 指针指向的空间已释放

1.动态分配的内存已释放。

2.函数调用返回临时变量地址,临时变量随函数调用结束而一同销毁,如果返回其地址并使用就会造成非法访问内存,属于危险行为。

📌4.3 如何规避野指针

📌1. 指针初始化

        该赋什么值就赋什么值,暂时还不知道赋什么值的时候赋个NULL(值为0)空指针。

        要注意NULL不可以解引用,写入权限冲突,也就是你没有权限去访问零地址,指向无效。

📌2. 小心指针越界

📌3. 指针指向空间释放及时置NULL

📌4. 避免返回局部变量的地址

        注意,即使变量销毁也只是说将内存返还给系统而不属于当前程序,而原来的空间还在,存储的值也还在,如果解引用返回的地址还是能访问那块空间的,这也是野指针危险的地方之一,并且那块空间存储的值有可能会变动,因为在函数调用完以后,如果要调用别的函数或者创建临时变量就有可能覆盖原来的空间。(结合函数栈帧来分析)

        关于函数栈帧的内容请戳这里跳转阅读:一文带你深入浅出函数栈帧http://t.csdn.cn/fJ4oP

📌5. 指针使用之前检查有效性

        在使用前,判断一下是不是空指针,如if(p != NULL)…,不过这是建立在遵循指针初始化原则的基础之上的方法。

📌5. 指针运算

📌5.1 指针的关系运算

说明:

        指针就是地址,进行比较比的也就是地址高低。

例子: 

for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}

修改一下:

for(vp = &values[N_VALUES-1]; vp >= &values[0]; vp--)
{
    *vp = 0;
}

        实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

        允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

📌5.2 指针+- 整数        

        指针+-整数就是发生地址偏移,根据指针类型决定偏移步长(类型所占大小),比如对于char*一步长为1字节,int*一步长为4字节,这时候加或减的整数就是步长数,char*p; ,p+2就是向高地址偏移2步长也就是2字节。当然指针变量可以自增自减以改变所存储的地址的值。

        ++比*优先级高!!*vp++ = 0;相当于*vp = 0; vp++;。而(*vp)++就是把vp指向的值自增1。

#define N_VALUES 5
int main()
{
    float values[N_VALUES];
    float *vp;
    //指针+-整数;指针的关系运算
    for (vp = &values[0]; vp < &values[N_VALUES];)
    {
         *vp++ = 0;
    }
}

        你可能会奇怪,这数组只有五个元素,下标为5不就越界了吗,为什么还能出现&values[5]呢,实际上[]是下标引用操作符,&values[5]<->&*(values + 5),&*一抵消,剩下values + 5就是按着float型指针偏移了5步长后得到的指针(地址),也是float类型,要是解引用的话也能访问。我们所说的越界访问是可以发生的,而我们一般是不希望它发生的,就像我们定义数组会定义数组长度,系统按我们的要求划分一块连续空间给我们使用,而这块空间后面的空间就是未分配给我们的、没有权限的空间,可是依靠指针我们还是能够随时访问这些空间。(指针权限太大了,简直是一把双刃剑)

        arr[5]实际上就是按照指针类型在原数组后面再找了一块空间进行访问,这块空间存的是什么值我们是无法预知的。

📌5.3 指针相减 

        |指针-指针|(注意是绝对值)得到指针间元素个数,不过要注意是在同一块连续空间(如数组)上同类型指针才能进行相减

比如:

int my_strlen(char *s)
{
       char *p = s;
       while(*p != '\0' )
              p++;
       return p-s;//双指针计算字符串字符个数
}

        那有没有指针+指针呢?没有,主要是没有实际意义,就好比如生活中有日期-日期,日期+-天数,但是没有日期+日期,因为没有意义。

📌5.4 数组与指针关系

        数组名表示的是数组首元素的地址。(2种情况除外,数组章节讲解了)

        更多关于数组的内容请戳这里跳转阅读:一文带你深入浅出C语言数组http://t.csdn.cn/Zs7wt

        那么这样写代码是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

        用int*p存放数组名arr即首元素地址,则 p+i 其实计算的是数组 arr 下标为i的地址。

        那我们就可以直接通过指针来访问数组,如*(p + i)就是arr[i]。

        也就是数组的指针表示:arr[i] <->*(arr + i)。

📌指针和数组访问互通

        实际上,数组和指针都可以互相用对方的方式来表示。

        比如:

#include<stdio.h>
#include<string.h>
#define N 10
int main()
{
    const char *str = "abcdef"; //str指针变量在栈上保存,“abcdef”在字符常量区,不可被修改
    char arr[] = "abcdef"; //整个数组都在栈上保存,可以被修改,这部分可以局部测试一下

    printf("以指针的形式访问指针和以数组下标的形式访问指针\n");
    int len = strlen(str);
    for (int i = 0; i < len; i++)
    {
        printf("%c\t", *(str + i));
        printf("%c \n", str[i]);
    }
    printf("\n");

    printf("以指针的形式访问数组和以数组下标的形式访问数组\n");
    len = strlen(arr);
    for (int i = 0; i < len; i++)
    {
        printf("%c\t", *(arr + i));
        printf("%c \n", arr[i]);
    }
    printf("\n");
    return 0;
}

        指针和数组指向或者表示一块空间的时候,访问方式是可以互通的,具有相似性。但是具有相似性,不代表它们是一个东西或者具有相关性。

        之所以这样设计,很有可能和数组传参的设计有关,这部分内容将在《指针进阶》的博文讲到。

📌指针和数组的区别

📌5.5 指针的强制转化

        强制类型转化,改变的是对特定内容的看待方式,在C中,就是只改变其类型,不会改变数据本身。

         强转一是为了编译器不报警,二是为了用户能够明确类型,三是为了改变看待数据的方式。

📌例1

📌 例2

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf("%x,%x\n", ptr1[-1], *ptr2);
    return 0;
}

📌6. 关于const修饰下的指针

📌6.1 常量指针

样式: 

        const 类型 * ptr

        如const int * ptr,而int const* ptr这样写也没问题。

        为什么要叫常量指针?意味着它指向的是常量吗?

比如

int a = 10;
const int* ptr = &a;

        这样一来就不能通过解引用ptr来改变a的值了,也就是对于指针来说指向的是常量(不可变更),实际上并不是说a就是常量了。

举个例子:

        一户人家为了防盗,特地锁好门,这样就限制了从门进入这一可能,窃贼不就没办法通过门进入了嘛,自以为万无一失,却没想到窃贼从窗户翻入,“条条大路通罗马”嘛(笑)。

📌6.2 指针常量

样式: 

        类型* const ptr

        如int* const ptr

        为什么要叫指针常量呢?真的变成常量了吗?

比如

int a = 10;
int const*ptr = &a;

        这样一来ptr存的地址值不能改变了,也就是ptr只能指向a了。

📌6.3 小总结

        const修饰指针变量的时候:

1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。

2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

📌6.4 常量指针常量

样式: 

        const 类型 * const ptr

        综合了常量指针和指针常量的特点,也就是既不能改变指针的内容,又不能改变指针指向的内容。

📌6.5 举个例子

        举个日常生活中的例子来加深理解🤗:

📌7. 二级指针

        指针变量也是变量,是变量就有地址,那一级指针变量的地址存放在哪里?

        指针变量的地址也放在一个指针变量里,我们称它为二级指针。

比如:

*ppa 通过对ppa中的地址进行解引用,其实就是*&pa,这样找到的是 pa , 也就是&a 。

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,也就是*&a那找到的是 a。

        int* *ppa靠近变量名的*说明是指针变量,靠近int的*说明指向的是一级int指针。

📌8. 指针数组简介

📌8.1 指针数组是指针还是数组?

        答案:是数组。是存放指针变量的数组。

        指针类型  *数组名[元素个数]

        比如:int * parr[3];

在操作符里[]优先级比*高,可以借助这个视角来区别指针数组(int*parr[])和数组指针(int(*arrp)[])。

📌8.2 用指针数组模拟二维数组

        数组名是首元素地址,把它作为指针数组的元素,这样一来,parr[0]也就是arr1,parr[0] + 1也就是arr1 + 1对应的是&arr1[1],那么*(parr[0])<->*arr1<->arr1[0]<->1,而

*(parr[0] + 1)<->*(arr1 + 1)<->arr1[1]<->2。

        还记得[]运算符吗?parr[i][j]<->*(parr[i] + j)<->*(*(parr + i) + j)。

        在C指针进阶还会再讲到指针数组的,到时候可以进一步巩固。


📌敬请期待更好的作品吧~

感谢观看,你的支持就是对我最大的鼓励,阁下何不成人之美,点赞收藏关注走一波~ 

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

桦秋静

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值