【问题标题】:Does casting actually DO anything?铸造真的有什么作用吗?
【发布时间】:2021-02-01 22:27:56
【问题描述】:

考虑以下 sn-p:

char x[100];
double *p = &x;

正如预期的那样,这会产生以下警告:

f.c:3:15: warning: initialization of ‘double *’ from incompatible pointer type ‘char (*)[100]’ 
[-Wincompatible-pointer-types]
    3 |   double *p = &x;
      |               ^

这很容易解决,只需更改为

double *p = (double*)&x;

我的问题是,选角真的有什么作用吗?如果没有演员表,代码会无效吗?或者它只是一种让编译器安静的方法?什么时候需要铸造?

我知道你可以像这样使用 sn-ps 产生一些效果:

int x = 666;
int y = (char)x;

但是这和这个不一样吗?

int x = 666;
char c = x;
int y = c;

如果是相同的,那么强制转换会做一些事情,但它不是必要的。对吧?

请帮助我理解这一点。

【问题讨论】:

  • 在该分配违反严格的别名规则之后取消引用p,无论有无强制转换。所以演员表使警告静音,但留下未定义的行为。
  • 转换可能会有所作为;在某些 C 实现中,double * 的表示可能与char * 的表示不同。演员做某事;如果赋值的左操作数和右操作数的类型不“匹配”(满足某些规则)但不用于强制转换,则 C 标准要求进行诊断。
  • 指针类型之间的转换通常在现代系统中大多是空操作。但是,如果char * 与其他数据类型未对齐,如果您尝试通过未对齐的指针访问double,某些机器可能会产生“总线错误”。其他机器可能会产生相当大的开销(系统陷阱和处理错位的代码)。
  • 整数类型之间的隐式转换是有规则的。指针的规则是不允许不兼容的指针赋值。
  • @Peter-ReinstateMonica:关于“C ISO 草案 1256, 6.3.2.3/7,使用独立于其类型的对象指针转换提供往返保证”:不,它没有。仅当满足对齐要求时,才存在往返保证。这意味着从更宽松对齐类型到更严格对齐类型的转换可能会丢失信息。

标签: c casting


【解决方案1】:

铸造可以做几个不同的事情。正如其他答案所提到的,它几乎总是会更改被强制转换的值的类型(或者,也许是类型的属性,例如const)。它也可能以某种方式改变数值。但是有很多可能的解释:

  • 有时它只是使警告静音,根本不执行“真正的”转换(如在许多指针转换中)。
  • 有时它会消除警告,只留下类型更改但不更改值(与其他指针强制转换一样)。
  • 有时类型更改,虽然它不涉及明显的值更改,但暗示稍后使用该值的非常不同语义(同样,在许多指针转换中)。
  • 有时它会要求进行无意义或不可能的转换。
  • 有时它会执行编译器自己会执行的转换(有或没有警告)。
  • 但有时它会强制执行编译器不会执行的转换。

此外,有时编译器试图发出的那些警告,即强制转换沉默,是无害的和/或令人讨厌的,但有时它们是非常真实的,并且代码可能会失败(即,作为沉默的警告试图告诉你)。

一些更具体的例子:

改变类型但不改变值的指针转换:

char *p1 = ... ;
const char *p2 = (const char *)p;

还有一个:

unsigned char *p3 = (unsigned char *)p;

以更重要的方式更改类型的指针转​​换,但保证没问题(在某些架构上,这也可能会更改值):

int i;
int *ip = &i;
char *p = (char *)ip;

一个同样重要的指针转换,但很可能是好的:

char c;
char *cp = &c;
int *ip = (int *)cp;
*ip = 5;                  /* likely to fail */

一个无意义的指针转换,编译器拒绝执行它,即使是显式转换:

float f = 3.14;
char *p = (char)f;        /* guaranteed to fail */

进行转换的指针转换,但编译器无论如何都会进行转换:

int *p = (int *)malloc(sizeof(int));

(这被认为是个坏主意,因为在您忘记包含 <stdlib.h> 来声明 malloc() 的情况下,演员可以使可能提醒您注意问题的警告静音。)

三个从整数到指针的强制转换,实际上是明确定义的,这是由于 C 语言中的一个 非常 特定的特殊情况:

