C 是用于开发系统软件和任何微处理器软件的经典语言。 linux,大部分windows和macOS都是写在上面的。 如果你使用任何现代可穿戴小工具或电子设备,在大多数情况下它们也会在 C 程序的控制下运行。 世界上有大量的代码是用 C 语言编写的,而且还会有更多。
C++ 是那些同时需要 C 的所有功能和面向对象编程的灵活性的人的选择。 Counter-Strike、StarCraft 和 World of Warcraft 是用 C++ 编写的,这意味着您可以将 C 的性能与现代技术相结合。 Unity 引擎的一部分也是用 C++ 编写的,以直接访问系统内存和资源。
简单描述一下这些语言的区别,C++是C的改进版。这些语言99%的语法和命令都是一样的,只是C更多的是结构化和过程化编程,而C++是面向对象的。
在本文中,我将分享一个 C 代码示例列表,这些示例不是 C++ 正确的或表现出某些 C 特定行为。 请注意,它是一个方向:C 代码,从 C++ 的角度来看,这是不正确的。
当然,C 语言与 C++ 语言有许多显着差异,任何人都不难举出基于关键字或其他明显的 C99 独有特性的不兼容示例。 您不会在此列表中找到它。 我选择示例的主要标准是,对于 C++ 观察者来说,代码在第一眼看来应该足够“无辜”(即,不包含明显的 C 独占代码),但仍然是特定于 C 语言的。
*对于 [C23],我将标记与 C23 发布无关的项目。
1.在C中,在用字符串文字初始化字符数组时允许“丢失”尾随的 :
char s[4] = "1234";
在 C++ 中,这样的初始化是不正确的。
2. C 支持暂定义。 在一个翻译单元中,您可以在没有初始化器的情况下对同一对象进行多个外部定义:
int a;
int a;
int a, a, a;
C++ 中不允许这样的多重定义。
3. C 语言允许定义不完整类型的外部对象,前提是该类型在同一翻译单元的某个地方被重新定义并变得完整:
struct S s;
struct S { int i; };
在基本原理层面,这种可能性很可能只是前一段的结果,即支持暂定定义。从 C++ 的角度来看,上述声明顺序是不正确的:C++ 语言立即禁止定义不完整类型的对象 .
4. 在C 中,可以使不完整类型的非定义实体声明为void。
extern void v;
但是在C中不能做相应的定义,因为void是一个不完整的类型。在C++中,你甚至不能做非定义声明。
5. C 语言允许使用 const 限定符定义变量而无需显式初始化:
void foo(void)
{
const int a;
}
在 C++ 中,这样的定义是不正确的。
6. C 语言允许在 cast 运算符、sizeof 运算符和函数声明(返回类型和参数类型)中声明新类型:
int a = sizeof(enum E { A, B, C }) + (enum X { D, E, F }) 0;
/* 下面的代码使用上面的声明 */
enum E e = B;
int b = e + F;
C++ 中不允许这样的声明。
7. 在 C 中,函数参数列表中提到的“不熟悉的”结构类型名称是该函数本地新类型的声明。 同时,在函数参数列表中,可以将此类型声明为不完整的,对函数体中已有的完整类型进行“追加声明”:
void foo(struct S *p)
{
struct S { int a; }s;
p = &s;
p->a = 5;
}
在这段代码中,从 C 语言的角度来看,一切都是正确的:p 与 &s 具有相同的类型,并且包含字段 a。
从 C++ 语言的角度来看,在函数参数列表中提到一个“陌生”的类类型名称也是一种新类型的声明。 然而,这个新类型不是本地的:它被认为属于封闭的命名空间。 因此,从C++语言的角度来看,函数体中类型S的局部定义与参数列表中提到的类型S无关。 由于类型不匹配,分配 p = &s 是不可能的。 从 C++ 的角度来看,上面的代码是不正确的。
8. C 语言允许将控制转移到“跳过”其初始化声明的自动变量的范围:
switch (1)
{
int a = 42;
case 1:;
}
从 C++ 的角度来看,这种控制转移是不允许的。
9. 自C99 以来,C 语言中出现了隐式块:一些语句本身就是块,此外,还引入了嵌套的子块。 例如,for 循环本身就是一个块,循环体是嵌套在 for 循环块中的一个单独的块。 因此,下面的代码在 C 中是合法的:
for (int i = 0; i < 10; ++i)
{
int i = 42;
}
在循环体中声明的变量 i 与在循环头中声明的变量 i 无关。
在 C++ 语言中,在这种情况下,循环头和循环体都形成一个单一的作用域,这就排除了 i 的“嵌套”声明的可能性。
10. C 语言允许在不声明任何对象的声明中使用无意义的存储类说明符:
static struct S { int i; };
这在 C++ 中是不允许的。
此外,您会注意到在 C 语言中,typedef 在形式上也只是存储类说明符之一,它允许您创建没有声明别名的无意义的 typedef 声明:
typedef struct S { int i; };
C++ 不允许这样的 typedef 声明。
公平地说,C 中的此类声明并非完全没有意义:它们仍然声明了 struct S 类型。
11. C 语言允许在声明中显式重复 cv 限定符:
const const const int a = 42;
从 C++ 的角度来看,代码是不正确的。 (C++ 也对类似的过度限定视而不见,但只能通过中间类型名称:typedef 名称,典型的模板参数)。
12.在C中,直接复制volatile对象是没有问题的(至少从形式代码正确性的角度来看):
void foo(void)
{
struct S { int i; };
volatile struct S v = { 0 };
struct S s = v;
s = v;
}
在 C++ 中,隐式生成的复制构造函数和赋值运算符不将 volatile 对象作为参数。
13、在C语言中,任何值为0的整型常量表达式都可以作为空指针常量:
void *p = 2 - 2;
void *q = -0;
在采用 C++11 标准之前,C++ 也是如此。 然而,在现代 C++ 中,整型值中,只有文字空值可以充当空指针常量,更复杂的表达式不再有效。 从 C++ 的角度来看,上述初始化是不正确的。
14. C 不支持右值的 cv 限定。 特别是,函数返回值的 cv 限定会立即被语言忽略。连同数组到指针的自动转换,这允许您绕过一些常量正确性规则:
struct S { int a[10]; };
const struct S foo()
{
struct S s;
return s;
}
int main()
{
int *p = foo().a;
}
然而,值得注意的是,尝试在 C 中修改右值会导致未定义的行为。
从 C++ 的角度来看,foo() 的返回值以及数组 foo().a 保留了 const 限定,并且不可能将 foo().a 隐式转换为类型 int *。
15. [C23] C 预处理器不熟悉 true 和 false 等字面量。 在 C 中,true 和 false 仅作为标准头文件 <stdbool.h> 中定义的宏可用。 如果没有定义这些宏,那么根据预处理器的规则,#if true 和#if false 都应该表现得像#if 0。
同时,C++ 预处理器必须自然地识别 true 和 false 文字,并且它的 #if 指令必须以“预期”的方式处理这些文字。
当 C 代码不包含 <stdbool.h> 时,这可能是不兼容的来源:
#if true
int a[-1];
#endif
这段代码在C++中显然是不正确的,但同时在C中却很容易编译。
16. 从 C++11 开始,C++ 预处理器不再将 <literal><identifier> 序列视为独立的标记。 从 C++ 语言的角度来看,这种情况下的 <identifier> 是一个文字后缀。 为了避免这种解释,在 C++ 中,这些标记应该用空格分隔:
#define D "d"
int a = 42;
printf("%"D, a);
printf 的这种格式对于 C 是正确的,但从 C++ 的角度来看是不正确的。
17. main函数的递归调用在C中是允许的,但在C++中是不允许的。 C++程序一般不允许以任何方式使用main函数。
18. 在 C 中,字符串文字是 char [N] 类型,而在 C++ 中它们是 const char [N]。 即使“旧”C++ 支持将字符串文字转换为类型 char * 作为异常,此异常仅在直接应用于字符串文字时才有效
char *p = &"abcd"[0];
从 C++ 的角度来看,这样的初始化是不正确的。
19. 在 C 中,声明为 int 类型但未明确指示有符号或无符号的位字段可以是有符号或无符号(这是实现定义的)。 在 C++ 中,这样的位域总是有符号的。
20、在C语言中,typedef类型名和struct类型标签在不同的命名空间,互不冲突。 例如,这样一组声明从 C 的角度来看是正确的:
struct A { int a; };
typedef struct B { int b; } A;
typedef struct C { int c; } C;
在 C++ 中,类类型没有单独的标记概念:类名与 typedef 名称共享相同的命名空间,并且可能与它们冲突。 为了与 C 代码部分兼容,C++ 允许您声明与现有类型类名称匹配的 typedef 别名,但前提是该别名引用具有完全相同名称的类型类。 在上面的示例中,第 2 行的 typedef 声明从 C++ 的角度来看是不正确的,但第 3 行的声明是正确的。
21. 在 C 中,您可以使用与现有类型名称相匹配的字段名称。
typedef int I;
struct S
{
I I;
};
在 C++ 中,标识符的这种“重新定义”是不允许的。
22. 在 C 中,声明相同变量时外部链接和内部链接之间的隐式冲突会导致未定义的行为,但在 C++ 中,这种冲突会使程序格式错误。 要安排这样的冲突,您需要构建一个相当棘手的配置:
static int a; /* Internal linking */
void foo(void)
{
int a; /* Hides external `a`, has no linking */
{
extern int a;
/* Because external `a` is hidden, declare `a` with internal
linking. Now `a` is declared with both external
and internal linking - conflict */
}
}
在 C++ 中,这样的 extern 声明格式错误。 尽管在 C++ 语言标准中有针对这种异常情况的单独示例,但流行的 C++ 编译器通常不会诊断这种违规情况。
以下是我认为微不足道、众所周知且无趣的差异示例。
我将它们包括在这里是为了完整性,因为它们正式满足我的标准:乍一看,代码在 C++ 观察者的眼中看起来或多或少是正常的。
23. C 语言允许从 void * 类型隐式转换指针:
void *p = 0;
int *pp = p;
24. 在 C 中,枚举类型的值可以隐式转换为 int 类型或从 int 类型转换:
enum E { A, B, C } e = A;
e = e + 1;
在 C++ 中,隐式转换只能以一种方式工作。
25. [C23] C语言支持无原型的函数声明:
void foo(); /* Declaration without prototype */
void bar()
{
foo(1, 2, 3);
}
26. 在 C 中,嵌套结构类型声明将内部类型的名称放在外部(封闭)范围内:
struct A
{
struct B { int b; } a;
};
struct B b; /* Refers to the type `struct B` declared on line 3 */
事实上,这就是目前积累的全部。 我希望你觉得我的观察很有趣,它们会对某人有所帮助。你认为我错过了什么重要的事情吗? 请随时留下任何问题、意见或建议。