以下のコード、
変数の宣言により、コンパイラーが、変数がメモリのどの部分を使うのか決定する。
そしてa=5により、そのメモリ領域に、値5(2進法の101)が入る。ここまでは直感的に分かるのですが。
b=aにより、bによるメモリ領域に、aの値5が入る所がすっきりしません。
たとえば、bにaが代入される際に、bはaのメモリのアドレスを参照し、そこから値の5を持ってくる。そういう理解で良いのでしょうか。
よろしくお願いいたします。
C
1# include <stdio.h> 2 3int main(void) 4 5{ 6 int a; 7 int b; 8 9 a=5; 10 b=a; 11 12 return 0; 13} 14 15
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答8件
0
大変難しい問題です。正しく理解するには、左辺値、右辺値、評価と言ったことがきちんと理解していなくてはなりません。
まず、代入というのは非対象です。式A = 式B
と書かれたとき、式Aを評価した結果である左辺値を式Bを評価した結果である右辺値に束縛します(左辺値が右辺値になると言える)。全ての左辺値は右辺値になることができますが、逆はそうとも限りません。つまり、どのような式も右辺値を返す事ができますが、左辺値を返すことができるのは左辺値式になる式だけです。例えば、a
は左辺値式であるため、式Aにも式Bにも書けます。しかし、5
は非左辺値式であるため、式Bにしか書けません。
もうわからなくなってきたと思いますが、次はもっとわからなくなると思います。**大丈夫です、私も自分で何を言っているのかわかっていません。**が、取りあえず、続けます。
まずは、左辺値を見てみましょう。a
は評価の結果、識別子aが示すストレージにあるオブジェクトです。ストレージはメモリ上のどこかかもしれませんし、CPUのレジスタかも知れませんが、何らかの場所として存在するものです。このストレージはint a;
によって作られており、識別子aの名前でアクセスできる場所です。対して、右辺値は単純です。その式を評価した結果のオブジェクトですが、このオブジェクトは何かしらのストレージを持つ必要はありません(もちろん持っても良い)。5
という整数リテラル定数は評価の結果、整数で5を表す値(オブジェクト)になりますが、どこかの場所(メモリやレジスタ)に5そのものが保存されている必要はありません。そして、代入は、この左辺値であるところのストレージにあるオブジェクトを右辺値であるところのオブジェクトに(左辺値のストレージに対して)置き換えます(識別子aが示すストレージの場所自体は変わりません)。より単純に言えば、ストレージの中身を右辺値にすると言ってもいいでしょう。こうして、識別子aが示すストレージにあるオブジェクトが整数5に相当する値になります。これがa = 5;
の動作です。
次にb = a;
を見てみましょう。まずは左辺ですが、先程のaと同様に考えることができて、識別子bが示すストレージにあるオブジェクトです。続いて右辺ですが、先程のa
と同じく識別子aが示すストレージにあるオブジェクトです。先程のa
は左辺値でしたが、今回は右辺値です。しかし、左辺値も右辺値になれると言ったように、ストレージのあるなしに関わらず、オブジェクトとして解釈できます。ストレージの中身、つまりストレージにあるオブジェクトは先程置き換えた整数5に相当する値です。これで左辺値も右辺値も定まったので、右辺値であるところの識別子bが示すストレージにあるオブジェクトを右辺値である整数5に相当するオブジェクトに置き換えます。こうして、変数b
も5になるということです。
なお、左辺と右辺でどちらが先に評価されるかは不定です。左辺が先になる場合もあれば、右辺が先になる場合もあります。順序に依存したコードは未定義の動作になります。
上の話は、ちょっと自信がない所もあります。左辺値と右辺値は言葉で表現するのが本当に難しい所です。Cは単純でありながら、結構曖昧な所もあるので、間違っているかも知れません。C++の方が左辺値右辺値の区別がより厳密に定義されているのですが、複雑ですので、説明しきれる自信はありません。
なお、コンパイラの最適化によって、同じ動作になるのであれば、多くの工程が省かれる場合があります。上の話は実際にコンパイルされた後のコードの話ではなく、言語仕様上の話であることに注意してください。そして、アドレスとかに依存した動作の解釈は正確ではありません。Cの規格上は(ポインタの値を表示するなどの演算がない場合)自動変数のストレージがメモリ上に存在しなくても許されるからです。
投稿2020/03/16 11:16
総合スコア21739
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/03/17 00:08
2020/03/17 09:32
2020/03/17 09:47
2020/03/20 01:20
2020/03/20 06:27
2020/03/20 06:34
0
既にした回答と方向性が異なるので、別回答としています。
コンパイル後のバイナリコード(機械語)においてどのようCPU命令になっているのかを確認することはCそのものというより、Cのコードが実際にコンピューター上でどのように実行されるのかを理解する上では有用です。機械語を直接理解するのはちょっと難しい(できる人はできるらしい)ので、機械語と一対一対応しているアセンブリにおいてどうなっているのかを見たいと思います。
Cのコードをアセンブリに変換したコードを見るにあたって、注意事項が二点あります。
- RISCアーキテクチャCPU向けのアセンブリを使う方が原始的な動作の理解がしやすい。
- 最適化を無効にする。
まず、CPUは大きく分けるとCISCとRISCにわかれます。例えばx86(およびその拡張であるx86_64)はCISCですが、SPARCやPowerPCはRISCです。RISCは非常に単純な命令しか用意されていませんが、CISCはより複雑な命令が用意されています。と言ってもRISCがCISCより劣るというわけではなく、RISCで複数の命令になるようなものをCISCでは一つにまとめた便利命令みたいな感じで用意しているだけです。CISCでのアセンブリを見た場合、RISCで複数の命令になるようなものが一つ命令にまとめられてしまっている場合があります。しかし、実際の所一つの命令でもCPU内部では二つ以上の処理を行っている場合もあるため、命令の一つをCPUの原始的な動作としてしまうには問題があると思います。なお、現在のCPUは、CISCであっても中身がRISCだったり、RISCであってもCISC並の命令が用意されていたりと、その垣根は曖昧になりつつあります。
今回は、コンパイル環境の構築が容易であり、また、後述の確認サイトで使えて、一般的にRISCに分類されるCPUの一つであるARM64(64bit版のARM)をターゲットにしたいと思います。ARM64のアセンブリを調べるにはARM® コンパイラ armasm ユーザガイド バージョン 6.02を参考にしました(でも、微妙に違うような…コンパイラがGCCだから?)。日本語版があってよかったです。
つぎに、最適化についてですが、Cでは最適化によって結果が同じになる場合は、処理が省略されたり、まとめられてしまうことはよくあることです。とくに、使用されない変数は、そもそも消される場合が多いです。最適化による省略などを防ごうとするのであれば、次のように使用されることを強制する方法もあります。
C
1# include <stdio.h> 2 3int main(void) 4 5{ 6 int a = 0; 7 int b = 0; 8 printf("%p: %d\n", &a, a); 9 printf("%p: %d\n", &b, b); 10 a = 5; 11 printf("%p: %d\n", &a, a); 12 printf("%p: %d\n", &b, b); 13 b = a; 14 printf("%p: %d\n", &a, a); 15 printf("%p: %d\n", &b, b); 16 17 return 0; 18}
しかし、これでも処理の順番やスタックの位置が前後したりすることは防ぐことはできません。なので、アセンブリで確認する場合は最適化無しで確認した方が良いでしょう。ただ、最適化した場合だけ発生する不具合のバグ取りなどは最適化した状態でみないと意味が無いと言うこともあります。今回は動きの確認だけが目的ですので、そのような問題は無視しても良いでしょう。
さて、これらを踏まえて実際にアセンブリに変換するのですが、わざわざARM64ターゲットを含めてコンパイラをインストールしている人も少ないでしょうから、オンラインで確認できるCompiler Explorerを使います。お手軽に確認ができるのでとても便利なサイトです。
対応するコード同士が色別されているため、何がどの部分にあたるのかを視覚的にわかるのもこのサイトの良いところです。
まず、a=5;
は次のコードです。
mov w0, 5 str w0, [sp, 12]
最初のmov
は値をレジスタへ移動します。w0
は0番目の32ビット汎用レジスタです。1番目の命令は5
という値を0番目の32ビット汎用レジスタに32bit分移動することを表します。移動とは言ってますが、実際はコピーです。次のstr
はレジスタの値を指定位置のスタックポインタを転送します。w0
は先程同じです。[sp, 12]
はスタックポインタから12個先の位置を表し、後述しますが、これはローカル自動変数a
の保存領域です。つまり、0番目の32ビット汎用レジスタの値をスタックポインタから12個先の位置に32bit分転送することを表します。
スタックポインタというのがでてきました。これは最初のsub sp, sp, #16
によって16個後ろになっており、この16個分の領域が関数内で色々操作できる領域となり、ローカル自動変数a
とb
の保存領域として使われます。
関数内でのスタックポインタを00(16進数)とすると次のようなイメージになります。
0000 0000 0000 0000 11... 0123 4567 89AB CDEF 02... S <b > <a > R
Rは関数が呼び出される前のスタックポインタで、Sは関数内で処理されている最中のスタックポインタです。スタックは後ろから使われるため、a
用に0C-0F、b
用に08-0Bが確保されます。今回の関数では00-07は使用されません。ローカル変数がもっと多ければスタックが埋まっていきますが、ARM64では16バイト毎に確保するようになっているようで、int
が5つ以上になると32になったりします。どの区切りで確保されるかや、スタックポインタの扱い方はCPUのアーキテクチャによって異なります。あくまで、これはARM64の話であることに注意して下さい。
さて、話は戻ってb=a;
です。
ldr w0, [sp, 12] str w0, [sp, 8]
最初のldr
はstr
の逆、指定位置のスタックポインタの値をレジスタに転送します。w0
も[sp, 12]
は先程と同じです。つまり、スタックポインタから12個先の位置の値を0番目の32ビット汎用レジスタに32bit分転送することを表します。先程あったstr w0, [sp, 12]
と逆の動作で、無駄なことをしているように見えますが、最適化が無効の場合は無駄なことを省かないようになります。次のstr
は先程見たのと同じです。w0
はもう説明不要でしょう。では、最後の[sp, 8]
は何かというと、スタックポインタから8個先の位置を表し、先程の説明にあった通り、これはローカル自動変数a
の保存領域です。つまり、0番目の32ビット汎用レジスタの値をスタックポインタから8個先の位置に32bit分転送することを表します。
後の処理はreturn
の値をセットしてスタックポインタを元に戻して、元の関数に戻ると言うだけになります。
ふー、たった二行だけなのに、長かったです。少しでもCのコードが実際はどんな機械語になって、CPUは処理しているのか理解していただけれたら幸いです。ただ、スタックの取り方とか、レジスタの扱い方とか、そういうところは、考え方はだいたい一緒ですが、実際の方法がアーキテクチャによって大きく異なる場合があるので、注意して下さい。
今回、私は始めてARMのアセンブリに触れました。x86等も含めてアセンブリで書くこともないですし、読むことも非常に稀です。なので、アセンブリに詳しい専門家の方から見たら間違っている部分があるかも知れません。コメント等でご指摘下さい。
今回のようなことは、アセンブリ超初心者の私でもできたので、単純なコードであれば、アセンブリでの解析は誰でもできると思います。たぶん。
投稿2020/03/20 06:33
編集2020/03/20 06:44総合スコア21739
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/03/20 11:06
2020/03/22 00:00
0
代入演算子 = は 2項演算子で、第1オペランド(左辺) には
値を入れる入れ物を要求し、第2オペランド(右辺)には値を要求します。
定数、変数、「演算子を使った式」はどれも「式」です。そして値を持ちます。
だから、右辺に置けるのです。
定数は値を持ちますが、値を入れる入れ物ではないので、左辺にはおけません。
変数は値を入れる入れ物であり、値も持ちます。
だから、左辺にも右辺にも置けるのです。
左辺に置いた場合、その値は要求されません。入れ物であることが要求されるのです。
a + 7 のような演算子を使った式は、演算結果の値を持ちますが、
それは入れ物ではありません。a が 5 のとき、a + 7 は 12 です。
a + 7 は入れ物ではなく、そこに別の値を入れることはできません。
a + 7 = 3 とは書けないのです。
では左辺には変数しか置けないのか言うとそんなことはありません。
a[i] は、 添字演算子 [ ] を使った式ですが、これは入れ物です。
*p は間接演算子 * を使った式ですが、これは入れ物です。
だから、a[i] も *p も左辺にも右辺にも置けるのです。
a = b を「変数 a に変数 b が入る」というのは不正確な表現です。
「変数 a(という入れ物) に、変数 b(という入れ物)から取り出した値が入る」
というのが正確な表現です。
もう一度言います。
代入演算子は、左辺の入れ物に、右辺の値を入れます。
定数 5 は、値そのものです。
変数 a は、入れ物ですが、値を取り出せます。
なお、入れ物のことを左辺値と呼びます。
投稿2020/03/17 12:17
総合スコア8224
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
bはaのメモリのアドレスを参照し
"bは" という表現に引っかかりました.
オブジェクトとして考えるとそのような表現もあるかと思いますが, それは考えすぎとも思います.
「a=5」はコンパイラが「a(のアドレス)に5を入れる」と解釈して該当するアセンブラコードを生成し,
「b=a」はコンパイラが「b(のアドレス)にa(のアドレス)の値を入れる」と解釈して該当するアセンブラコードを生成する
...という"感じ"でしょうか.
投稿2020/03/16 09:08
総合スコア13209
0
ベストアンサー
たとえば、bにaが代入される際に、bはaのメモリのアドレスを参照し、そこから値の5を持ってくる。そういう理解で良いのでしょうか。
はい、その理解で合っていると思います。
投稿2020/03/16 08:08
総合スコア6500
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/03/16 08:23
退会済みユーザー
2020/03/17 06:36
2020/03/18 02:18
2020/03/23 23:46
2020/03/23 23:57 編集
0
既に多くの方が良い回答をなさっていますので、わたしなりに自分の考えをまとめるために回答させていただきます。
b=aにより、bによるメモリ領域に、aの値5が入る所がすっきりしません。
の部分の理由がはっきり書かれていないので、そのあたり憶測になり的外れな答えかもしれません。
結論を先に言えば
- プログラムの数式は、数学の様式を一部借りてはいるが数学の数式とは別のものである
- 変数という概念を導入した以上、変数同士で値の受け渡しは必ず必要になり、C言語では、a = b という書式が選ばれた
数学の数式が、数学的事実の表記を目的としているのに対して、
プログラムの数式は(プログラムの他の部分同様)処理の手順を示しています。
そして、C言語等においては計算の手順を記載するうえで、
b = a
という形式が都合がいいとの判断から、数学の数式との意味の齟齬は無視されたのです。
実際、私が最初にこの言語仕様に出会った時思ったことは、「便利だな」「なるほどそうやるのか」程度で、質問者の方が持たれたような疑問は(数学の成績が悪かったせいか)あまり考えませんでした。
ただ、当時から「b = a」という記述方法しかなかったわけではなく、
代入の意味を明確にするために、代入記号に「:=」を使ったり、「LET」や「SET」と言ったキーワードを使って明確に代入を示す記法も存在しました。
C言語登場以降、大型計算機からより小型のPCなどへの計算機利用の移行という流れの中で、
C言語スタイルのシンプルな記述形式は流行し、
LET などのキーワードを省略可にしたりする言語も現れましたが、
PCの能力が往時の大型計算機に迫り、追い抜くようになってくると、
可読性や厳密性のために、こういった代入の書式を見直す言語も増えてきました。
いずれにしろ、プログラム一般において「変数から変数への代入」という処理の必要がまずあって、
書式はそれをどう表現するのがいいかを(それぞれの言語設計者がそれぞれの考えで)
後付けで考えて決めたものです。
その際に数式や英文の形式を借りてはいますが、そのままの意味であるとは限らないので
他所での解釈は一旦棚上げして考えるのが、プログラム言語を理解する早道だと思います。
投稿2020/04/15 00:08
編集2020/04/15 07:13総合スコア1193
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/04/15 02:04
2020/04/15 06:07
2020/04/15 12:09
2020/04/15 16:33
0
たとえば、bにaが代入される際に、bはaのメモリのアドレスを参照し、そこから値の5を持ってくる。そういう理解で良いのでしょうか。
代入文b = a
をなるべく言語仕様に合わせた用語で解釈すると、「識別子a
が指し示すデータ記憶域に保持された値5
で、識別子b
が指し示すデータ記憶域の内容を置き換える」となります。
より質問中の表現に近づけると「変数a
のメモリ領域に入っている値5
を読み取り、変数b
のメモリ領域にその値を入れる」でしょうか。
言語仕様にて規定されるプログラムの振る舞いは上記の通りですが、実際にはCコンパイラによる “最適化” が行われます。質問中ソースコードの場合、プログラム外部から観測可能な入出力をもたないため、結果的に下記のような「何もしないプログラム」まで最適化される可能性があります。
C
1int main() { return 0; }
日本工業規格(JIS) X 3010:2003 プログラム言語C、その翻訳元である ISO/IEC 9899:1999 Programming languages - C より、直接関連する文面を引用しておきます。
§3. 用語及び記号の定義/Terms, definitions, and symbols
バイト(byte)
実行環境の基本文字集合の任意の要素を保持するために十分な大きさをもつアドレス付け可能なデータ記憶域の単位。
addressable unit of data storage large enough to hold any member of the basic character set of the execution environment
オブジェクト(object)
その内容によって,値を表現することができる実行環境中の記憶域の部分。
region of data storage in the execution environment, the contents of which can represent values
値(value)
オブジェクトが特定の型をもっていると解釈する場合のオブジェクトの内容の厳密な意味。
precise meaning of the contents of an object when interpreted as having a specific type
§6.3.2.1.
左辺値(lvalue)は,オブジェクト型,又はvoid
以外の不完全型をもつ式とする。
An lvalue is an expression with an object type or an incomplete type other thanvoid
変更可能な左辺値(modifiable lvalue)とは,配列型をもたず,不完全型をもたず, const 修飾型をもたない左辺値とし,(後略)
A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, [...]§6.5.16.
代入演算子の左オペランドは,変更可能な左辺値でなければならない。
An assignment operator shall have a modifiable lvalue as its left operand.単純代入(simple assignment)(
=
)は,右オペランドの値を代入式の型に型変換し,左オペランドで指し示されるオブジェクトに格納されている値をこの値で置き換える。
In simple assignment (=
), the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.注釈) 左辺値という名前は,代入式
E1 = E2
から由来している。すなわち,代入式の左のオペランドE1
は,(変更可能な)左辺値となる必要がある。左辺値は,オブジェクトの位置を示す値を表現するものと考えたほうがよいかもしれない。時として右辺値と呼ばれるものは,この規格においては式の値として記述する。左辺値の最も明確な例は,オブジェクトの識別子である。(後略)
The name "lvalue" comes originally from the assignment expressionE1 = E2
, in which the left operandE1
is required to be a (modifiable) lvalue. It is perhaps better considered as representing an object "locator value". What is sometimes called "rvalue" is in this International Standard described as the "value of an expression". An obvious example of an lvalue is an identifier of an object. [...]
投稿2020/03/19 06:49
編集2020/03/19 06:53総合スコア6191
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
解決済みですが誰も書いてないので書きます。
そのソースコードをコンパイルしたらどんなプログラムになるかは、アセンブリ言語にコンパイルして中身を読むと分かるでしょう。
投稿2020/03/17 00:27
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/03/18 04:04 編集
退会済みユーザー
2020/03/18 06:42
2020/03/18 06:46
退会済みユーザー
2020/03/18 06:46
2020/03/18 06:47
退会済みユーザー
2020/03/18 06:48
2020/03/18 06:49
2020/03/18 07:12 編集
2020/03/18 14:29
退会済みユーザー
2020/03/19 00:27
2020/03/19 01:21
2020/03/20 01:10
2020/03/20 02:12 編集
2020/03/20 03:56
2020/03/20 04:07
2020/03/20 04:08
2020/03/20 04:16
2020/03/20 04:21
2020/03/20 04:41
2020/03/20 04:43
2020/03/20 04:45
2020/03/20 04:47
2020/03/20 04:56
2020/03/20 05:09 編集
退会済みユーザー
2020/03/26 07:51
2020/03/26 08:17 編集
2020/03/26 08:26
退会済みユーザー
2020/03/26 08:50
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。