詳細な読み物:extern "C"の詳細な分析

 

 

[ガイド]:この記事では、extern "C"の基本的な原理と実際のアプリケーションを詳細に分析します。

以下はテキストです

あなたが取り組んだシステムでは、次のようなコードが表示されるのではないかと思います。

 

これは問題ないようですが、「ええと...はい、コードはこのように記述されており、これが原因で問題が発生したことはありません〜」と考える必要があります。

ヘッダーファイルがC ++プログラムによって参照されたことがない場合は、その通りです。

これはC ++と何の関係がありますか?__ cplusplusの名前を見て(前の2つのアンダースコアに注意してください)、C ++と多くの関係があることを知っておく必要があります。__cplusplusは、C ++仕様で指定された定義済みのマクロです。信頼できるのは、すべての最新のC ++コンパイラーが事前定義していることですが、すべてのC言語コンパイラーはそうではありません。さらに、仕様によれば、__ cplusplusの値は19 9 7 1 1 Lに等しくなければなりませんが、すべてのコンパイラがこれを実装しているわけではありません。たとえば、g ++コンパイラはその値を1と定義します。

したがって、上記のコードがC言語プログラムによって参照されている場合、その内容は次のコードと同等です。

 

この場合、前処理後にextern "C" {}が存在しないため、extern "C" {}と#includeディレクティブの関係は、当然、何もないところから発生します。

extern「C」の過去と現在

C ++コンパイラには、「名前マングリング」と呼ばれるジョブを専門とするDiabloがあります。C ++ソースファイルがコンパイルされると、動作を開始し、ソースファイルに表示されるすべての外部から見える名前を完全に認識できないように粉砕し、バイナリオブジェクトファイルのシンボルテーブルに保存します。

このようなモンスターがC ++の世界に存在する理由は、セマンティクスにあいまいさがない限り、C ++では名前のさまざまな定義が許可されているためです。たとえば、パラメータリストが異なる限り、2つの関数の名前を同じにすることができます。これは、関数のオーバーロードと呼ばれます。さらに、名前空間()である限り、2つの関数のプロトタイプ宣言をまったく同じにすることができます。場所の名前空間)は同じではありません。実際、異なる名前空間にある場合、関数名、変数名、型名のいずれであっても、すべての名前を繰り返すことができます。

さらに、C ++プログラムの構築方法は、C言語の伝統を継承しています。コンパイラは、コマンドラインで指定された各ソースコードファイルを独立したコンパイル単位として扱い、オブジェクトファイルを生成します。次に、リンカはこれらのオブジェクトファイルを検索します。シンボルテーブルはそれらをリンクして実行可能プログラムを生成します。

コンパイルとリンクは2つの段階です。実際、コンパイラとリンカは2つの完全に独立したツールです。コンパイラは、セマンティック分析を通じて同じ名前のシンボルの違いを知ることができますが、リンカは、オブジェクトファイルのシンボルテーブルに格納されている名前でのみオブジェクトを識別できます。

したがって、コンパイラによる名前の細断処理の目的は、リンカーが動作中に混乱するのを防ぎ、すべての名前を再エンコードして、グローバルに一意で一意の新しい名前を生成し、リンカーがそれぞれに対応する名前を正確に識別できるようにすることです。名前。オブジェクト。

ただし、C言語は単一の名前空間を持つ言語であり、関数のオーバーロードは許可されていません。つまり、コンパイルとリンクの範囲内では、C言語は同じ名前のオブジェクトを許可しません。たとえば、コンパイルユニットでは、関数がstaticで変更されているかどうかに関係なく、同じ名前の関数は許可されません。実行可能プログラムに対応するすべてのオブジェクトファイルでは、同じ名前のオブジェクトは許可されません。グローバル変数または関数を表します。したがって、C言語コンパイラは、名前に対して複雑な処理を実行する必要はありません(または、名前の前に1つのアンダースコア_を追加するなど、単純で一貫性のある装飾を作成するだけです)。

C ++の作成者であるBjarneStroustrupは、Cと互換性があり、既存の多数のCライブラリを再利用できることをC ++言語の重要な目標として最初にリストしました。ただし、2つの言語コンパイラは名前の処理に一貫性がないため、リンクプロセスに問題が発生します。

たとえば、次の内容のmy_handle.hという名前のヘッダーファイルがあります。

 

次に、C言語コンパイラを使用してmy_handle.cをコンパイルし、オブジェクトファイルmy_handle.oを生成します。C言語コンパイラは名前を壊さないため、my_handle.oのシンボルテーブルでは、これら3つの関数の名前はソースコードファイルの宣言と一致しています。

 

