¿Por qué puede tener la definición del método dentro del archivo de encabezado en C ++ cuando en C no puede?

23

En C, no puede tener la definición / implementación de la función dentro del archivo de encabezado. Sin embargo, en C ++ puede tener una implementación completa del método dentro del archivo de encabezado. ¿Por qué es diferente el comportamiento?

Joshua Partogi
fuente

Respuestas:

28

En C, si define una función en un archivo de encabezado, esa función aparecerá en cada módulo que se compila que incluya ese archivo de encabezado, y se exportará un símbolo público para la función. Entonces, si la función additup se define en header.h, y foo.c y bar.c ambos incluyen header.h, entonces foo.o y bar.o incluirán copias de additup.

Cuando vaya a vincular esos dos archivos de objetos, el vinculador verá que el complemento de símbolo se define más de una vez y no lo permitirá.

Si declara que la función es estática, no se exportará ningún símbolo. Los archivos de objeto foo.o y bar.o seguirán conteniendo copias separadas del código para la función, y podrán usarlos, pero el enlazador no podrá ver ninguna copia de la función, por lo que No me quejaré. Por supuesto, ningún otro módulo podrá ver la función tampoco. Y su programa se hinchará con dos copias idénticas de la misma función.

Si solo declara la función en el archivo de encabezado, pero no la define, y luego la define en un solo módulo, el vinculador verá una copia de la función, y cada módulo en su programa podrá verla y úsalo. Y su programa compilado contendrá solo una copia de la función.

Por lo tanto, puede tener la definición de la función en el archivo de encabezado en C, es simplemente un mal estilo, una forma incorrecta y una mala idea general.

(Por "declarar", me refiero a proporcionar un prototipo de función sin cuerpo; por "definir" me refiero a proporcionar el código real del cuerpo de la función; esta es la terminología estándar de C.)

David Conrad
fuente
2
No es una mala idea, este tipo de cosas se pueden encontrar incluso en los encabezados GNU Libc.
SK-logic
pero ¿qué pasa con el ajuste idiomático del archivo de encabezado en una directiva de compilación condicional? Luego, incluso con la función declarada Y definida en el encabezado, solo se cargará una vez. Soy nuevo en C, por lo que puedo estar malentendido.
user305964
2
@papiro El problema es que el ajuste solo protege durante una sola ejecución del compilador. Entonces, si foo.c se compila en foo.o en una ejecución, bar.c en bar.o en otra, y foo.o y bar.o están vinculados a a.out en un tercero (como es típico), ese ajuste no evita múltiples instancias de la misma, una en cada archivo de objeto.
David Conrad
¿No se describe aquí el problema que #ifndef HEADER_Hse supone que debe prevenir?
Robert Harvey
27

C y C ++ se comportan de la misma manera en este aspecto: puede tener inlinefunciones en los encabezados. En C ++, cualquier método cuyo cuerpo está dentro de la definición de clase está implícitamente inline. Si desea hacer lo mismo en C, declare las funciones static inline.

Simon Richter
fuente
" declara las funcionesstatic inline " ... y aún tendrás múltiples copias de la función en cada unidad de traducción que la use. En C ++ sin static inlinefunción, solo tendrá una copia. Para tener la implementación en el encabezado en C, debe 1) marcar la implementación como inline(por ejemplo inline void func(){do_something();}) y 2) decir que esta función estará en alguna unidad de traducción en particular (por ejemplo void func();).
Ruslan
6

El concepto de un archivo de encabezado necesita una pequeña explicación:

O le das un archivo en la línea de comando del compilador, o haces un '#include'. La mayoría de los compiladores aceptan un archivo de comando con la extensión c, C, cpp, c ++, etc. como el archivo fuente. Sin embargo, generalmente incluyen una opción de línea de comandos para permitir el uso de cualquier extensión arbitraria a un archivo fuente también.

En general, el archivo dado en la línea de comando se llama 'Fuente', y el que se incluye se llama 'Encabezado'.

El paso del preprocesador en realidad los toma a todos y hace que todo aparezca como un solo archivo grande para el compilador. Lo que estaba en el encabezado o en la fuente en realidad no es relevante en este momento. Generalmente hay una opción de un compilador que puede mostrar el resultado de esta etapa.

