【※ 当記事は2020年7月25日時点の情報です】
ペイヴメント(@pavement1234)です。
C言語の基本的な文法をざっくり網羅的に知りたい。
バージョン情報
Windows10 Home(64bit)1903
VMware Player 15上で動くUbuntu18.04
中編で出来ること
C言語の基本的な文法をざっくり紹介します。
極力さらっと読める感じにしていますが、
たまにマニアックな話題(中・上級者向けのネタ)に触れています。
中編では
⑦関数、
⑧構造体・共用体・enum、
⑨配列、
⑩ポインタ、
⑪メモリの動的確保、
⑫キャストを説明します。
⑦関数、
⑧構造体・共用体・enum、
⑨配列、
⑩ポインタ、
⑪メモリの動的確保、
⑫キャスト
⑦関数
戻り値 関数名( 引数1, 引数2, 引数3, … ){ 処理 }という構文です。
関数とは引数と呼ばれるパラメタを受け取り、
何らかの処理を実行して、戻り値を返すものです。
※引数も戻り値もvoidを指定することで無しにできます。
前編でC言語プログラムはmain関数から始まるルールという話をしましたが、
main関数も関数の1つです。
プログラムはまとまった処理毎に関数分割することで見通しが良くなりますし、
再利用しやすくなります。
サンプルプログラム:関数分割
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> void AAA( void ) { printf("AAA\n"); } int BBB( int a, int b ) { printf("BBB a=%d b=%d\n", a, b ); return 0; } int main( int argc, char** argv ) { AAA(); BBB( 1, 2 ); AAA(); return 0; }<br /><br /> |
解説します。
3~6行目:AAA()という関数を定義しています。
”AAA\n”という文字列をprintf()します。
8~12行目:BBB()という関数を定義しています。
“BBB a=%d b=%d\n”という文字列をprintf()します。
入力パラメタa、bによって表示が変わります。
16行目:main関数からAAA()を呼び出しました。
17行目:main関数からBBB()を呼び出しました。
18行目:main関数からAAA()を呼び出しました(2回目)。
サンプルプログラム:関数分割しない
1 2 3 4 5 6 7 8 |
#include <stdio.h> int main( int argc, char** argv ) { printf("AAA\n"); printf("BBB a=%d b=%d\n", 1, 2 ); printf("AAA\n"); return 0; } |
行数は減りますが、
たとえば関数AAA()で実装していた
printf(“AAA\n”);をprintf(“ABA\n”);に変更したい場合、
2か所変更しなければなりません。
メンテナンス性を上げるためにも関数分割は重要です。
⑧構造体・共用体・enum
構造体
struct 構造体名 { 型名 メンバ変数1; 型名 メンバ変数2; 型名 メンバ変数3; …. 型名 メンバ変数n; }という構文で宣言します。
構造体とは複数の変数を1つの型にまとめて扱いやすくするものです。
例えば3次元座標という構造体を作り
x, y, zという3つの変数をまとめて扱ったりすると便利です。
構造体を定義(実体確保)するときは
truct 構造体名 構造体変数名;と書きます。
メンバ変数にアクセスするときは
構造体変数名.メンバ変数 = 値;という形でアクセスします。
サンプルプログラム:struct aaaという構造体を宣言し、main関数の中で定義して使う
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> //構造体宣言 struct aaa{ int a1; char a2; }; int main( int argc, char** argv ) { struct aaa a; a.a1 = 1; a.a2 = 2; print( "a1=%d a2=%d\n", a.a1, a.a2 ); return 0; } |
【ノウハウ】宣言と定義の違い
宣言とは(変数とか関数の)名前の型をコンパイルに知らせること。
定義とは名前を持つ実体(メモリ)を確保すること。
という違いがあります。
分類 | 宣言 | 定義 |
変数 | extern int aaa; | int aaa; |
関数 | void AAA( void ); | void AAA( void ){ } |
【ノウハウ】構造体はネスト(入れ子)することができる
構造体は別の構造体をメンバに持つことが出来ます。
サンプルプログラム:bbbという構造体にaaaという構造体のメンバ変数aを持つ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> struct aaa{ int a1; char a2; }; struct bbb{ struct aaa a; int b1; char b2; }; int main( int argc, char** argv ) { struct bbb b; b.a.a1 = 1; b.a.a2 = 2; b.b1 = 3; b.b2 = 4; return 0; } |
メンバ変数aには15行目のように
b.a.a1 = 1;という形でアクセスします。
【ノウハウ】typedefで型名を新しくつけることが可能
typedef 既存の型名 新しい型名;
という構文で型名を新しくつけることが出来ます。
構造体を定義する(実体を作る)ときに
毎回structを付けるのは面倒なので
typedefで別名を定義しましょう。
サンプルプログラム:typedefで構造体の別名を定義
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> typedef struct _aaa{ int a1; char a2; } AAA; int main( int argc, char** argv ) { AAA a; a.a1 = 1; a.a2 = 2; return 0; } |
上記例ではstruct _aaaという構造体を
AAAという新しい型名で宣言しました。
type.hという標準ヘッダでは
以下のように型名を再定義しています。
1 2 3 4 |
typedef unsigned char u_char; typedef unsigned short u_short; typedef unsigned int u_int; typedef unsigned long u_long; |
共用体
union 共用体名 { 型名 メンバ変数1; 型名 メンバ変数2; 型名 メンバ変数3; …. メンバ変数n; }という構文です。
構文的には構造体とほとんど一緒ですが、
構造体のメンバ変数がそれぞれ独立のメモリ領域を持つのに対して、
共用体のメンバ変数はメモリ領域を共有します。
ちょっと分かりにくいと思うので
サンプルプログラムを使って具体的に説明します。
サンプルプログラム:union
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> union uuu { char a; short b; int c; long d; }; void main( void ) { union uuu u; int size = sizeof( union uuu ); printf( "sizeof( union uuu )=%d\n", size ); u.d = 1; printf( "u.a=%d\n", u.a ); printf( "u.b=%d\n", u.b ); printf( "u.c=%d\n", u.c ); printf( "u.d=%d\n", u.d ); return; } |
上記の例で示したunion uuuという共用体は
a、b、c、dという4つのメンバ変数を持ち、
普通に計算すると11バイト(1+2+4+4)必要ですが、
sizeofでunion uuuのサイズを調べると4バイトです。
要はint型の変数1つ分のメモリしか確保されていません。
u.dに1を代入するとa、b、cも1になります。
実行結果は以下の通りです。
$ gcc -g -o test test.c
./test
sizeof( union uuu )=4
u.a=1
u.b=1
u.c=1
u.d=1
【ノウハウ】
unionは昔、推奨されていなかったが今は…
昔々、共用体はメモリを節約するためにある構文だがわかりにくいので非推奨という話を
先輩から聞いたことがあるのですが、
最近はLinuxカーネル内で拡張性を持たせるためにunionが使われているという話を
聞きました。
【プログラマーの心得】
過去の常識にとらわれず常に最新情報をウォッチしましょう。
≫参考:第9回 機能拡張でよく使われる共用体(union)※途中から有料記事なのでご注意ください(私は無料部分しか読んでません)
enum
enum キー名 { メンバ1, メンバ2, … , メンバn };という構文です。
共用体とは連続するユニークな定数を宣言する構文です。
列挙型とも言います。
例えばコマンドIDみたいな重複が許されない定数を#defineするには
重複しないように注意が必要ですが
enumを使えば必ずユニークになるのでストレスフリーです。
最初のメンバは0で初期化され、次のメンバは1。その次は2と+1されます。
代入することで初期値を変えることも可能。
サンプルプログラム:enum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> enum Alphabet{ A, B, C, D, LAST, }; int main( void ) { enum Alphabet val; val = A; printf("A=%d\n", val ); val = B; printf("B=%d\n", val ); val = C; printf("C=%d\n", val ); val = D; printf("D=%d\n", val ); val = END; printf("LAST=%d\n", val ); return 0; } |
上記例のように最後の要素を意味する「LAST」を定義しておくと、
LAST+1で全要素数を知ることが出来て便利です。
実行結果は以下の通りです。
$ gcc -g -o test test.c
./test
A=0
B=1
C=2
D=3
LAST=4
⑨配列
型名 変数名[ 要素数 ];という構文です。
配列とは、同じ型の変数を要素数分まとめて宣言するものです。
配列を定義(実体確保)すると連続されたメモリエリアが確保されます。
例えばint型のaという変数を定義した場合、
このように0xC0000000から0xC000000Fまでの
連続16バイトの領域が確保されます。
変数名[ 添え字 ]という構文で
配列の各要素にアクセスできます。
添え字とは0から始まる要素の通し番号です。
例えば要素数を4にした場合、0、1、2、3という添え字が使えます。
要素数-1 = 最後の添え字になることに注意。
サンプルプログラム:配列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> int main( int argc, char** argv ) { int a[4]; int i; for( i = 0; i < 4; i++ ){ a[i] = i; } for( i = 0; i < 4; i++ ){ printf( "a[%d]=%d\n", i, a[i] ); } return 0; } |
これを実行すると、こうなります。
$ gcc -g -o test test.c
$./test
a[0]=0
a[0]=1
a[0]=2
a[0]=3
※n次元配列は後編で少し説明します。
⑩ポインタ
型名 *変数名; という構文です。
ポインタとはアドレスを保持する変数です。
初心者にとってポインタは難関だと思いますが、
ポインタを理解すると色々なプログラムが読めるようになります
頑張りましょう。
⑨配列の項で説明したプログラムを
ポインタで書き換えてみます。
サンプルプログラム:配列をポインタに書き換える
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <stdio.h> int main( int argc, char** argv ) { int a[4]; int *pa = &a[0]; // 配列の先頭アドレスをポインタ変数に代入 // 以降、pa経由で配列aにアクセスできます int i; for( i = 0; i < 4; i++ ){ *pa = i; // pa++; // アドレスを1つ進める(int型は4バイトなので1回++すると4バイト進む) } pa = a; // 配列名は配列の先頭アドレスです for( i = 0; i < 4; i++ ){ printf("a[%d]=%d\n", i, *pa ); pa++; } return 0; } |
6行目:配列の先頭アドレスは&配列名[ 0 ]で取得できますので、
配列の先頭アドレス&a[0]をポインタ変数paに代入しました。
10行目:ポインタ変数のアドレスが示す実体(int型の変数)にアクセスするには
*(アスタリスク)を付けます。
11行目:ポインタ変数を1つ進めます。
int型のアドレスを1つ++すると、4バイト進みます。
13行目:配列の変数名=配列の先頭アドレスなので、
ポインタ変数paに配列名aを代入します。
※おそらく「???」と思った方もいらっしゃると思います。
私は最初に聞いたとき理解できませんでした。
もし今わからなくても慣れてきますので大丈夫です。
これを実行すると、こうなります。
配列版と同じですね。
$ gcc -g -o test test.c
$./test
a[0]=0
a[0]=1
a[0]=2
a[0]=3
ダブルポインタ(**)は後編で説明します。
main関数の第2引数argv(argument vector)は
char型のダブルポインタです。
⑪メモリの動的確保
mallocという標準関数でヒープメモリを動的確保し、
freeという標準関数で解放します。
サンプルプログラム:malloc / free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> #include <stdlib.h> int main( int argc, char** argv ) { char *p = ( char* )malloc( 100 ); char *ptop = p; // 先頭アドレスを保持 int i; for( i = 0; i < 100; i++ ){ *p = i; p++; } p = ptop;//先頭アドレスを復元(巻き戻し) for( i = 0; i < 100; i++ ){ printf("*p=%d\n", *p ); p++; } free( ptop ); // 先頭アドレスを解放 ptop = NULL; p = NULL; return 0; } |
2行目:<stdlib.h>をインクルードしました。
mallocとfreeを使えるようになります。
6行名:mallocでヒープメモリを確保できます。
char型のメモリを100個(100バイト)確保したいので100を指定しました。
※int型のメモリを100個確保すると400バイトになるのでご注意ください。
7行目:mallocで獲得した先頭アドレスを後で解放するときに使いたいので
ptopにpをコピーしておきます。
18行目:freeに先頭アドレス(ptop)を渡すとメモリが解放されます。
※このタイミングのpは先頭アドレスの100バイト先を示しているのでご注意ください。
19、20行目:ポインタ変数にNULL(ヌル)を設定します。
使い終わったポインタはNULLで初期化する習慣を付けましょう。
アドレスが設定されていると解放(free)したかしていないか
判別つかなくなることがあります。
これを実行すると、こうなります。
$ gcc -g -o test test.c
$./test
*p=0
*p=1
*p=2
(省略)
*p=99
⑫キャスト
変数動詞の演算や、関数の引数に渡すときに
型が異なるとエラーになることがあります。
それを回避するためにキャスト(型変換)することがありますので、
覚えておきましょう。
サンプルプログラム:キャスト(型変換)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> void AAA( int aaa ) { printf("aaa=%d\n", aaa ); } void FFF( float fff ) { printf("%fff=%f\n", fff ); } int main( int argc, char** argv ) { float f = 1.2; int a = 1; AAA( ( int )f ); FFF( ( float )a ); printf( "f-a=%f\n", f - ( float )a ); printf( "f-a=%d\n", ( int )f - a ); return 0; } |
解説します。
14行目:float型の変数fを定義しました。
15行目:int型の変数aを定義しました。
17行目:int型の変数を引数にするAAAにfloat型の変数fを渡すために
( int )でint型にキャストしました。※小数点以下が切り捨てられます。
18行目:float型変数を引数にするFFFにint型の変数fを渡すために
( float )でfloat型にキャストしました。
19行目:f-aをfloat型で統一して計算すると0.200000になります。
20行目:f-aをint型で統一して計算すると0になります。
※小数点以下が切り捨てられます。
これを実行すると、こうなります。
$ gcc -g -o test test.c
$./test
aaa = 1
fff = 1.000000
f-a=0.200000
f-a=0
まとめ
後編に続きます。