ChatGPT解决这个技术问题 Extra ChatGPT

在 C 和 C++ 中都有效的代码在用每种语言编译时会产生不同的行为吗?

c++ c

C 和 C++ 有很多不同之处,并非所有有效的 C 代码都是有效的 C++ 代码。 (“有效”是指具有已定义行为的标准代码,即不是特定于实现/未定义/等。)

当使用每种语言的标准编译器编译时,在 C 和 C++ 中都有效的一段代码是否会产生不同的行为?

为了使它成为一个合理/有用的比较(我试图学习一些实际有用的东西,而不是试图在问题中找到明显的漏洞),让我们假设:

没有与预处理器相关的东西(这意味着没有 #ifdef __cplusplus、pragma 等的 hack)

任何实现定义在两种语言中都是相同的(例如数字限制等)

我们正在比较每个标准的最新版本(例如,C++98 和 C90 或更高版本)如果版本很重要,那么请说明每个标准的哪些版本会产生不同的行为。

顺便说一句,同时用 C 和 C++ 的方言编程会很有用。我过去和一个当前的项目都做过这个:TXR 语言。有趣的是,Lua 语言的开发者也做了同样的事情,他们称这种方言为“Clean C”。您可以从 C++ 编译器获得更好的编译时间检查和可能的额外有用诊断,同时保留 C 的可移植性。
我将较旧的问题合并到这个问题中,因为它有更多的观点和赞成的答案。这仍然是一个非建设性问题的例子,但它是非常边缘的,因为是的,它确实教会了 SO 用户一些东西。我关闭它只是为了反映合并前问题的状态。随意不同意并重新打开。
投票重新开放,因为我认为可以客观地回答“是”,然后是一个例子(如下所示)。我认为这是建设性的,因为人们可以从中学习相关的行为。
@AndersAbel 纯粹的答案数量,所有这些都是正确的,清楚地表明它仍然是一个清单问题。如果没有得到一份清单,你就不可能问这个问题。
@dmckee 对于它的价值,我同意你的看法。但是,C++ 标记的人是... 我们应该说... feisty

A
Ayxan Haqverdili

这是一个利用 C 和 C++ 中函数调用和对象声明之间的差异以及 C90 允许调用未声明函数的事实的示例:

#include <stdio.h>

struct f { int x; };

int main() {
    f();
}

int f() {
    return printf("hello");
}

在 C++ 中,这不会打印任何内容,因为创建和销毁了临时 f,但在 C90 中,它将打印 hello,因为可以在没有声明的情况下调用函数。

如果您想知道名称 f 被使用了两次,C 和 C++ 标准明确允许这样做,并且要创建一个对象,您必须说 struct f 以消除您想要结构的歧义,或者不使用 struct如果你想要这个功能。


严格来说,在 C 下这不会编译,因为“int f()”的声明是在“int main()”的定义之后:)
@Sogartar,真的吗? codepad.org/STSQlUhh C99 编译器会给您一个警告,但它们仍会让您编译它。
允许隐式声明 C 函数中的 @Sogartar。
@AlexB 不在 C99 和 C11 中。
@user529758 - 它们很可能是 C99 编译器。检测未声明标识符的 C99 需要将其视为语法错误,并且需要“发出诊断”;不需要编译文件失败。
A
Antti Haapala -- Слава Україні

对于 C++ 与 C90,至少有一种方法可以实现未定义的不同行为。 C90 没有单行注释。稍加注意,我们可以使用它来创建一个在 C90 和 C++ 中具有完全不同结果的表达式。

int a = 10 //* comment */ 2 
        + 3;

在 C++ 中,从 // 到行尾的所有内容都是注释,因此可以这样计算:

int a = 10 + 3;

由于 C90 没有单行注释,因此只有 /* comment */ 是注释。第一个 /2 都是初始化的一部分,所以结果是:

int a = 10 / 2 + 3;

因此,正确的 C++ 编译器将给出 13,但严格正确的 C90 编译器将给出 8。当然,我在这里选择了任意数字——您可以使用您认为合适的其他数字。


