¿Puede un código válido en C y C ++ producir un comportamiento diferente cuando se compila en cada lenguaje?

664

C y C ++ tienen muchas diferencias, y no todos los códigos C válidos son códigos C ++ válidos.
(Por "válido" me refiero a código estándar con comportamiento definido, es decir, no específico de implementación / indefinido / etc.)

¿Hay algún escenario en el que un código válido tanto en C como en C ++ produzca un comportamiento diferente cuando se compila con un compilador estándar en cada idioma?

Para hacer una comparación razonable / útil (estoy tratando de aprender algo prácticamente útil, no para tratar de encontrar lagunas obvias en la pregunta), supongamos:

  • Nada relacionado con el preprocesador (lo que significa que no hay hacks #ifdef __cplusplus, pragmas, etc.)
  • Cualquier cosa definida por la implementación es la misma en ambos idiomas (por ejemplo, límites numéricos, etc.)
  • Estamos comparando versiones razonablemente recientes de cada estándar (por ejemplo, C ++ 98 y C90 o posterior).
    Si las versiones son importantes, mencione qué versiones de cada una producen un comportamiento diferente.
usuario541686
fuente
11
Por cierto, puede ser útil programar en un dialecto que sea C y C ++ al mismo tiempo. Lo he hecho en el pasado y en un proyecto actual: el lenguaje TXR. Curiosamente, los desarrolladores del lenguaje Lua hicieron lo mismo, y llaman a este dialecto "Clean C". Obtiene el beneficio de una mejor comprobación del tiempo de compilación y posiblemente diagnósticos útiles adicionales de los compiladores de C ++, pero conserva la portabilidad de C.
Kaz
99
Fusioné la pregunta anterior con esta pregunta ya que tiene más puntos de vista y respuestas con votos. Este sigue siendo un ejemplo de una pregunta no constructiva, pero es bastante límite ya que sí, enseña algo a los usuarios de SO. Lo cierro como no constructivo solo para reflejar el estado de la pregunta antes de la fusión. Siéntase libre de estar en desacuerdo y volver a abrir.
George Stocker
13
Votar para volver a abrir, ya que creo que se puede responder objetivamente con un "sí" seguido de un ejemplo (como se demuestra a continuación). Creo que es constructivo ya que las personas pueden aprender comportamientos relevantes de él.
Anders Abel
66
@AndersAbel El número puro de respuestas, todas las cuales son correctas, demuestra inequívocamente que sigue siendo una pregunta para hacer una lista. No había forma de que pudieras haber hecho esta pregunta sin obtener una lista.
dmckee --- ex gatito moderador
2
@dmckee Por lo que vale, estoy de acuerdo contigo. Sin embargo, la gente de la etiqueta C ++ es ... Deberíamos decir ... luchadora .
George Stocker

Respuestas:

397

Lo siguiente, válido en C y C ++, va a resultar (muy probablemente) en diferentes valores ien C y C ++:

int i = sizeof('a');

Consulte Tamaño del carácter ('a') en C / C ++ para obtener una explicación de la diferencia.

Otro de este artículo :

#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;
}
Alexey Frunze
fuente
8
¡Definitivamente no esperaba este! Esperaba algo un poco más dramático, pero esto sigue siendo útil, gracias. :) +1
user541686
17
+1 el segundo ejemplo es bueno por el hecho de que C ++ no requiere structantes de los nombres de estructura.
Seth Carnegie
1
@ Andrew Pensé lo mismo hace un tiempo y lo probé y funcionó en GCC 4.7.1 sin el estándar, en contra de lo que esperaba. ¿Es eso un error en GCC?
Seth Carnegie
3
@SethCarnegie: un programa no conforme no necesita dejar de funcionar, pero tampoco está garantizado que funcione.
Andrey Vihrov
3
struct sz { int i[2];};significaría que C y C ++ tienen que producir valores diferentes. (Mientras que un DSP con sizeof (int) == 1, podría producir el mismo valor).
Martin Bonner apoya a Mónica el
464

Aquí hay un ejemplo que aprovecha la diferencia entre las llamadas a funciones y las declaraciones de objetos en C y C ++, así como el hecho de que C90 permite la llamada a funciones no declaradas:

#include <stdio.h>

struct f { int x; };

int main() {
    f();
}

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

En C ++, esto no imprimirá nada porque fse crea y destruye un temporal , pero en C90 imprimirá helloporque las funciones se pueden invocar sin haber sido declaradas.

En caso de que se esté preguntando si el nombre fse usará dos veces, los estándares C y C ++ lo permiten explícitamente, y para hacer un objeto que tiene que decir struct fpara desambiguar si desea la estructura, o dejarlo structsi desea la función.