後で、C ++プログラムでこれらの関数を呼び出す必要があるため、ヘッダーファイルmy_handle.hも含まれています。このC ++ソースコードファイルの名前がmy_handle_client.cppであり、その内容が次のとおりであるとします。

 

なかでも太字部分は、つぶした後の3つの機能の名前の登場です。

次に、プログラムを機能させるには、my_handle.oとmy_handle_client.oをリンクする必要があります。2つのオブジェクトファイルは同じオブジェクトに対して異なる名前を持っているため、リンカは関連する「シンボルが定義されていません」エラーを報告します。

 

この問題を解決するために、C ++は外部の「言語文字列」として表されるリンケージ仕様の概念を導入しました。C++コンパイラで一般的にサポートされる「言語文字列」には、C言語に対応する「C」と「C ++」があります。そしてC ++言語。

リンク仕様の機能は、リンク仕様によって変更されたすべての宣言または定義を、名前、呼び出し規約など、指定された言語の方法で処理する必要があることをC ++コンパイラに通知することです。

リンク仕様には2つの用途があります。

1.次のような単一ステートメントのリンク仕様。

extern "C" void foo();

2.次のような宣言されたリンク仕様のセット。

extern "C"{   void foo();  int bar();}

前の例では、ヘッダーファイルmy_handle.hの内容を次のように変更した場合:

 

次に、C ++コンパイラを使用してmy_handle_client.cppを再コンパイルすると、生成されたオブジェクトファイルmy_handle_client.oのシンボルテーブルは次のようになります。

 

このことから、現時点では、extern "C"で変更された宣言によって生成されたシンボルは、C言語コンパイラによって生成されたシンボルと一致していることがわかります。このように、my_handle.oとmy_handle_client.oを再度リンクすると、「シンボル未定義」エラーは発生しなくなります。

ただし、この時点でmy_handle.cを再コンパイルすると、extern "C"はC ++の構文であり、C言語コンパイラーはそれを認識しないため、C言語コンパイラーは「構文エラー」を報告します。この時点で、マクロ__cplusplusを使用して、前に説明したようにCおよびC ++コンパイラーを識別できます。my_handle.hの改訂されたコードは次のとおりです。

 

ドアの後ろの未知の世界に注意してください

extern "C"の起源と目的を理解した後、元のトピックに戻って、extern "C" {...}に#includeディレクティブを配置できないのはなぜですか?

最初に、既存のah、bh、ch、およびfoo.cppの例を見てみましょう。ここで、foo.cppにはchが含まれ、chにはbhが含まれ、bhにはahが含まれます。

 

 

ここで、C ++コンパイラの前処理オプションを使用してfoo.cppをコンパイルすると、次の結果が得られます。

 

ご覧のとおり、#includeディレクティブをextern "C" {}に配置すると、extern "C" {}がネストされます。このネストは、C ++仕様で許可されています。入れ子が発生すると、最も内側の入れ子が優先されます。たとえば、次のコードでは、関数fooはC ++リンク仕様を使用し、関数バーはCリンク仕様を使用します。

 

C言語のヘッダーファイルに直接または間接的に依存するすべてのヘッダーファイルもC言語であることが保証できる場合は、C ++言語の仕様に従って、このネストに問題はありません。ただし、MSVC2005などの一部のコンパイラの実装に固有の場合、extern "C" {}の過度のネストが原因でエラーが報告される場合があります。この問題に関する限り、この入れ子は無意味なので、これについてマイクロソフトを非難しないでください。#includeディレクティブをextern "C" {}の外に配置することで、ネストを完全に回避できます。前の例では、各ヘッダーファイルの#includeディレクティブをextern "C" {}の外に移動し、C ++コンパイラの前処理オプションを使用してfoo.cppをコンパイルすると、次の結果が得られます。

 

このような結果は、MSVCを使用しても、コンパイルの問題を引き起こすことはありません。

#includeディレクティブをextern "C" {}に配置するもう1つの大きなリスクは、関数宣言のリンク仕様を誤って変更する可能性があることです。例:次のように、2つのヘッダーファイルah、bhがあります。bhにはahが含まれています。

 

ahの作者の当初の意図によれば、関数fooはC ++フリー関数であり、そのリンク仕様は「C ++」です。しかし、bhでは、#include "ah"がextern "C" {}内に配置されているため、関数fooのリンク仕様が誤って変更されています。

すべての#includeディレクティブはこの未知の世界を隠すため、意図的に探索しない限り、わかりません。すべての#includeディレクティブをextern "C" {}に入れると、何が起こりますか?どのような結果が生成され、どのようなリスクが発生しますか?それはもたらすだろう。「インクルードされたヘッダーファイルを確認でき、問題が発生しないことを保証できます」と言うかもしれません。しかし、なぜわざわざ?結局のところ、不要なものにお金を払う必要はありませんね。

 

