C++編【言語解説】 第10章 言語間の連携

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

C言語との連携

C言語で書かれたソースと、C++ で書かれたソースとを混在することは可能です。 しかし、実際に試してみると、うまくいかないことがあります。

// main.cpp
#include <iostream>
#include "sub.h"

int main()
{
	std::cout << function(100) << std::endl;
	return 0;
}
/* sub.c */
#include "sub.h"

int function(int num)
{
	return num * 2;
}
/* sub.h */

int function(int num);

main.cpp は C++ で書かれています。 一方、sub.c と sub.h はC言語のつもりで書かれています。 「つもり」というのは、sub.c も sub.h も、C++ とみなしても何ら問題のないソースコードになっているからです。 ここでは、拡張子が .c であることで、コンパイラがC言語であるとみなしています。

このプログラムを、VisualC++ でビルドすると、次のようなログが出ます。

1>コンパイルしています...
1>sub.c
1>コンパイルしています...
1>main.cpp
1>リンクしています...
1>main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl function(int)" (?function@@YAHH@Z) が関数 _main で参照されました。
1>C:\test.exe : fatal error LNK1120: 外部参照 1 が未解決です。

このログから分かるように、sub.c 、main.cpp のコンパイルには成功しています。 しかし、その後の過程でリンクに失敗しているようです。
"int __cdecl function(int)" というのが、sub.c にある function関数のことを指していますが、 これが「未解決の外部シンボル」であると言われています。 そのため、main関数から function関数を参照しようとしても、「function」という名前を見つけられずエラーになっています。

ログを良く見ると、"?function@@YAHH@Z" という謎の文字列が含まれています。 C++ としてコンパイルされた main.cpp は、function関数のことを "?function@@YAHH@Z" という名前で参照しようとします。
実は、関数などの名前はコンパイラによって別の名前に置き換えられていることがあります。 C++ の場合、オーバーロードや名前空間の機能があるので、同名の関数が複数存在し得ますから、 何らかのルールに従って区別を付ける必要があります。
なお、このように名前を置き換えることを、名前マングルと言います。

そのため、名前マングルされた後の名前で呼び出すようにすれば、正しく動作するのですが、 名前マングル後の名前がどんな風になるのかは不定(コンパイラの実装次第)ですし、 "?function@@YAHH@Z" のような名前だと、「?」や「@」のように、C++ のソースコード上で名前として使えない文字が含まれるので、 やはりどうにもなりません。
そこで、関数宣言の方に手を加えます。

/* sub.h */

extern "C" int function(int num);

このように、extern "C" という目印を付けることで、 C言語の方式に従った関数名を使うようになります。("C" はC言語を意味しています)。

また、extern "C" は次のように書くことができます。

/* sub.h */

extern "C" {

int function(int num);

}

複数の名前に対して一気に extern "C" を適用したい場合は、この書き方の方が便利です。

ところで、extern "C" 自体が C++ の機能なので、先ほどの例の sub.h をC言語のプログラムからインクルードすると、 エラーになってしまいます。 そこで、extern "C" を使うときには次のように書くことになります。

/* sub.h */

#ifdef __cplusplus
extern "C" {
#endif

int function(int num);

#ifdef __cplusplus
}
#endif

__cplusplus は、C++ のプログラムでだけ定義されるマクロです。 こうすることで、C++ のときには extern "C" が有効になり、 C言語プログラムの中で使うときには extern "C" が無効になります。

プログラムの全体像を書くと、次のようになります。

/* main.c */
#include <stdio.h>
#include "sub.h"

int main(void)
{
	printf("%d\n", function(100));
	return 0;
}
// sub.cpp
#include "sub.h"

int function(int num)
{
	return num * 2;
}
/* sub.h */

#ifdef __cplusplus
extern "C" {
#endif

int function(int num);

#ifdef __cplusplus
}
#endif

実行結果:

200

基本的にはこれで良いのですが、C++ の方がC言語よりも多機能ですから、 C++ にしか無い機能が使われていると、うまく動かない可能性はあります。

stdio と iostream の連携

C言語のプログラムと、C++ のプログラムを混在して使うことは可能ですが、入出力周りには注意が必要です。
C言語では stdio.h に用意されている機能を使い、C++ では iostream に用意されている機能が使えますが、 両者にとっての目的は同じですから、互いが好き勝手に動くと、不具合が起きる可能性があります。

C++ 側としては、デフォルトで両者の同期を取るようになっていますから、実のところ問題は起きませんが、 同期を取ることによって処理効率が大きく落ちていることがあります。 そのため、処理効率の低下を嫌うのなら、同期を取らないように設定を変えることも考えた方が良いでしょう。

stdio と iostream の同期を ON/OFF するためには、std::ios_base::sync_with_stdio関数を呼び出します。

bool sync_with_stdio (bool sync = true);

引数に true を渡すと、同期が ON になり、false を渡すと、同期が OFF になります。 戻り値は、以前の状態が返されます。 前述の通り、デフォルトでは同期 ON なので、初回呼び出し時の戻り値は必ず true になります。

実際に、どの程度パフォーマンスに変化があるのか確認してみましょう。 パフォーマンスの測定のために、コードライブラリppp_perform.h を使用しています。

#include <iostream>
#include <fstream>
#include <cstdio>
#include "ppp_perform.h"

#define TEST_TYPE 0    // 0: stdio
                       // 1: iostream(同期あり)
                       // 2: iostream(同期なし)

