在嵌入式开发中,使用C语言而不是C++是否有任何理由?

97

问题

我在我的硬件上有两个编译器,分别是C++和C89。

我正在考虑使用带类但不使用多态性(以避免虚函数表)的C++。 我想使用C++的主要原因是:

  • 我更喜欢使用“inline”函数而不是宏定义。
  • 我想使用名称空间来避免代码混乱。
  • 我认为C++更加类型安全,主要是因为有模板和详细的强制类型转换。
  • 我真的很喜欢重载函数和构造函数(用于自动转换)。

您认为在为非常有限的硬件(4KB RAM)开发时坚持使用C89有任何理由吗?

结论

感谢您的回答,它们非常有帮助!

我经过深思熟虑,决定坚持使用C,主要是因为:

  1. 在C中更容易预测实际代码,如果只有4KB RAM,这非常重要。
  2. 我的团队主要由C开发人员组成,因此不会经常使用高级的C ++功能。
  3. 我已经找到了一种方法,在我的C编译器(C89)中内联函数。

很难接受一个答案,因为您提供了许多好的答案。不幸的是,我无法创建维基并接受它,因此我将选择一种让我思考最多的答案。


13
一件事情:始终要清楚地知道你正在使用哪种语言进行编写。不要试图用“C/C++”来编写程序。要么使用C编写,要么使用C++编写,并明确知道你将使用哪些语言特性以及哪些不会使用。 - David Thornley
1
请参见https://dev59.com/c3RB5IYBdhLWcg3wWGEH - Suma
1
@DavidThornley,对于嵌入式案例你可能是正确的,但我非常惊讶地发现,在我试图通过STL扩展常见的行业开源应用程序(如Kamailio)时,混合使用C和C++代码非常不错。我正式鼓励这种使用STL和C代码,因为它提供了巨大的功能和易于维护,同时几乎不会产生任何问题(C++中缺少嵌套结构体是一种可怕的犯罪,应该尽快改正)。 - user2548100
思考的食物,这是一篇很棒的文章,其中ZeroMQ的设计师和作者讨论了他为什么后悔使用C++而不是C编写代码库。这并不是我预期的,原因在此页面上也没有找到。http://250bpm.com/blog:4 - user2548100
嗯,确实如此。匿名结构体就是它了。 - user2548100
显示剩余3条评论
29个回答

75
对于像4KB RAM这样资源非常有限的目标,我建议在投入大量不易迁移回ANSI C实现之前先进行一些样本测试。嵌入式C++工作组确实提出了语言的标准子集和相应的标准库子集。不过,当C用户杂志停刊后,我就失去了对该工作的关注。看起来Wikipedia上有一篇文章,并且committee仍然存在。
在嵌入式环境中,你真的必须小心内存分配。为了强制执行这种小心,你可能需要将全局operator new()及其相关函数定义为无法链接的内容,以便你知道它们没有被使用。另一方面,当与稳定、线程安全和延迟保证的分配方案一起谨慎使用时,放置new可能会成为你的朋友。
内联函数不会引起太大的问题,除非它们足够大,本来应该是真正的函数。当然,宏替换也有同样的问题。
模板也可能不会引起问题,除非它们的实例化失控。对于任何你使用的模板,审核生成的代码(链接图可能有足够的线索),确保只发生了你打算使用的实例化。
另一个可能出现的问题是与调试器的兼容性。通常情况下,可用的硬件调试器在与原始源代码交互方面的支持非常有限。如果您必须有效地进行汇编调试,则 C++ 的有趣名称重整可能会增加额外的困惑。
RTTI、动态转换、多重继承、重型多态和异常都会带来一定的运行时成本。如果使用这些功能,其中一些特性将在整个程序中平衡这些成本,而其他特性则仅增加需要它们的类的权重。了解差异,并选择具备完全了解至少概述成本/收益分析的高级功能。
在小型嵌入式环境中,您将直接链接到实时内核或直接在硬件上运行。无论哪种方式,您都需要确保运行时启动代码正确处理 C++ 特定的启动任务。这可能只是简单地确保使用正确的链接器选项,但由于通常对源代码具有直接控制权,因此您可能需要审核以确保它执行所有任务。例如,在我使用的 ColdFire 平台上,开发工具随附了一个 CRT0.S 模块,其中包含 C++ 初始化程序,但已注释掉。如果我直接使用它,那么全局对象的构造函数根本不会运行,这会让我感到困惑。
此外,在嵌入式环境中,通常需要在使用硬件设备之前初始化它们。如果没有操作系统和引导加载程序,则需要您的代码完成这项工作。请记住,全局对象的构造函数在调用main()之前运行,因此您需要修改本地CRT0.S(或其等效物)以在调用全局构造函数之前完成硬件初始化。显然,main()的顶部太晚了。