哇,这太令人兴奋了!!在所有可能的事情中,我永远不会想到评论可以用来改变行为哈哈。 +1
即使没有 2,它也会读取为有效的 10 / + 3(一元 +)。
现在为了好玩,修改它,使 C 和 C++ 都计算不同的算术表达式并计算相同的结果。
@RyanThompson 琐碎。 s/2/1/
@Mehrdad我错了还是评论与预处理器相关?因此,它们应该作为您问题的可能答案被排除在外! ;-)
D
Donald Duck

以下在 C 和 C++ 中有效,将(很可能)在 C 和 C++ 中导致 i 中的不同值:

int i = sizeof('a');

有关差异的说明,请参见 Size of character ('a') in C/C++

来自 this article 的另一个:

#include <stdio.h>

int  sz = 80;

int main(void)
{
    struct sz { char c; };

    int val = sizeof(sz);      // sizeof(int) in C,
                               // sizeof(struct sz) in C++
    printf("%d\n", val);
    return 0;
}

绝对没想到这个!我希望有一些更戏剧化的东西,但这仍然有用,谢谢。 :) +1
+1 第二个例子是一个很好的例子,因为 C++ 在结构名称之前不需要 struct
@Andrey我前段时间也这么想并对其进行了测试,它在没有std的情况下在GCC 4.7.1上工作,这与我的预期相反。这是 GCC 中的错误吗?
@SethCarnegie:不符合标准的程序不一定会失败,但也不能保证也能正常工作。
struct sz { int i[2];}; 表示 C 和 C++ 必须 产生不同的值。 (而 sizeof(int) == 1 的 DSP,可以 产生相同的值)。
v
vonbrand

C90 与 C++11(intdouble):

#include <stdio.h>

int main()
{
  auto j = 1.5;
  printf("%d", (int)sizeof(j));
  return 0;
}

在 C 中 auto 表示局部变量。在 C90 中,可以省略变量或函数类型。默认为 int。在 C++11 中 auto 意味着完全不同的东西,它告诉编译器从用于初始化它的值推断变量的类型。


@SethCarnegie:是的,它是一个存储类;当你省略它时,默认情况下会发生这种情况,所以没有人使用它,他们改变了它的含义。我认为默认情况下它是 int。这很聪明! +1
@KeithThompson 嗯? C11 仍然有 auto,从它的概念开始就在 C 中。
C11 没有隐式-int
@KeithThompson 啊,我猜你的意思是推断的int。尽管如此,在现实世界中,存在大量遗留代码,而市场领导者仍未实现 C99 并且无意这样做,因此谈论“过时的 C 版本”是荒谬的。
“每个变量都必须有一个明确的存储类。你真的,高层管理人员。”
g
godlygeek

另一个我还没有看到的例子,这个例子突出了预处理器的区别:

#include <stdio.h>
int main()
{
#if true
    printf("true!\n");
#else
    printf("false!\n");
#endif
    return 0;
}

这在 C 中打印“false”,在 C++ 中打印“true”——在 C 中,任何未定义的宏的计算结果为 0。在 C++ 中,有 1 个例外:“true”计算结果为 1。


有趣的。有谁知道这种变化背后的原因?
因为“真”是一个关键字/有效值,所以它像任何“真值”一样被评估为真(所以就像任何正整数一样)。您仍然可以执行 #define true false 以在 C++ 中打印“false”;)
#define true false ಠ_ಠ
@DarioOO 这种重新定义不会导致 UB 吗?
@DarioOO:是的,你错了。不允许重新定义关键字,惩罚留给命运(UB)。预处理器是一个单独的编译阶段,无法承受。
K
Kirill Kobelev

根据 C++11 标准:

一个。逗号运算符在 C 但不是 C++ 中执行左值到右值转换:

   char arr[100];
   int s = sizeof(0, arr);       // The comma operator is used.

在 C++ 中,该表达式的值为 100,而在 C 中,该值为 sizeof(char*)

湾。在 C++ 中,枚举器的类型是它的枚举。在 C 中,枚举数的类型是 int。

   enum E { a, b, c };
   sizeof(a) == sizeof(int);     // In C
   sizeof(a) == sizeof(E);       // In C++

这意味着 sizeof(int) 可能不等于 sizeof(E)

C。在 C++ 中,使用空参数列表声明的函数不接受任何参数。在 C 中,空参数列表意味着函数参数的数量和类型未知。

   int f();           // int f(void) in C++
                      // int f(*unknown*) in C

