在C语言中有哪些操作是C++无法实现的?

10

在C语言中可以做但在C++中不能做的事情有哪些,你在编写C++代码时最错过哪些特性?

我能想到的几个:

  1. 在C中,我们可以将任何类型的指针分配给void指针而无需使用强制转换,但在C++中不行。
  2. 声明变量名为C++关键字,而这在C中则可以。

编辑:感谢 @sbi 指出:
1. 应该是:在C中,我们可以将void指针分配给任何类型的指针,但在C++中不行。


9
这里有一个建议:每天对项目进行多次编译。 - imgx64
3
@Als: C++编译速度非常慢,稍微大一点的项目可能需要几个小时来编译,特别是如果使用了很多模板、STL和Boost。实际上,许多地方使用专门的编译集群,这是荒谬的。C编译速度也比较慢,但与C++相比还算合理。 - imgx64
2
@imgx64:肯定有一些人不知道如何良好地构建C++项目,最终导致出现这样的问题,还有一些库,比如Boost Spirit,会将编译器弯曲成大量递归和复杂操作,耗费时间,或者像Java一样为每个10行类创建一个头文件/实现文件,但是STL的正常使用和设计良好的项目具有平衡的编译依赖性意识,也许有一些“瓶颈”地方采用pImpl或未模板化的外部函数包装模板使用等。 - Tony Delroy
7
我的项目有10万行代码,从零开始编译只需要4分钟。你是在ZX Spectrum上编译C++代码吗?(意为对方的编译速度非常慢) - paercebal
4
三倍经验。在大规模的项目中,我看不到明显的区别。 - Martin York
显示剩余9条评论
13个回答

37

注意:我猜我会因为这个问题而受到责备,但是,毕竟这是一个针对C++开发人员的C++问题,所以...

在C中可以做什么而在C ++中无法做什么?你在使用C ++编码时最想念哪些特性?

作为一名C++开发人员,我不会错过C的任何东西,无论是C99还是其他。

我并不是出于恶意写这篇文章。这是一个问题,针对那些错过了C/C99某些特性的C++开发人员,因为他们忽略了C++的基本特性。我确信这个问题及其答案忽略了C++中可行或更好的替代方案(不,"C++向量很讨厌"的评论只是一个虚假的理由)。

这就是为什么我将在这里讨论每一个所谓的“缺失功能”...

可变长度数组?

可变长度数组是C99的一种语言特性。它的主要优点是:

  1. 在堆栈上分配
  2. 创建时长度可变
  3. 无需解除分配

对于大多数常见情况,std::vector可以胜任工作,并且具有更多的功能。例如,除非我错了,可变长度数组具有以下缺点:

  1. 在堆栈上分配意味着无法从声明了它的函数中返回VLA
  2. VLA 无法调整大小,这意味着如果太小,则会出现问题
  3. VLA 必须在原型范围或块范围内声明。它不能是 extern 或 static。您不能将其声明为结构的成员。
  4. 它不能有初始化程序

向量可以调整大小,并且可以返回。使用 C++0x(和右值引用),您可以使用移动语义返回向量,这意味着不需要无用的临时对象。它可以放在结构/类中,可以是 extern、static。您可以使用默认值,数组内容,容器或者使用 C++0x 使用初始化列表进行初始化。

即使在这之后,如果您真的需要像 VLA 一样的东西,在 C++ 中,平均 C++ 开发人员可以编写基于堆栈的类似向量的容器。而且不需要完整的语言委员会更新。

只是为了好玩,我恰好发布了一个 answer with a simple proof-of-concept of a C++ VLA-like class.

C ++ 的向量大多数情况下是更好的选择,具有更多功能。在真正需要 VLA 的罕见情况下,可以通过用户定义的类来模拟其功能。

void * 强制转换为 T *