Q&A

 

Q:extern "C"に#includeディレクティブを配置することはできませんか?

 

A:この世界のほとんどのルールと同じように、常に特別な状況があります。

 

場合によっては、ヘッダーファイルメカニズムを使用して、いくつかの問題を「スマートに」解決することができます。たとえば、#pragmapackの問題です。これらのヘッダーファイルと通常のヘッダーファイルの機能は同じではありません。C関数宣言や変数定義は配置されず、リンク指定はそれらのコンテンツに影響を与えません。この場合、これらのルールに従う必要はありません。

 

より一般的な原則は、これらすべての原則を理解した後、自分が何をしているかを理解している限り、それを実行することです。


Q:extern "C"は入れてはいけないと言っただけですが、何を入れることができますか?

 

A:リンク仕様は、関数と変数、および関数タイプを変更するためにのみ使用されます。したがって、厳密に言えば、これら3つのタイプのオブジェクトのみをextern "C"内に配置する必要があります。

 

ただし、非関数型の定義(構造体、列挙型など)などのC言語の他の要素を外部変数「C」に入れても、影響はありません。マクロ定義の前処理ディレクティブは言うまでもありません。

 

したがって、優れた組織と管理の習慣をより重視する場合は、外部の「C」宣言を使用する必要がある場合にのみ使用する必要があります。怠惰な場合でも、ほとんどの場合、ヘッダー自体のすべての定義と宣言をextern "C"に入れることは大きな問題ではありません。

 

Q:関数/変数宣言を含むCヘッダーファイルにextern "C"宣言がない場合はどうなりますか?

 

A:このヘッダーファイルがC ++コードで使用されることはないと判断できる場合は、そのままにしておきます。

 

しかし現実には、ほとんどの場合、将来を正確に予測することはできません。このextern "C"を今すぐ追加すれば、それほど費用はかかりませんが、今すぐ追加しないと、将来このヘッダーファイルが他の誰かのC ++プログラムに誤ってインクルードされたときに、他の人が必要になる可能性があります。エラーを見つけて問題を修正するため。

 

Q:C ++プログラムにC関数/変数宣言を含むCヘッダーファイルa。hをインクルードしたいが、外部「C」リンク仕様を使用していない場合はどうすればよいですか?

 

A:ああに追加してください。

 

ahにextern "C"がなく、b.cppにahが含まれている場合は、b.cppを追加できると提案する人もいます。

extern "C"
{
  #include "a.h"
}

これは邪悪な計画であり、前に説明した理由です。しかし、このスキームの背後にある仮定があるかもしれない、つまり、ああを変更することはできないということを議論する価値があります。変更できない理由は、次の2つの側面から考えられます。

 

1.ヘッダーファイルコードは他のチームまたはサードパーティ企業に属しており、コードを変更する権限がありません。


2.コードを変更する権限がありますが、このヘッダーファイルはレガシーシステムに属しているため、急いで変更すると、予期しない問題が発生する可能性があります。

 

最初のケースでは、不必要なトラブルを引き起こすので、自分で回避しようとしないでください。正しい解決策は、それをバグとして扱い、対応するチームまたはサードパーティ企業に欠陥レポートを送信することです。あなたが支払ったのがあなた自身の会社のチームまたはサードパーティの会社である場合、彼らはあなたのためにそのような変更を行う義務があります。彼らがこの問題の重要性を理解していない場合は、彼らに伝えてください。これらのヘッダーファイルが無料のオープンソースソフトウェアに属している場合は、自分で正しい変更を加え、パッチを開発チームにリリースしてください。

 

2番目のケースでは、この不要なセキュリティ意識を破棄する必要があります。まず第一に、ほとんどのヘッダーファイルでは、この変更は複雑でリスクの高い変更ではないため、すべてが制御可能な範囲内にあります。次に、ヘッダーファイルが乱雑で複雑な場合、レガシーではありませんが、システムは次のようになります。「問題が発生する前に触れないでください。」しかし、問題が発生したので、それに直面する方がよいでしょう。したがって、最善の戦略は、問題を整理する良い機会として扱うことです。清潔でリーズナブルな状態。

 

Q:コード内のextern "C"の表現は次のとおりですが、これは正しいですか?

 

A:わかりません。

 

C ++仕様によれば、__ cplusplusの値はゼロ以外の値である199711Lとして定義する必要があります。一部のコンパイラは仕様に従って実装していませんが、__ cplusplusの値がゼロ以外であることを保証できます。少なくとも私はこれまでのところ、どのコンパイラがそれを0として実装しているかを見ていません。この場合、#if __cplusplus ...#endifは完全に冗長です。

 