第一个也是实现定义的,就像 Alexey 的一样。但是+1。
@Seth,以上所有材料均直接取自 C++11 标准的附件 C.1。
是的,但它仍然是实现定义的。 sizeof(char*) 可能是 100,在这种情况下,第一个示例将在 C 和 C++ 中产生相同的可观察行为(即,虽然获得 s 的方法不同,但 s 最终会是 100)。 OP提到这种类型的实现定义的行为很好,因为他只是想避免语言律师的回答,所以第一个是他的例外。但无论如何,第二个都很好。
有一个简单的解决方法 - 只需将示例更改为:char arr[sizeof(char*)+1]; int s = sizeof(0, arr);
为避免实现定义的差异,您还可以使用 void *arr[100]。在这种情况下,元素与指向同一元素的指针大小相同,因此只要有 2 个或更多元素,数组就必须大于其第一个元素的地址。
P
Pavel Chikulaev

该程序在 C++ 中打印 1,在 C 中打印 0

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int d = (int)(abs(0.6) + 0.5);
    printf("%d", d);
    return 0;
}

发生这种情况是因为 C++ 中存在 double abs(double) 重载,因此 abs(0.6) 返回 0.6 而在 C 中它返回 0,因为在调用 int abs(int) 之前隐式转换为双整数。在 C 中,您必须使用 fabs 才能使用 double


不得不调试其他人的代码与该问题。哦,我多么喜欢那个。无论如何,您的程序也在 C++ 中打印 0 。 C++ 必须使用标题“cmath”查看比较第一个返回 0 ideone.com/0tQB2G 第二个返回 1 ideone.com/SLeANo
很高兴/很遗憾听到我不是唯一通过调试发现这种差异的人。刚刚在 VS2013 中测试过,如果扩展名为 .cpp,则只有包含此内容的文件的空文件将输出 1,如果扩展名为 .c,则输出 0。看起来 间接包含在 VS 中。
并且看起来像在 VS C++ 中, 将 C++ 的东西包含在全局命名空间中,而 GCC 则没有。但是不确定哪个是标准行为。
此特定代码示例依赖于实现:stdlib.h 仅定义 abs(int)abs(long);版本 abs(double)math.h 声明。所以这个程序可能仍然调用 abs(int) 版本。 stdlib.h 是否也会导致包含 math.h 是一个实现细节。 (我认为如果调用 abs(double) 将是一个错误,但不包括 math.h 的其他方面)。
第二个问题是,尽管 C++ 标准似乎确实说包含 <math.h> 还包括额外的重载;实际上,除非使用 <cmath> 形式,否则所有主要编译器都不包含这些重载。
A
Adam Rosenfield
#include <stdio.h>

int main(void)
{
    printf("%d\n", (int)sizeof('a'));
    return 0;
}

在 C 中,这会打印当前系统上 sizeof(int) 的任何值,在当今常用的大多数系统中通常是 4

在 C++ 中,这必须打印 1。


是的,我实际上很熟悉这个技巧,因为 'c' 在 C 中是一个 int,而在 C++ 中是一个 char,但在这里列出它仍然很好。
这将是一个有趣的面试问题——尤其是对于那些将 c/c++ 专家放在他们的简历上的人
不过有点卑鄙。 sizeof 的全部目的是让您不需要确切知道类型有多大。
在 C 中,值是实现定义的,1 是一种可能性。 (在 C++ 中,它必须按说明打印 1。)
实际上它在这两种情况下都有未定义的行为。 %d 不是 size_t 的正确格式说明符。
A
Alex B

另一个 sizeof 陷阱:布尔表达式。

#include <stdio.h>
int main() {
    printf("%d\n", (int)sizeof !0);
}

它在 C 中等于 sizeof(int),因为表达式是 int 类型,但在 C++ 中通常为 1(尽管不是必须的)。在实践中,它们几乎总是不同的。