关于将任何void *转换为另一种类型指针,这不是C语言缺少而C++具有的功能:这是弱类型与强类型之间的选择。
在C++中做到这一点并不困难,你可以使用强制转换。这种差异的重点在于减少语言中错误风险,其中void *不像其他语言那样有用:在我的当前的C++ 100k行项目中,我没有出现过void *的情况。
指定初始化程序?构造函数提供了更好的替代方案。
当然,你无法直接初始化结构体中的数据,但是,数据封装意味着大多数时候我的对象数据都是私有的,因此,使用指定初始化器来初始化它们的整个概念就是荒谬的。
至于类似POD的结构,编写构造函数很容易,并且可以处理指定初始化器永远不会处理的情况(例如默认情况下使用非零值初始化成员甚至调用函数)。
由于C++专注于数据封装,因此构造函数提供了指定初始化器的更好替代品。
编辑2011-11-05:
重新阅读本节后,我想澄清一点:指定初始化程序对于非常有限的情况(即PODs)可能是有用的,这意味着虽然我不需要它们(如问题所问),但我不介意拥有它们。
复合字面量?

这种语法糖假设您知道结构体的确切实现并具有其成员的公共访问权限,在C++中通常应避免这种情况。

再次声明,复合字面值不是无法通过函数、方法甚至构造函数处理的,具有上述附加功能的优势

如何声明C++中关键字但非C中关键字的变量名?

我了解你的感受:每当我有可能在C++中使用interfacefinalsynchronized时,我也会感到Java的颤动...

:-P

类型通用宏?

C中的问题在于您有很多函数对不同类型执行相同的语义操作,这意味着每个函数必须有不同的名称。例如,根据OpenGroup的说法,存在以下函数:

  • double sin(double x);
  • float sinf(float x);
  • long double sinl(long double x);
  • 等等。

来源:http://www.opengroup.org/onlinepubs/009695399/functions/sin.html

但是它们的名称真让人头疼,所以有人有了一个想法。大致上是使用编译器内置扩展调用正确的函数,具体根据所用参数的类型。

这就是C99中<tgmath.h>的神奇之处。

而且这个想法看起来如此awesome以至于他们甚至提出了一个建议,在下一个C标准中为所有函数提供这个功能,大概是这样:

#define sin(x) __tgmath(x,,,     \
float, sinf, long double, sinl,  \
/* etc. */                       \
, , sin)(x)

来源: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1340.htm

现在,令人震惊的消息是:自几十年前以来,C ++ 就已经具备了这个功能:这被称为函数重载。

例如,上面的函数在 C++ 中声明如下:

  • double sin (double x );
  • float sin (float x );
  • long double sin (long double x );
  • 等等。

因此,“类型通用宏”是一种破解实现,旨在(部分地)模拟更通用的 C++ 函数重载。

猜猜看:您甚至可以为自己定义的用户定义类型添加自己的重载。

结论

正如上面所示,每次我学习 C99 功能时,结论都是:“嘿,我已经可以在 C++ 中做到这一点了!”(并且通常在句子中某处有“更好”的词语)。

说真的,作为一个 C++ 开发者,我现在错过的是能够在工作中使用 C++0x。例如,以下 C++0x 功能:

  • auto
  • constexpr
  • 初始化列表
  • r-value 引用
  • lambda 表达式
  • nullptr
  • 等等。
整个“C++缺少的C特性”是一个被高估的概念,我怀疑对于C开发者(以及使用C++编写类的C开发者)比C++开发者更有趣。

