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

    作者: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

  • Power Integrations推出采用BridgeSwitch IC作为驱动的单相无刷直流电机精调控制软件

    Power Integrations推出采用BridgeSwitch IC作为驱动的单相无刷直流电机精调控制软件

    深耕于高压集成电路高能效功率变换领域的知名公司Power Integrations(纳斯达克股票代号:POWI)今日发布Motor-Expert软件,这是一款嵌入C语言应用程序、库及控制GUI的软件。使用该公司BridgeSwitch无刷直流(BLDC)电机驱动器IC的设计人员借助该软件可对单相电机的运行性能进行精确的控制和调整。无刷直流电机广泛用于现代高效家电应用,如家电中的压缩机、风扇和水泵,以及吊扇和室内空调系统。 无刷直流电机通常使用三个绕组(多相),需要六个高压IGBT或MOSFET才能运行。Motor-Expert软件支持具有成本效益的单相电机架构,可大幅缩减高压器件的数量及相关成本、降低系统复杂度并减轻库存负担。它同时支持有传感器和无传感器的操作方式,为开发人员提供了更多的降低系统成本的选择,并且软件还可移植到其它许多常见的嵌入式微控制器上运行。 高级产品营销经理Cristian Ionescu-Catrina表示:“由于新的能效法规陆续出台,无刷直流电机正在家用电器和其他市场中呈指数增长。BridgeSwitch Motor-Expert软件降低了无刷直流驱动的成本和复杂度。新软件随附提供可实现恒速和恒扭矩操作的即用型应用实例,所有这些实例均符合IEC6730 A类控制标准。Power Integrations在开发Motor-Expert时充分考虑了设计工程师的实际需求 – 它可以从根本上简化设计流程,缩短上市时间。” 电机精调是通过Motor-Expert用户界面进行的,新的控制环路系数可以实时更新,无需重新编译代码。软件界面还使用户能够实现系统操作的可视化,显示各种数据的状态,包括电流、转速、操作状态、电流误差和转速误差。软件中的故障诊断界面可帮助用户深入了解逆变器和电机的运行情况。 Motor-Expert软件具有精确的转速和电流控制环路调整功能。基于API软件架构的模块化和灵活性的特点允许用户添加新的用例和功能,用户也可以将软件移植到他们喜欢使用的微控制器上,或与系统CPU中的其他代码结合使用。该软件支持静态(MISRA)和动态性能分析,包括时延、调制和执行时间。它只需要14kB的代码存储器和5kB的SRAM,适合具有较小存储能力的微控制器。BridgeSwitch电机驱动IC可以与3V和5V MCU搭配使用,并且无需外部电流检测电阻。 BridgeSwitch集成半桥IC产品系列极大简化了应于用单相或多相永磁电机及无刷直流电机驱动的高压逆变器的开发和生产。BridgeSwitch电机驱动的优异效率和分布式散热架构可省去散热片,有助于降低系统成本和重量。集成的硬件方式实现的电机过流保护可增强安全性和可靠性,并简化IEC60335-01和IEC60730-01认证过程,大大缩短上市时间。

    时间:2021-05-14 关键词: 无刷直流电机 C语言

  • 魔幻!2021年,6种将死的编程语言?

    随着编程语言的更新迭代,编程界语言排行榜又要面临一次全新的洗牌,六大编程语言将要黄了!此消息一出,令众多程序员心碎!那么这将“亡”的六大语言中有你所擅长的吗?   壹 dashuju Perl 曾几何时,几乎每个人都在使用Perl语言编程。但是那些经常使用的人慢慢地发现,关于这个Perl语言似乎总是有点不对劲。至少我知道有这么个叫做“piecemeal”的编程语言,它的创造者似乎就只是将这个功能堆在另一个功能上面而已,并没有好好考虑将它们结合在一起。 事实上,甚至是它的创造者也不得不承认这种编程语言是有问题的。经过完整地改造之后,现在的开发工作开始倾向于使用Perl6,这个大概是在2000年的时候。至于Perl?俨然已经销声匿迹了!所以完全没有必要去学习它了。顺便说一句,下面这个“Goodbye World”就是用Perl写的: #!/usr/bin/perlprint “Content-type: text/html\n\n”;print “Goodbye, world!\n”; 上面这个例子会出来一个网页。现在的Perl,由于可以作为CGI脚本语言,所以使用的最广泛的是在生成web页面上。但是为了适应时代的变化,我们最好还是将Perl语言“弃之如敝履”。  贰 dashuju Haskell 据说,Haskell 即将在今年进行重大更新。有很多巨头公司和项目(Facebook、GitHub 等)曾经使用 Haskell 开发过一些重要项目。不过,Haskell 在 RedMonk 语言排行榜上的表现一直都很平淡,这表明没有更多的开发者在关注这门语言。它要死了,还是已经死了? 另外一种声音: 在以前的Haskell 用户调查 中,我们可以看到下面五大亮点: 1.Haskell 社区已经开始更加多样化和专注于项目,虽然 Haskell 一直以来以“仅限科学家”著称。 2.Haskell 不仅被用于混合语言项目,还被用于构建完全用 Haskell 编写的端到端解决方案。3.Haskell 社区被认为能给用户提供许多支持。 4.Haskell 在商业环境,特别是 FinTech 中的应用日益增多,但在网络安全和电子商务方面的应用规模仍较小。 5、在过去三年中,Haskell的工具已经有了很大的改进,Stack和Cabal等工具已有大约80%的用户使用。 对于两种声音你们怎么看?  叁 dashuju Ruby 关于Ruby,可以这么唱“十年之后,我不认识你你不属于我……”。因为就在10年前,Ruby语言可谓是风靡一时。它出生于1995年,5年左右达到它的鼎盛时期。如果你经常使用的话,绝对会义无反顾地爱上它。但是,像我们这些学着C语言风格长大的孩子在学习Ruby时往往会觉得有点囧。 下面是用Ruby写的“Goodbye World”: puts ‘Bye bye, Miss American Ruby! Drove my Chevy to the Levie…’puts ’2011 was the day that Ruby died, yeah…’ 下面是一个用于计算阶乘的例子: def fact(n)  if n == 0    1  else    n * fact(n-1)  endendputs fact(ARGV[0].to_i) 我测试了这个例子,来计算1000的阶乘。下面是结果(由于篇幅限制,中间略过了2569个数字): ruby fact.rb 100040238726007709377354370243392300…0000000 从各方面来看,Ruby都很好,几乎是一片赞誉声……除了Twitter。在2011年4月,Twitter宣称他们已经将几乎大部分的代码都改写过了,以便不必使用Ruby和它的web框架——Ruby on Rails,据他们所说这个平台非常之低效。不过,我想说的是,也正是那一天起,Ruby开始走下坡路,使用的人数也是越来越少。  肆 dashuju Visual Basic.NET 十年前,我应聘到一个需要重写大量代码的公司,名字我已经忘记了,主要工作就是将VB6转换为Visual basic.NET。大概就只干了一两个月吧,我就跳槽了:真心太痛苦了。 微软钟爱于BASIC编程语言的扩展可以一路追溯到1991年,那时他们刚刚采购了来自Alan Cooper的一个非常酷(对于那个时候而言)的可视化编程设计。Alan Cooper初期使用的是别的编程语言,但是比尔盖茨让他换成BASIC语言,因为盖茨认为那是当时最为简单的编程语言。于是乎,大名鼎鼎的Visual Basic,就从BASIC中衍生出来——对象这一概念以及新的编程技术问世了。 后面又发生了一些很有意思的事情。Borland Delphi的创造引领者,Anders Hejlsberg也到微软工作,并且引领创建了一个新的编程语言——C#。这种编程语言非常类似于Java语言。刚开始的学习或许有点难,但是一旦上手,你绝对会对它爱不释手。C#很快就成为了微软的旗舰编程语言。现在的话,在软件行业中,有很多很多需要C#的工作岗位,不少都是高薪聘用的。 虽然针对自己的CLR运行,微软创建了C#,但是它的工程师们另外还创建了一个盖茨深爱的BASIC语言版本,命名为Visual Basic.NET。该编程语言借用了BASIC语言的语法,但是它的编码方法却与C#相似。虽然Visual Basic.NET也在发展,但是优胜劣汰总是不可避免的——大家都选择了C#,于是Visual Basic.NET就成为了明日黄花。 下面是摘自微软网页上的一段Visual Basic.NET程序: ‘ Allow easy reference to the System namespace classes.Imports System‘ This module houses the application’s entry point.Public Module modmain   ‘ Main is the application’s entry point.   Sub Main()     ‘ Write text to the console.     Console.WriteLine (“Hello World using Visual Basic!”)   End SubEnd Module (这里的“Hello World”也可以替换成“Goodbye World”,这个没关系。  伍 dashuju Adobe Flash和AIR 从技术上讲,这些都是平台,而非编程语言。我之所以将它们包含进来是因为如果你想要使用它们,就必须安装Adobe自己的ECMAScript版本,即ActionScript。ActionScript是JavaScript(当前最流行的编程语言之一,因为它能用于所有的浏览器)的一个近亲。ActionScript在ECMAScript(这是JavaScript实现标准的官方名称)中增加了一些细节;但是除了Adobe Flash,其他地方几乎没有ActionScript的用武之地。 你使用Flash不?乔布斯非常讨厌它,并且也不允许iPhone使用它。然后随着iPhone(以及随后的iPad)的逐渐普及,Web开发人员不得不创建不必依赖于Flash的网站。那些以ActionScript为生的开发人员也不得不纷纷下岗。(我曾经看到过一个Flash开发人员指责另一个JavaScript开发人员毁了他的职业生涯。) Adobe也曾试图通过AIR以求得其编程平台的一线生机,于是配建了一个用于构建AIR app的工具,称为Flex。至于AIR,许多人都说,这是一场灾难。不过我们目前也不知道为什么Adobe会推出AIR,可能是想用AIR取代Flash?也可能是想要AIR和Flash相亲相爱共同发展? 记得有一段时间,得益于Twitter平台——TweetDeck(要求用户在电脑上安装AIR运行时)的使用,AIR很是红火了一阵子。那时大概有数以百万计的pc AIR应用被开发出来,只是后来Twitter在2011年买了TweetDeck之后,又改写本地代码取代了AIR。于是乎,AIR的辉煌就到此为止。 随着Flash和AIR的逐渐逝去,Adobe的ActionScript也开始向世界吻别。下面是一些用ActionScript写的代码示例。 package {import flash.display.*;import flash.text.*;public class HelloWorld extends Sprite {   private var greeting:TextField = new TextField();public function HelloWorld() {     greeting.text = “Hello World!”;     greeting.x = 100;     greeting.y = 100;     addChild(greeting);   } }} (你可能会发现这与JavaScript非常相似,都使用var、function和new,并且也使用小数点来访问成员变量。)  陆 dashuju Delphi’s Object Pascal 首先我得向我曾经的好伙伴Delphi表示歉意,因为我不得不公布Object Pascal的“死讯”。well,Delphi(用于发Object Pascal的工具)历经变迁之后,依然苟延残喘着(它起源于Borland公司,现在抱着Embarcadero公司的大腿)。 早先Delphi和它的Object Pascal语言确实给我们提供了一个良好的工作环境:虽然有点啰嗦,但是编译器很快,而且相比Visual Basic(这里指的是pre-Visual Basic.NET,1995年左右),创建Windows程序更容易。 但是它的优势并没有持续下去。也很难说是什么原因,因为这个平台真心是不错的。就在这时,Borland公司开始在其Delphi的产品线上支持C#和C++。发展到后来,Borland公司甚至直接将Delphi卖给了Embarcadero公司,然后Embarcadero公司继续使用Delphi开发产品。话说,它做得相当不错,但是重点再也不是Pascal了。当然,你依然可以用Pascal编程,但是几乎没人走这条路了。事实上,我们可以使用Delphi建立许多不同的平台,包括iOS、Android,以及Linux操作系统。 但是,如果你去Embarcadero公司的网站看看,你会发现他们主要是在促进Delphi’s C++ 的支持。因此,换言之就是,Object Pascal已然逝去了。写到这里,我不禁悲从心来,因为我花了很多很多时间来学习Pascal语言,特别是Delphi’s Object Pascal。但是没办法,现实就是如此残酷,不转行就只能饿死。 下面请看Object Pascal的代码: program HelloWorld;begin    writeln(‘You say goodbye.’) 译者注:以上观点仅代表作者个人观点,请文明礼貌按秩序吐槽。 译文链接:http://www.codeceo.com/article/5-die-programming-language.html英文原文:5 Programming Languages Marked for Death翻译作者:码农网 – 小峰 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-05-11 关键词: 嵌入式 编程 C语言

  • C语言函数返回1和返回0究竟哪个好?

    编排 | strongerHuang 微信公众号 | 嵌入式专栏 基本上,没有人会将大段的C语言代码全部塞入 main() 函数。更好的做法是按照复用率高、耦合性低的原则,尽可能的将代码拆分不同的功能模块,并封装成函数。 C语言代码的组合千变万化,因此函数的功能可能会比较复杂,不同的输入,常常产生不同的输出结果。 嵌入式专栏 1 C语言函数的返回值 C语言函数可以通过返回值表示输出结果,例如 log() 函数的返回值会根据不同的输入,返回不同的值。 再比如,我们定义一个函数 myopen(),用于打开某个文件,那么,这个函数要么能够成功打开文件,要么打开文件失败,这时,可以通过返回值区分“成功”和“失败”。 当然,myopen() 函数失败的原因可能很多,但不是本文关注的重点,本文更关心的是,该以何值表示“成功”,何值表示“失败”。 按照C语言语法, 0 表示假,非零(常常用 1)表示真,那是否函数也用 0 返回值表示“失败”,1 返回值表示“成功”呢? 嵌入式专栏 2 行业“潜规则” C语言函数当然可以使用返回值 0 表示“失败”,用返回值 1 表示“成功”。 事实上,C语言函数用什么样的返回值表示成功或者失败,只是一种人为的约定,函数的调用者遵守这个“约定”就可以了。 C语言也有“行业潜规则”, 不过,对于一般的函数,大多数C语言程序员常常使用返回值 0 表示成功,非零值表示失败。 因此,如果希望我们定义的函数能够被大多数人愉快的使用,最好不要反其道而行,遵守“行业潜规则”更好一点。 仔细考虑下,其实C语言函数使用返回值 0 表示成功是有原因的。更一般的C语言函数返回值并不一定只有两种可能值(成功/失败),它可能还会返回对应错误原因的返回值。总之,函数成功只有一种可能,函数失败却有多种可能。 实数要么是 0,要么非 0,因此可以将 0 看作一个比较特殊的“唯一”数值,使用 0 这个“唯一”的返回值,表示唯一的“成功”,多种非零的返回值,表示多种原因的失败,无疑更好一些。 当然,我们也可以说“实数要么是 1,要么非 1”,不过这显然也不是“行业潜规则”。 例如我们可以规定,如果 myopen() 函数因为“文件或者目录不存在”的原因失败,返回 -1,如果因为“权限不够”的原因失败,则返回 -2。-1 和 -2 都是“非零值”,而成功作为失败的对立面,也即“非零值”的对立面,myopen() 函数使用返回值 0 表示成功无可厚非。 嵌入式专栏 3 C语言程序员中另一中行业“潜规则” 当然,C语言程序员中还有一种“行业潜规则”。 如果定义的函数是个布尔函数,也即返回值显式的使用类似于 bool 关键字定义,或者函数名类似于 is_true(),那么显然此时应该遵守C语言语法,使用“真”值表示成功,“假”值表示失败。 if( is_true() ) printf("true\n"); 请看上面这两行C语言代码,显然,遵守C语言语法的布尔函数更便于程序员写出布尔判断类的代码。 嵌入式专栏 4 小结 本文主要讨论了C语言程序开发中关于函数返回值的问题。 可见,使用什么样的返回值表示成功,什么用的返回值表示失败,其实只是一种人为约定。 只不过,如果希望我们编写的代码能够被大多数同行愉快的使用,最好遵守下“行业潜规则”。 当然了,若是希望我们的C语言代码应用性更广,则可以使用标准头文件里预先定义好的 EXIT_SUCCESS 和 EXIT_FAILURE 宏。

    时间:2021-04-22 关键词: 代码 C语言

  • 设计模式_工厂方法模式_C语言实现

    简介 我们接简单工厂方法模式_C语言实现中生产电脑的模拟场景来看。 在简单工厂方法中,不同供应商的硬盘都由HardDiskFactory创建出来。这种做法存在几个问题: 1、当供应商很多时,HardDiskFactory就会成为一个“过大类” 2、假设现在要新引入一家硬盘供应商,那就需要在HardDiskFactory的Create方法中增加一个case分支。在Create方法中增加case分支的做法违反了“开闭原则”。 我们现在要讲的“工厂方法”可以解决上述问题: 工厂方法针对每一个供应商的硬盘提供一个工厂。通过不同工厂对象来创建不同的产品对象。也就是说,工厂方法不直接定义用于创建对象的类,而是定义一个创建硬盘对象的接口,让子类决定实例化哪一个供应商的硬盘。 当新增一个硬盘供应商时,只需要新增一个工厂对象。不需要在原有工厂中增加代码,所以不违反“开闭原则”,也不会因为不断增加供应商而产生“过大类”。 硬盘对象创建 参与者 1、Product: HardDisk 定义硬盘对象的接口(与简单工厂方法相同) 2、Concrete Product: SeagateHardDisk, ToshibaHardDisk 实现不同供应商的硬盘(与简单工厂方法相同) 3、Factory: HardDiskFactory 声明硬盘工厂方法,具体硬盘对象的创建交给其子类 ConcreteFactory 4、ConcreteFactory: SeagateHardDiskFactory, ToshibaHardDiskFactory 创建具体硬盘对象 UML HardDisk、SeagateHardDisk、ToshibaHardDisk示例代码与简单工厂方法完全一致,此处不再重复 HardDiskFactory代码示例: hard_disk_factory.h: #include "seagate_hard_disk_factory.h" #include "seagate_hard_disk.h" #include "stdio.h" #include "stdlib.h" struct HardDisk* SeagateCreate(struct HardDiskFactory *this) { struct SeagateHardDisk *seagateHardDisk = NULL; if ((seagateHardDisk = malloc(sizeof(struct SeagateHardDisk))) == NULL) { printf("fail in malloc\n"); return NULL;     }     SeagateHardDisk(seagateHardDisk); return (struct HardDisk*)seagateHardDisk; } void SeagateDestroy(struct HardDiskFactory *this, struct HardDisk *hardDisk) { if (hardDisk != NULL) { free(hardDisk);         hardDisk = NULL;     }     } // 构造函数 void SeagateHardDiskFactory(struct SeagateHardDiskFactory *this) { this->hardDiskFactory.Create = SeagateCreate; this->hardDiskFactory.Destroy = SeagateDestroy; } // 析构函数 void _SeagateHardDiskFactory(struct SeagateHardDiskFactory *this) { this->hardDiskFactory.Create = NULL; this->hardDiskFactory.Destroy = NULL; } ToshibaHardDiskFactory代码示例: toshiba_hard_disk_factory.h: #include "hard_disk.h" #include "hard_disk_factory.h" #include "seagate_hard_disk_factory.h" #include "toshiba_hard_disk_factory.h" #include "stddef.h" void main() { struct HardDisk *hardDisk = NULL; struct HardDiskFactory *hardDiskFactory; struct SeagateHardDiskFactory seagateHardDiskFactory; SeagateHardDiskFactory(&seagateHardDiskFactory);     hardDiskFactory = (struct HardDiskFactory *)&seagateHardDiskFactory; // 创建 seagate 硬盘对象 hardDisk = hardDiskFactory->Create(hardDiskFactory); // 使用 seagate 硬盘对象 hardDisk->Operation(hardDisk); // 销毁 seagate 硬盘对象 hardDiskFactory->Destroy(hardDiskFactory, hardDisk);     _SeagateHardDiskFactory(&seagateHardDiskFactory); struct ToshibaHardDiskFactory toshibaHardDiskFactory; ToshibaHardDiskFactory(&toshibaHardDiskFactory);     hardDiskFactory = (struct HardDiskFactory *)&toshibaHardDiskFactory; // 创建 toshiba 硬盘对象 hardDisk = hardDiskFactory->Create(hardDiskFactory); // 使用 seagate 硬盘对象 hardDisk->Operation(hardDisk); // 销毁 toshiba 硬盘对象 hardDiskFactory->Destroy(hardDiskFactory, hardDisk);     _ToshibaHardDiskFactory(&toshibaHardDiskFactory); } 客户端显示示例:

    时间:2021-04-05 关键词: 设计模式 工厂方法 C语言

  • 工作四年,分享50个让你代码更好的小建议

    1. 仅仅判断是否存在时,select count 比 select 具体的列,更好。 我们经常遇到类似的业务场景,如,判断某个用户userId是否是会员。 「(反例):」 一些小伙伴会这样实现,先查从用户信息表查出用户记录,然后再去判断是否是会员: 2. 复杂的if逻辑条件,可以调整顺序,让程序更高效 假设业务需求是这样:如果用户是会员,并且第一次登陆时,需要发一条通知的短信。假如没有经过思考,代码很可能直接这样写了。 if(isFirstLogin && isUserVip ){     sendMsg(); } isFirstLogin执行的次数是5次,isUserVip执行的次数是1次。 如果你的isFirstLogin,判断逻辑只是select count 一下数据库表,isUserVip也是select count 一下数据库表的话,显然,把isFirstLogin放在前面更高效。 select * from user_info where user_id =#{userId}; 「正例:」 4. 优化你的程序,拒绝创建不必要的对象 如果你的变量,后面的逻辑判断,一定会被赋值;或者说,只是一个字符串变量,直接初始化字符串常量就可以了,没有必要愣是要new String(). 反例: String s= "欢迎关注公众号:捡田螺的小男孩 ”; //initialCapacity = 15/0.75+1=21  Map map = new HashMap(21);    又因为hashMap的容量跟2的幂有关,所以可以取32的容量  Map map = new HashMap(32); try{   // do something }catch(Exception e){   log.info("捡田螺的小男孩,你的程序有异常啦"); } 「正例:」 7. 打印日志的时候,对象没有覆盖Object的toString的方法,直接把类名打印出来了。 我们在打印日志的时候,经常想看下一个请求参数对象request是什么。于是很容易有类似以下这些代码: 请求参数是:local.Request@49476842 这是因为对象的toString方法,默认的实现是“类名@散列码的无符号十六进制”。所以你看吧,这样子打印日志就没啥意思啦,你都不知道打印的是什么内容。 所以一般对象(尤其作为传参的对象),「都覆盖重写toString()方法」:

    时间:2021-04-04 关键词: 代码 C语言

  • 前几天哪位老哥让我推荐C语言书籍来着?

    大家好,我是小麦,就在前几天,有几位老哥和我私聊,让我整理一份C语言经典书籍的书单,现在我把这个清单重新整理了一下,这里有零基础入门和进阶提高的书籍,对每个阶段的学习都会有帮助,下面它来了。 关于C语言 先看C语言的历史;1973 年,KenThompson 和 DennisRitchie 在做系统内核移植开发时,感觉使用汇编语言很难实现。后来决定使用一种称为 BCPL的语言进行开发,在开发过程中,他们在 BCPL 的基础上做了进一步的改进,推出了 B 语言(取 BCPL 第一个字母)。 后来发现使用 B 语言开发的 UNIX 内核,还是无法达到他们的预期要求,于是在 B 语言的基础上,做了进一步的改进,设计出了具有丰富的数据类型,并支持大量运算符的编程语言。改进后的语言较B语言有质的飞跃,1970年左右,取名为 C 语言,并使用 C 语言成功重新编写了 UNIX内核。 这也是为什么UNIX的时间戳默认是从1970年1月1日开始; C/C++无处不在,到底能做哪些事情呢? 大多数操作系统内核都是用C编写的,包括但不限于Windows,Linux,Mac,iOS,Android等。 现代浏览器也是用C/C++编写的。像Chrome,Firefox等。 现代游戏引擎是用C/C++编写的,例如Unity3D,虚幻引擎,cocos2d-x等。 编程语言的编译器和解释器也是用C/C++实现的。 下面给大家推荐基本C语言学习非常优秀的书籍。 C Primer Plus Linux C编程一站式学习 C 和指针 C 程序设计语言 深入理解C指针 C 专家编程 C 陷阱与缺陷 C 语言的科学和艺术 C 语言程序设计现代方法 C 语言接口与实现 数据结构与算法分析——C语言描述 UNIX环境高级编程(第3版) Linux程序设计 C Primer Plus 《C Primer Plus(第5版)(中文版)》是C语言书最好的入门书籍之一,0基础完全可以。 可以说是满分入门书籍,内容循序渐进,这本书重要的不止是让你学会了C语言,更重要的是能够锻炼你的编程思想,这对以后的学习很有帮助。 遇到看不懂的地方多看几遍,再看不懂就先跳过,有时候一回头就突然懂了。 如果想把C当作吃饭的技能,除了这本书,还必须要再补一下数据结构和算法方面的知识。 Linux C编程一站式学习 本书有两条线索: 一条线索是以Linux平台为载体全面深入地介绍C语言的语法和程序的工作原理; 另一条线索是介绍程序设计的基本思想和开发调试方法。 本书分为两部分: 第一部分讲解编程语言和程序设计的基本思想方法,让读者从概念上认识C语言; 第二部分结合操作系统和体系结构的知识讲解程序的工作原理,让读者从本质上认识C语言。 本书适合做零基础的初学者学习C语言的第一本教材,帮助读者打下牢固的基础。 有一定的编程经验但知识体系不够完整的读者也可以对照本书查缺补漏,从而更深入地理解程序的工作原理。 本书对于C语言的语法介绍得非常全面,对C99标准做了很多解读,因此也可以作为一本精简的C语言语法参考书。 C 和指针 这本书和《专家编程》《C缺陷和陷阱》可以并称C语言(进阶书)三杰; 这本书提供与C语言编程相关的全面资源和深入讨论,由浅入深; 它涵盖了C语言的全部内容,特别注重指针的讲解,除了头尾的几章,指针的话题几乎是贯穿了全书。 正是指针使得C语言如此之强大,所以要学习C语言的精髓,就是要精通指针! C 程序设计语言 在计算机发展的历史上,没有哪一种程序设计语言像C语言这样应用广泛。 本书作者是C语言之父,相当经典,“hello,World"程序就是由本书首次引入的。 不过读这本书,我们得有一些unix like系统的操作经验,需要知道文件描述符,输入输出流,重定向,管道以及”anything is file“等在unix世界里这些归为常识的概念; 这些离我们这些在windows的世界里长大的一辈太远。 深入理解C指针 深入理解C指针和内存管理,提升编程效率!这是一本实战型图书,通过它,读者可以掌握指针动态操控内存的机制、对数据结构的增强支持,以及访问硬件等技术。 C 专家编程 虽然是技术类书籍,但是作者很幽默,书里面八卦比较多,趣味性比较强; 同时也展示了优秀的C程序员所使用的编码技巧,并专门开辟了一章对C++的基础知识进行了介绍。 对于有一定经验的C程序员会很有帮助; 对于C语言功底深厚的程序员,本书可以帮助他们站在C的高度了解和学习C++。 C 陷阱与缺陷 作者以自己1985年在Bell实验室时发表的一篇论文为基础,结合自己的工作经验扩展成为这本对C程序员具有珍贵价值的经典著作。 写作本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍。 本书适合有一定经验的C程序员阅读学习,即便你是C编程高手,本书也应该成为你的案头必备书籍。 正如书上所说,“本书所揭示的知识,至少能够帮助你减少C代码和初级C++代码中90%的Bug”,我觉得这并不夸张。 C 语言的科学和艺术 《C语言的科学和艺术》是一本C语言经典教材,强调软件工程和优秀的程序设计风格。 此外,读者还可以从书中学习到ANSIC的基础知识,这些内容已经成为计算机行业的标准。 作者的写作风格使得书中深奥的概念变得易于理解和引人入胜。 这本书集中讨论库和抽象的用法,这是当代程序设计技术中最基本的知识。 使用库来隐藏C语言的复杂性,更加突出主题,使读者可以较好地掌握每一个主题的精髓。 然后,进一步给出每个库的底层实现,较好地展示了库自身的抽象威力。 C 语言程序设计现代方法 《C语言程序设计现代方法》最主要的一个目的就是通过一种“现代方法”来介绍C语言,实现客观评价C语言、强调标准化C语言、强调软件工程、不再强调“手工优化”、强调与c++语言的兼容性的目标。《C语言程序设计现代方法》分为C语言的基础特性。C语言的高级特性、C语言标准库和参考资料4个部分。每章都有“问与答”小节,给出一系列与本章内容相关的问题及其答案,此外还包含适量的习题。 C 语言接口与实现 《C语言接口与实现:创建可重用软件的技术》概念清晰、实例详尽,是一本有关设计、实现和有效使用C语言库函数,掌握创建可重用C语言软件模块技术的参考指南。 书中提供了大量实例,重在阐述如何用一种与语言无关的方法将接口设计实现独立出来,从而用一种基于接口的设计途径创建可重用的API。 数据结构与算法分析——C语言描述 本书是《Data Structures and Algorithm Analysis in C》一书第2版的简体中译本。 原书曾被评为20世纪顶尖的30部计算机著作之一,作者Mark Allen Weiss在数据结构和算法分析方面卓有建树,他的数据结构和算法分析的著作尤其畅销,并受到广泛好评.已被世界500余所大学用作教材。 在本书中,作者更加精炼并强化了他对算法和数据结构方面创新的处理方法。通过C程序的实现,着重阐述了抽象数据类型的概念,并对算法的效率、性能和运行时间进行了分析。 UNIX环境高级编程 《UNIX环境高级编程》被誉为UNIX编程“圣经”。 经典中的经典。不过看这本书的前提是你熟悉linux,哪怕不是使用linux接口编程,至少要用过,了解shell,gcc,vim。所以适合有一些基础的读者。 Linux程序设计 时至今日,Linux系统发展越来越成熟,因为具备跨平台、开源、支持众多应用软件和网络协议等优点,它得到了各大主流软硬件厂商的支持,也成为广大程序设计人员理想的开发平台。 本书是Linux程序设计领域的经典名著,以简单易懂、内容全面和示例丰富而受到广泛好评。 中文版前两版出版后,在国内的Linux爱好者和程序员中也引起了强烈反响,这一热潮一直持续至今。 —— The End —

    时间:2021-04-02 关键词: 编程 C语言

  • 关于堆栈的讲解(我见过的最经典的)

    关于堆栈的讲解(我见过的最经典的)

    一、预备知识—程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。- 程序结束后有系统释放4、文字常量区—常量字符串就是放在这里的。程序结束后由系统释放5、程序代码区—存放函数体的二进制代码。 二、例子程序 这是一个前辈写的,非常详细 //main.cppint a = 0; //全局初始化区int a = 0; //全局初始化区char *p1; //全局未初始化区main() {    int b; //栈    char s[] = "abc"; //栈    char *p2; //栈    char *p3 = "123456"; //123456\0在常量区,p3在栈上。    static int c = 0; //全局(静态)初始化区    p1 = (char *)malloc(10);    p2 = (char *)malloc(20);    //分配得来得10和20字节的区域就在堆区。    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。} 二、堆和栈的理论知识 2.1申请方式 stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间heap:需要程序员自己申请,并指明大小,在c中malloc函数如p1 = (char *)malloc(10);在C++中用new运算符如p2 = (char *)malloc(10);但是注意p1、p2本身是在栈中的。 2.2 申请后系统的响应 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 2.3 申请大小的限制 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 2.4 申请效率的比较: 栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 2.5 堆和栈中的存储内容 栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 2.6 存取效率的比较 char s1[] = "aaaaaaaaaaaaaaa";char *s2 = "bbbbbbbbbbbbbbbbb";aaaaaaaaaaa是在运行时刻赋值的;而bbbbbbbbbbb是在编译时就确定的;但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。比如: #includevoid main() {    char a = 1;    char c[] = "1234567890";    char *p ="1234567890";    a = c[1];    a = p[1];    return;} 对应的汇编代码 10: a = c[1];00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]0040106A 88 4D FC mov byte ptr [ebp-4],cl11: a = p[1];0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]00401070 8A 42 01 mov al,byte ptr [edx+1]00401073 88 45 FC mov byte ptr [ebp-4],al 第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 2.7小结: 堆和栈的区别可以用如下的比喻来看出:使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 三 、windows进程中的内存结构 在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。 首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码: #include int g1=0, g2=0, g3=0;int main(){    static int s1=0, s2=0, s3=0;    int v1=0, v2=0, v3=0;    //打印出各个变量的内存地址        printf("0x%08x\n",&v1); //打印各本地变量的内存地址    printf("0x%08x\n",&v2);    printf("0x%08x\n\n",&v3);    printf("0x%08x\n",&g1); //打印各全局变量的内存地址    printf("0x%08x\n",&g2);    printf("0x%08x\n\n",&g3);    printf("0x%08x\n",&s1); //打印各静态变量的内存地址    printf("0x%08x\n",&s2);    printf("0x%08x\n\n",&s3);    return 0;} 编译后的执行结果是: 0x0012ff780x0012ff7c0x0012ff800x004068d00x004068d40x004068d80x004068dc0x004068e00x004068e4 输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。 ├———————┤低端内存区域│ …… │├———————┤│ 动态数据区 │├———————┤│ …… │├———————┤│ 代码区 │├———————┤│ 静态数据区 │├———————┤│ …… │├———————┤高端内存区域 堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码: #include void __stdcall func(int param1,int param2,int param3){    int var1=param1;    int var2=param2;    int var3=param3;    printf("0x%08x\n",param1); //打印出各个变量的内存地址    printf("0x%08x\n",param2);    printf("0x%08x\n\n",param3);    printf("0x%08x\n",&var1);    printf("0x%08x\n",&var2);    printf("0x%08x\n\n",&var3);    return;}int main() {    func(1,2,3);    return 0;} 编译后的执行结果是: 0x0012ff780x0012ff7c0x0012ff800x0012ff680x0012ff6c0x0012ff70 ├———————┤

    时间:2021-04-02 关键词: 堆栈 C C语言

  • 简单工厂方法模式_C语言实现

    时间:2021-04-01 关键词: 工厂方法 简单工厂 C语言

  • MISRA C:2012 又是什么标准?

    编排 | strongerHuang 微信公众号 | 嵌入式专栏 C语言的标准有很多,之前给大家分享过相关的内容,比如:C89、C99标准,ANSI C、ISO C、Standard C标准等。 可能你在一些地方还看到过:MISRA C:2012,MISRA C++:2008,那你知道这是什么吗? 今天分享的就是另外一种C语言标准:MISRA C. 嵌入式专栏 1 MISRA C MISRA C是由汽车产业软件可靠性协会(MISRA)提出的C语言开发标准。 其目的是在增进嵌入式系统的安全性及可移植性,针对C++语言也有对应的标准MISRA C++。 MISRA C一开始主要是针对汽车产业,不过其他产业也逐渐开始使用MISRA C:包括航天、电信、国防、医疗设备、铁路等领域中都已有厂商使用MISRA C。 MISRA C的第一版《Guidelines for the use of the C language in vehicle based software》是在1998年发行,一般称为MISRA-C:1998.。MISRA-C:1998有127项规则,规则从1号编号到127号,其中有93项是强制要求,其余的34项是推荐使用的规则。 在2004年时发行了第二版的MISRA C的第一版《Guidelines for the use of the C language in critical systems》(或称作MISRA-C:2004),其中有许多重要建议事项的变更,其规则也重新编号。MISRA-C:2004有141项规则,其中121项是强制要求,其余的20项是推荐使用的规则。规则分为21类,从“开发环境”到“运行期错误”。 2012年发布第三版,为当前最新有效的C语言规范版本,称为MISRA C:2012。 MISRA C 版本历史 MISRA版本 发布年份 C语言版本 指令 数量 规则 数量 指南 总数 1998 1998 C90 不详 127 不详 2004 2004 C90 不详 142 不详 2012 2012 C99 16 143 159 2012 AMD-1 2016 C99 17 156 173 2012 AMD-2 2020 C11 17 158 175 嵌入式专栏 2 MISRA C:2012(修订版2) 当前最新有效的C语言规范版本为MISRA C:2012,下面来讲讲其修订版2。 MISRA C工作组发布了对MISRA C:2012的修订版,以支持称为“C11”的C标准,并正式批准为ISO/IEC 9899:2011。C11已得到广泛使用,对于一直推迟迁移到C11的项目和组织,这是一个广受欢迎的公告。C11还取代了C99(标准ISO/IEC 9899:1999),并已被C18(标准ISO/IEC 9899:2018)取代。除了更新的C语言准则之外,MISRA C工作组还发布了MISRA 2020合规性指南。 MISRA C:2012(修订版2)现在引用ISO/IEC 9899:2011,并包含C语言更新,为可能使用但受限制的功能和受禁止的功能提供指导,除非其存在您的团队软件审查过程已批准的偏差。在制定修订版2的过程中,还获得了利用先前的补充内容纠正任何已知问题的机会。MISRA工作组的任务不止是提供指导,以防止发生不可预测的行为,减少或消除编码缺陷并在嵌入式软件系统的环境中促进代码安全、安全性、可移植性和可靠性。 新的MISRA C:2012标准: 添加了适用于C11中新功能的新MISRA规则的一个示例,即规则1.4,“不得使用紧急语言功能。” 如果使用设施和_Thread_local存储类说明符,则可能是这种情况的一个实例。该规则将使用违反类别类型“必需”标记C11语言结构。 C11标准化了可能在多核平台上运行的多线程程序的语义,以及使用原子变量的轻量级线程间通信。使用线程本地的全局内存,其中已标识出未定义和未指定行为的实例,包括未满足预期的已定义行为。 向前迈进并符合MISRA C:2012(修订版2),如果我使用_Thread_local,不仅需要进行偏离,而且还需要采取保证措施来解决危害安全性的行为。 _Generic关键字是另一个不应使用的C11语言新功能,它可能表现出不良行为,并且一些人发现C11标准在某些情况下含糊不清。_Generic运算符是一种宏重载。它用于帮助程序员将任何宏都用作通用宏,以使其更高效。下面的代码行显示_Generic关键字如何用于声明不同类型的数据类型的任何宏,以及如何将其声明为不同方法的泛型。以下面的VOL宏示例为例;VOL(x)根据x的类型转换为VOLc(x),VOLl(x),VOL(x)或VOLf(x)。 #define VOL(x) _Generic((x), char: VOLc, long double: VOLl, default: VOL, float: VOLf)(x) 安全编码 安全漏洞的一个常见原因是使用了中定义的标准库函数系统。MISRA C:2012(修订版2)添加了新的规则21.21,该规则规定不得使用标准库函数系统。系统调用是一个阻塞函数,用于执行子进程和命令,等待子进程终止并返回其退出值。认识到原型为“int系统(const char *command);”不需要是单个命令,而可以是一个管道或一系列管道。(例如,系统("pngtopnm \"My Picture.png\" | pnmtoxwd > fout.xwd && xwud fout.xwd");)由于可变命令是由用户提供的数据构成的,因此攻击者可能会发现 引用并在父级上下文中执行任意命令。一些建议的措施可能是利用预定的命令字符串或一起绕开系统调用,而使用spawn代替。 合规报告 对于声称符合MISRA的要求,有一些书面指南,这些指南在过去的几年中不断完善和修订。本文档的最新版本是MISRA Compliance 2020,它于2月发布。从较高的角度来看,适当报告了使用良好的软件开发过程的使用情况,准确地应用了哪些指南以及执行方法的有效性的列表,包括偏差的程度或程度,以及为了声称MISRA符合要求,必须考虑到项目外开发的所有软件组件的状态。Parasoft DTP提供了专用的报告扩展,完全符合MISRA Compliance标准的要求。DTP将指导您完成构建准则执行计划(GEP)和准则重新分类计划(GRP)的过程,并自动生成准则合规摘要(GCS)以及已批准偏差的完整列表。自动化报告消除了繁琐的手工工作,使组织能够遵循MISRA编码准则现在强制执行的合规性流程。 最后 还有其他新的MISRA C:2012(修订版2)规则,例如_Noreturn函数说明符、_Atomic类型说明符、_Alignas对齐说明符和_Alignof运算符,非常引人注目。使用这些类型说明符将触发类别“必需的违规”,并且将不被使用,从而解决了C11覆盖和安全漏洞的问题。此外,还进行了许多修订版2更新和文本替换,以澄清和改进标准。同样,再次提醒您非常重要的一点是,与该标准一起,用户现在可以遵循MISRA Compliance 2020指南的强制性和补充性法规遵循版本。感谢MISRA工作组继续做出色的工作,并为软件界做出了巨大贡献。

    时间:2021-04-01 关键词: 安全编码 C语言

  • C语言中的“悬空指针”和“野指针”是什么意思?

    提起C语言大部分开发者很自然就会想到指针二字,没错,作为C的核心和灵魂,它的地位咱们就不再赘述了,今天我们想跟大家讲的是指针中的两个特有名词:“悬空指针”和“野指针”。 C语言中的指针可以指向一块内存,如果这块内存稍后被操作系统回收(被释放),但是指针仍然指向这块内存,那么,此时该指针就是“悬空指针”。下面这段C语言代码是一个例子,请看: void *p = malloc(size);assert(p);free(p); // 现在 p 是“悬空指针” C语言中的“悬空指针”会引发不可预知的错误,而且这种错误一旦发生,很难定位。这是因为在 free(p) 之后,p 指针仍然指向之前分配的内存,如果这块内存暂时可以被程序访问并且不会造成冲突,那么之后使用 p 并不会引发错误。 最难调试的 bug 总是不能轻易复现的 bug,对不? 所以在实际的C语言程序开发中,为了避免出现“悬空指针”引发不可预知的错误,在释放内存之后,常常会将指针 p 赋值为 NULL: void *p = malloc(size);assert(p);free(p); // 避免“悬空指针”p = NULL; 这么做的好处是一旦再次使用被释放的指针 p,就会立刻引发“段错误”,程序员也就能立刻知道应该修改C语言代码了。 2 野指针 这两点内容,是C语言比较重要的内容,希望大家在编码的时候一定要注意。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-31 关键词: C语言

  • 初学者宝典:C语言入门基础知识大全

    初学者宝典:C语言入门基础知识大全

    01 C语言程序的结构认识 用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,使小伙伴对c语言有个初步认识。 例1:计算两个整数之和的c程序: #include main() { int a,b,sum; a=20; /*定义变量a,b,sum为整型变量*/ b=15; /*把整数20赋值给整型变量a*/ sum=a+b; /*把整数15赋值给整型变量b*/ printf(“a=%d,b=%d,sum=%d\n”,a,b,sum); /*把两个数之和赋值给整型变量sum*/ } 重点说明 — 1、任何一个c语言程序都必须包括以下格式:main() { }——这是c语言的基本结构,任何一个程序都必须包含这个结构。括号内可以不写任何内容,那么该程序将不执行任何结果。 2、main()——在c语言中称之为“主函数”,一个c程序有且仅有一个main函数,任何一个c程序总是从main函数开始执行,main函数后面的一对圆括号不能省略。 3、被大括号{ }括起来的内容称为main函数的函数体,这部分内容就是计算机要执行的内容。 4、在{ }里面每一句话后面都有一个分号(;),在c语言中,我们把以一个分号结尾的一句话叫做一个c语言的语句,分号是语句结束的标志。 5、printf(“a=%d,b=%d,sum=%d\n”,a,b,sum); ——通过执行这条c语言系统提供给我们直接使用的屏幕输出函数,用户即可看到运行结果,本程序运行后,将在显示器上显示如下结果:a=20,b=15,sum=35。 6、#include——注意:以#号开头。不以分号结尾 这一行没有分号,所以不是语句,在c语言中称之为命令行,或者叫做“预编译处理命令”。 7、程序中以 /*开头并且以*/结尾的部分表示程序的注释部分,注释可以添加在程序的任何位置,为了提高程序的可读性而添加,但计算机在执行主函数内容时完全忽略注释部分,换而言之就是计算机当做注释部分不存在于主函数中。 02 C程序的生成过程 C程序是先由源文件经编译生成目标文件,然后经过连接生成可执行文件。 源程序的扩展名为 .c ,目标程序的扩展名为 .obj , 可执行程序的扩展名为 .exe 。 03 标识符 在编写程序时,必须为函数、变量等命名,这个名字称为标识符。C语言中标识符的命名规则如下: 标识符只能由字母、数字、下划线组成; 标识符的第一个字母必须是字母和下划线; 标识符区分大小写字母,如If和if是两个完全不同的标识符。 合法标识符如下:A6, b_3 , _mn 非法的标识符如下:ab#12 , 8m , tr3:4 , yes no 标识符不能与程序中具有特殊意义的关键字相同,不能与用户编制的函数名、C语言库函数相同,在程序中各种标识符尽量不要重复,以便区分。选择变量名和其他标识符时,应注意做到 “见名知义”。 标识符分为如下三类 — 1、关键字 关键字是具有特定含义的,专门用来说明c语言特定成分的一类标识符,不能用作用户的标识符。 2、预定义标识符 预定义标识符在c语言中也有特定的含义,但可以用作用户标识符,预定义标识符分为两类: 1)、库函数名字,比如printf,scanf,sin,isdigit等 2)、编译处理命令名,比如define,include 3、用户标识符 用户根据需要自己定义的标识符称为用户标识符。无论如何自定义标识符,都必须符合标识符的三条命名规则。 04 常量 在程序运行中,其值不能被改变的量称为常量。常量有5种类型:整型常量、实型常量、字符常量、字符串常量和符号常量。 4.1 数值转换 — 数值的四种表现形式: ①:二进制:所有数字由0,1构成,逢二进一,二进制数中不会出现2。 ②:八进制:以数字0(注意不是以字母O,o)开头,所有数字由0~7构成,逢八进一,八进制数中不会出现8。例:0112,0123,077等 ③:十进制:所有数字由0~9构成,逢十进一,十进制数中不会出现10。例:0,12,-15等 ④:十六进制:以0x或者0X(数字0加字母x)开头,所有数字由0~9,A~F(或者a~f)构成,逢十六进一(其中A、B、C、D、E、F分别代表10、11、12、13、14、15) 例:0x4A、0X14c7等 在计算机内部,数字均以二进制形式表示和存放,用户输入的普通十进制数字都要被计算机转换成二进制才能在计算机内部存储,同样计算机的运算结果也为二进制,一般要将其转换成十进制数再输出给用户阅读,这种转换通常由计算机自动实现。 1)将十进制转换二进制、八进制和十六进制 除法:将十进制数除以2,记录余数,得到的商继续除以2,直到商为0,然后将各次相处所得的余数从后往前逆序排列,所得余数数字序列就是该十进制数对应的二进制数。八进制和十六进制转换方法同上。 例:十进制数13转换成二进制数的值为1101,转换八进制为015,转换成十六进制为D. 2)将二进制、八进制和十六进制转换成十进制 乘积求和:将二进制的每一位从低位到高位(右边为低位,左边为高位)分别乘以20,21,22。。。。,然后将这些值求和。 3)二进制与八进制、十六进制数之间的相互转换 ①:二进制转八进制:从右往左每三位一组转换成十进制数,将所得数据组合就是对应的八进制数(注意:高位不足三位补零)。例:(010 110 111)2=(267)8 ②:二进制转十六进制:从右往左每四位一组转换成十进制数,将所得数据组合就是对应的十六进制数(注意:高位不足四位补零)。例:(0101 1011)2=(5B)16 ③:八进制转化二进制:每一位数字转换为三位二进制数字 例:(13)8=(001 011)2= (注意:去掉前面的两个00,因为0在高位没有意义) ④:十六进制转化二进制:每一位数字转换为四位二进制数字 例:(E3)16=(1110 0011)2 4.2 数形常量 — 整型常量有3种形式:十进制整型常量、八进制整型常量和十六进制整型常量。 (注意:c语言中没有直接表示二进制的整型常量,在c语言源程序中不会出现二进制。) 书写方式如下: 十进制整型常量:123 , 0 ,-24 , 85L(长整型常量) 等 八进制整型常量:051 ,-026 ,0773 等 十六进制整型常量:0x55 , 0x1101 , 0x , 0x5AC0 , -0xFF。其中L为长整型。 4.3 实型常量 — 实型常量有两种表示形式:小数形式和指数形式。 小数形式:5.4 0.074 -23.0 指数形式:5.4e0 4.3e-3 -3.3e4 1)小数部分为0的实型常量,可以写为453.0 或453。 2)用小数表示时,小数点的两边必须有数,不能写成“ .453“和“453.“,而应该写成“0.453“和“453.0“。 3)用指数写法时,e前必须有数字,e后面的指数必须为整数(注意:整数阶码可以是正数,负数,也可以是八进制数、十六进制数,但必须为整数)。 4.4 字符常量 — 字符常量的标志是一对单引号‘ ’,c语言中的字符常量有两类: 1)由一对单引号括起来的一个字符,如‘a ’, ‘r’ ,‘#’。注意: ′a′ 和 ′A′ 是两个不同的字符常量。 2)由一对单引号括起来,以反斜杠\开头,后跟若干数字或者字母,比如‘\n’,其中“\“是转义的意思,后面跟不同的字符表示不同的意思,这类字符常量叫转义字符。 4.5 字符串常量 — C语言中,以双引号括起来的,由若干个字符组成的序列即为字符串常量。 例:“ni hao” “happy”等等。 4.6 符号常量 — 符号常量是由宏定义“#define“定义的常量,在C程序中可用标识符代表一个常量。 例:计算圆的面积的c程序。 #include #define PI 3. main() { float r,s; r=12.5; S=PI *r*r; printf(“s= %f ”,s); } 说明: #define 是宏定义,此程序中所有出现PI的地方都代表3.,同时PI称为符号常量。习惯上我们用大写字母来表示符号常量,小写字母表示变量,这样比较容易区别。 05 变量 变量就是其值可以改变的量。变量要有变量名,在内存中占据一定的存储单元,存储单元里存放的是该变量的值。不同类型的变量其存储单元的大小不同,变量在使用前必须定义。 5.1 整型变量 — 整型变量分为4种:基本型(int)、短整型(short int 或short)、长整型(long int 或 long)和无符号型(unsigned int ,unsigned short,unsigned long)。 不同的编译系统对上述四种整型数据所占用的位数和数值范围有不同的规定。 类型说明符 单词signed来说明“有符号”(即有正负数之分),不写signed也隐含说明为有符号,unsigned用来说明“无符号”(只表示正数)。 5.2 实型变量 — C语言中,实型变量分为单精度类型( float )和双精度类型( double )两种。 如:float a , b ; double m ; 在vc中,float 型数据在内存中占4个字节(32位),double型数据占8个字节。单精度实数提供7位有效数字,双精度实数提供15~16位有效数字。实型常量不分float型和double型,一个实型常量可以赋给一个float 型或double型变量,但变量根据其类型截取实型常量中相应的有效数字。 注意:实型变量只能存放实型值,不能用整型变量存放实型值,也不能用实型变量存放整型值。 5.3 字符变量 — 字符变量用来存放字符常量,定义形式: char 变量名; 其中关键字char定义字符型数据类型,占用一个字节的存储单元。 例:char cr1,cr2; cr1= ‘A’ , cr2=‘B’ ; 将一个字符赋给一个字符变量时,并不是将该字符本身存储到内存中,而是将该字符对应的ASCII码存储到内存单元中。 由于在内存中字符以ASCII码存放,它的存储形式和整数的存储形式类似,所以C语言中字符型数据与整型数据之间可以通用,一个字符能用字符的形式输出,也能用整数的形式输出,字符数据也能进行算术运算,此时相当于对它们的ASCII码进行运算。 06 类型的自动转换和强制转换 当同一表达式中各数据的类型不同时,编译程序会自动把它们转变成同一类型后再进行计算。转换优先级为:char < int < float < double 即左边级别“低“的类型向右边转换。 具体地说,若在表达式中优先级最高的数据是double型,则此表达式中的其他数据均被转换成double型,且计算结果也是double型;若在表达式中优先级最高的数据是float型,则此表达式中的其他数据均被转换成float型,且计算结果也是float型。 在做赋值运算时,若赋值号左右两边的类型不同,则赋值号右边的类型向左边的类型转换;当右边的类型高于左边的类型时,则在转换时对右边的数据进行截取。 除自动转换外,还有强制转换,表示形式是:( 类型 )(表达式); 例:(int)(a+b) 讨论:当a值赋值为3.4,b值赋值为2.7,(int)(a+b)和(int)a+b的值分别为多少? 07 C运算符认识 C语言的运算符范围很广,可分为以下几类: 1、算术运算符:用于各类数值运算。包括加(+)、减(-)、乘(*)、除(/)、求余(%)、自增(++)、自减(--)共七种。 2、赋值运算符:用于赋值运算,分为简单赋值(=)、复合算术赋值(+=,-=,*=,/=,%=)和复合位运算赋值(&=,|=,^=,>>=,<<=)三类共十一种。<="" span=""> 3、逗号运算符:用于把若干表达式组合成一个表达式(,)。 4、关系运算符:用于比较运算。包括大于(>)、小于(<)、等于(==)、 大于等于(="">=)、小于等于(<=)和不等于(!=)六种。<="" span=""> 5、逻辑运算符:用于逻辑运算。包括与(&&)、或(||)、非(!)三种。 6、条件运算符:这是一个三目运算符,用于条件求值(?:)。 7、位操作运算符:参与运算的量,按二进制位进行运算。包括位与(&)、位或(|)、位非(~)、位异或(^)、左移(<<)、右移(>>)六种。 8、指针运算符:用于取内容(*)和取地址(&)二种运算。 9、求字节数运算符:用于计算数据类型所占的字节数(sizeof)。 10、特殊运算符:有括号(),下标[],成员(→,.)等几种。 另外,按参与运算的对象个数,C语言运算符可分为:单目运算符 (如 !)、双目运算符 (如+,- )和三目运算符 (如 ? :)。 08 算术运算符和算术表达式 8.1 基本的算数运算符 — (1)+(加法运算符或正值运算符,如2+5)。 (2)-(减法运算符或负值运算符,如4-2)。 (3)*(乘法运算符,如3*8)。 (4)/(除法运算符,如11/5)。 /的运算分为两种情况: a、“除”的左右两边都为整数时,所得结果必然是整数(注意:仅取整数部分,不是四舍五入)比如:5/2的值为2,不是2.5,1/2的值为0。 b、“除”的左右两边至少有一个是实型数据(即小数)时,所得结果为实型数据。比如:5/2.0的值为2.5,7.0/2.0的值为3.5. (5)%(模运算符或称求余运算符,%两侧均应为整型数据,如9%7的值为2)。 需要说明的是:当运算对象为负数时,所得结果随编译器不同而不同,在vc中,结果的符号与被除数相同,比如:13%-2值为1,而-15%2值为-1。 8.2 算术表达式和运算符的优先级与结合性 — 算术表达式是用算术运算符和括号将运算量(也称操作数)连接起来的、符合C语言语法规则的表达式。运算对象包括函数、常量和变量等。 在计算机语言中,算术表达式的求值规律与数学中的四则运算的规律类似,其运算规则和要求如下。 (1)在算术表达式中,可使用多层圆括号,但括号必须配对。运算时从内层圆括号开始,由内向外依次计算各表达式的值。 (2)在算术表达式中,对于不同优先级的运算符,可按运算符的优先级由高到低进行运算,若表达式中运算符的优先级相同,则按运算符的结合方向进行运算。 (3)如果一个运算符两侧的操作数类型不同,则先利用自动转换或强制类型转换,使两者具有相同类型,然后进行运算。 8.3 自增自减运算符 — 作用:使变量的值增1或减1。 如:++i,--i (在使用i之前,先使i的值加1、减1)。i++,i-- (在使用i之后,使i的值加1、减1)。 (1)只有变量才能用自增运算符 (++)和自减运算符(--),而常量或表达式不能用,如10++或(x+y)++都是不合法的。 (2)++和--的结合方向是“自右向左“,如 -i++ ,i的左边是负号运算符,右边是自增运算符,负号运算和自增运算都是 “自右向左“结合的,相当于 -(i++)。 在循环语句中常用到自增(减)运算符,在指针中也常用到该运算符,考生要弄清楚“i++”和“++i”及“i--”和“--i”的区别,特别弄清楚表达式的值和变量的值。 09 赋值运算符与赋值表达式 9.1 赋值运算符与赋值表达式 — 赋值符号 “=“就是赋值运算符,作用是将一个数据赋给一个变量或将一个变量的值赋给另一个变量,由赋值运算符组成的表达式称为赋值表达式。一般形式为:变量名 = 表达式。 在程序中可以多次给一个变量赋值,每赋一次值,与它相应的存储单元中的数据就被更新一次,内存中当前的数据就是最后一次所赋值的那个数据。 例:a=12; 此表达式读作“将10的值赋值给变量a”。 a、如果赋值号两边的运算对象类型不一致,系统会自动进行类型转换,转换的规则:将赋值号右边表达式的值的类型转换成赋值号左边变量的类型, 例:int y=3.5;在变量y中最终存储的是整数3。 b、 可以将复制表达式的值再赋值给变量,形成连续赋值。 例如:x=y=25 是一个连续赋值表达式,x=y=25 等价于x=(y=25),所以表达式x=y=25 最终的值为25 。 9.2 复合的赋值运算符 — 在赋值运算符之前加上其他运算符可以构成复合赋值运算符。其中与算术运算有关的复合运算符是:+=,-=,*=,/=,%= 。 两个符号之间不可以有空格,复合赋值运算符的优先级与赋值运算符的相同。表达式n+=1等价于n=n+1,作用是取变量n中的值增1再赋给变量n,其他复合的赋值运算符的运算规则依次类推。 如求表达a+=a-=a*a 的值,其中a的初值为12 。 步骤: 先进行“a-=a*a“运算,相当于a=a-a*a=12-144=-132 。(2)再进行“a+=-132“运算,相当于 a=a+(-132)==-264 。 9.3 逗号运算符和逗号表达式 — 在c语言中,逗号除了作为分隔符,还可以用作一种运算符----逗号运算符,用逗号运算符将几个表达式连接起来,例如a=b+c,a=b*c等称为逗号表达式。 一般形式为: 表达式1 ,表达式2 ,表达式3 , …,表达式n 例:x=2,y=3,z=4 逗号表达式具有从左至右的结合性,即先求解表达式1,然后依次求解表达式2,直到表达式n的值。表达式n的值就是整个逗号表达式的值。上述的逗号表达式的值就是表达式z=4的值4.需要注意的是,逗号运算符是所有运算符中级别最低的。 例:有如下程序段: main() { int a=2,b=4,c=6,x,y; y=(x=a+b),(b+c); printf("y=%d,x=%d",y,x); } 程序显示结果为:y=6,x=6 讨论:将y=(x=a+b),(b+c);改为y=((x=a+b),b+c) 的程序结果? 10 关系运算符和关系表达式 10.1 C语言中的逻辑值 — C语言中的逻辑值只有两个:真(true)和假(flase)。用非零代表真,用零代表假。因此,对于任意一个表达式,如果它的值为零,就代表一个假值,如果它的值为非零,就代表一个真值。只要值不是零,不管是正数,负数,整数,实数,都代表一个真值。例如-5的逻辑值为真。 10.2 逗号运算符和逗号表达式 — “&&”和“||”的运算对象有两个,故它们都是双目运算符,而!的运算对象只有一个,因此它是单目运算符。逻辑运算举例如下: (1)a&&b: 当&&两边都为“真”时,表达式a&&b的值才是真。 值得注意的是:在数学中,关系式0 (2)a||b: 当||两边有一个为“真”时,表达式a||b的值就是真。 (3)!a: 表示取反,如果a为真,则!A为假,反之亦然。例如!-5的值就为0。 在C语言中,由&&或||组成的逻辑表达式,在某些特定情况下会产生“短路“现象。 (1)x && y && z ,只有当x为真(非0)时,才需要判别y的值;只有x和y都为真时,才需要去判别z的值;只要x为假就不必判别y和z,整个表达式的值为0。口诀:“一假必假”。 例: (!5==1)&&(++i==0) (!5==1)表达式的值为0,所以计算机运行中就跳过(++i==0)此表达式,(!5==1)&&(++i==0)表达式的值为0。 (2)x||y||z ,只要x的值为真(非零),就不必判别y和z的值 ,整个表达式的值为1,只有x的值为假,才需要判别y的值,只有x和y的值同时为假才需要判别z的值,口诀:“一真必真”。 11 位运算 11.1 位运算符 — 在计算机中,数据都是以二进制数形式存放的,位运算就是指对存储单元中二进制位的运算。C语言提供6种位运算符。 11.2 位运算 — 位运算符 & |~<< >> ∧ 按优先级从高到低排列的顺序是: 位运算符中求反运算“~“优先级最高,而左移和右移相同,居于第二,接下来的顺序是按位与 “&“、按位异或 “∧“和按位或 “|“。顺序为~ << >> & ∧ | 。 例1:左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。="" <="" span=""> 例如:a<<4 指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00(十进制48)。 例2:右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。 例如:设 a=15, a>>2 表示把右移为十进制3)。 应该说明的是,对于有符号数,在右移时,符号位将随同移动。当为正数时,最高位补0,而为负数时,符号位为1,最高位是补0或是补1 取决于编译系统的规定。 例3:设二进制数a是00 ,若通过异或运算a∧b 使a的高4位取反,低4位不变,则二进制数b是。 解析:异或运算常用来使特定位翻转,只要使需翻转的位与1进行异或操作就可以了,因为原数中值为1的位与1进行异或运算得0 ,原数中值为0的位与1进行异或运算结果得1。而与0进行异或的位将保持原值。异或运算还可用来交换两个值,不用临时变量。 C语言在全世界普及推广,无论在中国还是世界各国,C语言都是高等学校一门基本的计算机课程,C语言也在计算机领域发挥重要的作用。

    时间:2021-03-28 关键词: 标识符 关键字 C语言

  • C语言模拟实现字符串操作函数

    前言 在编写程序过程中,我们经常使用到一些字符串函数,例如求字符串长度,拷贝字符串……,这些函数都在C标准库中存在,我们可以直接使用。但我们还需要掌握这些函数的实现方法,今天来看看一些常用的字符串操作函数的实现方法。 带长度参数的函数 1. strlen strlen是用来求字符串长度的函数,字符串长度就是字符串中包含的字符的个数,但是不包含字符串结尾的 ‘\0’ 实现strlen有三种方法: (1)定义一个计数器 size_t mystrlen(const char* str) { size_t count = 0; while (*str != '\0')     {         count++;         str++;     } return count; } (2)递归版本 size_t my_strlen(const char *str) { if (*str == '\0') return 0; else return my_strlen(str + 1) + 1; } (3)利用指针 - 指针 size_t mystrlen(const char* str) { const char* end = str; while (*end++); return end - str - 1; } 2.strcpy 用于复制字符串的函数是strcpy,它的原型如下: char* strcpy ( char* dest, const char* src ); 使用这个函数时,要注意几点 (1)目标字符数组的空间必须足够大,足以容纳需要复制的字符串 (2)目标字符数组要可以被修改 (3)被复制的字符串要可以找到’\ 0’ char *mystrcpy(char *dest, const char *src) { char *res = dest;     assert(dest);     assert(src); while (*dest++ = *src++) ;//注意这里是一个等号 return res; } 3.strcat strcat函数是可以把一个字符串添加(连接)到另一个字符串的后面。strcat函数要求dest参数原先已经包含了一个字符串(可以是空字符串)。它找到这个字符串的末尾,并把src字符串的一份拷贝添加到这个位置。 char *mystrcat(char *dest, const char *src) { char *res = dest;     assert(dest);     assert(src); while (*dest != '\0')         dest++; while (*dest++ = *src); //这里同样也是一个等号 return res; } 4.strcmp strcmp用于比较两个字符串,及对两个字符串对应的字符逐个进行比较,直到发现不匹配。那个最先不匹配的字符中较“小”的那个字符所在的字符串被认为“小于”另外一个字符串。如果其中一个字符串是另外一个字符串的前面一部分,那么它也被认为“小于”另外一个字符串,因为它的’\0’出现的更早。 int my_strcmp(const char* src1, const char* src2) { while (*src1 == *src2)     { if (*src1 == '\0') return 0;          src1 ++;          src2 ++;      } return *src1 - *src2;  } 5.strstr 为了在一个字符串中查找一个子串,可以使用strstr函数,该函数是在s1中查找整个s2第1次出现的起始位置,并返回一个指向该位置的指针。如果s2并没有出现在s1的任何地方,函数将返回一个NULL指针。如果第二个函数是一个空字符串,函数就返回s1。 char* my_strstr(char* s1, const char* s2) { char* p = s1; const char* q = s2; char* cur = NULL;     assert(s1);     assert(s2); if (*s2 == '\0') return s1; while (*p != '\0')     {         cur = p; while ((*p != '\0') && (*q != '\0') && (*p == *q))         {             p++;             q++;         } if (*q == '\0') return cur;         p = cur + 1;         q = s2;     } return NULL; } 6.strchr strchr是用来查找一个特定的字符,在字符串str中查找字符ch第一次出现的位置,找到后函数返回一个指向该位置的指针。如果该字符并不存在于字符串中,函数就返回一个NULL指针 char* my_strchr(const char* str, char ch) { const char* tmp = str; while (*tmp)     { if (*tmp == ch) return tmp;         tmp++;     } return NULL; } 7.strrchr 与strchr类似的查找函数还有一个是strrchr,它和strchr的不同之处在于,该函数返回的是一个指向字符串中该字符最后一次出现的位置 char* my_strrchr(const char* str, int ch) { char* pos = 0;     assert(str); while (*str)     { if (*str == ch)         {             pos = str;         }         str++;     } if (pos != 0)     { return pos;     } else return NULL; } 长度受限制的字符串函数 标准库中还包含一些函数,它们以一种不同的方式去处理字符串。这些函数接受一个显示的长度参数,用于限定进行复制或比较的字符数。 1.strncpy 和strcpy一样,strncpy()函数把源字符串的字符复制到目标空间,但是,它总是正好向dest中拷贝len个字符,如果strlen的(src)的值小于len,dest数组就用额外的’\0’填充到len字节长度。如果strlen的(src)的值大于或等于len,那么只有len个字符被复制到目标寄存器中。 char* my_strncpy(char* dest, const char* src, size_t len) { char* res = dest;     assert(dest);     assert(src); while (len--)     {         *res++ = *src++;     } if (*(res) != '\0')         *res = '\0'; return dest; } 2.strncat strncat函数,它从src中最多复制的len个字符到目标数组的后面。 char* my_strncat(char* dest, const char* src, size_t len) { char* res = dest;     assert(dest);     assert(src); while (*dest != '\0')         dest++; while (len--)     {         *dest = *src;         dest++;         src++;     } return res; } 3.strncmp strncmp也用于比较两个字符串,但它最多比较len个字节。如果两个字符串在第len个字符之前存在不相等的字符,这个函数就像的strcmp一样停止比较,返回结果。如果两个字符串的前len个字符相等,函数就返回零。 int my_strncmp(const char* s1, const char* s2, size_t len) {     assert(s1);     assert(s2); while (len--)     { if (*s1 == *s2)         {             s1++;             s2++;         } else return *s1 - *s2;     } return 0; } 标准库里的字符串函数还有很多,今天就先介绍到这里。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-23 关键词: 字符串 C语言

  • 不按套路出牌,这么来写IIC驱动?

    前言 前面分享了:干货 | C语言实现面向对象编程(附代码),分享了一些概念及基础例子。这不,给大家找来了嵌入式中的编程实例:C语言使用面向对象实现IIC驱动。 一.简述 使用面向对象的编程思想封装IIC驱动,将IIC的属性和操作封装成一个库,在需要创建一个IIC设备时只需要实例化一个IIC对象即可,本文是基于STM32和HAL库做进一步封装的。 底层驱动方法不重要,封装的思想很重要。在完成对IIC驱动的封装之后借助继承特性实现AT24C64存储器的驱动开发,仍使用面向对象的思想封装AT24C64驱动。 二.IIC驱动面向对象封装 iic.h头文件主要是类模板的定义,具体如下: //定义IIC类 typedef struct IIC_Type { //属性 GPIO_TypeDef  *GPIOx_SCL; //GPIO_SCL所属的GPIO组(如:GPIOA) GPIO_TypeDef  *GPIOx_SDA; //GPIO_SDA所属的GPIO组(如:GPIOA) uint32_t GPIO_SCL; //GPIO_SCL的IO引脚(如:GPIO_PIN_0) uint32_t GPIO_SDA; //GPIO_SDA的IO引脚(如:GPIO_PIN_0) //操作 void (*IIC_Init)(const struct IIC_Type*); //IIC_Init void (*IIC_Start)(const struct IIC_Type*); //IIC_Start void (*IIC_Stop)(const struct IIC_Type*); //IIC_Stop uint8_t (*IIC_Wait_Ack)(const struct IIC_Type*); //IIC_Wait_ack,返回wait失败或是成功 void (*IIC_Ack)(const struct IIC_Type*); //IIC_Ack,IIC发送ACK信号 void (*IIC_NAck)(const struct IIC_Type*); //IIC_NAck,IIC发送NACK信号 void (*IIC_Send_Byte)(const struct IIC_Type*,uint8_t); //IIC_Send_Byte,入口参数为要发送的字节 uint8_t (*IIC_Read_Byte)(const struct IIC_Type*,uint8_t); //IIC_Send_Byte,入口参数为是否要发送ACK信号 void (*delay_us)(uint32_t); //us延时 }IIC_TypeDef; iic.c源文件主要是类模板具体操作函数的实现,具体如下: //设置SDA为输入模式 static void SDA_IN(const struct IIC_Type* IIC_Type_t) { uint8_t io_num = 0; //定义io Num号 switch(IIC_Type_t->GPIO_SDA)   { case GPIO_PIN_0:     io_num = 0; break; case GPIO_PIN_1:     io_num = 1; break; case GPIO_PIN_2:     io_num = 2; break; case GPIO_PIN_3:     io_num = 3; break; case GPIO_PIN_4:     io_num = 4; break; case GPIO_PIN_5:     io_num = 5; break; case GPIO_PIN_6:     io_num = 6; break; case GPIO_PIN_7:     io_num = 7; break; case GPIO_PIN_8:     io_num = 8; break; case GPIO_PIN_9:     io_num = 9; break; case GPIO_PIN_10:     io_num = 10; break; case GPIO_PIN_11:     io_num = 11; break; case GPIO_PIN_12:     io_num = 12; break; case GPIO_PIN_13:     io_num = 13; break; case GPIO_PIN_14:     io_num = 14; break; case GPIO_PIN_15:     io_num = 15; break;   }   IIC_Type_t->GPIOx_SDA->MODER&=~(3<<(io_num*2)); //将GPIOx_SDA->GPIO_SDA清零 IIC_Type_t->GPIOx_SDA->MODER|=0<<(io_num*2); //将GPIOx_SDA->GPIO_SDA设置为输入模式 } //设置SDA为输出模式 static void SDA_OUT(const struct IIC_Type* IIC_Type_t) { uint8_t io_num = 0; //定义io Num号 switch(IIC_Type_t->GPIO_SDA)   { case GPIO_PIN_0:     io_num = 0; break; case GPIO_PIN_1:     io_num = 1; break; case GPIO_PIN_2:     io_num = 2; break; case GPIO_PIN_3:     io_num = 3; break; case GPIO_PIN_4:     io_num = 4; break; case GPIO_PIN_5:     io_num = 5; break; case GPIO_PIN_6:     io_num = 6; break; case GPIO_PIN_7:     io_num = 7; break; case GPIO_PIN_8:     io_num = 8; break; case GPIO_PIN_9:     io_num = 9; break; case GPIO_PIN_10:     io_num = 10; break; case GPIO_PIN_11:     io_num = 11; break; case GPIO_PIN_12:     io_num = 12; break; case GPIO_PIN_13:     io_num = 13; break; case GPIO_PIN_14:     io_num = 14; break; case GPIO_PIN_15:     io_num = 15; break;   }   IIC_Type_t->GPIOx_SDA->MODER&=~(3<<(io_num*2)); //将GPIOx_SDA->GPIO_SDA清零 IIC_Type_t->GPIOx_SDA->MODER|=1<<(io_num*2); //将GPIOx_SDA->GPIO_SDA设置为输出模式 } //设置SCL电平 static void IIC_SCL(const struct IIC_Type* IIC_Type_t,int n) { if(n == 1)   {     HAL_GPIO_WritePin(IIC_Type_t->GPIOx_SCL,IIC_Type_t->GPIO_SCL,GPIO_PIN_SET); //设置SCL为高电平 } else{     HAL_GPIO_WritePin(IIC_Type_t->GPIOx_SCL,IIC_Type_t->GPIO_SCL,GPIO_PIN_RESET); //设置SCL为低电平 } } //设置SDA电平 static void IIC_SDA(const struct IIC_Type* IIC_Type_t,int n) { if(n == 1)   {     HAL_GPIO_WritePin(IIC_Type_t->GPIOx_SDA,IIC_Type_t->GPIO_SDA,GPIO_PIN_SET); //设置SDA为高电平 } else{     HAL_GPIO_WritePin(IIC_Type_t->GPIOx_SDA,IIC_Type_t->GPIO_SDA,GPIO_PIN_RESET); //设置SDA为低电平 } } //读取SDA电平 static uint8_t READ_SDA(const struct IIC_Type* IIC_Type_t) { return HAL_GPIO_ReadPin(IIC_Type_t->GPIOx_SDA,IIC_Type_t->GPIO_SDA); //读取SDA电平 } //IIC初始化 static void IIC_Init_t(const struct IIC_Type* IIC_Type_t) {       GPIO_InitTypeDef GPIO_Initure; //根据GPIO组初始化GPIO时钟 if(IIC_Type_t->GPIOx_SCL == GPIOA || IIC_Type_t->GPIOx_SDA == GPIOA)    {      __HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟 } if(IIC_Type_t->GPIOx_SCL == GPIOB || IIC_Type_t->GPIOx_SDA == GPIOB)    {      __HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB时钟 } if(IIC_Type_t->GPIOx_SCL == GPIOC || IIC_Type_t->GPIOx_SDA == GPIOC)    {      __HAL_RCC_GPIOC_CLK_ENABLE(); //使能GPIOC时钟 } if(IIC_Type_t->GPIOx_SCL == GPIOD || IIC_Type_t->GPIOx_SDA == GPIOD)    {      __HAL_RCC_GPIOD_CLK_ENABLE(); //使能GPIOD时钟 } if(IIC_Type_t->GPIOx_SCL == GPIOE || IIC_Type_t->GPIOx_SDA == GPIOE)    {      __HAL_RCC_GPIOE_CLK_ENABLE(); //使能GPIOE时钟 } if(IIC_Type_t->GPIOx_SCL == GPIOH || IIC_Type_t->GPIOx_SDA == GPIOH)    {      __HAL_RCC_GPIOH_CLK_ENABLE(); //使能GPIOH时钟 } //GPIO_SCL初始化设置 GPIO_Initure.Pin=IIC_Type_t->GPIO_SCL;      GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FREQ_VERY_HIGH; //快速 HAL_GPIO_Init(IIC_Type_t->GPIOx_SCL,&GPIO_Initure); //GPIO_SDA初始化设置 GPIO_Initure.Pin=IIC_Type_t->GPIO_SDA;      GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FREQ_VERY_HIGH; //快速 HAL_GPIO_Init(IIC_Type_t->GPIOx_SDA,&GPIO_Initure); //SCL与SDA的初始化均为高电平 IIC_SCL(IIC_Type_t,1);        IIC_SDA(IIC_Type_t,1); } //IIC Start static void IIC_Start_t(const struct IIC_Type* IIC_Type_t) {   SDA_OUT(IIC_Type_t); //sda线输出 IIC_SDA(IIC_Type_t,1);         IIC_SCL(IIC_Type_t,1);   IIC_Type_t->delay_us(4);    IIC_SDA(IIC_Type_t,0); //START:when CLK is high,DATA change form high to low IIC_Type_t->delay_us(4);   IIC_SCL(IIC_Type_t,0); //钳住I2C总线,准备发送或接收数据 } //IIC Stop static void IIC_Stop_t(const struct IIC_Type* IIC_Type_t) {   SDA_OUT(IIC_Type_t); //sda线输出 IIC_SCL(IIC_Type_t,0);   IIC_SDA(IIC_Type_t,0); //STOP:when CLK is high DATA change form low to high IIC_Type_t->delay_us(4);   IIC_SCL(IIC_Type_t,1);    IIC_SDA(IIC_Type_t,1); //发送I2C总线结束信号 IIC_Type_t->delay_us(4);  } //IIC_Wait_ack 返回HAL_OK表示wait成功,返回HAL_ERROR表示wait失败 static uint8_t IIC_Wait_Ack_t(const struct IIC_Type* IIC_Type_t) //IIC_Wait_ack,返回wait失败或是成功 { uint8_t ucErrTime = 0;   SDA_IN(IIC_Type_t); //SDA设置为输入 IIC_SDA(IIC_Type_t,1);IIC_Type_t->delay_us(1);      IIC_SCL(IIC_Type_t,1);IIC_Type_t->delay_us(1); while(READ_SDA(IIC_Type_t))   {     ucErrTime++; if(ucErrTime>250)     {       IIC_Type_t->IIC_Stop(IIC_Type_t); return HAL_ERROR;     }   }   IIC_SCL(IIC_Type_t,0);//时钟输出0 return HAL_OK;   } //产生ACK应答 static void IIC_Ack_t(const struct IIC_Type* IIC_Type_t) {   IIC_SCL(IIC_Type_t,0);   SDA_OUT(IIC_Type_t);   IIC_SDA(IIC_Type_t,0);   IIC_Type_t->delay_us(2);     IIC_SCL(IIC_Type_t,1);   IIC_Type_t->delay_us(2);     IIC_SCL(IIC_Type_t,0); } //产生NACK应答 static void IIC_NAck_t(const struct IIC_Type* IIC_Type_t) {   IIC_SCL(IIC_Type_t,0);   SDA_OUT(IIC_Type_t);   IIC_SDA(IIC_Type_t,1);   IIC_Type_t->delay_us(2);     IIC_SCL(IIC_Type_t,1);   IIC_Type_t->delay_us(2);     IIC_SCL(IIC_Type_t,0); } //IIC_Send_Byte,入口参数为要发送的字节 static void IIC_Send_Byte_t(const struct IIC_Type* IIC_Type_t,uint8_t txd) { uint8_t t = 0;         SDA_OUT(IIC_Type_t);            IIC_SCL(IIC_Type_t,0);//拉低时钟开始数据传输 for(t=0;t<8;t++)      {                         IIC_SDA(IIC_Type_t,(txd&0x80)>>7);           txd <<= 1;            IIC_Type_t->delay_us(2); //对TEA5767这三个延时都是必须的 IIC_SCL(IIC_Type_t,1);        IIC_Type_t->delay_us(2);          IIC_SCL(IIC_Type_t,0);         IIC_Type_t->delay_us(2);        }   } //IIC_Send_Byte,入口参数为是否要发送ACK信号 static uint8_t IIC_Read_Byte_t(const struct IIC_Type* IIC_Type_t,uint8_t ack) { uint8_t i,receive = 0;    SDA_IN(IIC_Type_t);//SDA设置为输入 for(i=0;i<8;i++ )    {       IIC_SCL(IIC_Type_t,0);        IIC_Type_t->delay_us(2);       IIC_SCL(IIC_Type_t,1);       receive<<=1; if(READ_SDA(IIC_Type_t))receive++;          IIC_Type_t->delay_us(1);    } if (!ack)          IIC_Type_t->IIC_NAck(IIC_Type_t);//发送nACK else IIC_Type_t->IIC_Ack(IIC_Type_t); //发送ACK return receive; } //实例化一个IIC1外设,相当于一个结构体变量,可以直接在其他文件中使用 IIC_TypeDef IIC1 = {   .GPIOx_SCL = GPIOA, //GPIO组为GPIOA .GPIOx_SDA = GPIOA, //GPIO组为GPIOA .GPIO_SCL = GPIO_PIN_5, //GPIO为PIN5 .GPIO_SDA = GPIO_PIN_6, //GPIO为PIN6 .IIC_Init = IIC_Init_t,   .IIC_Start = IIC_Start_t,   .IIC_Stop = IIC_Stop_t,   .IIC_Wait_Ack = IIC_Wait_Ack_t,   .IIC_Ack = IIC_Ack_t,   .IIC_NAck = IIC_NAck_t,   .IIC_Send_Byte = IIC_Send_Byte_t,   .IIC_Read_Byte = IIC_Read_Byte_t,   .delay_us = delay_us //需自己外部实现delay_us函数 }; 上述就是IIC驱动的封装,由于没有应用场景暂不测试其实用性,待下面ATC64的驱动缝缝扎黄写完之后一起测试使用。 三.ATC64XX驱动封装实现 at24cxx.h头文件主要是类模板的定义,具体如下: // 以下是共定义个具体容量存储器的容量 #define AT24C01  127 #define AT24C02  255 #define AT24C04  511 #define AT24C08  1023 #define AT24C16  2047 #define AT24C32  4095 #define AT24C64   8191 //8KBytes #define AT24C128 16383 #define AT24C256 32767 //定义AT24CXX类 typedef struct AT24CXX_Type { //属性 u32 EEP_TYPE; //存储器类型(存储器容量) //操作 IIC_TypeDef IIC; //IIC驱动 uint8_t (*AT24CXX_ReadOneByte)(const struct AT24CXX_Type*,uint16_t); //指定地址读取一个字节 void (*AT24CXX_WriteOneByte)(const struct AT24CXX_Type*,uint16_t,uint8_t); //指定地址写入一个字节 void (*AT24CXX_WriteLenByte)(uint16_t,uint32_t,uint8_t); //指定地址开始写入指定长度的数据 uint32_t (*AT24CXX_ReadLenByte)(uint16_t,uint8_t); //指定地址开始读取指定长度数据 void (*AT24CXX_Write)(uint16_t,uint8_t *,uint16_t); //指定地址开始写入指定长度的数据 void (*AT24CXX_Read)(uint16_t,uint8_t *,uint16_t); //指定地址开始写入指定长度的数据 void (*AT24CXX_Init)(const struct AT24CXX_Type*); //初始化IIC uint8_t (*AT24CXX_Check)(const struct AT24CXX_Type*); //检查器件 }AT24CXX_TypeDef; extern AT24CXX_TypeDef AT24C_64; //外部声明实例化AT24CXX对象 at24cxx.c源文件主要是类模板具体操作函数的实现,具体如下: //在AT24CXX指定地址读出一个数据 //ReadAddr:开始读数的地址 //返回值  :读到的数据 static uint8_t AT24CXX_ReadOneByte_t(const struct AT24CXX_Type* AT24CXX_Type_t,uint16_t ReadAddr) { uint8_t temp=0;                             AT24CXX_Type_t->IIC.IIC_Start(&AT24CXX_Type_t->IIC); //根据AT的型号发送不同的地址 if(AT24CXX_Type_t->EEP_TYPE > AT24C16)   {     AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,0XA0); //发送写命令 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);     AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,ReadAddr>>8);//发送高地址 }else AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);    AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,ReadAddr%256); //发送低地址 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);        AT24CXX_Type_t->IIC.IIC_Start(&AT24CXX_Type_t->IIC);           AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,0XA1); //进入接收模式 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);     temp=AT24CXX_Type_t->IIC.IIC_Read_Byte(&AT24CXX_Type_t->IIC,0);        AT24CXX_Type_t->IIC.IIC_Stop(&AT24CXX_Type_t->IIC);//产生一个停止条件 return temp; } //在AT24CXX指定地址写入一个数据 //WriteAddr  :写入数据的目的地址 //DataToWrite:要写入的数据 static void AT24CXX_WriteOneByte_t(const struct AT24CXX_Type* AT24CXX_Type_t,uint16_t WriteAddr,uint8_t DataToWrite) {                                    AT24CXX_Type_t->IIC.IIC_Start(&AT24CXX_Type_t->IIC); if(AT24CXX_Type_t->EEP_TYPE > AT24C16)   {     AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,0XA0); //发送写命令 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);     AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,WriteAddr>>8);//发送高地址 }else AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);     AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,WriteAddr%256); //发送低地址 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);                  AT24CXX_Type_t->IIC.IIC_Send_Byte(&AT24CXX_Type_t->IIC,DataToWrite); //发送字节 AT24CXX_Type_t->IIC.IIC_Wait_Ack(&AT24CXX_Type_t->IIC);                AT24CXX_Type_t->IIC.IIC_Stop(&AT24CXX_Type_t->IIC);//产生一个停止条件 AT24CXX_Type_t->IIC.delay_us(10000);   } //在AT24CXX里面的指定地址开始写入长度为Len的数据 //该函数用于写入16bit或者32bit的数据. //WriteAddr  :开始写入的地址 //DataToWrite:数据数组首地址 //Len        :要写入数据的长度2,4 static void AT24CXX_WriteLenByte_t(uint16_t WriteAddr,uint32_t DataToWrite,uint8_t Len) { uint8_t t; for(t=0;t>(8*t))&0xff);   }                 } //在AT24CXX里面的指定地址开始读出长度为Len的数据 //该函数用于读出16bit或者32bit的数据. //ReadAddr   :开始读出的地址 //返回值     :数据 //Len        :要读出数据的长度2,4 static uint32_t AT24CXX_ReadLenByte_t(uint16_t ReadAddr,uint8_t Len) { uint8_t t; uint32_t temp=0; for(t=0;t

    时间:2021-03-16 关键词: IIC驱动 C语言

  • 干货 | C语言实现面向对象编程(附代码)

    前言 GOF的《设计模式》一书的副标题叫做“可复用面向对象软件的基础”,从标题就能看出面向对象是设计模式基本思想。 由于C语言并不是面向对象的语言,C语言没有直接提供封装、继承、组合、多态等面向对象的功能,但C语言有struct和函数指针。我们可以用struct中的数据和函数指针,以此来模拟对象和类的行为。 所以在正式开始设计模式前,先看看如何用C语言实现面向对象编程。 本章针对面向对象的封装、继承、组合、多态给出C语言的实现方法。 封装 封装是指对象仅暴露必要的对外接口(这里指public方法)来和其它对象进行交互,其它的属性和行为都无需暴露,这使得对象的内部实现可以自由修改。 这也要求对象包含它能进行操作所需要的所有信息,不必依赖其它对象来完成自己的操作。 以下以电力公司的例子演示封装。 电力公司生产并提供电力。为了汇聚各种发电厂的电力并让用户获得电力,电力公司提供了两个统一接口: 1、电力公司汇聚各种发电厂的电力,无论是火力发电厂、水力发电厂、原子能发电厂等都使用一个接口。如果什么时候一家火力发电厂改造成了风力发电厂,发电厂的实现完全不一样了,但接口不变,所以电力公司感觉不到发电厂变了,不需要为了发电厂实现升级而改造电力公司的系统。 2、电力公司向用户提供电力,无论用户用电的设备是烤面包机还是洗衣机,电力公司和用户之间都使用一个接口(电源插座)。用户的用电设备可以千变万化,但接口(电源插座)不变。所以电力公司不用关系用户的什么设备在用电。 代码: #include struct PowerCompany { int powerReserve; void (*PowerPlant)(struct PowerCompany *this, int power); void (*PowerUser)(struct PowerCompany *this, int power); }; void PowerPlant(struct PowerCompany *this, int power) { this->powerReserve += power; printf("默认发电厂,发电%d瓦\n", power); return;   } void PowerUser(struct PowerCompany *this, int power) { if (this->powerReserve >= power) { printf("用电%d瓦\n", power); this->powerReserve -= power;     } else { printf("电力不足,用电失败\n");     } return; } /* struct PowerCompany 的构造函数 */ void PowerCompany(struct PowerCompany *this) { this->powerReserve = 0; this->PowerPlant = PowerPlant; this->PowerUser = PowerUser; return; } /* struct PowerCompany 的析构函数 */ void _PowerCompany(struct PowerCompany *this) { } int main(void) { struct PowerCompany myPowerCompany; PowerCompany(&myPowerCompany); /* 发电 */ myPowerCompany.PowerPlant(&myPowerCompany, 1000); /* 用电 */ myPowerCompany.PowerUser(&myPowerCompany, 800);     myPowerCompany.PowerUser(&myPowerCompany, 800);          _PowerCompany(&myPowerCompany); return 0; } 从电力公司的例子中可以看出,良好的封装可以有效减少耦合性,封装内部实现可以自由修改,对系统的其它部分没有影响。 继承 面向对象编程最强大的功能之一就是代码重用,而继承就是实现代码重用的主要手段之一。继承允许一个类继承另一个类的属性和方法。 我们可以通过识别事物之间的共性,通过抽象公共属性和行为来构造父类,而通过继承父类来构造各子类。父类,即公共属性和行为,就得到了复用。 以下哺乳动物的例子演示继承。 猫和狗都是哺乳动物,它们具有公共的属性和行为。比如,猫和狗都有眼睛,且它们都会叫。 我们把眼睛的颜色、会叫抽象出来,作为哺乳动物父类的属性,让猫类、狗类都继承哺乳动物父类,可实现对”眼睛的颜色“、”会叫“实现的复用。 UML: 代码: #include struct Mammal { int eyeColor; void (*ShowEyeColor)(struct Mammal *this); int callNum; void (*Call)(struct Mammal *this); }; void ShowEyeColor(struct Mammal *this) { if (this->eyeColor == 1) { printf("眼睛是绿色\n");     } else { printf("眼睛是蓝色\n");     } return; } void Call(struct Mammal *this) { printf("叫%d声\n", this->callNum); return; } // struct Mammal 的构造函数 void Mammal(struct Mammal *this, int eyeColor, int callNum) { this->eyeColor = eyeColor; this->ShowEyeColor = ShowEyeColor; this->callNum = callNum; this->Call = Call; return;   } struct Dog { struct Mammal mammal; }; // struct Dog 的构造函数 void Dog(struct Dog *this, int eyeColor, int callNum) {     Mammal(this, eyeColor, callNum); // 狗类的其它属性,略 return; } // struct Dog 的析构函数 void _Dog(struct Dog *this) { } struct Cat { struct Mammal mammal; // 猫类的其它属性,略 }; // struct Cat 的构造函数 void Cat(struct Cat *this, int eyeColor, int callNum) {     Mammal(this, eyeColor, callNum); return; } // struct Cat 的析构函数 void _Cat(struct Cat *this) { } int main(void) { struct Dog myDog; Dog(&myDog, 1, 3);     myDog.mammal.ShowEyeColor(&myDog);     myDog.mammal.Call(&myDog);     _Dog(&myDog); struct Cat myCat; Cat(&myCat, 2, 5);     myCat.mammal.ShowEyeColor(&myCat);     myCat.mammal.Call(&myCat);     _Cat(&myCat); return 0; } 多态 多态与继承是紧耦合的关系,但它通常作为面向对象技术中最强大的优点之一。 子类从继承父类的接口,每个子类是单独的实体,每个子类需要对同一消息有单独的应答。 每个子类对同一消息的应答采用继承的相同接口,但每个子类可以有不同的实现,这就是多态。 在猫和狗的例子中,猫类、狗类都继承了哺乳动物父类的“叫”的方法,但猫类、狗类的叫声并不一样,所以猫类、狗类可以采用不同的“叫”的实现。 以下代码演示了多态。 代码: #include struct Mammal { int eyeColor; void (*ShowEyeColor)(struct Mammal *this); int callNum; void (*Call)(struct Mammal *this); }; void ShowEyeColor(struct Mammal *this) { if (this->eyeColor == 1) { printf("眼睛是绿色\n");     } else { printf("眼睛是蓝色\n");     } return; } void Call(struct Mammal *this) { printf("叫%d声\n", this->callNum); return; } /* struct Mammal 的构造函数 */ void Mammal(struct Mammal *this, int eyeColor, int callNum) { this->eyeColor = eyeColor; this->ShowEyeColor = ShowEyeColor; this->callNum = callNum; this->Call = Call; return;   } struct Dog { struct Mammal mammal; }; void Bark(struct Dog *this) { int i; for (i = 0; i < this->mammal.callNum; i++) { printf("汪 ");     } printf("\n"); return; } /* struct Dog 的构造函数 */ void Dog(struct Dog *this, int eyeColor, int callNum) {     Mammal(this, eyeColor, callNum); this->mammal.Call = Bark; return; } // struct Dog 的析构函数 void _Dog(struct Dog *this) { } struct Cat { struct Mammal mammal; }; void Meow(struct Cat *this) { int i; for (i = 0; i < this->mammal.callNum; i++) { printf("喵 ");     } printf("\n"); return; } /* struct Cat 的构造函数 */ void Cat(struct Cat *this, int eyeColor, int callNum) {     Mammal(this, eyeColor, callNum); this->mammal.Call = Meow; return; } // struct Cat 的析构函数 void _Cat(struct Cat *this) { } int main(void) { struct Dog myDog; Dog(&myDog, 1, 3); struct Cat myCat; Cat(&myCat, 2, 5); struct Mammal *myMammal; myMammal = &myDog;     myMammal->Call(myMammal);     myMammal = &myCat;     myMammal->Call(myMammal);     _Dog(&myDog);     _Cat(&myCat); return 0; } 组合 组合与继承都是面向对象中代码复用的方式,也只有通过组合和继承两种方式能够实现使用其他类构建新类。 在前面讲的继承关系中,我们把通用属性和行为抽象出来作为父类。 例如:猫、狗都是哺乳动物,它们具有哺乳动物通用的属性和行为。猫、狗与哺乳动物的关系是“is-a”,即猫、狗(is-a)哺乳动物。而组合关系体现的是“has-a”。以房子和窗户的关系举例。 我们可以单独构建窗户类,然后把窗户类应用到各种房子类上。此时房子(has-a)窗户,但绝不是窗户(is-a)房子。 以下UML和代码演示了组合。 UML: 代码 #include struct Window { int length; int width; void (*ShowWindow)(struct Window *this); }; void ShowWindow(struct Window *this) { printf("这是长%d厘米、宽%d厘米的窗户\n", this->length, this->width); return; } void Window(struct Window *this, int length, int width) { this->length = length; this->width = width; this->ShowWindow = ShowWindow; return; } void _Window(struct Window *this) { } struct House { struct Window *window; int livingRoomNum; int bedRoomNum; int bathRoomNum; void (*ShowHouse)(struct House *this); }; void ShowHouse(struct House *this) { printf("这是%d室%d厅%d卫的房子\n", this->bedRoomNum, this->livingRoomNum, this->bathRoomNum); return; } void House(struct House *this, int livingRoomNum, int bedRoomNum, int bathRoomNum) { this->livingRoomNum = livingRoomNum; this->bedRoomNum = bedRoomNum; this->bathRoomNum = bathRoomNum; this->ShowHouse = ShowHouse; return; } void _House(struct House *this) { } void main() { struct House myHouse; House(&myHouse, 2, 3, 2); /* 组合是一种低耦合,如果不初始化,子类只是存放了一个空指针来占位关联。        此处是与继承关系的区别。继承是一种强耦合,在继承关系中,无论如何子类拥有父类全部的信息。*/ struct Window myWindow1; myHouse.window = &myWindow1;     Window(myHouse.window, 100, 50); /* 通过获得其它对象的引用而在“运行时”动态定义 */ myHouse.ShowHouse(&myHouse);     myHouse.window->ShowWindow(myHouse.window);     _Window();     _House(); return; } 组合和继承的区别有以下几点: 组合关系体现的是“has-a”。继承关系体现的是“is-a”。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-11 关键词: 设计模式 面向过程 C语言

  • 听说,高手都用记事本写C语言代码?

    坊间传闻高手都喜欢用记事本写代码,那么问题来了,我们以C语言为例,如何用记事本编译运行呢? 其实,最简单的方式就是安装GCC编译器,在记事本编写C语言程序,然后再在命令行用GCC编译运行。 下面,简单介绍一下实现过程,感兴趣的朋友可以尝试一下。 安装GCC编译器 这里推荐直接安装MinGW,一个集成编译调试工具,集成了常见的GCC、G++、GDB等命令,可以轻松编译运行C语言程序。 下载的话,直接到官网上下载即可,一个exe文件,直接双击安装就行,安装后主目录如下,这里需要将bin目录添加到环境变量中,后面在cmd窗口就可以直接使用GCC命令编译C语言程序: 打开cmd窗口,输入“gcc -v”命令,如果输出以下信息,则说明GCC命令配置成功: 编译运行程序 GCC命令配置完成后,接着就是编译运行C语言程序了,这里为了方便演示,我用记事本新建了一个C文件,测试代码如下,非常简单,后面就是用GCC编译运行这个程序: 接着在cmd窗口切换到文件所在目录,运行命令“gcc -o test test.c”就可直接编译程序,成功编译后会在当前目录下生成一个exe程序,可以直接运行: 至此,我们就完成了记事本C语言程序的编译和运行。 总的来说,整个过程非常简单,主要就是配置GCC编译器(和Linux非常相似),只要你熟悉一下操作过程,就很容易实现。 当然,还有其他很多方法实现,大家可以自己去了解一下。希望以上分享的内容对大家有所帮助,感谢耐心阅读!

    时间:2021-03-10 关键词: 代码 GCC C语言

  • C语言中,break和continue都是跳出循环,有啥区别?

    首先说明 :continue 只能用于循环语句中,而break可用于循环和 switch 语句,两者都是辅助循环;尽管如此,如果 switch 语句在一个循环中,continue便可作为 switch 语句的一部分;这种情况下,就像在其他循环中一样,continue 让程序跳出循环的剩余部分,包括 switch 语句的其他部分。 一般而言,程序进入循环后,在下一次循环测试之前会执行完循环体内部的所有语句。而continue和break语句可以根据循环体内部的测试结果来忽略一部分循环内容,甚至结束循环。 c 语言中循环语句有 3 种:while(); do     while(); for;且 3 种循环都可以使用 continue 和 break 语句 对于continue语句,执行到该语句时,会跳过本次迭代的剩余部分,并开始下一轮迭代;但是若 continue 语句在嵌套循环的内部,则只会影响包含该语句(即 continue 语句)的内层循环(即内层循环的后面的语句不会被执行,而跳出内层循环后,外层循环内部的语句正常执行。); 然而对于 while() 和 do  while() 循环,执行 continue 语句后的下一个行为是对循环的测试表达式求值,看代码实例: #include  int main() {       //while()     char CH;     int count=0; while(count < 10){ CH = getchar(); if(CH != ' ') continue;         putchar(CH);         count++;     } printf("Hello, World!\n"); return 0; } 对于 for 循环,执行 continue 之后的下一个行为是对更新表达式求值,然后是对循环测试表达式求值,下面的代码示例包括了嵌套循环中使用 continue 的情形:  #include  int main() {     char ch;     int cunt;     int i; for(cunt=0;cunt<10;cunt++){ ch = getchar(); for(i=0;i<5;i++){ if (ch != ' ') continue;             putchar(ch); printf("我是内层循环的---小可爱!!!\n");         } printf("我是外层循环的---小可爱!!!\n"); printf("如果continue语句在嵌套循环内,则只会影响包含continue的内层循环,不影响外层循环!!!\n");     } printf("Hello, World!\n"); return 0; } 对于 break 语句: 程序执行到循环中的break语句时,会终止包含它的循环,并继续执行下一阶段;若break位于嵌套循环内部,它只影响包含它的当前循环。 比较 break 和 continue 对程序执行的不同之处,看下图: continue: continue跳出本次循环,执行下一次循环。 break: break跳出整个循环 下面看代码 while 示例: #include  int main() {     //while()     char CH;     int count=0; while(count < 10){ CH = getchar(); if(CH != ' ') break;         putchar(CH);         count++;     } printf("Hello, World!\n"); return 0; } for循环及嵌套循环示例: 注:只会直接跳出内层循环,外层循环正常执行 #include  int main() {     char ch;     int cunt;     int i; for(cunt=0;cunt<10;cunt++){ ch = getchar(); for(i=0;i<5;i++){ if (ch != ' ') break;             putchar(ch); printf("我是内层循环的---小可爱!!!\n");         } printf("我是外层循环的---小可爱!!!\n"); printf("如果continue语句在嵌套循环内,则只会影响包含continue的内层循环,不影响外层循环!!!\n");     } printf("Hello, World!\n"); return 0; } 要想外层循环一并终止;需要在外层在使用 break; #include  int main() {     char ch;     int cunt;     int i; for(cunt=0;cunt<10;cunt++){ ch = getchar(); for(i=0;i<5;i++){ if (ch != ' ') break;             putchar(ch); printf("我是内层循环的---小可爱!!!\n");          } if (ch != ' ') break; printf("我是外层循环的---小可爱!!!\n"); printf("如果continue语句在嵌套循环内,则只会影响包含continue的内层循环,不影响外层循环!!!\n");     } printf("Hello, World!\n"); return 0; } 在多重选择 switch 语句中使用 continue 和 break的示例: /* animals.c -- uses a switch statement */ #include  #include  int main(void) {     char ch; printf("Give me a letter of the alphabet, and I will give "); printf("an animal name\nbeginning with that letter.\n"); printf("Please type in a letter; type # to end my act.\n"); while ((ch = getchar()) != '#')     { if('\n' == ch) continue; if (islower(ch))     /* lowercase only          */             switch (ch)         { case 'a' : printf("argali, a wild sheep of Asia\n"); break; case 'b' : printf("babirusa, a wild pig of Malay\n"); break; case 'c' : printf("coati, racoonlike mammal\n"); break; case 'd' : printf("desman, aquatic, molelike critter\n"); break; case 'e' : printf("echidna, the spiny anteater\n"); break; case 'f' : printf("fisher, brownish marten\n"); break;             default : printf("That's a stumper!\n");         }                /* end of switch           */ else printf("I recognize only lowercase letters.\n"); while (getchar() != '\n') continue;      /* skip rest of input line */ printf("Please type another letter or a #.\n");     }                        /* while loop end          */ printf("Bye!\n"); return 0; } 在本例中 continue 的作用与上述类似,但是 break 的作用不同:它让程序离开 switch 语句,跳至switch语句后面的下一条语句;如果没有 break 语句,就会从匹配标签开始执行到 switch 末尾; 注:C语言中的 case 一般都指定一个值,不能使用一个范围;switch 在圆括号中的测试表达式的值应该是一个整数值(包括 char 类型);case 标签必须是整数类型(包括 char 类型)的常量 或 整型常量表达式( 即, 表达式中只包含整型常量)。不能使用变量作为 case 的标签 switch中有 break 遇到break后跳出,继续匹配switch。 switch 中 无break 顺序执行每个case。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-03-02 关键词: break continue C语言

  • 常用C语言头文件库的漂亮宏定义

    写好C语言,漂亮的宏定义很重要,使用宏定义可以防止出错,提高可移植性,可读性,方便性等等。下面列举一些成熟软件中常用的宏定义。 1#ifndef COMDEF_H 2#define COMDEF_H 3//头文件内容 4#endif 1typedef unsigned char boolean; /* Boolean value type. */ 2typedef unsigned long int uint32; /* Unsigned 32 bit value */ 3typedef unsigned short uint16; /* Unsigned 16 bit value */ 4typedef unsigned char uint8; /* Unsigned 8 bit value */ 5typedef signed long int int32; /* Signed 32 bit value */ 6typedef signed short int16; /* Signed 16 bit value */ 7typedef signed char int8; /* Signed 8 bit value */ 下面的不建议使用 1typedef unsigned char byte; /* Unsigned 8 bit value type. */ 2typedef unsigned short word; /* Unsinged 16 bit value type. */ 3typedef unsigned long dword; /* Unsigned 32 bit value type. */ 4typedef unsigned char uint1; /* Unsigned 8 bit value type. */ 5typedef unsigned short uint2; /* Unsigned 16 bit value type. */ 6typedef unsigned long uint4; /* Unsigned 32 bit value type. */ 7typedef signed char int1; /* Signed 8 bit value type. */ 8typedef signed short int2; /* Signed 16 bit value type. */ 9typedef long int int4; /* Signed 32 bit value type. */ 10typedef signed long sint31; /* Signed 32 bit value */ 11typedef signed short sint15; /* Signed 16 bit value */ 12typedef signed char sint7; /* Signed 8 bit value */ 1#define MEM_B( x ) ( *( (byte *) (x) ) ) 2#define MEM_W( x ) ( *( (word *) (x) ) ) 1#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) ) 2#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) ) 1#define FPOS( type, field ) \ 2/*lint -e545 */ ( (dword) &(( type *) 0)-> field ) /*lint +e545 */ 1#define FSIZ( type, field ) sizeof( ((type *) 0)->field ) 1#define FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] ) 1#define FLOPW( ray, val ) \ 2(ray)[0] = ((val) / 256); \ 3(ray)[1] = ((val) & 0xFF) 1#define B_PTR( var ) ( (byte *) (void *) &(var) ) 2#define W_PTR( var ) ( (word *) (void *) &(var) ) 1#define WORD_LO(xxx) ((byte) ((word)(xxx) & 255)) 2#define WORD_HI(xxx) ((byte) ((word)(xxx) >> 8)) 1#define RND8( x ) ((((x) + 7) / 8 ) * 8 ) 1#define UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) ) 1#define DECCHK( c ) ((c) >= '0' && (c) <= '9') 1#define HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') ||\ 2((c) >= 'A' && (c) <= 'F') ||\ 3((c) >= 'a' && (c) <= 'f') ) 1#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val)) 1#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) ) 1#define MOD_BY_POWER_OF_TWO( val, mod_by ) \ 2( (dword)(val) & (dword)((mod_by)-1) ) 1#define inp(port) (*((volatile byte *) (port))) 2#define inpw(port) (*((volatile word *) (port))) 3#define inpdw(port) (*((volatile dword *)(port))) 4#define outp(port, val) (*((volatile byte *) (port)) = ((byte) (val))) 5#define outpw(port, val) (*((volatile word *) (port)) = ((word) (val))) 6#define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val))) A N S I标准说明了五个预定义的宏名。它们是: 1_ L I N E _ 2_ F I L E _ 3_ D A T E _ 4_ T I M E _ 5_ S T D C _ 如果编译不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序也许还提供其它预定义的宏名。 _ L I N E _及 _ F I L E _宏指令在有关 # l i n e的部分中已讨论,这里讨论其余的宏名。 _ D AT E _宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期。 源代码翻译到目标代码的时间作为串包含在 _ T I M E _中。串形式为时:分:秒。 如果实现是标准的,则宏 _ S T D C _含有十进制常量1。如果它含有任何其它数,则实现是非标准的。 可以定义宏,例如: 当定义了 _DEBUG,输出数据信息和所在文件所在行 1#ifdef _DEBUG 2#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_) 3#else 4#define DEBUGMSG(msg,date) 5#endif 例如: 1#define ADD(a,b) (a+b) 用 do{}while(0)语句包含多语句防止错误 例如: 1#difne DO(a,b) a+b;\ 2a++; 应用时: 1if(….) 2DO(a,b); //产生错误 3else 解决方法: 1#difne DO(a,b) do{a+b;\ 2a++;}while(0)

    时间:2021-03-01 关键词: 宏定义 C语言

  • 10张图让你彻底理解回调函数

    大家好,以前写过一篇关于回调函数的文章C语言函数指针之回调函数,今天又安排了一篇。 不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数? 这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器。 一切要从这样的需求说起 假设你们公司要开发下一代国民App“明日油条”,一款主打解决国民早餐问题的App,为了加快开发进度,这款应用由A小组和B小组协同开发。 其中有一个核心模块由A小组开发然后供B小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()。 如果make_youtiao()这个函数执行的很快并可以立即返回,那么B小组的同学只需要: 调用make_youtiao() 等待该函数执行完成 该函数执行完后继续后续流程 从程序执行的角度看这个过程是这样的: 保存当前被执行函数的上下文 开始执行make_youtiao()这个函数 make_youtiao()执行完后,控制转回到调用函数中 如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。 现实并不容易 现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。 这时你该怎么办呢?想一想这个问题。 可能有的同学会问,和刚才一样直接调用不可以吗,这样多简单。 是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。 想一想直接调用会有什么问题? 显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。 这并不是一种高效的做法。 没有一个程序员想死盯着屏幕10分钟后才能得到结果。 那么有没有一种更加高效的做法呢? 想一想我们上一篇中那个一直盯着你写代码的老板(见《从小白到高手,你需要理解同步与异步》),我们已经知道了这种一直等待直到另一个任务完成的模式叫做同步。 如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步。 回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样: 在这种情况下,回调(callback)就必须出场了。 为什么我们需要回调callback 有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。 假设我们“明日油条”App代码第一版是这样写的: make_youtiao(10000);sell(); 可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。 我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的: “制作10000个油条, 炸好后卖出去 ”,因此调用make_youtiao就变出这样了: make_youtiao(10000, sell); 看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外 还可以指定制作好后该干什么 ,第二个被make_youtiao这个函数调用的函数就叫回调,callback。 现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样: make_youtiao这个函数是怎么实现的呢,很简单: void make_youtiao(int num, func call_back) { // 制作油条 call_back(); //执行回调 } 这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。 有的同学可能还是有疑问,为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢? 不要忘了明日油条这个App是由A小组和B小组同时开发的,A小组在编写make_youtiao时怎么知道B小组要怎么用这个模块,假设A小组真的自己定义sell函数就会这样写: void make_youtiao(int num) { real_make_youtiao(num); sell(); //执行回调 } 同时A小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么A小组该怎么写呢? void make_youtiao(int num) { real_make_youtiao(num); if (Team_B) { sell(); // 执行回调 } else if (Team_D) { store(); // 放到仓库 }} 故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗? 这样的话A小组的同学只需要维护make_youtiao这个函数就能做到工作量饱满了,显然这是一种非常糟糕的设计。 所以你会看到,制作完油条后接下来该做什么不是实现make_youtiao的A小组该关心的事情,很明显只有调用make_youtiao这个函数的使用方才知道。 因此make_youtiao的A小组完全可以通过回调函数将接下来该干什么交给调用方实现,A小组的同学只需要针对回调函数这一抽象概念进行编程就好了,这样调用方在制作完油条后不管是卖掉、放到库存还是自己吃掉等等想做什么都可以,A小组的make_youtiao函数根本不用做任何改动,因为A小组是针对回调函数这一抽象概念来编程的。 以上就是回调函数的作用,当然这也是针对抽象而不是具体实现进行编程这一思想的威力所在。面向对象中的多态本质上就是让你用来针对抽象而不是针对实现来编程的。 异步回调 故事到这里还没有结束。 在上面的示例中,虽然我们使用了回调这一概念,也就是调用方实现回调函数然后再将该函数当做参数传递给其它模块调用。 但是,这里依然有一个问题,那就是make_youtiao函数的调用方式依然是同步的,关于同步异步请参考《从小白到高手,你需要理解同步与异步》,也就是说调用方是这样实现的: make_youtiao(10000, sell);// make_youtiao函数返回前什么都做不了 我们可以看到,调用方必须等待make_youtiao函数返回后才可以继续后续流程,我们再来看下make_youtiao函数的实现: void make_youtiao(int num, func call_back) { real_make_youtiao(num); call_back(); //执行回调 } 看到了吧,由于我们要制作10000个油条,make_youtiao函数执行完需要10分钟,也就是说即便我们使用了回调,调用方完全不需要关心制作完油条后的后续流程,但是调用方依然会被阻塞10分钟,这就是同步调用的问题所在。 如果你真的理解了上一节的话应该能想到一种更好的方法了。 没错,那就是异步调用。 反正制作完油条后的后续流程并不是调用方该关心的,也就是说调用方并不关心make_youtiao这一函数的返回值,那么一种更好的方式是:把制作油条的这一任务放到另一个线程(进程)、甚至另一台机器上。 如果用线程实现的话,那么make_youtiao就是这样实现了: void make_youtiao(int num, func call_back) { // 在新的线程中执行处理逻辑 create_thread(real_make_youtiao, num, call_back);} 看到了吧,这时当我们调用make_youtiao时就会 立刻返回 ,即使油条还没有真正开始制作,而调用方也完全无需等待制作油条的过程,可以立刻执行后流程: make_youtiao(10000, sell);// 立刻返回// 执行后续流程 这时调用方的后续流程可以和制作油条 同时 进行,这就是函数的 异步调用 ,当然这也是异步的高效之处。 新的编程思维模式 让我们再来仔细的看一下这个过程。 程序员最熟悉的思维模式是这样的: 调用某个函数,获取结果 处理获取到的结果 res = request();handle(res); 这就是函数的同步调用,只有request()函数返回拿到结果后,才能调用handle函数进行处理,request函数返回前我们必须 等待 ,这就是同步调用,其控制流是这样的: 但是如果我们想更加高效的话,那么就需要异步调用了,我们不去直接调用handle函数,而是作为参数传递给request: request(handle); 我们根本就不关心request什么时候真正的获取的结果,这是request该关心的事情,我们只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真的获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。 这就是异步调用,其控制流是这样的: 从编程思维上看,异步调用和同步有很大的差别,如果我们把处理流程当做一个任务来的话,那么同步下整个任务都是我们来实现的,但是异步情况下任务的处理流程被分为了两部分: 第一部分是我们来处理的,也就是调用request之前的部分 第二部分不是我们处理的,而是在其它线程、进程、甚至另一个机器上处理的。 我们可以看到由于任务被分成了两部分,第二部分的调用不在我们的掌控范围内,同时只有调用方才知道该做什么,因此在这种情况下回调函数就是一种必要的机制了。 也就是说回调函数的本质就是“只有我们才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”。 现在你应该能看出异步回调这种编程思维模式和同步的差异了吧。 接下来我们给回调一个较为学术的定义 正式定义 在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。 这就是回调函数的定义了。 回调函数就是一个函数,和其它函数没有任何区别。 注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。 对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。 但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示: 而之所以需要给第三方库指定回调函数,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子油条制作完成、接收到网络数据、文件读取完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供一个回调函数,库的使用方来实现该函数,第三方库在特定的节点调用该回调函数就可以了。 另一点值得注意的是,从图中我们可以看出回调函数和我们的主程序位于同一层中,我们只负责编写该回调函数,但并不是我们来调用的。 最后值得注意的一点就是回调函数被调用的时间节点,回调函数只在某些特定的节点被调用,就像上面说的油条制作完成、接收到网络数据、文件读取完成等,这些都是事件,也就是event,本质上我们编写的回调函数就是用来处理event的,因此从这个角度看回调函数不过就是event handler,因此回调函数天然适用于事件驱动编程event-driven,我们将会在后续文章中再次回到这一主题。 回调的类型 我们已经知道有两种类型的回调,这两种类型的回调区别在于回调函数被调用的时机。 注意,接下来会用到同步和异步的概念,对这两个概念不熟悉的同学可以参考上一盘文章《从小白到高手,你需要理解同步和异步》。 同步回调 这种回调就是通常所说的同步回调synchronous callbacks、也有的将其称为阻塞式回调blocking callbacks,或者什么修饰都没有,就是回调,callback,这是我们最为熟悉的回调方式。 当我们调用某个函数A并以参数的形式传入回调函数后,在A返回之前回调函数会被执行,也就是说我们的主程序会等待回调函数执行完成,这就是所谓的同步回调。 有同步回调就有异步回调。 异步回调 不同于同步回调, 当我们调用某个函数A并以参数的形式传入回调函数后,A函数会立刻返回,也就是说函数A并不会阻塞我们的主程序,一段时间后回调函数开始被执行,此时我们的主程序可能在忙其它任务,回调函数的执行和我们主程序的运行同时进行。 既然我们的主程序和回调函数的执行可以同时发生,因此一般情况下,主程序和回调函数的执行位于不同的线程或者进程中。 这就是所谓的异步回调,asynchronous callbacks,也有的资料将其称为deferred callbacks ,名字很形象,延迟回调。 从上面这两张图中我们也可以看到,异步回调要比同步回调更能充分的利用机器资源,原因就在于在同步模式下主程序会“偷懒”,因为调用其它函数被阻塞而暂停运行,但是异步调用不存在这个问题,主程序会一直运行下去。 因此,异步回调更常见于I/O操作,天然适用于Web服务这种高并发场景。 回调对应的编程思维模式 让我们用简单的几句话来总结一下回调下与常规编程思维模式的不同。 假设我们想处理某项任务,这项任务需要依赖某项服务S,我们可以将任务的处理分为两部分,调用服务S前的部分PA,和调用服务S后的部分PB。 在常规模式下,PA和PB都是服务调用方来执行的,也就是我们自己来执行PA部分,等待服务S返回后再执行PB部分。 但在回调这种方式下就不一样了。 在这种情况下,我们自己来执行PA部分,然后告诉服务S:“等你完成服务后执行PB部分”。 因此我们可以看到,现在一项任务是由不同的模块来协作完成的。 即: 常规模式:调用完S服务后后我去执行X任务, 回调模式:调用完S服务后你接着再去执行X任务, 其中X是服务调用方制定的,区别在于谁来执行。 为什么异步回调越来越重要 在同步模式下,服务调用方会因服务执行而被阻塞暂停执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景, 针对高并发这一场景,异步其实是更加高效的,原因很简单,你不需要在原地等待,因此从而更好的利用机器资源,而回调函数又是异步下不可或缺的一种机制。 回调地狱,callback hell 有的同学可能认为有了异步回调这种机制应付起一切高并发场景就可以高枕无忧了。 实际上在计算机科学中还没有任何一种可以横扫一切包治百病的技术,现在没有,在可预见的将来也不会有,一切都是妥协的结果。 那么异步回调这种机制有什么问题呢? 实际上我们已经看到了,异步回调这种机制和程序员最熟悉的同步模式不一样,在可理解性上比不过同步,而如果业务逻辑相对复杂,比如我们处理某项任务时不止需要调用一项服务,而是几项甚至十几项,如果这些服务调用都采用异步回调的方式来处理的话,那么很有可能我们就陷入回调地狱中。 举个例子,假设处理某项任务我们需要调用四个服务,每一个服务都需要依赖上一个服务的结果,如果用同步方式来实现的话可能是这样的: a = GetServiceA();b = GetServiceB(a);c = GetServiceC(b);d = GetServiceD(c); 代码很清晰,很容易理解有没有。 我们知道异步回调的方式会更加高效,那么使用异步回调的方式来写将会是什么样的呢? GetServiceA(function(a){ GetServiceB(a, function(b){ GetServiceC(b, function(c){ GetServiceD(c, function(d) { .... }); }); });}); 我想不需要再强调什么了吧,你觉得这两种写法哪个更容易理解,代码更容易维护呢? 博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,一个不得不去重读一边代码;另一个在一旁骂自己为什么当初选择维护这个项目。 异步回调代码稍不留意就会跌到回调陷阱中,那么有没有一种更好的办法既能结合异步回调的高效又能结合同步编码的简单易读呢? 幸运的是,答案是肯定的,我们会在后续文章中详细讲解这一技术。 总结 在这篇文章中,我们从一个实际的例子出发详细讲解了回调函数这种机制的来龙去脉,这是应对高并发、高性能场景的一种极其重要的编码机制,异步加回调可以充分利用机器资源,实际上异步回调最本质上就是事件驱动编程,这是我们接下来要重点讲解的内容。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-02-25 关键词: 回调函数 C语言

  • C语言实现MD5加密,竟如此简单!

    本文详细讲解视频已经上传到B站: https://www.bilibili.com/video/BV1uy4y1p7on/ 公众号后台回复【md5】即可获得本文所有源码。 一、摘要算法 摘要算法又称哈希算法。 它表示输入任意长度的数据,输出固定长度的数据,它的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密。 目前可以被解密逆向的只有CRC32算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。 消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。由于其加密计算的工作量相当巨大,所以以前的这种算法通常只用于数据量有限的情况下的加密。 消息摘要算法分为三类: MD(Message Digest):消息摘要 SHA(Secure Hash Algorithm):安全散列 MAC(Message Authentication Code):消息认证码 这三类算法的主要作用:验证数据的完整性 二、MD5简介 MD5即Message-Digest Algorithm 5(信息-摘要算法)。 属于摘要算法,是一个不可逆过程,就是无论多大数据,经过算法运算后都是生成固定长度的数据,结果使用16进制进行显示的128bit的二进制串。通常表示为32个十六进制数连成的字符串。 MD5有什么用? 用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现。更多用在文档校验上,用来生成密钥检测文档是否被篡改。 三、在线MD5加密 有很多在线进行MD5加密的网站,如下: http://www.metools.info/code/c26.html 举例: 给字符串 12334567 加密成。 如图结果为: 32135A337F8DC8E2BB9A9B80D86BDFD0 四、C语言实现MD5算法 源文件如下:md5.h #ifndef MD5_H#define MD5_H typedef struct{    unsigned int count[2];    unsigned int state[4];    unsigned char buffer[64];   }MD5_CTX;                          #define F(x,y,z) ((x & y) | (~x & z))#define G(x,y,z) ((x & z) | (y & ~z))#define H(x,y,z) (x^y^z)#define I(x,y,z) (y ^ (x | ~z))#define ROTATE_LEFT(x,n) ((x  (32-n)))#define FF(a,b,c,d,x,s,ac) \          { \          a += F(b,c,d) + x + ac; \          a = ROTATE_LEFT(a,s); \          a += b; \          }#define GG(a,b,c,d,x,s,ac) \          { \          a += G(b,c,d) + x + ac; \          a = ROTATE_LEFT(a,s); \          a += b; \          }#define HH(a,b,c,d,x,s,ac) \          { \          a += H(b,c,d) + x + ac; \          a = ROTATE_LEFT(a,s); \          a += b; \          }#define II(a,b,c,d,x,s,ac) \          { \          a += I(b,c,d) + x + ac; \          a = ROTATE_LEFT(a,s); \          a += b; \          }                                            void MD5Init(MD5_CTX *context);void MD5Update(MD5_CTX *context,unsigned char *input,unsigned int inputlen);void MD5Final(MD5_CTX *context,unsigned char digest[16]);void MD5Transform(unsigned int state[4],unsigned char block[64]);void MD5Encode(unsigned char *output,unsigned int *input,unsigned int len);void MD5Decode(unsigned int *output,unsigned char *input,unsigned int len); #endif md5.c #include #include "md5.h" unsigned char PADDING[]={0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,                         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,                         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,                         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};                         void MD5Init(MD5_CTX *context){     context->count[0] = 0;     context->count[1] = 0;     context->state[0] = 0x67452301;     context->state[1] = 0xEFCDAB89;     context->state[2] = 0x98BADCFE;     context->state[3] = 0x10325476;}void MD5Update(MD5_CTX *context,unsigned char *input,unsigned int inputlen){    unsigned int i = 0,index = 0,partlen = 0;    index = (context->count[0] >> 3) & 0x3F;    partlen = 64 - index;    context->count[0] += inputlen  29;        if(inputlen >= partlen)    {       memcpy(&context->buffer[index],input,partlen);       MD5Transform(context->state,context->buffer);       for(i = partlen;i+64 state,&input[i]);       index = 0;            }      else    {        i = 0;    }    memcpy(&context->buffer[index],&input[i],inputlen-i);}void MD5Final(MD5_CTX *context,unsigned char digest[16]){    unsigned int index = 0,padlen = 0;    unsigned char bits[8];    index = (context->count[0] >> 3) & 0x3F;    padlen = (index count,8);    MD5Update(context,PADDING,padlen);    MD5Update(context,bits,8);    MD5Encode(digest,context->state,16);}void MD5Encode(unsigned char *output,unsigned int *input,unsigned int len){    unsigned int i = 0,j = 0;    while(j > 8) & 0xFF;         output[j+2] = (input[i] >> 16) & 0xFF;         output[j+3] = (input[i] >> 24) & 0xFF;         i++;         j+=4;    }}void MD5Decode(unsigned int *output,unsigned char *input,unsigned int len){     unsigned int i = 0,j = 0;     while(j 

    时间:2021-02-25 关键词: MD5 摘要算法 C语言

首页  上一页  1 2 3 4 5 6 7 8 9 10 下一页 尾页
发布文章

技术子站