C言語は結局何ができるのか?
四則演算、条件判定、処理の分岐、変数の用意などなど、ここまではほかの言語とも共通。
メモリの読み書き(ポインタの利用)が特徴的。
だから、ポインタの理解はC言語においてとても重要。
また、自由度が高いので辻褄が合えばいろんなことができる。
逆に言うと、開発者は自分の書いたコードが辻褄が合うように保証してあげなければならないとも言える。
Hello Worldをいきなり理解するのは実は難しい
printf()関数は、可変長引数("...")を持つ関数であり、この意味を本当に理解するのは初心者には酷である。
第1引数で指定した書式に合った個数/データ型の引数を第2引数以降で渡さないと実行時にエラーが発生する場合があるが、
初心者はエラーの原因に気づけないかもしれない。
int main()
{
printf("Hello, world!\n");
}
int printf(const char * restrict format, ...);
データ型ってなに?
データの入れ物(変数)はデータサイズによっていくつか種類が用意されている。
データの使用目的に合わせて、適切なデータ型を選択する。
charは、符号あり1バイトのデータを入れる入れ物。
charという名前の通り、一般的には文字データを格納するために使用されるが、使用目的は文字データである必要はない。
エンディアンの違いってなに?
ビッグエンディアンとリトルエンディアン
リトルエンディアンでは、メモリの並び順が下から上へ(右から左へ)
Intel系CPUは、リトルエンディアン
int value = 0x12345678;
パディング
64bitCPU環境であれば、変数は8バイト区切りをまたぐような配置は行われない。
struct person {
char name[7];
int id;
short age;
}
例えば、構造体のメンバとメンバの間に、隙間が空くことがある。
- 例)sizeof(struct person)は、16バイトとなる。
- 例)name[]の後ろに1バイト、ageの後ろに2バイトのパディング。
ただし、コンパイルオプションでパディングなしとなるよう指定可能な場合がある。
よって、パディングの有無を前提としたコードは書かないようにする。
ポインタってなに?
メモリアドレスを表すもの。
ポインタ型とは、メモリアドレスの番地(数値)を入れるデータ型の1つ。
64bitCPU環境であれば、ポインタ型の変数は8バイトのデータサイズ。
メモリアドレスが指す先にどのようなデータ型の変数が存在するかによって、charポインタ型などと使い分ける。
つまり、charポインタ型であれば、ポインタの指すメモリアドレスから、char型のサイズの値を読み込んだり書き換えたりできる。
int id;
int *ptr = &id;
char name[7];
char *ptr = &name[0];
ポインタ型の++。 sizeof(ポインタのデータ型)の分だけ、メモリアドレスの値が更新される。
char name[7];
char *ptr = &name[0];
ptr++; // 変数ptrの値は、+1される
assert(ptr == &name[1]);
short age[4];
short *ptr = &age[0];
ptr++; // 変数ptrの値は、+2される
assert(ptr == &age[1]);
ポインタのポインタ
ポインタ変数の存在する位置を指すポインタ。
ポインタ変数の値(メモリアドレス)を読み書きするために使用する。
char name[7];
char *ptr = &name[0];
char **p_ptr = &ptr;
assert(ptr == *p_ptr);
assert(*ptr == **p_ptr);
文字列定数
Q:プログラムコード中で、
char *ptr = "ABC";
のように記述した場合、変数ptrの指す"ABC"はどこにある?
A:プログラムコードの中に"ABC"という文字列データが埋め込まれる。
実行されたプログラムのプログラムコードはメインメモリ上に読み込まれるので、
変数ptrは、プログラムコード中に埋め込まれた"ABC"のメモリアドレスを指すことになる。
関数
あるまとまった処理を関数にすると、処理を何度も繰り返し再利用できるようになる。
また、複雑な処理をサブ関数に分割することで、理解しやすいコードにすることができるし、
テストコードが簡潔となり、バグを生みにくいコードにすることもできる。
例)"z = 3x + 5y - 2" という公式(処理)を関数にするなら、
// 引数でxとyを渡せば、zの値を出力してくれる。
int function(int x, int y)
{
int z = 3 * x + 5 * y - 2;
return(z)
}
スタックメモリとは?
プログラムが起動する際、OSは各プログラムにスタック目的のメモリを用意してくれる。
スタックの名の通り、Last In First Outで値を出し入れするもの。
関数の引数や関数内で宣言したローカル変数は、スタック上に確保される(スタックに積まれる)。
CPUにはスタックポインタと呼ばれるレジスタが存在する。スタックポインタは、スタックの現在使用中の位置を指す。
関数コールの仕組み
リターンアドレス、および、コールする関数の引数をスタックに積む。
プログラム実行の現在位置(CPUレジスタであるプログラムカウンタの値)を、コールする関数の先頭アドレスに変更する。
関数内の処理が実行される。
戻り値をレジスタにセットする。
リターンアドレスの位置にプログラムカウンタの値を戻す。
関数のコール元に戻り、処理が継続される。
int sub(int c)
{
return c * c; // (3)戻り値がレジスタにセットされ、スタックから引数aが解放される。
}
int function()
{
int a = 10;
int b = 0; // (1)関数functionに入ると、ローカル変数a,bがスタックに積まれる。
b = sub(a); // (2)スタックにリターンアドレスと関数subの引数cが積まれる。
// (4)関数subから戻ると、スタックからリターンアドレスが解放され、レジスタの戻り値が変数bにセットされる。
return b;
}
関数ポインタ
関数もメモリアドレスを持つ。
配列の名前が配列の先頭アドレスを表すのと同様に、関数の名前は関数の先頭アドレスを表す。
// 動物の種類を表示する関数の型を、関数ポインタとして宣言する。
typedef void (*animal_type)(void);
// animal_type型の関数ポインタをメンバに持つ構造体
struct animal {
animal_type print_type; // print_type()と書くと関数ポインタが指す関数の実体を呼ぶことができる。
}
void print_dog(void)
{
printf("Dog\n");
}
void print_cat(void)
{
printf("Cat\n");
}
// 構造体animalを犬として初期化する処理
void init_dog(struct animal *dog)
{
// 関数ポインタprint_typeに、print_dog関数のアドレスをセットする。
dog->print_type = print_dog;
}
// 構造体animalを猫として初期化する処理
void init_cat(struct animal *cat)
{
// 関数ポインタprint_typeに、print_cat関数のアドレスをセットする。
cat->print_type = print_cat;
}
int main(int argc, char *argv[])
{
struct animal Animal = { NULL };
// 犬で初期化する
init_dog(&Animal);
Animal.print_type(); // print_dog()が実行され、"Dog"と表示される
// 猫で初期化する
init_cat(&Animal);
Animal.print_type(); // print_cat()が実行され、"Cat"と表示される
}
最後に
ポインタでC言語に挫折する人もいるかもしれませんが、
図を描いてみたり、実際にメモリ上の値をデバッガで見て1つ1つ確認することで
理解できるようになると思います。
初心者脱出を目指して諦めずに頑張りましょう!