+1 我个人不是很喜欢C ++,但我有些同情。 C99具有的所有功能,而C ++没有的功能都不能被认为是你真正会错过的东西。 所以你不能隐式地转换为void指针? 哦,不,最好取消项目...或者输入七个额外的字符(void*)(或者现在C ++程序员所做的任何内容来执行强制转换)。 - JeremyP
2
@Jeremy:在 C++ 中,隐式转换禁止从 void* 进行转换,而不是向其进行转换。 - sbi
1
@Jeremy:只是你在这个帖子中第二次说将类型转换为void*在C++中受到限制。那是错误的。限制的是从void*进行类型转换,而且有很好的理由。 - sbi
@kriss:我认为你的情况属于“无法将void *转换为T *”类别,也就是C语言弱类型安全性与C++强类型安全性之间的差异。在你的情况下,一个简单的宏/内联函数将会让你的生活更轻松(就像Windows中使用的_T宏一样,用于在char和wchar_t字符串字面值之间进行切换)。 - paercebal
@paercebal:我可以接受这个问题。由于这个问题仅出现在某些类的外部API中,我通常使用重载来解决它(例如,创建两个函数,一个使用uint8_t *作为输入,另一个使用char *,一个调用另一个并进行强制转换)。但是我不能说我对C ++关于符号管理的整体行为感到满意。为什么允许有符号/无符号数字类型之间的转换仅发出警告,但当通过指针访问时却使其成为错误?在我看来,两者都应该是警告或错误,C在这方面更加一致。 - kriss
显示剩余10条评论

17
你可能会觉得这个网页“ISO C和ISO C++之间的不兼容性”很有趣。
我最想要但C++中没有的一些C99特性包括:
  • 复合字面量;
  • 指定初始化器;
  • 可变参数宏(已包含在C++0X中)。

1
@Als:什么?这是一篇冗长的C++文章,发布在某个知道自己在说什么的人的私人网站上,对于那些喜欢Facebook等社交媒体的人来说可能太过枯燥。就连致谢名单上也有一半的人都是C++社区中著名的人物。哦天啊,找份新工作吧,你现在的工作只会妨碍你的工作进展。 - sbi
1
@Als:网络连接被审查对开发人员来说真的是非常限制。我和@sbi一样持这种观点。我希望他们能够给你足够的报酬来弥补这一点。 - Matt Joiner
2
不要忘记限制,VLA,类型通用宏(tgmath.h)。 - Nyan
1
@Nyan:是的,但问题是关于我错过的功能。我差点添加了restrict,但我个人不会错过VLA和<tgmath.h>。 - schot
1
@Als/@Matt/@sbi:我必须通过Google缓存阅读我的一半页面 :-/. - Tony Delroy
显示剩余9条评论

15

C语言本身并没有这个特性,但可以说作为C语言的杀手级特性之一的是,C89的简单语法使得编写编译器变得容易。与编写C++编译器相比,这尤其如此。


1
使用C++,如果你愿意的话,几乎可以以完全相同的方式编写编译器。 - Kaz Dragon
@Kaz Dragon:不,他的意思是用任何语言编写C++编译器比编写C编译器更困难,因为C的语法更简单。 - JeremyP
10
我同意,不过有趣的是,C++ 大部分笨拙的语法都源于它旨在保持向后兼容性的事实 :-) - Matthieu M.
@JeremyP - 哦,对了。我没有那样阅读它。我认为用C语言编写编译器更容易。nm - Kaz Dragon
2
我认为,这种语言的简单性不仅有利于编译器的编写者,而且还有利于其他人。 - Nick Van Brunt

4
在C语言中,我们可以将任何类型的指针赋值给void指针而无需进行转换,但在C++中则不行。在C++中,任何类型的指针都可以转换为void指针,但这样会丢失信息,编译器也不会阻止你这样做。相反,如果反过来,你获得了编译器无法检查的信息,那么这就是一个问题。我认为C允许这样做,而C++则绝对不行。

我想知道为什么这个被踩了。它有什么问题吗? - sbi

3
如果我们忽略明显的差异源 - C99 - 限制自己在C89/90,并且也舍弃像C++关键字这样的琐碎变体,仍然会有一些C和C++之间的差异。
(1) 您已经提到了能够将void *转换为任何具体指针类型而无需进行强制转换的能力。
(2) 具有“未指定”参数的函数类型,即函数类型声明中的()。在C中,您可以这样做。
void foo(int, int);
void bar(double);

