从C ++迁移到C


83

用C ++编码几年后,最近在嵌入式领域为我提供了C语言的作业编码。

抛开在嵌入式领域解雇C ++是对还是错的问题,C ++有一些功能/习惯用法,我会想念很多。仅举几个:

  • 通用的类型安全的数据结构(使用模板)。
  • RAII。特别是在具有多个返回点的函数中,例如,不必记住在每个返回点上释放互斥量。
  • 析构函数一般。即,您为MyClass编写了一次d'tor,然后,如果MyClass实例是MyOtherClass的成员,则MyOtherClass不必显式取消初始化MyClass实例-它的d'tor会自动调用。
  • 命名空间。

从C ++到C的经历是什么?
您为自己喜欢的C ++功能/习惯找到了哪些C替代品?您是否发现了希望C ++具有的C功能?


12
如果您只是要求经验而不是建议,则应该是社区Wiki。
彼得·亚历山大

6
您可能对Prog.SE感兴趣。

11
@Peter:OP不再让CW提出问题了,它需要的代表人数比他仍然有可能时要多。如果您认为某个问题应以允许其他用户编辑“社区拥有的”帖子之外的任何其他原因而成为社区Wiki,那么您真正想要的是关闭问题。

4
这个问题不是更适合程序员吗?由于这绝对是一个“真实”的问题,我说我们重新打开它,然后投票决定将其移开。那是不可能的。好。
Lasse V. Karlsen

21
直到prog SE超出beta时,这种转移才会发生,无论如何,我认为这种QA方法令人震惊。它分散了社区,使用户烦恼,重复了问题和答案。它造成了混乱的无组织信息,这些信息以前可以在单个“程序员”站点上访问和浏览。另外,像这样的问题,拥有巨大的见解和令人难以置信的反对意见,这让我在5个重击者与整个社区之间感到愤怒。
Stefano Borini 2010年

Answers:


68

在嵌入式项目中,我曾经尝试在所有C语言中工作,但受不了。太冗长了,以至于很难阅读任何东西。另外,我喜欢我编写的针对嵌入式优化的容器,这种容器必须变得安全性低得多,并且很难修复#define块。

C ++中的代码如下所示:

if(uart[0]->Send(pktQueue.Top(), sizeof(Packet)))
    pktQueue.Dequeue(1);

变成:

if(UART_uchar_SendBlock(uart[0], Queue_Packet_Top(pktQueue), sizeof(Packet)))
    Queue_Packet_Dequeue(pktQueue, 1);

很多人可能会说这很好,但是如果您必须要做多个而不是一行中的“方法”调用,这将变得很荒谬。两行C ++将变成五行C(由于80个字符的行长限制)。两者都会生成相同的代码,因此与目标处理器无关!

有一次(早在1995年),我尝试为多处理器数据处理程序编写大量的C语言。每个处理器都有自己的内存和程序的类型。供应商提供的编译器是C编译器(某种HighC派生类),它们的库是封闭源代码,因此我无法使用GCC进行构建,并且其API的设计思想是程序将主要是初始化/进程/终止多样性,因此处理器间通信充其量是基本的。

我放弃了大约一个月的时间,找到了cfront的副本,并将其入侵到makefile中,以便可以使用C ++。Cfront甚至不支持模板,但是C ++代码清晰得多。

通用的类型安全的数据结构(使用模板)。

C与模板最接近的事情是用许多代码声明一个头文件,如下所示:

TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this)
{ /* ... */ }

然后用类似的方式将其拉入:

#define TYPE Packet
#include "Queue.h"
#undef TYPE

请注意,unsigned char除非您typedef先进行复合操作,否则这不适用于复合类型(例如,没有队列)。

哦,请记住,如果实际上没有在任何地方使用此代码,那么您甚至都不知道它在语法上是否正确。

编辑:另外一件事:您将需要手动管理代码的实例化。如果您的“模板”代码不是全部内联函数,那么您将必须进行一些控制以确保事物仅被实例化一次,以便您的链接器不会吐出一堆“ Foo的多个实例”错误。

为此,您必须将非内联的内容放在头文件的“实现”部分中:

#ifdef implementation_##TYPE

/* Non-inlines, "static members", global definitions, etc. go here. */

#endif

然后,在每种模板变体的所有代码中的某个位置,您必须:

#define TYPE Packet
#define implementation_Packet
#include "Queue.h"
#undef TYPE

此外,这实现部分需求是标准#ifndef/ #define/#endif一长串,因为你可能包括另一头文件模板头文件,但需要实例化后的.c文件。

