こちらの記事を読んで気になったので、自分なりにCとC++が混在している場合の注意点や差異について調べました。
「undefined referenceって言われたけど、なんとなくextern "C"を付けたら上手くいった・・・」という方向け。
gccコマンドを叩ける前提で書いています。gccコマンドが不安な方はこちら。
1. Cのみの場合
C言語だけで書かれたソースコードをコンパイルして実行してみます。
#include <stdio.h>
void func1()
{
printf("func1\n");
}
#include <stdio.h>
void func1(void);
int main()
{
func1();
}
$ gcc -c *.c
$ gcc -o program *.o
$ gcc ./program
func1
上手くいきました。ここで、オブジェクトファイルのシンボルをnmコマンドを使って確認してみます。シンボルというのは、プログラム中で関数を特定するための名前だと思ってよいです。
$ nm func1.o
0000000000000000 T func1
U puts
$ nm main.o
U func1
0000000000000000 T main
ここで大事なのは、func1()
関数のシンボルがfunc1
で統一されていることです。func1.oも、main.oも、同じfunc1
を使っているので、不整合が起きないのです。
2. C++のみの場合
同内容で、C++だけで書かれたソースコードをコンパイルして実行してみます。
#include <stdio.h>
void func1()
{
printf("func1\n");
}
#include <stdio.h>
void func1();
int main()
{
func1();
}
$ gcc -c *.cpp
$ gcc -o program *.o
$ gcc ./program
func1
上手くいきました。ここで、オブジェクトファイルのシンボルをnmコマンドを使って確認してみます。
$ nm func1.o
0000000000000000 T _Z5func1v
U puts
$ nm main.o
U _Z5func1v
0000000000000000 T main
先ほどとは違い、シンボルが_Z5func1v
になっていますが、統一はされていますので、不整合は起きません。ここでは、C++の場合はシンボルに関数名だけでなく色々な情報が付加される、ということがわかればよいです(マングリングと言います)。
3. CとC++が混在する場合
では、Cで関数定義をして、C++から呼び出す場合はどうなるでしょうか。
func1.c
とmain.cpp
をコンパイルして実行してみましょう。
$ gcc -c func1.c
$ gcc -c main.cpp
$ gcc -o program *.o
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x9): undefined reference to `func1()'
collect2: error: ld returned 1 exit status
ダメでした。シンボルを確認してみましょう。
$ nm func1.o
0000000000000000 T func1
U puts
$ nm main.o
U _Z5func1v
0000000000000000 T main
なぜダメかというと、C++ソースだったmain.o
側は_Z5func1v
を探しているのに、Cソースだったfunc1.o
側はfunc1
を提供しているからです。シンボルが一致していないので、関数が見つからなくなっているというわけです(リンケージが異なる)。
ではどうすればいいかというと、C++ソース上で、「func1はCリンケージですよ」と教えてあげればよいです。
#include <stdio.h>
//void func1(); // これはNG.
extern "C" void func1(); // Cで書かれた関数だと教えてあげる
int main()
{
func1();
}
$ gcc -c func1.c
$ gcc -c main.cpp
$ gcc -o program *.o
$ ./program
func1
上手くいきました。シンボルも確認してみましょう。
$ nm func1.o
0000000000000000 T func1
U puts
$ nm main.o
U func1
0000000000000000 T main
main.o
がシンボルfunc1
を探すようになったので、無事にシンボルが一致してうまくいくようになりました。
4. C++にマングリングがある意味
Cでは、シンボル名が単なる関数名ですが、C++では関数名だけでなく色々と情報が付加されています。少々複雑ですが、C++ではこれにより関数のオーバーロード(関数名は同じでも引数が異なる)が可能になっています。
#include <stdio.h>
void func1()
{
printf("func1\n");
}
void func1(int num)
{
printf("func1:%d\n", num);
}
#include <stdio.h>
void func1();
void func1(int num);
int main()
{
func1();
func1(10);
}
$ gcc -c func1.cpp
$ gcc -c main.cpp
$ gcc -o program *.o
$ ./program
func1
func1:10
シンボルを見てみましょう。
$ nm func1.o
000000000000001a T _Z5func1i
0000000000000000 T _Z5func1v
U printf
U puts
$ nm main.o
U _Z5func1i
U _Z5func1v
0000000000000000 T main
同じfunc1
という関数名でも、シンボルは_Z5func1i
と_Z5func1v
で異なっているため、正しくコンパイルできているのです。
また、C++では名前空間を分けることで、関数名も引数も全く同じ関数を使用することができます(名前空間を指定して呼び出す必要があります)。
以下は、C++側に同じ関数名の関数を2つ定義し、かつCリンケージにも同じ関数名の関数がある例です。乱暴なことをしているようですがシンボルを確認すると、別のシンボルがそれぞれ与えられており、問題ないことがわかります。
#include <stdio.h>
namespace name1
{
void func()
{
printf("func name1\n");
}
}
#include <stdio.h>
namespace name2
{
void func()
{
printf("func name2\n");
}
}
#include <stdio.h>
void func()
{
printf("func C\n");
}
#include <stdio.h>
namespace name1 {
void func();
}
namespace name2 {
void func();
}
extern "C" void func();
int main()
{
name1::func();
name2::func();
func();
}
$ gcc -c func_name1.cpp
$ gcc -c func_name2.cpp
$ gcc -c func.c
$ gcc -c main.cpp
$ gcc -o program *.o
$ ./program
func name1
func name2
func C
$ nm *.o
func.o:
0000000000000000 T func
U puts
func_name1.o:
0000000000000000 T _ZN5name14funcEv
U puts
func_name2.o:
0000000000000000 T _ZN5name24funcEv
U puts
main.o:
U _ZN5name14funcEv
U _ZN5name24funcEv
U func
0000000000000000 T main