ただし、C ++コンパイラの製造元は非常に多いため、特定のコンパイラ、または特定のコンパイラの以前のバージョンで__cplusplusの値が0として定義されていないことを保証することはできません。ただし、それでも、マクロ__cplusplusがC ++コンパイラでのみ事前定義されていることが保証できる限り、#ifdef __cplusplus ... #endifを使用するだけで、意図の正確性を確保できます。#ifを追加で使用することもできます。 __cplusplus ...#endif代わりにそれは間違っています。

 

この場合のみ:つまり、特定のメーカーのC言語およびC ++言語コンパイラーには事前定義された__cplusplusがありますが、区別するために値は0でゼロ以外なので、#if __cplusplus ...#endifを使用してください。

現実の世界は非常に複雑なので、目標を明確にしてから、目標に基づいて対応する戦略を定義する必要があります。例:仕様に正しく準拠する複数のメインストリームコンパイラでコードをコンパイルできるようにすることが目標の場合は、#ifdef __cplusplus ...#endifを使用するだけで十分です。

 

ただし、製品がさまざまなコンパイラ(不明を含む)との互換性を試みる野心的なクロスプラットフォーム製品である場合は、次の方法を使用してさまざまな状況に対処する必要があります。__ALIEN_C_LINKAGE__は、CコンパイラとC ++コンパイラの両方でそれらを識別します。 __cplusplusマクロコンパイラを定義します。

 

これは機能するはずですが、各ヘッダーファイルにこのような大きなリストを書き込むと、見苦しいだけでなく、戦略が変更されるとどこでも変更されるという状況が発生します。DRY(Do n't Repeat Yourself)の原則に違反すると、常に追加料金を支払う必要があります。これを解決する簡単な解決策は、linkage.hなどの特定のヘッダーファイルを定義し、それにこの定義を追加することです。

 

 次の例では、cの関数宣言と定義はそれぞれcfun.hとcfun.cにあり、関数は文字列「this is c fun call」を出力し、c ++関数の宣言と定義はcppfun.hとcppfunにあります。それぞれ.cpp。関数は文字列 "this is cpp fun call"、コンパイル環境vc2010を出力します

 

cを呼び出すc ++メソッド(重要なのは、c ++の方法ではなく、cの方法でcの関数をコンパイルすることです)

 

(1)cfun.hは次のとおりです。

#ifndef _C_FUN_H_
#define _C_FUN_H_


    void cfun();


#endif

   cppfun.cppは次のとおりです。

//#include "cfun.h"  不需要包含cfun.h
#include "cppfun.h"
#include <iostream>
using namespace std;
extern "C"     void cfun(); //声明为 extern void cfun(); 错误


void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}


int main()
{
    cfun();
    return 0;
}

(2)cfun.hは上記と同じです

 

  cppfun.cppは次のとおりです。

extern "C"
{
    #include "cfun.h"//注意include语句一定要单独占一行;
}
#include "cppfun.h"
#include <iostream>
using namespace std;


void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}


int main()
{
    cfun();
    return 0;
}

(3)cfun.hは次のとおりです。

#ifndef _C_FUN_H_
#define _C_FUN_H_


#ifdef __cplusplus
extern "C"
{
#endif


    void cfun();


#ifdef __cplusplus
}
#endif


#endif

cppfun.cppは次のとおりです。

#include "cfun.h"
#include "cppfun.h"
#include <iostream>
using namespace std;


void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}


int main()
{
    cfun();
    return 0;
}

 cはc ++を呼び出します(重要なのは、C ++がCの呼び出し規約に準拠した関数を提供することです)

 

vs2010でテストする場合、externなどのステートメントはなく、cppfun.hのみがcfun.cに含まれ、cppfun()を呼び出してコンパイルおよび実行できます。c++の標準に従って、gccでエラーをコンパイルします。 / c。それは間違っているはずです。次のメソッドは両方のコンパイラを実行できます

 

cppfun.hは次のとおりです。

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_


extern "C" void cppfun();




#endif

cfun.cは次のとおりです。

//#include "cppfun.h" //不要包含头文件,否则编译出错
#include "cfun.h"
#include <stdio.h>


void cfun()
{
    printf("this is c fun call\n");
}


extern void cppfun();


int main()
{
#ifdef __cplusplus
    cfun();
#endif
    cppfun();
    return 0;
}

 

https://www.cnblogs.com/TenosDoIt/p/3163621.html

 

おすすめ

転載: blog.csdn.net/miaozenn/article/details/113063355