是的,它变得很难看。这就是为什么大多数C程序员甚至不尝试的原因。

RAII。

特别是在具有多个返回点的函数中,例如,不必记住在每个返回点上释放互斥量。

好吧,忘记您的漂亮代码,并习惯于所有返回点(函数末尾除外)gotos:

TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this)
{
    TYPE * result;
    Mutex_Lock(this->lock);
    if(this->head == this->tail)
    {
        result = 0;
        goto Queue_##TYPE##_Top_exit:;
    }

    /* Figure out `result` for real, then fall through to... */

Queue_##TYPE##_Top_exit:
    Mutex_Lock(this->lock);
    return result;
}

析构函数一般。

即,您为MyClass编写了一次d'tor,然后,如果MyClass实例是MyOtherClass的成员,则MyOtherClass不必显式取消初始化MyClass实例-它的d'tor会自动调用。

对象构造必须以相同的方式显式处理。

命名空间。

这实际上是一个简单的解决方法:只需在每个符号上添加前缀即可。这是前面提到的源代码膨胀的主要原因(因为类是隐式名称空间)。C人民一直在好好生活,直到永远,也许看不到有什么大不了的。

青年汽车


59
当然,如果您试图强迫它成为C ++,那么您会讨厌C。我怀疑如果您尝试将$ more_expressive_language的功能强加到C ++中,C ++会看起来很棒。无需评论您的帖子,只需观察

关于goto代替RAII的技术:这不是维护的噩梦吗?也就是说,无论何时添加需要清理的代码路径,甚至只是更改函数内部事物的顺序,都必须记住最后要转到goto标签并对其进行更改。我希望看到一种可以以某种方式在需要清除的内容旁边注册清除代码的技术。
乔治,2010年

2
@george:我讨厌这么说,但是按照C标准,我见过的大多数嵌入式C代码都非常糟糕。例如,我现在正在使用Atmel的at91lib,它要求您编写一个“ board.h”文件,该文件的大多数代码都作为依赖项插入。(对于他们的演示板,此标头的长度为792行。)此外,您必须为开发板自定义的“ LowLevelInit()”函数几乎全部是注册访问权限,例如AT91C_BASE_PMC->PMC_MOR = (0x37 << 16) | BOARD_OSCOUNT | AT91C_CKGR_MOSCRCEN | AT91C_CKGR_MOSCXTEN | AT91C_CKGR_MOSCSEL;
Mike DeSimone 2010年

1
哦,那里什么都没有告诉您BOARD_OSCOUNT(这是等待时钟切换的超时值;清除,是吗?)实际上是#definein board.h。在同一功能中,还有许多复制粘贴的自旋循环代码,应该将它们转换为两行#define(并且当我这样做时,它节省了一些字节的代码,并制作了通过使寄存器集和自旋循环更独特来使函数更易读)。使用C的主要原因之一是它可以让您微管理和优化所有内容,但是我见过的大多数代码都不会打扰。
Mike DeSimone 2010年

5
同意@Mads。没有理由针对您真正不需要的功能进行所有这些操作。我发现我喜欢与GTK库类似的样式。将您的“类”定义为结构,然后创建一致的方法(例如my_class_new()),然后将其传递到“方法”中:my_class_do_this(my_class_instance)
最大,时间

17

我从C ++转到C的原因有所不同(某种过敏反应;),我只错过了几件事,而有些收获了。如果您坚持使用C99,则可以使用某些结构,使您可以很好地,安全地进行编程,尤其是

  • 指定的初始化程序(最终与宏组合)使简单类的初始化像构造函数一样轻松
  • 临时变量的复合文字
  • for-scope变量可以帮助您进行范围限制的资源管理,特别是即使在初步函数返回的情况下,也可以确保unlock互斥或free数组
  • __VA_ARGS__ 宏可用于为函数提供默认参数并进行代码展开
  • inline 函数和宏可以很好地结合以替换(某种)重载函数

2
@迈克:特别是哪一部分?如果您遵循我为for示波器提供的链接,那么您将进入P99,在这里您还可以查看其他部分的示例和说明。
詹斯·古斯特


@乔治:谢谢!@Jens:其他四个例子。我的C语言落后了;最后,我听说他们添加了运行时大小的自动分配(即堆栈)数组(例如void DoSomething(unsigned char* buf, size_t bufSize) { unsigned char temp[bufSize]; ... }),并通过字段名(例如struct Foo bar = { .field1 = 5, .field2 = 10 };)进行结构初始化,我很想在C ++中看到后者,尤其是非POD对象(例如UART uart[2] = { UART(0x378), UART(0x278) };) 。
Mike DeSimone