int main() {
  void (*pf)();

  pf = foo;
  pf(1, 2); /* valid call */

  pf = bar;
  pf(5.0);  /* valid call */
}

在C++中是不可能实现这一点的。当然,也可以说通用的非原型函数声明是C的特性,而不是C++的特性(同样适用于C99)。

(3) 使用字符串字面值进行数组初始化时存在一些差异:在C中,尾随的\0允许被省略,但在C++中不允许。

char str[2] = "ab"; /* valid C, not valid C++ */

(4) 在C语言中,有一些初步定义,尽管它们大多数情况下没有实际意义。

(5) 另一个大多数情况下不重要的“特性”:在C语言中,您可以使用返回值函数,但是忘记实际返回任何内容。

int foo() {
}

这段代码在C和C++中都是合法的,但在C++中,这样的函数会无条件地产生未定义行为。在C中,只有当您尝试使用返回值时,该函数才会产生未定义的行为。

foo(); /* fine in C, undefined behavior in C++ */

(6) 如果我想起来,我会随后添加一些其他的内容。


你真的想念那些功能吗? - paercebal
@paercebal:小姐?我现在主要使用C语言,所以我不会“想念”它们。无论如何,在OP的上下文中,是否有人想念这些功能都不是重点。 - AnT stands with Russia
我的错。有两个问题:问题是“在C中可以做什么而在C++中不能做,你在使用C++编码时最想念哪些功能?”...我猜我关注了“想念”的部分,读成了“在C中可以做但在C++中不能做的事情,你在使用C++编码时最想念的是什么”... - paercebal
1
@paercebal:好的。嗯,通常我在我的C代码中并不真正使用这些功能(除了void *转换),所以如果我在C++中工作,我可能不会错过它们中的任何一个。 - AnT stands with Russia
在我的 C 代码中,void * 转换被广泛使用。 - Matt Joiner

2
在C语言中,你可以隐式地在void指针和其他指针之间进行转换,但是在C++中必须进行显式转换。
void* void_ptr;
int* int_ptr;

int_ptr = void_ptr;  // Invalid in C++, but not in C
void_ptr = int_ptr;  // Valid in C and C++
void_ptr = (void*)int_ptr; // Valid in C and C++
int_ptr = (int*)void_ptr;  // Valid in C and C++

这样吗?我得再确认一下。 - Alexander Rafferty
4
在C++中,执行 b = a 的操作是非法的。 - Adam Bowen

2
在C语言中,您可以使用可变长度数组,但在C++中不行。我相信这会比使用new[]更加有用。

6
不过,C++ 有 std::vector<> - sbi
3
vector 在堆上分配内存,而可变长度数组(VLA)可能在栈上分配内存。由于大多数情况下堆上的内存分配比栈上的更昂贵,因此我相信这会很有用。 - Naveen
8
@Matt:这是一种让你希望有评论踩的评论。你毫无理由地进行指责。对此我该怎么办呢?只能将其忽略掉了。 - sbi
4
@Matt:我不确定向量如何对你施加压力。如果你不需要调整大小,那么 std::array 就适合你。此外,我不知道 std::vector 如何在处理非 POD(平凡标量类型) 和 POD 的方面有所不同(除了一些实现可能基于这种区别进行优化)。作为一个教授 C++ 多年的人,我可以证明初学者似乎比 C 数组更容易理解 std::vector。最后,我还没有看到测量结果表明 std::vector 比 C 数组慢。 - sbi
3
“向量是数组的有限实现”这种说法是不正确的。向量是一种对象,可以模拟数组,并具有额外的功能。至于性能问题,最后一次我分析代码时,strlen是罪魁祸首,而不是 C++ 的向量或字符串。 - paercebal
显示剩余10条评论

2

在语法糖和类型滥用方面存在一些微妙的差异,但是它们可以轻松解决。

