私はK&Rの「Cプログラミング言語」を読んでいて、この声明に出くわしました[はじめに、p。3]:
Cが提供するデータ型と制御構造はほとんどのコンピューターで直接サポートされているため、自己完結型プログラムの実装に必要なランタイムライブラリはごくわずかです。
太字のステートメントはどういう意味ですか?コンピューターで直接サポートされていないデータ型または制御構造の例はありますか?
私はK&Rの「Cプログラミング言語」を読んでいて、この声明に出くわしました[はじめに、p。3]:
Cが提供するデータ型と制御構造はほとんどのコンピューターで直接サポートされているため、自己完結型プログラムの実装に必要なランタイムライブラリはごくわずかです。
太字のステートメントはどういう意味ですか?コンピューターで直接サポートされていないデータ型または制御構造の例はありますか?
回答:
はい、直接サポートされていないデータ型があります。
多くの組み込みシステムでは、ハードウェア浮動小数点ユニットはありません。したがって、次のようなコードを書くと、
float x = 1.0f, y = 2.0f;
return x + y;
それはこのようなものに翻訳されます:
unsigned x = 0x3f800000, y = 0x40000000;
return _float_add(x, y);
次に、コンパイラまたは標準ライブラリがの実装を提供する_float_add()
必要があります。これは、組み込みシステムのメモリを占有します。非常に小さなシステムでバイト数を数えている場合、これは加算されます。
もう1つの一般的な例は、64ビット整数(long long
1999年以降のC標準)で、32ビットシステムでは直接サポートされていません。古いSPARCシステムは整数乗算をサポートしていなかったため、乗算はランタイムによって提供されなければなりませんでした。他の例があります。
比較すると、他の言語にはより複雑なプリミティブがあります。
たとえば、Lispシンボルは、Luaのテーブル、Pythonの文字列、Fortranの配列などと同様に、多くのランタイムサポートを必要とします。Cの同等の型は通常、標準ライブラリの一部ではない(標準のシンボルやテーブルがない)か、はるかに単純であり、ランタイムサポートをあまり必要としません(Cの配列は基本的に単なるポインタであり、nullで終了する文字列はほぼ同じです)。
Cから欠落している注目すべき制御構造は、例外処理です。非ローカル出口はsetjmp()
and longjmp()
に限定されており、プロセッサー状態の特定の部分を保存および復元するだけです。比較すると、C ++ランタイムはスタックをウォークして、デストラクタと例外ハンドラを呼び出す必要があります。
実際、この紹介の内容は、カーニガンとリッチーが本の初版で最初に書いた1978年以降、それほど変わっていないと思います。それらは、現代よりも当時のCの歴史と進化に言及しています。実装。
コンピュータは基本的にメモリバンクと中央処理装置にすぎず、各プロセッサはマシンコードを使用して動作します。各プロセッサの設計の一部は、アセンブリ言語と呼ばれる命令セットアーキテクチャであり、人間が読めるニーモニックのセットからマシンコード(すべて数字)に1対1でマッピングされます。
C言語の作成者(およびその直前のBおよびBCPL言語)は、できるだけ効率的にアセンブリにコンパイルされる言語で構文を定義することに熱心でした...実際、ターゲットの制限によって強制されましたハードウェア。他の答えが指摘しているように、これには、関連する分岐(GOTOおよびその他のCのフロー制御)、移動(割り当て)、論理演算(&| ^)、基本的な算術(加算、減算、増分、減分)、およびメモリアドレス指定(ポインター)。良い例は、Cのプリ/ポストインクリメントおよびデクリメント演算子です。これらは、特に一度コンパイルされた単一のオペコードに直接変換できるため、ケントンプソンによってB言語に追加されたと思われます。
これは、著者が「ほとんどのコンピュータで直接サポートされている」と言ったときに意味したものです。他の言語に、直接サポートされていない型や構造が含まれているという意味ではありませんでした。つまり、設計により、 Cの構成要素がアセンブリに最も直接(場合によっては文字どおり直接)変換されました。
基になるアセンブリとの密接な関係は、構造化プログラミングに必要なすべての要素を提供しながら、Cの早期採用につながったものであり、コンパイルされたコードの効率が依然として重要である環境で今日の人気の高い言語を維持しています。
言語の歴史に関する興味深い記事については、C言語の開発-デニス・リッチーを参照してください。
簡単に言えば、Cでサポートされているほとんどの言語構造はターゲットコンピュータのマイクロプロセッサでもサポートされているため、コンパイルされたCコードはマイクロプロセッサのアセンブリ言語に非常にうまく効率的に変換されるため、コードが小さくなり、フットプリントも小さくなります。
長い回答には、アセンブリ言語の知識が少し必要です。Cでは、次のようなステートメント:
int myInt = 10;
アセンブリでは次のようなものに変換されます。
myInt dw 1
mov myInt,10
これをC ++のようなものと比較してください。
MyClass myClass;
myClass.set_myInt(10);
結果のアセンブリ言語コード(MyClass()の大きさに依存します)は、何百ものアセンブリ言語行を追加する可能性があります。
実際にアセンブリ言語でプログラムを作成することなく、純粋なCはおそらく、プログラムを作成できる「最も細く」、「最もタイト」なコードです。
編集
私の回答に対するコメントを踏まえて、私は自分の正気のためだけにテストを実行することにしました。次のような「test.c」というプログラムを作成しました。
#include <stdio.h>
void main()
{
int myInt=10;
printf("%d\n", myInt);
}
これをgccを使用してアセンブリにコンパイルしました。次のコマンドラインを使用してコンパイルしました。
gcc -S -O2 test.c
結果のアセンブリ言語は次のとおりです。
.file "test.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d\n"
.section .text.unlikely,"ax",@progbits
.LCOLDB1:
.section .text.startup,"ax",@progbits
.LHOTB1:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
movl $10, %edx
movl $.LC0, %esi
movl $1, %edi
xorl %eax, %eax
jmp __printf_chk
.cfi_endproc
.LFE24:
.size main, .-main
.section .text.unlikely
.LCOLDE1:
.section .text.startup
.LHOTE1:
.ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
.section .note.GNU-stack,"",@progbits
次に、クラスを定義して「test.c」と同じものを出力する「test.cpp」というファイルを作成します。
#include <iostream>
using namespace std;
class MyClass {
int myVar;
public:
void set_myVar(int);
int get_myVar(void);
};
void MyClass::set_myVar(int val)
{
myVar = val;
}
int MyClass::get_myVar(void)
{
return myVar;
}
int main()
{
MyClass myClass;
myClass.set_myVar(10);
cout << myClass.get_myVar() << endl;
return 0;
}
次のコマンドを使用して、同じ方法でコンパイルしました。
g++ -O2 -S test.cpp
結果のアセンブリファイルは次のとおりです。
.file "test.cpp"
.section .text.unlikely,"ax",@progbits
.align 2
.LCOLDB0:
.text
.LHOTB0:
.align 2
.p2align 4,,15
.globl _ZN7MyClass9set_myVarEi
.type _ZN7MyClass9set_myVarEi, @function
_ZN7MyClass9set_myVarEi:
.LFB1047:
.cfi_startproc
movl %esi, (%rdi)
ret
.cfi_endproc
.LFE1047:
.size _ZN7MyClass9set_myVarEi, .-_ZN7MyClass9set_myVarEi
.section .text.unlikely
.LCOLDE0:
.text
.LHOTE0:
.section .text.unlikely
.align 2
.LCOLDB1:
.text
.LHOTB1:
.align 2
.p2align 4,,15
.globl _ZN7MyClass9get_myVarEv
.type _ZN7MyClass9get_myVarEv, @function
_ZN7MyClass9get_myVarEv:
.LFB1048:
.cfi_startproc
movl (%rdi), %eax
ret
.cfi_endproc
.LFE1048:
.size _ZN7MyClass9get_myVarEv, .-_ZN7MyClass9get_myVarEv
.section .text.unlikely
.LCOLDE1:
.text
.LHOTE1:
.section .text.unlikely
.LCOLDB2:
.section .text.startup,"ax",@progbits
.LHOTB2:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB1049:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $10, %esi
movl $_ZSt4cout, %edi
call _ZNSolsEi
movq %rax, %rdi
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE1049:
.size main, .-main
.section .text.unlikely
.LCOLDE2:
.section .text.startup
.LHOTE2:
.section .text.unlikely
.LCOLDB3:
.section .text.startup
.LHOTB3:
.p2align 4,,15
.type _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, @function
_GLOBAL__sub_I__ZN7MyClass9set_myVarEi:
.LFB1056:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $_ZStL8__ioinit, %edi
call _ZNSt8ios_base4InitC1Ev
movl $__dso_handle, %edx
movl $_ZStL8__ioinit, %esi
movl $_ZNSt8ios_base4InitD1Ev, %edi
addq $8, %rsp
.cfi_def_cfa_offset 8
jmp __cxa_atexit
.cfi_endproc
.LFE1056:
.size _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, .-_GLOBAL__sub_I__ZN7MyClass9set_myVarEi
.section .text.unlikely
.LCOLDE3:
.section .text.startup
.LHOTE3:
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I__ZN7MyClass9set_myVarEi
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.hidden __dso_handle
.ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
.section .note.GNU-stack,"",@progbits
はっきりとわかるように、結果のアセンブリファイルは、C ++ファイルよりもC ++ファイルよりもはるかに大きくなります。他のすべてのものを切り取って、Cの「メイン」とC ++の「メイン」を比較するだけでも、余分なものがたくさんあります。
MyClass myClass { 10 }
、C ++ などの実際のコードは、まったく同じアセンブリにコンパイルされる可能性が非常に高くなります。最新のC ++コンパイラは、抽象化のペナルティを排除しました。その結果、Cコンパイラに勝ることがよくあります。たとえば、Cの抽象化ペナルティqsort
は現実ですが、C ++にstd::sort
は、基本的な最適化さえ行っても抽象化ペナルティはありません。
K&Rは、ほとんどのC式(技術的な意味)が、サポートライブラリへの関数呼び出しではなく、1つまたはいくつかのアセンブリ命令にマップされることを意味します。通常の例外は、ハードウェアdiv命令のないアーキテクチャの整数除算、またはFPUのないマシンの浮動小数点です。
引用があります:
Cは、アセンブリ言語の柔軟性と能力を、アセンブリ言語の使いやすさと組み合わせています。
(ここにあります。「アセンブリ言語の便利さと表現力を備えたアセンブリ言語の速度」のような別のバリエーションを覚えていると思いました。)
一部の高水準言語は、データ型の正確な幅を定義しており、すべてのマシンでの実装は同じように機能する必要があります。Cではありません。
x86-64で128ビットの整数、または一般的な場合は任意のサイズのBigIntegerで作業する場合は、そのための関数のライブラリが必要です。すべてのCPUは負の整数の2進数表現として2の補数を使用するようになりましたが、Cが設計されたときはそうではありませんでした。(これが、2の補数でないマシンで異なる結果をもたらすいくつかの事柄が、C標準では技術的に定義されていない理由です。)
参照カウントされた参照が必要な場合は、自分で行う必要があります。ポインターが指しているオブジェクトの種類に応じて異なる関数を呼び出すc ++仮想メンバー関数が必要な場合、C ++コンパイラーはcall
、固定アドレスの命令だけではなく、はるかに多くを生成する必要があります。
ライブラリ関数以外では、提供される文字列操作は文字の読み取り/書き込みのみです。連結なし、部分文字列なし、検索なし。(文字'\0'
列は、ポインタ+長さではなく、8ビット整数のNUL終了()配列として格納されるため、部分文字列を取得するには、NULを元の文字列に書き込む必要があります。)
CPUには、文字列検索関数で使用するように設計された命令が含まれている場合がありますが、通常は、実行される命令ごとに1バイトをループで処理します。(または、x86 repプレフィックスを使用します。Cがx86で設計されている場合、文字列検索または比較は、ライブラリ関数呼び出しではなく、ネイティブ操作になります。)
他の多くの回答は、例外処理、ハッシュテーブル、リストなど、ネイティブでサポートされていないものの例を示しています。K&Rの設計哲学が、Cがこれらをネイティブに持たない理由です。
プロセスのアセンブリ言語は通常、ジャンプ(移動)、ステートメント、移動ステートメント、バイナリ関節炎(XOR、NAND、AND ORなど)、メモリフィールド(またはアドレス)を扱います。メモリを命令とデータの2つのタイプに分類します。これは、すべてのアセンブリ言語についてです(私は、アセンブリプログラマがそれ以上のものがあると主張するでしょうが、それは一般的にこれに要約されます)。Cは、この単純さに非常に似ています。
Cは、代数と算術を組み合わせることです。
Cは、アセンブリーの基本(プロセッサーの言語)をカプセル化します。「Cが提供するデータ型と制御構造はほとんどのコンピューターで直接サポートされているため」よりもおそらく正しい記述です。
コンピューターで直接サポートされていないデータ型または制御構造の例はありますか?
C言語でのすべての基本的なデータ型とその操作は、ループすることなく1つまたはいくつかの機械語命令で実装できます。これらは(実質的にすべての)CPUで直接サポートされています。
いくつかの一般的なデータ型とその操作には、何十もの機械語命令が必要か、またはいくつかのランタイムループの反復、あるいはその両方が必要です。
多くの言語には、そのような型とその操作のための特別な省略構文があります-Cでそのようなデータ型を使用するには、通常、さらに多くのコードを入力する必要があります。
このようなデータ型と操作には次のものがあります。
これらの操作はすべて、何十もの機械語命令を必要とするか、ほぼすべてのプロセッサでいくつかのランタイムループを繰り返す必要があります。
何十もの機械語命令またはループも必要とする一般的な制御構造には、次のものがあります。
Cまたは他の言語で記述されているかどうかに関係なく、プログラムがそのようなデータ型を操作するとき、CPUは最終的にそれらのデータ型を操作するために必要な命令を実行する必要があります。これらの指示は、多くの場合「ライブラリ」に含まれています。Cを含むすべてのプログラミング言語には、デフォルトですべての実行可能ファイルに含まれている各プラットフォーム用の「ランタイムライブラリ」があります。
コンパイラを作成するほとんどの人は、「言語に組み込まれている」すべてのデータ型を操作するための命令をランタイムライブラリに入れます。Cが持っていないので任意のランよりもCランタイムライブラリが小さくなります-言語に組み込まれて上記のデータ型と演算や制御構造のを、それらのどれもがCランタイムライブラリに含まれていません上記の他のものが言語に組み込まれている他のプログラミング言語のタイムライブラリ。
プログラマーが、C言語または任意の他の言語で、「言語に組み込まれていない」他のデータ型を操作することを望む場合、そのプログラマーは通常、そのプログラムに追加のライブラリーを含めるようにコンパイラーに指示します。 (「依存関係を回避する」ために)これらの操作のさらに別の実装をプログラムに直接書き込みます。
の組み込みデータ型はC
何ですか?彼らはのようなものですint
、char
、* int
、float
、配列等...これらのデータ型は、CPUによって理解されています。CPUは、配列の操作方法、ポインターの逆参照方法、ポインター、整数、浮動小数点数の演算方法を認識しています。
しかし、より高いレベルのプログラミング言語に行くと、抽象的なデータ型とより複雑な構造が組み込まれています。たとえば、C ++プログラミング言語の組み込みクラスの膨大な配列を見てください。CPUはクラス、オブジェクト、または抽象データ型を理解しないため、C ++ランタイムはCPUと言語の間のギャップを埋めます。これらは、ほとんどのコンピュータで直接サポートされていないデータ型の例です。
それはコンピュータに依存します。Cが発明されたPDP-11では、long
サポートが不十分でした(32ビット操作のすべてではなく一部をサポートする、購入可能なオプションのアドオンモジュールがありました)。元のIBM PCを含め、16ビットシステムのさまざまな程度で同じことが当てはまります。同様に、32ビットマシンまたは32ビットプログラムでの64ビット操作の場合も同様ですが、K&R本の執筆時のC言語には64ビット操作はまったくありませんでした。そしてもちろん、80年代と90年代には多くのシステム(386や一部の486プロセッサを含む)があり、今日の組み込みシステムでさえ、浮動小数点演算(float
またはdouble
)を直接サポートしていません。
よりエキゾチックな例として、一部のコンピュータアーキテクチャは「ワード指向」のポインタ(メモリ内の2バイトまたは4バイトの整数を指す)のみをサポートし、追加のオフセットフィールドを追加することによってバイトポインタ(char *
またはvoid *
)を実装する必要がありました。この質問は、そのようなシステムについてのいくつかの詳細に入ります。
それが参照する「ランタイムライブラリ」関数は、マニュアルに表示されるものではありませんが、マシンでサポートされていない基本的なタイプの演算を実装するために使用される最新のコンパイラのランタイムライブラリでは、このような関数です。K&R自身が参照していたランタイムライブラリは、Unix Heritage SocietyのWebサイトにあります-の除算を実装するために使用される(当時存在しなかった、同じ名前のC関数とは異なる)のような関数を見ることができますPDP-11がアドオンでもサポートしなかった32ビット値、および関数の呼び出しと戻りを管理するためにスタックのレジスタを保存および復元する(およびcsv.cでも)。ldiv
csv
cret
彼らはまた、CPUの基になるポインターのサポートにマップされなかった配列のセマンティクスを持つFORTRANなどの他の現代の言語とは異なり、基になるマシンで直接サポートされない多くのデータ型をサポートしないという選択についても言及している可能性があります。 Cの配列。C配列には常にゼロのインデックスが付けられ、すべてのランクで常に既知のサイズであるという事実がありますが、最初の意味は、配列のインデックス範囲またはサイズを格納する必要がなく、それらにアクセスするためのランタイムライブラリ関数が必要ないことです。コンパイラーは必要なポインター演算を単純にハードコーディングできます。
ステートメントは、Cのデータと制御構造がマシン指向であることを単に意味します。
ここで考慮すべき2つの側面があります。1つは、C言語にデータ型の定義方法に自由度を許可する定義(ISO標準)があることです。これは、C言語の実装がマシンに合わせて調整されていることを意味します。Cコンパイラーのデータ型は、コンパイラーがターゲットとするマシンで使用可能なものと一致します。これは、言語がそのための自由度を持っているためです。マシンが36ビットなどの異常なワードサイズを持っている場合、タイプint
またはそれlong
に適合させることができます。それint
がちょうど32ビットであることを前提とするプログラムは壊れます。
第二に、そのような移植性の問題のために、第二の効果があります。ある意味で、K&Rの声明は、一種の自己実現的予言、あるいはおそらくその逆になっています。つまり、新しいプロセッサの実装者は、Cコンパイラをサポートする熱心な必要性を認識しており、「すべてのプロセッサが80386のように見える」と想定するCコードがたくさんあることを知っています。アーキテクチャは、Cを念頭に置いて設計されています。Cだけでなく、Cの移植性に関する一般的な誤解も念頭に置いて設計されています。9ビットバイトのマシンや、一般的な目的で使用するマシンを導入することはできません。タイプを想定するプログラムchar
正確に8ビット幅です。移植性の専門家によって書かれた一部のプログラムのみが機能し続けます。ツールチェーン、カーネル、ユーザースペース、および有用なアプリケーションを備えた完全なシステムを、妥当な労力でまとめるにはおそらく不十分です。言い換えると、ハードウェアは、移植性のない多くのCプログラムが作成された他のハードウェアのように見えるため、Cタイプはハードウェアから利用可能なもののように見えます。
コンピューターで直接サポートされていないデータ型または制御構造の例はありますか?
多くの機械語で直接サポートされていないデータ型:倍精度整数。リンクされたリスト; ハッシュ表; 文字列。
ほとんどの機械語で直接サポートされていない制御構造:ファーストクラスの継続。コルーチン/スレッド; 発生器; 例外処理。
これらはすべて、多数の汎用命令を使用して作成されたかなりのランタイムサポートコードと、より基本的なデータ型を必要とします。
Cには、一部のマシンでサポートされていないいくつかの標準データ型があります。C99以降、Cには複素数があります。これらは2つの浮動小数点値から作成され、ライブラリルーチンで動作するように作成されています。一部のマシンには、浮動小数点ユニットがまったくありません。
一部のデータ型については明確ではありません。マシンが1つのレジスタをベースアドレスとして使用し、別のレジスタをスケーリングされた変位として使用してメモリのアドレス指定をサポートしている場合、それは配列が直接サポートされるデータ型であることを意味しますか?
また、浮動小数点といえば、IEEE 754浮動小数点という標準化があります。Cコンパイラにdouble
プロセッサがサポートする浮動小数点形式に同意する理由は、両者が同意するように作られただけでなく、その表現に独立した標準があるためです。
直接サポートされるとは、プロセッサの命令セットに効率的にマッピングされることを理解する必要があります。
長整数型(拡張算術ルーチンが必要な場合があります)と短整数型サイズ(マスキングが必要な場合があります)を除き、整数型の直接サポートがルールです。
浮動小数点型を直接サポートするには、FPUが利用可能である必要があります。
ビットフィールドの直接サポートは例外です。
構造体と配列は、ある程度直接サポートされているアドレス計算を必要とします。
ポインターは常に間接アドレッシングを介して直接サポートされます。
goto / if / while / for / doは、無条件/条件付きブランチによって直接サポートされています。
ジャンプテーブルを適用すると、スイッチを直接サポートできます。
関数呼び出しは、スタック機能によって直接サポートされます。