@Mike:是的,有可变长度数组(VLA),但由于潜在的堆栈溢出,使用它们可能会有些危险。您描述的第二个恰好是“指定的初始化程序”,因此您将使用自己的示例;-)对于其他示例,如果单击“相关页面”,则可以在上面的P99链接中找到信息。
詹斯·古斯特

8

没有像C那样的STL存在。
有可用的库提供类似的功能,但不再内置。

认为那将是我最大的问题之一...知道我可以使用哪种工具来解决问题,但是却没有使用必须使用的语言提供的工具。


这是真的。有人可以详细说明一个容器类库应该用于C吗?还是答案是“自己写一个”?
Sandeep

@Sandeep:对于初学者来说,这个答案只对容器不在标准库中是正确的。除了缺少等效的STL(C ++的最佳组成部分)之外,C标准库也要优越得多。POSIX除了libc中的qsort之外,还包含tsearch,lsearch,hsearch和bsearch。Glib是C的权威性“ Boost”,请看一下它包装的好东西(包括容器)。library.gnome.org/devel/glib/stable。Glib还与Gtk +集成,该组合超过了Boost和Qt。还有libapr,在诸如Subversion和Apache之类的xplatform东西中很流行。
马特·乔纳

我找不到任何可以与stl竞争的c库,它们更难使用,更难维护,当他们想要将c库与stl保持通用时,性能不是stl的竞争对手。 c的局限性,以及为什么我们在c库中没有stl之类的原因,因为c根本没有能力开发stl之类的东西。
StereoMatching

8

C和C ++之间的区别是代码行为的可预测性。

精确地预测您的代码将在C中做什么将更容易,而在C ++中,进行精确的预测可能会变得更加困难。

C语言的可预测性使您可以更好地控制代码在做什么,但这也意味着您必须做更多的事情。

在C ++中,您可以编写更少的代码来完成相同的工作,但是(对我而言,这是我的荣幸)有时我很难知道目标代码在内存中的布局及其预期的行为。


4
每当我担心代码的实际作用时,都会添加该-s标志gcc以获取程序集转储,搜索所关注的功能并开始阅读。这是学习任何编译语言怪癖的好方法。
Mike DeSimone 2010年

2
看到C ++生成的程序集就像阅读P​​erl一样,这也是浪费时间。无论如何,真是太棒了。
马特·乔纳

7

顺便说一下,在我的工作线(已嵌入)中,我不断地在C和C ++之间来回切换。

当我使用C语言时,会想念C ++:

  • 模板(包括但不限于STL容器)。我将它们用于特殊计数器,缓冲池等(建立了自己的类模板和函数模板库,这些库在不同的嵌入式项目中使用)

  • 非常强大的标准库

  • 析构函数,这当然使RAII成为可能(互斥体,禁止中断,跟踪等)

  • 访问说明符,以更好地强制谁可以使用(看不到)什么

我在较大的项目上使用继承,并且C ++的内置支持比将基类作为第一个成员嵌入的C“ hack”更加简洁明了(更不用说自动调用构造函数,init.list等了)。 ),但上面列出的项目是我最想念的项目。

另外,大概只有三分之一的嵌入式C ++项目是我处理使用异常的,所以我已经习惯了没有它们,因此回到C时我不会错过太多。

另一方面,当我返回到具有大量开发人员的C项目时,经常会遇到整个类的C ++问题,这些问题通常用来向那些会消失的人解释。大多数情况是由于C ++的复杂性引起的问题,以及一些认为自己知道发生了什么的人,但是他们确实处于C ++置信度曲线的“带有类的C ”部分。

如果有选择,我宁愿在项目上使用C ++,但前提是团队必须精通该语言。当然,当然还要假设这不是一个8KμC项目,无论如何我都在有效地编写“ C”。


2
“ C ++置信度曲线”让我有些困扰。它的编写方式和注释暗示C ++绝望,失败的原因或其他原因。我想念什么吗?
Mike DeSimone 2010年

尝试一下,几年后再见。多数优秀的程序员带着反感而走到另一边。
马特·乔纳

3

几个观察

  • 除非您打算使用c ++编译器来构建C(如果坚持使用明确定义的C ++子集,这是可能的),否则您很快就会发现编译器在C中允许的东西,这将是C ++中的编译错误。
  • 不再有神秘的模板错误(是的!)
  • 没有(支持语言)面向对象的编程