C语言最重要的能力是生成完全独立于外部依赖的程序。这就是为什么操作系统内核几乎普遍采用C语言编写的原因。实际上,C语言是专门为实现操作系统而设计的。虽然有可能使用C++的受限子集来编写操作系统内核,但只有在链接时才会或根本不会强制执行这些限制,因此与细微的语法差异相比,更加麻烦。


1
在C语言中,我喜欢的是能够表达类似 a = b; 这样的东西,并且确切地知道它在做什么。但在C++中,任何人都可以重载运算符,这意味着像这样简单的语句可能会调用一些大型的复制构造函数(或更糟糕的是,一些完全无关的东西)。当你在C++中看到 a = b;时,你不得不去猜测(或查找)是否有人只是想捣乱。

1
这是一种“世界充满了破坏者!”的综合症。你不信任你的同事(或你的库提供者),也不信任语言本身。如果你和那些会做一些与语义上下文无关的事情的人一起工作(比如在重载+时编写一个-),而他们这样做只是为了恶心你,那么你应该另找一份工作。和正常人一起用C++编码可以非常启发人:在我10多年的职业生涯中,我从未和你描述的白痴一起工作过,当一些代码效率低下时,大多数情况下,它并不在操作符重载代码中。 - paercebal
现在,a = b 的例子是一个很好的例子。在 C++ 中,这意味着 a 将成为 b 的一个副本。如果 b 是一个大对象,那么你可以肯定这个复制操作会很昂贵。这里重要的部分是“如果 b 是一个大对象”。如果我们忽略“破坏者”阴谋论,这意味着在 C++ 中,类型是重要的,而在 C 中,函数是重要的。这就是为什么我写了“相信语言”的原因。为了在 C++ 中正确地工作,你需要从“过程式”的视角转换到“对象应该如何行为”的视角。如果没有这个转换,那么你所看到的都是敌对的代码。 - paercebal
我的问题更多的是,我无法查看低效的代码并“看到”它的低效之处,因为C++和OOP提供了抽象。 (我不指望破坏,但我确实希望很多时候它们会让CPU跳过更多不必要的步骤来满足我的特定需求。)同时,我也不能完全信任抽象,因为在某些情况下(例如std::endl?),它可能会导致更多的浪费。在C++中,你必须走过一个奇怪的中间地带,关心一些细节而忽略其他细节,而在C中,陷阱所在更加清晰。 - cHao
@cHao:你的“我看不惯低效的代码还能发现它低效”的模式只适用于C运算符。对于其他所有代码,它都是无用的。在C中,这意味着大部分代码,因为按定义,运算符几乎什么都不做。结论是,与其他人一样,如果你需要测量某些代码的性能,你需要对其进行剖析(或查看源代码并猜测)。 - paercebal
@paercebal:在C++中,这可能意味着所有代码。C运算符“几乎什么都不做”的事实可能是一件好事。当您没有将函数伪装成运算符时,这意味着要担心的事情更少。别误会我; 我倾向于喜欢运算符重载可以提供的表现力。但同时,它破坏了“运算符几乎什么都不做”的规则。而这个规则是我喜欢C的原因之一:a + ba = ba(b)就是它看起来的样子,并且不是一个巨大的操作,并且不会根据它所操作的内容而改变其含义。 - cHao
@cHao:你说得对,关于“...不会因为它所操作的对象而改变其含义”,这又是另一种说法,“在C++中,类型很重要,而在C中,函数很重要”。两种语言有如此多的共同点,最终却变得如此不同,这真是奇怪。我想这种“弱类型与强类型”和/或“类型与函数”的差异是我们意见不合的原因,尽管在每种语言的上下文中我们都有些许正确。 - paercebal

0

C99 中被忽略的一个特性是 VLA,而 C++ 据说没有相应的功能。

甚至有人质疑在 C++ 中编写类似 VLA 的对象是否可能。