void *p1 = (void *)0;
char *p2 = (void *)0;
int *p3 = (int *)0;

从整数到指针的两次强制转换不一定有效,尽管编译器通常会做一些显而易见的事情,并且强制转换会使其他警告静音:

int i = 123;
char *p1 = (char *)i;
char *p2 = (char *)124;
*p1 = 5;                  /* very likely to fail, except when */
*p2 = 7;                  /* doing embedded or OS programming */

从指针返回到int非常 可疑转换:

char *p = ... ;
int i = (int)p;

从指针返回到应该足够大的整数的问题较少:

char *p = ... ;
uintptr_t i = (uintptr_t)p;

改变类型的转换,但“丢弃”而不是“转换”一个值,并且使警告静音:

(void)5;

进行数字转换的转换,但编译器无论如何都会进行转换:

float f = (float)0;

改变类型和解释值的强制转换,虽然它通常不会改变位模式:

short int si = -32760;
unsigned short us = (unsigned short)si;

进行数字转换的强制转换,但编译器可能会警告:

int i = (int)1.5;

进行编译器不会进行的转换:

double third = (double)1 / 3;

底线是演员肯定会做一些事情:其中一些有用,一些不必要但无害,一些危险。

如今,许多 C 程序员的共识是大多数强制转换是或应该是不必要的,这意味着避免显式强制转换是一个不错的规则,除非您确定自己知道自己在做什么,而且这样做是合理的怀疑你在别人的代码中发现的显式转换,因为它们很可能是麻烦的迹象。


作为最后一个例子,在过去,就指针投射而言,这确实让我觉得灯泡亮了:

char *loc;
int val;
int size;

/* ... */

switch(size) {
    case 1: *loc += val; break;
    case 2: *(int16_t *)loc += val; break;
    case 4: *(int32_t *)loc += val; break;
}

loc += val 的这三个实例做了三件完全不同的事情:一个更新一个字节,一个更新一个 16 位整数,一个更新一个 32 位整数。 (有问题的代码是一个动态链接器,执行符号重定位。)

【讨论】:

  • 后者只有指针,对吧? IE。第 2 点。
  • 不错。如果您为每个提供示例会很好。
  • 此外,第 2 点中的警告可能专门针对可能会更改值的情况,例如缩小转换范围(因此发出警告)。
  • @klutt 当然可以,但是我花了一段时间才有时间添加一些。
【解决方案2】:

演员至少做了一件事 - 它满足以下分配约束:

6.5.16.1 简单赋值

约束

1    应满足以下条件之一:112)
...
— 左操作数具有原子的、合格的或不合格的指针类型,并且(考虑 左操作数在左值转换后的类型)两个操作数都是 指向兼容类型的合格或不合格版本的指针,以及指向的类型 to by left 具有由 right 指向的类型的所有限定符;
112) 这些约束相对于类型限定符的不对称外观是由于转换 (在 6.3.2.1 中指定)将左值更改为“表达式的值”,从而删除任何类型 应用于表达式类型类别的限定符(例如,它删除 const 但 不是 int volatile * const 类型的 volatile)。

这是一个编译时间约束——它影响源代码是否被翻译成可执行文件,但不一定影响翻译后的机器代码。

可能导致在运行时执行实际转换,但这取决于表达式中涉及的类型和主机系统。