Entonces, para cada archivo que se proporcionó en la línea de comandos del compilador, se le entrega un archivo enorme al compilador. Esto puede tener código / datos que ocuparán memoria y / o crearán un símbolo para ser referenciado desde otros archivos. Ahora cada uno de estos generará una imagen de 'objeto'. El enlazador puede dar un 'símbolo duplicado' si el mismo símbolo se encuentra en más de dos archivos de objetos que se están vinculando entre sí. Quizás esta sea la razón; No se recomienda poner código en un archivo de encabezado, que puede crear símbolos en el archivo objeto.

Los 'en línea' generalmente están en línea ... pero al depurar pueden no estar en línea. Entonces, ¿por qué el enlazador no da errores definidos multiplicados? Simple ... Estos son símbolos 'débiles', y siempre y cuando todos los datos / códigos para un símbolo débil de todos los objetos tengan el mismo tamaño y contenido, el enlace mantendrá una copia y soltará una copia de otros objetos. Funciona.

vrdhn
fuente
3

Puede hacer esto en C99: inlinese garantiza que las funciones se proporcionarán en otro lugar, por lo que si una función no estaba en línea, su definición se traduce en una declaración (es decir, la implementación se descarta). Y, por supuesto, puedes usar static.

SK-logic
fuente
1

Cotizaciones estándar de C ++

El borrador estándar de C ++ 17 N4659 10.1.6 "El especificador en línea" dice que los métodos están implícitamente en línea:

4 Una función definida dentro de una definición de clase es una función en línea.

y luego más abajo vemos que los métodos en línea no solo pueden, sino que deben definirse en todas las unidades de traducción:

6 Una función o variable en línea se definirá en cada unidad de traducción en la que se utilice odr y tendrá exactamente la misma definición en cada caso (6.2).

Esto también se menciona explícitamente en una nota en 12.2.1 "Funciones de miembro":

1 Se puede definir una función miembro (11.4) en su definición de clase, en cuyo caso es una función miembro en línea (10.1.6) [...]

3 [Nota: Puede haber como máximo una definición de una función miembro no en línea en un programa. Puede haber más de una definición de función miembro en línea en un programa. Ver 6.2 y 10.1.6. - nota final]

Implementación de GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

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

Compilar y ver símbolos:

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

salida:

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

entonces vemos man nmque el MyClass::myMethodsímbolo está marcado como débil en los archivos de objetos ELF, lo que implica que puede aparecer en múltiples archivos de objetos:

"W" "w" El símbolo es un símbolo débil que no ha sido etiquetado específicamente como un símbolo de objeto débil. Cuando un símbolo definido débil se vincula con un símbolo definido normal, el símbolo definido normal se usa sin error. Cuando se vincula un símbolo indefinido débil y el símbolo no está definido, el valor del símbolo se determina de una manera específica del sistema sin error. En algunos sistemas, mayúsculas indica que se ha especificado un valor predeterminado.

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

Probablemente por la misma razón por la que debe poner la implementación del método completo dentro de la definición de clase en Java.

Pueden parecer similares, con corchetes y muchas de las mismas palabras clave, pero son idiomas diferentes.

Carnicero paul
fuente
1
No, en realidad hay una respuesta real, y uno de los objetivos principales de C ++ era ser retrocompatible con C.
Ed S.
44
No, fue diseñado para tener "Un alto grado de compatibilidad con C" y "Sin incompatibilidades gratuitas con C". (Ambos de Stroustrup). Estoy de acuerdo en que se podría dar una respuesta más profunda, para resaltar por qué esta incompatibilidad particular no es gratuita. Siéntase libre de suministrar uno.
Paul Butcher
Lo habría hecho, pero Simon Richter ya lo tenía cuando publiqué eso. Podemos objetar la diferencia entre "compatible con versiones anteriores" y "Un alto grado de compatibilidad con C", pero el hecho es que esta respuesta es incorrecta. La última afirmación sería correcta si estuviéramos comparando C # y C ++, pero no tanto con C y C ++.
Ed S.