栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

一文带你深入浅出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 数组与指针关系

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位住户。这样一来想要访问和管理这么多住户中特定的一户就容易的多了。

2. 指针变量

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

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

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

地址对应字节

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

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个字节,才能存放一个地址。

3. 指针的基本类型

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

        准确的说:有的。

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

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

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

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

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

意义何在

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

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

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

关于两者存储与读取方式的不同,想了解更多请戳这里跳转阅读:一文带你深入浅出C语言数据

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

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. 避免返回局部变量的地址

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

        关于函数栈帧的内容请戳这里跳转阅读:一文带你深入浅出函数栈帧

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 != '' )
              p++;
       return p-s;//双指针计算字符串字符个数
}

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

5.4 数组与指针关系

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

(更多关于数组的内容请戳这里跳转阅读:)

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

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)。

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指针进阶还会再讲到指针数组的,到时候可以进一步巩固。

 


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

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

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/937856.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号