Pourquoi pouvez-vous avoir la définition de méthode dans le fichier d'en-tête en C ++ alors qu'en C vous ne pouvez pas?

23

En C, vous ne pouvez pas avoir la définition / implémentation de fonction dans le fichier d'en-tête. Cependant, en C ++, vous pouvez avoir une implémentation complète de la méthode dans le fichier d'en-tête. Pourquoi le comportement est-il différent?

Joshua Partogi
la source

Réponses:

28

En C, si vous définissez une fonction dans un fichier d'en-tête, cette fonction apparaîtra dans chaque module compilé qui inclut ce fichier d'en-tête et un symbole public sera exporté pour la fonction. Donc, si la fonction additup est définie dans header.h, et foo.c et bar.c incluent tous les deux header.h, alors foo.o et bar.o incluront tous les deux des copies d'additup.

Lorsque vous allez lier ces deux fichiers objets ensemble, l'éditeur de liens verra que le symbole additup est défini plusieurs fois et ne le permettra pas.

Si vous déclarez que la fonction est statique, aucun symbole ne sera exporté. Les fichiers objets foo.o et bar.o contiendront tous les deux des copies distinctes du code de la fonction, et ils pourront les utiliser, mais l'éditeur de liens ne pourra voir aucune copie de la fonction, donc il ne se plaindra pas. Bien entendu, aucun autre module ne pourra non plus voir la fonction. Et votre programme sera gonflé de deux copies identiques de la même fonction.

Si vous déclarez uniquement la fonction dans le fichier d'en-tête, mais ne la définissez pas, puis la définissez dans un seul module, l'éditeur de liens verra une copie de la fonction, et chaque module de votre programme pourra la voir et utilise le. Et votre programme compilé ne contiendra qu'une seule copie de la fonction.

Ainsi, vous pouvez avoir la définition de la fonction dans le fichier d'en-tête en C, c'est juste un mauvais style, une mauvaise forme et une mauvaise idée générale.

(Par «déclarer», je veux dire fournir un prototype de fonction sans corps; par «définir», je veux dire fournir le code réel du corps de fonction; c'est la terminologie C standard.)

David Conrad
la source
2
Ce n'est pas une si mauvaise idée - ce genre de choses peut être trouvé même dans les en-têtes GNU Libc.
SK-logic
mais qu'en est-il de l'habillage idiomatique du fichier d'en-tête dans une directive de compilation conditionnelle? Ensuite, même avec la fonction déclarée ET définie dans l'en-tête, elle ne sera chargée qu'une seule fois. Je suis nouveau à C, donc je peux me méprendre.
user305964
2
@papiro Le problème est que l'habillage ne protège que pendant une seule exécution du compilateur. Donc, si foo.c est compilé en foo.o dans une exécution, bar.c en bar.o dans une autre, et foo.o et bar.o sont liés dans a.out dans un troisième (comme c'est typique), cet habillage n'en empêche pas plusieurs instances, une dans chaque fichier objet.
David Conrad
Le problème décrit ici #ifndef HEADER_Hn'est-il pas censé empêcher?
Robert Harvey
27

C et C ++ se comportent à peu près de la même manière à cet égard - vous pouvez avoir des inlinefonctions dans les en-têtes. En C ++, toute méthode dont le corps est à l'intérieur de la définition de classe l'est implicitement inline. Si vous souhaitez faire de même en C, déclarez les fonctions static inline.

Simon Richter
la source
" déclarer les fonctionsstatic inline " ... et vous aurez toujours plusieurs copies de la fonction dans chaque unité de traduction qui l'utilise. En C ++ avec non static inlinefonction, vous n'aurez qu'une seule copie. Pour avoir réellement l'implémentation dans l'en-tête en C, vous devez 1) marquer l'implémentation comme inline(par exemple inline void func(){do_something();}), et 2) dire en fait que cette fonction sera dans une unité de traduction particulière (par exemple void func();).
Ruslan
6

Le concept d'un fichier d'en-tête a besoin d'une petite explication:

Soit vous donnez un fichier sur la ligne de commande du compilateur, soit vous faites un '#include'. La plupart des compilateurs acceptent un fichier de commandes avec l'extension c, C, cpp, c ++, etc. comme fichier source. Cependant, ils incluent généralement une option de ligne de commande pour permettre également l'utilisation de toute extension arbitraire dans un fichier source.

Généralement, le fichier donné sur la ligne de commande est appelé «Source», et celui inclus est appelé «En-tête».

