変数
変数は、おそらくプログラミング言語における最も広く適用可能な概念です。 実際に、私が見てきたプログラミング言語では、変数の概念が採用されています(DSLを除く)。
では変数とは何でしょうか? 単純に言えば、変数は「存在するもの」です。 プログラミング言語では、変数はプログラムの状態を構成します。 変数は、単純な整数や文字列からディスク上のファイルやネットワークソケットなど、何でもあります。 ISAの観点からは、変数は通常、メモリの一部を抽象化したものです。
コードラボ:基本的な変数
PythonとC++でいくつかの基本的な変数を作成しましょう!
Pythonスクリプトに次の内容を入力します。
スクリプトを実行して出力を観察します。 まだPythonがインストールされていない場合やPythonスクリプトの実行方法がわからない場合は、AIに尋ねてください。 (ヒント: Anacondaなどの環境マネージャを使用すると、オペレーティングシステムが破損することなく済みます。)
上記のコードでは、a
、b
、c
は変数であり、最後のprint
文はそれらの内容をコンソールに出力します。
さて、次にC++の変数の動作を見てみましょう。
C++ソースファイルに次の内容を入力します。
#include <iostream>
#include <string>
int main() {
int a = 5;
std::string b = "コアラはとてもかわいいです!";
std::cout << a << "\n" << b << std::endl;
return 0;
}
ファイルをコンパイルして実行します。 もし方法がわからない場合は、AIに尋ねてください。
PythonコードとC/C++コードを比較してみてください。 PythonとC++が変数の作成方法でどのように異なるかわかりますか? なぜそのような違いがあるのでしょうか? (ヒント: Pythonはインタープリテッド言語であり、C++はコンパイル言語です。 この質問は、まだコーディングを始めたばかりの場合は少し難しいです。 答えがわからない場合は、AIにこの質問を聞いて、どのように答えるか確認してみてください。)
変数の種類
変数に関連する一般的な概念の1つは、その型です。 変数の型は次の質問に答えます。
- これはどの種類のものですか?
- それには何ができますか?
基本的に、型は変数に意味を与えます。 既知の型がないと、変数は意味のないメモリピースになります。 何ができるかわかりません。 型は、変数の抽象化とも考えることができます。 変数は裏では単なるメモリの一部ですが、 ほとんどの開発者はそのように考えていません: 明らかに、整数、文字列、ファイルなどの変数という言葉で考える方がはるかに簡単であり、 バイナリ値0xffee37を含むメモリピースの代わりになっています。
同時に、重要なことは、型(および変数)は物理的なオブジェクトではなく、抽象化であるということです。 C++を使用してアプリケーションをコーディングする場合は、 コンパイルされたアプリケーションには型と変数は含まれていません。 メモリ操作とISAで定義されたバイナリマシンコードのみが含まれています。
基本的な変数の構文
異なるプログラミング言語で定義されている正確な変数の構文は異なる場合がありますが、 多くの共通点があります。 このサブセクションでは、それらの共通点を説明します。
変数の代入
ほぼすべての言語では、変数に値を代入するために単一の等号「=」を使用します。
変数に新しい値を代入すると、その値に変数の内容が変更されます。 ISAの観点からは、値を変数に代入することは、変数に対応するメモリピースに書き込むことと同等です。
次の表は、いくつかの言語で整数型の変数「a」に値「5」を代入する方法を示しています。
Python | C/C++ | Rust | Java | C# |
---|---|---|---|---|
a = 5 |
a = 5; |
a = 5; |
a = 5; |
a = 5; |
変数の宣言
コンパイルされる言語では、変数に値を代入したり読み取ったりする前に変数を宣言する必要があります。 変数の宣言は、単に「この変数が存在する」と「宣言するプレースホルダ」の役割を果たします。
たとえば、C/C++では「int a;」という表記を使ってint
(つまり整数型)の変数a
を宣言します。
Rustでは、let a = 5;
と同じようにします。
宣言してから値を代入することになると長くなりすぎるため、 多くのコンパイルされる言語では変数の宣言と初期値の代入を組み合わせることができます。
次の表は、いくつかの言語でint
型の変数を宣言し、値を5
に設定する方法を示しています。
C/C++ | Rust | Java | C# |
---|---|---|---|
int a = 5; |
let a = 5; |
int a = 5; |
int a = 5; |
型推論と型注釈
Rustでは、初期値を割り当てることによって変数の型を明示的に注釈する必要はありません。
これはコンパイルされ、静的型付けされた言語ですが、
Rustコンパイラは変数の型をフェアな適切な値(この場合は5
)を割り当てることによって推論することができます。
コンパイラが変数の型を推論できる能力は、型推論と呼ばれます。 型推論は多くのプログラミング言語でサポートされており、 型名が長い場合や変数の正確な型がわからない場合に非常に便利です。
型推論は「型の注釈を付けないことは動的型付けと同じではない」ということを示しています。
逆に、型の注釈を付けることは静的型付けと同じではありません。 たとえば、Pythonでは変数の型の注釈を許可しますが、 変数の型注釈を要求しません。 ユーザーとのインタフェースを持つコード(ライブラリ関数のシグネチャなど)では、 変数の型を注釈することが一般的に良い慣習です。
質問:変数の宣言
コンパイルされた言語では宣言が必要なのに対し、インタプリテッド言語ではなぜ宣言が不要なのでしょうか?
ヒント:静的型付けと動的型付けについて考えてみてください。さらに実行される方法についても考えてみてください。 答えがわからない場合は、AIに尋ねてみてください。
プリミティブ型
ほとんどのプログラミング言語は、事前に定義されたプリミティブ型のセットをサポートしています。 これらの型は通常、原子的であり、変数のメモリ使用量が小さく、 より小さな部分に分解することはできません。
次の表は、ほとんどのプログラミング言語がサポートしている一部のプリミティブ型を示しています (変数タイプの「識別子」は、特定の言語でそのタイプの名前です)。
タイプ | 識別子(Python) | 識別子(C++) | 識別子(Rust) |
---|---|---|---|
整数 | int |
int (基本)、unsigned int 、int32_t 、long 、long long 、size_t (他のバリエーション) |
usize 、i32 、u32 、i64 、u64 |
小数点数 | float |
float 、double |
f32 、f64 |
ブール値 | bool |
bool |
bool |
文字 | N/A | char |
char |
ポインタ | N/A | <type>* 、int* 、float* 、void* など |
&<type> 、&i32 、&f32 、&bool など |
質問
上記の表を見て、次の興味深い質問ができます:
- Rustのタイプ識別子上の「u」と「i」は何を意味するのですか?
- 上記のタイプ識別子の数字「32」、「64」などは何を意味するのですか?
int
とint32_t
のどちらを使用した方がよいでしょうか?- 十進数を「float」と呼ぶのはなぜですか?「decimal」ではなくて。
- 「ブール値」の意味は何ですか?
- 「ポインタ」の意味は何ですか?なぜPythonにはポインタがないのですか?
- C++とRustにはなぜ整数/小数用のさまざまなタイプがあるのですか?
これらの質問に答えることができますか? (答えがわからない場合は、AIに質問してみてください。 初心者にとっては難しい質問もありますので。)
コードラボ:型変換
次のC++コードは、10進数を整数変数に割り当て、整数変数の値を表示します。
コードをコンパイルして実行します。 何が観察されますか?なぜこのように動作するのでしょうか?
次に、Rustでも同じことを試してみましょう。
コードをコンパイルしてみてください。 何が観察されますか?
C++とRustコンパイラの動作から判断すると、C++の設計はRustの設計よりも優れていると思いますか、 それとも逆でしょうか?なぜですか?
(ヒント:「silent bugs」を検索してください。)
コードラボ:型と変数名は存在するか?
次のシンプルなC++コードをご覧ください。
コードをアセンブリ(ASM)にコンパイルしますが、 最適化オプションをオフにするようにしてください (方法がわからない場合は、AIに尋ねてください)。
おそらく次のような結果が得られるでしょう。
.file "test.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $5, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
上記のコンパイルされたASMで変数の名前やint
を見ることができますか?
上記のASMで変数koalas_are_so_cute
がどこにあるか特定できますか?
この観察結果に基づいて、コンパイルされた実行可能ファイルには変数名や型が存在するのでしょうか?
なぜでしょうか?
ヒント:初心者には非常に難しい質問です。C++のソースコードとASMを提供し、AIに各行のASMがやっていることを説明してもらいましょう。
コードラボ:大きな数値
C++、Rust、Pythonで、整数が表現できる最大の数値を求めるコードを書いてみてください。
(C++ではint
、Rustではi32
、Pythonではint
を使用します。)
プログラムの出力を観察し、インターネットで調査してみてください。 Pythonが整数を表現する方法は、C++やRustとは異なる方法であることを説明できますか?
複合型
ほとんどのプログラミング言語では、独自の複合型を定義できます。
複合型は、他の型のフィールドで構成された型です。
たとえば、次のRustコードは、10進数番号のフィールド「score」と文字列のフィールド「name」を持つ
Student
という名前のstruct
(複合型)を定義しています。
フィールドも複合型であることに注意してください。
同じStudent
型をC++で定義するには次のようにします:
Pythonでは次のようにします:
複合型の一般的な識別子
プログラミング言語は、複合型を定義するためのほぼ同じ構文を持っています。 複合型は通常「struct」または「class」と呼ばれるものです。 したがって、言語の構文を知りたい場合は、初めて使う言語で「struct」または「class」を検索してみてください。 たとえば、Goで「struct」を定義する方法を検索してみることができます。
コードラボ:複合型
Rustで独自の複合型を定義してみてください。 次に、この型の変数を作成し、それを適切にコンソールに表示するためにAIの助けを求めてみてください。
列挙型
ほとんどすべてのプログラミング言語は、列挙型を定義することができます。 列挙型は、有限の数の値の1つを取る型です。それぞれの値は列挙バリアントと呼ばれます。
たとえば、次のC++コードは、「RED」、「GREEN」、または「BLUE」になるColor
という名前のenum
型を定義しています。
次に、「RED」に設定されたColor
変数a
を宣言します。
拡張された列挙型
一部のプログラミング言語の列挙型は、単に有限の列挙バリアントのセットを定義するだけでなく、より多くの機能を備えています。
たとえば、Rustでは、さまざまな列挙バリアントに異なるフィールドを関連付けることができます。
変数に関連する一般的な概念と設計選択
静的型付けと動的型付け
変数に関連する一般的な設計選択の1つは、静的型付けまたは動的型付けを使用するかどうかです。
静的型付けは、変数の型がそのライフサイクル中に変更されないことを意味します。 一方、動的型付けは、変数の型を実行時に変更することができます。
一般的に(必ずしもそうではないが)、コンパイルされた言語では静的型付けが使用され、 インタプリテッド言語では動的型付けが使用されます。
質問:静的型付けと動的型付け
コンパイルされた言語ではなぜ通常静的型付けを使用し、 インタプリテッド言語ではなぜ動的型付けを使用するのでしょうか?
ヒント:プログラムの実行時に何が起こるかについて考えてみてください。 この質問は初心者には少し難しいかもしれませんが、AIに助けを求めることもできます。
スタックとヒープ変数
より低レベルな言語、特にガベージコレクタのない言語 (C++やRustなど)では、スタック変数とヒープ変数を区別することが重要です。
名前が示すように、スタック変数のメモリはスタックに割り当てられます。 一方、ヒープ変数のメモリはヒープに割り当てられます (ただし、ヒープ変数にはスタック上にポインタやメタデータも格納されることがあります)。
通常、スタック変数は小さく、ライフタイムが短い傾向があります。 一方、ヒープ変数は大きく、ライフタイムが長い傾向があります。
質問:スタック変数とヒープ変数
よく現れる質問に対してAIとインターネットのヘルプを活用して答えてみましょう:
- なぜスタック変数のライフタイムが短いのですか?
- スタック変数とヒープ変数のアクセスはどちらが速いですか?その理由は何ですか?
- スタック変数を使用する場合とヒープ変数を使用する場合はそれぞれいつですか?
- Pythonではスタック変数とヒープ変数を区別しますか?なぜですか?
変数の可変性とデフォルトの可変性
変数に関連するもう1つの重要な概念は、変数が可変か不変かということです。 通常、変数は可変または不変のいずれかになります。
変数が可変である場合、初期値を割り当てた後でも別の値を代入することができます。 変数が不変である場合、初期値を設定した後に別の値を代入することはできません。
多くのプログラミング言語には可変性の概念があり、変数を可変または不変にマークすることができます。
ただし、デフォルトで変数を可変または不変にするかどうかについては一致しているものが少なく、 (つまり、ユーザーが可変性を指定しない場合)のデフォルトの可変性については一致しているものが少ないです。 たとえば、C/C++では変数はデフォルトで可変ですが、Rustでは不変です。
質問:デフォルトの可変性
デフォルトで変数を可変または不変にするのは、どちらがよりよい設計選択でしょうか? C/C++では変数をデフォルトで可変にしている一方、Rustでは不変にしていますが、なぜでしょうか?
ヒント:これらの質問に答えるには、コーディングの経験が必要ですので、 初心者の場合はAIに質問してみてください。
ライフタイム
変数のライフタイムは、変数が有効で存在する範囲です。
ほとんどのプログラミング言語でライフタイムの概念が適用されますが、 明示的にライフタイムを注釈するための特別な文法を持つ言語はほとんどありません。 例外は、Rustです。
質問:ライフタイム
変数のライフタイムを知ることはなぜ役立つのでしょうか?
コードラボ:ライフタイムの実践
次のRustコードを確認してください。
変数a
のライフタイムは何ですか?
b
はどうですか?
AIの助けを借りながら、これらの質問に答えてみてください。
所有権
所有権は、リソース管理の重要な概念であり、モデルでもあります。 基本的には、所有権システムは、リソースの1つ以上の所有者変数を指定します。 すべての所有者がなくなると、リソースは解放されます。
所有権は、本番用の範囲外のトピックですので、ここでは詳細には触れません。 興味がある場合は、「Rust ownership」、「RAII」、「C++のスマートポインタ」などを検索してみてください。
結論
お疲れ様でした! すべての資料、質問、およびコードラボを経て、 変数についての基本的な理解と、 それらをコードを書く際にどのように使用するかについて学ぶことができました。 より重要なのは、AIやインターネットを参照して必要な情報を見つける方法を学んだことです!
このセクションでは、次のことを扱いました。
- 変数は「もの」を表します。
- 変数には型があります。
- 変数に関連する一般的な構文:変数の代入、変数の宣言、 プリミティブおよび複合型、列挙型。
- 変数に関連する一般的な概念と設計選択:静的型付けと動的型付け、 スタックとヒープ変数、変数の可変性、ライフタイム、所有権。