一文带你深入浅出C语言指针(初阶)-程序员宅基地

技术标签: 一文深入浅出C语言  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指针进阶还会再讲到指针数组的,到时候可以进一步巩固。


敬请期待更好的作品吧~

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_61561736/article/details/125049068

智能推荐

适合入门的8个趣味机器学习项目-程序员宅基地

文章浏览阅读86次。首发地址:https://yq.aliyun.com/articles/221708谈到机器学习,相信很多除学者都是通过斯坦福大学吴恩达老师的公开课《Machine Learning》开始具体的接触机器学习这个领域,但是学完之后又不知道自己的掌握情况,缺少一些实际的项目操作。对于机器学习的相关竞赛挑战,有些项目的门槛有些高,参加后难以具体的实现,因此造..._scrath五子棋下载

oracle 12c avg,Oracle 12c新特性系列专题-安徽Oracle授权认证中心-程序员宅基地

文章浏览阅读83次。原标题:Oracle 12c新特性系列专题-安徽Oracle授权认证中心 随着Oracle database 12c的普及,数据库管理员 (DBA) 的角色也随之发生了转变。 Oracle 12c数据库对 DBA 而言是下一代数据管理。它让 DBA 可以摆脱单调的日常管理任务,能够专注于如何从数据中获取更多价值。未来我们会推出基于Oracle12c的技术文章,帮助DBA尽快掌握新一代数据库的新特性..._ilm add policy row store compress advanced row after

第七周项目三(负数把正数赶出队列)-程序员宅基地

文章浏览阅读150次。问题及代码:*Copyright(c)2016,烟台大学计算机与控制工程学院 *All right reserved. *文件名称:负数把正数赶出队列.cpp *作者:张冰 *完成日期;2016年10月09日 *版本号;v1.0 * *问题描述: 设从键盘输入一整数序列a1,a2,…an,试编程实现: 当ai>0时,ai进队,当ai<0时,将队首元素出队,当ai

Linux命名空间学习教程(二) IPC-程序员宅基地

文章浏览阅读150次。本文讲的是Linux命名空间学习教程(二) IPC,【编者的话】Docker核心解决的问题是利用LXC来实现类似VM的功能,从而利用更加节省的硬件资源提供给用户更多的计算资源。而 LXC所实现的隔离性主要是来自内核的命名空间, 其中pid、net、ipc、mnt、uts 等命名空间将容器的进程、网络、消息、文件系统和hostname 隔离开。本文是Li..._主机的 ipc 命名空间

adb强制安装apk_adb绕过安装程序强制安装app-程序员宅基地

文章浏览阅读2w次,点赞5次,收藏7次。在设备上强制安装apk。在app已有的情况下使用-r参数在app版本低于现有版本使用-d参数命令adb install -r -d xxx.apk_adb绕过安装程序强制安装app

随便推点

STM32F407 越界问题定位_stm32flash地址越界怎么解决-程序员宅基地

文章浏览阅读290次。如果是越界进入硬件错误中断,MSP 或者 PSP 保存错误地址,跳转前会保存上一次执行的地址,lr 寄存器会保存子函数的地址,所以如果在 HardFault_CallBack 中直接调用 C 语言函数接口会间接修改了 lr,为了解决这个问题,直接绕过 lr 的 C 语言代码,用汇编语言提取 lr 寄存器再决定后面的操作。由于 STM32 加入了 FreeRTOS 操作系统,可能导致无法准确定位,仅供参考(日常编程需要考虑程序的健壮性,特别是对数组的访问,非常容易出现越界的情况)。_stm32flash地址越界怎么解决

利用SQL注入上传木马拿webshell-程序员宅基地

文章浏览阅读1.8k次。学到了一种操作,说实话,我从来没想过还能这样正常情况下,为了管理方便,许多管理员都会开放MySQL数据库的secure_file_priv,这时就可以导入或者导出数据当我如图输入时,就会在D盘创建一个名为123456.php,内容为<?php phpinfo();?>的文件我们可以利用这一点运用到SQL注入中,从拿下数据库到拿下目标的服务器比如我们在使用联合查询注入,正常是这样的语句http://xxx?id=-1 union select 1,'你想知道的字段的内容或查询语句',

Html CSS的三种链接方式_html链接css代码-程序员宅基地

文章浏览阅读2.9w次,点赞12次,收藏63次。感谢原文:https://blog.csdn.net/abc5382334/article/details/24260817感谢原文:https://blog.csdn.net/jiaqingge/article/details/52564348Html CSS的三种链接方式css文本的链接方式有三种:分别是内联定义、链入内部css、和链入外部css1.代码为:<html>..._html链接css代码

玩游戏哪款蓝牙耳机好?2021十大高音质游戏蓝牙耳机排名_适合游戏与运动的高音质蓝牙耳机-程序员宅基地

文章浏览阅读625次。近几年,蓝牙耳机市场发展迅速,越来越多的消费者希望抛弃线缆,更自由地听音乐,对于运动人士来说,蓝牙耳机的便携性显得尤为重要。但目前市面上的大多数蓝牙耳机实际上都是“有线”的,运动过程中产生的听诊器效应会严重影响听歌的感受。而在“真无线”耳机领域,除了苹果的AirPods外,可供选择的产品并不多,而AirPods又不是为运动场景打造的,防水能力非常差。那么对于喜欢运动又想要“自由”的朋友来说,有没有一款产品能够满足他们的需求呢?下面这十款小编专门为大家搜罗的蓝牙耳机或许就能找到适合的!网红击音F1_适合游戏与运动的高音质蓝牙耳机

iOS 17 测试版中 SwiftUI 视图首次显示时状态的改变导致动画“副作用”的解决方法-程序员宅基地

文章浏览阅读1k次,点赞6次,收藏7次。在本篇博文中,我们在 iOS 17 beta 4(SwiftUI 5.0)测试版中发现了 SwiftUI 视图首次显示时状态的改变会导致动画“副作用”的问题,并提出多种解决方案。

Flutter 自定义 轮播图的实现_flutter pageview轮播图 site:csdn.net-程序员宅基地

文章浏览阅读1.9k次。  在 上篇文章–Flutter 实现支持上拉加载和下拉刷新的 ListView 中,我们最终实现的效果是在 listView 上面留下了一段空白,本意是用来加载轮播图的,于是今天就开发了一下,希望能给各位灵感。一 、效果如下说一下大体思路   其实图片展示是用的 PageView ,然后,下面的指示器 是用的 TabPageSelector ,当然整体是用 Stack 包裹起来的。1、..._flutter pageview轮播图 site:csdn.net