一个 ! 应该足够一个 bool
!!是 int 到布尔转换运算符 :)
sizeof(0) 在 C 和 C++ 中都是 4,因为 0 是整数右值。 sizeof(!0) 在 C 中是 4,在 C++ 中是 1。逻辑 NOT 对 bool 类型的操作数进行操作。如果 int 值为 0,则它被隐式转换为 false(一个布尔右值),然后它被翻转,产生 truetruefalse 都是 C++ 中的布尔右值,sizeof(bool)1。但是在 C 中,!0 的计算结果为 1,它是 int 类型的右值。 C 编程语言默认没有 bool 数据类型。
P
Peter Mortensen

一个依赖 C 编译器的老栗子,无法识别 C++ 行尾注释......

...
int a = 4 //* */ 2
        +2;
printf("%i\n",a);
...

D
Donald Duck

C++ 编程语言(第 3 版)给出了三个示例:

sizeof('a'),正如@Adam Rosenfield 提到的那样; // 用于创建隐藏代码的注释: int f(int a, int b) { return a //* blah */ b ;结构等将东西隐藏在范围之外,如您的示例所示。


J
Johannes Schaub - litb

C++ 标准列出的另一个:

#include <stdio.h>

int x[1];
int main(void) {
    struct x { int a[2]; };
    /* size of the array in C */
    /* size of the struct in C++ */
    printf("%d\n", (int)sizeof(x)); 
}

所以你得到填充差异?
啊抱歉,我知道了,顶部还有另一个 x。我以为你说的是“数组 a”。
M
M.M

C 中的内联函数默认为外部作用域,而 C++ 中的内联函数则没有。

在 GNU C 的情况下,将以下两个文件一起编译将打印“我是内联的”,但对于 C++ 则没有。

文件 1

#include <stdio.h>

struct fun{};

int main()
{
    fun();  // In C, this calls the inline function from file 2 where as in C++
            // this would create a variable of struct fun
    return 0;
}

文件 2

#include <stdio.h>
inline void fun(void)
{
    printf("I am inline\n");
} 

此外,C++ 将任何全局 const 隐式视为 static,除非它显式声明为 extern,这与 C 中 extern 是默认值不同。


我真的不这么认为。可能你错过了重点。这与 struct st 的定义无关,它仅用于使代码有效 C++。关键是它突出了 c 与 c++ 中内联函数的不同行为。同样适用于外部。在任何解决方案中都没有讨论这些。
此处演示的内联函数和 extern 的不同行为是什么?
它写得很清楚。 “c 中的内联函数默认为外部作用域,而 c++ 中的内联函数不是(代码显示)。C++ 也隐式将任何 const 全局视为文件作用域,除非它显式声明为 extern,这与 C 中的默认值不同。类似可以为此创建示例”。我很困惑——这不是可以理解的吗?
@fayyazkl 显示的行为只是因为查找的差异(struct funfn),与函数是否内联无关。如果您删除 inline 限定符,结果是相同的。
在 ISO C 中,这个程序的格式不正确:直到 C99 才添加 inline,但在 C99 中,如果没有范围内的原型,则不能调用 fun()。所以我认为这个答案只适用于 GNU C。
佚名
struct abort
{
    int x;
};

int main()
{
    abort();
    return 0;
}

在 C++ 中返回退出代码 0,在 C 中返回 3。

这个技巧可能可以用来做一些更有趣的事情,但我想不出一个好的方法来创建一个适合 C 的构造函数。我试着用复制构造函数做一个类似无聊的例子,它会让一个参数被通过,尽管是以一种相当不便携的方式:

struct exit
{
    int x;
};

int main()
{
    struct exit code;
    code.x=1;

    exit(code);

    return 0;
}

但是,VC++ 2005 拒绝在 C++ 模式下编译它,抱怨“退出代码”是如何重新定义的。 (我认为这是一个编译器错误,除非我突然忘记了如何编程。)当编译为 C 时,它以进程退出代码 1 退出。


不幸的是,您使用 exit 的第二个示例无法在 gcc 或 g++ 上编译。不过,这是个好主意。
exit(code) 显然是 exit 类型的变量 code 的有效声明。 (请参阅“最令人烦恼的解析”,这是一个不同但相似的问题)。
w
wefwefa3
#include <stdio.h>

struct A {
    double a[32];
};

int main() {
    struct B {
        struct A {
            short a, b;
        } a;
    };
    printf("%d\n", sizeof(struct A));
    return 0;
}