Seth Carnegie
fuente
77
Hablando estrictamente bajo C, esto no se compilará, porque la declaración de "int f ()" es posterior a la definición de "int main ()" :)
Sogartar
15
@Sogartar, ¿en serio? Los compiladores codepad.org/STSQlUhh C99 le darán una advertencia, pero aún le permitirán compilarlo.
jrajav
22
@Sogartar en las funciones de C se puede declarar implícitamente.
Alex B
11
@AlexB No en C99 y C11.
66
@jrajav Esos no son compiladores C99, entonces. Un compilador C99 detecta identificadores no declarados como un error de sintaxis. Un compilador que no hace eso es un compilador C89, o un compilador pre-estándar u otro tipo de compilador no conforme.
430

Para C ++ vs. C90, hay al menos una forma de obtener un comportamiento diferente que no está definido en la implementación. C90 no tiene comentarios de una sola línea. Con un poco de cuidado, podemos usar eso para crear una expresión con resultados completamente diferentes en C90 y en C ++.

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

En C ++, todo desde el //final de la línea es un comentario, por lo que esto funciona como:

int a = 10 + 3;

Como C90 no tiene comentarios de una sola línea, solo el /* comment */es un comentario. El primero /y los 2dos son partes de la inicialización, por lo que se trata de:

int a = 10 / 2 + 3;

Por lo tanto, un compilador C ++ correcto dará 13, pero un compilador C90 estrictamente correcto 8. Por supuesto, acabo de elegir números arbitrarios aquí; puede usar otros números como mejor le parezca.

Jerry Coffin
fuente
34
¡Whoa, esto es alucinante! De todas las cosas posibles, nunca hubiera pensado que los comentarios podrían usarse para cambiar el comportamiento jaja. +1
user541686
89
incluso sin el 2, se leería como 10 / + 3válido (unario +).
Benoit
12
Ahora por diversión, modifíquelo para que C y C ++ calculen diferentes expresiones aritméticas y evalúen el mismo resultado.
Ryan C. Thompson
21
@RyanThompson Trivial. s /
2/1
44
@Mehrdad ¿Estoy equivocado o los comentarios están relacionados con el preprocesador? Por lo tanto, deben excluirse como una posible respuesta de su pregunta. ;-)
Ale
179

C90 vs. C ++ 11 ( intvs. double):

#include <stdio.h>

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

En C autosignifica variable local. En C90 está bien omitir el tipo de variable o función. Por defecto es int. En C ++ 11 autosignifica algo completamente diferente, le dice al compilador que infiera el tipo de la variable a partir del valor utilizado para inicializarla.

desintonizado
fuente
10
C90 tiene auto?
Seth Carnegie
22
@SethCarnegie: Sí, es una clase de almacenamiento; es lo que sucede por defecto cuando lo omites, por lo que nadie lo usó y cambiaron su significado. Creo que es intpor defecto. Esto es inteligente! +1
usuario541686
55
C11 no tiene implícito int.
R .. GitHub DEJA DE AYUDAR A ICE
23
@KeithThompson Ah, supongo que te refieres a lo inferido int. Aún así, en el mundo real, donde hay toneladas de código heredado y el líder del mercado aún no ha implementado C99 y no tiene intención de hacerlo, hablar de "una versión obsoleta de C" es absurdo.
Jim Balter
11
"Todas las variables DEBEN tener una clase de almacenamiento explícito. La suya, la gerencia superior"
btown
120

Otro ejemplo que no he visto mencionado aún, este destaca una diferencia de preprocesador:

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

Esto imprime "falso" en C y "verdadero" en C ++: en C, cualquier macro indefinida se evalúa en 0. En C ++, hay 1 excepción: "verdadero" se evalúa en 1.

Godlygeek
fuente
2
Interesante. ¿Alguien sabe la razón detrás de este cambio?
antred
3
porque "verdadero" es una palabra clave / valor válido, por lo que se evalúa como verdadero como cualquier "valor verdadero" (como cualquier número entero positivo). Todavía puede hacer #define true false para imprimir "false" también en C ++;)
CoffeDeveloper
22
#define true false ಠ_ಠ
Bryan Boettcher
2
@DarioOO ¿no resultará tal redefinición en UB?
Ruslan
3
@DarioOO: Sí, estás equivocado. La redefinición de palabras clave no está permitida, el castigo se deja al destino (UB). No obstante, el preprocesador es una fase separada de compilación.
Deduplicador
108

Por estándar C ++ 11:

a. El operador de coma realiza la conversión de valor a valor en C pero no en C ++:

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

En C ++ el valor de esta expresión será 100 y en C será sizeof(char*).

si. En C ++, el tipo de enumerador es su enumeración. En C, el tipo de enumerador es int.

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

Esto significa que sizeof(int)puede no ser igual a sizeof(E).

C. En C ++, una función declarada con una lista de parámetros vacía no toma argumentos. En C la lista de parámetros vacíos significa que se desconoce el número y el tipo de parámetros de función.

   int f();           // int f(void) in C++
                      // int f(*unknown*) in C
Kirill Kobelev
fuente
El primero también está definido en la implementación como el de Alexey. Pero +1.
Seth Carnegie
1
@Seth, todo el material anterior está tomado directamente del Anexo C.1 del estándar C ++ 11
Kirill Kobelev
Sí, pero todavía está definido por la implementación. sizeof(char*)podría ser 100 en cuyo caso el primer ejemplo produciría el mismo comportamiento observable en C y C ++ (es decir, aunque el método de obtención ssería diferente, sterminaría siendo 100). El OP mencionó que este tipo de comportamiento definido por la implementación estaba bien, ya que solo quería evitar las respuestas de los abogados de idiomas, por lo que el primero está bien por su excepción. Pero el segundo es bueno en cualquier caso.
Seth Carnegie
55
Hay una solución fácil: simplemente cambie el ejemplo a:char arr[sizeof(char*)+1]; int s = sizeof(0, arr);
Mankarse
55
Para evitar diferencias definidas por la implementación, también puede usar void *arr[100]. En este caso, un elemento tiene el mismo tamaño que un puntero al mismo elemento, por lo tanto, siempre que haya 2 o más elementos, la matriz debe ser más grande que la dirección de su primer elemento.
finnw
53

Este programa imprime 1en C ++ y 0en C:

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

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

Esto sucede porque hay una double abs(double)sobrecarga en C ++, por lo que abs(0.6)regresa 0.6mientras que en C regresa 0debido a la conversión implícita de doble a int antes de invocar int abs(int). En C, tienes que usar fabspara trabajar double.

Pavel Chikulaev
fuente
55
tuvo que depurar el código de otra persona con ese problema. Oh, cómo me encantó eso. De todos modos, su programa también imprime 0 en C ++. C ++ tiene que usar el encabezado "cmath" ver comparación primero regresando 0 ideone.com/0tQB2G 2do regresando 1 ideone.com/SLeANo
CoffeDeveloper
Me alegra saber que no soy el único que encuentra esta diferencia mediante la depuración. Recién probado en VS2013, un archivo vacío con solo este contenido generará 1 si la extensión es .cpp y 0 si la extensión es .c. Parece que <math.h> está incluido indirectamente en VS.
Pavel Chikulaev
Y parece que en VS C ++, <math.h> incluye cosas de C ++ en el espacio de nombres global, donde en cuanto a GCC no lo es. Sin embargo, no estoy seguro de cuál es el comportamiento estándar.
Pavel Chikulaev
2
Este ejemplo de código en particular depende de la implementación: stdlib.hsolo define abs(int)y abs(long); La versión abs(double)es declarada por math.h. Entonces este programa aún puede llamar a la abs(int)versión. Es un detalle de implementación si stdlib.htambién hace math.hque se incluya. (Creo que sería un error si abs(double)se llamaran, pero math.hno se incluyeron otras especificaciones ).
MM
1
Un problema secundario es que, aunque el estándar C ++ parece decir que incluir <math.h>también incluye las sobrecargas adicionales; en la práctica resulta que todos los compiladores principales no incluyen esas sobrecargas a menos que se use el formulario <cmath>.
MM
38
#include <stdio.h>

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

En C, esto imprime cualquier valor que sizeof(int)tenga el sistema actual, que generalmente 4se usa en la mayoría de los sistemas de hoy en día.

En C ++, esto debe imprimir 1.

Adam Rosenfield
fuente
3
Sí, en realidad estaba familiarizado con este truco, ya que 'c' es un int en C y un char en C ++, pero aún así es bueno tenerlo aquí.
Sean
99
Esa sería una pregunta interesante para la entrevista, especialmente para las personas que ponen expertos de c / c ++ en sus CV
Martin Beckett
2
Sin embargo, un poco descuidado. Todo el propósito de sizeof es que no necesites saber exactamente qué tan grande es un tipo.
Dana the Sane
13
En C, el valor se define como implementación y 1 es una posibilidad. (En C ++ tiene que imprimir 1 como se indica.)
Programador de Windows
3
En realidad, tiene un comportamiento indefinido en ambos casos. %dno es el especificador de formato correcto para size_t.
R .. GitHub DEJA DE AYUDAR AL HIELO
37

