Perché è possibile avere la definizione del metodo all'interno del file di intestazione in C ++ quando in C non è possibile?


23

In C, non è possibile avere la definizione / implementazione della funzione all'interno del file di intestazione. Tuttavia, in C ++ è possibile avere l'implementazione completa del metodo all'interno del file di intestazione. Perché il comportamento è diverso?

Risposte:


28

In C, se si definisce una funzione in un file di intestazione, tale funzione verrà visualizzata in ciascun modulo che viene compilato che include quel file di intestazione e verrà esportato un simbolo pubblico per la funzione. Quindi, se la funzione additup è definita in header.h e foo.c e bar.c includono entrambi header.h, allora foo.o e bar.o includeranno entrambi copie di additup.

Quando vai a collegare insieme questi due file di oggetti, il linker vedrà che il simbolo additup è definito più di una volta e non lo consentirà.

Se si dichiara che la funzione è statica, non verrà esportato alcun simbolo. I file oggetto foo.o e bar.o conterranno entrambi copie separate del codice per la funzione e saranno in grado di usarli, ma il linker non sarà in grado di vedere alcuna copia della funzione, quindi non si lamenterà. Ovviamente, nessun altro modulo sarà in grado di vedere la funzione. E il tuo programma sarà gonfiato con due copie identiche della stessa funzione.

Se dichiari solo la funzione nel file di intestazione, ma non la definisci, e poi la definisci in un solo modulo, il linker vedrà una copia della funzione e ogni modulo nel tuo programma sarà in grado di vederla e usalo. E il tuo programma compilato conterrà solo una copia della funzione.

Quindi, puoi avere la definizione della funzione nel file di intestazione in C, è solo cattivo stile, cattiva forma e una cattiva idea a tutto tondo.

(Con "dichiarare" intendo fornire un prototipo di funzione senza un corpo; con "definisci" intendo fornire il codice effettivo del corpo della funzione; questa è la terminologia C standard.)


2
Non è una cattiva idea: questo genere di cose può essere trovato anche nelle intestazioni GNU Libc.
Logica SK

ma che dire dell'involucro idiomatico del file header in una direttiva di compilazione condizionale? Quindi, anche con la funzione dichiarata E definita nell'intestazione, verrà caricata una sola volta. Sono nuovo di C, quindi potrei essere frainteso.
user305964,

2
@papiro Il problema è che il wrapping protegge solo durante una singola esecuzione del compilatore. Quindi se foo.c è compilato in foo.o in una corsa, bar.c in bar.o in un'altra, e foo.o e bar.o sono collegati in a.out in un terzo (come è tipico), quel wrapping non ne impedisce più istanze, una in ciascun file oggetto.
David Conrad,

Il problema qui descritto non #ifndef HEADER_Hè cosa dovrebbe prevenire?
Robert Harvey,

27

C e C ++ si comportano allo stesso modo in questo senso: puoi avere inlinefunzioni nelle intestazioni. In C ++, qualsiasi metodo il cui corpo è all'interno della definizione di classe è implicitamente inline. Se si desidera fare lo stesso in C, dichiarare le funzioni static inline.


" dichiara le funzionistatic inline " ... e avrai ancora più copie della funzione in ogni unità di traduzione che la utilizza. In C ++ con non- static inlinefunzione avrai solo una copia. Per avere effettivamente l'implementazione nell'intestazione in C, devi 1) contrassegnare l'implementazione come inline(es. inline void func(){do_something();}), E 2) effettivamente dire che questa funzione sarà in qualche particolare unità di traduzione (es void func();.).
Ruslan,

6

Il concetto di un file di intestazione richiede una piccola spiegazione:

O dai un file sulla riga di comando del compilatore o fai un '#include'. La maggior parte dei compilatori accetta un file di comando con estensione c, C, cpp, c ++, ecc. Come file di origine. Tuttavia, di solito includono un'opzione della riga di comando per consentire l'utilizzo di qualsiasi estensione arbitraria anche a un file di origine.

Generalmente il file fornito nella riga di comando si chiama 'Origine' e quello incluso si chiama 'Intestazione'.

Il passaggio del preprocessore li prende effettivamente tutti e fa apparire tutto come un singolo file di grandi dimensioni al compilatore. Ciò che era nell'intestazione o nella fonte in realtà non è rilevante a questo punto. Di solito c'è un'opzione di un compilatore che può mostrare l'output di questo stage.

