Un code valide à la fois en C et en C ++ peut-il produire un comportement différent lorsqu'il est compilé dans chaque langage?
C et C++ ont beaucoup de différences, et tout le code C valide n'est pas un code C++ valide.
(Par "valide", je veux dire un code standard avec un comportement défini, c’est-à-dire non spécifique à la mise en oeuvre/indéfini/etc.)
Existe-t-il un scénario dans lequel un morceau de code valide à la fois en C et en C++ produirait un comportement différent lorsqu’il est compilé avec un compilateur standard dans chaque langage?
Pour en faire une comparaison raisonnable/utile (j'essaie d'apprendre quelque chose de pratiquement utile, et non d'essayer de trouver des failles évidentes dans la question), supposons:
- Rien de lié au préprocesseur (ce qui signifie pas de piratage avec
#ifdef __cplusplus
, pragmas, etc.) - Tout ce qui est défini par l'implémentation est le même dans les deux langues (par exemple, limites numériques, etc.)
- Nous comparons des versions raisonnablement récentes de chaque norme (par exemple, C++ 98 et C90 ou ultérieur).
Si les versions importent, veuillez indiquer quelles versions de chacune d’elles produisent un comportement différent.
Ce qui suit, valide en C et C++, va (probablement) donner des valeurs différentes dans i
en C et C++:
int i = sizeof('a');
Voir Taille du caractère ('a') en C/C++ pour une explication de la différence.
Un autre de cet 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;
}
Voici un exemple qui tire parti de la différence entre les appels de fonction et les déclarations d'objet en C et C++, ainsi que du fait que C90 autorise l'appel de fonctions non déclarées:
#include <stdio.h>
struct f { int x; };
int main() {
f();
}
int f() {
return printf("hello");
}
En C++, cela n'imprimera rien car un f
temporaire est créé et détruit, mais en C90, il imprimera hello
car les fonctions peuvent être appelées sans avoir été déclarées.
Si vous vous demandiez si le nom f
était utilisé deux fois, les normes C et C++ le permettaient explicitement. Pour créer un objet, vous devez dire struct f
pour ne pas être ambiguë si vous voulez que la structure soit laissée. off struct
si vous voulez la fonction.
Pour C++ vs C90, il existe au moins un moyen d'obtenir un comportement différent qui n'est pas défini par l'implémentation. C90 n'a pas de commentaires sur une seule ligne. Avec un peu de soin, nous pouvons utiliser cela pour créer une expression avec des résultats totalement différents en C90 et en C++.
int a = 10 //* comment */ 2
+ 3;
En C++, tout ce qui va du //
à la fin de la ligne est un commentaire.
int a = 10 + 3;
C90 n'ayant pas de commentaire sur une seule ligne, seul le /* comment */
est un commentaire. Le premier /
et le 2
font tous les deux partie de l'initialisation. Il en résulte:
int a = 10 / 2 + 3;
Donc, un compilateur C++ correct donnera 13, mais un compilateur C90 strictement correct.
C90 vs. C++ 11 (int
vs. double
):
#include <stdio.h>
int main()
{
auto j = 1.5;
printf("%d", (int)sizeof(j));
return 0;
}
En C auto
signifie variable locale. En C90, vous pouvez omettre le type de variable ou de fonction. La valeur par défaut est int
. En C++ 11, auto
signifie quelque chose de complètement différent. Il indique au compilateur de déduire le type de la variable à partir de la valeur utilisée pour l'initialiser.
Un autre exemple que je n'ai pas encore vu mentionné, celui-ci soulignant une différence de pré-processeur:
#include <stdio.h>
int main()
{
#if true
printf("true!\n");
#else
printf("false!\n");
#endif
return 0;
}
Ceci affiche "false" en C et "true" en C++ - En C, toute macro non définie est évaluée à 0. En C++, il existe une exception: "true" est évaluée à 1.
Selon la norme C++ 11:
a. L'opérateur de virgule effectue la conversion lvalue en rvalue en C mais pas C++:
char arr[100];
int s = sizeof(0, arr); // The comma operator is used.
En C++, la valeur de cette expression sera 100 et en C, ce sera sizeof(char*)
.
b. En C++, le type d'énumérateur est son enum. En C, le type d'énumérateur est int.
enum E { a, b, c };
sizeof(a) == sizeof(int); // In C
sizeof(a) == sizeof(E); // In C++
Cela signifie que sizeof(int)
peut ne pas être égal à sizeof(E)
.
c. En C++, une fonction déclarée avec une liste de paramètres vide ne prend aucun argument. La liste de paramètres vide en C signifie que le nombre et le type de paramètres de fonction sont inconnus.
int f(); // int f(void) in C++
// int f(*unknown*) in C
Ce programme imprime 1
en C++ et 0
en C:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int d = (int)(abs(0.6) + 0.5);
printf("%d", d);
return 0;
}
Cela se produit car il y a une surcharge de double abs(double)
en C++. Ainsi, abs(0.6)
renvoie 0.6
alors qu'en C, il renvoie 0
en raison d'une conversion implicite de double en int avant d'appeler int abs(int)
. En C, vous devez utiliser fabs
pour travailler avec double
.
Un autre piège sizeof
: expressions booléennes.
#include <stdio.h>
int main() {
printf("%d\n", (int)sizeof !0);
}
Cela équivaut à sizeof(int)
en C, car l'expression est de type int
, mais correspond généralement à 1 en C++ (bien que ce ne soit pas obligatoire). En pratique, ils sont presque toujours différents.
#include <stdio.h>
int main(void)
{
printf("%d\n", (int)sizeof('a'));
return 0;
}
En C, ceci imprime quelle que soit la valeur de sizeof(int)
sur le système actuel, qui est généralement 4
dans la plupart des systèmes couramment utilisés de nos jours.
En C++, cela doit imprimer 1.
Le langage de programmation C++ (3ème édition) donne trois exemples:
sizeof ('a'), comme l'a mentionné @ Adam Rosenfield;
//
commentaires utilisés pour créer du code caché:int f(int a, int b) { return a //* blah */ b ; }
Les structures, etc., cachent des objets dans des champs d’application, comme dans votre exemple.
Un vieux châtaignier qui dépend du compilateur C, ne reconnaissant pas les commentaires de fin de ligne C++ ...
...
int a = 4 //* */ 2
+2;
printf("%i\n",a);
...
Une autre liste répertoriée par le standard 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));
}
Les fonctions inline en C utilisent par défaut une étendue externe, contrairement à celles de C++.
La compilation des deux fichiers suivants ensemble afficherait le "Je suis inline" dans le cas de GNU C mais rien pour le C++.
Fichier 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;
}
Fichier 2
#include <stdio.h>
inline void fun(void)
{
printf("I am inline\n");
}
En outre, C++ traite implicitement toute const
globale comme static
à moins qu'elle ne soit explicitement déclarée extern
, contrairement à C dans lequel extern
est la valeur par défaut.
struct abort
{
int x;
};
int main()
{
abort();
return 0;
}
Renvoie avec le code de sortie 0 en C++ ou 3 en C.
Cette astuce pourrait probablement être utilisée pour faire quelque chose de plus intéressant, mais je ne pouvais pas penser à un bon moyen de créer un constructeur qui plairait à C. J'ai essayé de faire un exemple aussi ennuyeux avec le constructeur de copie, qui laisserait un argument être adopté, bien que de manière plutôt non portable:
struct exit
{
int x;
};
int main()
{
struct exit code;
code.x=1;
exit(code);
return 0;
}
VC++ 2005 a cependant refusé de compiler cela en mode C++, se plaignant de la façon dont le "code de sortie" avait été redéfini. (Je pense que c'est un bogue du compilateur, sauf si j'ai soudainement oublié comment programmer.) Il est sorti avec un code de sortie de processus de 1 lorsqu'il a été compilé en C.
#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;
}
Ce programme imprime 128
(32 * sizeof(double)
) lorsqu'il est compilé à l'aide d'un compilateur C++ et 4
lorsqu'il est compilé à l'aide d'un compilateur C.
En effet, C n’a pas la notion de résolution de la portée. En C, les structures contenues dans d'autres structures sont intégrées dans la portée de la structure externe.
N'oubliez pas la distinction entre les espaces de noms globaux C et C++. Supposons que vous ayez un foo.cpp
#include <cstdio>
void foo(int r)
{
printf("I am C++\n");
}
et un foo2.c
#include <stdio.h>
void foo(int r)
{
printf("I am C\n");
}
Supposons maintenant que vous avez un main.c et main.cpp qui ressemblent tous les deux à ceci:
extern void foo(int);
int main(void)
{
foo(1);
return 0;
}
Une fois compilé en C++, il utilisera le symbole dans l'espace de noms global C++. en C il utilisera le C un:
$ 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
int main(void) {
const int dim = 5;
int array[dim];
}
Ceci est assez particulier en ce sens qu’il est valide en C++ et en C99, C11 et C17 (bien qu’il soit facultatif en C11, C17); mais non valide en C89.
En C99 +, il crée un tableau de longueur variable, qui a ses propres particularités par rapport aux tableaux normaux, car il possède un type d'exécution au lieu d'un type à la compilation, et sizeof array
n'est pas une expression constante entière en C. le type est totalement statique.
Si vous essayez d’ajouter un initialiseur ici:
int main(void) {
const int dim = 5;
int array[dim] = {0};
}
est valide C++ mais pas C, car tableaux de longueur variable ne peut pas avoir d'initialiseur.
Les structures vides ont la taille 0 en C et 1 en C++:
#include <stdio.h>
typedef struct {} Foo;
int main()
{
printf("%zd\n", sizeof(Foo));
return 0;
}
Ceci concerne les lvalues et les rvalues en C et C++.
Dans le langage de programmation C, les opérateurs de pré-incrémentation et de post-incrémentation renvoient des valeurs et non des valeurs. Cela signifie qu'ils ne peuvent pas être à gauche de l'opérateur d'affectation =
. Ces deux instructions donneront une erreur de compilation 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 */
Cependant, en C++, l'opérateur de pré-incrémentation renvoie une valeur lvalue, tandis que l'opérateur de post-incrémentation renvoie une valeur rvalue. Cela signifie qu'une expression avec l'opérateur de pré-incrémentation peut être placée à gauche de l'opérateur d'affectation =
!
int a = 5;
a++ = 2; // error: lvalue required as left operand of assignment
++a = 2; // No error: a gets assigned to 2!
Maintenant, pourquoi est-ce ainsi? Le post-incrément incrémente la variable et renvoie la variable telle qu'elle était avant l'incrément s'est produit. Ceci est en fait juste une valeur. L'ancienne valeur de la variable a est copiée dans un registre en tant que temporaire, puis a est incrémentée. Mais l'ancienne valeur de a est renvoyée par l'expression, c'est une valeur. Il ne représente plus le contenu actuel de la variable.
Le pré-incrément incrémente d'abord la variable, puis renvoie la variable telle qu'elle est devenue après l'incrément s'est produit. Dans ce cas, il n'est pas nécessaire de stocker l'ancienne valeur de la variable dans un registre temporaire. Nous venons de récupérer la nouvelle valeur de la variable après son incrémentation. Donc, la pré-incrémentation retourne une lvalue, elle retourne la variable a elle-même. Nous pouvons utiliser assigner cette valeur à quelque chose d'autre, cela ressemble à la déclaration suivante. Ceci est une conversion implicite de lvalue en rvalue.
int x = a;
int x = ++a;
Comme la pré-incrémentation retourne une valeur lvalue, nous pouvons également lui affecter quelque chose. Les deux déclarations suivantes sont identiques. Dans la deuxième affectation, a commence par être incrémenté, puis sa nouvelle valeur est remplacée par 2.
int a;
a = 2;
++a = 2; // Valid in C++.