compilación mixta c y cpp
#ifdef __cplusplus
extern "C" {
#endif
extern int test(int, int);
#ifdef __cplusplus
}
#endif
En este código, el código entre #ifdef __cplusplus y #endif es para garantizar que el compilador maneje correctamente las diferencias de sintaxis entre C y C++ cuando se utilizan declaraciones y definiciones de funciones en lenguaje C en C++. Esto se debe a que existen algunas diferencias entre C y C++, incluida la sobrecarga de nombres de funciones, conversiones de tipos, etc.
En este código, "C" externo es una característica de C++ que le dice al compilador que trate las funciones en la forma C. Específicamente:
#ifdef __cplusplus: esta directiva de compilación condicional comprueba si se está compilando código C++. __cplusplus es una macro que se define cuando el compilador compila código C++. Por lo tanto, si el código se compila en C++, esta condición se cumple.
externa “C” { : si está compilando en C++, entonces el "C" externo le dice al compilador que las funciones en el siguiente bloque de código deben vincularse en C, no en C++. Esto se debe a que en C++, los nombres de funciones se pueden sobrecargar (el mismo nombre de función puede tener diferentes listas de parámetros), pero no existe tal concepto en C. Entonces, al envolver las declaraciones de funciones en "C" externo, se asegura de que estén vinculadas en forma C para que el código cpp se pueda usar con código C puro sin problemas.
#endif: esta es la instrucción final de la compilación condicional. Se utiliza para finalizar el bloque de compilación condicional iniciado por #ifdef __cplusplus. Si no está compilando código C++, el código de este bloque se ignora.
En resumen, el propósito de este código es garantizar que cuando se utilizan funciones del lenguaje C en C++, el compilador no las confunda con las características de sintaxis de C++, garantizando así la corrección del código. Esto es útil cuando se trata de interoperar con código C, ya que existen algunas diferencias importantes entre C++ y C.
Cuando se compila C++, se cambiará el nombre de la función.
C++ adaptará el nombre de la función durante la compilación. Este proceso se llama alteración de nombres. Esto se debe a que C++ permite la sobrecarga de funciones, es decir, se pueden definir múltiples funciones con el mismo nombre siempre que sus listas de parámetros sean diferentes. Para distinguir estas funciones sobrecargadas, el compilador de C++ modifica el nombre de la función para incluir información de parámetros en el nombre de la función, creando así un identificador de función único.
Por ejemplo, si tiene las siguientes dos funciones:
int add(int a, int b);
double add(double a, double b);
En tiempo de compilación, el compilador de C++ alteró los nombres de estas dos funciones para distinguirlas. El resultado de la manipulación de nombres puede convertirse en identificadores como _Z3addii y _Z3adddd, según el compilador y la plataforma.
Sin embargo, el lenguaje C no admite la sobrecarga de funciones, por lo que no es necesario modificar nombres en C. El nombre de la función permanece sin cambios en C. Es por eso que al interactuar con código C en C++ es necesario usar la declaración "C" externa para indicarle al compilador que no decore el nombre de la función para poder vincularse correctamente al código C.
Por lo tanto, para garantizar que el código C++ interactúe correctamente con el código C, se deben utilizar declaraciones "C" externas para evitar que el compilador de C++ adorne el nombre de la función, manteniendo así la compatibilidad con el código C. Esto garantiza que el código C++ coincida correctamente con las funciones C sin decoración de nombres.
Explicación detallada desde el punto de vista del montaje.
La manipulación de nombres es un concepto que va desde los lenguajes de alto nivel (como C ++) hasta el nivel del lenguaje ensamblador e implica la forma en que los nombres de funciones y clases se representan en el código ensamblador. Diferentes compiladores y plataformas pueden tener diferentes reglas de modificación de nombres. A continuación ilustraré este concepto con un ejemplo simple.
Primero, veamos un código C++ simple (externo "C"):
#include <iostream>
extern "C"
{
int add(int a, int b) {
return a + b;
}
}
int main() {
int result = add(3, 4);
std::cout << result << std::endl;
return 0;
}
A continuación, compilaremos este código usando g++ y veremos el código ensamblador resultante a través de una herramienta ensambladora. Puede utilizar los siguientes comandos para compilar el código y generar un archivo ensamblador:
g++ -S -o example.s example.cpp
En el comando anterior, la opción -S le dice a g++ que genere código ensamblador y lo envíe al archivo example.s. Ahora, echemos un vistazo al archivo example.s generado:
.file "example.cpp"
.text
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.globl add
.type add, @function
add:
.LFB1731:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1731:
.size add, .-add
.globl main
.type main, @function
main:
.LFB1732:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $4, %esi
movl $3, %edi
call add
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %esi
leaq _ZSt4cout(%rip), %rax
movq %rax, %rdi
call _ZNSolsEi@PLT
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
movq %rdx, %rsi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1732:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2232:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L7
cmpl $65535, -8(%rbp)
jne .L7
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rdi
call _ZNSt8ios_base4InitC1Ev@PLT
leaq __dso_handle(%rip), %rax
movq %rax, %rdx
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rsi
movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
movq %rax, %rdi
call __cxa_atexit@PLT
.L7:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2232:
.size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I_add, @function
_GLOBAL__sub_I_add:
.LFB2233:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $65535, %esi
movl $1, %edi
call _Z41__static_initialization_and_destruction_0ii
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2233:
.size _GLOBAL__sub_I_add, .-_GLOBAL__sub_I_add
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_add
.hidden __dso_handle
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
En el código ensamblador anterior, nos centramos en la función agregar y la función principal. Tenga en cuenta los siguientes puntos:
-
.globl add significa que la función agregar es globalmente visible porque la llamamos en la función principal.
-
.type add, @function indica que la función agregar es una función.
-
.size add, .-add proporciona el tamaño de la función agregar.
-
agregar: identifica el punto de entrada de la función agregar.
-
call add es una instrucción para llamar a la función add en la función principal.
En este ejemplo, puede ver que el nombre de la función agregar no ha cambiado significativamente en el código ensamblador, porque es una función C normal. La decoración de nombres a menudo se vuelve más compleja cuando se trata de características del lenguaje de alto nivel, como sobrecarga de funciones, espacios de nombres, clases, etc. Diferentes compiladores y plataformas pueden usar diferentes reglas para la modificación de nombres, por lo que los métodos específicos de modificación de nombres serán diferentes.
Aquí está el código ensamblador sin "C" externa:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
std::cout << result << std::endl;
return 0;
}
Utilice el siguiente comando nuevamente para compilar el código y generar un archivo ensamblador:
g++ -S -o example.s example.cpp
Vea el archivo example.s generado:
.file "example.cpp"
.text
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.globl _Z3addii
.type _Z3addii, @function
_Z3addii:
.LFB1731:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1731:
.size _Z3addii, .-_Z3addii
.globl main
.type main, @function
main:
.LFB1732:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $4, %esi
movl $3, %edi
call _Z3addii
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %esi
leaq _ZSt4cout(%rip), %rax
movq %rax, %rdi
call _ZNSolsEi@PLT
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
movq %rdx, %rsi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1732:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2232:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L7
cmpl $65535, -8(%rbp)
jne .L7
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rdi
call _ZNSt8ios_base4InitC1Ev@PLT
leaq __dso_handle(%rip), %rax
movq %rax, %rdx
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rsi
movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
movq %rax, %rdi
call __cxa_atexit@PLT
.L7:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2232:
.size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I__Z3addii, @function
_GLOBAL__sub_I__Z3addii:
.LFB2233:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $65535, %esi
movl $1, %edi
call _Z41__static_initialization_and_destruction_0ii
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2233:
.size _GLOBAL__sub_I__Z3addii, .-_GLOBAL__sub_I__Z3addii
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I__Z3addii
.hidden __dso_handle
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
Para ver el verdadero efecto de la alteración de nombres, intente crear un ejemplo de C++ más complejo que incluya sobrecarga de clases y funciones, y observe el código ensamblador generado para comprender las reglas de alteración de nombres en diferentes situaciones. Sin embargo, cabe señalar que la modificación de nombres suele ser un detalle de implementación interna del compilador, y diferentes compiladores pueden tener diferentes esquemas de modificación de nombres.
Aquí hay un ejemplo de C++ más complejo que involucra sobrecarga de clases y funciones:
#include <iostream>
class Math {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
};
int main() {
Math math;
int intResult = math.add(3, 4);
double doubleResult = math.add(2.5, 3.7);
std::cout << "Integer result: " << intResult << std::endl;
std::cout << "Double result: " << doubleResult << std::endl;
return 0;
}
En este ejemplo, definimos una clase llamada Math que contiene dos funciones de suma, una para sumar números enteros y otra para sumar números de punto flotante. Estas dos funciones tienen el mismo nombre pero diferentes tipos de parámetros, esto es sobrecarga de funciones.
A continuación, compilamos este ejemplo y vemos el código ensamblador generado. Utilice el siguiente comando para generar el archivo de ensamblaje:
g++ -S -o complex_example.s complex_example.cpp
Ahora, veamos las partes del archivo complex_example.s generado relacionadas con la clase Math y la función agregar:
.file "complex_example.cpp"
.text
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.section .text._ZN4Math3addEii,"axG",@progbits,_ZN4Math3addEii,comdat
.align 2
.weak _ZN4Math3addEii
.type _ZN4Math3addEii, @function
_ZN4Math3addEii:
.LFB1731:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp)
movl %esi, -12(%rbp)
movl %edx, -16(%rbp)
movl -12(%rbp), %edx
movl -16(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1731:
.size _ZN4Math3addEii, .-_ZN4Math3addEii
.section .text._ZN4Math3addEdd,"axG",@progbits,_ZN4Math3addEdd,comdat
.align 2
.weak _ZN4Math3addEdd
.type _ZN4Math3addEdd, @function
_ZN4Math3addEdd:
.LFB1732:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp)
movsd %xmm0, -16(%rbp)
movsd %xmm1, -24(%rbp)
movsd -16(%rbp), %xmm0
addsd -24(%rbp), %xmm0
movq %xmm0, %rax
movq %rax, %xmm0
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1732:
.size _ZN4Math3addEdd, .-_ZN4Math3addEdd
.section .rodata
.LC2:
.string "Integer result: "
.LC3:
.string "Double result: "
.text
.globl main
.type main, @function
main:
.LFB1733:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -21(%rbp), %rax
movl $4, %edx
movl $3, %esi
movq %rax, %rdi
call _ZN4Math3addEii
movl %eax, -20(%rbp)
movsd .LC0(%rip), %xmm0
movq .LC1(%rip), %rdx
leaq -21(%rbp), %rax
movapd %xmm0, %xmm1
movq %rdx, %xmm0
movq %rax, %rdi
call _ZN4Math3addEdd
movq %xmm0, %rax
movq %rax, -16(%rbp)
leaq .LC2(%rip), %rax
movq %rax, %rsi
leaq _ZSt4cout(%rip), %rax
movq %rax, %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq %rax, %rdx
movl -20(%rbp), %eax
movl %eax, %esi
movq %rdx, %rdi
call _ZNSolsEi@PLT
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
movq %rdx, %rsi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E@PLT
leaq .LC3(%rip), %rax
movq %rax, %rsi
leaq _ZSt4cout(%rip), %rax
movq %rax, %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq %rax, %rdx
movq -16(%rbp), %rax
movq %rax, %xmm0
movq %rdx, %rdi
call _ZNSolsEd@PLT
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
movq %rdx, %rsi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E@PLT
movl $0, %eax
movq -8(%rbp), %rdx
subq %fs:40, %rdx
je .L7
call __stack_chk_fail@PLT
.L7:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1733:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2237:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L10
cmpl $65535, -8(%rbp)
jne .L10
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rdi
call _ZNSt8ios_base4InitC1Ev@PLT
leaq __dso_handle(%rip), %rax
movq %rax, %rdx
leaq _ZStL8__ioinit(%rip), %rax
movq %rax, %rsi
movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
movq %rax, %rdi
call __cxa_atexit@PLT
.L10:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2237:
.size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB2238:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $65535, %esi
movl $1, %edi
call _Z41__static_initialization_and_destruction_0ii
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2238:
.size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_main
.section .rodata
.align 8
.LC0:
.long -1717986918
.long 1074633113
.align 8
.LC1:
.long 0
.long 1074003968
.hidden __dso_handle
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
En este código ensamblador, puede ver que los nombres de las dos funciones de adición han cambiado y se denominan _ZN4Math3addEii y _ZN4Math3addEdd. El compilador genera estas decoraciones de nombres en función de los tipos de parámetros de la función para garantizar que las dos funciones sobrecargadas se puedan distinguir en el nivel de ensamblado.
Este ejemplo demuestra el uso de la decoración de nombres en situaciones que involucran sobrecarga de funciones para garantizar que se llame a la función correcta. En aplicaciones prácticas, la modificación de nombres es muy importante para admitir características del lenguaje de alto nivel, como la sobrecarga de funciones, la sobrecarga de operadores y las funciones miembro de clases.