C不支持模板并不意味着我们不需要“通用范式”,在C中,如果需要“通用范式”,则必须使用void *和宏来模仿模板。void*不是类型安全的,宏的错误也很糟糕,不比模板好。模板比宏更易于阅读和维护,而且类型安全。
StereoMatching

2

我使用C ++或混合使用C / C ++而不是纯C的原因几乎相同。我可以没有命名空间,但是如果代码标准允许,我会一直使用它们。原因是您可以用C ++编写更紧凑的代码。这对我来说非常有用,我用C ++编写服务器,但有时会崩溃。在这一点上,如果您正在查看的代码简短且包含内容,将大有帮助。例如,考虑以下代码:

uint32_t 
ScoreList::FindHighScore(
  uint32_t p_PlayerId)
{
  MutexLock lock(m_Lock); 

  uint32_t highScore = 0; 
  for(int i = 0; i < m_Players.Size(); i++)
  {
    Player& player = m_Players[i]; 
    if(player.m_Score > highScore)
      highScore = player.m_Score; 
  }

  return highScore; 
}

在C中看起来像:

uint32_t 
ScoreList_getHighScore(
  ScoreList* p_ScoreList)
{
  uint32_t highScore = 0; 

  Mutex_Lock(p_ScoreList->m_Lock); 

  for(int i = 0; i < Array_GetSize(p_ScoreList->m_Players); i++)
  {
    Player* player = p_ScoreList->m_Players[i]; 
    if(player->m_Score > highScore)
      highScore = player->m_Score; 
  }

  Mutex_UnLock(p_ScoreList->m_Lock);

  return highScore; 
}

没有什么不同。再多一行代码,但这往往会加起来。通常,您会尽力使它保持清洁和稀薄,但有时您必须做一些更复杂的事情。在这种情况下,您很重视行数。当您试图弄清广播网络突然停止传递消息的原因时,需要多看一遍。

无论如何,我发现C ++允许我以安全的方式执行更复杂的事情。


在C语言中,你不能这样做“的for(int i = 0”。
维克多

6
Victor虽然是有效的c99(或对于某些输入问题使用c ++编译器编译c)。
罗曼·泰切

我发现我不能相信安全。因此,您的互斥锁是有作用域的。现在,您不知道为什么异常应该“徘徊”通过它的原因。您甚至都不知道何时将其解锁,您代码的任何部分都可能决定其是否足够并抛出。这些额外的隐式“安全性”可能掩盖了错误。
马特·乔纳

Matt,我们确实知道为什么要解锁,通常情况下,当程序到达作用域的末尾时,互斥锁会解锁,我们不需要手工编写的代码来解锁,这是维护的噩梦。发生异常时,互斥锁将被解锁,我们可以捕获异常并从中读取错误消息。该错误消息是否足够好取决于您如何处理异常。
StereoMatching 2013年

0

我认为,为什么在嵌入式环境中更难以接受c ++的主要问题是因为缺乏了解如何正确使用c ++的工程师。

是的,同样的推理也可以应用于C,但是幸运的是,C中没有很多陷阱可以使自己陷入困境。另一方面,对于C ++,您需要知道何时不使用c ++中的某些功能。

总而言之,我喜欢c ++。我在O / S服务层,驱动程序,管理代码等上使用了它。但是,如果您的团队没有足够的经验,这将是一个艰巨的挑战。

我对两者都有经验。当团队的其他成员还没有做好准备时,那完全是一场灾难。另一方面,这是很好的经验。


0

是!我已经经历过这两种语言,而我发现C ++是更友好的语言。它具有更多功能。最好说C ++是C语言的超集,因为它提供了其他特性,例如多态性,互斥性,运算符和函数重载,用户定义的数据类型,而C语言并不真正支持这种情况。面向对象编程的帮助,这是从C转向C ++的主要原因。


C ++确实不是C的超集。编写将无法在C ++编译器上编译的C代码非常容易。在另一方面,目标C为(是?)℃的严格的超
无中生有

@exnihilo此处的约定超集是定义C ++具有更多功能。它还可以改善语法和语义,并减少错误机会。有些代码不是在C ++中编译的,而是可以在C中编译的,例如const int a;。这将在C ++中产生错误,因为有必要在声明时初始化常量,而在C中将编译概念。因此,超集不是像数学(A⊂B)那样固定的规则,而是C ++提供诸如面向对象概念之类的附加功能的近似值。
kaynat liaqat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.