Quindi, per ogni file che è stato dato sulla riga di comando del compilatore, viene dato un enorme file al compilatore. Questo può contenere codice / dati che occuperanno memoria e / o creeranno un simbolo a cui fare riferimento da altri file. Ora ciascuno di questi genererà un'immagine "oggetto". Il linker può dare un "simbolo duplicato" se lo stesso simbolo viene trovato in più di due file oggetto che sono collegati tra loro. Forse questo è il motivo; non è consigliabile inserire il codice in un file di intestazione, che può creare simboli nel file oggetto.

Gli "inline" sono generalmente incorporati ... ma durante il debug potrebbero non essere incorporati. Quindi perché il linker non fornisce errori definiti in modo multiplo? Semplice ... Questi sono simboli "deboli" e fintanto che tutti i dati / codice per un simbolo debole di tutti gli oggetti hanno le stesse dimensioni e il contenuto, il collegamento manterrà una copia e rilascerà copia da altri oggetti. Funziona.


3

Puoi farlo in C99: le inlinefunzioni sono garantite per essere fornite da qualche altra parte, quindi se una funzione non è stata incorporata, la sua definizione viene tradotta in una dichiarazione (cioè l'implementazione viene scartata). E, ovviamente, puoi usare static.


1

Citazioni standard C ++

La bozza standard C ++ 17 N4659 10.1.6 "L' identificatore in linea" afferma che i metodi sono implicitamente in linea:

4 Una funzione definita all'interno di una definizione di classe è una funzione incorporata.

e poi più in basso vediamo che i metodi inline non solo possono, ma devono essere definiti su tutte le unità di traduzione:

6 Una funzione o variabile in linea deve essere definita in ogni unità di traduzione in cui è utilizzata per un errore e deve avere esattamente la stessa definizione in ogni caso (6.2).

Ciò è anche esplicitamente menzionato in una nota in 12.2.1 "Funzioni membro":

1 Una funzione membro può essere definita (11.4) nella sua definizione di classe, nel qual caso si tratta di una funzione membro in linea (10.1.6) [...]

3 [Nota: in un programma può esserci al massimo una definizione di una funzione membro non inline. Potrebbe esserci più di una definizione di funzione membro in linea in un programma. Vedi 6.2 e 10.1.6. - nota finale]

Implementazione di GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

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

Compilare e visualizzare simboli:

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

produzione:

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

quindi vediamo da man nmche il MyClass::myMethodsimbolo è contrassegnato come debole sui file oggetto ELF, il che implica che può apparire su più file oggetto:

"W" "w" Il simbolo è un simbolo debole che non è stato specificamente etichettato come simbolo di un oggetto debole. Quando un simbolo definito debole è collegato con un simbolo definito normale, il simbolo definito normale viene utilizzato senza errori. Quando un simbolo indefinito debole è collegato e il simbolo non è definito, il valore del simbolo viene determinato in modo specifico per il sistema senza errori. Su alcuni sistemi, lettere maiuscole indicano che è stato specificato un valore predefinito.


-4

Probabilmente per lo stesso motivo per cui è necessario inserire l'implementazione completa del metodo all'interno della definizione della classe in Java.

Potrebbero sembrare simili, con parentesi quadre e molte delle stesse parole chiave, ma sono lingue diverse.


1
No, in realtà esiste una vera risposta, e uno degli obiettivi principali del C ++ era di essere retrocompatibile con C.
Ed S.

4
No, è stato progettato per avere "Un alto grado di compatibilità C" e "Nessuna incompatibilità gratuita con C". (entrambi da Stroustrup). Concordo sul fatto che potrebbe essere data una risposta più approfondita, per evidenziare perché questa particolare incompatibilità non è gratuita. Sentiti libero di fornirne uno.
Paul Butcher,

Lo avrei fatto, ma Simon Richter lo aveva già fatto quando l'ho pubblicato. Possiamo cavillare sulla differenza tra "retrocompatibile" e "Un alto grado di compatibilità C", ma resta il fatto che questa risposta non è corretta. L'ultima affermazione sarebbe corretta se stessimo confrontando C # e C ++, ma non tanto con C e C ++.
Ed S.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.