这就是我添加这个答案的原因:尽管它略微偏离主题,但仍然表明,通过正确的库,C++ 开发人员仍然可以访问模仿 C99 特性的对象。因此,这些特性并不像人们想象的那样被忽视。

主要代码如下:

#include <iostream>
#include <string>
#include "MyVLA.hpp"

template <typename T>
void outputVLA(const std::string & p_name, const MyVLA<T> & p_vla)
{
    std::cout << p_name << "\n   MyVla.size() : ["
              << p_vla.size() << "]\n" ;

    for(size_t i = 0, iMax = p_vla.size(); i < iMax; ++i)
    {
        std::cout << "   [" << i << "] : [" << p_vla[i] << "]\n" ;
    }
}

int main()
{
    {
        MY_VLA(vlaInt, 5, int) ;

        outputVLA("vlaInt: Before", vlaInt) ;

        vlaInt[0] = 42 ;
        vlaInt[1] = 23 ;
        vlaInt[2] = 199 ;
        vlaInt[3] = vlaInt[1] ;
        vlaInt[4] = 789 ;

        outputVLA("vlaInt: After", vlaInt) ;
    }

    {
        MY_VLA(vlaString, 4, std::string) ;

        outputVLA("vlaString: Before", vlaString) ;

        vlaString[0] = "Hello World" ;
        vlaString[1] = "Wazaabee" ;
        vlaString[2] = vlaString[1] ;
        vlaString[3] = "Guess Who ?" ;

        outputVLA("vlaString: After", vlaString) ;
    }
}

正如您所见,MyVLA对象知道其大小(这比在C99 VLAs上使用sizeof运算符要好得多)。

当然,MyVLA类的行为就像一个数组,并且由size_t值初始化(可以在运行时更改)。唯一的问题是由于alloca()函数的性质,这意味着构造函数只能通过宏MY_VLA间接使用:

下面是文件MyVLA.hpp中该类的代码:

#include <alloca.h>

template <typename T>
class MyVLA
{
    public :
        MyVLA(T * p_pointer, size_t p_size) ;
        ~MyVLA() ;

        size_t          size()                          const ;
        const T &       operator[] (size_t p_index)     const ;
        T &             operator[] (size_t p_index) ;

    private :
        T * m_begin ;
        T * m_end ;
} ;

#define MY_VLA(m_name, m_size, m_type)                                                      \
m_type * m_name_private_pointer = static_cast<m_type *>(alloca(m_size * sizeof(m_type))) ;  \
MyVLA<m_type> m_name(m_name_private_pointer, m_size)

template <typename T>
inline MyVLA<T>::MyVLA(T * p_pointer, size_t p_size)
{
    m_begin = p_pointer ;
    m_end = m_begin + p_size ;

    for(T * p = m_begin; p < m_end; ++p)
    {
        new(p) T() ;
    }
}

template <typename T>
inline MyVLA<T>::~MyVLA()
{
    for(T * p = m_begin; p < m_end; ++p)
    {
        p->~T() ;
    }
}

template <typename T>
inline size_t MyVLA<T>::size() const
{
    return (m_end - m_begin) ;
}

template <typename T>
inline const T & MyVLA<T>::operator[] (size_t p_index) const
{
    return *(m_begin + p_index) ;
}

template <typename T>
inline T & MyVLA<T>::operator[] (size_t p_index)
{
    return *(m_begin + p_index) ;
}

宏定义有些混乱,可能需要重新编写。该类本身可能不是异常安全的,但可以进行改进。无论如何,为了使其可用(即处理复制/赋值,使new/delete私有化,在可能的情况下添加const等),需要更多的代码。我猜测,投入时间并不能得到相应的回报。因此,它将保持概念验证状态。
重点是“C++可以模拟C99 VLAs,并且甚至可以作为C++对象数组工作!”我想我成功地证明了这一点。
我让读者复制粘贴代码并编译以查看结果(我在Ubuntu 10.04上的g++ 4.4.3上编译了它)。

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