当前位置:首页 > 指针
  • 干货 | 函数指针与软件设计

    作者:Li XianJing 记得刚开始工作时,一位高手告诉我,说,longjmp和setjmp玩得不熟,就不要自称为C语言高手。当时我半信半疑,为了让自己向高手方向迈进,还是花了一点时间去学习longjmp和setjmp的用法。 后来明白那不单是跳来跳去那样简单,而是一种高级的异常处理机制,在某些情况下确实很有用。 为了显示自己的技巧,也在自己的程序中用过几次。渐渐发现这样的技巧带来的好处是有代价的,破坏了程序的结构化设计,程序变得很难读,尤其对新手来说。 终于明白这种技巧不过是一种调味料,在少数情况使用几次,可以简化对问题的处理。如果把调味拿来当饭吃,一定会本末倒置,写出的程序会呈现营养不良之状。 事实上,longjmp和setjmp玩得熟不熟与是不是C语言高手,不是因果关系。但是,如果可以套用那位高手的话,我倒想说如果函数指针玩得不熟,就不要自称为C语言高手。为什么这么说呢,函数指针有那么复杂吗? 当然不是,任何一个稍有编程常识的人,不管他懂不懂C语言,在10分钟内,我想他一定可以明白C语言中的函数指针是怎么回事。 原因在于,难的不是函数指针的概念和语法本身,而是在什么时候,什么地方该使用它。函数指针不仅是语法上的问题,更重要的是它是一个设计范畴。 真正的高手当然不单应该懂得语法层面上的技巧,更应该懂得设计上的方法。不懂设计,能算高手吗?怀疑我在夸大其辞吗?那我们先看看函数指针与哪些设计方法有关:与分层设计有关。分层设计早就不是什么新的概念,分层的好处是众所周知的,比较明显好处就是简化复杂度、隔离变化。 采用分层设计,每层都只需关心自己的东西,这减小了系统的复杂度,层与层之间的交互仅限于一个很窄的接口,只要接口不变,某一层的变化不会影响其它层,这隔离了变化。 分层的一般原则是,上层可以直接调用下层的函数,下层则不能直接调用上层的函数。这句话说来简单,在现实中,下层常常要反过来调用上层的函数。 比如你在拷贝文件时,在界面层调用一个拷贝文件函数。界面层是上层,拷贝文件函数是下层,上层调用下层,理所当然。但是如果你想在拷贝文件时还要更新进度条,问题就来了。 一方面,只有拷贝文件函数才知道拷贝的进度,但它不能去更新界面的进度条。另外一方面,界面知道如何去更新进度条,但它又不知道拷贝的进度。怎么办? 常见的做法,就是界面设置一个回调函数给拷贝文件函数,拷贝文件函数在适当的时候调用这个回调函数来通知界面更新状态。 与抽象有关。抽象是面向对象中最重要的概念之一,也是面向对象威力强大之处。面向对象只是一种思想,大家都知道,用C语言一样可以实现面向对象的编程。 这可不是为了赶时髦,而是一种实用的方法。如果你对此表示怀疑,可以去看看GTK+、linux kernel等开源代码。 接口是最高级的抽象。在linux kernel里面,接口的概念无处不在,像虚拟文件系统(VFS),它定义一个文件系统的接口,只要按照这种接口的规范,你可以自己开发一个文件系统挂上去。 设备驱动程序更是如此,不同的设备驱动程序有自己一套不同的接口规范。在自己开发设备开发驱动程序时,只要遵循相应的接口规范就行了。接口在C语言中如何表示?很简单,就是一组函数指针。 与接口与实现分开有关。针对接口编程,而不是针对实现编程,此为《设计模式》的第一条设计准则。分开接口与实现的目标是要隔离变化。软件是变化的,如果不能把变化的东西隔离开来,导致牵一发而动全身,代价是巨大的。这是大家所不愿看到的。 C语言既然可以实现面向对象的编程,自然可以利用设计模式来分离接口与实现。像桥接模式、策略模式、状态模式、代理模式等等,在C语言中,无一不需要利用函数指针来实现。 与松耦合原则有关。面向过程与面向对象相比,之所以显得苍白无力,原因之一就是它不像面向对象一样,可以直观的把现实模型映射到计算机中。 面向过程讲的是层层控制,而面向对象更强调的对象间的分工合作。现实世界中的对象处于层次关系的较少,处于对等关系的居多。也就是说,对象间的交互往往是双向的。这会加强对象间的耦合性。 耦合本身没有错,实际上耦合是必不可少的,没有耦合就没有协作,对象之间无法形成一个整体,什么事也做不了。关键在于耦合要恰当,在实现预定功能的前提下,耦合要尽可能的松散。这样,系统的一部分变化对其它部分的影响会很少。 函数指针是解耦对象关系的最佳利器。Signal(如boost的signal和glib中的signal)机制是一个典型的例子,一个对象自身的状态可能是在变化的(或者会触发一些事件),而其它对象关心它的变化。一旦该对象有变化发生,其它对象要执行相应的操作。 如果该对象直接去调用其它对象的函数,功能是完成了,但对象之间的耦合太紧了。如何把这种耦合降到最低呢,signal机制是很好的办法。 它的原理大致如下:其它关注该对象变化的对象主动注册一个回调函数到该对象中。一旦该对象有变化发生,就调用这些回调函数通知其它对象。功能同样实现了,但它们之间的耦合度降低了。 在C语言中,要解决以上这些问题,不采用函数指针,将是非常困难的。在编程中,如果你从没有想到用函数指针,很难想像你是一个C语言高手。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-05-17 关键词: 指针 C语言 longjmp

  • C++ 连载内容汇总

    C ++ 的连载教程就到此结束了,所涉及的内容也只是 C++ 中很浅显的一部分,写这个连载教程也是记录笔者学习 C++ 的一个过程,同时也提供了一个适合具备 C 语言基础的C++入门教程, 快速的掌握 C++ 相对于 C 语言来说独特的语言特性,为了方便大家查看,以下就是所有 C++ 连载教程的一个汇总链接。 (一) C++ 的引入,this 指针,程序结构,函数重载,引用和指针 (二) 构造函数,析构函数,拷贝构造函数,对象的构造顺序 (三) C++命名空间,静态成员,友元函数,运算符重载 (四)返回值为引用和非引用的区别,类内实现运算符重载函数 (五)封装,继承,继承后的访问控制,调整访问控制,三种不同继承方式的差异 (六)派生类扩展父类的功能,派生类的空间分布,多重继承,虚拟继承 (七)多态,虚函数,多态的限制 (八)C++类型转换 (九)纯虚函数,抽象类,多文件编程,动态链接库 (十)抽象类界面,模板函数的引入,模板函数参数的推导过程 (十一)函数模板重载,类模板,类重载 (十二)异常的引入,异常处理机制 (十三)智能指针 (十四)轻量级指针 (十五)强指针,弱指针 以上就是 C++ 连载的所有内容,笔者能力有限,内容难免有纰漏之处,如果各位朋友在阅读的时候发现错误,欢迎指出来,不胜感激,下面是笔者的联系方式,也欢迎各位朋友添加微信好友,一起探讨技术问题,共同进步~ 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-23 关键词: C 指针

  • 从编译器角度分析数组名和指针的区别

    int *p; p++; 却不能写这样的代码: p = a; 则 p 就表示数组a的起始地址,而&p是存储数组a的起始地址的那个地址。 这是因为编译器把a当成数组首地址,而&a当作数组第一个元素的地址,因此得到的值是一样的。 另外,sizeof(a)得到的是a所表示的数组的大小,而sizeof(p)得到的是指针的大小。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-08 关键词: 数组 编译器 指针

  • 《逆袭进大厂》之C++篇49问49答(绝对的干货)

    大家好,我是阿秀 答应你们的《逆袭进大厂》系列正式开始了。 好吧我说实话,这些都是我自己整理的秋招笔记,一把屎一把尿慢慢总结出来的那种,这些笔记可以说对我帮助良多。 它是在 github 上的 clone 下来的仓库笔记 + 自己看书理解到的知识点 + 网上相关问题的博客总结这几大基础上慢慢总结形成的,并不仅仅只是简单的收集整理,没有加入自己思考的笔记没有灵魂。 在接下来的十篇文章里我会陆陆续续将自己的秋招笔记整理出来,主要涉及C++、操作系统、计算机网络、MySQL、Redis等知识点。 C++笔记实在太多,有足足6W多字之多,我打算分成两期文章 友情提示,这篇文章字数 3W+…. 这篇文章能看完算我输!建议收藏本文,不开玩笑,真心建议。 这是本期的 C++ 八股文目录,看看你会哪些? 这是下期的 C++ 八股文目录,下期的要难一些。 闲言少叙,发车了 开车开车 1、在main执行之前和之后执行的代码可能是什么? main函数执行之前,主要就是初始化系统相关资源: 设置栈指针 初始化静态static变量和global全局变量,即.data段的内容 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数 main函数执行之后: 全局对象的析构函数会在main函数之后执行; 可以用 atexit 注册一个函数,它会在main 之后执行; 2、结构体内存对齐问题? 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。) 3、指针和引用的区别 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名 指针可以有多级,引用只有一级 指针可以为空,引用不能为NULL且在定义时必须初始化 指针在初始化后可以改变指向,而引用在初始化之后不可再改变 sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。 参考代码: void test(int *p) { int a=1;   p=&a; cout<

    时间:2021-02-05 关键词: C 指针

  • C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻

    一、前言 二、变量与指针的本质 三、指针的几个相关概念 四、指向不同数据类型的指针 五、总结 一、前言 如果问C语言中最重要、威力最大的概念是什么,答案必将是指针!威力大,意味着使用方便、高效,同时也意味着语法复杂、容易出错。指针用的好,可以极大的提高代码执行效率、节约系统资源;如果用的不好,程序中将会充满陷阱、漏洞。 这篇文章,我们就来聊聊指针。从最底层的内存存储空间开始,一直到应用层的各种指针使用技巧,循序渐进、抽丝剥茧,以最直白的语言进行讲解,让你一次看过瘾。 说明:为了方便讲解和理解,文中配图的内存空间的地址是随便写的,在实际计算机中是要遵循地址对齐方式的。 二、变量与指针的本质 1. 内存地址 我们编写一个程序源文件之后,编译得到的二进制可执行文件存放在电脑的硬盘上,此时它是一个静态的文件,一般称之为程序。 当这个程序被启动的时候,操作系统将会做下面几件事情: 把程序的内容(代码段、数据段)从硬盘复制到内存中; 创建一个数据结构PCB(进程控制块),来描述这个程序的各种信息(例如:使用的资源,打开的文件描述符...); 在代码段中定位到入口函数的地址,让CPU从这个地址开始执行。 当程序开始被执行时,就变成一个动态的状态,一般称之为进程。 内存分为:物理内存和虚拟内存。操作系统对物理内存进行管理、包装,我们开发者面对的是操作系统提供的虚拟内存。 这2个概念不妨碍文章的理解,因此就统一称之为内存。 在我们的程序中,通过一个变量名来定义变量、使用变量。变量本身是一个确确实实存在的东西,变量名是一个抽象的概念,用来代表这个变量。就比如:我是一个实实在在的人,是客观存在与这个地球上的,道哥是我给自己起的一个名字,这个名字是任意取得,只要自己觉得好听就行,如果我愿意还可以起名叫:鸟哥、龙哥等等。 那么,我们定义一个变量之后,这个变量放在哪里呢?那就是内存的数据区。内存是一个很大的存储区域,被操作系统划分为一个一个的小空间,操作系统通过地址来管理内存。 内存中的最小存储单位是字节(8个bit),一个内存的完整空间就是由这一个一个的字节连续组成的。在上图中,每一个小格子代表一个字节,但是好像大家在书籍中没有这么来画内存模型的,更常见的是下面这样的画法: 也就是把连续的4个字节的空间画在一起,这样就便于表述和理解,特别是深入到代码对齐相关知识时更容易理解。(我认为根本原因应该是:大家都这么画,已经看顺眼了~~) 2. 32位与64位系统 我们平时所说的计算机是32位、64位,指的是计算机的CPU中寄存器的最大存储长度,如果寄存器中最大存储32bit的数据,就称之为32位系统。 在计算机中,数据一般都是在硬盘、内存和寄存器之间进行来回存取。CPU通过3种总线把各组成部分联系在一起:地址总线、数据总线和控制总线。地址总线的宽度决定了CPU的寻址能力,也就是CPU能达到的最大地址范围。 刚才说了,内存是通过地址来管理的,那么CPU想从内存中的某个地址空间上存取一个数据,那么CPU就需要在地址总线上输出这个存储单元的地址。假如地址总线的宽度是8位,能表示的最大地址空间就是256个字节,能找到内存中最大的存储单元是255这个格子(从0开始)。即使内存条的实际空间是2G字节,CPU也没法使用后面的内存地址空间。如果地址总线的宽度是32位,那么能表示的最大地址就是2的32次方,也就是4G字节的空间。 【注意】:这里只是描述地址总线的概念,实际的计算机中地址计算方式要复杂的多,比如:虚拟内存中采用分段、分页、偏移量来定位实际的物理内存,在分页中还有大页、小页之分,感兴趣的同学可以自己查一下相关资料。 3. 变量 我们在C程序中使用变量来“代表”一个数据,使用函数名来“代表”一个函数,变量名和函数名是程序员使用的助记符。变量和函数最终是要放到内存中才能被CPU使用的,而内存中所有的信息(代码和数据)都是以二进制的形式来存储的,计算机根据就不会从格式上来区分哪些是代码、哪些是数据。CPU在访问内存的时候需要的是地址,而不是变量名、函数名。 问题来了:在程序代码中使用变量名来指代变量,而变量在内存中是根据地址来存放的,这二者之间如何映射(关联)起来的? 答案是:编译器!编译器在编译文本格式的C程序文件时,会根据目标运行平台(就是编译出的二进制程序运行在哪里?是x86平台的电脑?还是ARM平台的开发板?)来安排程序中的各种地址,例如:加载到内存中的地址、代码段的入口地址等等,同时编译器也会把程序中的所有变量名,转成该变量在内存中的存储地址。 变量有2个重要属性:变量的类型和变量的值。 示例:代码中定义了一个变量 int a = 20; 类型是int型,值是20。这个变量在内存中的存储模型为: 我们在代码中使用变量名a,在程序执行的时候就表示使用0x11223344地址所对应的那个存储单元中的数据。因此,可以理解为变量名a就等价于这个地址0x11223344。换句话说,如果我们可以提前知道编译器把变量a安排在地址0x11223344这个单元格中,我们就可以在程序中直接用这个地址值来操作这个变量。 在上图中,变量a的值为20,在内存中占据了4个格子的空间,也就是4个字节。为什么是4个字节呢?在C标准中并没有规定每种数据类型的变量一定要占用几个字节,这是与具体的机器、编译器有关。 比如:32位的编译器中: char: 1个字节; short int: 2个字节; int: 4个字节; long: 4个字节。 比如:64位的编译器中: char: 1个字节; short int: 2个字节; int: 4个字节; long: 8个字节。 为了方便描述,下面都以32位为例,也就是int型变量在内存中占据4个字节。 另外,0x11223344,0x11223345,0x11223346,0x11223347这连续的、从低地址到高地址的4个字节用来存储变量a的数值20。在图示中,使用十六进制来表示,十进制数值20转成16进制就是:0x00000014,所以从开始地址依次存放0x00、0x00、0x00、0x14这4个字节(存储顺序涉及到大小端的问题,不影响文本理解)。 根据这个图示,如果在程序中想知道变量a存储在内存中的什么位置,可以使用取地址操作符&,如下: printf("&a = 0x%x \n", &a); 这句话将会打印出:&a = 0x11223344。 考虑一下,在32位系统中:指针变量占用几个字节? 4. 指针变量 指针变量可以分2个层次来理解: 指针变量首先是一个变量,所以它拥有变量的所有属性:类型和值。它的类型就是指针,它的值是其他变量的地址。 既然是一个变量,那么在内存中就需要为这个变量分配一个存储空间。在这个存储空间中,存放着其他变量的地址。 指针变量所指向的数据类型,这是在定义指针变量的时候就确定的。例如:int *p; 意味着指针指向的是一个int型的数据。 首先回答一下刚才那个问题,在32位系统中,一个指针变量在内存中占据4个字节的空间。因为CPU对内存空间寻址时,使用的是32位地址空间(4个字节),也就是用4个字节就能存储一个内存单元的地址。而指针变量中的值存储的就是地址,所以需要4个字节的空间来存储一个指针变量的值。 示例: int a = 20; int *pa; pa = &a; printf("value = %d \n", *pa); 在内存中的存储模型如下: 对于指针变量pa来说,首先它是一个变量,因此在内存中需要有一个空间来存储这个变量,这个空间的地址就是0x11223348; 其次,这个内存空间中存储的内容是变量a的地址,而a的地址为0x11223344,所以指针变量pa的地址空间中,就存储了0x11223344这个值。 这里对两个操作符&和*进行说明: &:取地址操作符,用来获取一个变量的地址。上面代码中&a就是用来获取变量a在内存中的存储地址,也就是0x11223344。 *:这个操作符用在2个场景中:定义一个指针的时候,获取一个指针所指向的变量值的时候。 int pa; 这个语句中的表示定义的变量pa是一个指针,前面的int表示pa这个指针指向的是一个int类型的变量。不过此时我们没有给pa进行赋值,也就是说此刻pa对应的存储单元中的4个字节里的值是没有初始化的,可能是0x00000000,也可能是其他任意的数字,不确定; printf语句中的*表示获取pa指向的那个int类型变量的值,学名叫解引用,我们只要记住是获取指向的变量的值就可以了。 5. 操作指针变量 对指针变量的操作包括3个方面: 操作指针变量自身的值; 获取指针变量所指向的数据; 以什么样数据类型来使用/解释指针变量所指向的内容。 5.1 指针变量自身的值 int a = 20;这个语句是定义变量a,在随后的代码中,只要写下a就表示要操作变量a中存储的值,操作有两种:读和写。 printf("a = %d \n", a);这个语句就是要读取变量a中的值,当然是20; a = 100;这个语句就是要把一个数值100写入到变量a中。 同样的道理,int *pa;语句是用来定义指针变量pa,在随后的代码中,只要写下pa就表示要操作变量pa中的值: printf("pa = %d \n", pa);这个语句就是要读取指针变量pa中的值,当然是0x11223344; pa = &a;这个语句就是要把新的值写入到指针变量pa中。再次强调一下,指针变量中存储的是地址,如果我们可以提前知道变量a的地址是 0x11223344,那么我们也可以这样来赋值:pa = 0x11223344; 思考一下,如果执行这个语句printf("&pa =0x%x \n", &pa);,打印结果会是什么? 上面已经说过,操作符&是用来取地址的,那么&pa就表示获取指针变量pa的地址,上面的内存模型中显示指针变量pa是存储在0x11223348这个地址中的,因此打印结果就是:&pa = 0x11223348。 5.2 获取指针变量所指向的数据 指针变量所指向的数据类型是在定义的时候就明确的,也就是说指针pa指向的数据类型就是int型,因此在执行printf("value = %d \n", *pa);语句时,首先知道pa是一个指针,其中存储了一个地址(0x11223344),然后通过操作符*来获取这个地址(0x11223344)对应的那个存储空间中的值;又因为在定义pa时,已经指定了它指向的值是一个int型,所以我们就知道了地址0x11223344中存储的就是一个int类型的数据。 5.3 以什么样的数据类型来使用/解释指针变量所指向的内容 如下代码: int a = 30000; int *pa = &a; printf("value = %d \n", *pa); 根据以上的描述,我们知道printf的打印结果会是value = 30000,十进制的30000转成十六进制是0x00007530,内存模型如下: 现在我们做这样一个测试: char *pc = 0x11223344; printf("value = %d \n", *pc); 指针变量pc在定义的时候指明:它指向的数据类型是char型,pc变量中存储的地址是0x11223344。当使用*pc获取指向的数据时,将会按照char型格式来读取0x11223344地址处的数据,因此将会打印value = 0(在计算机中,ASCII码是用等价的数字来存储的)。 这个例子中说明了一个重要的概念:在内存中一切都是数字,如何来操作(解释)一个内存地址中的数据,完全是由我们的代码来告诉编译器的。刚才这个例子中,虽然0x11223344这个地址开始的4个字节的空间中,存储的是整型变量a的值,但是我们让pc指针按照char型数据来使用/解释这个地址处的内容,这是完全合法的。 以上内容,就是指针最根本的心法了。把这个心法整明白了,剩下的就是多见识、多练习的问题了。 三、指针的几个相关概念 1. const属性 const标识符用来表示一个对象的不可变的性质,例如定义: const int b = 20; 在后面的代码中就不能改变变量b的值了,b中的值永远是20。同样的,如果用const来修饰一个指针变量: int a = 20; int b = 20; int * const p = &a; 内存模型如下: 这里的const用来修饰指针变量p,根据const的性质可以得出结论:p在定义为变量a的地址之后,就固定了,不能再被改变了,也就是说指针变量pa中就只能存储变量a的地址0x11223344。如果在后面的代码中写p = &b;,编译时就会报错,因为p是不可改变的,不能再被设置为变量b的地址。 但是,指针变量p所指向的那个变量a的值是可以改变的,即:*p = 21;这个语句是合法的,因为指针p的值没有改变(仍然是变量c的地址0x11223344),改变的是变量c中存储的值。 与下面的代码区分一下: int a = 20; int b = 20; const int *p = &a; p = &b; 这里的const没有放在p的旁边,而是放在了类型int的旁边,这就说明const符号不是用来修饰p的,而是用来修饰p所指向的那个变量的。所以,如果我们写p = &b;把变量b的地址赋值给指针p,就是合法的,因为p的值可以被改变。 但是这个语句*p = 21就是非法了,因为定义语句中的const就限制了通过指针p获取的数据,不能被改变,只能被用来读取。这个性质常常被用在函数参数上,例如下面的代码,用来计算一块数据的CRC校验,这个函数只需要读取原始数据,不需要(也不可以)改变原始数据,因此就需要在形参指针上使用const修饰符: short int getDataCRC(const char *pData, int len) { short int crc = 0x0000; // 计算CRC return crc; } 2. void型指针 关键字void并不是一个真正的数据类型,它体现的是一种抽象,指明不是任何一种类型,一般有2种使用场景: 函数的返回值和形参; 定义指针时不明确规定所指数据的类型,也就意味着可以指向任意类型。 指针变量也是一种变量,变量之间可以相互赋值,那么指针变量之间也可以相互赋值,例如: int a = 20; int b = a; int *p1 = &a; int *p2 = p1; 变量a赋值给变量b,指针p1赋值给指针p2,注意到它们的类型必须是相同的:a和b都是int型,p1和p2都是指向int型,所以可以相互赋值。那么如果数据类型不同呢?必须进行强制类型转换。例如: int a = 20; int *p1 = &a; char *p2 = (char *)p1; 内存模型如下: p1指针指向的是int型数据,现在想把它的值(0x11223344)赋值给p2,但是由于在定义p2指针时规定它指向的数据类型是char型,因此需要把指针p1进行强制类型转换,也就是把地址0x11223344处的数据按照char型数据来看待,然后才可以赋值给p2指针。 如果我们使用void *p2来定义p2指针,那么在赋值时就不需要进行强制类型转换了,例如: int a = 20; int *p1 = &a; void *p2 = p1; 指针p2是void*型,意味着可以把任意类型的指针赋值给p2,但是不能反过来操作,也就是不能把void*型指针直接赋值给其他确定类型的指针,而必须要强制转换成被赋值指针所指向的数据类型,如下代码,必须把p2指针强制转换成int*型之后,再赋值给p3指针: int a = 20; int *p1 = &a; void *p2 = p1; int *p3 = (int *)p2; 我们来看一个系统函数: void* memcpy(void* dest, const void* src, size_t len); 第一个参数类型是void*,这正体现了系统对内存操作的真正意义:它并不关心用户传来的指针具体指向什么数据类型,只是把数据挨个存储到这个地址对应的空间中。 第二个参数同样如此,此外还添加了const修饰符,这样就说明了memcpy函数只会从src指针处读取数据,而不会修改数据。 3. 空指针和野指针 一个指针必须指向一个有意义的地址之后,才可以对指针进行操作。如果指针中存储的地址值是一个随机值,或者是一个已经失效的值,此时操作指针就非常危险了,一般把这样的指针称作野指针,C代码中很多指针相关的bug就来源于此。 3.1 空指针:不指向任何东西的指针 在定义一个指针变量之后,如果没有赋值,那么这个指针变量中存储的就是一个随机值,有可能指向内存中的任何一个地址空间,此时万万不可以对这个指针进行写操作,因为它有可能指向内存中的代码段区域、也可能指向内存中操作系统所在的区域。 一般会将一个指针变量赋值为NULL来表示一个空指针,而C语言中,NULL实质是 ((void*)0) , 在C++中,NULL实质是0。在标准库头文件stdlib.h中,有如下定义: #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif 3.2 野指针:地址已经失效的指针 我们都知道,函数中的局部变量存储在栈区,通过malloc申请的内存空间位于堆区,如下代码: int *p = (int *)malloc(4); *p = 20; 内存模型为: 在堆区申请了4个字节的空间,然后强制类型转换为int*型之后,赋值给指针变量p,然后通过*p设置这个地址中的值为14,这是合法的。如果在释放了p指针指向的空间之后,再使用*p来操作这段地址,那就是非常危险了,因为这个地址空间可能已经被操作系统分配给其他代码使用,如果对这个地址里的数据强行操作,程序立刻崩溃的话,将会是我们最大的幸运! int *p = (int *)malloc(4); *p = 20; free(p); // 在free之后就不可以再操作p指针中的数据了。 p = NULL; // 最好加上这一句。 四、指向不同数据类型的指针 1. 数值型指针 通过上面的介绍,指向数值型变量的指针已经很明白了,需要注意的就是指针所指向的数据类型。 2. 字符串指针 字符串在内存中的表示有2种: 用一个数组来表示,例如:char name1[8] = "zhangsan"; 用一个char *指针来表示,例如:char *name2 = "zhangsan"; name1在内存中占据8个字节,其中存储了8个字符的ASCII码值;name2在内存中占据9个字节,因为除了存储8个字符的ASCII码值,在最后一个字符'n'的后面还额外存储了一个'\0',用来标识字符串结束。 对于字符串来说,使用指针来操作是非常方便的,例如:变量字符串name2: char *name2 = "zhangsan"; char *p = name2; while (*p != '\0') { printf("%c ", *p); p = p + 1; } 在while的判断条件中,检查p指针指向的字符是否为结束符'\0'。在循环体重,打印出当前指向的字符之后,对指针比那里进行自增操作,因为指针p所指向的数据类型是char,每个char在内存中占据一个字节,因此指针p在自增1之后,就指向下一个存储空间。 也可以把循环体中的2条语句写成1条语句: printf("%c ", *p++); 假如一个指针指向的数据类型为int型,那么执行p = p + 1;之后,指针p中存储的地址值将会增加4,因为一个int型数据在内存中占据4个字节的空间,如下所示: 思考一个问题:void*型指针能够递增吗?如下测试代码: int a[3] = {1, 2, 3}; void *p = a; printf("1: p = 0x%x \n", p); p = p + 1; printf("2: p = 0x%x \n", p); 打印结果如下: 1: p = 0x733748c0 2: p = 0x733748c1 说明void*型指针在自增时,是按照一个字节的跨度来计算的。 3. 指针数组与数组指针 这2个说法经常会混淆,至少我是如此,先看下这2条语句: int *p1[3]; // 指针数组 int (*p2)[3]; // 数组指针 3.1 指针数组 第1条语句中:中括号[]的优先级高,因此与p1先结合,表示一个数组,这个数组中有3个元素,这3个元素都是指针,它们指向的是int型数据。可以这样来理解:如果有这个定义char p[3],很容易理解这是一个有3个char型元素的数组,那么把char换成int*,意味着数组里的元素类型是int*型(指向int型数据的指针)。内存模型如下(注意:三个指针指向的地址并不一定是连续的): 如果向指针数组中的元素赋值,需要逐个把变量的地址赋值给指针元素: int a = 1, b = 2, c = 3; char *p1[3]; p1[0] = &a; p1[1] = &b; p1[2] = &c; 3.2 数组指针 第2条语句中:小括号让p2与*结合,表示p2是一个指针,这个指针指向了一个数组,数组中有3个元素,每一个元素的类型是int型。可以这样来理解:如果有这个定义int p[3],很容易理解这是一个有3个char型元素的数组,那么把数组名p换成是*p2,也就是p2是一个指针,指向了这个数组。内存模型如下(注意:指针指向的地址是一个数组,其中的3个元素是连续放在内存中的): 在前面我们说到取地址操作符&,用来获得一个变量的地址。凡事都有特殊情况,对于获取地址来说,下面几种情况不需要使用&操作符: 字符串字面量作为右值时,就代表这个字符串在内存中的首地址; 数组名就代表这个数组的地址,也等于这个数组的第一个元素的地址; 函数名就代表这个函数的地址。 因此,对于一下代码,三个printf语句的打印结果是相同的: int a[3] = {1, 2, 3}; int (*p2)[3] = a; printf("0x%x \n", a); printf("0x%x \n", &a); printf("0x%x \n", p2); 思考一下,如果对这里的p2指针执行p2 = p2 + 1;操作,p2中的值将会增加多少? 答案是12个字节。因为p2指向的是一个数组,这个数组中包含3个元素,每个元素占据4个字节,那么这个数组在内存中一共占据12个字节,因此p2在加1之后,就跳过12个字节。 4. 二维数组和指针 一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的,从内存角度来看,一维数组和二维数组没有本质差别。 和一维数组类似,二维数组的数组名表示二维数组的第一维数组中首元素的首地址,用代码来说明: int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二维数组 int (*p0)[3] = NULL; // p0是一个指针,指向一个数组 int (*p1)[3] = NULL; // p1是一个指针,指向一个数组 int (*p2)[3] = NULL; // p2是一个指针,指向一个数组 p0 = a[0]; p1 = a[1]; p2 = a[2]; printf("0: %d %d %d \n", *(*p0 + 0), *(*p0 + 1), *(*p0 + 2)); printf("1: %d %d %d \n", *(*p1 + 0), *(*p1 + 1), *(*p1 + 2)); printf("2: %d %d %d \n", *(*p2 + 0), *(*p2 + 1), *(*p2 + 2)); 打印结果是: 0: 1 2 3 1: 4 5 6 2: 7 8 9 我们拿第一个printf语句来分析:p0是一个指针,指向一个数组,数组中包含3个元素,每个元素在内存中占据4个字节。现在我们想获取这个数组中的数据,如果直接对p0执行加1操作,那么p0将会跨过12个字节(就等于p1中的值了),因此需要使用解引用操作符*,把p0转为指向int型的指针,然后再执行加1操作,就可以得到数组中的int型数据了。 5. 结构体指针 C语言中的基本数据类型是预定义的,结构体是用户定义的,在指针的使用上可以进行类比,唯一有区别的就是在结构体指针中,需要使用->箭头操作符来获取结构体中的成员变量,例如: typedef struct { int age; char name[8]; } Student; Student s; s.age = 20; strcpy(s.name, "lisi"); Student *p = &s; printf("age = %d, name = %s \n", p->age, p->name); 看起来似乎没有什么技术含量,如果是结构体数组呢?例如: Student s[3]; Student *p = &s; printf("size of Student = %d \n", sizeof(Student)); printf("1: 0x%x, 0x%x \n", s, p); p++; printf("2: 0x%x \n", p); 打印结果是: size of Student = 12 1: 0x4c02ac00, 0x4c02ac00 2: 0x4c02ac0c 在执行p++操作后,p需要跨过的空间是一个结构体变量在内存中占据的大小(12个字节),所以此时p就指向了数组中第2个元素的首地址,内存模型如下: 6. 函数指针 每一个函数在经过编译之后,都变成一个包含多条指令的集合,在程序被加载到内存之后,这个指令集合被放在代码区,我们在程序中使用函数名就代表了这个指令集合的开始地址。 函数指针,本质上仍然是一个指针,只不过这个指针变量中存储的是一个函数的地址。函数最重要特性是什么?可以被调用!因此,当定义了一个函数指针并把一个函数地址赋值给这个指针时,就可以通过这个函数指针来调用函数。 如下示例代码: int add(int x,int y) { return x+y; } int main() { int a = 1, b = 2; int (*p)(int, int); p = add; printf("%d + %d = %d\n", a, b, p(a, b)); } 前文已经说过,函数的名字就代表函数的地址,所以函数名add就代表了这个加法函数在内存中的地址。int (*p)(int, int);这条语句就是用来定义一个函数指针,它指向一个函数,这个函数必须符合下面这2点(学名叫:函数签名): 有2个int型的参数; 有一个int型的返回值。 代码中的add函数正好满足这个要求,因此,可以把add赋值给函数指针p,此时p就指向了内存中这个函数存储的地址,后面就可以用函数指针p来调用这个函数了。 在示例代码中,函数指针p是直接定义的,那如果想定义2个函数指针,难道需要像下面这样定义吗? int (*p)(int, int); int (*p2)(int, int); 这里的参数比较简单,如果函数很复杂,这样的定义方式岂不是要烦死?可以用typedef关键字来定义一个函数指针类型: typedef int (*pFunc)(int, int); 然后用这样的方式pFunc p1, p2;来定义多个函数指针就方便多了。注意:只能把与函数指针类型具有相同签名的函数赋值给p1和p2,也就是参数的个数、类型要相同,返回值也要相同。 注意:这里有几个小细节稍微了解一下: 在赋值函数指针时,使用p = &a;也是可以的; 使用函数指针调用时,使用(*p)(a, b);也是可以的。 这里没有什么特殊的原理需要讲解,最终都是编译器帮我们处理了这里的细节,直接记住即可。 函数指针整明白之后,再和数组结合在一起:函数指针数组。示例代码如下: int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } int main() { int a = 4, b = 2; int (*p[4])(int, int); p[0] = add; p[1] = sub; p[2] = mul; p[3] = divide; printf("%d + %d = %d \n", a, b, p[0](a, b)); printf("%d - %d = %d \n", a, b, p[1](a, b)); printf("%d * %d = %d \n", a, b, p[2](a, b)); printf("%d / %d = %d \n", a, b, p[3](a, b)); } 这条语句不太好理解:int (*p[4])(int, int);,先分析中间部分,标识符p与中括号[]结合(优先级高),所以p是一个数组,数组中有4个元素;然后剩下的内容表示一个函数指针,那么就说明数组中的元素类型是函数指针,也就是其他函数的地址,内存模型如下: 如果还是难以理解,那就回到指针的本质概念上:指针就是一个地址!这个地址中存储的内容是什么根本不重要,重要的是你告诉计算机这个内容是什么。如果你告诉它:这个地址里存放的内容是一个函数,那么计算机就去调用这个函数。那么你是如何告诉计算机的呢,就是在定义指针变量的时候,仅此而已! 五、总结 我已经把自己知道的所有指针相关的概念、语法、使用场景都作了讲解,就像一个小酒馆的掌柜,把自己的美酒佳肴都呈现给你,但愿你已经酒足饭饱! 如果以上的内容太多,一时无法消化,那么下面的这两句话就作为饭后甜点为您奉上,在以后的编程中,如果遇到指针相关的困惑,就想一想这两句话,也许能让你茅塞顿开。 指针就是地址,地址就是指针。 指针就是指向内存中的一块空间,至于如何来解释/操作这块空间,由这个指针的类型来决定。 另外还有一点嘱咐,那就是学习任何一门编程语言,一定要弄清楚内存模型,内存模型,内存模型! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-02-05 关键词: 底层原理 指针 C语言

  • 弄懂这些指针基础知识,再遇C指针咱就不慌了

    一、前言 二、变量与指针的本质 三、指针的几个相关概念 四、指向不同数据类型的指针 五、总结 一、前言 如果问C语言中最重要、威力最大的概念是什么,答案必将是指针!威力大,意味着使用方便、高效,同时也意味着语法复杂、容易出错。指针用的好,可以极大的提高代码执行效率、节约系统资源;如果用的不好,程序中将会充满陷阱、漏洞。 这篇文章,我们就来聊聊指针。从最底层的内存存储空间开始,一直到应用层的各种指针使用技巧,循序渐进、抽丝剥茧,以最直白的语言进行讲解,让你一次看过瘾。 说明:为了方便讲解和理解,文中配图的内存空间的地址是随便写的,在实际计算机中是要遵循地址对齐方式的。 二、变量与指针的本质 1. 内存地址 我们编写一个程序源文件之后,编译得到的二进制可执行文件存放在电脑的硬盘上,此时它是一个静态的文件,一般称之为程序。 当这个程序被启动的时候,操作系统将会做下面几件事情: 把程序的内容(代码段、数据段)从硬盘复制到内存中; 创建一个数据结构PCB(进程控制块),来描述这个程序的各种信息(例如:使用的资源,打开的文件描述符...); 在代码段中定位到入口函数的地址,让CPU从这个地址开始执行。 当程序开始被执行时,就变成一个动态的状态,一般称之为进程。 内存分为:物理内存和虚拟内存。操作系统对物理内存进行管理、包装,我们开发者面对的是操作系统提供的虚拟内存。 这2个概念不妨碍文章的理解,因此就统一称之为内存。 在我们的程序中,通过一个变量名来定义变量、使用变量。变量本身是一个确确实实存在的东西,变量名是一个抽象的概念,用来代表这个变量。就比如:我是一个实实在在的人,是客观存在与这个地球上的,道哥是我给自己起的一个名字,这个名字是任意取得,只要自己觉得好听就行,如果我愿意还可以起名叫:鸟哥、龙哥等等。 那么,我们定义一个变量之后,这个变量放在哪里呢?那就是内存的数据区。内存是一个很大的存储区域,被操作系统划分为一个一个的小空间,操作系统通过地址来管理内存。 内存中的最小存储单位是字节(8个bit),一个内存的完整空间就是由这一个一个的字节连续组成的。在上图中,每一个小格子代表一个字节,但是好像大家在书籍中没有这么来画内存模型的,更常见的是下面这样的画法: 也就是把连续的4个字节的空间画在一起,这样就便于表述和理解,特别是深入到代码对齐相关知识时更容易理解。(我认为根本原因应该是:大家都这么画,已经看顺眼了~~) 2. 32位与64位系统 我们平时所说的计算机是32位、64位,指的是计算机的CPU中寄存器的最大存储长度,如果寄存器中最大存储32bit的数据,就称之为32位系统。 在计算机中,数据一般都是在硬盘、内存和寄存器之间进行来回存取。CPU通过3种总线把各组成部分联系在一起:地址总线、数据总线和控制总线。地址总线的宽度决定了CPU的寻址能力,也就是CPU能达到的最大地址范围。 刚才说了,内存是通过地址来管理的,那么CPU想从内存中的某个地址空间上存取一个数据,那么CPU就需要在地址总线上输出这个存储单元的地址。假如地址总线的宽度是8位,能表示的最大地址空间就是256个字节,能找到内存中最大的存储单元是255这个格子(从0开始)。即使内存条的实际空间是2G字节,CPU也没法使用后面的内存地址空间。如果地址总线的宽度是32位,那么能表示的最大地址就是2的32次方,也就是4G字节的空间。 【注意】:这里只是描述地址总线的概念,实际的计算机中地址计算方式要复杂的多,比如:虚拟内存中采用分段、分页、偏移量来定位实际的物理内存,在分页中还有大页、小页之分,感兴趣的同学可以自己查一下相关资料。 3. 变量 我们在C程序中使用变量来“代表”一个数据,使用函数名来“代表”一个函数,变量名和函数名是程序员使用的助记符。变量和函数最终是要放到内存中才能被CPU使用的,而内存中所有的信息(代码和数据)都是以二进制的形式来存储的,计算机根据就不会从格式上来区分哪些是代码、哪些是数据。CPU在访问内存的时候需要的是地址,而不是变量名、函数名。 问题来了:在程序代码中使用变量名来指代变量,而变量在内存中是根据地址来存放的,这二者之间如何映射(关联)起来的? 答案是:编译器!编译器在编译文本格式的C程序文件时,会根据目标运行平台(就是编译出的二进制程序运行在哪里?是x86平台的电脑?还是ARM平台的开发板?)来安排程序中的各种地址,例如:加载到内存中的地址、代码段的入口地址等等,同时编译器也会把程序中的所有变量名,转成该变量在内存中的存储地址。 变量有2个重要属性:变量的类型和变量的值。 示例:代码中定义了一个变量 int a = 20; 类型是int型,值是20。这个变量在内存中的存储模型为: 我们在代码中使用变量名a,在程序执行的时候就表示使用0x11223344地址所对应的那个存储单元中的数据。因此,可以理解为变量名a就等价于这个地址0x11223344。换句话说,如果我们可以提前知道编译器把变量a安排在地址0x11223344这个单元格中,我们就可以在程序中直接用这个地址值来操作这个变量。 在上图中,变量a的值为20,在内存中占据了4个格子的空间,也就是4个字节。为什么是4个字节呢?在C标准中并没有规定每种数据类型的变量一定要占用几个字节,这是与具体的机器、编译器有关。 比如:32位的编译器中: char: 1个字节; short int: 2个字节; int: 4个字节; long: 4个字节。 比如:64位的编译器中: char: 1个字节; short int: 2个字节; int: 4个字节; long: 8个字节。 为了方便描述,下面都以32位为例,也就是int型变量在内存中占据4个字节。 另外,0x11223344,0x11223345,0x11223346,0x11223347这连续的、从低地址到高地址的4个字节用来存储变量a的数值20。在图示中,使用十六进制来表示,十进制数值20转成16进制就是:0x00000014,所以从开始地址依次存放0x00、0x00、0x00、0x14这4个字节(存储顺序涉及到大小端的问题,不影响文本理解)。 根据这个图示,如果在程序中想知道变量a存储在内存中的什么位置,可以使用取地址操作符&,如下: printf("&a = 0x%x \n", &a); 这句话将会打印出:&a = 0x11223344。 考虑一下,在32位系统中:指针变量占用几个字节? 4. 指针变量 指针变量可以分2个层次来理解: 指针变量首先是一个变量,所以它拥有变量的所有属性:类型和值。它的类型就是指针,它的值是其他变量的地址。 既然是一个变量,那么在内存中就需要为这个变量分配一个存储空间。在这个存储空间中,存放着其他变量的地址。 指针变量所指向的数据类型,这是在定义指针变量的时候就确定的。例如:int *p; 意味着指针指向的是一个int型的数据。 首先回答一下刚才那个问题,在32位系统中,一个指针变量在内存中占据4个字节的空间。因为CPU对内存空间寻址时,使用的是32位地址空间(4个字节),也就是用4个字节就能存储一个内存单元的地址。而指针变量中的值存储的就是地址,所以需要4个字节的空间来存储一个指针变量的值。 示例: int a = 20; int *pa; pa = &a; printf("value = %d \n", *pa); 在内存中的存储模型如下: 对于指针变量pa来说,首先它是一个变量,因此在内存中需要有一个空间来存储这个变量,这个空间的地址就是0x11223348; 其次,这个内存空间中存储的内容是变量a的地址,而a的地址为0x11223344,所以指针变量pa的地址空间中,就存储了0x11223344这个值。 这里对两个操作符&和*进行说明: &:取地址操作符,用来获取一个变量的地址。上面代码中&a就是用来获取变量a在内存中的存储地址,也就是0x11223344。 *:这个操作符用在2个场景中:定义一个指针的时候,获取一个指针所指向的变量值的时候。 int pa; 这个语句中的表示定义的变量pa是一个指针,前面的int表示pa这个指针指向的是一个int类型的变量。不过此时我们没有给pa进行赋值,也就是说此刻pa对应的存储单元中的4个字节里的值是没有初始化的,可能是0x00000000,也可能是其他任意的数字,不确定; printf语句中的*表示获取pa指向的那个int类型变量的值,学名叫解引用,我们只要记住是获取指向的变量的值就可以了。 5. 操作指针变量 对指针变量的操作包括3个方面: 操作指针变量自身的值; 获取指针变量所指向的数据; 以什么样数据类型来使用/解释指针变量所指向的内容。 5.1 指针变量自身的值 int a = 20;这个语句是定义变量a,在随后的代码中,只要写下a就表示要操作变量a中存储的值,操作有两种:读和写。 printf("a = %d \n", a);这个语句就是要读取变量a中的值,当然是20; a = 100;这个语句就是要把一个数值100写入到变量a中。 同样的道理,int *pa;语句是用来定义指针变量pa,在随后的代码中,只要写下pa就表示要操作变量pa中的值: printf("pa = %d \n", pa);这个语句就是要读取指针变量pa中的值,当然是0x11223344; pa = &a;这个语句就是要把新的值写入到指针变量pa中。再次强调一下,指针变量中存储的是地址,如果我们可以提前知道变量a的地址是 0x11223344,那么我们也可以这样来赋值:pa = 0x11223344; 思考一下,如果执行这个语句printf("&pa =0x%x \n", &pa);,打印结果会是什么? 上面已经说过,操作符&是用来取地址的,那么&pa就表示获取指针变量pa的地址,上面的内存模型中显示指针变量pa是存储在0x11223348这个地址中的,因此打印结果就是:&pa = 0x11223348。 5.2 获取指针变量所指向的数据 指针变量所指向的数据类型是在定义的时候就明确的,也就是说指针pa指向的数据类型就是int型,因此在执行printf("value = %d \n", *pa);语句时,首先知道pa是一个指针,其中存储了一个地址(0x11223344),然后通过操作符*来获取这个地址(0x11223344)对应的那个存储空间中的值;又因为在定义pa时,已经指定了它指向的值是一个int型,所以我们就知道了地址0x11223344中存储的就是一个int类型的数据。 5.3 以什么样的数据类型来使用/解释指针变量所指向的内容 如下代码: int a = 30000; int *pa = &a; printf("value = %d \n", *pa); 根据以上的描述,我们知道printf的打印结果会是value = 30000,十进制的30000转成十六进制是0x00007530,内存模型如下: 现在我们做这样一个测试: char *pc = 0x11223344; printf("value = %d \n", *pc); 指针变量pc在定义的时候指明:它指向的数据类型是char型,pc变量中存储的地址是0x11223344。当使用*pc获取指向的数据时,将会按照char型格式来读取0x11223344地址处的数据,因此将会打印value = 0(在计算机中,ASCII码是用等价的数字来存储的)。 这个例子中说明了一个重要的概念:在内存中一切都是数字,如何来操作(解释)一个内存地址中的数据,完全是由我们的代码来告诉编译器的。刚才这个例子中,虽然0x11223344这个地址开始的4个字节的空间中,存储的是整型变量a的值,但是我们让pc指针按照char型数据来使用/解释这个地址处的内容,这是完全合法的。 以上内容,就是指针最根本的心法了。把这个心法整明白了,剩下的就是多见识、多练习的问题了。 三、指针的几个相关概念 1. const属性 const标识符用来表示一个对象的不可变的性质,例如定义: const int b = 20; 在后面的代码中就不能改变变量b的值了,b中的值永远是20。同样的,如果用const来修饰一个指针变量: int a = 20; int b = 20; int * const p = &a; 内存模型如下: 这里的const用来修饰指针变量p,根据const的性质可以得出结论:p在定义为变量a的地址之后,就固定了,不能再被改变了,也就是说指针变量pa中就只能存储变量a的地址0x11223344。如果在后面的代码中写p = &b;,编译时就会报错,因为p是不可改变的,不能再被设置为变量b的地址。 但是,指针变量p所指向的那个变量a的值是可以改变的,即:*p = 21;这个语句是合法的,因为指针p的值没有改变(仍然是变量c的地址0x11223344),改变的是变量c中存储的值。 与下面的代码区分一下: int a = 20; int b = 20; const int *p = &a; p = &b; 这里的const没有放在p的旁边,而是放在了类型int的旁边,这就说明const符号不是用来修饰p的,而是用来修饰p所指向的那个变量的。所以,如果我们写p = &b;把变量b的地址赋值给指针p,就是合法的,因为p的值可以被改变。 但是这个语句*p = 21就是非法了,因为定义语句中的const就限制了通过指针p获取的数据,不能被改变,只能被用来读取。这个性质常常被用在函数参数上,例如下面的代码,用来计算一块数据的CRC校验,这个函数只需要读取原始数据,不需要(也不可以)改变原始数据,因此就需要在形参指针上使用const修饰符: short int getDataCRC(const char *pData, int len) { short int crc = 0x0000; // 计算CRC return crc; } 2. void型指针 关键字void并不是一个真正的数据类型,它体现的是一种抽象,指明不是任何一种类型,一般有2种使用场景: 函数的返回值和形参; 定义指针时不明确规定所指数据的类型,也就意味着可以指向任意类型。 指针变量也是一种变量,变量之间可以相互赋值,那么指针变量之间也可以相互赋值,例如: int a = 20; int b = a; int *p1 = &a; int *p2 = p1; 变量a赋值给变量b,指针p1赋值给指针p2,注意到它们的类型必须是相同的:a和b都是int型,p1和p2都是指向int型,所以可以相互赋值。那么如果数据类型不同呢?必须进行强制类型转换。例如: int a = 20; int *p1 = &a; char *p2 = (char *)p1; 内存模型如下: p1指针指向的是int型数据,现在想把它的值(0x11223344)赋值给p2,但是由于在定义p2指针时规定它指向的数据类型是char型,因此需要把指针p1进行强制类型转换,也就是把地址0x11223344处的数据按照char型数据来看待,然后才可以赋值给p2指针。 如果我们使用void *p2来定义p2指针,那么在赋值时就不需要进行强制类型转换了,例如: int a = 20; int *p1 = &a; void *p2 = p1; 指针p2是void*型,意味着可以把任意类型的指针赋值给p2,但是不能反过来操作,也就是不能把void*型指针直接赋值给其他确定类型的指针,而必须要强制转换成被赋值指针所指向的数据类型,如下代码,必须把p2指针强制转换成int*型之后,再赋值给p3指针: int a = 20; int *p1 = &a; void *p2 = p1; int *p3 = (int *)p2; 我们来看一个系统函数: void* memcpy(void* dest, const void* src, size_t len); 第一个参数类型是void*,这正体现了系统对内存操作的真正意义:它并不关心用户传来的指针具体指向什么数据类型,只是把数据挨个存储到这个地址对应的空间中。 第二个参数同样如此,此外还添加了const修饰符,这样就说明了memcpy函数只会从src指针处读取数据,而不会修改数据。 3. 空指针和野指针 一个指针必须指向一个有意义的地址之后,才可以对指针进行操作。如果指针中存储的地址值是一个随机值,或者是一个已经失效的值,此时操作指针就非常危险了,一般把这样的指针称作野指针,C代码中很多指针相关的bug就来源于此。 3.1 空指针:不指向任何东西的指针 在定义一个指针变量之后,如果没有赋值,那么这个指针变量中存储的就是一个随机值,有可能指向内存中的任何一个地址空间,此时万万不可以对这个指针进行写操作,因为它有可能指向内存中的代码段区域、也可能指向内存中操作系统所在的区域。 一般会将一个指针变量赋值为NULL来表示一个空指针,而C语言中,NULL实质是 ((void*)0) , 在C++中,NULL实质是0。在标准库头文件stdlib.h中,有如下定义: #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif 3.2 野指针:地址已经失效的指针 我们都知道,函数中的局部变量存储在栈区,通过malloc申请的内存空间位于堆区,如下代码: int *p = (int *)malloc(4); *p = 20; 内存模型为: 在堆区申请了4个字节的空间,然后强制类型转换为int*型之后,赋值给指针变量p,然后通过*p设置这个地址中的值为14,这是合法的。如果在释放了p指针指向的空间之后,再使用*p来操作这段地址,那就是非常危险了,因为这个地址空间可能已经被操作系统分配给其他代码使用,如果对这个地址里的数据强行操作,程序立刻崩溃的话,将会是我们最大的幸运! int *p = (int *)malloc(4); *p = 20; free(p); // 在free之后就不可以再操作p指针中的数据了。 p = NULL; // 最好加上这一句。 四、指向不同数据类型的指针 1. 数值型指针 通过上面的介绍,指向数值型变量的指针已经很明白了,需要注意的就是指针所指向的数据类型。 2. 字符串指针 字符串在内存中的表示有2种: 用一个数组来表示,例如:char name1[8] = "zhangsan"; 用一个char *指针来表示,例如:char *name2 = "zhangsan"; name1在内存中占据8个字节,其中存储了8个字符的ASCII码值;name2在内存中占据9个字节,因为除了存储8个字符的ASCII码值,在最后一个字符'n'的后面还额外存储了一个'\0',用来标识字符串结束。 对于字符串来说,使用指针来操作是非常方便的,例如:变量字符串name2: char *name2 = "zhangsan"; char *p = name2; while (*p != '\0') { printf("%c ", *p); p = p + 1; } 在while的判断条件中,检查p指针指向的字符是否为结束符'\0'。在循环体重,打印出当前指向的字符之后,对指针比那里进行自增操作,因为指针p所指向的数据类型是char,每个char在内存中占据一个字节,因此指针p在自增1之后,就指向下一个存储空间。 也可以把循环体中的2条语句写成1条语句: printf("%c ", *p++); 假如一个指针指向的数据类型为int型,那么执行p = p + 1;之后,指针p中存储的地址值将会增加4,因为一个int型数据在内存中占据4个字节的空间,如下所示: 思考一个问题:void*型指针能够递增吗?如下测试代码: int a[3] = {1, 2, 3}; void *p = a; printf("1: p = 0x%x \n", p); p = p + 1; printf("2: p = 0x%x \n", p); 打印结果如下: 1: p = 0x733748c0 2: p = 0x733748c1 说明void*型指针在自增时,是按照一个字节的跨度来计算的。 3. 指针数组与数组指针 这2个说法经常会混淆,至少我是如此,先看下这2条语句: int *p1[3]; // 指针数组 int (*p2)[3]; // 数组指针 3.1 指针数组 第1条语句中:中括号[]的优先级高,因此与p1先结合,表示一个数组,这个数组中有3个元素,这3个元素都是指针,它们指向的是int型数据。可以这样来理解:如果有这个定义char p[3],很容易理解这是一个有3个char型元素的数组,那么把char换成int*,意味着数组里的元素类型是int*型(指向int型数据的指针)。内存模型如下(注意:三个指针指向的地址并不一定是连续的): 如果向指针数组中的元素赋值,需要逐个把变量的地址赋值给指针元素: int a = 1, b = 2, c = 3; char *p1[3]; p1[0] = &a; p1[1] = &b; p1[2] = &c; 3.2 数组指针 第2条语句中:小括号让p2与*结合,表示p2是一个指针,这个指针指向了一个数组,数组中有3个元素,每一个元素的类型是int型。可以这样来理解:如果有这个定义int p[3],很容易理解这是一个有3个char型元素的数组,那么把数组名p换成是*p2,也就是p2是一个指针,指向了这个数组。内存模型如下(注意:指针指向的地址是一个数组,其中的3个元素是连续放在内存中的): 在前面我们说到取地址操作符&,用来获得一个变量的地址。凡事都有特殊情况,对于获取地址来说,下面几种情况不需要使用&操作符: 字符串字面量作为右值时,就代表这个字符串在内存中的首地址; 数组名就代表这个数组的地址,也等于这个数组的第一个元素的地址; 函数名就代表这个函数的地址。 因此,对于一下代码,三个printf语句的打印结果是相同的: int a[3] = {1, 2, 3}; int (*p2)[3] = a; printf("0x%x \n", a); printf("0x%x \n", &a); printf("0x%x \n", p2); 思考一下,如果对这里的p2指针执行p2 = p2 + 1;操作,p2中的值将会增加多少? 答案是12个字节。因为p2指向的是一个数组,这个数组中包含3个元素,每个元素占据4个字节,那么这个数组在内存中一共占据12个字节,因此p2在加1之后,就跳过12个字节。 4. 二维数组和指针 一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的,从内存角度来看,一维数组和二维数组没有本质差别。 和一维数组类似,二维数组的数组名表示二维数组的第一维数组中首元素的首地址,用代码来说明: int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二维数组 int (*p0)[3] = NULL; // p0是一个指针,指向一个数组 int (*p1)[3] = NULL; // p1是一个指针,指向一个数组 int (*p2)[3] = NULL; // p2是一个指针,指向一个数组 p0 = a[0]; p1 = a[1]; p2 = a[2]; printf("0: %d %d %d \n", *(*p0 + 0), *(*p0 + 1), *(*p0 + 2)); printf("1: %d %d %d \n", *(*p1 + 0), *(*p1 + 1), *(*p1 + 2)); printf("2: %d %d %d \n", *(*p2 + 0), *(*p2 + 1), *(*p2 + 2)); 打印结果是: 0: 1 2 3 1: 4 5 6 2: 7 8 9 我们拿第一个printf语句来分析:p0是一个指针,指向一个数组,数组中包含3个元素,每个元素在内存中占据4个字节。现在我们想获取这个数组中的数据,如果直接对p0执行加1操作,那么p0将会跨过12个字节(就等于p1中的值了),因此需要使用解引用操作符*,把p0转为指向int型的指针,然后再执行加1操作,就可以得到数组中的int型数据了。 5. 结构体指针 C语言中的基本数据类型是预定义的,结构体是用户定义的,在指针的使用上可以进行类比,唯一有区别的就是在结构体指针中,需要使用->箭头操作符来获取结构体中的成员变量,例如: typedef struct { int age; char name[8]; } Student; Student s; s.age = 20; strcpy(s.name, "lisi"); Student *p = &s; printf("age = %d, name = %s \n", p->age, p->name); 看起来似乎没有什么技术含量,如果是结构体数组呢?例如: Student s[3]; Student *p = &s; printf("size of Student = %d \n", sizeof(Student)); printf("1: 0x%x, 0x%x \n", s, p); p++; printf("2: 0x%x \n", p); 打印结果是: size of Student = 12 1: 0x4c02ac00, 0x4c02ac00 2: 0x4c02ac0c 在执行p++操作后,p需要跨过的空间是一个结构体变量在内存中占据的大小(12个字节),所以此时p就指向了数组中第2个元素的首地址,内存模型如下: 6. 函数指针 每一个函数在经过编译之后,都变成一个包含多条指令的集合,在程序被加载到内存之后,这个指令集合被放在代码区,我们在程序中使用函数名就代表了这个指令集合的开始地址。 函数指针,本质上仍然是一个指针,只不过这个指针变量中存储的是一个函数的地址。函数最重要特性是什么?可以被调用!因此,当定义了一个函数指针并把一个函数地址赋值给这个指针时,就可以通过这个函数指针来调用函数。 如下示例代码: int add(int x,int y) { return x+y; } int main() { int a = 1, b = 2; int (*p)(int, int); p = add; printf("%d + %d = %d\n", a, b, p(a, b)); } 前文已经说过,函数的名字就代表函数的地址,所以函数名add就代表了这个加法函数在内存中的地址。int (*p)(int, int);这条语句就是用来定义一个函数指针,它指向一个函数,这个函数必须符合下面这2点(学名叫:函数签名): 有2个int型的参数; 有一个int型的返回值。 代码中的add函数正好满足这个要求,因此,可以把add赋值给函数指针p,此时p就指向了内存中这个函数存储的地址,后面就可以用函数指针p来调用这个函数了。 在示例代码中,函数指针p是直接定义的,那如果想定义2个函数指针,难道需要像下面这样定义吗? int (*p)(int, int); int (*p2)(int, int); 这里的参数比较简单,如果函数很复杂,这样的定义方式岂不是要烦死?可以用typedef关键字来定义一个函数指针类型: typedef int (*pFunc)(int, int); 然后用这样的方式pFunc p1, p2;来定义多个函数指针就方便多了。注意:只能把与函数指针类型具有相同签名的函数赋值给p1和p2,也就是参数的个数、类型要相同,返回值也要相同。 注意:这里有几个小细节稍微了解一下: 在赋值函数指针时,使用p = &a;也是可以的; 使用函数指针调用时,使用(*p)(a, b);也是可以的。 这里没有什么特殊的原理需要讲解,最终都是编译器帮我们处理了这里的细节,直接记住即可。 函数指针整明白之后,再和数组结合在一起:函数指针数组。示例代码如下: int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } int main() { int a = 4, b = 2; int (*p[4])(int, int); p[0] = add; p[1] = sub; p[2] = mul; p[3] = divide; printf("%d + %d = %d \n", a, b, p[0](a, b)); printf("%d - %d = %d \n", a, b, p[1](a, b)); printf("%d * %d = %d \n", a, b, p[2](a, b)); printf("%d / %d = %d \n", a, b, p[3](a, b)); } 这条语句不太好理解:int (*p[4])(int, int);,先分析中间部分,标识符p与中括号[]结合(优先级高),所以p是一个数组,数组中有4个元素;然后剩下的内容表示一个函数指针,那么就说明数组中的元素类型是函数指针,也就是其他函数的地址,内存模型如下: 如果还是难以理解,那就回到指针的本质概念上:指针就是一个地址!这个地址中存储的内容是什么根本不重要,重要的是你告诉计算机这个内容是什么。如果你告诉它:这个地址里存放的内容是一个函数,那么计算机就去调用这个函数。那么你是如何告诉计算机的呢,就是在定义指针变量的时候,仅此而已! 五、总结 我已经把自己知道的所有指针相关的概念、语法、使用场景都作了讲解,就像一个小酒馆的掌柜,把自己的美酒佳肴都呈现给你,但愿你已经酒足饭饱! 如果以上的内容太多,一时无法消化,那么下面的这两句话就作为饭后甜点为您奉上,在以后的编程中,如果遇到指针相关的困惑,就想一想这两句话,也许能让你茅塞顿开。 指针就是地址,地址就是指针。 指针就是指向内存中的一块空间,至于如何来解释/操作这块空间,由这个指针的类型来决定。 另外还有一点嘱咐,那就是学习任何一门编程语言,一定要弄清楚内存模型,内存模型,内存模型! 祝您好运! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-01-30 关键词: 内存 指针 C语言

  • 干货 | 常见的C编程段错误及对策

    时间:2021-01-19 关键词: 内存 C编程 指针

  • 指针的应用,一个简单例子讲清楚你多年的糊涂

    很多人遇到指针就糊涂,搞不清到底指向什么,其实是你没搞清楚 * 修饰谁,还有一些关键字修饰谁。 看下面的例子,定义一个无符号字符变量x,然后同时定义一个可以指向x的指针p,即可以将x的地址放到指针变量p里面,然后又定义了一个可以存放p地址的指针pp: #include "stdio.h"int main(void){//无符号字符变量x,指向无符号字符变量类型的指针p,指向指针类型p的指针pp.unsigned char x,*p,**pp; x=12; p=&x; pp=&p;//const是修饰指针指向的对象属性,意思是指针kp只能指向一个const常量,而kp可以被修改指向不同的const常量,但是不能通过kp指针修改指向变量的值。int const *kp,k=20,kv=32; kp=&k; kp=&kv;//const是修饰指针mp,mp将指向固定的地址,因此在定义的时候就应该指定好该地址,之后无法修改该指针内的地址。int mv=14;int *const mp=&mv;//可以让kp指向mv的地址,但是不能通过kp,修改mv的值; kp=&mv;/* 总结: const *p 表示*p是一个整体,即指针p指向的某个变量,而const修饰该变量; * const p表示const p是一个整体,const 修饰指针p,即指针p存放的地址不能变。 */printf("*p=%d\n",*p);printf("**pp=%d\n",**pp);printf(" p=%d\n",p);printf("pp=%d\n",pp);printf("pp=%d\n",&pp);} 很多时候还会遇到const关键字,很多人不知道该如何结合,到底const修饰谁? 这里你可以将定义看成是从右到左的结合。 int const *kp; int *const mp; 例如例子中的这两个,你看好了,从右到左 int const (*kp); int *(const mp); 第一个括号里是表示kp指针指向的某个变量,该变量是个const常量,不可变。 第二个是const直接修饰了指针mp,意思是指针存放的内容是不可变的,就是地址不可变。 *与&是一对逆操作,*的出现用于定义指针,在使用的时候用于通过指针找到指向的变量。 而&用于取出变量的地址。 在PC测试上,推荐一个C语言的IDE,是C语言入门圣经  C primer plus上推荐的一款:Pelles C IDE 如果想使用中文界面可以访问下面这个中文链接:https://www.pellesc.cn/ 例如一个指向固定地址的指针,若想修改它指向的地址,那么会报错 上面的报错信息意思是赋值错误,'='的操作数有不兼容的类型'int *'和'int',分配到了固定的地址。该固定的地址是不可以被修改的。 另外需要注意,如果一个变量定义为指向常量的指针,那么该指针还是可以指向一个可变的变量,但是无法通过该指针修改该变量。该变量的属性就是通过它操作指向的变量都当做不可修改常量看待。 报错内容如下: 最后奉上,注释掉不合法的语句后的完整学习代码 #include "stdio.h" int main(void){//无符号整形变量x,指向无符号整形变量类型的指针p,指向指针类型p的指针pp. unsigned int x,*p,**pp; x=12; p=&x; pp=&p; printf("通过指针p,打印x的值12:*p=%d\n",*p); printf("通过指针pp,打印pp存放的指针p指向的变量x的值:**pp=%d\n",**pp); printf("打印变量x的地址: &x=%p\n",&x); printf("打印指针p存放的变量x的地址:p=%p\n",p); printf("打印指针p的地址: pp=%p\n",pp); printf("打印指针变量pp的地址: pp=%p\n",&pp); printf("--------------------------------------------\n");//const是修饰指针指向的对象属性,意思是指针kp只能指向一个const常量,而kp可以被修改指向不同的const常量,但是不能通过kp指针修改指向变量的值。 int const *kp,k=20,kv=32; kp=&k; printf("通过指针kp访问固定的数k,*kp=%d\n",*kp); kp=&kv; printf("通过指针kp访问固定的数kv,*kp=%d\n",*kp);// 通过kp不可以修改指向地址存放变量的固定值。// *kp=21; //const是修饰指针mp,mp将指向固定的地址,因此在定义的时候就应该指定好该地址,之后无法修改该指针内的地址。 int mv=14,mx=24; int *const mp=&mv; printf("通过存放固定地址的指针访问指向的变量mv=14:*mp=%d\n",*mp); *mp=15; printf("通过存放固定地址的指针修改指向的变量mv=15:*mp=%d\n",*mp);//可以让kp指向mv的地址,但是不能通过kp,修改mv的值; kp=&mx; printf("将指向固定数据的指针kp指向一个可以修改的变量mx=24,*kp=%d\n",*kp);// 试图通过指向固定常量的指针kp修改指向的可修改变量mx时候,失败了。// *kp=25; //因为mp指针指向的地址是const类型,所以下面的操作是非法的,会报错。 // mp=&24; /* 总结: const *p 表示*p是一个整体,即指针p指向的某个变量,而const修饰该变量; * const p表示const p是一个整体,const 修饰指针p,即指针p存放的地址不能变。 */} 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-01-19 关键词: 关键字 指针

  • 指针兆欧表怎么使用_指针兆欧表怎样看读数

    指针兆欧表怎么使用_指针兆欧表怎样看读数

      指针兆欧表采用嵌入式工业单片机实时操作系统,超薄形张丝表头与图形点阵液晶显示器完美结合,该系列表具有多种电压输出等级、容量大、抗干扰强、指针与数字同步显示、交直流两用、操作简单、自动计算各种绝缘指标,各种测量结果具有防掉电功能等特点。      指针兆欧表参数      指针兆欧表使用方法   一、准备工作1、试验前应拆除被试设备电源及一切对外连线,并将被试物短接后接地放电1min,电容量较大的应至少放电2min以免触电和影响测量结果。   2、 校验仪表指针是否在无穷大上,否则需调整机械调零螺丝。   3、用干燥清洁的柔软布擦去被试物的表面污垢,必要时先用汽油洗净套管的表面积垢,以消除表面漏电电流影响测试结果。   4、将带屏蔽高压测试线一端(红色)插入(1)LINE端,另一端(红色)接于被试设备的高压导体上,将带屏蔽高压测试线屏蔽端(黑色)插入(2)GROUND端,另一端接于被试设备的高压护环上,以消除表面泄漏电流的影响(祥见“屏蔽端的使用方法”相关内容。将另外一根黑色测试线插入地端(EARTH)(3)端,另一头接于被试设备的外壳或地上。   二、开始测试   1、打开电源开关,这时开关上的电源指示灯应发亮。   2、整机开始自检,液晶屏幕上出现操作提示。   3、按动电压选择键(16)选择需要的测试电压(2.5KV或5KV)。如不选择电压可进入下一步操作。   4、按动翻页键(13)可选择测试编号。(编号反黑)如不选择编号可进入下一步操作,编号在该次测试完成后自动累加。   5、按动测试键(14),开始测试。这时高压状态指示灯发亮并且仪表内置蜂鸣器每隔1秒钟响一声,代表LINE端有高压输出。   6、这时液晶屏进入测试状态显示模式   7、仪表每隔一定时间发出提示音(15秒、1分钟、10分钟)。   8、根据所需要的测试结果(普通测试、吸收比测试、极化指数测试),再次按下测试键。   9、需连续进行第二次测量时,再次按下测试键(14),返回选择测试编号状态时可按4-7步骤执行。   三、调阅测试结果   1、在选择测试编号状态下,可按动功能键(15)进入历史测试结果状态。   2、按动翻页键(13)和电压选择键(16)来增加和减少测试结果的编号。(相应的编号反黑)。   3、选择好编号后,按动测试键(14)可进入该编号的测试结果,按动翻页键(13)可调阅改次测试结果(当前阻值、15秒阻值、60秒阻值、10分钟阻值、吸收比、极化指数)。   4、查阅完毕,按动功能键(15)返回准备测试状态。   指针式兆欧表怎么读数   指针式兆欧表的刻度值是自右向左增加的,所以读数时,指针若恰好指在刻度线上,直接读取即可;若不在刻度线上,则读取指针右侧最近的刻度值,加上指针左右侧刻度之间的估计值。例如,指针在5和6 之间,则读数近似为5.5兆欧,指针在10和15之间,则读数可近似读为12兆欧。需要注意的是,兆欧表的刻度不是线性的,而是近似于对数关系,读数时应该注意这一点。另外,使用时要首先调整指针的机械零点。

    时间:2020-08-07 关键词: 指针兆欧表 指针

  • 一种简单的奇偶分类

    输入10个数,按照奇数在前,偶数在后的方式输出。顺带测试一下是以指针的方式还是以数组的方式在内存中效率最高!代码如下:#include "iostream" #include "stdio.h" int main() { //输入十个数 int ch1[10]={0}; int ch2[10]={0};   for(int x = 0;x<10;x++) { std::cout<<"please input ten numbers :"; std::cin>>*(ch1+x); } //进行算法,奇数在前,偶数在后 int i,j; for(i=0,j=0;i<10;i++) { if(ch1[i]%2!=0) { *(ch2+j)=*(ch1+i); j++; } } for(i=0;i<10;i++) {         if(ch1[i]%2==0)    {              *(ch2+j)=*(ch1+i); j++;    }     } for(i=0;i<10;i++) { std::cout<<"  "<<*(ch2+i); } return 0; }  

    时间:2019-07-10 关键词: 指针

  • 经典趣味编程问题

    相信很多人在笔试的时候会遇到类似的题目吧,问题是这样描述的: 有n个人围成一圈,顺序排号,从第一个人开始报数(从1~3报数),凡报到3的人退出圈子, 问最后留下的人原来排在第几号。 这个题目的思路其实不难,首先第一轮是需要被三整除,标记下报到3的那个人,然后在循环找下一个 报到3的个,记得找到的时候需要重新清下这个,从此人在往下找,依次找到,最后会留下那个没有被标记的数,就算找到了。 有点啰嗦,还是上代码吧 int FindLastNumber(int nums) { int i,isThree, total; int array[nums]; int *pArray = array; for(i=0; i

    时间:2019-07-10 关键词: c 指针

  • C和指针_第10章_结构和联合_学习笔记

    1.结构struct { int a; char b; float c; }x; struct { int a; char b; float c; }y[20], *z;警告:以上两个声明被编译器当做两个截然不同的类型。即y和z为同一类型,但与x类型不用。    使用结构标签声明结构:struct SIMPLE{ int a; char b; float c; };    此时SIMPLE是结构标签,使用标签创建需要如下代码:struct SIMPLE x; struct SIMPLE y[20], *z;    使用typedef定义一个新类型:typedef struct { int a; char b; float c; }SIMPLE;    此时SIMPLE是一个类型名,使用创建变量如下所示:SIMPLE x; SIMPLE y[20], *z;1.5结构自引用    结构中不能包含类型为结构本身的成员,但可以包含一个指向类型为结构本身的成员的指针。注意://错误 typedef struct { int a; char b; float c; struct SIMPLE *d; }SIMPLE; //正确 struct SIMPLE{ int a; char b; float c; struct SIMPLE *d; };    同时使用结构标签和typedef也是正确的声明方式:typedef struct SIMPLE{ int a; char b; float c; struct SIMPLE *d; }SIMPLE;3.结构的内存分配    编译器按照成员列表的顺序一个接一个的给每个成员分配内存,但要满足正确的编剧对齐要求。一般而言:struct SIMPL{ int b; char a; char c; };比下面声明要占用更少的内存:struct SIMPL{ char a;         int b; char c; };    sizeof操作符可以获得一个结构的整体长度,包括因边界对齐而跳过的字节。如果必须确定结构某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)。offsetof( type, member );    type是结构类型,member是需要的成员名。4.结构做函数参数。    为提示效率,对较大的结构体传递参数时采用指针会比传递值更优效率。担心传递指针会导致函数修改结构体成员值可采用如下代码:void func( SIMPLE const *simple);    为提示效率还可以采用寄存器类型,即如下代码:void func( register SIMPLE const *simple);5.位段    位段的声明和结构体类似,但其的成员是一个或多个位的字段。这些不同长度的字段实际存储于一个或多个整型变量中。struct CHAR {     unsigned ch        : 7;     unsigned font      : 6;     unsigned size      : 19; };注意:    1.注重可移植性的程序应避免使用位段。    2.位段中位的最大数目。声明于32位机的位段可能在16位的机器上无法运行。    3.位段中的成员在内存中是从左往右分配还是从右往左分配。    4.当一个声明指定了两个位段,第2个位段比较大,无法容纳于第1个位段剩余位时,编译器可能把第2个位段放在内存的下一个字,也可能直接放在第1个位段后面,从而在两个内存位置的边界形成重叠。6.联合    联合中的所有成员引用的是内存中的相同位置,所占内存空间为成员中占用内存空间最大的成员所占的内存空间。因此,若成员的占用内存空间大小差距悬殊,采用在联合中存储指向不同成员的指针而不是直接存储成员本身的方法可以节省内存。联合变量可以被初始化,但这个初始值必须是联合第1个成员的类型,并且必须位于一对花括号内:union {     int a;     float b;     char c[4]; } x = { 5 };    结构和联合的结合使用以节省内存:struct VARIABLE {     enum { INT, FLOAT, STRING } type;     union {         int i;         float f;         char *s;     }value; };

    时间:2019-07-09 关键词: 指针 C语言

  • 数组与指针

    在我们教学的时候,常常会碰到学生问:老师,数组和指针有没有区别,是不是数组就是指针,如果有区别,区别在哪里?为此我写了这篇文章,希望能有点启发给学生。先从简单的说起,一维数组和指针。平常我们操作数组都是通过数组名加下标的方式,那么这个数组名到底代表什么含义?其实数组名它是一个指针常量,它是一个地址,这个地址是数组的首地址,也就是数组第一个元素的地址。例如:int a[10];你可以去看一下,a, &a, &a[0]它都是同一个值,都是数组的首地址。那么这个指针常量它的类型的是什么呢,如果数组元素的类型是int类型的,那么这个指针常量的类型就是int*,像这里就是int*类型。还有要注意的是这个指针是一个常量,不能修改这个值,但是有两种情况下数组名不被当作一个指针常量看待,一个是&运算,另一个是sizeof,一个常量肯定是没有地址的,所以&运算是取得数组的首地址,而sizeof是返回整个数组的长度,而不是返回的指针长度。所以数组和指针是如此的相似,以至于它们可以互换使用。例如,数组元素除了使用下标方式操作外,还可以使用指针的方式:*(a + 1)代表第二个元素, *(a + 2)代表第三个元素。数组可以当作指针来使用,指针同样也可以当作数组来使用。虽然数组和指针有如此相似地方,但是它们还是有区别的。(1) 数组名这个指针它是一个指针常量,也就是你不能对它进行算数运算,例如a++这是错误的操作。但是我们定义的指针它却是一个变量。(2) 我们定义一个数组,它就分配了相应大小的空间,但是定义指针,它只是分配4字节大小空间,它只能指向其它存储空间,否则,它没有任何意义。既然数组和指针有如此多的相似之处,那我们到底是使用数组还是指针呢,这要看具体的上下环境,如果是数组,那么使用数组的下标形式更让人理解,让人一看就知道这是一个数组,如果是指针,那就直接使用指针的方式操作,这样也不会混淆程序的其它阅读者。一维数组作为函数参数当我们将一个数组作为参数传递给函数时,实际上是将数组的首地址传给了子函数。那么这个函数参数该怎么去定义呢,有两种写法,例如:int strlen(char string[]);int strlen(char* string);这两种写法都是正确的,但是哪种写法更好呢,当然是使用指针的写法更好,因为我们的的确确是传递的一个指针给函数。上面是简单的介绍,接下来是具体的解释:数组是什么?什么是左值和右值?笔者引用《C专家编程》中的一段话:出现在赋值符左边的符号有时被称为左值,出现在赋值符右边的符号有时被称为右值。编译器为每个变量分配一个地址(左值)。这个地址在编译时可知,而且该变 量在运行时一直保存于这个地址。相反,存储于变量中的值(它的右值)只有在运行时才可知。如果需要用到变量中存储的值,编译器就发出指令从指定地址读入变 量值并将它存于寄存器中我对左值的理解和书上有些区别,我把这里的“符号”称为“对象”,每一个符号都代表一个对象,对象与地址是一一对应的。即如果声明了 int a,那么 a 作为一个左值时,a 即代表这个保存在某个特定的地址的对象,对这个对象赋值即为把值放在这个特定的地址;a 作为右值时即代表 a 的内容,就是一个单纯的值,而不是对象。一个值是不能作为左值的,比如一个常数 1, 1 = a 这样的赋值语句是无法编译通过的。在我看来,“左值”义同“对象”,“右值”义同“值”,所以下面“左值”和“对象”指的是相同的东西。但是“左值”又有 一个子集:“可修改的左值”,只有这个子集中的东西才能放在赋值号左边,因此我认为将引用中的第一句话修改为“出现在赋值符左边的符号有时被称为可修改的 左值”更能表达其实际的意思。为什么要引出这个子集,为的就是要把数组分出来,数组是左值,但并不是可修改的左值,因此你也不能直接把数组名放在等号左边进行赋值。数组就是数组!我先把结论放在这里,然后在进行分析:数组就是数组,一个数组名就代表一个数组对象,这个对象内可以有一个或多个元素,每个元素类型都相同;正如 int 就是 int,一个 int 变量名就代表一个 int 类型对象。看到这里,你可能要笑了,这不是什么都没说吗,谁不知道数组是这个意思啊,我想知道数组和指针什么关系。其实对数组的认识就是这样一个返璞归真过程,看我来慢慢解释。以下是代码:1 /* 1.c */2 int main()3 {4 int foo[] = {1};5 int bar = 1;6 return 0;7 }使用 gcc 将其汇编并以 intel 格式输出汇编语言文件:1 gcc –S –masm=intel 1.c关键部分:1 mov DWORD PTR [esp+8], 12 mov DWORD PTR [esp+12], 1esp+8 位置就是那个 int foo[],esp+12 位置就是那个 int bar。可见,给 int 数组的赋值时就像给一个 int 变量赋值一样,并没用指针来进行间接访问,这个 int 数组对象 foo 的内存地址在编译时就确定了,是 esp+8;正如那个 int 对象 bar 一样,它的内存地址在编译时也确定了,是esp+12。以示区别,我将下面代码同样以汇编语言输出:1 /* 2.c */2 #include3 int main()4 {5 int *foo = (int *)malloc(sizeof (int));6 *foo = 1;7 return 0;8 }汇编的关键部分:1 mov DWORD PTR [esp], 42 call _malloc3 mov DWORD PTR [esp+28], eax4 mov eax, DWORD PTR [esp+28]5 mov DWORD PTR [eax], 1前两句为 foo分配内存空间,第三句将分配的内存空间地址值赋给 foo,foo 的地址为 esp+28,编译时已知。下面是赋值部分,首先从 foo 那里得到地址值,然后向这个地址赋值,这里可以看出和给数组赋值的差别,给数组赋值时是将值直接赋到了数组中,而不用从哪里得到数组的地址。由上面可以看出,数组更像一个普通的变量,编译时就知道了其地址,可以直接赋值。数组作为左值数组不能放在赋值号左边,但数组仍可以作为一个左值或者说对象出现在语句中,一个重要的例子就是取地址操作:&。取地址操作 &的操作数必须是一个左值,而不能是一个右值。比如一个变量int a = 1,&a 就可以得到 a 的地址,但 &1是非法的,一个单纯的数值是没有地址的。那么对于一个int foo[],&foo 会返回一个什么样的值呢?自然是一个指向数组的指针咯,下面的程序可以看出来:1 int main()2 {3 int foo[1];4 int bar[1];5 bar = &foo; //故意触发一个 error6 return 0;7 }那个赋值语句一定会触发错误,我们可以根据编译输出来确定它们的类型,错误为:1 error: incompatible types when assigning to type 'int[1]' from type 'int (*)[1]'没错,&foo 返回数据类型为 int (*)[1],就是一个指向数组的指针。指向数组?指向数组的哪里呢?指向数组对象首地址,正如一个指向 int 对象的指针指向那个 int 对象占有的两个或四个内存单元的首地址一样。把 &foo 赋给一个普通的指针是可以的,不过会触发一个 warning,因为int * 与 int (*)[1] 并不相容。赋值后普通指针的值与 &foo 的值是相同的,都是数组对象的首地址,只是普通指针把这块内存当做 int 对象处理而已。由于 C 语言是弱类型语言,你把 &foo 赋给int **********bar 或者 int *baz都是可以的,都不会导致 error,只会导致 warning,此时你打印出 *bar 或者 *baz 的值都是 foo 中第一个整数的值(前提是指针和数组占用空间大小相等)。正如文章开头的代码那样,以这个整数的值作为一个地址值进行间接访问(*(*bar))就会导致 非法访问的错误。数组作为右值数组作为右值时会发生什么?返回数组对象内的所有值自然不可能,因此 C 语言中采取的方法是数组作为右值时返回对象中元素类型的指针,指针指向第一个元素,类似上一个例子:1 int main()2 {3 int foo[1];4 int bar[1];5 bar = foo; //故意触发一个 error6 return 0;7 }出错信息为:1 error: incompatible types when assigning to type 'int[1]' from type 'int *'数组作为左值和数组作为右值时的区别造成了无数人的困惑与误解:foo 作为右值时确实等价于一个指针,因为数组无法像普通对象那样返回它的值,它的元素可能有成百上千个,但作为一个右值时——比如作为取地址操作符的操作数 时,数组就是作为一个数组对象而出现的,而不是指针,取地址返回一个指向数组的指针,而不是指向指针的指针。接下来再分析下数组指针和指针数组的区别:数组指针(也称行指针)定义 int (*p)[n];()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。如要将二维数组赋给一指针,应这样赋值:int a[3][4];int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。p=a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]所以数组指针也称指向一维数组的指针,亦称行指针。指针数组定义 int *p[n];[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针 类型的数组元素。这里执行p+1是错误的,这样赋值也是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n- 1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。如要将二维数组赋给一指针数组:int *p[3];int a[3][4];for(i=0;i<3;i++)p[i]=a[i];这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值。这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。比如要表示数组中i行j列一个元素:*(p[i]+j)、*(*(p+i)+j)、(*(p+i))[j]、p[i][j]综上所述一句话就是:数组就是数组,有着自己的特性。

    时间:2019-07-09 关键词: 指针

  • C和指针_第15章_输入输出函数_学习笔记

    1.错误报告    perror函数一种简单、统一的方式报告错误。标准库函数在一个外部整型变量errno(在errno.h中定义)中保存错误代码之后把这个信息传递给用户程序,提示操作失败的准确原因。perror函数简化向用户报告这些特定错误的过程。定义于stdio.h中:void perror( char const *message );    如果message不是NULL并且指向一个非空的字符串,perror函数将打印这个字符串,后面跟一个分号和一个空格,然后打印出一条用于解释errno当前错误代码的信息。    注意,只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改。这意味着我们不能通过测试errno的值来判断是否有错误发生。反之,只有当被调用函数提示有错误发生时检查errno的值才有意义。 2.终止执行void exit( int status );    status参数返回给操作系统,提示程序是否正常完成。预定义符号EXIT_SUCCESS和EXIT_FAILURE分别提示程序的终止是成功还是失败。经常会出现的是在调用perror函数之后再调用exit终止程序。 4.ANSI I/O概念    stdio.h包含FILE结构的声明,FILE是一个数据结构,用于访问一个流。编译器保证一个程序同时最多可以打开文件的数量至少为FOPEN_MAX。这个常量包括了三个标准流,它的值至少为8。 5.流总览  字符 getcharputchar 读取(写入)单个字符 文本行 getsscanfputsprintf文本行未格式化的输入(输出)格式化的输入(输出) 二进制数据 fread fwrite 读取(写入)二进制数据     斜体为函数家族,指一组函数中的每个都执行相同的基本任务,只是方式稍有不同。这些函数的区别在于获得输入的来源或输出写入的地方不同。这些变种用于执行下面任务:    1.只用于stdin或stdout    2.随作为参数的流使用    3.使用内存中的字符串而不是流 getchar 字符输入 fgets,getc getchar ① putchar 字符输出 fputc,putc putchar ① gets 文本行输入 fgets gets ② puts 文本行输出 fputs puts ② scanf 格式化输入 fscanf scanf sscanf printf 格式化输出 fprintf printf sprintf     ①对指针使用下标引用或间接访问操作从内存中获得一个字符(或向内存写入一个字符)。    ②使用strcpy函数从内存中读取文本行(或向内存中写入文本行) 6.打开流FILE *fopen( char const *name, char const *mode );    应该始终检查fopen函数的返回值。FILE *fptr; fptr = fopen( "data_name", "r" ); if( fptr == NULL ) {     perror("data_name");     exit(EXIT_FAILURE); }    freopen函数用于打开(重新打开)一个特定的文件流。原型如下:FILE *freopen( char const *name, char const *mode, FILE *stream );    最后一个参数就是需要打开的流。它可能是一个先前从fopen函数返回的流,也可能是标准流。这个函数首先试图关闭这个流,然后用指定的文件和模式重新打开这个流。如果打开失败,返回NULL。如果打开成功,函数返回其第3个参数。 7.关闭流    流是使用函数fclose关闭的,关闭前会刷新缓冲区。如果只选成功,返回0,否则返回EOF。函数原型如下:int fclose( FILE *stream );    只要操作成功后和操作失败后的执行指令不一样,就应该进行错误检查。#include#includeint main( int argc, char **argv) { int exit_status = EXIT_SUCCESS; FILE *input; //when the file not only one while(*++argv != NULL ) { //open the file input = fopent( *argv, "r" ); if( input == NULL ) { perror( *argv ); exit_status = EXIT_FAILURE; continue; } //deal with the opened file //close the file if( fclose( input ) != 0) { perror( "fclcose" ); exit( EXIT_FAILURE ); } } return EXIT_SUCCESS; } 8.字符I/O  int fgetc( FILE *stream ); int getc( FILE *stream ); int getchar( void );    getc和fgetc是从传入的操作流中读取,而getchar从标准输入流中读取。每个函数从流中读取下一个字符,并把它作为函数的返回值返回。如果流中不存在更多字符,函数返回常量值EOF。返回int值的真正原因是为了运行函数报告文件的末尾(EOF),如果返回值为char,则256个字符中一定有一个被指定为EOF,导致如果文件中出现这个字符,字符后的内容不会被读取。int fputc( int character, FILE *stream ); int putc( int character, FILE *stream ); int putchar( int character );    fgetc和fputc是真正的函数,但getc、putc、getchar和putchar都是通过#define指令定义的宏。8.2撤销字符I/O  int ungetc( int character, FILE *stream );    ungetc把一个先前读入的字符返回到流中,使其在以后可以被重新读入。每个流都允许至少一个字符被退回。如果一个流允许退回多个字符,那么这些字符再次被读取的顺序就以退回时的反序进行。注意把字符退回到流中和写入到流中并不相同。与一个流相关联的外部存储并不受ungetc的影响。    警告:“退回”字符和流的当前位置有关,如果用fseek、fsetpos或rewind函数改变了流的位置,所有退回的字符将被丢弃。 9.未格式化的行I/O    未格式化的行I/O简单读取或写入字符串char *fgets( char *buffer, int buffer_size, FILE *stream ); char *gets( char *buffer );    fgets从指定的stream流中读取字符并把它们复制到buffer(至少2个字节,因为最后一个需要用来放NUL)中。当他读到一个换行符并存储到缓冲区中后不再读取。如果缓冲区内存储的字符数达到buffer_size - 1个时也停止读取。下一次调用fgets将从流的下一个字符开始读取。在任何一种情况下,一个NUL字节将被添加到缓冲区所存储的数据的末尾,使其成为一个字符串。    如果在任何字符读取前就到达了文件尾,缓冲区就未进行修改,fgets函数返回一个NULL指针。否则,fgets就返回它的第一个参数(指向缓冲区的指针)。返回值通常只用于检查是否到达了文件尾。    gets和fgets主要不同在于读取一行输入时,并不在缓冲区中存储结尾的换行符。由于gets没有缓冲区参数,所以无法判断缓冲区长度。有可能缓冲区溢出。 int fputs( char const *buffer, FILE *stream ); int puts( char const *buffer );    传递给fputs的缓冲区必须包含一个字符串,它的字符被写入到流中。这个字符串预期以NUL字节结尾,这个字符串是逐字写入。写入出现错误时,fputs返回常量值EOF,否则返回一个非负值。    puts与fputs的主要不同在于写入一个字符串时,在字符串写入之后向输出再添加一个换行符。#include#define MAX_LINE_LENGTH 1024 void copylines( FILE *input, FILE *output ) { char buffer[MAX_LINE_LENGTH]; while( (fgets( buffer, MAX_LINE_LENGTH, input ) != EOF ) ) fputs( buffer, output ); } 10.格式化的行I/O    格式化的行I/O会执行数字和其他变量的内部和外部表示形式之间的转换。10.1 scanf函数家族int fscanf( FILE *stream, char const *format, ...); int scanf( char const *format, ...); int sscanf( char const *string, char const *format, ...);    原型中的省略号表示一个可变长度的指针列表。从输入源读取字符并根据format字符串给出的格式代码对其进行转换,将从输入转换而来的值逐个存储到这些指针参数所指向的内存位置。    当格式化字符串到达末尾或者读取的输入不在匹配格式字符串所指定的类型时,输入停止。被转换的参数数目作为函数的返回值返回。如果在任何输入值被转换前 文件就达到尾部,返回常量EOF。    scanf函数家族中的format字符串参数可能包含的内容有:空白字符——与输入中的0个或多个空白字符匹配,在处理过程中被忽略。 格式代码——指定函数如何解释接下来的输入字符 其他字符——任何其他字符出现在格式字符串时,下一个输入字符必须和它匹配。匹配后将被丢弃,不匹配函数不再读取直接返回。    scanf函数家族的格式代码都以一个%开头,后面可以是:一个可选的星号——星号将使转换后的值丢弃而不是存储。 一个可选的宽度——宽度以一个非负的整数给出,限制将被读取用于转换的输入字符的个数。如果未给出宽度,函数就连续读入字符直到遇见输入中的下一个空白字符。 一个可选的限定符 d,i,n short long   o,u,x unsigned short unsigned long   e,f,g   double long double     4.格式代码  c char * 读取和存储单个字符。前导的空白字符并不跳过。如果给出宽度,就读取和存储这个数目的字符。字符后面不会再添加一个NUL字节。参数必须指向一个足够大的字符数组id int * 一个可选的有符号整数被转换。d把输入解释为10进制数;i根据它的第1个字符决定值的基数,就像整型字面值常量的表达式一样。uox unsigned * 一个可选的有符号整数被转换,但它按照无符号数存储。如果使用u,值被解释为十进制数,如果使用0,值被解释为八进制数;如果使用x,值被解释为十六进制数。X与x同义efg float * 期待一个浮点值。它的形式必须像一个浮点型字面值常量,但小数点并非必需。E和G分别与e和g同义 s char * 读取一串非空白字符。参数必须指向一个足够大的字符数组。当发现空白输入时就停止,字符串后面会自动加上NUL终止符 [xxx] char * 根据给定组合的字符从输入中读取一串字符。参数必须指向一个足够大的字符数组。当遇到第1个不在给定组合中出现的字符时,输入就停止。字符串后面会自动加上NUL终止符。代码%[abc]表示字符组包括a、b和c。如果列表中以一个^字符开头,表示字符组合是所列出的字符的补集,所以%[^abc]表示字符组为a、b、c之外的所有字符。右方括号也可以出现在字符列表中,但它必须是列表的第1个字符。至于横杆是否用来指定某个范围的字符(例如%[a-z]),则因编译器而异 p void * 输入预期为一串字符,诸如那些由printf函数的%p格式代码所产生的输出。它的转换方式因编译器而异,但转换结果将和按照上面描述的进行打印所产生的字符的值是相同的 n int * d到目前为止通过这个scanf函数的调用从输入读取的字符数被返回。%n转换的字符并不计算在scanf函数的返回值之内。它本身并不消耗任何输入。 %   这个代码与输入中的一个%相匹配,该%符号将被丢弃。     fscanf中将换行符也当做空白字符跳过,所以在使用fscanf时,在输入中保持行边界的同步有困难。 10.3printf家族int fprintf( FILE *stream, char const *format, ... ); int printf( char const *format, ... ); int sprintf( char *buffer, char const *format, ... );    sprintf把它的结果作为一个NUL结尾的字符串存储到指定的buffer缓冲区,注意,缓冲区的大小不是sprintf的参数,所以易出现内存块越位,可以通过对格式进行分析,看看最大可能出现的值被转换后的输出结果将由多长来防止越位。格式代码以一个%开头,后面: 零个或多个标志字符,用于修改有些转换的执行方式 一个可选的最小字段宽度 一个可选的精度 一个可选的修改符 转换类型  c int 参数被裁剪为unsigned char类型并作为字符进行打印 di int 参数作为一个十进制数打印。如果给出精度而且值的位数小于精度位数,前面就用0填充 uox,X int 参数作为一个无符号值打印,u使用十进制,0使用八进制,x或X使用十六进制,两者区别在于x使用abcdef,X使用ABCDEF eE double 参数根据指数形式打印。小数点后面的位数由精度字段决定,默认是6 f double 参数按照常规的浮点格式打印。小数点后面的位数由精度字段决定,默认是6 gG double 如果指数≥-4但小于进度字段就用%f,否则使用指数格式 s char * 打印一个字符串 p void * 指针值被转换为一串因编译器而异的可打印字符。这个代码主要和scanf中的%p代码组合使用 n int * 独特代码,不产生任何输出。相反,到目前为止函数所产生的输出字符数目将被保存到对应的参数中 % (无) 打印一个%字符 - 值在字段中左对齐。默认情况是右对齐 0 d当数值是右对齐时,默认情况下是使用空格填充值左边未使用的列,这个标志表示用0填充。它可用于d,i,u,o,x,X,e,E,f,g和G代码。使用d,i,o,u,x和X代码时,如果给出精度,零标志就被忽略。如果格式代码中出现负号(-),零标志也没有效果 + 当用于一个格式化某个符号值的代码时,如果值非负,正号标志就会给它加上一个正号。如果该值为负,就像往常一样显示一个负号。默认情况下,正号并不会显示 空格 只用于转换有符号值的代码。当值非负时,这个标志把一个空格添加到它的开始位置。注意这个标志和正号标志是相互排斥的。如果两个同时给出,空格标志将被忽略 # 选择某些代码的另一种转换形式。 11.二进制I/O    把数据写到文件效率最高的方法就是用二进制写入。size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); size_t fwrite( void *buffer,size_t size, size_t count, FILE *stream );    buffer是一个指向保存数据的内存位置的指针,size是缓冲区每个元素的字节数,count是读取或写入的元素数。函数返回值是实际写入或读取的元素数目。 12.刷新和定位函数int fflush( FILE *stream );    fflush迫使一个输出流的缓冲区内的数据进行物理写入,不管缓冲区是否被写满。例如,为保证调试信息实际打印出来,使用fflush。long ftell( FILE *stream ); int fseek( FILE *stream, long offset, int from);    from可以是SEEK_SET、SEEK_CUR和SEEK_END。在二进制流中,从SEEK_END进行定位可能不被支持。在文本流中,如果from是SEEK_CUR或SEEK_END,offset必须是0.如果from是SEEK_SET,offset必须是一个从同一个流中以前调用ftell返回的值。  SEEK_SET 从流的起始位置起offset个字节,offset必须是一个非负值。 SEEK_CUR 从流的当前位置起offset个字节,offset的值可正可负 SEEK_END 从流的尾部位置起offset个字节,offset的值可正可负,如果为正,将定位到文件尾的后面     之所以存在这些限制,部分原因是文本流所执行的行末字符映射。由于这个映射的存在,文本文件的字节数可能和程序写入的字节数不同。因此,一个可移植的程序不能根据实际写入字符数的计算结果定位到文本流的一个位置。    用fseek改变一个流的位置会带来三个副作用: 行末指示符被清除 如果在fseek之前使用ungetc把一个字符返回到流中,那么这个被退回的字符会被丢弃,因为在定位操作后,它不再是“下一个字符” 定位允许从写入模式切换到读取模式,或者回到打开的流以便更新。void rewind( FILE *stream ); int fgetpos( FILE *stream, fpos_t *position ); int fsetpos( FILE *stream, fpos_t *position );    rewind函数将读/写指针设置回指定流的起始位置,同时清除流的错误提示标志。 13.改变缓冲方式    在流上执行的缓冲方式有时并不合适,下面两个函数可以用于对缓冲方式进行修改。这两个函数只有当指定的流被打开但还没有在它上面执行任何操作前才能被调用。void setbuf( FILE *stream, char *buf ); int setvbuf( FILE *stream, char *buf, int mode, size_t size );    setbuf设置了另一个数组,用于对流进行缓冲。这个数组的字符长度必须是BUFSIZ(定义于stdio.h)。为一个流自行指定缓冲区可以防止I/O函数库为它动态分配一个缓冲区。如果用一个NULL参数调用该函数,setbuf将关闭流的所有缓冲方式,字符准确地将程序所规划的方式进行读取和写入。    setvbuf函数中,mode参数用于指定缓冲的类型。_IOFBF指定一个完全缓冲的流,_IONBF指定一个不缓冲的流,_IOLBF指定一个行缓冲的流。所谓行缓冲,就是每当一个换行符写入到缓冲区,缓冲区就进行刷新。 14.流错误函数int feof( FILE *stream ); int ferror( FILE *stream ); void clearerr( FILE *stream );    如果流当前处于文件尾,feof函数返回真。这个状态可以通过对流执行fseek、rewind或fsetpos函数来清除。ferror函数报告流的错误状态,如果出现任何读/写错误函数就返回真。最后,clearerr函数对指定流的错误标志进行重置。 15.临时文件FILE *tmpfile( void );    这个函数创建一个文件,当文件被关闭或程序终止时,这个文件便自动删除。这个文件以wb+模式打开,这使它可用于二进制和文本数据。临时文件的名字可以用tmpnam函数创建,它的原型如下:char *tmpnam( char *name );    如果传递给函数的参数为NULL,那么这个函数便返回一个指向静态数组的指针,该数组包含了被创建的文件名。否则,参数便假定是一个指向长度至少为L_tmpnam的字符数组的指针。在这个情况下,文件名在这个数组中创建,返回值就是这个参数。 16.文件操纵函数int remove( char const *filename ); int rename( char const *oldname, char const *newname );    成功返回0值,失败返回非0值。  执行字符、文本行和二进制I/O的函数数据类型输入输出描述家族名目的可用于所有的流只用于stdin和stdout内存中的字符串格式码hlL代码参数含义代码参数含义标志含义from定位

    时间:2019-07-09 关键词: 指针 C语言

  • C和指针_第3章_数据_学习笔记

    1.typedeftypeof char *ptr_to_char; ptr_to_char a, b; //等价于 char *a , *b; //若如下则不然 #define d_ptr_to_char char* d_ptr_to_char a ,b; //等价于 char *a; char b;2.常量//关于指针和常量 int const *ptr;这是一个指向整型常量的指针。可以修改指针的值,但不能修改指针所指向的值。int * const ptr;申明ptr是一个指向整型的常量指针。指针的值为常量,无法修改。但其指向的整型值可修改。int const * const ptr;无论是指针还是指针指向的值均为常量,不能修改。3.作用域文件作用域、函数作用域、代码块作用域和原型作用域。其中一对花括号之间的所有语句成为一个代码块。4.链接属性external、internal和nonetypedef char *a; int b; int c(int d) {     int e;     int f(int g ); }默认状态下,标识符b,c,f的链接属性为external。代码块外变量默认为external,函数声名默认具有external属性。关键字exter和static可以在声明中修改标识符的链接属性。static只对默认链接属性为exter的声明才具有改变链接属性的效果。5.存储类型普通内存、运行时栈、硬件寄存器。凡在任何代码块之外声明的变量总存储于静态内存中,也就是不属于堆栈的内存,静态变量(static)。若不设置初始值,默认0.在代码块内部声明的变量的默认存储类型为automatic,若不设置初始值,值未知。注意:    static:        当其用于函数定义或者用于代码块之外的变量声明时,值用于改变标识符的链接属性,不改变其存储类型和作用域。        当其用于代码块内部的变量声明时,用于修改变量的存储类型。全局所有代码块之外否从声明处至文件尾不允许从其他源文件访问局部代码块起始处是整个代码块变量不存储于堆栈中,它的值在程序整个执行期一直保持形式参数函数头部是整个函数不允许6.编程提示    6.1 为了保持最佳的可移植性,把字符的值限制在有符号和无符号字符范围的交集之内,或者不要在字符上执行算数运算。    6.2 用它们在使用时最自然的形式来表示整型常量。    6.3  不要把整型值和枚举值混用。    6.4 不要依赖隐式声明。    6.5 在定义类型的新名字时,采用typedef而不是#define    6.6 用const声明其值不会修改的变量。    6.7 使用名字常量而不是整型常量。    6.8 不要在嵌套的代码块使用相同的变量名。    6.9 除了实体的具体定义位置之外,在它的其他声明位置都使用extern关键字。作用域、链接属和存储类型总结变量类型 声明位置是否存在于堆栈作用域如果申明为static

    时间:2019-07-09 关键词: c 指针

  • 彻底搞定C指针-函数名与函数指针

    函数名与函数指针一 通常的函数调用    一个通常的函数调用的例子://自行包含头文件void MyFun(int x);    //此处的申明也可写成:void MyFun( int );int main(int argc, char* argv[]){   MyFun(10);     //这里是调用MyFun(10);函数      return 0;}void MyFun(int x)  //这里定义一个MyFun函数{   printf(“%dn”,x);}    这个MyFun函数是一个无返回值的函数,它并不完成什么事情。这种调用函数的格式你应该是很熟悉的吧!看主函数中调用MyFun函数的书写格式:MyFun(10);    我们一开始只是从功能上或者说从数学意义上理解MyFun这个函数,知道MyFun函数名代表的是一个功能(或是说一段代码)。    直到——    学习到函数指针概念时。我才不得不在思考:函数名到底又是什么东西呢?    (不要以为这是没有什么意义的事噢!呵呵,继续往下看你就知道了。)二 函数指针变量的申明    就象某一数据变量的内存地址可以存储在相应的指针变量中一样,函数的首地址也以存储在某个函数指针变量里的。这样,我就可以通过这个函数指针变量来调用所指向的函数了。    在C系列语言中,任何一个变量,总是要先申明,之后才能使用的。那么,函数指针变量也应该要先申明吧?那又是如何来申明呢?以上面的例子为例,我来申明一个可以指向MyFun函数的函数指针变量FunP。下面就是申明FunP变量的方法:void (*FunP)(int) ;   //也可写成void (*FunP)(int x);    你看,整个函数指针变量的申明格式如同函数MyFun的申明处一样,只不过——我们把MyFun改成(*FunP)而已,这样就有了一个能指向MyFun函数的指针FunP了。(当然,这个FunP指针变量也可以指向所有其它具有相同参数及返回值的函数了。)三 通过函数指针变量调用函数    有了FunP指针变量后,我们就可以对它赋值指向MyFun,然后通过FunP来调用MyFun函数了。看我如何通过FunP指针变量来调用MyFun函数的://自行包含头文件void MyFun(int x);    //这个申明也可写成:void MyFun( int );void (*FunP)(int );   //也可申明成void(*FunP)(int x),但习惯上一般不这样。int main(int argc, char* argv[]){   MyFun(10);     //这是直接调用MyFun函数   FunP=&MyFun;  //将MyFun函数的地址赋给FunP变量   (*FunP)(20);    //这是通过函数指针变量FunP来调用MyFun函数的。}void MyFun(int x)  //这里定义一个MyFun函数{   printf(“%dn”,x);}    请看黑体字部分的代码及注释。     运行看看。嗯,不错,程序运行得很好。    哦,我的感觉是:MyFun与FunP的类型关系类似于int 与int *的关系。函数MyFun好像是一个如int的变量(或常量),而FunP则像一个如int *一样的指针变量。int i,*pi;pi=&i;    //与FunP=&MyFun比较。    (你的感觉呢?)    呵呵,其实不然——四 调用函数的其它书写格式函数指针也可如下使用,来完成同样的事情://自行包含头文件void MyFun(int x);    void (*FunP)(int );    //申明一个用以指向同样参数,返回值函数的指针变量。int main(int argc, char* argv[]){   MyFun(10);     //这里是调用MyFun(10);函数   FunP=MyFun;  //将MyFun函数的地址赋给FunP变量   FunP(20);    //这是通过函数指针变量来调用MyFun函数的。      return 0;}void MyFun(int x)  //这里定义一个MyFun函数{   printf(“%dn”,x);}我改了黑体字部分(请自行与之前的代码比较一下)。运行试试,啊!一样地成功。咦?FunP=MyFun;可以这样将MyFun值同赋值给FunP,难道MyFun与FunP是同一数据类型(即如同的int 与int的关系),而不是如同int 与int*的关系了?(有没有一点点的糊涂了?)看来与之前的代码有点矛盾了,是吧!所以我说嘛!请容许我暂不给你解释,继续看以下几种情况(这些可都是可以正确运行的代码哟!):代码之三:int main(int argc, char* argv[]){   MyFun(10);     //这里是调用MyFun(10);函数   FunP=&MyFun;  //将MyFun函数的地址赋给FunP变量   FunP(20);    //这是通过函数指针变量来调用MyFun函数的。      return 0;}代码之四:int main(int argc, char* argv[]){   MyFun(10);     //这里是调用MyFun(10);函数   FunP=MyFun;  //将MyFun函数的地址赋给FunP变量   (*FunP)(20);    //这是通过函数指针变量来调用MyFun函数的。      return 0;}真的是可以这样的噢!(哇!真是要晕倒了!)还有呐!看——int main(int argc, char* argv[]){   (*MyFun)(10);     //看,函数名MyFun也可以有这样的调用格式      return 0;}你也许第一次见到吧:函数名调用也可以是这样写的啊!(只不过我们平常没有这样书写罢了。)那么,这些又说明了什么呢?呵呵!假使我是“福尔摩斯”,依据以往的知识和经验来推理本篇的“新发现”,必定会由此分析并推断出以下的结论:1. 其实,MyFun的函数名与FunP函数指针都是一样的,即都是函数指针。MyFun函数名是一个函数指针常量,而FunP是一个函数数指针变量,这是它们的关系。2. 但函数名调用如果都得如(*MyFun)(10);这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许MyFun(10);这种形式地调用(这样方便多了并与数学中的函数形式一样,不是吗?)。3. 为统一起见,FunP函数指针变量也可以FunP(10)的形式来调用。4. 赋值时,即可FunP=&MyFun形式,也可FunP=MyFun。上述代码的写法,随便你爱怎么着!请这样理解吧!这可是有助于你对函数指针的应用喽!最后——补充说明一点:在函数的申明处:void MyFun(int );    //不能写成void (*MyFun)(int )。void (*FunP)(int );   //不能写成void FunP(int )。(请看注释)这一点是要注意的。五 定义某一函数的指针类型:就像自定义数据类型一样,我们也可以先定义一个函数指针类型,然后再用这个类型来申明函数指针变量。我先给你一个自定义数据类型的例子。typedef int* PINT;    //为int* 类型定义了一个PINT的别名int main(){  int x;  PINT px=&x;   //与int * px=&x;是等价的。PINT类型其实就是int * 类型  *px=10;       //px就是int*类型的变量    return 0;}根据注释,应该不难看懂吧!(虽然你可能很少这样定义使用,但以后学习Win32编程时会经常见到的。)下面我们来看一下函数指针类型的定义及使用:(请与上对照!)//自行包含头文件void MyFun(int x);    //此处的申明也可写成:void MyFun( int );typedef void (*FunType)(int );   //这样只是定义一个函数指针类型FunType FunP;              //然后用FunType类型来申明全局FunP变量int main(int argc, char* argv[]){//FunType FunP;    //函数指针变量当然也是可以是局部的 ,那就请在这里申明了。    MyFun(10);        FunP=&MyFun;     (*FunP)(20);          return 0;}void MyFun(int x)  {   printf(“%dn”,x);}看黑体部分:首先,在void (*FunType)(int ); 前加了一个typedef 。这样只是定义一个名为FunType函数指针类型,而不是一个FunType变量。然后,FunType FunP;  这句就如PINT px;一样地申明一个FunP变量。其它相同。整个程序完成了相同的事。这样做法的好处是:有了FunType类型后,我们就可以同样地、很方便地用FunType类型来申明多个同类型的函数指针变量了。如下:FunType FunP2;FunType FunP3;//……六 函数指针作为某个函数的参数既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。所以,你还应知道函数指针是如何作为某个函数的参数来传递使用的。给你一个实例:要求:我要设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。实现:代码如下://自行包含头文件 void MyFun1(int x);  void MyFun2(int x);  void MyFun3(int x);  typedef void (*FunType)(int ); //②. 定义一个函数指针类型FunType,与①函数类型一至void CallMyFun(FunType fp,int x);int main(int argc, char* argv[]){   CallMyFun(MyFun1,10);   //⑤. 通过CallMyFun函数分别调用三个不同的函数   CallMyFun(MyFun2,20);      CallMyFun(MyFun3,30);   }void CallMyFun(FunType fp,int x) //③. 参数fp的类型是FunType。{  fp(x);//④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的}void MyFun1(int x) // ①. 这是个有一个参数的函数,以下两个函数也相同{   printf(“函数MyFun1中输出:%dn”,x);}void MyFun2(int x)  {   printf(“函数MyFun2中输出:%dn”,x);}void MyFun3(int x)  {   printf(“函数MyFun3中输出:%dn”,x);}输出结果:略分析:(看我写的注释。你可按我注释的①②③④⑤顺序自行分析。)

    时间:2019-07-09 关键词: 指针 C语言

  • C和指针_第13章_高级指针话题_学习笔记

    2.高级声明int *func(); int (*func)(); int *arr[]; int (*func[])();    第1行声明一个返回值为int型指针的函数。()优先级高于间接访问操作符*。    第2行的第2对括号是函数调用操作符,但第1对括号只起到聚类的作用。它迫使间接访问在函数调用之前进行,使func成为一个函数指针,它所指向的函数返回一个int值。    第3行声明一个数组,元素类型是指向整型的指针。    第4行func是一个数组,数组元素的类型是函数指针,其指向的函数返回值是一个int值。3.函数指针    函数指针常见的用途有转换表(jump table)和作为参数传递给另一个函数。对函数指针执行间接访问之前必须把它初始化为指向某个函数。int func( int ); int (*pf)( int ) = &func; int ans; ans = func(25); ans = (*pf)( 25 ); ans = pf( 25 );    调用函数时的执行过程如:首先函数名func被转换成一个函数指针,该指针指定函数在内存中的位置。然后函数调用操作符调用该函数,执行开始于这个地址的代码。所以三个示例效果一样。3.1回调函数int (*compare_ints)( void const *a, void const *b) { if( *(int *)a == *(int *)b) return 0; else return 1; } Node *search_list( Node *node, void const *value, int (*compare)( void const *, void const *)) { while( node != NULL) { if( compare( &node->value, value ) == 0) break; node = node->link; } return node; } desired_node = search_list( root, &desired_value, compare_ints);    函数search_list的第3个参数是一个函数指针。这个参数用一个完整的原型进行声明。node若被声明为const,函数将不得不返回一个const结果,这将限制调用函数,它便无法查找函数所找到的节点。desired_node = search_list( root, &desired_value, strcmp );    若链表是字符串链表,则上述代码可以完成比较。3.2转移表    程序其他部分读入两个数(op1和op2)和一个操作符。switch( oper ) { case ADD: result = add( op1, op2 ); break; case SUB: result = sub( op1, op2 ); break; case MUL: result = mul( op1, op2 ); break case DIV: result = div( op1, op2 ); break; ... }    采用调用函数来执行这些操作可以体现一种良好的设计方案,即把具体操作和选择操作的代码分开。double add( double, double ); double sub( double, double ); double mul( double, double ); double div( double, double ); ... double (*oper_func[])( double, double ) = { add, sub, mul, div, ... }4.命令行函数int main( int argc, char **argv );5.字符串常量    当一个字符串常量出现在表达式中时,它的值是个指针常量(常量么?)。编译器把这些指定字符的一份拷贝存储在内存的某个位置,并存储一个指向第1个字符的指针。所以,"xyz" + 1    上面这行代码的意义是计算“指针值加上1”。结果是一个指针,指向字符串中第2个字符:y。*"xyz"    上面这个间接访问的结果就是它指向的字符:x。"xyz"[2]    上面这个表达式也是正确的。因为当数组名用于表达式中时,其值为常量指针。remainder = value % 16; if( remainder < 10)     putchar( remainder + '0' ); else     putchar( remainder - 10 + 'A');    上面代码与下面代码实现相同的功能。putchar( "0123456789ABCDEF"[remainder % 16] );void binary_to_ascii( unsigned int value ) {     unsigned int quotient;     quotient = value / 10;     if( quotient != 0)         binary_to_ascii(quotient);     putchar( value % 10 + '0'); }

    时间:2019-07-09 关键词: 指针 C语言

  • C和指针_第12章_使用结构和指针_学习笔记

    2.单列表插入函数示例#include#includetypedef struct Node{ struct Node *link; int value; }Node; int sll_insert( register Node **rootp, int new_value ) { register Node *current; register Node *new; //1.注意链表是否到尾部         //2.理解每个结构体均有一个指针指向该结构体,所以只需要一个指向当前节点的指针和一个指向“当前节点的link字段”的指针 while( ( current = *rootp ) != NULL && current->value < new_value ) rootp = &current->link; new = (Node *)malloc( sizeof( Node ) ); if( new == NULL ) return 0; else new->value = new_value; new->link = current; *rootp = new; return 1; }    以上代码为最终修改和简化后代码,修改和简化有如下几点:    1.函数不能越过链表尾部,所以采用判断current值是否为空。防止越位    2.函数不能处理头指针,所以采用将头指针作为一个参数传递给函数,即使用Node **而不是Node *。    3.为消除把节点插入链表起始位置作为特殊情况来处理的情况,采用linkp = &current->link来简化,此时linkp指向的是指向结构的link字段。只需2个指针而不是3个。    4.由于循环之前的最后一条语句和循环之前的语句相同,将current的赋值嵌入到while表达式中。消除current的冗余赋值。3.双向链表#include#includetypedef struct Node{ struct Node *fwk; struct Node *bwk; int value; }Node; int sll_insert( Node **rootp, int new_value ) { Node *this; Node *next; Node *newNode; for( this = *rootp ;( next = this->fwk ) != NULL; this = next ) { if( next->value == new_value ) return 0; if( next->value > new_value ) break; } newNode = (Node *)malloc( sizeof( Node ) ); if( newNode == NULL ) return -1; else newNode->value = new_value; if( next != NULL) { if( this != rootp ) { newNode->bwk = this; newNode->fwk = next; this->fwk = newNode; next->bwk = newNode; } else { newNode->bwk = NULL; newNode->fwk = next; rootp->fwk = newNode; next->bwk = newNode; } } else { if( this != rootp ) { newNode->bwk = this; newNode->fwk = NULL; this->fwk = newNode; rootp->bwk = newNode; } else { newNode->bwk = NULL; newNode->fwk = NULL; rootp->fwk = newNode; rootp->bwk = newNode; } } return 1; }    简化插入函数: if( next != NULL) { newNode->fwk = next; if( this != rootp ) { newNode->bwk = this; this->fwk = newNode; } else { newNode->bwk = NULL; rootp->fwk = newNode; } next->bwk = newNode; } else { newNode->fwk = NULL; if( this != rootp ) { newNode->bwk = this; this->fwk = newNode; } else { newNode->bwk = NULL; rootp->fwk = newNode; } rootp->bwk = newNode; }    再一步简化: newNode->fwk = next; if( this != rootp ) { newNode->bwk = this; this->fwk = newNode; } else { newNode->bwk = NULL; rootp->fwk = newNode; } if( next != NULL) next->bwk = newNode; else rootp->bwk = newNode;    再简化:int sll_insert( register Node **rootp, int new_value ) { register Node *this; register Node *next; register Node *newNode; for( this = *rootp ;( next = this->fwk ) != NULL; this = next ) { if( next->value == new_value ) return 0; if( next->value > new_value ) break; } newNode = (Node *)malloc( sizeof( Node ) ); if( newNode == NULL ) return -1; else newNode->value = new_value; newNode->fwk = next; this->fwk = newNode; if( this != rootp ) newNode->bwk = this; else newNode->bwk = NULL; if( next != NULL) next->bwk = newNode; else rootp->bwk = newNode; return 1; }    倘若丧心病狂,那么如下定是极好的: newNode->fwk = next; this->fwk = newNode; newNode->bwk = ( this != rootp) ? this : NULL; ( next != NULL ? next : rootp )->bwk = newNode总结:    1.消除特殊情况使代码易于维护。    2.通过提炼语句消除if中的重复语句。    3.不要仅仅根据代码的大小评估其质量。

    时间:2019-07-09 关键词: 指针 C语言

  • 一个简单的指针指向问题的讨论

    #include #include #include typedef struct node{ int data; struct node * next; }Link; int main(void) { Link l[3]; //建立三个结点,分别存储1,2,3和自己的地址 for(int i=0;i

    时间:2019-07-05 关键词: 指针 C语言

  • 干货 || 对于C语言指针最详尽的讲解

    干货 || 对于C语言指针最详尽的讲解

    指针对于C来说太重要。然而,想要全面理解指针,除了要对C语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识。所以本文尽可能的通过一篇文章完全讲解指针。为什么需要指针?指针解决了一些编程中基本的问题。第一,指针的使用使得不同区域的代码可以轻易的共享内存数据。当然小伙伴们也可以通过数据的复制达到相同的效果,但是这样往往效率不太好。因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)。 第二,指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树等等。 第三,有些操作必须使用指针。如操作申请的堆内存。还有:C语言中的一切函数调用中,值传递都是“按值传递”的。如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成。指针是什么?我们知道:C语言中的数组是指一类类型,数组具体区分为  int 类型数组,double类型数组,char数组 等等。同样指针这个概念也泛指一类数据类型,int指针类型,double指针类型,char指针类型等等。通常,我们用int类型保存一些整型的数据,如 int num = 97 , 我们也会用char来存储字符:char ch = 'a'。我们也必须知道:任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。 为什么程序中的数据会有自己的地址?弄清这个问题我们需要从操作系统的角度去认知内存。电脑维修师傅眼中的内存是这样的:内存在物理上是由一组DRAM芯片组成的。 而作为一个程序员,我们不需要了解内存的物理结构,操作系统将RAM等硬件和软件结合起来,给程序员提供的一种对内存使用的抽象。这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。在程序员眼中的内存应该是下面这样的。也就是说,内存是一个很大的,线性的字节数组(平坦寻址)。每一个字节都是固定的大小,由8个二进制位组成。最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。如上图中,这是一个256M的内存,他一共有256x1024x1024  = 268435456个字节,那么它的地址范围就是 0 ~268435455  。 由于内存中的每一个字节都有一个唯一的编号。因此,在程序中使用的变量,常量,甚至数函数等数据,当他们被载入到内存中后,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。下面用代码说明#include <stdio.h> int main(void) {     char ch = 'a';        int  num = 97;           printf("ch 的地址:%p",&ch);             //ch 的地址:0028FF47              printf("num的地址:%p",&num);               //num的地址:0028FF40                 return 0; }指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值)使用一个机器字的大小来存储。也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~2w - 1 ,程序最多能访问2w个字节。这就是为什么xp这种32位系统最大支持4GB内存的原因了。我们可以大致画出变量ch和num在内存模型中的存储。(假设 char占1个字节,int占4字节) 变量和内存为了简单起见,这里就用上面例子中的  int num = 97 这个局部变量来分析变量在内存中的存储模型。 已知:num的类型是int,占用了4个字节的内存空间,其值是97,地址是0028FF40。我们从以下几个方面去分析。 1、内存的数据内存的数据就是变量的值对应的二进制,一切都是二进制。97的二进制是 : 00000000 00000000 00000000 0110000 , 但使用的小端模式存储时,低位数据存放在低地址,所以图中画的时候是倒过来的。 2、内存数据的类型内存的数据类型决定了这个数据占用的字节数,以及计算机将如何解释这些字节。num的类型是int,因此将被解释为 一个整数。 3、内存数据的名称内存的名称就是变量名。实质上,内存数据都是以地址来标识的,根本没有内存的名称这个说法,这只是高级语言提供的抽象机制 ,方便我们操作内存数据。而且在C语言中,并不是所有的内存数据都有名称,例如使用malloc申请的堆内存就没有。 4、内存数据的地址如果一个类型占用的字节数大于1,则其变量的地址就是地址值最小的那个字节的地址。因此num的地址是 0028FF40。内存的地址用于标识这个内存块。 5、内存数据的生命周期num是main函数中的局部变量,因此当main函数被启动时,它被分配于栈内存上,当main执行结束时,消亡。    如果一个数据一直占用着他的内存,那么我们就说他是“活着的”,如果他占用的内存被回收了,则这个数据就“消亡了”。C语言中的程序数据会按照他们定义的位置,数据的种类,修饰的关键字等因素,决定他们的生命周期特性。实质上我们程序使用的内存会被逻辑上划分为:栈区,堆区,静态数据区,方法区。不同的区域的数据有不同的生命周期。无论以后计算机硬件如何发展,内存容量都是有限的,因此清楚理解程序中每一个程序数据的生命周期是非常重要的。指针变量和指向关系用来保存指针的变量,就是指针变量。如果指针变量p1保存了变量 num的地址,则就说:p1指向了变量num,也可以说p1指向了num所在的内存块 ,这种指向关系,在图中一般用 箭头表示。 上图中,指针变量p1指向了num所在的内存块 ,即从地址0028FF40开始的4个byte 的内存块。 定义指针变量 C语言中,定义变量时,在变量名前写一个 * 星号,这个变量就变成了对应变量类型的指针变量。必要时要加( ) 来避免优先级的问题。 引申:C语言中,定义变量时,在定义的最前面写上typedef ,那么这个变量名就成了一种类型,即这个类型的同义词。int a ; //int类型变量 aint *a ; //int* 变量aint arr[3]; //arr是包含3个int元素的数组int (* arr )[3]; //arr是一个指向包含3个int元素的数组的指针变量//-----------------各种类型的指针------------------------------int* p_int; //指向int类型变量的指针 double* p_double; //指向idouble类型变量的指针 struct Student *p_struct; //结构体类型的指针int(*p_func)(int,int); //指向返回类型为int,有2个int形参的函数的指针 int(*p_arr)[3]; //指向含有3个int元素的数组的指针 int** p_pointer; //指向 一个整形变量指针的指针 取地址既然有了指针变量,那就得让他保存其它变量的地址,使用& 运算符取得一个变量的地址。int add(int a , int b){    return a + b;}int main(void){    int num = 97;    float score = 10.00F;    int arr[3] = {1,2,3};    //-----------------------    int* p_num = &num;    float* p_score = &score;    int (*p_arr)[3] = &arr;              int (*fp_add)(int ,int )  = add;  //p_add是指向函数add的函数指针    return 0;} 特殊的情况,他们并不一定需要使用&取地址:数组名的值就是这个数组的第一个元素的地址。函数名的值就是这个函数的地址。字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址。int add(int a , int b) {     return a + b; } int main(void) { int arr[3] = {1,2,3};//----------------------- int* p_first = arr; int (*fp_add)(int ,int )  =  add; const char* msg = "Hello world"; return 0; } 解地址我们需要一个数据的指针变量干什么?当然使用通过它来操作(读/写)它指向的数据啦。对一个指针解地址,就可以取到这个内存数据,解地址的写法,就是在指针的前面加一个*号。解指针的实质是:从指针指向的内存块中取出这个内存数据。int main(void){    int age = 19;    int*p_age = &age;    *p_age  = 20;  //通过指针修改指向的内存数据    printf("age = %d",*p_age);   //通过指针读取指向的内存数据    printf("age = %d",age);    return 0;} 指针之间的赋值指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。int* p1  = & num;int* p3 = p1;//通过指针 p1 、 p3 都可以对内存数据 num 进行读写,如果2个函数分别使用了p1 和p3,那么这2个函数就共享了数据num。空指针指向空,或者说不指向任何东西。在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是 ((void*)0) ,  在C++中,NULL实质是0。换种说法:任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。 下面代码摘自 stdlib.h#ifdef __cplusplus     #define NULL    0#else         #define NULL    ((void *)0)#endif 坏指针 指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是坏指针。不能对他们做解指针操作,否则程序会出现运行时错误,导致程序意外终止。任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。坏指针是造成C语言Bug的最频繁的原因之一。下面的代码就是错误的示例。void opp(){     int*p = NULL;     *p = 10;      //Oops! 不能对NULL解地址}void foo(){     int*p;     *p = 10;      //Oops! 不能对一个未知的地址解地址}void bar(){     int*p = (int*)1000;     *p =10;      //Oops!   不能对一个可能不属于本程序的内存的地址的指针解地址}指针的2个重要属性指针也是一种数据,指针变量也是一种变量,因此指针 这种数据也符合前面变量和内存主题中的特性。这里要强调2个属性:指针的类型,指针的值。int main(void){    int num = 97;    int *p1  = &num;    char* p2 = (char*)(&num);    printf("%d",*p1);    //输出  97    putchar(*p2);          //输出  a    return 0;} 指针的值:很好理解,如上面的num 变量 ,其地址的值就是0028FF40 ,因此 p1的值就是0028FF40。数据的地址用于在内存中定位和标识这个数据,因为任何2个内存不重叠的不同数据的地址都是不同的。指针的类型:指针的类型决定了这个指针指向的内存的字节数并如何解释这些字节信息。一般指针变量的类型要和它指向的数据的类型匹配。 由于num的地址是0028FF40,因此 p1 和 p2 的值都是0028FF40*p1  :  将从地址0028FF40 开始解析,因为p1是int类型指针,int占4字节,因此向后连续取4个字节,并将这4个字节的二进制数据解析为一个整数 97。*p2  :  将从地址0028FF40 开始解析,因为p2是char类型指针,char占1字节,因此向后连续取1个字节,并将这1个字节的二进制数据解析为一个字符,即'a'。 同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据。 void*类型指针 由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址。如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。因为,编译器不允许直接对void*类型的指针做解指针操作。结构体和指针结构体指针有特殊的语法:-> 符号如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员typedef struct{    char name[31];    int age;    float score;}Student;int main(void){    Student stu = {"Bob" , 19, 98.0};    Student*ps = &stu;    ps->age = 20;    ps->score = 99.0;    printf("name:%s age:%d",ps->name,ps->age);    return 0;}数组和指针1、数组名作为右值的时候,就是第一个元素的地址。int main(void){    int arr[3] = {1,2,3};    int*p_first = arr;    printf("%d",*p_first);  //1    return 0;} 2、指向数组元素的指针 支持 递增 递减 运算。(实质上所有指针都支持递增递减 运算 ,但只有在数组中使用才是有意义的)int main(void){    int arr[3] = {1,2,3};    int*p = arr;    for(;p!=arr+3;p++){        printf("%d",*p);    }    return 0;}3、p= p+1 意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。同一个数组中,元素的指针之间可以做减法运算,此时,指针之差等于下标之差。 4、p[n]    == *(p+n)     p[n][m]  == *(  *(p+n)+ m ) 5、当对数组名使用sizeof时,返回的是整个数组占用的内存字节数。当把数组名赋值给一个指针后,再对指针使用sizeof运算符,返回的是指针的大小。 这就是为什么将一个数组传递给一个函数时,需要另外用一个参数传递数组元素个数的原因了。int main(void){    int arr[3] = {1,2,3};    int*p = arr;    printf("sizeof(arr)=%d",sizeof(arr));  //sizeof(arr)=12    printf("sizeof(p)=%d",sizeof(p));   //sizeof(p)=4    return 0;}函数和指针函数的参数和指针C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。void change(int a) {     a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。 } int main(void) {     int age = 19; change(age); printf("age = %d",age);   // age = 19     return 0; } 有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的。但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。 传递变量的指针可以轻松解决上述问题。 void change(int* pa) {     (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,               //会直接去内存中找到age这个数据,然后把它增1。    }    int main(void)    {        int age = 19;    change(&age);           printf("age = %d",age);// age = 20               return 0;           } 再来一个老生常谈的,用函数交换2个变量的值的例子:#include<stdio.h>void swap_bad(int a,int b);void swap_ok(int*pa,int*pb);int main(){    int a = 5;    int b = 3;    swap_bad(a,b);       //Can`t swap;    swap_ok(&a,&b);      //OK    return 0;}//错误的写法void swap_bad(int a,int b){    int t;    t=a;    a=b;    b=t;}//正确的写法:通过指针void swap_ok(int*pa,int*pb){    int t;    t=*pa;    *pa=*pb;    *pb=t;} 有的时候,我们通过指针传递数据给函数不是为了在函数中改变他指向的对象。相反,我们防止这个目标数据被改变。传递指针只是为了避免拷贝大型数据。考虑一个结构体类型Student。我们通过show函数输出Student变量的数据。 typedef struct{    char name[31];    int age;    float score;}Student;//打印Student变量信息void show(const Student * ps){    printf("name:%s , age:%d , score:%.2f",ps->name,ps->age,ps->score);   } 我们只是在show函数中取读Student变量的信息,而不会去修改它,为了防止意外修改,我们使用了常量指针去约束。另外我们为什么要使用指针而不是直接传递Student变量呢?从定义的结构看出,Student变量的大小至少是39个字节,那么通过函数直接传递变量,实参赋值数据给形参需要拷贝至少39个字节的数据,极不高效。而传递变量的指针却快很多,因为在同一个平台下,无论什么类型的指针大小都是固定的:X86指针4字节,X64指针8字节,远远比一个Student结构体变量小。函数的指针 每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。C语言中,函数名作为右值时,就是这个函数的指针。 void echo(const char *msg){    printf("%s",msg);}int main(void){    void(*p)(const char*) = echo;   //函数指针变量指向echo这个函数    p("Hello ");      //通过函数的指针p调用函数,等价于echo("Hello ")    echo("World");    return 0;}const和指针const到底修饰谁?谁才是不变的?如果const 后面是一个类型,则跳过最近的原子类型,修饰后面的数据。(原子类型是不可再分割的类型,如int, short , char,以及typedef包装后的类型)如果const后面就是一个数据,则直接修饰这个数据。int main(){    int a = 1;    int const *p1 = &a;        //const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值    const int*p2 =  &a;        //const后面是int类型,则跳过int ,修饰*p2, 效果同上    int* const p3 = NULL;      //const后面是数据p3。也就是指针p3本身是const .    const int* const p4 = &a;  // 通过p4不能改变a 的值,同时p4本身也是 const    int const* const p5 = &a;  //效果同上    return 0;} typedef int* pint_t;  //将 int* 类型 包装为 pint_t,则pint_t 现在是一个完整的原子类型int main(){    int a  = 1;    const pint_t p1 = &a;  //同样,const跳过类型pint_t,修饰p1,指针p1本身是const    pint_t const p2 = &a;  //const 直接修饰p,同上    return 0;}深拷贝和浅拷贝如果2个程序单元(例如2个函数)是通过拷贝他们所共享的数据的指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互不受影响,则叫做深拷贝。 附加知识指针和引用这个2个名词的区别。他们本质上来说是同样的东西。指针常用在C语言中,而引用,则用于诸如Java,C#等 在语言层面封装了对指针的直接操作的编程语言中。大端模式和小端模式 1) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。个人PC常用,Intel X86处理器是小端模式。2) B i g-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。有些机器同时支持大端和小端模式,通过配置来设定实际的端模式。 假如 short类型占用2个字节,且存储的地址为0x30。short a = 1; 如下图:   //测试机器使用的是否为小端模式。是,则返回true,否则返回false//这个方法判别的依据就是:C语言中一个对象的地址就是这个对象占用的字节中,地址值最小的那个字节的地址。bool isSmallIndain(){      unsigned int val = 'A';      unsigned char* p = (unsigned char*)&val;  //C/C++:对于多字节数据,取地址是取的数据对象的第一个字节的地址,也就是数据的低地址      return *p == 'A';}

    时间:2019-05-20 关键词: 指针 C语言

首页  上一页  1 2 3 4 5 下一页 尾页
发布文章

技术子站