【讨论】:

    【解决方案3】:

    强制转换会改变类型,这在有符号或无符号类型很重要时非常重要,

    例如isupper()等字符处理函数为defined as taking an unsigned char value or EOF

    标题<ctype.h> 声明了几个对字符分类和映射有用的函数。在所有情况下,参数都是一个 int,其值应表示为无符号字符或应等于宏 EOF 的值。如果参数有任何其他值,则行为未定义。

    这样的代码如

    int isNumber( const char *input )
    {
        while ( *input )
        {
            if ( !isdigit( *input ) )
            {
                 return( 0 );
            }
            input++;
        }
        // all digits
        return( 1 );
    }
    

    应该正确地将*inputconst char 值转换为unsigned char

    int isNumber( const char *input )
    {
        while ( *input )
        {
            if ( !isdigit( ( unsigned char ) *input ) )
            {
                 return( 0 );
            }
            input++;
        }
        // all digits
        return( 1 );
    }
    

    如果不强制转换为unsigned char,当*input 提升为int 时,一个负数的char 值(假设char 是有符号的并且小于int)将被符号扩展为一个负值,不能表示为unsigned char 值,因此会调用未定义的行为。

    所以是的,这种情况下的演员表做了一些事情。它会更改类型,因此 - 在几乎所有当前系统上 - 避免输入 char 为负值的未定义行为。

    在某些情况下,float 值可以强制转换为 double(或相反)以强制代码以所需的方式运行。*

    * - 我最近看到过这样的案例 - 如果有人可以找到示例,请随时添加您自己的答案...

    【讨论】:

    • 更重要的是,强制转换为 unsigned char 可能会更改值。 问题不在于类型——所有字符类型都将被转换(提升?)为 int美好的。问题是普通的char 的值可能是负数,这是被禁止的。
    • @Peter-ReinstateMonica 实际上,传递给isdigit() 在我的示例中很重要。
    • 这就是为什么“铸造改变类型,这可能非常重要......”语句在这种情况下是错误的。类型无关紧要。如果你愿意,你可以传递 bools、_uint128 或 long doubles。
    • @Peter-ReinstateMonica 类型无关紧要。 不,不是这种情况,因为生成的int 中的二进制值可能会因@987654345 的类型而异@ 已通过 - signedunsigned。如果 signed char 是负数并提升为匹配 isdigit()int 参数,它将被符号扩展为不能表示为 unsigned char 的值(几乎所有系统...),因此调用未定义的行为。由于标准对允许值的限制,转换对于更改类型以避免此处的符号扩展至关重要。
    • 是的,显然int 中的结果值可以改变——但这些值不依赖于参数的类型!参数可以——正如我之前所说的——具有 any 数字类型。关键是它必须有一个可以用unsigned char 表示的。你如何到达那里并不重要。当然,您不需要 unsigned char 或强制转换(例如,您可以简单地使用 if/else 链计算具有与负字节相同的位模式的正值,并将其分配给 int 参数)。 类型真的不重要。该值确实如此。
    【解决方案4】:

    强制转换可能会也可能不会改变实际的二进制值。但这不是它的主要目的,只是一个副作用。

    它告诉编译器将一个值解释为不同类型的值。二进制值的任何变化都是其副作用。

    你(程序员)应该让编译器知道:我知道我在做什么。因此,您可以在编译器不询问您的情况下对自己开枪。

    不要误会我的意思,在现实世界的代码中强制转换是绝对必要的,但必须小心谨慎地使用它们。切勿仅仅为了摆脱警告而施放,确保您了解后果。

    【讨论】:

      【解决方案5】:

      理论上,系统可能对void *char * 使用与其他指针类型不同的表示。可能存在通常使用窄宽度寄存器来保存指针值的系统。但是,如果代码需要处理每个字节,那么窄的宽度可能不够,因此void *char * 将使用更宽的表示。

      转换指针值很有用的一种情况是函数采用可变数量的指针参数,并以 NULL 终止,例如 execl() 系列函数。

      execl("/bin/sh", "sh", "-c", "echo Hello world!", (char *)NULL);
      

      没有强制转换,NULL 可能会扩展为 0,这将被视为 int 参数。当execl() 函数检索最后一个参数时,它可能会错误地提取预期的指针值,因为传递了int 值。

      【讨论】:

        【解决方案6】:

        我的问题是,选角真的有什么作用吗?

        是的。它告诉编译器,以及其他程序员,包括未来的你,你认为你知道你在做什么,你真的打算把 char 当作 @ 987654322@ 或其他。它可能会也可能不会改变编译后的代码。

        如果没有演员,代码会无效吗?

        这取决于所讨论的演员阵容。一个让我想起的例子涉及除法:

        int a = 3;
        int b = 5;
        float c = a / b;
        

        关于这类事情的问题一直出现在 SO 上:人们想知道为什么 c 的值为 0。答案当然是 ab 都是 int,并且整数除法的结果也是一个整数,仅在分配给c 时才转换为float。要在 c 中获得 0.6 的预期值,请将 ab 转换为 float

        float c = a / (float)b;
        

        您可能不认为没有强制转换的代码是无效,但下一个计算可能涉及除以c,此时如果没有上述强制转换,可能会发生除以零错误。

        或者它只是让编译器安静的一种方式?

        即使在更改编译代码方面强制转换是无操作的,防止编译器抱怨类型不匹配也在做一些事情

        什么时候需要强制转换?

        当它改变编译器生成的目标代码时,它是必要的。如果您的组织的编码标准需要,它也可能是必要的

        【讨论】:

        • 但是在浮动的情况下,我们可以——就像我在上一个例子中所做的那样——也可以使用临时变量,对吧? float tmp = b; float c = a / tmp;
        • @klutt 当然,然后当您将b 转换为float 时,您正在明确地执行编译器所做的事情。 必要你的意思是有什么你可以用演员做你不能用其他方式做的吗??我想没有 - 无论给定的演员做什么,你都可以自己明确地做同样的事情,就像你的临时变量一样。但这就像声称不需要整数乘法一样,因为您可以使用for 循环和加法来做同样的事情。
        • 如有必要,我使用了“在在合理范围内,你可以用演员做什么而你不能以其他方式做的事情”的模糊定义。根据我的定义,您使用 for 循环的示例非常笨拙。
        • @klutt 您的临时变量可能只是将b 转换为float 的另一种方式。在我的示例中,演员表实际上并没有将 b 更改为浮点数; b 在演员表之后仍然是 int。转换将计算中使用的值更改为float。使用临时变量显式执行此操作是相同的,但语法不同。强制转换操作是必要的,无论你是用临时变量还是用强制转换语法来表达。
        【解决方案7】:

        演员表有所作为的例子。

        int main(void)
        {
            unsigned long long x = 1 << 33,y = (unsigned long long)1 << 33;
        
            printf("%llx, %llx\n", x, y);
        }
        

        https://godbolt.org/z/b3qcPn

        【讨论】:

        • 但是你也可以unsigned long long y = 1UUL &lt;&lt; 33 对吗?
        • @klutt 太好了,我很高兴知道您知道这一点。干得好。
        • 我实际上不确定你是否在讽刺。 :D
        • @klutt 顺便说一句,它不是1UUL &lt;&lt; 33 只有1ULL &lt;&lt; 33
        • 确实如此。我的错。
        【解决方案8】:

        强制转换只是一种类型转换:实现将通过目标类型表示参数值。新类型的表达式(假设目标类型不同)可能有

        • 不同的尺寸
        • 不同的值
        • 和/或表示该值的不同位模式。

        这三个变化是相互正交的。任何子集,包括没有子集和所有子集,都可能出现(所有示例都假定二进制补码):

        • 没有一个:(unsigned int)1;
        • 仅尺寸:(char)1
        • 仅值:(unsigned int)-1
        • 仅位模式:(float)1(我的机器有 sizeof(int) == sizeof(float))
        • 大小和值,但不是位模式(原始值中存在的位):(unsigned int)(char)-4
        • 大小和位模式,但不是值:(float)1l
        • 值和位模式,但不是大小:(float)1234567890)(32 位整数和浮点数)
        • 全部:(float)1234567890l(long 为 64 位)。

        当然,新类型可能会影响使用它的表达式,并且通常会有不同的文本表示(例如通过 printf),但这并不奇怪。

        指针转换可能值得进一步讨论:新指针通常具有相同的值、大小和位表示(尽管正如 Eric Postpischli 正确指出的那样,理论上可能不会)。指针转换的主要意图和效果不是改变指针;就是改变通过它访问的内存的语义。

        在某些情况下,强制转换是执行转换的唯一方法(不兼容的指针类型,指针与整数)。

        在其他情况下,例如缩小算术转换(可能会丢失信息或产生溢出),强制转换表示意图,从而使警告静音。在所有可以隐式执行转换的情况下,强制转换不会改变程序的行为——它是可选的装饰。

        【讨论】:

          猜你喜欢
          • 2012-06-01
          • 1970-01-01
          • 1970-01-01
          • 2013-05-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多