当前位置:首页 > C语言
  • 万能算法PID最全总结

    PID的数学模型 在工业应用中PID及其衍生算法是应用最广泛的算法之一,是当之无愧的万能算法,如果能够熟练掌握PID算法的设计与实现过程,对于一般的研发人员来讲,应该是足够应对一般研发问题了,而难能可贵的是,在很多控制算法当中,PID控制算法又是最简单,最能体现反馈思想的控制算法,可谓经典中的经典。经典的未必是复杂的,经典的东西常常是简单的,而且是最简单的。PID算法的一般形式: PID算法通过误差信号控制被控量,而控制器本身就是比例、积分、微分三个环节的加和。这里我们规定(在t时刻): 1.输入量为 2.输出量为 3.偏差量为  PID算法的数字离散化 假设采样间隔为T,则在第K个T时刻: 偏差=  积分环节用加和的形式表示,即  微分环节用斜率的形式表示,即 PID算法离散化后的式子:  则可表示成为:   其中式中: 比例参数 :控制器的输出与输入偏差值成比例关系。系统一旦出现偏差,比例调节立即产生调节作用以减少偏差。特点:过程简单快速、比例作用大,可以加快调节,减小误差;但是使系统稳定性下降,造成不稳定,有余差。 积分参数 :积分环节主要是用来消除静差,所谓静差,就是系统稳定后输出值和设定值之间的差值,积分环节实际上就是偏差累计的过程,把累计的误差加到原有系统上以抵消系统造成的静差。 微分参数 :微分信号则反应了偏差信号的变化规律,或者说是变化趋势,根据偏差信号的变化趋势来进行超前调节,从而增加了系统的快速性。 PID的基本离散表示形式如上。目前的这种表述形式属于位置型PID,另外一种表述方式为增量式PID,由上述表达式可以轻易得到: 那么: 上式就是离散化PID的增量式表示方式,由公式可以看出,增量式的表达结果和最近三次的偏差有关,这样就大大提高了系统的稳定性。需要注意的是最终的输出结果应该为: 输出量 =  + 增量调节值 目的 PID 的重要性应该无需多说了,这个控制领域的应用最广泛的算法了. 本篇文章的目的是希望通过一个例子展示算法过程,并解释以下概念: (1)简单描述何为PID, 为何需要PID,PID 能达到什么作用。 (2)理解P(比例环节)作用:基础比例环节。 缺点: 产生稳态误差. 疑问: 何为稳态误差 为什么会产生稳态误差. (3)理解I(积分环节)作用:消除稳态误差. 缺点: 增加超调 疑问: 积分为何能消除稳态误差? (4) 理解D(微分环节)作用:加大惯性响应速度,减弱超调趋势 疑问: 为何能减弱超调 (5)理解各个比例系数的作用 何为PID以及为何需要PID? 以下即PID 控制的整体框图,过程描述为:  设定一个输出目标,反馈系统传回输出值,如与目标不一致,则存在一个误差,PID 根据此误差调整输入值,直至输出达到设定值. 疑问: 那么我们为什么需要PID 呢,比如我控制温度,我不能监控温度值,温度值一到就停止吗? 这里必须要先说下我们的目标,因为我们所有的控制无非就是想输出能够达到我们的设定,即如果我们设定了一个目标温度值,那么我们想要一个什么样的温度变化呢. 比如设定目标温度为30度, 目标无非是希望达到图1 希望其能够快速而且没有抖动的达到30度. 那这样大家应该就明白,如果使用温度一到就停止的办法,当然如果要求不高可能也行,当肯定达不到图1 这样的要求,因为温度到了后余温也会让温度继续升高.而且温度自身也会通过空气散热的. 图  系统输出的响应目标 综上所述,我们需要PID的原因无非就是普通控制手段没有办法使输出快速稳定的到达设定值。 控制器的P,I,D项选择 下面将常用的各种控制规律的控制特点简单归纳一下: (1)、比例控制规律P:采用P控制规律能较快地克服扰动的影响,它的作用于输出值较快,但不能很好稳定在一个理想的数值,不良的结果是虽较能有效的克服扰动的影响,但有余差出现。它适用于控制通道滞后较小、负荷变化不大、控制要求不高、被控参数允许在一定范围内有余差的场合。如:金彪公用工程部下设的水泵房冷、热水池水位控制;油泵房中间油罐油位控制等。 (2)、比例积分控制规律(PI):在工程中比例积分控制规律是应用最广泛的一种控制规律。积分能在比例的基础上消除余差,它适用于控制通道滞后较小、负荷变化不大、被控参数不允许有余差的场合。如:在主线窑头重油换向室中F1401到F1419号枪的重油流量控制系统;油泵房供油管流量控制系统;退火窑各区温度调节系统等。 (3)、比例微分控制规律(PD):微分具有超前作用,对于具有容量滞后的控制通道,引入微分参与控制,在微分项设置得当的情况下,对于提高系统的动态性能指标,有着显著效果。因此,对于控制通道的时间常数或容量滞后较大的场合,为了提高系统的稳定性,减小动态偏差等可选用比例微分控制规律。如:加热型温度控制、成分控制。需要说明一点,对于那些纯滞后较大的区域里,微分项是无能为力,而在测量信号有噪声或周期性振动的系统,则也不宜采用微分控制。如:大窑玻璃液位的控制。 (4)、例积分微分控制规律(PID):PID控制规律是一种较理想的控制规律,它在比例的基础上引入积分,可以消除余差,再加入微分作用,又能提高系统的稳定性。它适用于控制通道时间常数或容量滞后较大、控制要求较高的场合。如温度控制、成分控制等。 鉴于D规律的作用,我们还必须了解时间滞后的概念,时间滞后包括容量滞后与纯滞后。其中容量滞后通常又包括:测量滞后和传送滞后。测量滞后是检测元件在检测时需要建立一种平衡,如热电偶、热电阻、压力等响应较慢产生的一种滞后。而传送滞后则是在传感器、变送器、执行机构等设备产生的一种控制滞后。纯滞后是相对与测量滞后的,在工业上,大多的纯滞后是由于物料传输所致,如:大窑玻璃液位,在投料机动作到核子液位仪检测需要很长的一段时间。 总之,控制规律的选用要根据过程特性和工艺要求来选取,决不是说PID控制规律在任何情况下都具有较好的控制性能,不分场合都采用是不明智的。如果这样做,只会给其它工作增加复杂性,并给参数整定带来困难。当采用PID控制器还达不到工艺要求,则需要考虑其它的控制方案。如串级控制、前馈控制、大滞后控制等。 Kp,Ti,Td三个参数的设定是PID控制算法的关键问题。一般说来编程时只能设定他们的大概数值,并在系统运行时通过反复调试来确定最佳值。因此调试阶段程序须得能随时修改和记忆这三个参数。 数字PID控制器 (1)模拟PID控制规律的离散化   (2)数字PID控制器的差分方程 参数的自整定 在某些应用场合,比如通用仪表行业,系统的工作对象是不确定的,不同的对象就得采用不同的参数值,没法为用户设定参数,就引入参数自整定的概念。实质就是在首次使用时,通过N次测量为新的工作对象寻找一套参数,并记忆下来作为以后工作的依据。具体的整定方法有三种:临界比例度法、衰减曲线法、经验法。 1、临界比例度法(Ziegler-Nichols) 1.1  在纯比例作用下,逐渐增加增益至产生等副震荡,根据临界增益和临界周期参数得出PID控制器参数,步骤如下: (1)将纯比例控制器接入到闭环控制系统中(设置控制器参数积分时间常数Ti =∞,实际微分时间常数Td =0)。 (2)控制器比例增益K设置为最小,加入阶跃扰动(一般是改变控制器的给定值),观察被调量的阶跃响应曲线。 (3)由小到大改变比例增益K,直到闭环系统出现振荡。 (4)系统出现持续等幅振荡时,此时的增益为临界增益(Ku),振荡周期(波峰间的时间)为临界周期(Tu)。 (5) 由表1得出PID控制器参数。 表1 1.2  采用临界比例度法整定时应注意以下几点: (1)在采用这种方法获取等幅振荡曲线时,应使控制系统工作在线性区,不要使控制阀出现开、关的极端状态,否则得到的持续振荡曲线可能是“极限循环”,从线性系统概念上说系统早已处于发散振荡了。 (2)由于被控对象特性的不同,按上表求得的控制器参数不一定都能获得满意的结果。对于无自平衡特性的对象,用临界比例度法求得的控制器参数往住使系统响应的衰减率偏大(ψ>0.75 )。而对于有自平衡特性的高阶等容对象,用此法整定控制器参数时系统响应衰减率大多偏小(ψ<0.75 )。为此,上述求得的控制器参数,应针对具体系统在实际运行过程中进行在线校正。 (3) 临界比例度法适用于临界振幅不大、振荡周期较长的过程控制系统,但有些系统从安全性考虑不允许进行稳定边界试验,如锅炉汽包水位控制系统。还有某些时间常数较大的单容对象,用纯比例控制时系统始终是稳定的,对于这些系统也是无法用临界比例度法来进行参数整定的。 (4)只适用于二阶以上的高阶对象,或一阶加纯滞后的对象,否则,在纯比例控制情况下,系统不会出现等幅振荡。 1.3  若求出被控对象的静态放大倍数KP=△y/△u ,则增益乘积KpKu可视为系统的最大开环增益。通常认为Ziegler-Nichols闭环试验整定法的适用范围为: (1) 当KpKu > 20时,应采用更为复杂的控制算法,以求较好的调节效果。 (2)当KpKu < 2时,应使用一些能补偿传输迟延的控制策略。 (3)当1.5

    时间:2020-10-25 关键词: pid C语言

  • 《我想进大厂》之JVM夺命连环10问

    这是面试专题系列第五篇JVM篇。这一篇可能稍微比较长,没有耐心的同学建议直接拖到最后。 说说JVM的内存布局? Java虚拟机主要包含几个区域: 堆:堆Java虚拟机中最大的一块内存,是线程共享的内存区域,基本上所有的对象实例数组都是在堆上分配空间。堆区细分为Yound区年轻代和Old区老年代,其中年轻代又分为Eden、S0、S1 3个部分,他们默认的比例是8:1:1的大小。 栈:栈是线程私有的内存区域,每个方法执行的时候都会在栈创建一个栈帧,方法的调用过程就对应着栈的入栈和出栈的过程。每个栈帧的结构又包含局部变量表、操作数栈、动态连接、方法返回地址。 局部变量表用于存储方法参数和局部变量。当第一个方法被调用的时候,他的参数会被传递至从0开始的连续的局部变量表中。 操作数栈用于一些字节码指令从局部变量表中传递至操作数栈,也用来准备方法调用的参数以及接收方法返回结果。 动态连接用于将符号引用表示的方法转换为实际方法的直接引用。 元数据:在Java1.7之前,包含方法区的概念,常量池就存在于方法区(永久代)中,而方法区本身是一个逻辑上的概念,在1.7之后则是把常量池移到了堆内,1.8之后移出了永久代的概念(方法区的概念仍然保留),实现方式则是现在的元数据。它包含类的元信息和运行时常量池。 Class文件就是类和接口的定义信息。 运行时常量池就是类和接口的常量池运行时的表现形式。 本地方法栈:主要用于执行本地native方法的区域 程序计数器:也是线程私有的区域,用于记录当前线程下虚拟机正在执行的字节码的指令地址 知道new一个对象的过程吗? 当虚拟机遇见new关键字时候,实现判断当前类是否已经加载,如果类没有加载,首先执行类的加载机制,加载完成后再为对象分配空间、初始化等。 首先校验当前类是否被加载,如果没有加载,执行类加载机制 加载:就是从字节码加载成二进制流的过程 验证:当然加载完成之后,当然需要校验Class文件是否符合虚拟机规范,跟我们接口请求一样,第一件事情当然是先做个参数校验了 准备:为静态变量、常量赋默认值 解析:把常量池中符号引用(以符号描述引用的目标)替换为直接引用(指向目标的指针或者句柄等)的过程 初始化:执行static代码块(cinit)进行初始化,如果存在父类,先对父类进行初始化 Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调用!(此处该有问题static代码块线程安全吗?) 当类加载完成之后,紧接着就是对象分配内存空间和初始化的过程 首先为对象分配合适大小的内存空间 接着为实例变量赋默认值 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等 执行构造函数(init)初始化 知道双亲委派模型吗? 类加载器自顶向下分为: Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib目录下的jar Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext目录下的jar Application ClassLoader应用程序类加载器:比如我们的web应用,会加载web程序中ClassPath下的类 User ClassLoader用户自定义类加载器:由用户自己定义 当我们在加载类的时候,首先都会向上询问自己的父加载器是否已经加载,如果没有则依次向上询问,如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。 说说有哪些垃圾回收算法? 标记-清除 统一标记出需要回收的对象,标记完成之后统一回收所有被标记的对象,而由于标记的过程需要遍历所有的GC ROOT,清除的过程也要遍历堆中所有的对象,所以标记-清除算法的效率低下,同时也带来了内存碎片的问题。 复制算法 为了解决性能的问题,复制算法应运而生,它将内存分为大小相等的两块区域,每次使用其中的一块,当一块内存使用完之后,将还存活的对象拷贝到另外一块内存区域中,然后把当前内存清空,这样性能和内存碎片的问题得以解决。但是同时带来了另外一个问题,可使用的内存空间缩小了一半! 因此,诞生了我们现在的常见的年轻代+老年代的内存结构:Eden+S0+S1组成,因为根据IBM的研究显示,98%的对象都是朝生夕死,所以实际上存活的对象并不是很多,完全不需要用到一半内存浪费,所以默认的比例是8:1:1。 这样,在使用的时候只使用Eden区和S0S1中的一个,每次都把存活的对象拷贝另外一个未使用的Survivor区,同时清空Eden和使用的Survivor,这样下来内存的浪费就只有10%了。 如果最后未使用的Survivor放不下存活的对象,这些对象就进入Old老年代了。 PS:所以有一些初级点的问题会问你为什么要分为Eden区和2个Survior区?有什么作用?就是为了节省内存和解决内存碎片的问题,这些算法都是为了解决问题而产生的,如果理解原因你就不需要死记硬背了 标记-整理 针对老年代再用复制算法显然不合适,因为进入老年代的对象都存活率比较高了,这时候再频繁的复制对性能影响就比较大,而且也不会再有另外的空间进行兜底。所以针对老年代的特点,通过标记-整理算法,标记出所有的存活对象,让所有存活的对象都向一端移动,然后清理掉边界以外的内存空间。 那么什么是GC ROOT?有哪些GC ROOT? 上面提到的标记的算法,怎么标记一个对象是否存活?简单的通过引用计数法,给对象设置一个引用计数器,每当有一个地方引用他,就给计数器+1,反之则计数器-1,但是这个简单的算法无法解决循环引用的问题。 Java通过可达性分析算法来达到标记存活对象的目的,定义一系列的GC ROOT为起点,从起点开始向下开始搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT没有任何引用链相连的话,则对象可以判定是可以被回收的。 而可以作为GC ROOT的对象包括: 栈中引用的对象 静态变量、常量引用的对象 本地方法栈native方法引用的对象 垃圾回收器了解吗?年轻代和老年代都有哪些垃圾回收器? 年轻代的垃圾收集器包含有Serial、ParNew、Parallell,老年代则包括Serial Old老年代版本、CMS、Parallel Old老年代版本和JDK11中的船新的G1收集器。 Serial:单线程版本收集器,进行垃圾回收的时候会STW(Stop The World),也就是进行垃圾回收的时候其他的工作线程都必须暂停 ParNew:Serial的多线程版本,用于和CMS配合使用 Parallel Scavenge:可以并行收集的多线程垃圾收集器 Serial Old:Serial的老年代版本,也是单线程 Parallel Old:Parallel Scavenge的老年代版本 CMS(Concurrent Mark Sweep):CMS收集器是以获取最短停顿时间为目标的收集器,相对于其他的收集器STW的时间更短暂,可以并行收集是他的特点,同时他基于标记-清除算法,整个GC的过程分为4步。 初始标记:标记GC ROOT能关联到的对象,需要STW 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记,需要STW 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW 从整个过程来看,并发标记和并发清除的耗时最长,但是不需要停止用户线程,而初始标记和重新标记的耗时较短,但是需要停止用户线程,总体而言,整个过程造成的停顿时间较短,大部分时候是可以和用户线程一起工作的。 G1(Garbage First):G1收集器是JDK9的默认垃圾收集器,而且不再区分年轻代和老年代进行回收。 G1的原理了解吗? G1作为JDK9之后的服务端默认收集器,且不再区分年轻代和老年代进行垃圾回收,他把内存划分为多个Region,每个Region的大小可以通过-XX:G1HeapRegionSize设置,大小为1~32M,对于大对象的存储则衍生出Humongous的概念,超过Region大小一半的对象会被认为是大对象,而超过整个Region大小的对象被认为是超级大对象,将会被存储在连续的N个Humongous Region中,G1在进行回收的时候会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先回收收益最大的Region。 G1的回收过程分为以下四个步骤: 初始标记:标记GC ROOT能关联到的对象,需要STW 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象 最终标记:短暂暂停用户线程,再处理一次,需要STW 筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的Region。需要STW 总的来说除了并发标记之外,其他几个过程也还是需要短暂的STW,G1的目标是在停顿和延迟可控的情况下尽可能提高吞吐量。 什么时候会触发YGC和FGC?对象什么时候会进入老年代? 当一个新的对象来申请内存空间的时候,如果Eden区无法满足内存分配需求,则触发YGC,使用中的Survivor区和Eden区存活对象送到未使用的Survivor区,如果YGC之后还是没有足够空间,则直接进入老年代分配,如果老年代也无法分配空间,触发FGC,FGC之后还是放不下则报出OOM异常。 YGC之后,存活的对象将会被复制到未使用的Survivor区,如果S区放不下,则直接晋升至老年代。而对于那些一直在Survivor区来回复制的对象,通过-XX:MaxTenuringThreshold配置交换阈值,默认15次,如果超过次数同样进入老年代。 此外,还有一种动态年龄的判断机制,不需要等到MaxTenuringThreshold就能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。 频繁FullGC怎么排查? 这种问题最好的办法就是结合有具体的例子举例分析,如果没有就说一般的分析步骤。发生FGC有可能是内存分配不合理,比如Eden区太小,导致对象频繁进入老年代,这时候通过启动参数配置就能看出来,另外有可能就是存在内存泄露,可以通过以下的步骤进行排查: jstat -gcutil或者查看gc.log日志,查看内存回收情况 S0 S1 分别代表两个Survivor区占比 E代表Eden区占比,图中可以看到使用78% O代表老年代,M代表元空间,YGC发生54次,YGCT代表YGC累计耗时,GCT代表GC累计耗时。 [GC [FGC 开头代表垃圾回收的类型 PSYoungGen: 6130K->6130K(9216K)] 12274K->14330K(19456K), 0.0034895 secs代表YGC前后内存使用情况 Times: user=0.02 sys=0.00, real=0.00 secs,user表示用户态消耗的CPU时间,sys表示内核态消耗的CPU时间,real表示各种墙时钟的等待时间 这两张图只是举例并没有关联关系,比如你从图里面看能到是否进行FGC,FGC的时间花费多长,GC后老年代,年轻代内存是否有减少,得到一些初步的情况来做出判断。 dump出内存文件在具体分析,比如通过jmap命令jmap -dump:format=b,file=dumpfile pid,导出之后再通过 Eclipse Memory Analyzer等工具进行分析,定位到代码,修复 这里还会可能存在一个提问的点,比如CPU飙高,同时FGC怎么办?办法比较类似 找到当前进程的pid,top -p pid -H 查看资源占用,找到线程 printf “%x\n” pid,把线程pid转为16进制,比如0x32d jstack pid|grep -A 10 0x32d查看线程的堆栈日志,还找不到问题继续 dump出内存文件用MAT等工具进行分析,定位到代码,修复 JVM调优有什么经验吗? 要明白一点,所有的调优的目的都是为了用更小的硬件成本达到更高的吞吐,JVM的调优也是一样,通过对垃圾收集器和内存分配的调优达到性能的最佳。 简单的参数含义 首先,需要知道几个主要的参数含义。 -Xms设置初始堆的大小,-Xmx设置最大堆的大小 -XX:NewSize年轻代大小,-XX:MaxNewSize年轻代最大值,-Xmn则是相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值 -XX:NewRatio设置年轻代和年老代的比值,如果为3,表示年轻代与老年代比值为1:3,默认值为2 -XX:SurvivorRatio年轻代和两个Survivor的比值,默认8,代表比值为8:1:1 -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。 -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代 -XX:MaxDirectMemorySize当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC 调优 为了打印日志方便排查问题最好开启GC日志,开启GC日志对性能影响微乎其微,但是能帮助我们快速排查定位问题。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log 一般设置-Xms=-Xmx,这样可以获得固定大小的堆内存,减少GC的次数和耗时,可以使得堆相对稳定 -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,方便排查问题 -Xmn设置新生代的大小,太小会增加YGC,太大会减小老年代大小,一般设置为整个堆的1/4到1/3 设置-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-25 关键词: 嵌入式 C语言

  • SQL查询语句总是先执行SELECT?你们都错了

    来源 | infoq.cn/article/Oke8hgilga3PTZ3gWvbg 很多 SQL 查询都是以 SELECT 开始的。不过,最近我跟别人解释什么是窗口函数,我在网上搜索”是否可以对窗口函数返回的结果进行过滤“这个问题,得出的结论是”窗口函数必须在 WHERE 和 GROUP BY 之后,所以不能”。于是我又想到了另一个问题:SQL 查询的执行顺序是怎样的? 好像这个问题应该很好回答,毕竟自己已经写了上万个 SQL 查询了,有一些还很复杂。但事实是,我仍然很难确切地说出它的顺序是怎样的。 SQL 查询的执行顺序 于是我研究了一下,发现顺序大概是这样的。SELECT 并不是最先执行的,而是在第五个。 这张图回答了以下这些问题 这张图与 SQL 查询的语义有关,让你知道一个查询会返回什么,并回答了以下这些问题: 可以在 GRROUP BY 之后使用 WHERE 吗?(不行,WHERE 是在 GROUP BY 之后!) 可以对窗口函数返回的结果进行过滤吗?(不行,窗口函数是 SELECT 语句里,而 SELECT 是在 WHERE 和 GROUP BY 之后) 可以基于 GROUP BY 里的东西进行 ORDER BY 吗?(可以,ORDER BY 基本上是在最后执行的,所以可以基于任何东西进行 ORDER BY) LIMIT 是在什么时候执行?(在最后!) 但数据库引擎并不一定严格按照这个顺序执行 SQL 查询,因为为了更快地执行查询,它们会做出一些优化,这些问题会在以后的文章中解释。 所以: 如果你想要知道一个查询语句是否合法,或者想要知道一个查询语句会返回什么,可以参考这张图; 在涉及查询性能或者与索引有关的东西时,这张图就不适用了。 混合因素:列别名 有很多 SQL 实现允许你使用这样的语法: SELECT CONCAT(first_name, ' ', last_name) AS full_name, count(*)FROM tableGROUP BY full_name 从这个语句来看,好像 GROUP BY 是在 SELECT 之后执行的,因为它引用了 SELECT 中的一个别名。但实际上不一定要这样,数据库引擎可以把查询重写成这样: SELECT CONCAT(first_name, ' ', last_name) AS full_name, count(*)FROM tableGROUP BY CONCAT(first_name, ' ', last_name) 这样 GROUP BY 仍然先执行。数据库引擎还会做一系列检查,确保 SELECT 和 GROUP BY 中的东西是有效的,所以会在生成执行计划之前对查询做一次整体检查。 数据库可能不按照这个顺序执行查询(优化) 在实际当中,数据库不一定会按照 JOIN、WHERE、GROUP BY 的顺序来执行查询,因为它们会进行一系列优化,把执行顺序打乱,从而让查询执行得更快,只要不改变查询结果。 这个查询说明了为什么需要以不同的顺序执行查询: SELECT * FROMowners LEFT JOIN cats ON owners.id = cats.ownerWHERE cats.name = 'mr darcy' 如果只需要找出名字叫“mr darcy”的猫,那就没必要对两张表的所有数据执行左连接,在连接之前先进行过滤,这样查询会快得多,而且对于这个查询来说,先执行过滤并不会改变查询结果。 数据库引擎还会做出其他很多优化,按照不同的顺序执行查询,不过我并不是这方面的专家,所以这里就不多说了。 LINQ 的查询以 FROM 开头 LINQ(C#和 VB.NET 中的查询语法)是按照 FROM…WHERE…SELECT 的顺序来的。这里有一个 LINQ 查询例子: var teenAgerStudent = from s in studentList                      where s.Age > 12 && s.Age  1000] # WHEREdf = df.groupby('something', num_yes = ('yes', 'sum')) # GROUP BYdf = df[df.num_yes > 2]       # HAVING, 对 GROUP BY 结果进行过滤df = df[['num_yes', 'something1', 'something']] # SELECT, 选择要显示的列df.sort_values('sometthing', ascending=True)[:30] # ORDER BY 和 LIMITdf[:30] 这样写并不是因为 pandas 规定了这些规则,而是按照 JOIN/WHERE/GROUP BY/HAVING 这样的顺序来写代码会更有意义些。不过我经常会先写 WHERE 来改进性能,而且我想大多数数据库引擎也会这么做。 R 语言里的 dplyr 也允许开发人员使用不同的语法编写 SQL 查询语句,用来查询 Postgre、MySQL 和 SQLite。 原文链接:SQL queries don’t start with SELECThttps://jvns.ca/blog/2019/10/03/sql-queries-don-t-start-with-select/ 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-25 关键词: 嵌入式 C语言

  • 几款优秀的支持C、C++等多种语言的在线编译器

    关注+星标公众号,不错过精彩内容 作者 | strongerHuang 微信公众号 | strongerHuang 今天10.24程序员节,是一个特殊的日子,2020 - 1024 = 996,你没看错,2020年的1024更加特别(不要问我为什么特别)。 作为程序员,使用编译器是必备技能,但是从入门到放弃,基本上就是在开发环境安装、配置这一步。。。 大家可能体会过,使用编译器不是一件简单的事,下载、安装、各种配置······但最终不能使用,然后就放弃了。 今天就来分享几个支持C、 C++、 C#、 JAVA······等多种编程语言的在线编译器。 它们和本地编译器的区别在于:在线编译器非常的轻量级,不用安装、不用各种复杂配置,基本上直接就能使用。 在线编译器① 地址: https://rextester.com/ (公号不支持外链接,请复制链接到浏览器打开) 这款在线编译器相对还是比较专业,它可以显示编译时间、运行时间、内存占用等,同时它支持多种编程语言、编译器都能选择,比如:C语言可选择gcc、 clang、 vc等。 在线编译器② 地址: https://www.tutorialspoint.com/codingground.htm (公号不支持外链接,请复制链接到浏览器打开) 这是一款比较全面的在线工具,支持前端技术、文档编辑、在线编译等丰富的在线工具。 比如我们选在其中C语言(GCC)在线编译器: 如果代码有错误,在线编译,也会提示: 总的来说,这款在线编译器的功能挺多,也比较强大。 在线编译器③ 地址: https://www.codechef.com/ide (公号不支持外链接,请复制链接到浏览器打开) 这款工具同样支持多语言、多编译器,从网址可以看得出来,是一款轻量级的IDE。 使用也是很方便,直接编辑代码,或者打开源代码文件,就可以点击“RUN”直接编译运行了: 当然,如果代码有错误,也会很直观的提醒在哪一行的错误: 更多在线编译器 在线编译器比较多,我这里就不一一举例说明了,下面直接罗列一些在线编译器地址,感兴趣的可以试试。 地址: https://tech.io/snippet http://rextester.com https://codesandbox.io https://jsfiddle.net https://www.ideone.com https://www.onlinegdb.com (公号不支持外链接,请复制链接到浏览器打开) 最后,这些在线编译器对于一些初学者(不懂各种配置),或者想测试本地没有的编译环境都是非常有用的,大家有时间可以多尝试一下。 ------------ END ------------ 推荐阅读: 什么是Cortex-M内核的MPU 程序猿如何选择开源协议? 线程、进程、多线程、多进程 和 多任务  关注 微信公众号『strongerHuang』,后台回复“1024”查看更多内容,回复“加群”按规则加入技术交流群。 长按前往图中包含的公众号关注 点击“ 阅读原文 ”查看更多分享,欢迎点分享、收藏、点赞、在看。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-25 关键词: 嵌入式 C语言

  • 如何无侵入管理所有的微服务接口?

    本文是《微服务治理实践》系列篇的第四篇文章,主要分享 Spring Cloud 微服务框架下的服务契约。 在详细讲述服务契约之前,先给大家讲一个场景。 前言 随着微服务架构越来越流行,越来越多的公司使用微服务框架进行开发。甚至不止是公司,连笔者的研究生导师都要对实验室的 Spring Boot 工程项目转型使用微服务框架了。随着时间的推移,服务量逐渐上升,小学妹吃不消跑来问我问题: 一姐,我来交接你之前写的项目啦,你什么时间方便我想问你一些问题。这么多微服务接口,感觉不知道从哪里去看会比较好呢。 我想了想自己刚入门时候写的垃圾代码,还没有注释,无语凝噎。 好。我平时工作日在实习,周末给你讲哈。 于是到周末,花了整整一个晚上的时间,终于给零基础学妹从众多接口的含义,到参数列表的解析,最后到讲解百度应该搜什么关键词,全方位视频指导。学妹十分感动: 一姐你太贴心了555,跟别人协作项目的时候,经常能讲上几句就不错了,然后我还是什么都不明白,改完接口也不及时告诉我。还是你最好了,后面还有什么不懂的我再来问你哦。 从以上场景,我们可以总结出使用微服务框架后,会带来的几点进度协同问题: 1. 不及时提供接口API 尤其体现在项目交接上,该问题对人员变动比较频繁的组织,如高校项目的准毕业生和新生交接、企业项目的外包人员交接,问题会显得更加突出。开发人员经常过于关注微服务的内部实现,相对较少设计API接口。 程序员最讨厌的两件事:1. 写注释 2. 别人不写注释 是不是经常想着写完代码再写注释,但真正把代码写完以后,注释/接口描述一拖再拖最后就没有了? 2. 不及时变更接口 即使有了 API 文档,但由于文档的离线管理,微服务接口变更以后,文档却没有及时变更,影响协作人员的开发进度。 综上我们看到,我们不但希望所有的微服务接口都可以很方便的添加规范的接口描述,而且也能随着接口的变更及时更新文档。因此,我们需要服务契约来帮助我们解决这些问题。 为什么我们需要服务契约 首先我们来看服务契约的定义: 服务契约指基于 OpenAPI 规范的微服务接口描述,是微服务系统运行和治理的基础。 有人可能会问了,既然想要规范的描述接口,我有很多其他的方式啊,为什么我要用服务契约? 1. 我用 Javadoc 来描述接口然后生成文档不可以吗? 可以,但刚刚我们也提到了“程序员最讨厌的两件事”,要求所有的开发人员都去主动的按照规范写注释,把所有的接口、参数列表的类型、描述等信息全都写清楚,是一件比较费时费力的事情。我们希望有一个能够减少开发人员负担的方法。 2. 现在不是有很多专业的 API 管理工具吗,我直接用专业的 API 管理工具去维护也是可以的吧。 API 管理工具我们也是有考虑的,但是有如下的问题: 很多工具依然缺少自动化的API生成; 不是专注于解决微服务领域的问题,随着服务量迅速上升,管理起来依旧比较困难。 3. 那微服务框架本身也会有提供相关的接口管理功能吧,Dubbo可以用Dubbo Admin,Spring Cloud可以用Spring Boot Admin,它们不香吗? 这里篇幅有限,我们不再去详细讲述开源工具我们怎么去一步步使用,详情见表格: 从表格可以看到,EDAS 3.0 微服务治理的服务契约,支持版本更广泛了,配置难度更低了,代码侵入性没有了,直接用 EDAS 3.0 的 Agent 方案,它不是更香了? EDAS 3.0 服务契约实践 下面我们来体验一下,EDAS 3.0 上如何查看 Spring Cloud 的微服务契约。 创建应用 根据你的需要,选择集群类型和应用运行环境,创建 Provider 和 Consumer 应用。 服务查询控制台 登录 EDAS 3.0 控制台,在页面左上角选择地域; 左侧导航栏选择:微服务治理 -> Spring Cloud / Dubbo / HSF -> 服务查询; 服务查询页面单击某个服务的详情。 查看服务契约 服务详情页面包括基本信息、服务调用关系、接口元数据、元数据等信息。在“接口元数据”一栏,便可查看服务的API信息。当用户使用Swagger注解时,会在“描述”列显示相应信息。 服务契约实现细节 在设计服务契约功能的时候,我们不但解决了开源框架中配置难度大,且部分方案具有代码侵入性的问题,而且针对如下阶段的难点都做了相应的方案,相信这些地方也是微服务框架的使用者会关心的: 数据获取 获取的同时是否还需要其他配置? 如何获取所需的方法名及描述、参数列表及描述、返回类型等信息? 会不会影响服务的性能? 信息能不能全面的拿到? 能不能同步接口的变更? 数据解析 能不能看到参数类型/返回值类型的详细结构? 解析参数结构的时候会不会影响启动时间? 泛型、枚举是否支持? 循环引用如何解决? 下面我们来详细介绍一下这几点都是如何解决的。 数据获取 为了减少用户的配置和使用难度,我们采用了 Agent 方案,用户无需任何额外的代码和配置,就可以使用我们的微服务治理功能。 Java Agent是一种字节码增强技术,运行时插入我们的代码,便可稳定的享受到所有的增强功能。 而且通过测试可得,只要在 SpringMVC 的映射处理阶段,选取合适的拦截点,就可以获取到所有的方法映射信息,包括方法名、参数列表、返回值类型、注解信息。由于该点在应用启动过程中只发生一次,因此不会有性能的影响。 我们获取的注解主要是针对 Swagger 注解。作为 OpenAPI 规范的主要指定者, Swagger 虽并非是唯一支持 OpenAPI 的工具,但也基本属于一种事实标准。注解解析的内容在表格的描述部分进行展示: Swagger2的注解解析(如@ApiOperation,@ApiParam,@ApiImplicitParam),解析value值在“描述”列显示; OpenAPI3的注解解析(如@Operation,@Parameter),解析description值在“描述”列显示。 当接口发生变更时,只要将新版本的应用部署上去,显示的服务契约信息就会是最新的,无需担心接口描述信息不能同步的问题。 数据解析 如果参数列表/返回值的类型是一个复杂类型,一般情况我们只看到一个类型名。那么有没有办法可以看到这个复杂类型的具体构成呢? 聪明的你可能就会想到,通过反射来递归遍历该类所有的 Field ,不就都解决了?思路确实如此,但实际要考虑的情况会更复杂一些。 以该复杂类型 CartItem 为例,它可能不但会包含基本类型,还可能会涉及到泛型、枚举,以及存在循环引用的情况。 因此在解析该类型之前,我们需要先判断一下该类型是否存在泛型、枚举的情况,如果是,需要额外解析并存储泛型列表及枚举列表。 而循环引用问题,我们只需借助一个 typeCache 即可解决。如下图,A和B构成了一个循环引用。 如果我们不采取任何措施,递归遍历将永远没有出口。但是,如果我们在遍历A的所有类型之前,先判断一下 typeCache 里是否存在 TypeA 。对 TypeB 也以此类推: 那么当遍历 ObjB 中所包含类型时,如果遇到了 TypeA ,同样也会先判断 typeCache 中是否存在。如存在,就无需再递归遍历 ObjA 中所有的类型了,而是直接记录一个 A 的引用。因此,循环引用问题也就得以解决。 最终的解析信息,可以在服务测试功能中得以体现。未来我们可能会支持直接在服务查询中的服务契约页,通过一个入口显示复杂类型的具体解析结构。 由此我们看到,在服务契约的获取及解析阶段,涉及到的可能影响用户体验的问题都得到了一定的解决。 作者信息: 刘旖明,花名眉生,北京邮电大学计算机学院在读研究生,暑期作为阿里云云原生部门实习开发工程师,主要进行阿里云微服务产品的相关研发,目前关注微服务、云原生等技术方向。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-25 关键词: 嵌入式 C语言

  • ElasticSearch 索引 VS MySQL 索引

    前言 这段时间在维护产品的搜索功能,每次在管理台看到 elasticsearch 这么高效的查询效率我都很好奇他是如何做到的。 这甚至比在我本地使用 MySQL 通过主键的查询速度还快。 为此我搜索了相关资料: 这类问题网上很多答案,大概意思呢如下: ES 是基于 Lucene 的全文检索引擎,它会对数据进行分词后保存索引,擅长管理大量的索引数据,相对于 MySQL 来说不擅长经常更新数据及关联查询。 说的不是很透彻,没有解析相关的原理;不过既然反复提到了索引,那我们就从索引的角度来对比下两者的差异。 MySQL 索引 先从 MySQL 说起,索引这个词想必大家也是烂熟于心,通常存在于一些查询的场景,是典型的空间换时间的案例。 以下内容以 Innodb 引擎为例。 常见的数据结构 假设由我们自己来设计 MySQL 的索引,大概会有哪些选择呢? 散列表 首先我们应当想到的是散列表,这是一个非常常见且高效的查询、写入的数据结构,对应到 Java 中就是 HashMap 这个数据结构应该不需要过多介绍了,它的写入效率很高O(1),比如我们要查询 id=3 的数据时,需要将 3 进行哈希运算,然后再这个数组中找到对应的位置即可。 但如果我们想查询 1≤id≤6 这样的区间数据时,散列表就不能很好的满足了,由于它是无序的,所以得将所有数据遍历一遍才能知道哪些数据属于这个区间。 有序数组 有序数组的查询效率也很高,当我们要查询 id=4 的数据时,只需要通过二分查找也能高效定位到数据O(logn)。 同时由于数据也是有序的,所以自然也能支持区间查询;这么看来有序数组适合用做索引咯? 自然是不行,它有另一个重大问题;假设我们插入了 id=2.5 的数据,就得同时将后续的所有数据都移动一位,这个写入效率就会变得非常低。 平衡二叉树 既然有序数组的写入效率不高,那我们就来看看写入效率高的,很容易就能想到二叉树;这里我们以平衡二叉树为例: 由于平衡二叉树的特性: 左节点小于父节点、右节点大于父节点。 所以假设我们要查询 id=11 的数据,只需要查询 10—>12—>11 便能最终找到数据,时间复杂度为O(logn),同理写入数据时也为O(logn)。 但依然不能很好的支持区间范围查找,假设我们要查询5≤id≤20 的数据时,需要先查询10节点的左子树再查询10节点的右子树最终才能查询到所有数据。 导致这样的查询效率并不高。 跳表 跳表可能不像上边提到的散列表、有序数组、二叉树那样日常见的比较多,但其实 Redis 中的 sort set 就采用了跳表实现。 这里我们简单介绍下跳表实现的数据结构有何优势。 我们都知道即便是对一个有序链表进行查询效率也不高,由于它不能使用数组下标进行二分查找,所以时间复杂度是o(n) 但我们也可以巧妙的优化链表来变相的实现二分查找,如下图: 我们可以为最底层的数据提取出一级索引、二级索引,根据数据量的不同,我们可以提取出 N 级索引。 当我们查询时便可以利用这里的索引变相的实现了二分查找。 假设现在要查询 id=13 的数据,只需要遍历 1—>7—>10—>13 四个节点便可以查询到数据,当数越多时,效率提升会更明显。 同时区间查询也是支持,和刚才的查询单个节点类似,只需要查询到起始节点,然后依次往后遍历(链表有序)到目标节点便能将整个范围的数据查询出来。 同时由于我们在索引上不会存储真正的数据,只是存放一个指针,相对于最底层存放数据的链表来说占用的空间便可以忽略不计了。 平衡二叉树的优化 但其实 MySQL 中的 Innodb 并没有采用跳表,而是使用的一个叫做 B+ 树的数据结构。 这个数据结构不像是二叉树那样大学老师当做基础数据结构经常讲到,由于这类数据结构都是在实际工程中根据需求场景在基础数据结构中演化而来。 比如这里的 B+ 树就可以认为是由平衡二叉树演化而来。 刚才我们提到二叉树的区间查询效率不高,针对这一点便可进行优化: 在原有二叉树的基础上优化后:所有的非叶子都不存放数据,只是作为叶子节点的索引,数据全部都存放在叶子节点。 这样所有叶子节点的数据都是有序存放的,便能很好的支持区间查询。 只需要先通过查询到起始节点的位置,然后在叶子节点中依次往后遍历即可。 当数据量巨大时,很明显索引文件是不能存放于内存中,虽然速度很快但消耗的资源也不小;所以 MySQL 会将索引文件直接存放于磁盘中。 这点和后文提到 elasticsearch 的索引略有不同。 由于索引存放于磁盘中,所以我们要尽可能的减少与磁盘的 IO(磁盘 IO 的效率与内存不在一个数量级) 通过上图可以看出,我们要查询一条数据至少得进行 4 次IO,很明显这个 IO 次数是与树的高度密切相关的,树的高度越低 IO 次数就会越少,同时性能也会越好。 那怎样才能降低树的高度呢? 我们可以尝试把二叉树变为三叉树,这样树的高度就会下降很多,这样查询数据时的 IO 次数自然也会降低,同时查询效率也会提高许多。 这其实就是 B+ 树的由来。 使用索引的一些建议 其实通过上图对 B+树的理解,也能优化日常工作的一些小细节;比如为什么需要最好是有序递增的? 假设我们写入的主键数据是无序的,那么有可能后写入数据的 id 小于之前写入的,这样在维护 B+树 索引时便有可能需要移动已经写好数据。 如果是按照递增写入数据时则不会有这个考虑,每次只需要依次写入即可。 所以我们才会要求数据库主键尽量是趋势递增的,不考虑分表的情况时最合理的就是自增主键。 整体来看思路和跳表类似,只是针对使用场景做了相关的调整(比如数据全部存储于叶子节点)。 ES 索引 MySQL 聊完了,现在来看看 Elasticsearch 是如何来使用索引的。 正排索引 在 ES 中采用的是一种名叫倒排索引的数据结构;在正式讲倒排索引之前先来聊聊和他相反的正排索引。 以上图为例,我们可以通过 doc_id 查询到具体对象的方式称为使用正排索引,其实也能理解为一种散列表。 本质是通过 key 来查找 value。 比如通过 doc_id=4 便能很快查询到 name=jetty wang,age=20 这条数据。 倒排索引 那如果反过来我想查询 name 中包含了 li 的数据有哪些?这样如何高效查询呢? 仅仅通过上文提到的正排索引显然起不到什么作用,只能依次将所有数据遍历后判断名称中是否包含 li ;这样效率十分低下。 但如果我们重新构建一个索引结构: 当要查询 name 中包含 li 的数据时,只需要通过这个索引结构查询到 Posting List 中所包含的数据,再通过映射的方式查询到最终的数据。 这个索引结构其实就是倒排索引。 Term Dictionary 但如何高效的在这个索引结构中查询到 li 呢,结合我们之前的经验,只要我们将 Term 有序排列,便可以使用二叉树搜索树的数据结构在o(logn) 下查询到数据。 将一个文本拆分成一个一个独立Term 的过程其实就是我们常说的分词。 而将所有 Term 合并在一起就是一个 Term Dictionary,也可以叫做单词词典。 英文的分词相对简单,只需要通过空格、标点符号将文本分隔便能拆词,中文则相对复杂,但也有许多开源工具做支持(由于不是本文重点,对分词感兴趣的可以自行搜索)。 当我们的文本量巨大时,分词后的 Term 也会很多,这样一个倒排索引的数据结构如果存放于内存那肯定是不够存的,但如果像 MySQL 那样存放于磁盘,效率也没那么高。 Term Index 所以我们可以选择一个折中的方法,既然无法将整个 Term Dictionary 放入内存中,那我们可以为Term Dictionary 创建一个索引然后放入内存中。 这样便可以高效的查询Term Dictionary ,最后再通过Term Dictionary 查询到 Posting List。 相对于 MySQL 中的 B+树来说也会减少了几次磁盘IO。 这个 Term Index 我们可以使用这样的 Trie树 也就是我们常说的字典树 来存放。 更多关于字典树的内容请查看这里。 如果我们是以 j 开头的 Term 进行搜索,首先第一步就是通过在内存中的 Term Index 查询出以 j 打头的 Term 在 Term Dictionary 字典文件中的哪个位置(这个位置可以是一个文件指针,可能是一个区间范围)。 紧接着在将这个位置区间中的所有 Term 取出,由于已经排好序,便可通过二分查找快速定位到具体位置;这样便可查询出 Posting List。 最终通过 Posting List 中的位置信息便可在原始文件中将目标数据检索出来。 更多优化 当然 ElasticSearch 还做了许多针对性的优化,当我们对两个字段进行检索时,就可以利用 bitmap 进行优化。 比如现在需要查询 name=li and age=18 的数据,这时我们需要通过这两个字段将各自的结果 Posting List 取出。 最简单的方法是分别遍历两个集合,取出重复的数据,但这个明显效率低下。 这时我们便可使用 bitmap 的方式进行存储(还节省存储空间),同时利用先天的位与 计算便可得出结果。 [1, 3, 5]       ⇒ 10101 [1, 2, 4, 5] ⇒ 11011 这样两个二进制数组求与便可得出结果: 10001 ⇒ [1, 5] 最终反解出 Posting List 为[1, 5],这样的效率自然是要高上许多。 同样的查询需求在 MySQL 中并没有特殊优化,只是先将数据量小的数据筛选出来之后再筛选第二个字段,效率自然也就没有 ES 高。 当然在最新版的 ES 中也会对 Posting List 进行压缩,具体压缩规则可以查看官方文档,这里就不具体介绍了。 总结 最后我们来总结一下: 通过以上内容可以看出再复杂的产品最终都是基础数据结构组成,只是会对不同应用场景针对性的优化,所以打好数据结构与算法的基础后再看某个新的技术或中间件时才能快速上手,甚至自己就能知道优化方向。 最后画个饼,后续我会尝试按照 ES 倒排索引的思路做一个单机版的搜索引擎,只有自己写一遍才能加深理解。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-25 关键词: 嵌入式 C语言

  • C语言常用的6种转换工具函数

    1、字符串转十六进制 代码实现: void StrToHex(char *pbDest, char *pbSrc, int nLen){  char h1,h2;  char s1,s2;  int i;    for (i=0; i 9)            s1 -= 7;        s2 = toupper(h2) - 0x30;        if (s2 > 9)            s2 -= 7;        pbDest[i] = s1*16 + s2;    }} 2、十六进制转字符串 代码实现: void HexToStr(char *pszDest, char *pbSrc, int nLen){    char    ddl, ddh;    for (int i = 0; i  57) ddh = ddh + 7;        if (ddl > 57) ddl = ddl + 7;        pszDest[i * 2] = ddh;        pszDest[i * 2 + 1] = ddl;    }    pszDest[nLen * 2] = '\0';} 或者 u16 Hex2StringArray (u8 *pSrc,  u16 SrcLen, u8 *pObj){    u16 i=0;    for(i=0;    i= '9' || *str  10)        *pbDest = 0;    tmp = 1;    *pbDest = 0;    for (i=nLen-1; i>=0; i--)    {        *pbDest += tmp*(*(pbSrc+i)-'0');        tmp = tmp*10;    }} 效果:字符串:”123” 转为 123 第三种:包含转为浮点数: //m^n函数//返回值:m^n次方.u32 NMEA_Pow(u8 m,u8 n){    u32 result=1;        while(n--)result*=m;        return result;}//str转换为数字,以','或者'*'结束//buf:数字存储区//dx:小数点位数,返回给调用函数//返回值:转换后的数值int NMEA_Str2num(u8 *buf,u8*dx){    u8 *p=buf;    u32 ires=0,fres=0;    u8 ilen=0,flen=0,i;    u8 mask=0;    int res;    while(1) //得到整数和小数的长度    {        if(*p=='-'){mask|=0X02;p++;}//是负数        if(*p==','||(*p=='*'))break;//遇到结束了        if(*p=='.'){mask|=0X01;p++;}//遇到小数点了        else if(*p>'9'||(*p> 16) & 0xFF);    buf[2] = ((u32Value >> 8) & 0xFF);    buf[3] = (u32Value & 0xFF);} 效果:整型 50 转字符数组 {‘\0’,’\0’,’\0’,’2’} u8数组转u32 void U8ArrayToU32(uint8_t *buf, uint32_t *u32Value){    *u32Value = (buf[0] 

    时间:2020-10-24 关键词: 嵌入式 C语言

  • 知识贴!单片机C语言编程之.H文件与.C文件的关系

    这个8*8按键程序的过程中,不管是在自己写还是参考别人程序的过程中,发现自己对C语言有些基本知识点和编程规范有很多不懂的地方,有些是自己以前的编程习惯不好,有些就是基础知识不扎实的表现,所以总结出来。 一、.H文件与.C文件的关系: 迄今为止,写过的程序都是一些很简单的程序,从来没有想到要自己写.H文件,也不知道.H文件到底什么用,与.C文件什么关系。只是最近写键盘程序,参考别人的程序时,发现别人写的严格的程序都带有一个“KEY.H”,里面定义了.C文件里用到的自己写的函数,如Keyhit()、Keyscan()等。 经过查找资料得知,.H文件就是头文件,估计就是Head的意思吧,这是规范程序结构化设计的需要,既可以实现大型程序的模块化,又可以实现根各模块的连接调试。 1、.H文件介绍: 在单片机C程序设计中,项目一般按功能模块化进行结构化设计。将一个项目划分为多个功能,每个功能的相关程序放在一个C程序文档中,称之为一个模块,对应的文件名即为模块名。一个模块通常由两个文档组成,一个为头文件*.h,对模块中的数据结构和函数原型进行描述;另一个则为C文件*.c ,对数据实例或对象定义,以及函数算法具体实现。 2、.H文件的作用 作为项目设计,除了对项目总体功能进行详细描述外,就是对每个模块进行详细定义,也就是给出所有模块的头文件。通常H头文件要定义模块中各函数的功能,以及输入和输出参数的要求。模块的具体实现,由项目组成根据H文件进行设计、编程、调试完成。为了保密和安全,模块实现后以可连接文件OBJ、或库文件LIB的方式提供给项目其他成员使用。由于不用提供源程序文档,一方面可以公开发行,保证开发人员的所有权;另一方面可以防止别人有意或无意修改产生非一致性,造成版本混乱。所以H头文件是项目的详细设计和团队工作划分的依据,也是对模块进行测试的功能说明。要引用模块内的数据或算法,只要用包含include指定模块H头文件即可。 3、.H文件的基本组成 /*如下为键盘驱动的头文档*/#ifndef _KEY_H_ //防重复引用,如果没有定义过_KEY_H_,则编译下句#define _KEY_H_ //此符号唯一, 表示只要引用过一次,即#i nclude,则定义符号_KEY_H_/////////////////////////////////////////////////////////////////char keyhit( void ); //击键否unsigned char Keyscan( void ); //取键值/////////////////////////////////////////////////////////////////#endif 二、尽量使用宏定义#define 开始看别人的程序时,发现程序开头,在文件包含后面有很多#define语句,当时就想,搞这么多标示符替换来替换去的,麻不麻烦啊,完全没有理解这种写法的好处。原来,用一个标示符表示常数,有利于以后的修改和维护,修改时只要在程序开头改一下,程序中所有用到的地方就全部修改,节省时间。 #define KEYNUM 65//按键数量,用于Keycode[KEYNUM]#define LINENUM 8//键盘行数#define ROWNUM 8//键盘列数 注意的地方: 1、宏名一般用大写 2、宏定义不是C语句,结尾不加分号 三、不要乱定义变量类型 以前写程序,当需要一个新的变量时,不管函数内还是函数外的,直接在程序开头定义,虽然不是原则上的错误,但是很不可取的作法。 下面说一下,C语言中变量类型的有关概念: 从变量的作用范围来分,分为局部变量和全局变量: 1、全局变量:是在函数外定义的变量,像我以前定义在程序开头的变量都是全局变量,这里我就犯了一个大忌,使用了过多的全局变量。 带来的问题有两个:一是,全局变量在程序全部执行过程中都占用资源;二是,全局变量过多使程序的通用性变差,因为全局变量是模块间耦合的原因之一。 2、局部变量:在函数内部定义的变量,只在函数内部有效。 从变量的变量值存在的时间分为两种: 1、静态存储变量:程序运行期间分配固定的存储空间。 2、动态存储变量:程序运行期间根据需要动态地分配存储空间。 具体又包括四种存储方式:auto static register extern 1、局部变量,不加说明默认为auto型,即动态存储,如果不赋初值,将是一个不确定的值。而将局部变量定义为static型的话,则它的值在函数内是不变的,且初值默认为0。 static unsigned char sts;//按键状态变量 static unsigned char Nowkeycode;//此时的键码 static unsigned char Prekeycode;//上一次的键码 static unsigned char Keydowntime;//矩形键盘按下去抖时间变量 static unsigned char Keyuptime;//矩形键盘释放去抖时间变量 static unsigned char Onoffdowntime;//关机键按下去抖时间变量 static unsigned char Onoffuptime;//关机键释放去抖时间变量 static unsigned char onoff_10ms; //判断关机键中断次数变量,累计150次大约为3S,因为前后进了两个10ms中断 2、全局变量,编译时分配为静态存储区,可以被本文件中的各个函数引用。如果是多个文件的话,如果在一个文件中引用另外文件中的变量,在此文件中要用extern说明。不过如果一个全局变量定义为static的话,就只能在此一个文件中使用。 四、特殊关键字const volatile的使用 1、const const用于声明一个只读的变量 const unsigned char a=1;//定义a=1,编译器不允许修改a的值 作用:保护不希望被修改的参数 const unsigned char Key_code[KEYNUM]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, 0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10, 0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, 0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20, 0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, 0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30, 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38, 0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,0x40, 0x41 };//键码 const unsigned char Line_out[LINENUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//行输出编码 const unsigned char Row_in[ROWNUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//列输入编码 2、volatile 一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。 static int i=0;int main(void){...while (1){if (i)dosomething();}}/* Interrupt service routine. */void ISR_2(void){i=1;} 程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。 一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其它程序检测的变量需要加volatile; 2、多任务环境下各任务间共享的标志应该加volatile; 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。 ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ 免责声明:本文系网络转载,版权归原作者所有。如有问题,请联系我们,谢谢! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-24 关键词: 单片机 C语言

  • 什么是状态机?

    前言 状态机在实际工作开发中应用非常广泛,在刚进入公司的时候,根据公司产品做流程图的时候,发现自己经常会漏了这样或那样的状态,导致整体流程会有问题,后来知道了状态机这样的东西,发现用这幅图就可以很清晰的表达整个状态的流转。 一口君曾经做过很多网络协议模块,很多协议的开发都必须用到状态机;一个健壮的状态机可以让你的程序,不论发生何种突发事件都不会突然进入一个不可预知的程序分支。 本篇通过C语言实现一个简单的进程5状态模型的状态机,让大家熟悉一下状态机的魅力。 什么是状态机? 定义 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。 先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个LED等,就有 亮 和 灭两种状态。我们通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如LED灯的状态就是两个 亮和 灭。 状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。 举例 以物理课学的灯泡图为例,就是一个最基本的小型状态机 可以画出以下的状态机图 这里就是两个状态:①灯泡亮,②灯泡灭 如果打开开关,那么状态就会切换为 灯泡亮 。灯泡亮 状态下如果关闭开关,状态就会切换为 灯泡灭。 状态机的全称是有限状态自动机,自动两个字也是包含重要含义的。给定一个状态机,同时给定它的当前状态以及输入,那么输出状态时可以明确的运算出来的。例如对于灯泡,给定初始状态灯泡灭 ,给定输入“打开开关”,那么下一个状态时可以运算出来的。 四大概念 下面来给出状态机的四大概念。 State ,状态。一个状态机至少要包含两个状态。例如上面灯泡的例子,有 灯泡亮和 灯泡灭两个状态。 Event ,事件。事件就是执行某个操作的触发条件或者口令。对于灯泡,“打开开关”就是一个事件。 Action ,动作。事件发生以后要执行动作。例如事件是“打开开关”,动作是“开灯”。编程的时候,一个 Action 一般就对应一个函数。 Transition ,变换。也就是从一个状态变化为另一个状态。例如“开灯过程”就是一个变换。 状态机的应用 状态机是一个对真实世界的抽象,而且是逻辑严谨的数学抽象,所以明显非常适合用在数字领域。可以应用到各个层面上,例如硬件设计,编译器设计,以及编程实现各种具体业务逻辑的时候。 进程5状态模型 进程管理是Linux五大子系统之一,非常重要,实际实现起来非常复杂,我们来看下进程是如何切换状态的。 下图是进程的5状态模型: 关于该图简单介绍如下: 可运行态:当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。 浅度睡眠态(可中断):进程正在睡眠(被阻塞),等待资源到来是唤醒,也可以通过其他进程信号或时钟中断唤醒,进入运行队列。 深度睡眠态(不可中断):其和浅度睡眠基本类似,但有一点就是不可由其他进程信号或时钟中断唤醒。只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。 暂停状态:当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。 僵死状态:当进程已停止运行,但其父进程还没有询问其状态时,未释放PCB,则称该进程处于僵死状态。 进程的状态就是按照这个状态图进行切换的。 该状态流程有点复杂,因为我们目标只是实现一个简单的状态机,所以我们简化一下该状态机如下: 要想实现状态机,首先将该状态机转换成下面的状态迁移表。简要说明如下:假设当前进程处于running状态下,那么只有schedule事件发生之后,该进程才会产生状态的迁移,迁移到owencpu状态下,如果在此状态下发生了其他的事件,比如wake、wait_event都不会导致状态的迁移。 如上图所示: 每一列表示一个状态,每一行对应一个事件。 该表是实现状态机的最核心的一个图,请读者详细对比该表和状态迁移图的的关系。 实际场景中,进程的切换会远比这个图复杂,好在众多大神都帮我们解决了这些复杂的问题,我们只需要站在巨人的肩膀上就可以了。 实现 根据状态迁移表,定义该状态机的状态如下: typedef enum {  sta_origin=0,  sta_running,  sta_owencpu,  sta_sleep_int,  sta_sleep_unint}State; 发生的事件如下: typedef enum{  evt_fork=0,  evt_sched,  evt_wait,  evt_wait_unint,  evt_wake_up,  evt_wake, }EventID; 不论是状态还是事件都可以根据实际情况增加调整。 定义一个结构体用来表示当前状态转换信息: typedef struct {  State curState;//当前状态  EventID eventId;//事件ID  State nextState;//下个状态  CallBack action;//回调函数,事件发生后,调用对应的回调函数}StateTransform ;  事件回调函数:实际应用中不同的事件发生需要执行不同的action,就需要定义不同的函数, 为方便起见,本例所有的事件都统一使用同一个回调函数。功能:打印事件发生后进程的前后状态,如果状态发生了变化,就调用对应的回调函数。 void action_callback(void *arg){ StateTransform *statTran = (StateTransform *)arg;  if(statename[statTran->curState] == statename[statTran->nextState]) {  printf("invalid event,state not change\n"); }else{  printf("call back state from %s --> %s\n",   statename[statTran->curState],   statename[statTran->nextState]); }} 为各个状态定义迁移表数组: /*origin*/StateTransform stateTran_0[]={ {sta_origin,evt_fork,        sta_running,action_callback}, {sta_origin,evt_sched,       sta_origin,NULL}, {sta_origin,evt_wait,        sta_origin,NULL}, {sta_origin,evt_wait_unint,  sta_origin,NULL}, {sta_origin,evt_wake_up,     sta_origin,NULL}, {sta_origin,evt_wake,        sta_origin,NULL},}; /*running*/StateTransform stateTran_1[]={ {sta_running,evt_fork,        sta_running,NULL}, {sta_running,evt_sched,       sta_owencpu,action_callback}, {sta_running,evt_wait,        sta_running,NULL}, {sta_running,evt_wait_unint,  sta_running,NULL}, {sta_running,evt_wake_up,     sta_running,NULL}, {sta_running,evt_wake,        sta_running,NULL},}; /*owencpu*/StateTransform stateTran_2[]={ {sta_owencpu,evt_fork,        sta_owencpu,NULL}, {sta_owencpu,evt_sched,       sta_owencpu,NULL}, {sta_owencpu,evt_wait,        sta_sleep_int,action_callback}, {sta_owencpu,evt_wait_unint,  sta_sleep_unint,action_callback}, {sta_owencpu,evt_wake_up,     sta_owencpu,NULL}, {sta_owencpu,evt_wake,        sta_owencpu,NULL},}; /*sleep_int*/StateTransform stateTran_3[]={ {sta_sleep_int,evt_fork,        sta_sleep_int,NULL}, {sta_sleep_int,evt_sched,       sta_sleep_int,NULL}, {sta_sleep_int,evt_wait,        sta_sleep_int,NULL}, {sta_sleep_int,evt_wait_unint,  sta_sleep_int,NULL}, {sta_sleep_int,evt_wake_up,     sta_sleep_int,NULL}, {sta_sleep_int,evt_wake,        sta_running,action_callback},}; /*sleep_unint*/StateTransform stateTran_4[]={ {sta_sleep_unint,evt_fork,        sta_sleep_unint,NULL}, {sta_sleep_unint,evt_sched,       sta_sleep_unint,NULL}, {sta_sleep_unint,evt_wait,        sta_sleep_unint,NULL}, {sta_sleep_unint,evt_wait_unint,  sta_sleep_unint,NULL}, {sta_sleep_unint,evt_wake_up,     sta_running,action_callback}, {sta_sleep_unint,evt_wake,        sta_sleep_unint,NULL},};  实现event发生函数: void event_happen(unsigned int event)功能: 根据发生的event以及当前的进程state,找到对应的StateTransform 结构体,并调用do_action() void do_action(StateTransform *statTran)功能: 根据结构体变量StateTransform,实现状态迁移,并调用对应的回调函数。 #define STATETRANS(n)  (stateTran_##n)/*change state & call callback()*/void do_action(StateTransform *statTran){ if(NULL == statTran) {  perror("statTran is NULL\n");  return; } //状态迁移 globalState = statTran->nextState; if(statTran->action != NULL) {//调用回调函数  statTran->action((void*)statTran); }else{  printf("invalid event,state not change\n"); }}void event_happen(unsigned int event){ switch(globalState) {  case sta_origin:   do_action(&STATETRANS(0)[event]);   break;  case sta_running:   do_action(&STATETRANS(1)[event]);   break;  case sta_owencpu:   do_action(&STATETRANS(2)[event]);    break;  case sta_sleep_int:   do_action(&STATETRANS(3)[event]);    break;  case sta_sleep_unint:   do_action(&STATETRANS(4)[event]);    break;  default:   printf("state is invalid\n");   break; }} 测试程序:功能: 初始化状态机的初始状态为sta_origin; 创建子线程,每隔一秒钟显示当前进程状态; 事件发生顺序为:evt_fork-->evt_sched-->evt_sched-->evt_wait-->evt_wake。 读者可以跟自己的需要,修改事件发生顺序,观察状态的变化。 main.c /*显示当前状态*/void *show_stat(void *arg){ int len; char buf[64]={0};  while(1) {  sleep(1);  printf("cur stat:%s\n",statename[globalState]); } }void main(void){ init_machine(); //创建子线程,子线程主要用于显示当前状态 pthread_create(&pid, NULL,show_stat, NULL); sleep(5); event_happen(evt_fork); sleep(5); event_happen(evt_sched); sleep(5); event_happen(evt_sched); sleep(5); event_happen(evt_wait); sleep(5); event_happen(evt_wake); } 运行结果:由结果可知: evt_fork-->evt_sched-->evt_sched-->evt_wait-->evt_wake 该事件发生序列对应的状态迁移顺序为: origen-->running-->owencpu-->owencpu-->sleep_int-->running  猜你喜欢 C语言、嵌入式中几个非常实用的宏技巧 C语言高效编程与代码优化 1024G 嵌入式资源大放送!包括但不限于C/C++、单片机、Linux等。在公众号聊天界面回复1024,即可免费获取! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-24 关键词: 嵌入式 C语言

  • 单片机C语言之串口通信协议(代码分享)

    现实生活中, 我们总是要与人打交道,互通有无。单片机也一样,需要跟各种设备交互。例如汽车的显示仪表需要知道汽车的转速及电动机的运行参数,那么显示仪表就需要从汽车的底层控制器取得数据。而这个数据的获得过程就是一个通信过程。类似的例子还有控制器通常是单片机或者PLC与变频器的通信。通信的双方需要遵守一套既定的规则也称为协议,这就好比我们人之间的对话,需要在双方都遵守一套语言语法规则才有可能达成对话。 通信协议又分为硬件层协议和软件层协议。硬件层协议主要规范了物理上的连线,传输电平信号及传输的秩序等硬件性质的内容。常用的硬件协议有串口,IIC, SPI, RS485, CAN和 USB。软件层协议则更侧重上层应用的规范,比如modbus协议。好了,那这里我们就着重介绍51单片机的串口通信协议,以下简称串口。串口的6个特征如下。(1)、物理上的连线至少3根,分别是Tx数据发送线,Rx数据接收线,GND共用地线。 (2)、0与1的约定。RS232电平,约定﹣5V至﹣25V之间的电压信号为1,﹢5V至﹢25V之间的电压信号为0 。TTL电平,约定5V的电压信号为1,0V电压信号为0 。CMOS电平,约定3.3V的电压信号为1,0V电压信号为0 。其中,CMOS电平一般用于ARM芯片中。 (3)、发送秩序。低位先发。 (4)、波特率。收发双方共同约定的一个数据位(0或1)在数据传输线上维持的时间。也可理解为每秒可以传输的位数。常用的波特率有300bit/s, 600bit/s, 2400bit/s, 4800bit/s, 9600bit/s。 (5)、通信的起始信号。发送方在没有发送数据时,应该将Tx置1 。当需发送时,先将Tx置0,并且保持1位的时间。接受方不断地侦测Rx,如果发现Rx常时间变高后,突然被拉低(置为0),则视为发送方将要发送数据,迅速启动自己的定时器,从而保证了收发双方定时器同步定时。 (6)、停止信号。发送方发送完最后一个有效位时,必须再将Tx保持1位的时间,即为停止位。 好了,理论暂时到这里,现在我们要做一个实验,将一个字节从51单片机发送到电脑串口调试助手上。这个实验的目的是为了掌握串口通信协议的收发过程。 虚拟串口 实验一、虚拟串口实验 一般单片机都有专门的串口引脚,51里面分别是P3.0和P3.1,这些引脚拥有串口的硬件电路,因此使用它们并不需要设置信号的发送停止。为了掌握协议,我们使用其他的引脚来模拟串口,所以也叫虚拟串口。这里我们选用P1.0,然而注意到我们51单片机要发送数据给电脑,必须经过一个串口转USB设备(即TTL电平转换为RS232电平),而限于我们的开发板只有P3.0与P3.1连接到了串口转USB设备,所以我们可以将P1.0短接到P3.1 。下图是这个串口转USB的原理图。 好了直接上代码吧。 [cpp] view plain copy#include "reg51.h"/* 将P1.0虚拟成串口发送脚TX 以9600bit/s的比特率向外发送数据 因为波特率是 9600bit/s 所以me发送一位的时间是 t=1000000us/9600=104us */sbit TX=P3^1; //P1^0 output TTL signal, need to transferred to rs232 signal, can be connected to P3^1 #define u16 unsigned int //宏定义 #define u8 unsigned char u8 sbuf; bit ti=0; void delay(u16 x){ while(x--); } void Timer0_Init(){ TMOD |= 0x01; TH0=65440/256; TH0=65440%256; TR0=0; } void Isr_Init(){ EA=1; ET0=1; } void Send_Byte(u8 dat){ sbuf=dat;//通过引入全局变量sbuf,可以保存形参dat TX=0; //A 起始位 TR0=1; while(ti==0); //等待发送完成 ti=0; //清除发送完成标志 } void TF0_isr() interrupt 1 //每104us进入一次中断 { static u8 i; //记录进入中断的次数 TH0=65440/256; TL0=65440%256; i++; if(i>=1 && i

    时间:2020-10-24 关键词: 单片机 C语言

  • C语言如何实现动态扩容的string

    又好久没更新了,最近程序喵工作实在是太忙,业余时间也在学习新知识酝酿大招,便于后期更新好文,最近先水几篇吧,大家有想了解的知识点可以在文末读者讨论中留言哈! 众所周知,C++ 中的string使用比较方便,关于C++ 中的string源码实现可以看我的这篇文章:源码分析C++的string的实现最近工作中使用C语言,但又苦于没有高效的字符串实现,字符串的拼接和裁剪都比较麻烦,而且每个字符串都需要申请内存,内存的申请和释放也很容易出bug,怎么高效的实现一个不需要处理内存问题并且可以动态扩容进行拼接和裁剪的string呢? 一个好的string应该有以下功能? 创建字符串 删除字符串 尾部追加字符串 头部插入字符串 从尾部删除N个字符 从头部删除N个字符 裁剪字符串 获取字符串长度 获取完整字符串 下面来看看各个功能的实现:首先定义一个string的句柄,相当于C++中的实例 struct c_string;typedef struct c_string c_string_t; 在内部string的实现如下: // string的初始内存大小static const size_t c_string_min_size = 32;struct c_string {    char *str; // 字符串指针    size_t alloced; // 已分配的内存大小    size_t len; // 字符串的实际长度}; 创建字符串: c_string_t *c_string_create(void) {    c_string_t *cs;    cs = calloc(1, sizeof(*cs));    cs->str = malloc(c_string_min_size);    *cs->str = '\0';    // 初始分配内存大小是32,之后每次以2倍大小扩容    cs->alloced = c_string_min_size;     cs->len = 0;    return cs;} 销毁字符串: void c_string_destroy(c_string_t *cs) {    if (cs == NULL) return;    free(cs->str);    free(cs);} 内部如何扩容呢: static void c_string_ensure_space(c_string_t *cs, size_t add_len) {    if (cs == NULL || add_len == 0) return;    if (cs->alloced >= cs->len + add_len + 1) return;    while (cs->alloced len + add_len + 1) {        cs->alloced alloced--;        }    }    cs->str = realloc(cs->str, cs->alloced);} 在尾部追加字符串: void c_string_append_str(c_string_t *cs, const char *str, size_t len) {    if (cs == NULL || str == NULL || *str == '\0') return;    if (len == 0) len = strlen(str);    c_string_ensure_space(cs, len); // 确保内部有足够的空间存储字符串    memmove(cs->str + cs->len, str, len);    cs->len += len;    cs->str[cs->len] = '\0';} 在尾部追加字符: void c_string_append_char(c_string_t *cs, char c) {    if (cs == NULL) return;    c_string_ensure_space(cs, 1);    cs->str[cs->len] = c;    cs->len++;    cs->str[cs->len] = '\0';} 在尾部追加整数: void c_string_append_int(c_string_t *cs, int val) {    char str[12];    if (cs == NULL) return;    snprintf(str, sizeof(str), "%d", val); // 整数转为字符串    c_string_append_str(cs, str, 0);} 在头部插入字符串: void c_string_front_str(c_string_t *cs, const char *str, size_t len) {    if (cs == NULL || str == NULL || *str == '\0') return;    if (len == 0) len = strlen(str);    c_string_ensure_space(cs, len);    memmove(cs->str + len, cs->str, cs->len);    memmove(cs->str, str, len);    cs->len += len;    cs->str[cs->len] = '\0';} 在头部插入字符: void c_string_front_char(c_string_t *cs, char c) {    if (cs == NULL) return;    c_string_ensure_space(cs, 1);    memmove(cs->str + 1, cs->str, cs->len);    cs->str[0] = c;    cs->len++;    cs->str[cs->len] = '\0';} 在头部插入整数: void c_string_front_int(c_string_t *cs, int val) {    char str[12];    if (cs == NULL) return;    snprintf(str, sizeof(str), "%d", val);    c_string_front_str(cs, str, 0);} 清空字符串: void c_string_clear(c_string_t *cs) {    if (cs == NULL) return;    c_string_truncate(cs, 0);} 裁剪字符串: void c_string_truncate(c_string_t *cs, size_t len) {    if (cs == NULL || len >= cs->len) return;    cs->len = len;    cs->str[cs->len] = '\0';} 删除头部的N个字符: void c_string_drop_begin(c_string_t *cs, size_t len) {    if (cs == NULL || len == 0) return;    if (len >= cs->len) {        c_string_clear(cs);        return;    }    cs->len -= len;    memmove(cs->str, cs->str + len, cs->len + 1);} 删除尾部的N个字符: void c_string_drop_end(c_string_t *cs, size_t len) {    if (cs == NULL || len == 0) return;    if (len >= cs->len) {        c_string_clear(cs);        return;    }    cs->len -= len;    cs->str[cs->len] = '\0';} 获取字符串的长度: size_t c_string_len(const c_string_t *cs) {    if (cs == NULL) return 0;    return cs->len;} 返回字符串指针,使用的是内部的内存: const char *c_string_peek(const c_string_t *cs) {    if (cs == NULL) return NULL;    return cs->str;} 重新分配一块内存存储字符串返回: char *c_string_dump(const c_string_t *cs, size_t *len) {    char *out;    if (cs == NULL) return NULL;    if (len != NULL) *len = cs->len;    out = malloc(cs->len + 1);    memcpy(out, cs->str, cs->len + 1);    return out;} 测试代码如下: int main() {    c_string_t *cs = c_string_create();    c_string_append_str(cs, "123", 0);    c_string_append_char(cs, '4');    c_string_append_int(cs, 5);    printf("%s \n", c_string_peek(cs));    c_string_front_str(cs, "789", 0);    printf("%s \n", c_string_peek(cs));    c_string_drop_begin(cs, 2);    printf("%s \n", c_string_peek(cs));    c_string_drop_end(cs, 2);    printf("%s \n", c_string_peek(cs));    c_string_destroy(cs);    return 0;} 输出: 12345789123459123459123 完整代码如下:头文件: #include struct c_string;typedef struct c_string c_string_t;c_string_t *c_string_create(void);void c_string_destroy(c_string_t *cs);void c_string_append_str(c_string_t *cs, const char *str, size_t len);void c_string_append_char(c_string_t *cs, char c);void c_string_append_int(c_string_t *cs, int val);void c_string_front_str(c_string_t *cs, const char *str, size_t len);void c_string_front_char(c_string_t *cs, char c);void c_string_front_int(c_string_t *cs, int val);void c_string_clear(c_string_t *cs);void c_string_truncate(c_string_t *cs, size_t len);void c_string_drop_begin(c_string_t *cs, size_t len);void c_string_drop_end(c_string_t *cs, size_t len);size_t c_string_len(const c_string_t *cs);const char *c_string_peek(const c_string_t *cs);char *c_string_dump(const c_string_t *cs, size_t *len); 源文件: #include #include #include #include #include static const size_t c_string_min_size = 32;struct c_string {    char *str;    size_t alloced;    size_t len;};c_string_t *c_string_create(void) {    c_string_t *cs;    cs = calloc(1, sizeof(*cs));    cs->str = malloc(c_string_min_size);    *cs->str = '\0';    cs->alloced = c_string_min_size;    cs->len = 0;    return cs;}void c_string_destroy(c_string_t *cs) {    if (cs == NULL) return;    free(cs->str);    free(cs);}static void c_string_ensure_space(c_string_t *cs, size_t add_len) {    if (cs == NULL || add_len == 0) return;    if (cs->alloced >= cs->len + add_len + 1) return;    while (cs->alloced len + add_len + 1) {        cs->alloced alloced--;        }    }    cs->str = realloc(cs->str, cs->alloced);}void c_string_append_str(c_string_t *cs, const char *str, size_t len) {    if (cs == NULL || str == NULL || *str == '\0') return;    if (len == 0) len = strlen(str);    c_string_ensure_space(cs, len);    memmove(cs->str + cs->len, str, len);    cs->len += len;    cs->str[cs->len] = '\0';}void c_string_append_char(c_string_t *cs, char c) {    if (cs == NULL) return;    c_string_ensure_space(cs, 1);    cs->str[cs->len] = c;    cs->len++;    cs->str[cs->len] = '\0';}void c_string_append_int(c_string_t *cs, int val) {    char str[12];    if (cs == NULL) return;    snprintf(str, sizeof(str), "%d", val);    c_string_append_str(cs, str, 0);}void c_string_front_str(c_string_t *cs, const char *str, size_t len) {    if (cs == NULL || str == NULL || *str == '\0') return;    if (len == 0) len = strlen(str);    c_string_ensure_space(cs, len);    memmove(cs->str + len, cs->str, cs->len);    memmove(cs->str, str, len);    cs->len += len;    cs->str[cs->len] = '\0';}void c_string_front_char(c_string_t *cs, char c) {    if (cs == NULL) return;    c_string_ensure_space(cs, 1);    memmove(cs->str + 1, cs->str, cs->len);    cs->str[0] = c;    cs->len++;    cs->str[cs->len] = '\0';}void c_string_front_int(c_string_t *cs, int val) {    char str[12];    if (cs == NULL) return;    snprintf(str, sizeof(str), "%d", val);    c_string_front_str(cs, str, 0);}void c_string_clear(c_string_t *cs) {    if (cs == NULL) return;    c_string_truncate(cs, 0);}void c_string_truncate(c_string_t *cs, size_t len) {    if (cs == NULL || len >= cs->len) return;    cs->len = len;    cs->str[cs->len] = '\0';}void c_string_drop_begin(c_string_t *cs, size_t len) {    if (cs == NULL || len == 0) return;    if (len >= cs->len) {        c_string_clear(cs);        return;    }    cs->len -= len;    /* +1 to move the NULL. */    memmove(cs->str, cs->str + len, cs->len + 1);}void c_string_drop_end(c_string_t *cs, size_t len) {    if (cs == NULL || len == 0) return;    if (len >= cs->len) {        c_string_clear(cs);        return;    }    cs->len -= len;    cs->str[cs->len] = '\0';}size_t c_string_len(const c_string_t *cs) {    if (cs == NULL) return 0;    return cs->len;}const char *c_string_peek(const c_string_t *cs) {    if (cs == NULL) return NULL;    return cs->str;}char *c_string_dump(const c_string_t *cs, size_t *len) {    char *out;    if (cs == NULL) return NULL;    if (len != NULL) *len = cs->len;    out = malloc(cs->len + 1);    memcpy(out, cs->str, cs->len + 1);    return out;} 往期推荐 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-24 关键词: 嵌入式 C语言

  • 再谈指针:大佬给你拨开C指针的云雾

    作者:Harris Wilde,http://www.techzone.ltd/post/CPointer/ 说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,可以说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。 在讲指针之前,我们先来了解下变量在「内存」中是如何存放的。 在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。 通过变量名来访问变量,是一种「相对安全」的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。 那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节,那么你其实可以把内存比作一个超级大的「字符型数组」。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址。每个地址可以存放「1字节」的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。 那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。 初识指针和指针变量 那么我们现在就来切入正题,指针是个什么东西呢? 所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不一样的是,指针变量存储的是「地址」。 定义指针 指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢: 很简单:类型名* 指针变量名 char* pa;//定义一个字符变量的指针,名称为paint* pb;//定义一个整型变量的指针,名称为pbfloat* pc;//定义一个浮点型变量的指针,名称为pc 注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。 取地址运算符和取值运算符 获取某个变量的地址,使用取地址运算符&,如: char* pa = &a;int* pb = &f; 如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*,如: printf("%c, %d\n", *pa, *pb); 这里你可能发现,定义指针的时候也使用了*,这里属于符号的「重用」,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示「定义一个指针变量」,在其他的时候则用来「获取指针变量指向的变量的值」。 直接通过变量名来访问变量的值称之为直接访问,通过指针这样的形式访问称之为间接访问,因此取值运算符有时候也成为「间接运算符」。 比如: //Example 01//代码来源于网络,非个人原创#include int main(void){    char a = 'f';    int f = 123;    char* pa = &a;    int* pf = &f;        printf("a = %c\n", *pa);    printf("f = %d\n", *pf);        *pa = 'c';    *pf += 1;        printf("now, a = %c\n", *pa);    printf("now, f = %d\n", *pf);        printf("sizeof pa = %d\n", sizeof(pa));    printf("sizeof pf = %d\n", sizeof(pf));        printf("the addr of a is: %p\n", pa);    printf("the addr of f is: %p\n", pf);        return 0;} 程序实现如下: //Consequence 01a = ff = 123now, a = cnow, f = 124sizeof pa = 4sizeof pf = 4the addr of a is: 00EFF97Fthe addr of f is: 00EFF970 避免访问未初始化的指针 void f(){    int* a;    *a = 10;} 像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个「随机值」。但是如果是在指针里面,那么就有可能覆盖到「其他的内存区域」,甚至可能是系统正在使用的「关键区域」,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被「中止」并「报错」。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被「莫名其妙地修改」,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。 指针和数组 有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。 数组的地址 我们刚刚说了,指针实际上就是变量在「内存中的地址」,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢? 我们知道,从标准输入流中读取一个值到变量中,用的是scanf函数,一般貌似在后面都要加上&,这个其实就是我们刚刚说的「取地址运算符」。如果你存储的位置是指针变量的话,那就不需要。 //Example 02int main(void){    int a;    int* p = &a;        printf("请输入一个整数:");    scanf("%d", &a);//此处需要&    printf("a = %d\n", a);        printf("请再输入一个整数:");    scanf("%d", p);//此处不需要&    printf("a = %d\n", a);        return 0;} 程序运行如下: //Consequence 02请输入一个整数:1a = 1请再输入一个整数:2a = 2 在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要&来取地址完成这个任务。而对于指针变量来说,本身就是「另外一个」普通变量的「地址信息」,因此直接给出指针的值就可以了。 试想一下,我们在使用scanf函数的时候,是不是也有不需要使用&的时候?就是在读取「字符串」的时候: //Example 03#include int main(void){    char url[100];    url[99] = '\0';    printf("请输入TechZone的域名:");    scanf("%s", url);//此处也不用&    printf("你输入的域名是:%s\n", url);    return 0;} 程序执行如下: //Consequence 03请输入TechZone的域名:www.techzone.ltd你输入的域名是:www.techzone.ltd 因此很好推理:数组名其实就是一个「地址信息」,实际上就是数组「第一个元素的地址」。咱们试试把第一个元素的地址和数组的地址做个对比就知道了: //Example 03 V2#include int main(void){    char url[100];    printf("请输入TechZone的域名:");    url[99] = '\0';    scanf("%s", url);    printf("你输入的域名是:%s\n", url);    printf("url的地址为:%p\n", url);    printf("url[0]的地址为:%p\n", &url[0]);    if (url == &url[0])    {        printf("两者一致!");    }    else    {        printf("两者不一致!");    }    return 0;} 程序运行结果为: //Comsequense 03 V2请输入TechZone的域名:www.techzone.ltd你输入的域名是:www.techzone.ltdurl的地址为:0063F804url[0]的地址为:0063F804两者一致! 这么看,应该是实锤了。那么数组后面的元素也就是依次往后放置,有兴趣的也可以自己写代码尝试把它们输出看看。 指向数组的指针 刚刚我们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针自然也就有两种定义的方法: ...char* p;//方法1p = a;//方法2p = &a[0]; 指针的运算 当指针指向数组元素的时候,可以对指针变量进行「加减」运算,+n表示指向p指针所指向的元素的「下n个元素」,-n表示指向p指针所指向的元素的「上n个元素」。并不是将地址加1。 如: //Example 04#include int main(void){    int a[] = { 1,2,3,4,5 };    int* p = a;    printf("*p = %d, *(p+1) = %d, *(p+2) = %d\n", *p, *(p + 1), *(p + 2));    printf("*p -> %p, *(p+1) -> %p, *(p+2) -> %p\n", p, p + 1, p + 2);    return 0;} 执行结果如下: //Consequence 04*p = 1, *(p+1) = 2, *(p+2) = 3*p -> 00AFF838, *(p+1) -> 00AFF83C, *(p+2) -> 00AFF840 有的小伙伴可能会想,编译器是怎么知道访问下一个元素而不是地址直接加1呢? 其实就在我们定义指针变量的时候,就已经告诉编译器了。如果我们定义的是整型数组的指针,那么指针加1,实际上就是加上一个sizeof(int)的距离。相对于标准的下标访问,使用指针来间接访问数组元素的方法叫做指针法。 其实使用指针法来访问数组的元素,不一定需要定义一个指向数组的单独的指针变量,因为数组名自身就是指向数组「第一个元素」的指针,因此指针法可以直接作用于数组名: ...printf("p -> %p, p+1 -> %p, p+2 -> %p\n", a, a+1, a+2);printf("a = %d, a+1 = %d, a+2 = %d", *a, *(a+1), *(a+2));... 执行结果如下: p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF840b = 1, b+1 = 2, b+2 = 3 现在你是不是感觉,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然非常像,但是绝对「不是」一种东西。 甚至你还可以直接用指针来定义字符串,然后用下标法来读取每一个元素: //Example 05//代码来源于网络#include #include int main(void){    char* str = "I love TechZone!";    int i, length;        length = strlen(str);        for (i = 0; i 

    时间:2020-10-24 关键词: 嵌入式 C语言

  • 技术贴!常见的C语言内存错误及对策

    一、指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。浅显的例子就不举了,这里举几个比较隐蔽的例子。 1、结构体成员指针未初始化 struct student{   char *name;   int score;}stu,*pstu;int main(){   strcpy(stu.name,"Jimy");   stu.score = 99;   return 0;} 很多初学者犯了这个错误还不知道是怎么回事。这里定义了结构体变量stu,但是他没想到这个结构体内部char *name 这成员在定义结构体变量stu 时,只是给name 这个指针变量本身分配了4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用strcpy 函数时,会将字符串"Jimy"往乱码所指的内存上拷贝,而这块内存name 指针根本就无权访问,导致出错。解决的办法是为name 指针malloc 一块空间。 同样,也有人犯如下错误: int main(){   pstu = (struct student*)malloc(sizeof(struct student));   strcpy(pstu->name,"Jimy");   pstu->score = 99;   free(pstu);   return 0;} 为指针变量pstu 分配了内存,但是同样没有给name 指针分配内存。错误与上面第一种情况一样,解决的办法也一样。这里用了一个malloc 给人一种错觉,以为也给name 指针分配了内存。 2、没有为结构体指针分配足够的内存 int main(){   pstu = (struct student*)malloc(sizeof(struct student*));   strcpy(pstu->name,"Jimy");   pstu->score = 99;   free(pstu);   return 0;} 为pstu 分配内存的时候,分配的内存大小不合适。这里把sizeof(struct student)误写为sizeof(struct student*)。当然name 指针同样没有被分配内存。解决办法同上。 3、函数的入口校验 不管什么时候,我们使用指针之前一定要确保指针是有效的。 一般在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了。比如上面的例子,即使用if(NULL != p)校验也起不了作用,因为name 指针并没有被初始化为NULL,其内部是一个非NULL 的乱码。 assert 是一个宏,而不是函数,包含在assert.h 头文件中。如果其后面括号里的值为假,则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug 版本上起作用,而在Release 版本被编译器完全优化掉,这样就不会影响代码的性能。 有人也许会问,既然在Release 版本被编译器完全优化掉,那Release 版本是不是就完全没有这个参数入口校验了呢?这样的话那不就跟不使用它效果一样吗? 是的,使用assert 宏的地方在Release 版本里面确实没有了这些校验。但是我们要知道,assert 宏只是帮助我们调试代码用的,它的一切作用就是让我们尽可能的在调试函数的时候把错误排除掉,而不是等到Release 之后。它本身并没有除错功能。再有一点就是,参数出现错误并非本函数有问题,而是调用者传过来的实参有问题。assert 宏可以帮助我们定位错误,而不是排除错误。 二、为指针分配的内存太小 为指针分配了内存,但是内存大小不够,导致出现越界错误。 char *p1 = “abcdefg”;char *p2 = (char *)malloc(sizeof(char)*strlen(p1));strcpy(p2,p1); p1 是字符串常量,其长度为7 个字符,但其所占内存大小为8 个byte。初学者往往忘了字符串常量的结束标志“\0”。这样的话将导致p1 字符串中最后一个空字符“\0”没有被拷贝到p2 中。解决的办法是加上这个字符串结束标志符: char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char)); 这里需要注意的是,只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符了: char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’}; 另外,不要因为char 类型大小为1 个byte 就省略sizof(char)这种写法。这样只会使你的代码可移植性下降。 三、内存分配成功,但并未初始化 犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其值自然为0。未初始化指针变量也许看起来不那么严重,但是它确确实实是个非常严重的问题,而且往往出现这种错误很难找到原因。 曾经有一个学生在写一个windows 程序时,想调用字库的某个字体。而调用这个字库需要填充一个结构体。他很自然的定义了一个结构体变量,然后把他想要的字库代码赋值给了相关的变量。但是,问题就来了,不管怎么调试,他所需要的这种字体效果总是不出来。我在检查了他的代码之后,没有发现什么问题,于是单步调试。在观察这个结构体变量的内存时,发现有几个成员的值为乱码。就是其中某一个乱码惹得祸!因为系统会按照这个结构体中的某些特定成员的值去字库中寻找匹配的字体,当这些值与字库中某种字体的某些项匹配时,就调用这种字体。但是很不幸,正是因为这几个乱码,导致没有找到相匹配的字体!因为系统并无法区分什么数据是乱码,什么数据是有效的数据。只要有数据,系统就理所当然的认为它是有效的。 也许这种严重的问题并不多见,但是也绝不能掉以轻心。所以在定义一个变量时,第一件事就是初始化。你可以把它初始化为一个有效的值,比如: int i = 10;char *p = (char *)malloc(sizeof(char)); 但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为0 或NULL。 int i = 0;char *p = NULL; 如果定义的是数组的话,可以这样初始化: int a[10] = {0}; 或者用memset 函数来初始化为0: memset(a,0,sizeof(a)); memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置的值;第三个参数是要被设置的内存大小,单位为byte。这里并不想过多的讨论memset 函数的用法,如果想了解更多,请参考相关资料。 至于指针变量如果未被初始化,会导致if 语句或assert 宏校验失败。这一点,上面已有分析。 四、内存越界 内存分配成功,且已经初始化,但是操作越过了内存的边界。这种错误经常是由于操作数组或指针时出现“多1”或“少1”。比如: int a[10] = {0};for (i=0; i

    时间:2020-10-23 关键词: 嵌入式 C语言

  • C++仿函数你知道怎么做吗?

    【导读】:在我们日常编码中会发现有些功能代码,会不断的在不同的成员函数中用到,但是又不好将这些代码独立成一个成员函数。解决办法之一就是写一个公共的函数,不过函数用到的一些变量,就可能会成为全局变量。再说为了复用这么一段代码,就要单立出一个函数,也不是很好维护。此时就可以用到仿函数了。 以下是正文 引入仿函数(functor)原因 先考虑一个简单的例子:假设有一个vector,你的任务是统计长度小于5的string的个数,如果使用count_if函数的话,你的代码可能长成这样: bool LengthIsLessThanFive(const string& str){    return str.length() < 5;    }int res=count_if(vec.begin(), vec.end(), LengthIsLessThanFive); 其中count_if函数的第三个参数是一个函数指针,返回一个bool类型的值。一般的,如果需要将特定的阈值长度也传入的话,我们可能将函数写成这样: bool LenthIsLessThan(const string& str, int len) {    return str.length() < len;} 这个函数看起来比前面一个版本更具有一般性,但是他不能满足count_if函数的参数要求:count_if要求的是unary function(仅带有一个参数)作为它的最后一个参数。所以问题来了,怎么样找到以上两个函数的一个折中的解决方案呢? 这个问题其实可以归结于一个data flow的问题,要设计这样一个函数,使其能够access这个特定的length值,回顾我们已有的知识,有三种解决方案可以考虑: (1)函数的局部变量: 局部变量不能在函数调用中传递,而且caller无法访问。 (2)函数的参数: 这种方法我们已经讨论过了,多个参数不适用于count_if函数。 (3)全局变量: 我们可以将长度阈值设置成一个全局变量,代码可能像这样: int maxLength;bool LengthIsLessThan(const string& str) { return str.length() < maxLength;}int res=count_if(vec.begiin(), vec.end(), LengthIsLessThan); 这段代码看似很不错,实则不符合规范,更重要的是,它不优雅。原因有以下几点要考虑: (1)容易出错: 为什么这么说呢,我们必须先初始化maxLength的值,才能继续接下来的工作,如果我们忘了,则可能无法得到正确答案。此外,变量maxLength和函数LengthIsLessThan之间是没有必然联系的,编译器无法确定在调用该函数前是否将变量初始化,给码农平添负担。 (2)没有可扩展性: 如果我们每遇到一个类似的问题就新建一个全局变量,尤其是多人合作写代码时,很容易引起命名空间污染(namespace polution)的问题;当范围域内有多个变量时,我们用到的可能不是我们想要的那个。 (3)全局变量的问题: 每当新建一个全局变量,即使是为了coding的便利,我们也要知道我们应该尽可能的少使用全局变量,因为它的cost很高;而且可能暗示你这里有一些待解决的优化方案。 仿函数(functor)介绍 说了这么多,还是要回到我们原始的那个问题,有什么解决方案呢?答案当然就是这篇blog的正题部分:仿函数。 我们的初衷是想设计一个unary function,使其能做binary function的工作,这看起来并不容易,但是仿函数能解决这个问题。 先来看仿函数的通俗定义:仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,举个例子: class Func{ public: void operator() (const string& str) const { cout

    时间:2020-10-23 关键词: 嵌入式 C语言

  • 你知道硬件、软件工程师之间,还有一个固件工程师吗?

    软件跟硬件之间的界限已经越来越模糊了,那么处于这个灰色地带的,就是固件了。这就分成三类工作者。 1、软件工程师一般指做图形界面的程序员,工作内容就是写C++、JAVA、Web等。 2、硬件工程师当然是指玩电路板的,工作内容就是画原理图、PCB等。 3、固件工程师也叫单片机工程师,既写代码(主要是C语言、汇编)又要画电路图。 玩单片机的人,可能会有个疑问,为什么我写的C语言能操作到底层的硬件?其实在《计算机组成原理》已经有很详细的介绍了。 我这里粗略地介绍一下,这个原理。 首先你可以搜索一下“从零开始造电脑”,这位叫Steve的大神,就告诉你,用晶体管可以做出CPU(单片机也是CPU)。 当然,我们现在可不会落后到需要到晶体管来制造电脑。 接下来,你可以看一部叫《乔布斯》的电影,剧中就给你展示苹果公司的第一台计算机。 嘿嘿,看到那些黑色的芯片没有?还有两个大大的变压器。这说明了在大学玩单片机的时代,就相当于回到苹果公司的初始时期!是不是很激动人心? 其实你可以用74系列的逻辑IC、单片机等,来搭建一个属于自己的计算机。这就是说人们把若干个晶体管集成为一块74系列的IC,如果集成度更高呢?那就是手机或者台式机用的多核CPU了。 好,介绍了这些古董之后,就让你有个认识,计算机本质上是N个晶体管的组合,也是数字逻辑芯片的组合,更高级的,就是一块数模混合的芯片,具体形式是由你的工艺决定的。现在回到正题,介绍一下数电的基础知识。 因为CPU主要功能是计算,也就是可以直接运用数学知识来解决问题,这里就举个例子介绍一下,CPU如何计算加法,也就是用数电里的门电路搭一个加法器。 怎样用晶体管搭这些与、或、非门就不说了,不懂的,可以翻书。上图就告诉你,可以用这些门电路搭一个加法器。 怎样输入Ai=0,Bi=1,Ci=0?用74系列的IC的话,可以直接把Ai,Ci接GND,Bi接VCC,就实现加法了。而在CPU内部也是一样可以这样做的,但是CPU可没那么死板,只算常数的加法。 上图中,蓝色箭头指向的1,就是接VCC的,而红色箭头,就是接GND。 在CPU内部,还有ROM,它可以把你要计算的加数和被加数存进去(ROM输出的高低电平,跟你接GND和VCC是一样的效果),而结果则存在寄存器(先暂存,以备后面使用)。 现在有个问题,如果加完之后还要计算乘法(在信号处理领域的卷积运算的核心单元就是乘加器),怎么办?谁来自动完成这个动作?幸好,CPU里面有个叫ALU(算术逻辑单元)来处理这件事情。 这里的控制单元,就把ROM里面的数据取出来,再用选择器,来调用加法器和乘法器,最终把结果存到寄存器中。 如果ROM里面只存数据,那是无法让控制单元知道,你要执行加法还是乘法,要解决这个问题,就需要在ROM里面再划分一个区域,存放指令码。 这个指令码,跟数据是一样,都是0、1的二进制数,只是用途不同,所以起了不同的名字。 其实这个指令码,对应在单片机里面的汇编语言,就是操作码(如:MOV);而操作数就是数据(如:01H)。具体的,可以看看单片机的教材。 根据指令码的设计方法来分,有四种,分别是CISC、RISC、VLIW、TTA,具体区别可以看计算机组成原理。 而PC(程序计数器)就是控制ROM的地址,现在你要知道PC是不能出错的,一旦出错,就意味着单片机不按照你的代码来工作。 现在,我在8位的CPU的ROM里面,第一个地址存了0x03这个指令码来代表加法,而在第二、三个地址存了加数和被加数,然后在第四个地址存了0x05代表乘法,在第五、六个地址存了乘数和被乘数。那么,按照一定的规则来设计控制单元(这个规则可以自己定义的),它就知道0x03是要执行加法。 那么这个规则如何设计?最简单的,就是用与门了,然后输出一个使能信号,让加法器工作,就跟上面的74LS160差不多。 但是CPU可没那么简陋,它可以使用状态机、流水线等,来控制这些基本单元(如:加法器、乘法器),如下图所示。 说到这里,你至少应该知道,我们只要改变ROM的内容,就可以操作CPU内部的ALU,从而操作CPU的各个硬件单元了。 下面给个相对完整一点的ALU内部结构图。 ROM的内容本质上是一些电荷量(电容上有、无电荷,代表二进制的1和0),也就是固件、软件工程师写的代码。而硬件,就是由晶体管搭建的数字、模拟电路(如:单片机内部的比较器、ADC等)。所以硬件是物理器件,不容易更改;而ROM的内容完全可以用烧录器就轻松改变它,修改成本非常低,而且很灵活。 在这里,你很难表述,这些电荷量是软件还是硬件,但是CPU的这种结构,导致了两种不同类型的工作者,我们称他们为软件工程师和硬件工程师。而单片机程序员写的代码,跟硬件密切相关,而且一旦完成之后,很少需要修改的(不像软件工程师修改的那么频繁),我们称之为固件。 本文系网络转载,版权归原作者所有。如有问题,请联系我们,谢谢! 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-23 关键词: 嵌入式 C语言

  • 精品干货:C语言的高效编程与代码优化

    在本篇文章中,我收集了很多经验和方法。应用这些经验和方法,可以帮助我们从执行速度和内存使用等方面来优化C语言代码。 简介 在最近的一个项目中,我们需要开发一个运行在移动设备上但不保证图像高质量的轻量级JPEG库。期间,我总结了一些让程序运行更快的方法。 在本篇文章中,我收集了一些经验和方法。应用这些经验和方法,可以帮助我们从执行速度和内存使用等方面来优化C语言代码。 尽管在C代码优化方面有很多的指南,但是关于编译和你使用的编程机器方面的优化知识却很少。 通常,为了让你的程序运行的更快,程序的代码量可能需要增加。代码量的增加又可能会对程序的复杂度和可读性带来不利的影响。 这对于在手机、PDA等对于内存使用有很多限制的小型设备上编写程序时是不被允许的。因此,在代码优化时,我们的座右铭应该是确保内存使用和执行速度两方面都得到优化。 声明 实际上,在我的项目中,我使用了很多优化ARM编程的方法(该项目是基于ARM平台的),也使用了很多互联网上面的方法。但并不是所有文章提到的方法都能起到很好的作用。 所以,我对有用的和高效的方法进行了总结收集。同时,我还修改了其中的一些方法,使他们适用于所有的编程环境,而不是局限于ARM环境。 哪里需要使用这些方法? 没有这一点,所有的讨论都无从谈起。程序优化最重要的就是找出待优化的地方,也就是找出程序的哪些部分或者哪些模块运行缓慢亦或消耗大量的内存。只有程序的各部分经过了优化,程序才能执行的更快。 程序中运行最多的部分,特别是那些被程序内部循环重复调用的方法最该被优化。 对于一个有经验的码农,发现程序中最需要被优化的部分往往很简单。此外,还有很多工具可以帮助我们找出需要优化的部分。我使用过Visual C++内置的性能工具profiler来找出程序中消耗最多内存的地方。 另一个我使用过的工具是英特尔的Vtune,它也能很好的检测出程序中运行最慢的部分。根据我的经验,内部或嵌套循环,调用第三方库的方法通常是导致程序运行缓慢的最主要的起因。 整形数 如果我们确定整数非负,就应该使用unsigned int而不是int。有些处理器处理无符号unsigned 整形数的效率远远高于有符号signed整形数(这是一种很好的做法,也有利于代码具体类型的自解释)。 因此,在一个紧密循环中,声明一个int整形变量的最好方法是: register unsigned int variable_name; 记住,整形in的运算速度高浮点型float,并且可以被处理器直接完成运算,而不需要借助于FPU(浮点运算单元)或者浮点型运算库。 尽管这不保证编译器一定会使用到寄存器存储变量,也不能保证处理器处理能更高效处理unsigned整型,但这对于所有的编译器是通用的。 例如在一个计算包中,如果需要结果精确到小数点后两位,我们可以将其乘以100,然后尽可能晚的把它转换为浮点型数字。 除法和取余数 在标准处理器中,对于分子和分母,一个32位的除法需要使用20至140次循环操作。除法函数消耗的时间包括一个常量时间加上每一位除法消耗的时间。 Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)     = C0 + C1 * (log2 (numerator) - log2 (denominator)). 对于ARM处理器,这个版本需要20+4.3N次循环。这是一个消耗很大的操作,应该尽可能的避免执行。有时,可以通过乘法表达式来替代除法。 例如,假如我们知道b是正数并且bc是个整数,那么(a/b)>c可以改写为a>(cb)。如果确定操作数是无符号unsigned的,使用无符号unsigned除法更好一些,因为它比有符号signed除法效率高。 合并除法和取余数 在一些场景中,同时需要除法(x/y)和取余数(x%y)操作。这种情况下,编译器可以通过调用一次除法操作返回除法的结果和余数。如果既需要除法的结果又需要余数,我们可以将它们写在一起,如下所示: int func_div_and_mod (int a, int b) {             return (a / b) + (a % b);    } 通过2的幂次进行除法和取余数 如果除法中的除数是2的幂次,我们可以更好的优化除法。编译器使用移位操作来执行除法。因此,我们需要尽可能的设置除数为2的幂次(例如64而不是66)。并且依然记住,无符号unsigned整数除法执行效率高于有符号signed整形出发。 typedef unsigned int uint;uint div32u (uint a) {     return a / 32;}int div32s (int a){    return a / 32;} 上面两种除法都避免直接调用除法函数,并且无符号unsigned的除法使用更少的计算机指令。由于需要移位到0和负数,有符号signed的除法需要更多的时间执行。 取模的一种替代方法 我们使用取余数操作符来提供算数取模。但有时可以结合使用if语句进行取模操作。考虑如下两个例子: uint modulo_func1 (uint count){    return (++count % 60);}uint modulo_func2 (uint count){    if (++count >= 60)        count = 0;    return (count);} 优先使用if语句,而不是取余数运算符,因为if语句的执行速度更快。这里注意新版本函数只有在我们知道输入的count结余0至59时在能正确的工作。 使用数组下标 如果你想给一个变量设置一个代表某种意思的字符值,你可能会这样做: switch ( queue ) {    case 0 :   letter = 'W';           break;    case 1 :   letter = 'S';           break;    case 2 :   letter = 'U';           break;} 或者这样做: if ( queue == 0 )      letter = 'W';else if ( queue == 1 )      letter = 'S';else  letter = 'U'; 一种更简洁、更快的方法是使用数组下标获取字符数组的值。如下: static char *classes="WSU"; letter = classes[queue]; 全局变量 全局变量绝不会位于寄存器中。使用指针或者函数调用,可以直接修改全局变量的值。因此,编译器不能将全局变量的值缓存在寄存器中,但这在使用全局变量时便需要额外的(常常是不必要的)读取和存储。所以,在重要的循环中我们不建议使用全局变量。 如果函数过多的使用全局变量,比较好的做法是拷贝全局变量的值到局部变量,这样它才可以存放在寄存器。这种方法仅仅适用于全局变量不会被我们调用的任意函数使用。例子如下: int f(void);int g(void);int errs;void test1(void){      errs += f();      errs += g();} void test2(void){      int localerrs = errs;      localerrs += f();      localerrs += g();      errs = localerrs;} 注意,test1必须在每次增加操作时加载并存储全局变量errs的值,而test2存储localerrs于寄存器并且只需要一个计算机指令。 使用别名 考虑如下的例子: void func1( int *data ){        int i;         for(i=0; ix = 0;   p->pos->y = 0;   p->pos->z = 0;} 然而,这种的代码在每次操作时必须重复调用p->pos,因为编译器不知道p->pos->x与p->pos是相同的。一种更好的方法是缓存p->pos到一个局部变量: void InitPos2(Object *p){   Point3 *pos = p->pos;   pos->x = 0;   pos->y = 0;   pos->z = 0;} 另一种方法是在Object结构中直接包含Point3类型的数据,这能完全消除对Point3使用指针操作。 条件执行 条件执行语句大多在if语句中使用,也在使用关系运算符(等)或者布尔值表达式(&&,!等)计算复杂表达式时使用。对于包含函数调用的代码片段,由于函数返回值会被销毁,因此条件执行是无效的。 因此,保持if和else语句尽可能简单是十分有益处的,因为这样编译器可以集中处理它们。关系表达式应该写在一起。 下面的例子展示编译器如何使用条件执行: int g(int a, int b, int c, int d){   if (a > 0 && b > 0 && c xmin && p.x xmax &&                      p.y >= r->ymin && p.y ymax);} 这里有一种更快的方法:x>min && xymin) ymax); } 布尔表达式和零值比较 处理器的标志位在比较指令操作后被设置。标志位同样可以被诸如MOV、ADD、AND、MUL等基本算术和裸机指令改写。如果数据指令设置了标志位,N和Z标志位也将与结果与0比较一样进行设置。N标志表示结果是否是负值,Z标志表示结果是否是0。 C语言中,处理器中的N和Z标志位与下面的指令联系在一起:有符号关系运算x=0,x==0,x!=0;无符号关系运算x==0,x!=0(或者x>0)。 C代码中每次关系运算符的调用,编译器都会发出一个比较指令。如果操作符是上面提到的,编译器便会优化掉比较指令。例如: int aFunction(int x, int y){   if (x + y 

    时间:2020-10-23 关键词: 嵌入式 C语言

  • 嵌入式技术中C语言的诞生及其C++40年的相爱相杀

    嵌入式技术中C语言的诞生及其C++40年的相爱相杀

    70年代初,贝尔实验室创建了C语言,它是开发UNIX的副产品。很快C就成为了最受欢迎的编程语言之一。但是对于BjarneStroustrup来说,C的表达能力还不够。于是,他在1983年的博士论文中扩展了C语言。 于是,支持类的C语言诞生了。 当时,BjarneStroustrup明白编程语言有许多组成部分,除了语言本身,还有编译器、链接器和各种库。提供熟悉的工具有助于语言被广泛接受。在这种历史背景下,在C++语言的基础上开发C++也是有道理的。 40年后,C和C++都在行业中得到了广泛使用。但是,互联网上的C开发人员认为C++是有史以来最糟糕的人类发明,而许多C++开发人员则希望有朝一日C语言灰飞烟灭。 1、究竟发生了什么事? 从表面上看,C和C++都可以满足相同的用例:高性能、确定性、原生但可移植的代码,可用于最广泛的硬件和应用程序。 但是,更让C自豪的是它是一门低级语言,更接近汇编。 而C++,从诞生第一天开始就充斥了各种奇怪的东西。例如析构函数这个黑魔法。自作主张的编译器。尽管很早C++就有了类型推断功能,但是80年代中期的开发人员还无法接受这个概念,因此BjarneStroustrup不得不删除了auto,直到C++11又重新添加回来。 从那以后,C++就不断加入各种工具来实现抽象。很难说C++是一种低级语言还是高级语言。从设计目的上来说,C++两者都是。但是在不牺牲性能的情况下,建立高级抽象是很困难的。于是C++引入了各种工具来实现constexpr、move语义、模板和不断增长的标准库。 从根本上讲,我认为C信任开发人员,而C++信任编译器。这是一个巨大的差异,单凭“两者的原生类型相同”、“while循环的语法相同”等简单一致是无法掩盖的。 C++开发人员将有这些问题归咎于C,而C开发人员则认为C++过于疯狂。我觉得站在C的角度看C++,这种说法也很正确。作为C的超集,C++确实很疯狂。一个经验丰富的C开发人员面对C++可能没有熟悉的感觉。C++不是C,这就足以引发互联网上的激烈争论。 然而,虽然我不喜欢C,但也没有权利取笑C。尽管我有一定的C++经验,但用C编写过的代码少之又少,而且肯定是很糟糕的代码。好的编程语言包括良好的实践、模式、惯用写法,这些都需要多年的学习。如果你尝试用编写C++的方式写C的代码,或者用C的方式编写C++的代码,那感觉一定很糟糕。即便你懂C,也不一定会C++,反之亦然,懂C++也不一定会用C编程。 那么,我们是否应该停止说C/C++,为这两个不幸的命名而感到悲哀吗?也不至于。 尽管C++的设计理念与C不一样,但是C++仍然是C的超集。也就是说,你可以在C++转换单元中包含C的头文件,这样依然可以通过编译。而这正是造成混乱的地方。 C++不是C的扩展,它是由不同的委员会、不同的人独立设计的标准。从逻辑上讲,喜欢C++理念的人会参与C++社区以及C++标准化的过程,而其他人可能会尝试参与C。无论是C的委员会还是C++委员会,他们表达意图和方向的方式只能通过各自的最终产品:标准;而标准是众多投票的成果。 然而,编译器很难知道它正在处理的是C头文件还是C++头文件。 extern“C”标记并没有得到广泛一致的使用,而且它只能影响修饰,而不会影响语法或语义。头文件仅对预处理器有影响,对于C++编译器而言,所有内容都是C++转换单元,因此也就是C++。然而,人们依然会在C++中包含C头文件,并期望它“正常工作”,而大多数时候也确实可以正常工作。 那么,我们不禁想问: 2、由不同地方的、不同的人开发的C++代码如何保持C的兼容性? 恐怕很难。 最近,一位同事让我想起了康威定律: "设计系统的架构受制于产生这些设计的组织的沟通结构。" 根据这个逻辑,如果两个委员不互相合作,则他们创造的语言也不会互通。 C++维护了一个与C及其标准库的不兼容列表。然而该列表似乎并未反映出许多C11和C18中添加、但在C++中不合法的功能。更清晰的介绍请参见这个维基本科页面(https://en.wikipedia.org/wiki/CompaTIbility_of_C_and_C%2B%2B)。 然而,仅仅列出两种语言之间的不兼容性,并不足以衡量二者的不兼容性。 那些存在于C++标准库中但主要声明来自C的函数,很难声明成constexpr,更难声明成noexcept。C的兼容性会导致性能成本,而C函数是优化的障碍。 许多C的结构在C++中都是有效的,但无法通过代码审查(如NULL、longjmp、malloc、构造/析构函数、free、C风格的类型强制转换等)。 在C看来,这些惯用写法可能问题不大,但在C++中可不行。C++具有更强大的类型系统,不幸的是,C的惯用写法在这个类型系统中凿了一个洞,因此实现C的兼容性需要在安全性方面付出代价。 别误会,C++仍然关心C的兼容性,某种程度上。然而,有趣的是C也很关心C++,某种程度上。实话实说,C对C++的关心程度可能高于C++对C的关心。看来,每个委员会还是在乎另一个委员会的工作。但我们很不情愿。 C++知道,许多基础库都是用C编写的,不仅包括libc,而且还有zip、png、curl、openssl(!)以及许多其他库,无数的C++项目都在使用这些库。C++不能破坏这些兼容性。 但是最近,尤其是在过去的十年中,C++的规模已远远超过C。C++拥有更多的用户,并且社区更加活跃。也许这就是为什么如今C++委员会的规模是C委员会的10倍以上。 C++是不可忽视的力量,因此C委员会必须考虑不破坏C++兼容性。如果非要说一个标准追随另一个标准对话,那么如今C++是领头者,而C是追随者。 现在,C++处于稳定的三年周期中,无论是风雨还是烈日,抑或是致命的新疫情。而C每十年左右才发布一次主版本。不过这也很合理,因为作为一种较低级的语言,C不需要发展得那么快。 C语言的环境也与C++完全不同。C多用于平台,更多地用于编译器。每个人(甚至他们的狗狗)都会编写C编译器,因为该语言的特性集很小,所以任何人都可以编写C编译器。而C++委员会真正考虑的实现只有四种,而且在每次会议上这四种实现都会出现。所以,C语言中的许多功能都是与实现有关的,或者是可选支持的,这样各种编译器不需要做太多努力就可以声称自己遵从了标准,据说这样委员会的人会比较高兴。 如今,C++更加侧重于可移植性,而不是实现的自由。这又是一个理念的不同。 3、因此,你的提议破坏了C的兼容性 我提议的P2178的一部分理论上会影响与C的兼容性。这样的话所有方案都不会令人满意。 有人可能会说,你可以先向C委员会提议你的新特性。这意味着需要召开更多会议。C会议的严格出席规则可能导致你无法参加会议,这就将那些不愿意花上数千美元成为ISO会员的个人拒之门外。这是因为C委员会必须遵守ISO的规则。 而且,如果新的标准刚刚发布,那么可能还需要等待十年时间,你的提案才会被考虑。最重要的是,如果C委员不理解或不在乎你正在努力解决的问题,那么你的提案就石沉大海了。或者他们可能没有精力来处理这个问题。而且,可能你也没有精力来处理C。毕竟,你的本意是要改进C++。实际上,哪怕会议上无人反对你的提议(尽管不太可能发生),如果有人让你先去跟C委员会的人讨论,就等于给你的提议判了死刑。 另一种可能的情况是,C委员会接受与C++中存在的版本略有不同的版本。true只能做一个宏来实现。char16_t需要通过typedef。char32_t不一定是UTF-32。staTIc_assert对应的是_StaTIc_assert。 这类的情况还有很多,我们应该责备C吗?可能不应该。他们的委员会只是在尽力将C语言做好。反之亦然。在C++20中,指定的初始化器就受到了C的启发,但采取了略微不同的规则,因为如果完全一样的话就不符合C++的初始化规则。 对于这个问题,我也有责任。C有VLA。如果当时我在,我一定会反对在标准C++中采用它,因为它导致了太多安全性问题。我也会坚决反对将_Generic添加到C++中的提议。也许_Generic的目的是减少由于缺乏模板或缺乏重载而导致的问题,但是C++有这两个功能,从我的角度来看,_Generic并不适合我想象中的C++。 这两个委员会似乎对于对方语言的关心程度也不一样。有时我们会遇到兼容性非常好的情况(std::complex),有时完全不在乎兼容性(静态数组参数)。 这没有办法。别忘了每个委员会都是一群人,他们在不同的时间、不同的地点投票,而试图控制结果会导致投票毫无意义。将这些人放在同一个房间也不现实。ISO可能会反对,参与者的不平衡会导致C的人处于极大的劣势。 4、C的兼容性不重要 如果你是C开发人员,那么肯定会把C视为一种简洁的编程语言。但对于我们其他人而言,C的印象完全不同。 C是通用的、跨语言的胶水,可以将一切紧密地结合在一起。 对于C++用户而言,C就是他们的API。从这一点来看,C的价值在于其简单性。请记住,C++关心的那一部分C是出现在接口(头文件)中的C。我们关心的是声明,而不是定义。C++需要调用C库中的函数(Python、Fortran、Rust、D、Java等语言也一样,在所有情况下都可以在接口边界使用C)。 因此,C是一种接口定义语言。向C添加的内容越多,定义接口就越困难。这些接口随着时间的推移保持稳定的可能性较小。 那么,C++中缺少是否重要?可能并不重要,因为这不太可能出现在公共接口中。 5、如今大家都在谈论C 过去,C的兼容性是C++的一大卖点。但如今,每个人(甚至他们的金鱼)都懂C。Rust可以调用C函数,Python、Java、一切语言都可以!甚至怪异的Javascript都可以在WebAssemby中调用C函数。 但是在这些语言中,接口是显式的。该语言提供的工具可以公开特定的C声明。当然,这比较麻烦。但这可以让接口非常非常清晰。而且还是有界的。例如,在rust中,调用C函数并不会迫使Rust牺牲某些设计来容纳C子集。实际上C是被包含进去的。 modconfinment{usestd::os::raw::{c_char};extern"C"{pubfnputs(txt:*constc_char);}}pubfnmain(){unsafe{confinment::puts(std::ffi::CString::new("Hello,world!").expect("failed!").as_ptr());}} 6、编译器资源管理器 除非C的ABI发生变化,否则这段代码可以一直正常运行。而且Rust/C的边界非常清晰、不言自明。 因此,C++可能是为C兼容性付出最多的语言。 更糟糕的是,打开任何C的头文件,你很快就会发现一堆#ifdef__cplusplus。没错,C++的兼容性往往需要大量C开发人员的工作。兼容性一直是海市蜃楼。很多人都知道我的这条推文: 7、我们该何去何从? 我认为两个委员会都在尝试更多地沟通。他们计划明年在波特兰召开会议(尽管这个计划可能会变)。沟通是一件好事。 但是鸡同鸭讲的沟通效果会非常有限。两种语言的设计支柱可能都不协调。我会努力建议提供一个模板。但是首先我得吐槽C语言没有模块、没有命名空间,以及整个宏是什么玩意儿。 也许可以将C++能接受的C子集约束在C99上?也许两种语言都需要找到一个共同的子集并独立地发展?也许externC需要影响解析。如果C++经历了多个时代,那么C可能是其中之一。 也许我们需要接受将C作为C++的子集,但唯一的方法是将WG14融入到WG21中。 现状可能不会改变。C++可能永远也无法从自己的起源中解脱,而C可能永远都要与那些顶着C语言之名的肮脏特性战斗。

    时间:2020-10-23 关键词: 嵌入式技术 C语言

  • 10个冷门却实用的Docker使用技巧

    在平时的工作中,docker 接触得很多,除了经常使用的 docker run ,docker stop 等命令,docker 还有很多十分有用但是却不经常使用的命令,下面就来总结一下: 1. docker top 这个命令是用来查看一个容器里面的进程信息的,比如你想查看一个 nginx 容器里面有几个 nginx 进程的时候,就可以这么做: docker top 3b307a09d20dUID      PID    PPID    C    STIME  TTY    TIME       CMDroot     805    787     0    Jul13   ?   00:00:00  nginx: master process nginx -g daemon off;systemd+ 941     805     0   Jul13    ?   00:03:18  nginx: worker process 2. docker load && docker save 我一般使用这两个命令去下载打包 Kubernetes 的镜像,因为你知道的国内的网速并不像国外那么快。 docker save 可以把一个镜像保存到 tar 文件中,你可以这么做: ~ docker save registry:2.7.1 >registry-2.7.1.tar#同时 docker load 可以把镜像从 tar 文件导入到 docker 中~ docker load 

    时间:2020-10-21 关键词: 嵌入式 C语言

  • 纯干货:15000字语法手册,再也不担心SQL写不好了

       基础 1、创建数据库 CREATE DATABASE database-name 2、删除数据库 drop database dbname 2、删除数据库 drop database dbname 3、备份sql server --- 创建 备份数据的 device USE master EXEC sp_addumpdevice 'disk', 'testBack', 'c:\mssql7backup\MyNwind_1.dat'- --- 开始 备份 BACKUP DATABASE pubs TO testBack  4、创建新表 create table tabname(col1 type1 [not null] [primary key],col2 type2 [not null],..)   #根据已有的表创建新表:    A:create table tab_new like tab_old (使用旧表创建新表)   B:create table tab_new as select col1,col2… from tab_old definition only 5、删除新表 drop table tabname  6、增加一个列 Alter table tabname add column col type 注:列增加后将不能删除。DB2中列加上后数据类型也不能改变,唯一能改变的是增加varchar类型的长度。 7、主键 添加主键:Alter table tabname add primary key(col)  删除主键:Alter table tabname drop primary key(col)  8、索引 创建索引:create [unique] index idxname on tabname(col….)  删除索引:drop index idxname 注:索引是不可更改的,想更改必须删除重新建。 9、说明: 创建视图:create view viewname as select statement 删除视图:drop view viewname 10、几个简单的基本的sql语句 选择:select * from table1 where 范围 插入:insert into table1(field1,field2) values(value1,value2) 删除:delete from table1 where 范围 更新:update table1 set field1=value1 where 范围 查找:select * from table1 where field1 like ’%value1%’ ---like的语法很精妙,查资料! 排序:select * from table1 order by field1,field2 [desc] 总数:select count as totalcount from table1 求和:select sum(field1) as sumvalue from table1 平均:select avg(field1) as avgvalue from table1 最大:select max(field1) as maxvalue from table1 最小:select min(field1) as minvalue from table1 11、几个高级查询运算词 ▪ A:UNION 运算符 UNION 运算符通过组合其他两个结果表(例如 TABLE1 和 TABLE2)并消去表中任何重复行而派生出一个结果表。当 ALL 随 UNION 一起使用时(即 UNION ALL),不消除重复行。两种情况下,派生表的每一行不是来自 TABLE1 就是来自 TABLE2。 ▪ B:EXCEPT 运算符 EXCEPT运算符通过包括所有在 TABLE1 中但不在 TABLE2 中的行并消除所有重复行而派生出一个结果表。当 ALL 随 EXCEPT 一起使用时 (EXCEPT ALL),不消除重复行。 ▪ C:INTERSECT 运算符 INTERSECT运算符通过只包括 TABLE1 和 TABLE2 中都有的行并消除所有重复行而派生出一个结果表。当 ALL随 INTERSECT 一起使用时 (INTERSECT ALL),不消除重复行。 注:使用运算词的几个查询结果行必须是一致的。 12、使用外连接 A:left (outer) join:    #左外连接(左连接):结果集几包括连接表的匹配行,也包括左连接表的所有行。    SQL: select a.a, a.b, a.c, b.c, b.d, b.f from a LEFT OUT JOIN b ON a.a = b.c   B:right (outer) join:    #右外连接(右连接):结果集既包括连接表的匹配连接行,也包括右连接表的所有行。    C:full/cross (outer) join:    #全外连接:不仅包括符号连接表的匹配行,还包括两个连接表中的所有记录。 13、对数据库进行操作 分离数据库:sp_detach_db; 附加数据库:sp_attach_db 后接表明,附加需要完整的路径名 14、如何修改数据库的名称 sp_renamedb 'old_name', 'new_name' 02  提升 1、复制表(只复制结构,源表名:a 新表名:b) (Access可用) 法一:select * into b from a where 11(仅用于SQlServer) 法二:select top 0 * into b from a 2、拷贝表(拷贝数据,源表名:a 目标表名:b) (Access可用) insert into b(a, b, c) select d,e,f from b; 3、跨数据库之间表的拷贝(具体数据使用绝对路径) (Access可用) insert into b(a, b, c) select d,e,f from b in ‘具体数据库’ where 条件 例子:..from b in '"&Server.MapPath(".")&"\data.mdb" &"' where.. 4、子查询(表名1:a 表名2:b) select a,b,c from a where a IN (select d from b ) 或者: select a,b,c from a where a IN (1,2,3) 5、显示文章、提交人和最后回复时间 select a.title,a.username,b.adddate from table a,(select max(adddate) adddate from table where table.title=a.title) b 6、外连接查询(表名1:a 表名2:b) select a.a, a.b, a.c, b.c, b.d, b.f from a LEFT OUT JOIN b ON a.a = b.c 7、在线视图查询(表名1:a ) select * from (SELECT a,b,c FROM a) T where t.a > 1; 8、between的用法,between限制查询数据范围时包括了边界值,not between不包括 select * from table1 where time between time1 and time2 select a,b,c, from table1 where a not between 数值1 and 数值2 9、in 的使用方法 select * from table1 where a [not] in (‘值1’,’值2’,’值4’,’值6’) 10、两张关联表,删除主表中已经在副表中没有的信息 delete from table1 where not exists ( select * from table2 where table1.field1=table2.field1 ) 11、四表联查问题 select * from a left inner join b on a.a=b.b right inner join c on a.a=c.c inner join d on a.a=d.d where ..... 12、日程安排提前五分钟提醒 SQL: select * from 日程安排 where datediff('minute',f开始时间,getdate())>5 13、一条sql 语句搞定数据库分页 select top 10 b.* from (select top 20 主键字段,排序字段 from 表名 order by 排序字段 desc) a,表名 b where b.主键字段 = a.主键字段 order by a.排序字段 具体实现:关于数据库分页: declare @start int,@end int   @sql  nvarchar(600) set @sql=’select top’+str(@end-@start+1)+’+from T where rid not in(select top’+str(@str-1)+’Rid from T where Rid>-1)’ exec sp_executesql @sql#注意:在top后不能直接跟一个变量,所以在实际应用中只有这样的进行特殊的处理。Rid为一个标识列,如果top后还有具体的字段,这样做是非常有好处的。因为这样可以避免 top的字段如果是逻辑索引的,查询的结果后实际表中的不一致(逻辑索引中的数据有可能和数据表中的不一致,而查询时如果处在索引则首先查询索引) 14、前10条记录 select top 10 * form table1 where 范围 15、选择在每一组b值相同的数据中对应的a最大的记录的所有信息(类似这样的用法可以用于论坛每月排行榜,每月热销产品分析,按科目成绩排名,等等.) select a,b,c from tablename ta where a=(select max(a) from tablename tb where tb.b=ta.b) 16、包括所有在 TableA中但不在 TableB和TableC中的行并消除所有重复行而派生出一个结果表 (select a from tableA ) except (select a from tableB) except (select a from tableC) 03  技巧 1、1=1,1=2的使用,在SQL语句组合时用的较多 “where 1=1” 是表示选择全部    “where 1=2”全部不选,   如:   if @strWhere !=''    begin   set @strSQL = 'select count(*) as Total from [' + @tblName + '] where ' + @strWhere   end   else    begin   set @strSQL = 'select count(*) as Total from [' + @tblName + ']'    end 我们可以直接写成 set @strSQL = 'select count(*) as Total from [' + @tblName + '] where 1=1 安定 '+ @strWhere  2、收缩数据库 --重建索引  DBCC REINDEX  DBCC INDEXDEFRAG  --收缩数据和日志  DBCC SHRINKDB  DBCC SHRINKFILE 3、压缩数据库 dbcc shrinkdatabase(dbname) 4、转移数据库给新用户以已存在用户权限 exec  sp_change_users_login 'update_one','newname','oldname' go 5、检查备份集 RESTORE VERIFYONLY from disk='E:\dvbbs.bak' 6、修复数据库 ALTER DATABASE [dvbbs] SET SINGLE_USER   GO   DBCC CHECKDB('dvbbs',repair_allow_data_loss) WITH TABLOCK   GO   ALTER DATABASE [dvbbs] SET MULTI_USER   GO 7、日志清除 SET NOCOUNT ON DECLARE @LogicalFileName sysname,  @MaxMinutes INT,  @NewSize IN USE tablename -- 要操作的数据库名SELECT  @LogicalFileName = 'tablename\_log', -- 日志文件名@MaxMinutes = 10, -- Limit on time allowed to wrap log. @NewSize = 1  -- 你想设定的日志文件的大小(M) Setup / initializeDECLARE @OriginalSize int SELECT @OriginalSize = size FROM sysfiles WHERE name = @LogicalFileNameSELECT 'Original Size of ' + db\_name() + ' LOG is ' + CONVERT(VARCHAR(30),@OriginalSize) + ' 8K pages or ' + CONVERT(VARCHAR(30),(@OriginalSize\*8/1024)) + 'MB' FROM sysfiles WHERE name = @LogicalFileNameCREATE TABLE DummyTrans (DummyColumn char (8000) not null) DECLARE @Counter    INT, @StartTime DATETIME, @TruncLog   VARCHAR(255)SELECT @StartTime = GETDATE(), @TruncLog = 'BACKUP LOG ' + db\_name() + ' WITH TRUNCATE\_ONLY' DBCC SHRINKFILE (@LogicalFileName, @NewSize)EXEC (@TruncLog)-- Wrap the log if necessary.WHILE @MaxMinutes > DATEDIFF (mi, @StartTime, GETDATE()) -- time has not expired AND @OriginalSize = (SELECT size FROM sysfiles WHERE name = @LogicalFileName) AND (@OriginalSize * 8 /1024) > @NewSize BEGIN -- Outer loop.SELECT @Counter = 0 WHILE   ((@Counter [配置发布、订阅服务器和分发的属性]->[订阅服务器] 中添加 否则在订阅服务器上请求订阅时会出现的提示:改发布不允许匿名订阅 如果仍然需要匿名订阅则用以下解决办法 [企业管理器]->[复制]->[发布内容]->[属性]->[订阅选项] 选择允许匿名请求订阅 2)如果选择匿名订阅,则配置订阅服务器时不会出现以上提示 (10)[下一步] 设置快照 代理程序调度 (11)[下一步] 完成配置 当完成出版物的创建后创建出版物的数据库也就变成了一个共享数据库 有数据 srv1.库名..author有字段:id,name,phone, srv2.库名..author有字段:id,name,telphone,adress 要求: srv1.库名..author增加记录则srv1.库名..author记录增加 srv1.库名..author的phone字段更新,则srv1.库名..author对应字段telphone更新 --*/ --大致的处理步骤 --1.在 srv1 上创建连接服务器,以便在 srv1 中操作 srv2,实现同步 exec sp_addlinkedserver 'srv2','','SQLOLEDB','srv2的sql实例名或ip' exec sp_addlinkedsrvlogin 'srv2','false',null,'用户名','密码' go --2.在 srv1 和 srv2 这两台电脑中,启动 msdtc(分布式事务处理服务),并且设置为自动启动 。我的电脑--控制面板--管理工具--服务--右键 Distributed Transaction Coordinator--属性--启动--并将启动类型设置为自动启动 go --然后创建一个作业定时调用上面的同步处理存储过程就行了 企业管理器 --管理 --SQL Server代理 --右键作业 --新建作业 --"常规"项中输入作业名称 --"步骤"项 --新建 --"步骤名"中输入步骤名 --"类型"中选择"Transact-SQL 脚本(TSQL)" --"数据库"选择执行命令的数据库 --"命令"中输入要执行的语句: exec p_process --确定 --"调度"项 --新建调度 --"名称"中输入调度名称 --"调度类型"中选择你的作业执行安排 --如果选择"反复出现" --点"更改"来设置你的时间安排 然后将SQL Agent服务启动,并设置为自动启动,否则你的作业不会被执行 设置方法: 我的电脑--控制面板--管理工具--服务--右键 SQLSERVERAGENT--属性--启动类型--选择"自动启动"--确定. --3.实现同步处理的方法2,定时同步 --在srv1中创建如下的同步处理存储过程 create proc p_process as   --更新修改过的数据   update b set name=i.name,telphone=i.telphone   from srv2.库名.dbo.author b,author i   where b.id=i.id and   (b.name i.name or b.telphone i.telphone)   --插入新增的数据   insert srv2.库名.dbo.author(id,name,telphone)   select id,name,telphone from author i   where not exists(   select * from srv2.库名.dbo.author where id=i.id)   --删除已经删除的数据(如果需要的话)   delete b   from srv2.库名.dbo.author b   where not exists(select * from author where id=b.id)   go 来源:cnblogs.com/liuqifeng/p/9148831.html 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-21 关键词: 嵌入式 C语言

  • C代码交换a、b值不一样的写法

    关注、星标公众号,不错过精彩内容 转自:嵌入式大杂烩 交换a、b的值在C语言的学习中是很常见的问题。最常用的方法就是引入一个中间变量当做中间介质来交换a、b的值。 代码如下: void change_ab(int *a, int *b){    int temp = 0;    temp = *a;    *a = *b;    *b = temp;} 注意,不能写为如下代码: void change_ab(int a, int b){    int temp = 0;    temp = a;    a = b;    b = temp;} 普通的变量传递,则不会改变内存内容,以为普通变量作为参数时,其实是在内存块(栈空间)中新申请了一块空闲块,不是原来的内存块,而函数调用完毕之后,这块新申请的内存块会由于变量的作用域失效而被系统回收。 如果把指针作为实参进行传递,也就是把内存地址传了过去,那么操作这个指针所指向的内存块,必然会改变这个内存的内容了。 以上这种方法就是最常见的方法。那么,你知道如何将a、b的值进行交换,并且不使用任何其他的中间变量? 方法一:采用算术的方法 void change_ab(int *a, int *b){    *a = *a + *b;    *b = *a - *b;    *a = *a - *b;} 方法二:采用异或的方法 void change_ab(int *a, int *b){    *a = *a ^ *b;    *b = *a ^ *b;    *a = *a ^ *b;} 方法一存在一个潜在的危险,当形参的类型改为无符号16位时,若a、b的值很大,那么a+b的值就有可能超出16位整数所能表示的范围,从而造成程序运行错误。方法二则没有这个问题,这是一种比较好的方法。 推荐阅读: C++中字符编码的转换 手把手教你用STM32Trust生成加密固件 ELF相比Hex、Bin文件格式有哪些与众不同? 关注 微信公众号『strongerHuang』,后台回复“1024”查看更多内容,回复“加群”按规则加入技术交流群。 长按前往图中包含的公众号关注 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2020-10-21 关键词: 嵌入式 C语言

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

技术子站

更多

项目外包