该程序在使用 C++ 编译器编译时打印 128 (32 * sizeof(double)),在使用 C 编译器编译时打印 4

这是因为 C 没有范围解析的概念。在 C 中,包含在其他结构中的结构被放入外部结构的范围内。


这个很有趣! (我认为您的意思是 32*sizeof(double) 而不是 32 :))
请注意,您将在 printing size_t with %d 之前获得 UB
u
user23614

不要忘记 C 和 C++ 全局命名空间之间的区别。假设你有一个 foo.cpp

#include <cstdio>

void foo(int r)
{
  printf("I am C++\n");
}

和一个 foo2.c

#include <stdio.h>

void foo(int r)
{
  printf("I am C\n");
}

现在假设你有一个 main.c 和 main.cpp,它们看起来像这样:

extern void foo(int);

int main(void)
{
  foo(1);
  return 0;
}

当编译为 C++ 时,它将使用 C++ 全局命名空间中的符号;在 C 中,它将使用 C 之一:

$ diff main.cpp main.c
$ gcc -o test main.cpp foo.cpp foo2.c
$ ./test 
I am C++
$ gcc -o test main.c foo.cpp foo2.c
$ ./test 
I am C

你的意思是链接规范?
名称修改。 C++ 名称有前缀和后缀,而 C 没有
名称修饰不是 C++ 规范的一部分。 C语言禁止吗?
这是未定义的行为(foo 的多重定义)。没有单独的“全局命名空间”。
A
Antti Haapala -- Слава Україні
int main(void) {
    const int dim = 5; 
    int array[dim];
}

这是相当特殊的,因为它在 C++ 和 C99、C11 和 C17 中有效(尽管在 C11、C17 中是可选的);但在 C89 中无效。

在 C99+ 中,它创建了一个可变长度数组,它比普通数组有自己的特点,因为它具有运行时类型而不是编译时类型,并且 sizeof array 在 C 中不是整数常量表达式。在 C++ 中,类型是完全静态的。

如果您尝试在此处添加初始化程序:

int main(void) {
    const int dim = 5; 
    int array[dim] = {0};
}

是有效的 C++ 但不是 C,因为可变长度数组不能有初始值设定项。


G
Galaxy

这涉及 C 和 C++ 中的左值和右值。

在 C 编程语言中,前自增和后自增运算符都返回右值,而不是左值。这意味着它们不能位于 = 赋值运算符的左侧。这两个语句都会在 C 中给出编译器错误:

int a = 5;
a++ = 2;  /* error: lvalue required as left operand of assignment */
++a = 2;  /* error: lvalue required as left operand of assignment */

然而,在 C++ 中,前自增运算符返回一个左值,而后自增运算符返回一个右值。这意味着带有前置自增运算符的表达式可以放在 = 赋值运算符的左侧!

int a = 5;
a++ = 2;  // error: lvalue required as left operand of assignment
++a = 2;  // No error: a gets assigned to 2!

为什么会这样?后增量使变量递增,并返回发生增量之前的变量。这实际上只是一个右值。变量 a 的前一个值作为临时值复制到寄存器中,然后将 a 递增。但是 a 的前一个值是由表达式返回的,它是一个右值。它不再代表变量的当前内容。

预增量首先增加变量,然后返回发生增量后的变量。在这种情况下,我们不需要将变量的旧值存储到临时寄存器中。我们只是在变量增加后检索变量的新值。所以预增量返回一个左值,它返回变量 a 本身。我们可以使用将此左值分配给其他东西,就像下面的语句。这是从左值到右值的隐式转换。

int x = a;
int x = ++a;

由于预增量返回一个左值,我们也可以给它赋值。以下两个陈述是相同的。在第二个赋值中,首先 a 增加,然后它的新值被 2 覆盖。

int a;
a = 2;
++a = 2;  // Valid in C++.

这里没有“在 C 中有效”。
D
Daniel Frużyński

空结构在 C 中的大小为 0,在 C++ 中为 1:

#include <stdio.h>

typedef struct {} Foo;

int main()
{
    printf("%zd\n", sizeof(Foo));
    return 0;
}

不,区别在于 C 没有空结构,除了作为编译器扩展,即此代码不匹配“在 C 和 C++ 中都有效”