1
+1,非常好的回答。但我认为你真正需要担心的只是(相对罕见的)递归类型的模板实例化 - 对于“常规”的非递归类型,实例化就相当于您手动输入的代码。 - j_random_hacker
3
@j_random_hacker,没错。但是使用模板的习惯可能会导致偶尔出现第二(或第三)次实例化时没有进行适当类型转换,而在使用点进行类型强制转换可能已经可以防止这种情况。这只是需要注意的事情。 - RBerteig
@RBerteig:好观点,模板允许更少的类型强制转换可能性=>可能会产生比非模板代码更多的独特实例化。 - j_random_hacker

56

使用C而非C++的两个原因:

  1. 对于许多嵌入式处理器,可能没有C++编译器,或者您需要额外付费才能获得。
  2. 我的经验是,相当大一部分嵌入式软件工程师很少或根本没有C++经验--要么是因为(1),要么是因为它通常不在电子工程学位中教授--因此最好坚持他们所知道的。

此外,原始问题和一些评论提到了4 Kb的 RAM 。对于典型的嵌入式处理器,RAM的数量(大多数情况下)与代码大小无关,因为代码存储在闪存中并从中运行。

当然,代码存储空间的量是需要考虑的事项,但随着市场上出现新的、更宽敞的处理器,这已经不再是除了最注重成本的项目之外的问题了。

关于在嵌入式系统中使用C++的一个子集:现在有一个MISRA C++标准,值得一看。

编辑:另请参见此问题,它引发了关于嵌入式系统中C与C++的辩论。


2
请看下面的长篇回答:C++往往会使将常量数据放入闪存变得非常困难。 - jakobengblom2
3
使用C而不是C++的一个可能很好的理由是C的标准ABI。仅仅是为了完整性。 - Chris Lutz

27

没有。在嵌入式开发中,可以避免任何可能引起问题的C++语言特性(运行时多态性、RTTI等)。有一群嵌入式C++开发者的社区(我记得旧的 C/C++ Users' Journal 上有使用C++的嵌入式开发者专栏文章),我想象不出他们会非常强烈地反对这个选择。


22

C++性能技术报告是这方面的一本好指南。注意,它有一节关于嵌入式编程问题的内容!

此外,回答中提到了嵌入式C++,我也赞同。虽然标准不完全符合我的口味,但在决定放弃C++的哪些部分时,它是一个好的参考。

在为小型平台编程时,我们会禁用异常和RTTI,避免虚拟继承,并密切关注我们所拥有的虚拟函数数量。

然而,您的朋友是链接器映射: 经常检查它,您将快速发现代码和静态内存膨胀的源头。

之后,标准的动态内存使用考虑因素也适用:在您提到的如此受限制的环境中,您可能根本不想使用动态分配。对于小的动态分配,有时可以使用内存池,或者预分配一个块并稍后全部释放。


18

我建议使用C++编译器,但限制你使用C++特定的功能。你可以像在C语言中那样在C++中编程(当使用C++时,C运行库会被包含进来,尽管在大多数嵌入式应用中,你不需要使用标准库)。

你可以使用C++类等,只要:

  • 限制使用虚函数(就像你说的)
  • 限制使用模板
  • 为嵌入式平台重写操作符 new 和/或使用放置 new 来进行内存分配。

9
当然,如果你已经基本上在写C语言,那么最好正式采用它。 - Chuck
6
为什么限制模板的使用?我认为,在嵌入式系统中,模板函数可以非常有帮助,例如展开循环。 - Piotr Czapla
1
你仍然可以使用模板,但是我建议你要非常小心,因为它们可能会快速增加输出二进制文件的大小。当然,如果你的代码直接从ROM或类似设备运行,并且你有多余的ROM空间,那么当然可以使用,但除此之外,你需要小心处理模板(在最坏的情况下,每个模板实例基本上都是在最终可执行文件中再次复制所有模板化的代码)。 - Chris Walton

16
作为一名嵌入式系统工程师,我可以告诉大家为什么C语言仍然是比C++更优先的选择,是因为以下原因:
1) 我们需要在一些只有64kB RAM的目标平台上进行开发,所以你必须确保每个字节都能被充分利用。在2008年,我曾经进行过代码优化以节省4个字节,耗费了2个小时。
2) 由于存在尺寸限制,我们会审核每一个C库函数,因此我们更倾向于让人们不要使用除法(没有硬件除法器,所以需要一个庞大的库)、malloc(因为我们没有堆栈,所有内存都是从512字节数据缓冲区中分配并需要代码审核),或其他带来巨大惩罚的面向对象实践方法。记住,你使用的每一个库函数都是计算在内的。
3) 你听说过"overlay"这个术语吗?有时候你的代码空间非常有限,所以你必须使用另一组代码来代替它。如果你调用一个库函数,那么该库函数必须是驻留的。如果你只在叠加函数中使用它,那么你就浪费了很多空间,过多地依赖于面向对象的方法。因此,不要假设任何C库函数,更不要说C++,会被接受。
4) 由于硬件设计的限制(例如被连接某种方式的ECC引擎)或为了应对硬件错误,需要进行强制转换甚至打包(其中不对齐的数据结构越过了字边界)。你不能太过隐式地假设一些情况,那么为什么还要过度地面向对象呢?
5) 最坏的情况是:消除某些面向对象的方法将迫使开发者在使用可能会爆炸的资源之前进行思考(例如在堆栈上分配512字节而不是从数据缓冲区中分配),并防止一些未经测试的最坏情况发生或直接消除整个代码路径。