Otra sizeoftrampa: expresiones booleanas.

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

Es igual a sizeof(int)en C, porque la expresión es de tipo int, pero generalmente es 1 en C ++ (aunque no es obligatorio que sea). En la práctica, casi siempre son diferentes.

Alex B
fuente
66
Uno !debería ser suficiente para a bool.
Alexey Frunze
44
!! es el operador de conversión de int a booleano :)
EvilTeach
1
sizeof(0)está 4en C y C ++ porque 0es un valor entero r. sizeof(!0)está 4en C y 1en C ++. NO lógico opera en operandos de tipo bool. Si el valor int es 0que se convierte implícitamente en false(un valor bool r), entonces se invierte, lo que da como resultado true. Ambos truey falseson valores bool en C ++ y el sizeof(bool)is 1. Sin embargo, en C se !0evalúa como 1, que es un valor de tipo int. El lenguaje de programación C no tiene ningún tipo de datos bool por defecto.
Galaxy
26

El lenguaje de programación C ++ (3a edición) ofrece tres ejemplos:

  1. sizeof ('a'), como mencionó @Adam Rosenfield;

  2. // comentarios utilizados para crear código oculto:

    int f(int a, int b)
    {
        return a //* blah */ b
            ;
    }
  3. Estructuras, etc., ocultando cosas en nuestros ámbitos, como en su ejemplo.

derobert
fuente
25

Un viejo castaño que depende del compilador de C, que no reconoce los comentarios de fin de línea de C ++ ...

...
int a = 4 //* */ 2
        +2;
printf("%i\n",a);
...
dmckee --- gatito ex moderador
fuente
21

Otro listado por el Estándar 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)); 
}
Johannes Schaub - litb
fuente
entonces obtienes diferencias de relleno?
v.oddou
Ah lo siento, lo tengo, hay otro xen la parte superior. Pensé que habías dicho "la matriz a".
v.oddou
20

Las funciones en línea en C tienen por defecto un ámbito externo donde las de C ++ no.

Compilar los dos archivos siguientes juntos imprimiría "Estoy en línea" en el caso de GNU C pero nada para C ++.

Archivo 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;
}

Archivo 2

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

Además, C ++ trata implícitamente cualquier constglobal como a staticmenos que se declare explícitamente extern, a diferencia de C, que externes el valor predeterminado.

fkl
fuente
Realmente no lo creo. Probablemente has perdido el punto. No se trata de la definición de struct st que simplemente se usa para hacer que el código sea válido en c ++. El punto es que destaca el comportamiento diferente de las funciones en línea en c vs c ++. Lo mismo se aplica a extern. Ninguno de estos se discute en ninguna de las soluciones.
fkl
2
¿Cuál es el comportamiento diferente de las funciones en línea y externque se demuestra aquí?
Seth Carnegie
Está escrito con bastante claridad. "Las funciones en línea en c predeterminadas al alcance externo donde como en c ++ no lo están (el código lo muestra). También C ++ trata implícitamente cualquier const global como alcance de archivo a menos que se declare explícitamente extern, a diferencia de C en la que extern es el predeterminado. ejemplo puede ser creado para eso ". Estoy perplejo: ¿no es comprensible?
fkl
12
@fayyazkl El comportamiento que se muestra se debe solo a la diferencia de búsqueda ( struct funvs fn) y no tiene nada que ver si la función está en línea. El resultado es idéntico si elimina el inlinecalificador.
Alex B
3
En ISO C este programa está mal formado: inlineno se agregó hasta C99, pero en C99 fun()no se puede llamar sin un prototipo en su alcance. Así que supongo que esta respuesta solo se aplica a GNU C.
MM
16
struct abort
{
    int x;
};

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

Devuelve con el código de salida de 0 en C ++ o 3 en C.

Este truco probablemente podría usarse para hacer algo más interesante, pero no podía pensar en una buena forma de crear un constructor que fuera aceptable para C. Traté de hacer un ejemplo igualmente aburrido con el constructor de copias, que dejaría una discusión ser aprobado, aunque de una manera no portátil:

struct exit
{
    int x;
};

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

    exit(code);

    return 0;
}

Sin embargo, VC ++ 2005 se negó a compilar eso en modo C ++, quejándose de cómo se redefinió el "código de salida". (Creo que este es un error del compilador, a menos que de repente haya olvidado cómo programar). Sin embargo, salió con un código de salida de proceso de 1 cuando se compiló como C.