L'étape du préprocesseur les prend tous et fait tout apparaître comme un gros fichier unique au compilateur. Ce qui était dans l'en-tête ou dans la source n'est en fait pas pertinent à ce stade. Il y a généralement une option d'un compilateur qui peut afficher la sortie de cette étape.

Donc, pour chaque fichier qui a été donné sur la ligne de commande du compilateur, un énorme fichier est donné au compilateur. Cela peut avoir du code / des données qui occuperont de la mémoire et / ou créeront un symbole à référencer à partir d'autres fichiers. Maintenant, chacun d'eux va générer une image «objet». L'éditeur de liens peut donner un «symbole en double» si le même symbole se trouve dans plus de deux fichiers objets qui sont liés ensemble. C'est peut-être la raison; il n'est pas conseillé de mettre du code dans un fichier d'en-tête, ce qui peut créer des symboles dans le fichier objet.

Les 'inline' sont généralement en ligne .. mais lors du débogage, ils peuvent ne pas être en ligne. Alors pourquoi l'éditeur de liens ne donne-t-il pas d'erreurs définies de façon multiple? Simple ... Ce sont des symboles «faibles», et tant que toutes les données / code pour un symbole faible de tous les objets ont la même taille et le même contenu, le lien gardera une copie et supprimera la copie des autres objets. Ça marche.

vrdhn
la source
3

Vous pouvez le faire en C99: les inlinefonctions sont assurées d'être fournies ailleurs, donc si une fonction n'a pas été insérée, sa définition est traduite en une déclaration (c'est-à-dire que l'implémentation est rejetée). Et, bien sûr, vous pouvez utiliser static.

SK-logic
la source
1

Citations standard C ++

Le projet de norme C ++ 17 N4659 10.1.6 "Le spécificateur en ligne" dit que les méthodes sont implicitement en ligne:

4 Une fonction définie dans une définition de classe est une fonction en ligne.

et puis plus bas, nous voyons que les méthodes en ligne peuvent non seulement peuvent, mais doivent être définies sur toutes les unités de traduction:

6 Une fonction ou une variable en ligne doit être définie dans chaque unité de traduction dans laquelle elle est utilisée ou doit avoir exactement la même définition dans tous les cas (6.2).

Ceci est également explicitement mentionné dans une note au 12.2.1 "Fonctions membres":

1 Une fonction membre peut être définie (11.4) dans sa définition de classe, auquel cas il s'agit d'une fonction membre inline (10.1.6) [...]

3 [Remarque: Il peut y avoir au plus une définition d'une fonction membre non en ligne dans un programme. Il peut y avoir plus d'une définition de fonction de membre en ligne dans un programme. Voir 6.2 et 10.1.6. - note de fin]

Implémentation de GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Compilez et visualisez les symboles:

g++ -c main.cpp
nm -C main.o

production:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

on voit alors man nmque le MyClass::myMethodsymbole est marqué comme faible sur les fichiers objets ELF, ce qui implique qu'il peut apparaître sur plusieurs fichiers objets:

"W" "w" Le symbole est un symbole faible qui n'a pas été spécifiquement marqué comme symbole d'objet faible. Lorsqu'un symbole défini faible est lié à un symbole défini normalement, le symbole défini normalement est utilisé sans erreur. Lorsqu'un symbole faible non défini est lié et que le symbole n'est pas défini, la valeur du symbole est déterminée sans erreur spécifique au système. Sur certains systèmes, les majuscules indiquent qu'une valeur par défaut a été spécifiée.

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
la source
-4

Probablement pour la même raison que vous devez placer l'implémentation complète de la méthode dans la définition de classe en Java.

Ils peuvent ressembler, avec des crochets ondulés et plusieurs des mêmes mots clés, mais ce sont des langues différentes.

Paul Butcher
la source
1
Non, il y a en fait une vraie réponse, et l'un des principaux objectifs du C ++ était d'être rétrocompatible avec C.
Ed S.
4
Non, il a été conçu pour avoir "Un degré élevé de compatibilité C" et "Aucune incompatibilité gratuite avec C". (tous deux de Stroustrup). Je conviens qu'une réponse plus approfondie pourrait être donnée, pour souligner pourquoi cette incompatibilité particulière n'est pas gratuite. N'hésitez pas à en fournir un.
Paul Butcher
Je l'aurais fait, mais Simon Richter l'avait déjà fait quand j'ai posté ça. On peut chipoter sur la différence entre «rétrocompatible» et «A High degree of C compatibility», mais le fait demeure que cette réponse est incorrecte. La dernière déclaration serait correcte si nous comparions C # et C ++, mais pas tellement avec C et C ++.
Ed S.