6) 我们使用了很多抽象方法,以保持硬件与软件的分离,并使代码尽可能便携和易于模拟。必须用条件编译包裹硬件访问的宏或内联函数在不同平台之间进行编译,数据类型必须转换为字节大小而不是目标特定的,不允许直接使用指针(因为某些平台假设内存映射I/O与数据内存相同),等等。

我还可以想到更多,但你已经有了这个想法。我们嵌入式系统的人确实接受了面向对象的培训,但嵌入式系统的任务可以如此硬件导向和低级别,以至于本质上不是高级别的或可抽象的。

顺便说一下,我到过的每一个固件工作都使用源代码控制,我不知道你从哪里得到这个想法的。

-来自SanDisk的某个固件工程师。


在90年代初期,叠加技术非常流行(至少在DOS世界中)。 - psihodelia
很好的观点,Shing。在功能受限、资源更加有限的项目中,C++就像是一个相扑选手在电话亭里挣扎一样。 - user1899861
5
我认为这个答案非常主观,没有提供具体的推理。 - Venemo
2
C++并不一定意味着是“面向对象”的。 - Martin Bonner supports Monica
3
嵌入式系统任务天生不具备可抽象性,这种说法并不准确。你在第六点中已经说过:“我们确实使用了很多抽象来将硬件和软件隔离,并尽可能地使代码具有可移植性”。顺便说一句,“抽象”并不必然意味着“多态性”。 - Daniele Pallastrelli

10

我的个人偏好是C,因为:

  • 我知道每一行代码在做什么(成本)
  • 我不太了解C++,不知道每一行代码在做什么(成本)

为什么人们会这样说呢?除非您检查汇编输出,否则您无法知道每一行C代码的作用。同样适用于C++。

例如,这个看似无害的语句产生了什么汇编码:

a[i] = b[j] * c[k];

看起来很无辜,但是基于gcc的编译器为8位微控制器生成了这个汇编代码

