需要对C中的铸造进行一些澄清

我刚刚读到了关于转换malloc返回值的不良做法。 如果我理解正确,那么离开演员是绝对合法的,因为它是隐含的(并且应该留下,因为它可能产生其他问题)。 那么我的问题是,我什么时候应该施展我的价值观? 有一些一般规则或什么? 例如,此代码使用gcc -W -Wall编译时没有任何错误(除了未使用的bar ,但这不是重点):

 float foo(void) { double bar = 4.2; return bar; } int main(void) { double bar = foo(); return 0; } 

我现在很困惑。 有关铸造的良好做法和规则是什么?

谢谢。

有几种情况需要在C中进行完全有效的铸造。要注意像“铸造总是糟糕的设计”这样的清晰断言,因为它们显然是明显的假冒伪劣。

严重依赖演员表的一大群情境是算术运算。 在需要强制编译器解释与“默认”类型不同的类型中的算术表达式时,需要进行转换。 如在

 unsigned i = ...; unsigned long s = (unsigned long) i * i; 

避免溢出。 或者在

 double d = (double) i / 5; 

为了使编译器切换到浮点除法。 或者在

 s = (unsigned) d * 3 + i; 

为了取整个浮点值的一部分。 等等(例子是无穷无尽的)。

另一组有效用途是成语,即完善的编码实践。 例如,当函数将const指针作为输入并将非const指针返回到相同(可能是常量)的数据时,例如标准strstr ,这是经典的C语言。 实现这个习惯用法通常需要使用强制转换来抛弃输入的常量。 有人可能称之为糟糕的设计,但实际上在C中没有更好的设计选择。否则,它不会是一个成熟的成语:)

另外值得一提的是,作为一个例子,标准printf函数的迂腐正确使用可能需要在一般情况下对参数进行强制转换。 (就像%p格式说明符期望void *指针作为参数一样,这意味着必须以某种方式将int *参数转换为void * 。显式强制转换是执行转换的最合理方式。) 。

当然,在需要强制转换时,还有其他众多完全有效的情况。

当人们不加思索地使用它们时,通常会出现演员表的问题,即使在不需要它们的情况下也是如此(例如, malloc的返回,这种情况比一个人更糟糕)。 或者当人们使用强制转换来强制编译器接受他们的错误代码时。 毋庸置疑,需要一定程度的专业知识才能从糟糕的演员表中讲出有效的演员情况。

在某些情况下,强制转换用于使编译器停止发出一些烦人且不必要的警告消息。 这些演员阵容属于好人和坏人之间的灰色区域。 一方面,不必要的演员阵容很糟糕。 另一方面,用户可能无法控制编译设置,因此使转换成为处理警告的唯一方法。

如果你需要演员,我总是建议你明确地这样做,以向其他人展示,或者将来可能是你自己想要这种行为。

顺便说一句,gcc警告是-Wconversion 。 不幸的是-Wall和-Wextra仍然留下了很多好的警告。

以下是我希望gcc非常像lint时使用的标志

 -pedantic -std = c99 -ggdb3 -O0 -Wall -Wextra -Wformat = 2 -Wmissing-include-dirs -Winit-self -Wswitch-default -Wswitch-enum -Wunused-parameter -Wfloat-equal -Wundef -Wshadow -Wlarger -than-1000 -Wunnsafe-loop-optimizations -Wbad-function-cast -Wcast-qual -Wcast-align -Wconversion -Wlogical-op -Waggregate-return -Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wmissing -declarations -Wpacked -Wpadded -Wredundant-decls -Wnested-externs -Wunreachable-code -Winline -Winvalid-pch -Wvolatile-register-var -Wstrict-aliasing = 2 -Wstrict-overflow = 2 -Wortditional-conversion -Wwrite-strings

我还首先使用cppcheck检查我的代码, cppcheck是一个用于C和C ++的免费静态代码分析器。 强烈推荐。

我的简单指南 – 如果它需要演员,那可能是错的。 如果您不需要演员表,请不要使用它们。

不转换malloc返回值的原因是因为总是将返回值赋给指针类型,而C标准允许将void *隐式地转换为任何其他指针类型。 明确地施放它是多余的,因此是不必要的。

你不能在’C’中询问铸件是否理解铸件涵盖多种类型的操作。 基本上有两种类型转换和类型强制。 在C ++中,因为它有更多的类型信息,所以它创建了4种类型的强制转换,并使用独有的符号对其进行编码。 reinterpret_cast<>const_cast<>dynamic_cast<>static_cast<> 。 你没有在C中使用这些,因为所有的强制转换都有语法(ctype)但它们的原因仍然存在,并且有助于理解为什么需要强制转换,即使你的问题是关于’C’的。

静态演员的“需要”就是你在例子中展示的内容。 编译器会为你做这些,即使你没有指定它 – 但是,将警告级别提高到足够高,编译器会警告你是否有精度损失,因为从double到float(你的return bar;声明)。 添加转换会告诉编译器意图丢失精度。

第二个最不危险的演员是const cast <>。 它用于从类型中删除const或volatile。 这通常发生在结构具有内部“缓存”的地方。 因此调用者可能具有结构的const版本,但是“内部函数”需要更新缓存,因此必须从指向const结构的指针转换为常规结构以更新内部字段。

最危险的类型是重新演绎,以及为什么人们会继续谈论演员的糟糕程度。 这就是你没有转换任何内容的地方,而是告诉编译器将值重新解释为完全不同的类型。 试图摆脱编译器错误的天真程序员可能会添加以下内容。

  char **ptostr = (char **p) "this is not a good idea"; 

