C 言語なんもわからん
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
define マクロには第二引数はなくても良い
第二引数がある使い方
競プロでよく出てくるマクロの使い方しか知らなかった。こういうの↓(これは C++ だけど)
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (int i = 0; (i) < (n); ++(i))
int main() {
rep(i, 10) {
cout << i << '\n';
}
}
第二引数を渡さない使い方
Bash のソースコードのヘッダファイル include/stdc.h
にて、第二引数を渡さない使い方をしている部分があり、読み方がよくわからなかった。
GitHub Copilot Chat に聞いたところ以下のような回答が返ってきて理解できた。
Q. define マクロって第二引数に値がなくても良いの?
回答
はい、その通りです。C言語のプリプロセッサディレクティブ
#define
は、マクロ名だけを指定し、値を指定しないことも可能です。このようなマクロは、コード内で特定の条件が満たされているかどうかをチェックするためによく使用されます。例えば、以下のようなコードがあります:
#define ENABLE_FEATURE #ifdef ENABLE_FEATURE // ENABLE_FEATUREが定義されている場合にのみ実行されるコード #endif
この例では、
ENABLE_FEATURE
マクロは値を持っていませんが、#ifdef
ディレクティブを使用して、そのマクロが定義されているかどうかをチェックしています。このように、値を持たないマクロは、コードの特定の部分を有効化または無効化するためのフラグとして機能します。
なるほど、第二引数がなくてもマクロは定義でき、その機能を使ってコンパイル時のフラグとして使えるのか。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
typedef の文法
Bash の command.h
の typedef command {...} COMMAND;
を見た時、なぜ構造体の名前の command
があるのに型定義の末尾の COMMAND
があるのかわからなかった(昔調べた気がする、、、)。
Q. typedef の記法がよくわからないんだけど、なぜ command と末尾の COMMAND が存在するの?
なるほど、typedef 型 型名;
で型のエイリアスを定義するための文法か。そしてここでの書き方は構造体の定義と型定義を同時に行うための書き方なのか。
これわすれてたのやばみ。変な思い込みしてたな。
typedef
はC言語のキーワードで、新しい型名を定義するために使用されます。typedef
を使用すると、既存の型に対して新しい名前を付けることができます。これはコードの可読性を向上させたり、特定の型を短い名前で参照できるようにするために役立ちます。以下のコードを見てみましょう:
typedef struct command { // ... } COMMAND;
このコードは、
struct command
という構造体にCOMMAND
という新しい名前を付けています。これにより、struct command
をCOMMAND
として参照できるようになります。例えば、
struct command
の変数を宣言するには通常以下のようにします:struct command my_command;
しかし、
typedef
を使用してCOMMAND
という別名を定義した場合、以下のように短く書くことができます:COMMAND my_command;
このように、
typedef
はコードを短くし、可読性を向上させるために使用されます。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
なんかで見たと思ったらこれだ。Token Token
の部分、同じ識別子をしてて気持ち悪いけど、これで struct Token *token;
を Token *token;
と書けるようにしてるんだね。
// トークンの種類
typedef enum {
TK_RESERVED, // 記号
TK_NUM, // 整数トークン
TK_EOF, // 入力の終わりを表すトークン
} TokenKind;
// トークン型
struct Token {
TokenKind kind; // トークンの型
Token *next; // 次の入力トークン
int val; // kindがTK_NUMの場合、その数値
char *str; // トークン文字列
};
typedef struct Token Token;
// 現在着目しているトークン
Token *token;
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
volatile ってなに?
Bash の eval.c
の reader_loop()
関数内にて、変数を宣言するときに volatile
というキーワードがあった。
COMMAND * volatile current_command;
Q. これってどういう意味の書き方なの? 回答 by GitHub Copilot Chat
COMMAND * volatile current_command;
というコードは、C言語でのポインタ変数の宣言です。ここではcurrent_command
という名前のポインタ変数を宣言していますが、このポインタが指す先の内容(つまり、COMMAND
型のオブジェクト)がvolatile
として宣言されています。
volatile
キーワードは、その変数がコンパイラによる最適化から除外されることを示します。つまり、コンパイラはvolatile
が付けられた変数がプログラムの実行中に予期せぬ方法で変更される可能性があると認識します。そのため、コンパイラはvolatile
変数に対する読み取りや書き込みを最適化や再配置の対象から除外します。この
volatile
が付けられたポインタ変数current_command
は、マルチスレッド環境や割り込みハンドラなど、複数のコンテキストからアクセスされる可能性がある場合に有用です。これにより、current_command
が指すCOMMAND
オブジェクトが予期せぬタイミングで変更されることを防ぎ、データの一貫性を保つことができます。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
Q. volatile はどういう役割を持つの?一般的な説明を丁寧にお願い。そのあと、bash の文脈でなぜこの current_command に volatile をつける必要があるのか教えて。
回答
volatile
はC言語のキーワードで、変数がコンパイラによる最適化から除外されることを示します。具体的には、volatile
が付けられた変数は、プログラムの実行中に予期せぬ方法で変更される可能性があるとコンパイラに伝えます。そのため、コンパイラはvolatile
変数に対する読み取りや書き込みを最適化や再配置の対象から除外します。
volatile
キーワードは、以下のような状況で使用されます:
- ハードウェアレジスタへのアクセス:ハードウェアレジスタは、プログラムの実行中にハードウェアによって予期せぬタイミングで変更される可能性があります。そのため、これらのレジスタへのアクセスを表す変数には
volatile
を使用します。- 割り込みサービスルーチン:割り込みサービスルーチンは、非同期的に実行され、共有変数を変更する可能性があります。これらの変数は
volatile
で宣言する必要があります。- マルチスレッドアプリケーション:マルチスレッドアプリケーションでは、複数のスレッドが共有変数にアクセスします。これらの変数は、予期せぬタイミングで変更される可能性があるため、
volatile
で宣言する必要があります。bashのコードにおける
volatile
の使用について説明します。volatile
キーワードがcurrent_command
変数に適用されています。これは、current_command
がシグナルハンドラや他のスレッドなど、プログラムの主要な制御フローの外部から変更される可能性があることを示しています。具体的には、bashはシグナルを使用してタイムアウトを実装しています。タイムアウトが発生すると、シグナルハンドラが呼び出され、制御が現在のコマンドからシグナルハンドラに移ります。このとき、
current_command
の値が変更される可能性があります。そのため、current_command
はvolatile
として宣言されています。これにより、コンパイラはcurrent_command
の値がいつでも変更される可能性があると認識し、その最適化を防ぎます。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
Q. なぜ予期せぬ方法で変更される可能性がある変数は最適化をしてはいけないのですか?最適化について詳しく説明してください。
コンパイラの最適化とは、プログラムの実行速度を向上させたり、メモリ使用量を減らしたりするために、コンパイラが生成する機械語コードを改良するプロセスのことを指します。例えば、不要な計算を削除したり、ループの展開を行ったり、定数の畳み込みを行ったりします。
しかし、
volatile
キーワードが付けられた変数に対しては、これらの最適化を行うことは適切ではありません。なぜなら、volatile
変数はプログラムの実行中に予期せぬ方法で変更される可能性があるからです。これは、例えばハードウェアレジスタや割り込みサービスルーチン、マルチスレッド環境などからのアクセスによって変更される可能性があります。コンパイラの最適化が行われると、コードの一部が削除されたり、順序が変更されたりすることがあります。これは通常、プログラムの動作に影響を与えません。しかし、
volatile
変数に対してこれらの最適化を行うと、プログラムの動作が予期せぬ方法で変更される可能性があります。例えば、コンパイラがvolatile
変数の読み取りや書き込みを削除したり、順序を変更したりすると、プログラムは正しく動作しなくなる可能性があります。したがって、
volatile
キーワードは、コンパイラに対して特定の変数が予期せぬ方法で変更される可能性があることを示し、その変数に対する最適化を抑制する役割を果たします。これにより、volatile
変数に対する読み取りや書き込みは、プログラマが指定した順序で、かつ必ず実行されることが保証されます。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
Q. 例えば、Rust では volatile に相当する機能はある?
はい、Rustには
volatile
に相当する機能が存在します。それはstd::ptr::read_volatile
とstd::ptr::write_volatile
という2つの関数です。これらの関数は、メモリの特定の位置からの読み取りや書き込みを行い、その操作が最適化により削除されないことを保証します。以下に、これらの関数の使用例を示します:
use std::ptr;
let mut data = 42;
let ptr = &mut data as *mut i32;
unsafe {
let value = ptr.read_volatile(); // volatile read
ptr.write_volatile(0); // volatile write
}
このコードでは、
data
という変数のポインタを取得し、そのポインタを通じてread_volatile
とwrite_volatile
を使用しています。これにより、data
への読み取りと書き込みが最適化により削除されないことが保証されます。ただし、Rustの
volatile
操作はC言語のそれとは異なり、スレッド間のデータ競合を防ぐためのものではありません。Rustでは、スレッド間のデータ競合を防ぐためには、std::sync
モジュールの機能(例えば、Mutex
やRwLock
など)を使用する必要があります。
![nukopy](https://res.cloudinary.com/zenn/image/fetch/s--qEIa5cWX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/b55270ed59.jpeg)
Q. コンパイラの最適化とメモリ管理とハードウェアレジスタの知識がないとちょっと理解が及ばないかもと思った。むずかしい。
確かに、コンパイラの最適化、メモリ管理、ハードウェアレジスタなどは深い知識を必要とするテーマであり、初めて学ぶときは難しく感じることが多いです。
しかし、基本的な概念を理解することで、これらのテーマがどのようにプログラムの動作に影響を与えるのかを把握することができます。
コンパイラの最適化:コンパイラは、人間が書いたソースコードをコンピュータが理解できる機械語に変換します。このとき、コンパイラは単に変換するだけでなく、生成される機械語が効率的に動作するように「最適化」を行います。例えば、不要な計算を省いたり、ループの回数を減らしたりすることがあります。
メモリ管理:プログラムが動作するためには、変数の値を保存したり、関数の呼び出し履歴を保持したりするためにメモリが必要です。このメモリの割り当てや解放を適切に行うのがメモリ管理です。不適切なメモリ管理はプログラムの動作を不安定にする原因となります。
ハードウェアレジスタ:レジスタはCPU内部に存在する、非常に高速にアクセスできる小さなメモリ領域です。レジスタは計算の結果を一時的に保存したり、特定の情報(例えば、割り込みの発生源)を保持するために使用されます。
これらのテーマは、プログラムがどのように動作するのか、なぜ特定のコードが必要なのかを理解するための基礎となります。それぞれのテーマについて詳しく学ぶことで、より深い理解を得ることができます。