CLRF 0x1f, ACCESS
RLCF 0xfdb, W, ACCESS
ANDLW 0xfe
RLCF 0x1f, F, ACCESS
MOVWF 0x1e, ACCESS
MOVLW 0xf9
MOVF 0xfdb, W, ACCESS
ADDWF 0x1e, W, ACCESS
MOVWF 0xfe9, ACCESS
MOVLW 0xfa
MOVF 0xfdb, W, ACCESS
ADDWFC 0x1f, W, ACCESS
MOVWF 0xfea, ACCESS
MOVFF 0xfee, 0x1c
NOP
MOVFF 0xfef, 0x1d
NOP
MOVLW 0x1
CLRF 0x1b, ACCESS
RLCF 0xfdb, W, ACCESS
ANDLW 0xfe
RLCF 0x1b, F, ACCESS
MOVWF 0x1a, ACCESS
MOVLW 0xfb
MOVF 0xfdb, W, ACCESS
ADDWF 0x1a, W, ACCESS
MOVWF 0xfe9, ACCESS
MOVLW 0xfc
MOVF 0xfdb, W, ACCESS
ADDWFC 0x1b, W, ACCESS
MOVWF 0xfea, ACCESS
MOVFF 0xfee, 0x18
NOP
MOVFF 0xfef, 0x19
NOP
MOVFF 0x18, 0x8
NOP
MOVFF 0x19, 0x9
NOP
MOVFF 0x1c, 0xd
NOP
MOVFF 0x1d, 0xe
NOP
CALL 0x2142, 0
NOP
MOVFF 0x6, 0x16
NOP
MOVFF 0x7, 0x17
NOP
CLRF 0x15, ACCESS
RLCF 0xfdf, W, ACCESS
ANDLW 0xfe
RLCF 0x15, F, ACCESS
MOVWF 0x14, ACCESS
MOVLW 0xfd
MOVF 0xfdb, W, ACCESS
ADDWF 0x14, W, ACCESS
MOVWF 0xfe9, ACCESS
MOVLW 0xfe
MOVF 0xfdb, W, ACCESS
ADDWFC 0x15, W, ACCESS
MOVWF 0xfea, ACCESS
MOVFF 0x16, 0xfee
NOP
MOVFF 0x17, 0xfed
NOP

指令数的生成量与以下因素密切相关:

  • a、b和c的大小。
  • 这些指针是存储在栈上还是全局的。
  • i、j和k是存储在栈上还是全局的。

这在微型嵌入式系统中特别明显,因为处理器并未完全配置好以处理C语言。因此我的答案是,除非您始终检查汇编代码输出,否则C和C ++同样糟糕,如果始终检查它们的汇编代码输出,则它们同样出色。

Hugo


3
请注意,其中间有一条call指令,它实际上调用了multiply函数。所有的代码甚至并不是multiply指令! - Rocketmagnet
熟悉微型机的人通常会知道处理每个部分C代码的简单方法,而一个体面的编译器不应生成比那更糟糕的代码。上述表达式要想高效处理,唯一的方法就是做出可能不适合于C编译器的假设。 - supercat
这看起来像是Microchip XC8编译器的免费版本针对PIC18的输出。我相信这个编译器故意发出膨胀的代码,以鼓励人们购买他们的专业版。自从我上次使用它以来已经过了几年,所以我不知道它是否仍然表现得一样。 - Tagli

10

我听说有些人喜欢在嵌入式工作中使用C语言,因为它更简单,因此更容易预测将生成的实际代码。

个人认为编写C++风格的C语言(使用模板进行类型安全)会给您带来很多优势,我看不出任何真正的理由不这样做。


9
我认为没有理由使用 C 而不是 C++。无论你在 C 中能做什么,你也可以在 C++ 中做到同样的效果。如果你想避免虚表开销,就不要使用虚方法和多态。
然而,C++ 可以提供一些非常有用的习语,而不会有额外的开销。其中我最喜欢的之一是 RAII(资源获取即初始化)。从内存或性能方面来看,类并不一定昂贵...

8
人类大脑处理复杂性的方式是尽可能地评估,然后决定要关注什么,舍弃或贬低其他内容。这就是市场营销中品牌和大部分图标背后的全部基础。
为了对抗这种趋势,我更喜欢 C 语言而不是 C++,因为它迫使你更加密切地思考你的代码以及它如何与硬件交互——无情地紧密联系在一起。
通过长期的经验,我相信 C 强制你提出更好的问题解决方案,部分原因是它不会阻碍你花费大量时间来满足编译器编写者认为是一个好主意的约束条件,或者弄清楚“底层”发生了什么。
在这个方面,像 C 这样的低级语言让你花费大量时间专注于硬件和构建良好的数据结构/算法组合,而高级语言则让你花费大量时间探究内部运作,并想知道为什么在特定环境和上下文中不能完美地完成某些合理的事情。将编译器按照你的意愿进行调整(强类型是最严重的罪犯)并不是一个有效的时间利用方式。
我可能很适合程序员的模板——我喜欢掌控。在我看来,这不是程序员的个性缺陷。控制是我们拿到薪水的原因。更具体地说,是完美的控制。C 比 C++ 给你更多的控制。

ZeroMQ 的作者 Martin Sistrik 在讨论为什么他现在希望他当初写 ZeroMQ 时用 C 而不是 C++ 时,几乎表达了相同的观点。请查看 http://250bpm.com/blog:8。 - user2548100

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接