fuente
Su segundo ejemplo usando exit, no se compila en gcc o g ++, desafortunadamente. Sin embargo, es una buena idea.
Sean,
1
exit(code)Es una declaración válida de una variable codede tipo exit, aparentemente. (Ver "análisis más irritante", que es un problema diferente pero similar).
user253751
16
#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;
}

Este programa imprime 128( 32 * sizeof(double)) cuando se compila usando un compilador de C ++ y 4cuando se compila usando un compilador de C.

Esto se debe a que C no tiene la noción de resolución de alcance. En C, las estructuras contenidas en otras estructuras se ponen dentro del alcance de la estructura externa.

wefwefa3
fuente
Este es interesante! (Creo que quieres decir en 32*sizeof(double)lugar de 32 :))
user1354557
3
tenga en cuenta que está obteniendo UB imprimiendo size_tcon%d
phuclv
7

No olvide la distinción entre los espacios de nombres globales C y C ++. Supongamos que tienes un foo.cpp

#include <cstdio>

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

y un foo2.c

#include <stdio.h>

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

Ahora suponga que tiene un main.c y main.cpp que se ven así:

extern void foo(int);

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

Cuando se compila como C ++, usará el símbolo en el espacio de nombres global de C ++; en C usará el C uno:

$ 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
usuario23614
fuente
¿Te refieres a la especificación de vinculación?
user541686
nombre destrozado. Los nombres de C ++ tienen prefijos y sufijos mientras que C no
CoffeDeveloper
El cambio de nombre no forma parte de la especificación de C ++. ¿Está prohibido en C?
skyking
55
Este es un comportamiento indefinido (definición múltiple de foo). No hay "espacios de nombres globales" separados.
MM
4
int main(void) {
    const int dim = 5; 
    int array[dim];
}

Esto es bastante peculiar, ya que es válido en C ++ y en C99, C11 y C17 (aunque opcional en C11, C17); pero no válido en C89.

En C99 +, crea una matriz de longitud variable, que tiene sus propias peculiaridades sobre las matrices normales, ya que tiene un tipo de tiempo de ejecución en lugar de un tipo de tiempo de compilación, y sizeof arrayno es una expresión constante entera en C. En C ++, el tipo es totalmente estático.


Si intenta agregar un inicializador aquí:

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

es válido C ++ pero no C, porque las matrices de longitud variable no pueden tener un inicializador.

Antti Haapala
fuente
0

Esto se refiere a valores y valores en C y C ++.

En el lenguaje de programación C, tanto los operadores de incremento previo como los de incremento posterior devuelven valores, no valores. Esto significa que no pueden estar en el lado izquierdo del =operador de asignación. Ambas declaraciones darán un error de compilación en C:

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

Sin embargo, en C ++, el operador anterior al incremento devuelve un valor l , mientras que el operador posterior al incremento devuelve un valor r. ¡Significa que se puede colocar una expresión con el operador de pre-incremento en el lado izquierdo del =operador de asignación!

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

Ahora, ¿por qué es así? El incremento posterior incrementa la variable y devuelve la variable como estaba antes. ocurriera el incremento. En realidad, esto es solo un valor. El valor anterior de la variable a se copia en un registro como temporal, y luego se incrementa a. Pero el valor anterior de a es devuelto por la expresión, es un valor r. Ya no representa el contenido actual de la variable.

El pre-incremento primero incrementa la variable, y luego devuelve la variable tal como se convirtió después del incremento. En este caso, no necesitamos almacenar el valor anterior de la variable en un registro temporal. Simplemente recuperamos el nuevo valor de la variable después de que se ha incrementado. Entonces, el pre-incremento devuelve un valor l, devuelve la variable a sí mismo. Podemos usar asignar este valor de l a otra cosa, es como la siguiente declaración. Esta es una conversión implícita de lvalue en rvalue.

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

Como el incremento previo devuelve un valor l, también podemos asignarle algo. Las siguientes dos declaraciones son idénticas. En la segunda asignación, primero se incrementa a, luego su nuevo valor se sobrescribe con 2.

int a;
a = 2;
++a = 2;  // Valid in C++.
Galaxia
fuente
3
No hay "válido en C" aquí.
o11c
0

Las estructuras vacías tienen tamaño 0 en C y 1 en C ++:

#include <stdio.h>

typedef struct {} Foo;

int main()
{
    printf("%zd\n", sizeof(Foo));
    return 0;
}
Daniel Frużyński
fuente
1
No, la diferencia es que C no tiene estructuras vacías, excepto como una extensión del compilador, es decir, este código no coincide "es válido tanto en C como en C ++"
Antti Haapala