一文带你深入浅出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

智能推荐

StarRocks x Paimon 构建极速实时湖仓分析架构实践

当前 StarRocks x Paimon 的能力主要包括:支持各类存储系统,包括 HDFS 以及对象存储 S3/OSS/OSS-HDFS支持 HMS 以及阿里云 DLF 元数据管理系统支持 Paimon 的 Primary Key 和 Append Only 表类型查询支持 Paimon 系统表的查询,常见例如 Read Optimized 表,snapshots 表等支持 Paimon 表和其他类型数据湖格式的关联查询支持 Paimon 表和 StarRocks 内表的关联查询。

Java设计模式 _创建型模式_原型模式(Cloneable)

1、原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能比较好。一般对付出较大代价获取到的实体对象进行克隆操作,可以提升性能。(2)、复写clone方法(当前对象本身可以不复写,如果当前对象被继承,需要clone子类,则必须要复写)(1)、需要克隆的实体类实现Cloneable接口。可以看出,完整的复制了属性,且并不是同一个对象。

GPT与GAN结合生成图像——VQGAN原理解析

这篇文章,我们讲VQ_GAN,这是一个将特征向量离散化的模型,其效果相当不错,搭配Transformer(GPT)或者CLIP使用,达到的效果在当时可谓是令人拍案叫绝![GPT与GAN结合生成图像——VQGAN原理解析-哔哩哔哩]效果演示:图像生成其他任务。

希尔(谢尔)排序/缩小增量排序(Python实现)_希尔排序dk=1-程序员宅基地

文章浏览阅读280次。排序-希尔排序文章目录排序-希尔排序时间线前言定义一图流逻辑逻辑思考时间复杂度实现Python思考时间线 2020年7月4日——完成初稿 2020年7月4日——增加时间性能测试前言从学习算法到这篇笔记完成之前,一直都以为希尔排序是多么难懂以及高大上,但其实只要弄懂了就简单了。以前觉得只要掌握一些常见的即可,但后来才发现,其它的排序算法也有其独特的魅力——不是说这个算法可以实现了什么完成了什么,其魅力在于实现的过程,与其它算法的区别。定义一图流希尔排序是直接插入排序算法的改进,所_希尔排序dk=1

今天开始学Pattern Recognition and Machine Learning (PRML)书,章节1.2,Probability Theory 概率论(上)...-程序员宅基地

文章浏览阅读146次。原创书写,转载请注明出处http://www.cnblogs.com/xbinworld/archive/2013/04/25/3041505.html今天开始学Pattern Recognition and Machine Learning (PRML)书,章节1.2,Probability Theory (上)这一节是浓缩了整本书关于概率论的精华,突出一个不确定性(unc..._荆炳义 高等概率论讲义 pdf

Hive中的replace方法_hive replace-程序员宅基地

文章浏览阅读6.5w次,点赞7次,收藏31次。Hive中的replace方法Hive本身并没有replace方法,但是提供了两个方法可以实现replace功能translateregexp_replacetranslate例子使用空字符串替换#字符> select translate('This #is test to verify# translate #Function in Hive', '#','');+---..._hive replace

随便推点

php7.4在foreach中对使用数据使用无法??[]判读,无法使用引用传递&

代码如下图:这样子在foreach中是无法修改class_history的。

pdf转换成byte放入mysql_如何将生成的pdf文件保存到java中的mysql数据...-程序员宅基地

文章浏览阅读357次。我有一个使用itext库生成pdf文件的java类.现在根据我的需要,我必须将生成的pdf文件保存到mysql数据库表中,但是我不知道该怎么做.我的担心是:…1.我可以在pdf表的mysql列中提供什么数据类型以保存pdf文件.2.哪个查询将生成的pdf文件插入数据库..目前,我正在生成pdf文件,并将其存储到本地磁盘的硬编码文件路径中.这是我在java中的pdf生成代码…OutputStream..._java:mysql数据库据转换pdf格式并打印机输出java:mysql数据库据转换pdf格式并打印

LateX beamer 下的报错unknown CJK family \CJKsfdefault is being ignored_latex cjkfamily报错-程序员宅基地

文章浏览阅读1.8k次。报错信息unknown CJK family \CJKsfdefault is being ignored解决方法在文档中添加\setCJKsansfont{Heiti SC}注意这里我是Mac系统,选用的字体是『Heiti SC』,其它系统具体的字体名称可能不一样_latex cjkfamily报错

中科世为 Z6S Linux HMI 屏幕模组上手记录 | 01 - 环境搭建_linux hmi开源项目-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏9次。1. 中科世为Z6S串口屏中科世为官网最近到手一块中科世为的串口屏,开搞!Z6S串口屏中运行的是 FlyThings OS 嵌入式物联网界面系统,FlyThings OS是中科世为基于Linux为操作系统的核心并加入了GUI,硬件层,媒体层,网络层等为系统框架层。同时提供了基于Windows桌面上运行的FlyThings IDE为开发者提供了一个更加便捷的方式完成界面编辑,代码编译,下载调试的功能。FlyThings OS系统的组成如下:内核基于开源的Liunx3.4的内核版本针对_linux hmi开源项目

【Vue脚手架避坑】创建vue-cli后报错找不到package.json文件_vue找不到package.json-程序员宅基地

文章浏览阅读1.4k次。【Vue脚手架避坑】创建 vue-cli 后报错找不到 package.json 文件_vue找不到package.json

STM32点灯-程序员宅基地

文章浏览阅读384次,点赞5次,收藏5次。使用stm32点亮LED灯