我没有想到的优势(但请参阅底部的JasonS注释),将一行代码包装为一个函数或子例程。也许您可以将函数命名为“可读”。但是您也可以对这一行发表评论。而且由于在函数中包装一行代码会浪费代码存储空间,堆栈空间和执行时间,因此在我看来,这几乎适得其反。在教学情况下?这可能是有道理的。但这取决于学生的班级,他们的事先准备,课程和老师。通常,我认为这不是一个好主意。但这是我的意见。
这将我们带到了底线。几十年来,您的主要问题领域一直是一些辩论的问题,而至今仍是一些辩论的问题。因此,至少在我阅读您的问题时,在我看来,这是一个基于观点的问题(正如您所问的那样)。
如果您要更详细地了解情况并仔细描述您的首要目标,则可以脱离基于观点的观点。您定义的测量工具越好,答案可能越客观。
概括地说,您希望对任何编码执行以下操作。(下面,我将假设我们正在比较所有实现目标的不同方法。显然,无论编写方式如何,任何无法执行所需任务的代码都比成功的代码差。)
- 对您的方法保持一致,以便其他人阅读您的代码可以加深对您如何进行编码过程的理解。不一致可能是最严重的犯罪。这不仅使他人感到困难,而且使您自己在数年后重新回到代码中变得困难。
- 尽可能尝试安排事物,以便可以在不考虑顺序的情况下执行各种功能部分的初始化。在需要排序的情况下,如果是由于两个高度相关的子函数的紧密耦合而引起的,那么请考虑对两个函数进行一次初始化,以便可以对其进行重新排序而不会造成损害。如果不可能,请记录初始化订购要求。
- 尽可能将知识封装在一个地方。常量不应在代码的各处重复。解决某个变量的方程式应该只存在于一个位置。等等。如果您发现自己在不同位置复制并粘贴了一些行,这些行在某些位置执行了某些所需的行为,请考虑一种在一个位置捕获该知识并在需要时使用它的方法。例如,如果您具有必须以特定方式行走的树结构,则不要在您需要遍历树节点的每个位置复制树遍历代码。取而代之的是,将树走法捕获到一个地方并加以使用。这样,如果树变了,而步行方法也变了,那么您只需要担心一个地方,其余所有代码“都可以正常工作”。
- 如果您将所有例程分散到一张巨大的平板纸上,并用箭头将它们连接起来,就像其他例程所调用的那样,您会发现在任何应用程序中都会有包含大量箭头的例程的“簇”彼此之间,但小组外只有几支箭。因此,紧密耦合的例程之间存在自然界线,而其他紧密耦合的例程组之间则存在松散耦合的连接。利用这一事实将代码组织到模块中。这将极大地限制代码的外观复杂性。
以上对于所有编码通常都是正确的。我没有讨论参数,局部或静态全局变量等的使用。原因是对于嵌入式编程,应用程序空间通常会放置极端且非常重要的新约束,并且不讨论每个嵌入式应用程序就不可能讨论所有这些约束。无论如何,这不是在这里发生。
这些限制可能是以下任何(或更多)限制:
- 严重的成本限制要求极其原始的MCU具有极小的RAM,几乎没有I / O引脚数。对于这些,将应用全新的规则集。例如,由于没有太多的代码空间,您可能必须编写汇编代码。您可能只需要使用静态变量,因为使用局部变量过于昂贵且耗时。您可能必须避免过多使用子例程,因为(例如,某些Microchip PIC部件)只有4个硬件寄存器可存储子例程返回地址。因此,您可能必须大幅“展平”您的代码。等等。
- 严重的功率限制要求精心设计的代码来启动和关闭大多数MCU,并且在全速运行时对代码的执行时间设置了严格的限制。同样,这有时可能需要一些汇编代码。
- 严格的计时要求。例如,有时候我必须确保漏极开路0的传输必须与1的传输完全相同的周期数。并且还必须对同一条线进行采样与此时间有一个确切的相对相位。这意味着在这里不能使用C。唯一可以保证的方法就是精心编写汇编代码。(即使如此,并非所有ALU设计都总是如此。)
等等。(生命攸关的医疗仪器的接线代码也拥有自己的整个世界。)
这里的结果是,嵌入式编码通常不是全部免费的,您可以像在工作站上那样在其中进行编码。往往存在严重的竞争性原因,造成许多非常困难的限制。而这些可能会强烈反对将更多的传统和股票的答案。
关于可读性,我发现如果以一致的方式编写代码,并且在阅读时可以学习,那么代码是可读的。在没有故意混淆代码的地方。确实并不需要太多。
可读的代码效率很高,并且可以满足我已经提到的所有上述要求。最主要的是,您在编写代码时完全了解在汇编或机器级别编写的每一行代码会产生什么。C ++在这里给程序员带来了沉重的负担,因为在许多情况下,相同的C ++代码片段实际上会生成性能各不相同的不同的机器代码片段。但是,一般而言,C通常是一种“所见即所得”的语言。因此在这方面更安全。
根据JasonS编辑:
我从1978年开始使用C,从1987年以来一直使用C ++,并且在大型机,小型计算机和(主要是)嵌入式应用程序上使用这两种方法都有很多经验。
Jason提出了有关使用“内联”作为修饰语的评论。(在我看来,这是一个相对“新”的功能,因为使用C和C ++可能在我生命的一半或更长时间中根本不存在。)使用内联函数实际上可以进行这样的调用(即使是一行调用)。代码)非常实用。而且由于编译器可以应用的类型,它比使用宏要好得多。
但是也有局限性。首先,您不能依靠编译器来“获取提示”。它可能会也可能不会。并且有充分的理由不接受提示。(举一个明显的例子,如果使用了函数的地址,则需要实例化函数,并且使用该地址进行调用将...需要调用。然后无法内联代码。)其他原因也是如此。编译器可能有各种各样的标准来判断如何处理提示。作为程序员,这意味着您必须花一些时间来了解编译器的这一方面,否则您可能会基于有缺陷的想法做出决策。因此,这既增加了代码编写者的负担,也增加了任何读者以及计划将代码移植到其他编译器的人员的负担。
此外,C和C ++编译器支持单独的编译。这意味着他们可以编译一段C或C ++代码,而无需编译该项目的任何其他相关代码。为了内联代码,假设编译器可能选择这样做,则它不仅必须具有声明“ in scope”,而且还必须具有定义。通常,程序员在使用“内联”时会努力确保确实如此。但是错误很容易蔓延。
通常,虽然我在我认为合适的地方也使用内联,但是我倾向于认为我不能依赖它。如果性能是一个重要的要求,并且我认为OP已经清楚地写到,当他们采用更“实用”的路线时会对性能造成重大影响,那么我当然会选择避免将内联作为编码实践来使用,并且而是遵循稍微不同但完全一致的代码编写模式。
关于“内联”和定义在单独的编译步骤“范围内”的最后说明。有可能(并非始终可靠)在链接阶段执行工作。仅当C / C ++编译器将足够的详细信息嵌入到目标文件中以允许链接器对“内联”请求执行操作时,才会发生这种情况。我个人还没有体验过支持此功能的链接器系统(Microsoft之外)。但是它会发生。同样,是否应依赖它取决于情况。但是我通常认为这还没有被应用到链接器中,除非我有充分的证据可以知道。如果我确实依靠它,它将被记录在显眼的地方。
C ++
对于那些感兴趣的人,这是一个示例,说明为什么尽管嵌入式C ++现已可用,但我在对C ++进行编码时仍然非常谨慎。我会抛出一些我认为所有嵌入式C ++程序员都需要了解Cold的术语:
- 部分模板专业化
- 虚拟表
- 虚拟基础对象
- 激活框
- 激活框架展开
- 在构造函数中使用智能指针的原因
- 收益优化
那只是一个简短的清单。如果您还不了解这些术语的全部内容以及为什么我列出它们(以及更多我未在此处列出的术语),那么我建议您不要在嵌入式工作中使用C ++,除非它不是该项目的选择。
让我们快速看一下C ++异常语义,以弄清味道。
当C ++编译器完全不知道在单独的编译单元(在不同的时间编译)中可能需要哪种异常处理时,它必须为编译单元生成正确的代码。 乙AB
采取以下代码序列,这些代码是某些编译单元中某些功能的一部分:A
.
.
foo ();
String s;
foo ();
.
.
出于讨论目的,编译单元在其源代码中的任何地方都不使用“ try..catch” 。它也不使用“ throw”。实际上,可以说它不使用C编译器无法编译的任何源,除了它使用C ++库支持并且可以处理诸如String之类的对象外。该代码甚至可能是经过稍微修改以利用一些C ++功能(例如String类)的C源代码文件。A
另外,假定foo()是位于编译单元的外部过程,并且编译器对此进行了声明,但不知道其定义。B
C ++编译器会看到对foo()的首次调用,并且如果foo()引发异常,则可以只允许正常激活帧展开。换句话说,C ++编译器知道此时不需要额外的代码来支持异常处理中涉及的帧展开过程。
但是一旦创建String,C ++编译器就会知道,如果以后发生异常,则必须先将其正确销毁,然后才能允许帧展开。因此,对foo()的第二次调用在语义上与第一次调用不同。如果对foo()的第二次调用引发异常(它可能会也可能不会),则编译器必须先放置旨在处理String的销毁的代码,然后再释放通常的帧。这与第一次调用foo()所需的代码不同。
(可以在C ++中添加其他修饰来帮助限制此问题。但是事实是,使用C ++的程序员必须更加清楚自己编写的每一行代码的含义。)
与C的malloc不同,C ++的新函数使用异常来表示何时无法执行原始内存分配。“ dynamic_cast”也是如此。(有关C ++中的标准异常,请参见Stroustrup的第三版,C ++编程语言,第384页和第385页。)编译器可以允许禁用此行为。但是总的来说,由于生成的代码中格式正确的异常处理序言和结语,即使实际上并没有发生异常,甚至在编译的函数实际上没有任何异常处理块的情况下,也会招致一些开销。(Stroustrup公开感叹了这一点。)
如果没有部分模板专门化(并非所有C ++编译器都支持),模板的使用会给嵌入式编程带来灾难。没有它,代码泛滥将是一个严重的风险,可能会在一瞬间杀死一个小内存嵌入式项目。
当C ++函数返回一个对象时,将创建并销毁一个未命名的编译器临时文件。如果在return语句中使用对象构造函数而不是局部对象,则某些C ++编译器可以提供高效的代码,从而减少了一个对象的构造和销毁需求。但是,并不是每个编译器都这样做,而且许多C ++程序员甚至都不知道这种“返回值优化”。
为对象构造函数提供单个参数类型可能允许C ++编译器以完全不希望给程序员的方式在两种类型之间找到转换路径。这种“聪明”的行为不是C的一部分。
指定基本类型的catch子句将“切片”抛出的派生对象,因为抛出的对象是使用catch子句的“静态类型”而不是对象的“动态类型”复制的。异常苦难的根源并不罕见(当您认为您甚至可以负担嵌入式代码中的异常时)。
C ++编译器可以为您自动生成构造函数,析构函数,复制构造函数和赋值运算符,而产生意想不到的结果。详细了解此过程需要花费时间。
将派生对象数组传递给接受基础对象数组的函数,很少生成编译器警告,但几乎总是会产生错误的行为。
由于在对象构造函数中发生异常时C ++不会调用部分构造的对象的析构函数,因此在构造函数中处理异常通常会强制使用“智能指针”,以确保如果确实发生异常,则构造函数中的构造片段会被正确销毁。(请参阅Stroustrup,第367和368页。)这是用C ++编写好的类的常见问题,但由于C没有内置构造和销毁的语义,因此在C中当然可以避免。编写适当的代码来处理构造对象内子对象的存在意味着编写必须解决C ++中这种独特语义问题的代码;换句话说,“写” C ++语义行为。
C ++可以复制传递给对象参数的对象。例如,在以下片段中,调用“ rA(x);” 可能会导致C ++编译器调用参数p的构造函数,以便随后调用复制构造函数将对象x传递到参数p,然后是函数rA的返回对象(一个未命名的临时对象)的另一个构造函数,这当然是从参数p复制。更糟糕的是,如果A类拥有自己的需要构造的物体,这可能会带来灾难性的伸缩。(由于C程序员没有这样方便的语法,并且一次只能表达所有细节,因此AC程序员可以避免大部分这种浪费的手工优化工作。)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
最后,是C程序员的简短说明。longjmp()在C ++中没有可移植的行为。(某些C程序员将其用作一种“异常”机制。)某些C ++编译器实际上会尝试采用longjmp进行清理,但这种行为在C ++中是不可移植的。如果编译器确实清除了构造的对象,则它是不可移植的。如果编译器没有清除它们,则由于longjmp导致代码离开了构造对象的范围,并且行为无效,因此不会破坏对象。(如果在foo()中使用longjmp没有超出范围,则该行为可能很好。)C嵌入式程序员不太经常使用此方法,但在使用它们之前,他们应该使自己意识到这些问题。