可能正确的解决方法是使用’&’,这就是铸造获得不良声誉的方式。 像这样的演员可以用于善恶。 我用它来回答另一个关于如何找到2的最小功率的问题 ,以便在CPU中利用FPU的function。 实现链接列表是一个更好的用例。 如果链接存在于对象本身中,则必须从链接指针转换回封闭的对象(如果链接不能位于结构的顶部,则可以很好地使用offsetof宏)。

动态演员在C中没有语言支持,但情况仍然存在。 如果您有异构列表,则可以使用列表链接标题中的字段validation对象是否为给定类型。 手动实现,您将validation类型是否兼容,如果不是,则返回NULL。 这是重新解释演员的特殊版本。

有许多复杂的编程模式需要投射,所以我不会说需要避免投射或表明出现了问题。 ‘C’的问题在于你如何以与安全的方式相同的方式编写不安全的问题。 保持它包含和限制是一个很好的做法,所以你可以确保你正确(例如,如果可以的话,使用库例程,强类型和断言)。

只有在绝对必要的情况下才能施放结果; 如果您使用的是从两个不同的人(例如静态库或动态库)开发的代码,并且两个函数不使用兼容值,那么转换是唯一的解决方案(只要您不尝试转换字符串到一个整数)。

在使用转换之前,最好validation使用的数据类型是否正确。 在示例代码中(其目的是提供示例),当函数返回double时,将返回值声明为float值是没有意义的。

 double bar = foo(); 

这里发生的事情称为促销转换 ,其中转换后保留转换变量的值。 反之则不然,即float -> double 。 唯一的答案是只在你真的需要的时候施展。 施展很多是设计糟糕的表现。

在你的例子中,精度有所下降,但是演员是隐含的。 有些时候绝对需要进行强制转换,例如当您从字节流中读取数据时或者当您拥有的是通过void*指针进入的数据时,您知道它代表什么数据。 但在大多数情况下,应避免铸造并保留用于这些极端情况。

你正在看的是隐式类型转换。 如果你的类型范围比你最终的范围更有限,那么这被认为是安全的,即short为int是正常的,因为float是double。

我很惊讶gcc在将double转换为float时没有产生警告; 我相信微软的编译器确实如此。

您可能会发现这两个SOpost信息丰富:

具体来说,铸造malloc的结果有什么危险?

C中的Implict函数声明是否实际生成了对象代码?

由于C隐式地将先前未声明的函数的结果键入int (从C99开始不允许IINM malloc() ,因此反对转换malloc()的结果的警告是一种特殊情况。

通常,您希望尽可能限制显式强制转换的使用; 你需要使用的唯一一次是你试图将一种类型的值分配给不兼容类型的变量(例如,将指针值赋给int变量,反之亦然)。 由于void *与其他所有指针类型兼容,因此不需要显式转换。 但是,如果您尝试将类型为int *的值分配给struct foo *类型的变量, 需要显式强制转换。 如果您发现自己分配了不兼容类型的值,那么您可能需要重新访问您的设计。

基本上,您需要将参数转换为期望与其原型声明不同的参数的函数。

例如, isalpha()有一个带有int参数的原型,但实际上需要一个unsigned char

 char *p; if ((*p != EOF) && isalpha((unsigned char)*p) /* cast needed */ { /* ... */ } 

并且,您需要特别注意接受可变数量参数的函数,例如:

 long big; printf("%d\n", (int)big); 

编辑

编译器无法将可变参数函数的参数转换为正确的类型,因为原型本身没有可用的类型信息。 考虑一个类似printf()的函数

 int my_printf(const char *fmt, ...); 

就编译器而言,您可以在“…”参数中传递各种类型的值,并且必须确保参数与函数所期望的相匹配。 例如,假设my_printf()函数接受类型为time_t的值,并在格式字符串中使用相应的“%t”说明符。

 my_printf("UNIX Epoch: %t.\n", 0); /* here, 0 is an int value */ my_printf("UNIX Epoch: %t.\n", (time_t)0); /* and here it is a time_t */ 

我的编译器不想让这个失败! 显然它( 以及键盘上的那个 )在“…”中为每个参数传递8个字节

使用没有“…”的原型( int my_printf(const char *fmt, time_t data); ),编译器会自动将第一个0转换为正确的类型。

注意:如果格式字符串是文字字符串,则某些编译器(包括gcc)将针对printf()的格式字符串validation参数

至少有两种演员阵容:

  • 转换导致表示更改的数值。 (这是Harbison和Steele非常有用的C参考手册中的术语。)这些演员大多是无害的; 唯一可以造成伤害的方法是,例如,将较宽的类型转换为较窄的类型,在这种情况下,你故意扔掉一些东西。 只有你,程序员,知道扔掉那些碎片是否安全。

    C还有一个潜在的function,当您分配,返回或传递一个数字表达式时,它可能会代表您执行更改表示,该数字表达式的类型与关联左值,结果或参数的类型不完全匹配。 我认为这个function是有害的,但它牢牢地嵌入在C的做事方式中。 gcc选项-Wconversion将让您知道编译器代表您更改表示的位置,而不会被询问。

  • 不涉及更改表示的转换,只是要求编译器以某种方式查看位。 这些包括相同大小的有符号和无符号类型之间的转换,以及指向不同类型数据的指针类型之间的转换。 void *类型在C中享有特殊状态,因为编译器可以在void *和其他数据指针类型之间自由转换而无需强制转换。 请注意,两种不同类型数据的指针之间的强制转换从不涉及表示的更改。 这是void *约定可以工作的一个原因。

C程序员试图避免无偿演员的一个原因是, 当你编写演员表时,编译器完全信任你 。 如果你犯了一个错误,编译器不会为你捕获它。 出于这个原因,我建议我的学生永远不要投射指针类型,除非他们确切知道他们在做什么。


PS我认为可以为这个问题写一个简短有用的答案。 错误!