int main()
{
	static const int LOOP_TIMES = 10000;

#if TEST_TYPE == 0

	FILE* fp = std::fopen("test.txt", "r");

	PPP_CHECK_PERFORM_BEGIN(1);
	for (int i = 0; i < LOOP_TIMES; ++i) {
		char c = std::fgetc(fp);
		std::printf("%c", c);
	}
	std::printf("\n");
	PPP_CHECK_PERFORM_END("stdio");
	
	std::fclose(fp);

#elif TEST_TYPE == 1

	std::ifstream ifs("test.txt");

	PPP_CHECK_PERFORM_BEGIN(1);
	for (int i = 0; i < LOOP_TIMES; ++i) {
		char c;
		ifs >> c;
		std::cout << c;
	}
	std::cout << std::flush;
	PPP_CHECK_PERFORM_END("iostream (sync)");

#elif TEST_TYPE == 2
	std::ios::sync_with_stdio(false);

	std::ifstream ifs("test.txt");

	PPP_CHECK_PERFORM_BEGIN(1);
	for (int i = 0; i < LOOP_TIMES; ++i) {
		char c;
		ifs >> c;
		std::cout << c;
	}
	std::cout << std::flush;
	PPP_CHECK_PERFORM_END("iostream (no sync)");

#endif
}

TEST_TYPEマクロの値を変更してコンパイルして下さい。 結果は以下のようになりました。

方法 結果 (VC2013、Windows 7) 結果 (Xcode 8.3.3、macOS Sierra)
stdio 1.341秒 0.0209秒
iostream (同期あり) 1.856秒 0.0238秒
iostream (同期なし) 1.825秒 0.0176秒

このプログラムを試す限りでは、同期の有無の差はほとんど見られませんでした。 結果は環境や、プログラムの内容によっても大きく変わってくると思われますが、 速度が必要な場合には、std::ios_base::sync_with_stdio関数の存在を思い出して、 試してみると良いでしょう。

アセンブリ言語を使う

asm というキーワードを用いることで、 C++ のプログラム内に、アセンブリ言語のコードを埋め込むことができます。
このように、他のプログラミング言語のプログラム内にアセンブリ言語のコードを埋め込むことを、 インラインアセンブラと呼びます。

アセンブリ言語というのは、 コンピュータが直接理解できる機械語(マシン語)という言語を、 人間でも比較的読みやすいように記号化した言語です。
アセンブリ言語の1つの命令は、機械語の1つの命令と対応付いているため、 アセンブリ言語でプログラムを書くと、非常に効率の良いコードを作ることができる可能性があります。 また、直接的にレジスタを操作する等、アセンブリ言語でないとできないことも、一部には存在します。

実際のところ、現代のコンパイラは優秀であるため、C++ などのプログラミング言語で書いたプログラムをコンパイルしたコードでも、 ほとんどの場合、何の問題もないほど効率の良いコードになっています。 効率向上という意味合いで、インラインアセンブラを使う機会は少ないと言えます。
また、機械語というのは、CPU の種類によって異なるため、機械語と対応付いているアセンブリ言語も必然的に、CPU の種類によって異なります。 そのため、インラインアセンブラを使うと、他の CPU を使っている環境では動作しないプログラムになります (環境を判定してコードを分岐することは可能ですが、環境ごとに異なるコードを書かなければいけないということになります)。 更に、コンパイラの種類によっても、インラインアセンブラの構文が異なります

要するにインラインアセンブラを使うことで、 移植性の低下と引き換えに、高い処理効率を得たり、特殊な低レベルな処理を行ったりすることができるようになります。

C++編ですから、アセンブリ言語自体の使い方を解説するつもりはありませんが、 asmキーワードの使い方だけ紹介しておきます。

#include <iostream>

int main()
{
	int num;

#if defined(_MSC_VER)
	// VisualC++
	__asm {
		mov num, 100;
	}
	
#elif defined(__GNUC__)
	// Xcode
	__asm__ (
		"movl $100, %0;"
		:"=r"(num)
	);
	
#else
	// その他
	num = 100;
	
#endif

	std::cout << num << std::endl;
}

実行結果:

100

#if defined を使って、コンパイラの種類によって分岐させています。 _MSC_VER が定義されていれば VisualC++、__GNUC__ が定義されていれば Xcode であると判断しています。

C++ の標準規格としては asm というキーワードなのですが、 実際には、VisualC++ では __asm を使いますし、Xcode では __asm__ を使います。 していることは、C++ の構文内で宣言された変数num に 100 を代入しているだけですが、 構文がまったく違うことが分かると思います。


練習問題

問題@ extern "C" { } で囲むコードは、ヘッダファイルの数が多くなると毎回記述しなければならず、割と不便です。 #define を利用して、記述量を減らせないでしょうか?

問題A 次のヘッダファイルを、C言語と C++ で共用できるように修正して下さい。

/* sub.h */
#ifndef SUB_H
#define SUB_H

namespace MyLib {

enum Type {
	TYPE_A,
	TYPE_B,
};

struct Data {
	Type  type;
	char  name[16];
};


void print_data(const Data* data);

}

#endif

問題B 関数テンプレートをC言語からも利用することは可能でしょうか?


解答ページはこちら

参考リンク

更新履歴

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2014/10/18 clang 3.2 に対応。

'2014/3/19 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