C言語の文法の簡単なまとめ
ここではある程度のプログラミング知識のある人向けに、C言語の文法や基本的な事柄についての概要を簡単に説明します。
本編では解説していないものも一部あります。
Hello world
#include <stdio.h> //1
int main() //2
{
printf("Hello world."); //3
}
Hello world.
1、include文
#include
文は、指定したソースコードをコード上に展開します。
上記コードではstdio.h
というソースコード(ヘッダファイル)がこのinclude文が記述された個所に展開され、その機能が使用可能になります。
stdio.h
はC言語の標準入出力関数を提供するC言語標準ライブラリです。
2、main関数
main
関数は、C言語プログラムのエントリポイント(プログラムが開始される点)となる関数です。
C言語は、最初にmain関数が実行され、main関数が終了するとプログラムが終了します。
main関数の戻り値はint型で、戻り値はプログラムの終了コードとなります。
終了コード0は正常終了を意味します。
main関数のreturn文を省略した場合、またはreturn文の値指定を省略した場合は0が指定されたものとみなされます。
C99より以前ではmain関数のreturn文は省略できません。
3、printf関数
printf
関数は標準入出力に文字列を出力する関数です。
(C言語標準関数)
第一引数には出力する文字列を指定します。
printf関数の第一引数には書式指定文字列という特殊な文字列を指定することもできます。
書式指定文字列は変換指定子という特殊な文字を含む文字列で、変換指定子は第二引数以降に指定した値を文字列中に取り込みます。
以下は第二引数の文字列を変換指定子%s
で表示しています。
#include <stdio.h>
int main()
{
//「%s」は文字列を表示する
printf("Hello %s.", "John");
}
Hello John.
セミコロン
C言語では行末にセミコロン(;
)を使用します。
正確に言えば、式を文として扱うための区切りとして使用します。
処理の区切りごとにセミコロンを使用すると考えると良いでしょう。
セミコロンがない場合、改行していても処理の区切りとはみなされないので、文法的に正しくない場合はエラーになります。
セミコロンは式文を作るための文法なので、もともと文であるif文などは末尾にセミコロンは不要です。
#include <stdio.h>
int main()
{
printf("Hello world.")
printf("Hello world.");
//上記は以下と同じ意味
//文法的に正しくないのでエラー
printf("Hello world.")printf("Hello world.");
//セミコロンで区切れば一つの行に複数の処理を記述することはできる
printf("Hello world.");printf("Hello world.");
//改行は処理の区切りではないので
//以下のような記述も可能
printf(
"Hello world."
);
}
//関数定義(この例ではmain関数)などは文なので末尾にセミコロンは不要
コメント
/*
と*/
囲われた範囲はコメントとして扱われます。
//
が記述された以降の行は、行末までがコメントとして扱われます。(C99)
ただし文字列リテラル内では文字として扱われるのでコメント化はされません。
コメントはコンパイル時に除外されます。
//コメント行
/*
範囲コメント
範囲コメント
範囲コメント
*/
printf("Hello world."); //行の途中からでもコメント化可能
データ型
以下はC言語がサポートするデータ型です。
- char
- 文字型
1バイト整数型 - short
- 2バイト整数型
- int
- 4バイト整数型
(環境によって2、4、8バイト) - long
- 4バイト整数型
(環境によっては8バイト) - long long
- 8バイト整数型
(C99) - float
- 4バイト小数型
(単精度浮動小数点数) - double
- 8バイト小数型
(倍精度浮動小数点数) - long double
- 8~16バイト小数型
(倍制度以上~四倍精度以下の浮動小数点数、環境依存) - void
- 空のデータ型
(値がないことを表す)
データ型の先頭に以下のキーワードをつけることで符号のあり/なしを指定できます。
(省略した場合は通常は符号あり型です)
- signed
- 符号あり型
- unsigned
- 符号なし型
char a;
int b;
long long c;
//符号あり1バイト整数
//-128~127
signed char d;
//符号なし1バイト整数
//0~255
unsigned char e;
signed f; //signed int型
unsigned g; //unsigned int型
符号の指定を省略した場合は符号あり型(signed
)になりますが、char型だけは特殊な扱いとなっていて、符号の指定を省略した場合にどちらになるかは処理系に依存します。
C言語では「char型」は「文字型」を意味し、数値を扱うための型ではありません。
そのままでも整数型として使用はできるのですが、符号あり/なしのどちらであるかが明確ではないので、1バイト整数型として使用する場合は符号を明示すべきです。
なお、上掲したデータ型の各サイズは一般的な32ビット環境でのサイズです。
C言語にはデータ型の具体的なサイズについての規定はなく、「char型は1バイト」「short型はchar型以上」「int型はshort型以上」といった各データ型のサイズの前後関係が規定されています。
(「以上」なので同じサイズということもあり得る)
64ビット環境では、Windowsではint型、long型は共に4バイト整数です。
多くのUNIX系OS(macOS、多くのLinux系OSなど)ではint型は4バイト整数、long型は8バイト整数です。
真偽値
C99から_Bool
型が追加されています。
これは真か偽かの二種類の値を取る型で、if文などで使用されます。
同じくC99から標準化された<stdbool.h>
ヘッダにbool
、true
、false
というマクロ(定数)が定義されています。
bool
は_Bool
の別名、true
は1
(真)、false
は0
(偽)と定義されています。
#include <stdio.h>
#include <stdbool.h>
int main()
{
_Bool a = true;
bool b = false;
if (a) {
//実行される
}
if (b) {
//実行されない
}
}
C23からはbool
、true
、false
はキーワードとなったため、<stdbool.h>
ヘッダは不要です。
オブジェクトのサイズの取得
変数などのオブジェクトのサイズ(バイト数)はsizeof
演算子で取得できます。
この演算子はsize_t
型(符号なし整数型)を返します。
int a = 0;
size_t aSize = sizeof(a);
//「%zu」はsize_t型を表示する
//「\n」は文字列を改行する
printf("%zu\n", aSize);
//データ型名を直接指定しても良い
printf("%zu\n", sizeof(long));
printf("%zu\n", sizeof(long long));
4 4 8
size_t型は、システムで使用可能なサイズの最大数が格納可能な符号なし整数型です。
実際に使用される型はC言語の規格では決められていないので、それ以外の型で受け取ると値が変化する可能性があります。
size_t型を返す関数やsizeof演算子の値はunsigned int型などではなくsize_t型で受け取るべきです。
(size_t型は<stdio.h>などに定義されています)
変数
変数は「データ型 変数名」の形式で宣言します。
宣言された変数には代入演算子(=
)で値を代入できます。
宣言と同時に代入演算子を使用すると、変数を初期化できます。
//変数aを宣言
char a;
//変数aに1を代入
a = 1;
//変数bの宣言と初期化
char b = 2;
//二つ同時に宣言
int c, d;
//二つ同時に宣言と初期化
int e = 3, d = 4;
宣言のみで初期化も代入もしていない変数の値は不定です。
不定値を使用することは未定義動作です。
(どのような動作となってもC言語の規約違反ではない。不具合の原因となる可能性がある)
C99よりも以前では、関数内の変数はすべて関数の先頭で宣言する必要があります。
スコープと寿命
変数のスコープ(アクセス可能な範囲)は、変数を宣言した位置から宣言した行を囲うブロックの終了までです。
変数の寿命も原則として同じですが、静的変数の寿命は異なります。
void f()
{
//宣言より手前で変数は使用できない
//a = 0;
int a = 1;
{
int b = 2;
}//変数bの寿命はここまで
//ブロックの外で変数は使用できない
//b = 3;
}//変数aの寿命はここまで
関数の引数、およびfor文の初期化式で宣言された変数のスコープと寿命はその関数ブロック、for文の終了までです。
void f(int a)
{
for (int b = 0; b < 10; ++b)
{
}//変数bの寿命はここまで
}//引数aの寿命は関数の終了まで
グローバル変数
関数内で宣言された変数はローカル変数となり、その関数内からのみアクセスが可能です。
(ローカル変数のアクセス可能範囲の詳細はスコープと寿命を参照)
関数の外で宣言された変数はグローバル変数となり、プログラム全体からアクセスが可能です。
int g_number = 100; //グローバル変数
int main()
{
int a = 0; //ローカル変数
printf("a = %d", a);
//printf("b = %d", b); アクセス不可
printf("g_number = %d", g_number);
}
void f()
{
int b = 0; //ローカル変数
//printf("a = %d", a); //アクセス不可
printf("b = %d", b);
printf("g_number = %d", g_number);
}
ローカル変数を初期化しない場合の値は不定ですが、グローバル変数を初期化しない場合はプログラム実行時に自動的に0で初期化されます。
C言語ではグローバル変数を初期化する場合はコンパイル時定数である必要があります。
静的変数
変数の宣言時にstatic
キーワードを使用すると静的変数となります。
静的変数はプログラム開始時に値が初期化され、プログラムの終了まで値が保持されます。
値を初期化しない場合は0で初期化されます。
void f()
{
//静的変数
//この初期化処理はプログラム実行時に一度だけ実行される
//(関数呼び出し毎に実行されない)
static int sNum = 10;
printf("%d\n", sNum);
++sNum;
}
int main()
{
f();
f();
f();
//変数のスコープは変わらないので
//他の関数からはアクセスできない
//sNum = 0; //ダメ
}
10 11 12
C言語では静的変数の値はコンパイル時定数で初期化する必要があります。
(C++では変数等で初期化が可能)
その他、staticキーワードはグローバル変数やグローバル関数のスコープをソースコード内に限定する機能もあります。
const型修飾子(定数)
変数の宣言時にconst
キーワードを使用すると、その値は定数となります。
(あるデータ型にさらに意味を加えるキーワードを型修飾子と言います)
const定数の値を書き換えようとするとコンパイルエラーになります。
int a = 1;
const int b = a;
a = 2;
b = 3; //コンパイルエラー
//配列の要素数指定には実行時定数が必要
//const定数は指定できないので
//コンパイルエラー
int arr[b];
実行時定数とコンパイル時定数
const定数は実行時定数といい、値が変更できない以外は変数と扱いは同じです。
ローカルconst定数ならば、変数の宣言行でメモリが確保され、その宣言ブロックが終了するとメモリが解放されます。
(コンパイラの最適化によって変わる可能性はある)
グローバルconst定数ならば、プログラム開始時にメモリが確保され、プログラム終了時にメモリが解放されます。
(スコープと寿命)
コンパイル時定数はコンパイルの時に値が決定され、生の値としてプログラムに組み込まれます。
プログラム実行時には計算済みの値を読み込むだけなので実行時定数よりも高速です。
また、全ての変数の初期化の前にすでに値が決定しているので、実行時定数の初期化や配列の要素数の決定に使用することができます。
コンパイル時定数が必要な場合はリテラルを直接書くか、#defineや列挙型を使用します。
volatile型修飾子(揮発変数)
変数の宣言時にvolatile
キーワードを使用すると、その変数にアクセスするコードはコンパイラによる最適化が抑制され、コードが意図しない形で変更されるのを防ぐことができます。
例えばハードウェアの制御では特定のメモリ領域に対して特定の値の書き込みを行うことで、その値によって別々の機能を実行することがあります。
(メモリマップドI/O)
//0x4000(アドレスの番地)をポインタ型に変換し、
//その値にアクセスすることを示す定数
#define REGISTER (*(unsigned char *)0x4000)
void setRegister()
{
//1、2行目はコンパイラが
//最適化のために削除するかもしれない
REGISTER = 0x01;
REGISTER = 0x02;
REGISTER = 0x04;
}
コード上では三回それぞれ別の値が書き込まれていますが、コンパイラは「0x01と0x02のメモリ書き込みは、他から読み取られる前に0x04で上書きされているので無意味」と判断し、コードを削除する最適化を行うことがあります。
しかし全ての値のセットに意味がある場合、これは意図しない挙動となります。
このような場合にvolatile
を使用するとコンパイラは最適化を抑制し、コードを削除しなくなります。
#define REGISTER (*(volatile unsigned char *)0x4000)
void setRegister()
{
//以下の処理は全て実行される
REGISTER = 0x01;
REGISTER = 0x02;
REGISTER = 0x04;
}
配列
配列は変数の宣言時に、変数名に角括弧[]
を指定して作成します。
この角括弧は添字演算子(そえじえんざんし)と言います。
添字演算子の中には要素数を数値で指定します。
要素数はコンパイル時定数で指定する必要があります。
(可変長配列の場合はこの限りではない)
各要素へのアクセスは配列変数に添字演算子を使用して、要素番号を指定します。
配列の先頭のインデックス(添え字、要素番号)は0です。
int a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
//a[3] = 4; //これは範囲外アクセス
//要素数の指定に変数は使用できないため
//コンパイルエラー
int n = 3;
int b[n];
なお、配列の範囲外アクセスはコンパイルエラーにも実行時エラーにはなりません。
しかし管理していないメモリ位置への読み書きとなるため、予期しない動作となったりデータが破壊される可能性があります。
(その結果として実行時にプログラムが落ちることはあります)
→配列
配列の初期化
配列は波括弧を使用して宣言と同時に初期化が可能です。
要素数の指定は省略可能で、その場合は初期化リストの要素数の配列が作成されます。
要素数を指定し、初期化リストの要素数が少ない場合は残りの要素は0で初期化されます。
(要素数の指定よりも初期化リストの要素のほうが多い場合はコンパイルエラー)
//要素数3の配列
int a[] = { 1, 2, 3 };
printf("%d, %d, %d\n",
a[0], a[1], a[2]);
//要素数3の配列
int b[3] = { 4, 5 };
printf("%d, %d, %d\n",
b[0], b[1], b[2]);
//配列要素を
//全て0で初期化する
int c[100] = { 0 };
//これは全てを1で初期化するのではなく
//先頭要素だけが1で初期化される
//残りは全て0で初期化される
int d[100] = { 1 };
1, 2, 3 4, 5, 0
その他、文字列リテラルを配列の初期化値として渡すこともできます。
文字と文字列
配列の初期化リストは配列変数の初期化時のみ使用可能です。
すでに作成された配列に対して初期化リストを代入することはできません。
配列サイズと要素数
配列のサイズはsizeof演算子で取得可能です。
これは「配列全体のバイト数」なので、配列の要素数は「配列全体のバイト数 / 配列の要素のバイト数」で取得できます。
int a[] = { 1, 2, 3, 4, 5 };
size_t aSize = sizeof(a);
size_t elementSize = sizeof(a[0]);
size_t aLength = aSize / elementSize;
printf("配列a全体のサイズ: %zu\n", aSize);
printf("配列aの要素サイズ: %zu\n", elementSize);
printf("配列aの要素数: %zu\n", aLength);
配列a全体のサイズ: 20 配列aの要素サイズ: 4 配列aの要素数: 5
ただし配列を関数の引数に指定した場合、配列はポインタ型に変換されます。
関数側から配列のサイズを知る手段はないため、配列サイズ(または要素数)も引数で同時に渡す必要があります。
#include <stdio.h>
void f(int array[], size_t count)
{
for (int n = 0; n < count; ++n)
printf("%d ", array[n]);
//関数側で引数の配列にsizeofを使用しても
//正しい配列サイズは得られない
size_t s = sizeof(array);
}
int main()
{
int a[] = { 1, 2, 3 };
f(a, sizeof(a) / sizeof(a[0]));
}
可変長配列
C99で配列の要素数の指定に変数が使用可能な可変長配列が標準化されました。
しかしC11で実装はオプション扱いとなり、コンパイラによってサポートしない場合があります。
(gccはサポート、Visual C++は非サポート)
void f(int n)
{
//可変長配列
//動的に配列のサイズを決定できる
int arr[n];
}
可変長配列はローカル変数でのみ使用できます。
データは全てスタック領域に保存されるため、あまり大きなサイズの配列を作るとスタックオーバーフローが発生します。
動的配列を使用する場合はmalloc
関数などのメモリ確保関数を使用するほうが安全です。
(メモリの動的確保)
型変換
暗黙的な型変換
異なるデータ型同士の計算や変数への代入などが行われると、暗黙的な型変換が行われます。
大まかにいえば、整数型同士、小数型同士ではサイズが大きいほうのデータ型に変換して演算が行われます。
整数型と小数型の演算では小数型に変換されます。
char a = 1;
short b = a; //short型に変換
//aをshrot型に変換して演算
//その結果をint型に変換して代入
int c = a + b;
変換のルールの詳細は以下のページを参照してください。
→型変換
キャスト(明示的な変換)
データ型を明示的に指定して型変換を行うにはキャスト構文を使用します。
char a = 1;
int b = (int)a; //明示的にint型に変換
演算時に行われる暗黙的な型変換で意図しない値になってしまう場合は、明示的にデータ型を指定して変換した上で演算します。
//整数同士の割り算の結果は整数
//整数の計算結果をさらに小数に変換
//(結果として小数点以下切り捨て)
double a = 10 / 3;
//整数と小数の演算結果は小数
double b = 10 / (double)3;
printf("%f\n", a);
printf("%f\n", b);
3.000000 3.333333
typedef
データ型はtypedef
キーワードで別名を与えることができます。
//unsigned int型に「uint」という別名を与える
typedef unsigned int uint;
int main()
{
//unsigned int型
uint n = 1;
}
typedef
は構造体、共用体、列挙型に別名をつけるときによく使用されます。
以下のように構造体全体に対する別名を付けることで、構造体変数の宣言時にstruct
キーワードが不要になります。
struct Point1
{
int x;
int y;
};
typedef struct
{
int x;
int y;
} Point2;
int main()
{
//変数宣言時にも「struct」キーワードが必要
struct Point1 p1;
//typedefを利用すると「struct」キーワードを不要にできる
Point2 p2;
}
typeof/typeof_unqual
typeof
演算子は、型名や式からその型を取得します。
この機能はgccの独自拡張で、C23から標準化されています。
typeof(int) a; //int型
typeof(3.14) b; //double型
typeof(a) c; //int型
typeof(1 + 2.3) d; //double型
const int e = 1; //const int型
typeof(e) f = 2; //const int型
typeof_unqual
演算子はconst
やvolatile
などの型修飾を除去した型を取得します。
C23から標準化されています。
typeof_unqual(int) a; //int型
typeof_unqual(3.14) b; //double型
typeof_unqual(a) c; //int型
typeof_unqual(1 + 2.3) d; //double型
const int e = 1; //const int型
typeof_unqual(e) f = 2; //int型
この機能はマクロ関数で複数の型に対応するのに便利です。
#include <stdio.h>
#define swap(a, b) do { \
typeof(a) tmp = a; \
a = b; \
b = tmp; \
} while(0)
int main()
{
int a = 1, b = 2;
swap(a, b);
printf("%d, %d\n", a, b);
double c = 3.4, d = 5.6;
swap(c, d);
printf("%g, %g\n", c, d);
}
2, 1 5.6, 3.4
文字と文字列
シングルクォーテーションで囲われた一文字は文字を表し、char型変数に格納できます。
ダブルクォーテーションで囲われた文字は文字列を表し、char型の配列に格納できます。
ダブルクォーテーションで囲われた文字は一文字だけでも文字列として扱われます。
char型は1バイトサイズなので、日本語などの全角文字(マルチバイト文字)は格納できません。
日本語は一文字でも文字列として扱い、char型の配列に格納します。
文字列リテラルはchar型配列の初期化子(初期化に使用する値)に指定できます。
この配列の要素は文字列の先頭から一文字ずつ値をコピーしたものになります。
C言語の文字列は、文字列の終端を表すためにNULL文字という特殊文字が末尾に自動的に付加されます。
(この文字自体はprintf
関数などでは表示されません)
そのため、文字列のサイズは「文字列を格納するのに必要なサイズ+1」となります。
→文字型と文字列
文字列操作関数に関してはC言語の解説の本編を参照してください。
char a = 'a'; //1文字
char b[] = "a"; //1文字の文字列
char c[] = "abc"; //3文字の文字列
//文字は%c、文字列は%sで表示する
printf("%c\n", a);
printf("%s\n", b);
printf("%s\n", c);
printf("\n");
//文字列を一文字ずつバラして表示
//最後のNULL文字は文字として表示されない
printf("%c, %c, %c, %c\n",
c[0], c[1], c[2], c[3]);
printf("\n");
printf("%c\tsize=%zu\n", a, sizeof(a));
printf("%s\tsize=%zu\n", b, sizeof(b));
printf("%s\tsize=%zu\n", c, sizeof(c));
a a abc a, b, c, a size=1 a size=2 abc size=4
NULL終端文字列
C言語の(文字列を扱う)関数は、NULL文字を文字列の終端として処理を行います。
終端にNULL文字のない文字列は文字列として扱うことはできません。
char a[] = "abc";
//配列aは以下の配列bと全く同じ
//'\0'はNULL文字を表す特殊文字
char b[] = { 'a', 'b', 'c', '\0' };
//これはNULL文字が存在しないので
//文字列として使用できない
char c[] = { 'a', 'b', 'c' };
printf("%s\n", a);
printf("%s\n", b);
//配列cは終端にNULL文字がないので
//文字列操作関数に使用するとバッファオーバーランが発生する
printf("%s\n", c);
abc abc abcフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフ
上記コードはサンプルとしてNULL終端でない文字の配列をprintf関数に渡しています。
メモリ上の「abc」の次の位置にNULL文字が存在しないため、配列が管理するメモリ領域を超えてデータを読み取ってしまっています。
(メモリ上に「0」が現れるまでデータを読み取り続ける)
これをバッファオーバーラン(バッファオーバーフロー)といい、意図しない挙動や脆弱性の原因となります。
エスケープシーケンス
改行などの特殊な文字を文字列中に表すにはエスケープシーケンス(エスケープ文字)を使用します。
エスケープシーケンスは\
記号と特定の文字からなる特殊文字です。
(環境によって円記号またはバックスラッシュとして表示されます)
見た目は二文字になりますが、併せて一文字として扱われます。
- \a
- 警告音
- \b
- バックスペース
- \n
- 改行
(ラインフィード) - \r
- 改行
(キャリッジリターン) - \t
- タブ文字
(水平タブ) - \v
- 垂直タブ文字
- \?
- ?記号の表示
- \'
- シングルクォーテーション
- \"
- ダブルクォーテーション
- \0
- NULL文字
リテラル
C言語がサポートするリテラル、およびその記法を以下に示します。
//数値リテラル
int a = 10; //int型
int b = 012; //int型(8進数表記)
int c = 0x0A; //int型(16進数表記)(C89)
int d = 0b1010; //int型(2進数表記)(C23)
double e = 3.14; //double型
double f = .123; //double型
double g = 1e3; //double型(指数表記)
double h = 0x1.8p+1; //double型(16進数表記)(C99)
//文字リテラル
char i = 'A';
wchar_t j = L'A'; //ワイド文字
char8_t k = u8'A'; //UTF-8文字(C23)
char16_t l = u'A'; //UTF-16文字(C11、<uchar.h>)
char32_t m = U'A'; //UTF-32文字(C11、<uchar.h>)
//文字列リテラル
char n[] = "ABC";
wchar_t o[] = L"ABC"; //ワイド文字
char p[] = u8"ABC"; //UTF-8文字(C11)
char16_t q[] = u"ABC"; //UTF-16文字(C11、<uchar.h>)
char32_t r[] = U"ABC"; //UTF-32文字(C11、<uchar.h>)
//数値リテラルのサフィックス(C89)
unsigned int s = 10u; //unsigned int型
unsigned long t = 10ul; //unsigned long型
unsigned long long u = 10ull; //unsigned long型
long v = 10L; //long型
long long w = 10LL; //long long型(C99)
float x = 3.14f; //float型
文字リテラル、文字列リテラルの接頭辞の「L」「u8」「u」「U」は、大文字小文字が区別されます。
それ以外のアルファベットは大文字小文字は区別されません。
文字列リテラルの連結
連続する文字列リテラルは連結してコンパイルされます。
//全て同じ
char a[] = "Hello world.";
char b[] = "Hello"" world.";
char c[] = "Hello" " world.";
char d[] = "Hello" " world.";
char e[] = "Hello" " " "world.";
char f[] = "Hello"
" world.";
文字列リテラルは不変
ポインタなどによって文字列リテラルに直接アクセスすることはできますが、書き換えることは未定義動作です。
char型配列の初期化時に文字列リテラルを使用した場合、各要素に保存されるのは文字列リテラルを先頭から一文字ずつコピーしたものです。
そのためchar型配列の値を書き換えることは問題ありません。
char* str1 = "ABC";
char str2[] = "ABC";
str1[0] = 'Z'; //未定義動作
str2[0] = 'Z'; //OK
算術演算子
数値型の値は+
、-
、*
、/
の算術演算子で四則計算が可能です。
()
で計算の順序を変更できます。
剰余(割り算のあまり)は%
演算子(剰余演算子)で求められます。
int a = 1 + 2 / 3 * 4; //1
int b = (5 - 6) * (7 + 8); //-15
int c = 10 % 3; //1
int d = 10 % 2; //0
ある変数の演算結果を同じ変数に代入する場合は複合代入演算子が使用できます。
int a = 10;
//a = a + 2と同じ
a += 2;
a -= 2;
a /= 2;
a *= 2;
a %= 2;
なお、数値は単項-
演算子で正負を逆転できます。
単項+
演算子もありますがほぼ使われません。
int a = 1;
int b = -a; //-1
インクリメント/デクリメント
インクリメント演算子は変数の値を1増やします。
デクリメント演算子は変数の値を1減らします。
前置は変数の値を増やした後の値が評価値となります。
後置は変数の値を増やす前の値が評価値となります。
int a = 1;
int b = ++a; //前置インクリメント
int c = a++; //後置インクリメント
int d = --a; //前置デクリメント
int e = a--; //後置デクリメント
printf("%d\n", b);
printf("%d\n", c);
printf("%d\n", d);
printf("%d\n", e);
2 2 2 2
ビット演算
ビット演算は、値を0と1のビットの並びとみなして演算を行います。
演算される値は整数値である必要があります。
ビット演算にはビット演算子を使用します。
論理演算
- a & b
- ビット単位のAND演算子
aとbの両方のビットが1なら1を返す - a | b
- ビット単位のOR演算子
aとbの少なくともどちらか一方が1なら1を返す - a ^ b
- ビット単位のXOR演算子
aとbのビットが異なるなら1を返す - ~a
- NOT演算子
aのビットを反転して返す
- a &= b
- a = a & bと同等
- a |= b
- a = a | bと同等
- a ^= b
- a = a ^ bと同等
#include <stdio.h>
//char型のビット並びを表示
void printCharBit(unsigned char c, const char* expr, const char* explanation)
{
printf("%s \t%3d: ", expr, c);
unsigned char bit = (1 << 7);
while (bit) {
if (c & bit) putchar('1');
else putchar('0');
bit >>= 1;
}
if (explanation)
printf(" (%s)", explanation);
putchar('\n');
}
int main()
{
unsigned char a = 5;
unsigned char b = 12;
printCharBit(a, "a", NULL);
printCharBit(b, "b", NULL);
printf("\n");
unsigned char c = a & b;
unsigned char d = a | b;
unsigned char e = a ^ b;
unsigned char f = ~a;
printCharBit(c, "a & b", "両方が1なら1");
printCharBit(d, "a | b", "少なくとも片方が1なら1");
printCharBit(e, "a ^ b", "両者が異なるなら1");
printCharBit(f, "~a", "ビット反転");
}
a 5: 00000101 b 12: 00001100 a & b 4: 00000100 (両方が1なら1) a | b 13: 00001101 (少なくとも片方が1なら1) a ^ b 9: 00001001 (両者が異なるなら1) ~a 250: 11111010 (ビット反転)
シフト演算
- a << b
- aをb桁、左にシフトする
- a >> b
- aをb桁、右にシフトする
#include <stdio.h>
//char型のビット並びを表示
void printCharBit(unsigned char c, const char* expr, const char* explanation)
{
printf("%s \t%3d: ", expr, c);
unsigned char bit = (1 << 7);
while (bit) {
if (c & bit) putchar('1');
else putchar('0');
bit >>= 1;
}
if (explanation)
printf(" (%s)", explanation);
putchar('\n');
}
int main()
{
unsigned char a = 20;
printCharBit(a, "20\t", NULL);
printf("\n");
printCharBit(20 << 2, "20 << 2", NULL);
printCharBit(20 >> 2, "20 >> 2", NULL);
}
20 20: 00010100 20 << 2 80: 01010000 20 >> 2 5: 00000101
シフトする桁数の指定(演算子の右辺)は正の数を指定します。
負数の指定は未定義動作です。
C言語では、符号なし型のシフト演算は論理シフト(空いた桁は0で埋める)です。
それ以外のシフトは処理系依存です。
特に負数に対するシフト演算は環境により正負が逆転するなど期待通りの値にならないことがあります。
制御構文
if-else if-else文
ある条件に従って処理を振り分けるにはif-else if-else文を使用します。
→if文
以下のコードは、変数aの値が0よりも大きい場合にprintf
関数が実行されます。
int a = 1;
if (a > 0) {
printf("'a' is greater than 0.");
}
条件を満たさなかった場合に処理を実行する場合はif文に続いてelse文を記述します。
int a = 1;
if (a > 0) {
printf("'a' is greater than 0.");
}
else {
printf("'a' is not greater than 0.");
}
条件を複数設定したい場合はelse if文を使用します。
else if文は必要なだけ連続して使用でき、最初に条件に合致したif文(else if文)の内容が実行されます。
最後のelse文は省略可能です。
int a = 1;
if (a < 0) {
printf("'a' is less than 0.");
}
else if (a > 0) {
//この処理が実行される
printf("'a' is greater than 0.");
}
else {
printf("'a' equals 0.");
}
if文などに使用するブロック記号{}
は複数の文をひとまとめにするものです。
処理の本体が一行だけの文で済む場合はブロック記号は省略可能です。
int a = 1;
if (a > 0)
printf("'a' is greater than 0.");
else
printf("'a' is not greater than 0.");
関係演算子
条件式では関係演算子や論理演算子を使用して条件を指定できます。
関係演算子
- a < b
- aはbより小さい
- a > b
- aはbより大きい
- a <= b
- aはbより小さいか等しい
- a >= b
- aはbより大きいか等しい
- a == b
- aとbは等しい
- a != b
- aとbは等しくない
論理演算子
- a && b
- aとbが真の場合に真を返す
- a || b
- aとbの少なくとも一方が真なら真を返す
- !a
- aの真偽値を反転する
数値と真偽判定
数値を条件式に指定した場合、0は偽、0以外の値は真と判定されます。
if (0)
{
//実行されない
}
if (1)
{
//実行される
}
if (0.1)
{
//実行される
}
switch文
switch文は、ある式の評価値に一致するcase句を実行します。
case句にはコンパイル時定数を指定する必要があります。
一致するcase句が存在しない場合はdefault句が実行されます。
default句は省略可能です。
int a = 1;
switch (a + 1)
{
case 0:
printf("'a' + 1 is 0.");
break;
case 1:
printf("'a' + 1 is 1.");
break;
case 2: //この処理が実行される
printf("'a' + 1 is 2.");
break;
default:
printf("'a' + 1 is unknown value.");
}
case句の末尾はコロン(セミコロンではない)なので注意してください。
switch文中ではbreak文で処理を終了できます。
case句の終わりにbreak文を記述しない場合、次のcase句(またはdefault句)の処理が実行されます。
条件演算子
条件判定には条件演算子を使用することもできます。
条件演算子は?
記号と:
記号を組み合わせて使用します。
演算に三つの式を使用するので三項演算子とも呼ばれます。
(C言語で三項を使用するのは条件演算子だけです)
これはある条件式が真の場合は式Aを評価し、偽の場合は式Bを評価し、その評価値を条件式全体の評価値とします。
int a = 10;
int b = 7;
//a > bが真なら式aを実行
//偽なら式bを実行
//その実行結果が条件式全体の評価値となるので
//変数maxにはaとbの大きいほうの値が代入される
int max = a > b ? a : b;
printf("comparing %d and %d, %d is bigger.", a, b, max);
comparing 10 and 7, 10 is bigger.
for文
for文はある条件が真(true)を返す間処理を繰り返します。
→for文
for (int n = 0; n < 5; ++n) {
printf("n is %d\n", n);
}
n is 0 n is 1 n is 2 n is 3 n is 4
for文はループのために3つの式を使用します。
for (初期化式; 条件式; 反復式) {
ループ処理
}
- 最初に初期化式が実行されます。
- 次に条件式が実行されます。
- 条件式が真である場合、ループ処理が一回実行されます。
偽の場合はそのままfor文を終了します。 - ループ処理の実行後、反復式が実行されます。
- 再度条件式が実行され、条件式が偽になるまで上記処理が繰り返されます。
初期化式内で変数を宣言した場合、変数のスコープはループ内となります。
C99よりも以前では初期化式内で変数宣言はできません。
3つの式はそれぞれ省略可能です。
セミコロンは式の区切りとなるので省略できません。
//式を3つとも省略
for (;;) {
//条件式で終了条件を指定しないと
//無限ループになるので
//break文などでループを終了する必要がある
break;
}
break文、continue文
break文は現在のループ文またはswitch文を終了します。
continue文は処理をループ文の先頭に戻します。
for文の場合は反復式が実行され、条件式が実行されます。
for (int n = 0; n < 10; ++n) {
if (n < 2) //2未満ならループ先頭に戻る
continue;
if (n > 5) //5より大きければループ分終了
break;
printf("%d ", n);
}
2 3 4 5
while文
while文は、ある条件が真の間処理を繰り返します。
while文はfor文から初期化式と反復式を省略したものといえます。
int n = 0;
while (n < 5) {
printf("%d ", n);
++n;
}
0 1 2 3 4
do while文
do while文は、ある条件が真の間処理を繰り返します。
while文はループ処理の前に条件式を評価しますが、do while文はループ処理の後に条件式を評価します。
そのため条件式が偽となる場合でも必ず一度はループ処理が実行されます。
int n = 10;
do {
printf("%d ", n);
++n;
} while (n < 0);
10
goto文
goto文は、指定のラベルに処理を移動します。
ラベルはラベル名:
の形式で任意の位置に指定します。
ラベルへの移動は同じ関数内からのみ可能です。
int main()
{
int a = 0;
while (1) //無限ループ
{
if (a == 3)
goto endloop;
printf("%d ", a);
++a;
}
endloop:
printf("\n%d ", a);
}
0 1 2 3
関数
関数は複数の処理をひとまとめにして任意の位置から呼び出せるようにする機能です。
#include <stdio.h>
//関数の定義
int add(int a, int b)
{
return a + b;
}
int main()
{
//関数の呼び出し
int r = add(1, 2);
printf("%d", r);
}
3
関数定義は以下の形式で行います。
戻り値の型 関数名(引数の型 引数名, ...)
{
//何らかの処理
//...
return 戻り値;
}
引数は関数の呼び出し側から関数に対して渡すデータです。
引数は必要なだけ増やすことができます。
引数を取らないことを明確にする場合は引数リストを空にするか、void
と記述することができます。
//引数を取らない関数
int f(void) { return 0; }
return文が実行されると関数は終了し、return文で指定した値が関数の戻り値となります。
関数の戻り値にvoid型を指定した場合、return文は省略できます。
省略しない場合、return文の後に値を指定してはいけません。
戻り値の型がvoid型以外でreturn文を省略しても言語使用上はコンパイルが通ります。
しかしその値を使用することは未定義動作です。
(コンパイラによっては警告を発します)
関数の定義側に記述される引数を仮引数と言います。
関数の呼び出し側で指定される値を実引数と言います。
//引数aとbは仮引数
void f(int a, int b) {}
int main()
{
int x = 2;
//1と変数xは実引数
f(1, x);
}
プロトタイプ宣言
C言語のコンパイルはコードを上から順に読み込んでいくため、その時点でまだ定義されていない識別子(関数名や変数名)が使用されるとコンパイルエラーとなります。
これを防ぐために、ソースコードの先頭で関数のプロトタイプ宣言を行います。
プロトタイプ宣言は関数の戻り値と関数名、引数の型と数を指定します。
プロトタイプ宣言以降の行では、プログラム中のどこかにその名前の関数が存在することをコンパイルに伝えることができます。
#include <stdio.h>
//関数のプロトタイプ宣言
int add(int, int);
int main()
{
int r = add(1, 2);
printf("%d", r);
}
int add(int a, int b)
{
return a + b;
}
コマンドライン引数
main関数は引数なし(void)のほか、以下の形式の引数を指定することができます。
int main(int argc, char *argv[])
{}
int main(int argc, char **argv)
{}
//どちらでも意味は同じ
引数名は任意のものに変更可能です。
main関数の引数には、プログラムの開始時にシステム側から渡される値が格納されます。
例えば「test.exe」という実行ファイルに対して、以下のような形式でターミナル(コンソール、プロンプト)からプログラムを起動することでmain関数の引数に値を渡すことができます。
これをコマンドライン引数と言います。
(行頭の$はターミナルのコマンド入力であることを示します。実際には入力しません)
$ ./test.exe -option "abc"
//Windowsのコマンドプロンプトの場合は先頭の「./」は付けない
$ test.exe -option "abc"
第一引数は、プログラムに渡された引数の個数が格納されます。
つまり第二引数の配列の要素数で、この値は常に0以上の値です。
第二引数はプログラムに渡される文字列へのポインタの配列です。
先頭の要素にはプログラムの実行ファイル名が格納されています。
最後の要素の次の要素はNULLであることが保証されています。
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("main関数の引数の個数: %d\n", argc);
printf("main関数の引数:\n");
for (int n = 0; n < argc; ++n)
{
printf("%d: %s\n", n, argv[n]);
}
}
main関数の引数の個数: 3 main関数の引数: 0: C:\test.exe 1: -option 2: abc
inline関数
関数にinline
キーワードをつけることで、コンパイラに関数のインライン化を要求することができます。
実際にインライン関数化されるかはコンパイラが判断します。
この機能はC99から標準化されています。
関数のインライン化とは、関数の呼び出しを実際の関数の実体に置き換えることで呼び出しにかかるコストを削減し、速度の向上を図るものです。
その代わりにプログラム全体のサイズが増加します。
大きなサイズの関数はinline
キーワードをつけてもインライン化されないことが多いです。
inline void f()
{
}
構造体
構造体は、任意の数の変数をまとめて扱うことができます。
構造体はstruct
キーワードで定義します。
構造体にまとめられる各変数はメンバ変数といいます。
構造体変数からメンバ変数へのアクセスは.
(ドット演算子)で行います。
→構造体
#include <stdio.h>
//「Person」という名前の構造体
struct Person
{
char* name; //名前
int age; //年齢
char gender; //性別
};
int main()
{
//構造体変数
//初期化しない場合は不定値
struct Person person1;
//宣言と同時に初期化
struct Person person2 = {
"John",
20,
0
};
//先頭から順にメンバの初期化値として使用される
//初期化子が足りないメンバは0で初期化される
//指定子付き初期化(C99以降)
//特定のメンバだけ初期化できる
//初期化しなかったメンバは0で初期化される
struct Person person3 = {
.name = "Jony"
};
//複合リテラルによる代入(C99以降)
//一括で代入できる
person1 = (struct Person){
"Jane",
22,
1
};
//複合リテラルの書式で
//特定のメンバだけの代入はできない
//person1 = (struct Person){
// .name = 24
//};
printf("name: %s\nage: %d\ngender %d\n",
person1.name, person1.age, person1.gender
);
printf("\n");
printf("name: %s\nage: %d\ngender %d\n",
person2.name, person2.age, person2.gender
);
printf("\n");
printf("name: %s\nage: %d\ngender %d\n",
person3.name, person3.age, person3.gender
);
}
name: Jane age: 22 gender 1 name: John age: 20 gender 0 name: Jony age: 0 gender 0
構造体変数の代入
構造体変数は、同じデータ型の構造体変数をそのまま代入可能です。
この場合は構造体のメンバ全てがコピーされます。
memcpy
関数等のメモリ操作関数で値を割り当てるという方法もありますが、これは推奨されません。
#include <stdio.h>
#include <string.h>
struct Point
{
int x;
int y;
};
struct Vector2
{
double x;
double y;
};
int main()
{
struct Point point1 = { 1, 2 };
//別の変数による初期化や代入は
//単純に代入演算子を使用する
struct Point point2 = point1;
//メモリ操作関数でコピーは可能だが推奨されない
memcpy(&point2, &point1, sizeof(struct Point));
//別の構造体変数をコピーしてしまうとデータが破壊される
struct Vector2 vector2 = { 1, 2 };
memcpy(&point2, &vector2, sizeof(struct Point));
//代入ならコンパイルエラーになるので安全
point2 = vector2; //コンパイルエラー
}
共用体
共用体は、同じメモリ領域を複数のメンバ変数が共有します。
全てのメンバ変数は同じアドレスにアクセスするため(読み書きするサイズは異なる)、あるメンバ変数に値を代入すると他のメンバ変数の値も書き変わります。
共用体のメンバで意味のある値となっているのは最後に値を代入したメンバのみで、それ以外のメンバは使用できません。
(意味のあるデータになっていない)
→共用体
#include <stdio.h>
//char型、int型、double型の
//いずれかを格納する共用体
union Variant
{
char c;
int n;
float f;
};
int main()
{
//共用体変数
//初期化しない場合は値は不定値
union Variant variant1;
//Variant型の宣言と初期化
//初期化は先頭のメンバに対してのみ有効
union Variant variant2 = { 1 };
//指定子付き初期化(C99以降)
union Variant variant3 = { .f = 1.23f };
//メンバ変数fに値を代入
variant1.f = 1.23;
printf("n: %d\n", variant1.n);
printf("c: %d\n", variant1.c);
printf("f: %g\n", variant1.f);
printf("\n");
//別のメンバ変数nに値を代入
variant1.n = 456;
printf("n: %d\n", variant1.n);
printf("c: %d\n", variant1.c);
printf("f: %g\n", variant1.f);
}
n: 1067282596 c: -92 f: 1.23 n: 456 c: -56 f: 6.38992e-43
列挙型
列挙型(列挙体)は複数の定数を一括して定義します。
定義される定数はコンパイル時定数で、中身は整数値です。
構造体などと同じユーザー定義型なので、列挙型を保存する変数を作ることができます。
→列挙型
#include <stdio.h>
//列挙型の定義
enum LCR
{
LEFT,
CENTER,
RIGHT
};
int main()
{
//列挙型の各列挙子の値を表示
printf("%d\n", LEFT);
printf("%d\n", CENTER);
printf("%d\n", RIGHT);
//列挙型の変数
enum LCR lcr = LEFT;
lcr = RIGHT;
//定義されている以外の数値も受け付けてしまうので注意
//(コンパイルエラーや実行時エラーにはならない)
lcr = 9;
}
0 1 2
列挙型は、0から開始する番号が先頭の列挙子(定義されている名前)に順に割り振られます。
この値は変更することができ、値を変更した次の列挙子はその値を1増やしたものが割り振られます。
enum MyEnum
{
A, //0
B, //1
C = 10, //10
D, //11
E //12
};
値を重複させることもできますが、列挙子の名前で状態を識別するという用途には使えなくなるので注意が必要です。
ポインタ
C言語はポインタという機能で、データのメモリ上の位置(アドレス)を指定して値を読み書きすることができます。
(pointer=ポイントするもの、指し示すもの)
→ポインタ
ある変数に格納されている値のアドレスは&変数名
の形式で取得できます。
アドレスはポインタ変数(単にポインタと呼ぶことが多い)に格納できます。
ポインタ変数は、例えばint型のデータなら「int*型」というポインタ型となります。
&
はアドレス演算子、*
は間接演算子といいます。
「int*型」のポインタは、保存しているアドレスから4バイト分のメモリ領域にアクセスを行います。
(int型が4バイト環境の場合)
ポインタ型の変数の中身はアドレスですから、そのままではアドレスそのもの(整数値)が読み書きされます。
ポインタ変数の手前に間接演算子を付けることで、そのアドレスが示す実データにアクセスすることができます。
int num = 10;
//変数numの実データのアドレス取得
int* pointer = #
//アドレスを表示
printf("%p\n", pointer);
//アドレス位置にある値を表示
printf("%d\n", *pointer);
//ポインタ変数を通して値を書き換える
*pointer = 20;
printf("%d\n", *pointer);
//変数numの値も書き換わる事を確認
printf("%d\n", num);
000000FDAD8FFB84 10 20 20
アドレスの値は実行毎に異なります。
ポインタ演算
ポインタ変数のアドレスはポインタ演算によって変更することができます。
int array[] = { 12, 34, 56, 78, 90 };
//配列の2番目の要素のアドレスを取得
int* pointer = &array[2];
printf("%p: ", pointer);
printf("%d\n", *pointer);
//アドレスをひとつ後ろにずらす
pointer++;
printf("%p: ", pointer);
printf("%d\n", *pointer);
//アドレスを二つ手前にずらす
pointer -= 2;
printf("%p: ", pointer);
printf("%d\n", *pointer);
0000006EBB2FF830: 56 0000006EBB2FF834: 78 0000006EBB2FF82C: 34
ポインタ演算は+
、-
、およびインクリメントとデクリメント演算子が使用できます。
ポインタ演算で実際に増減する値は「増減させる値×データ型のサイズ」となります。
例えばint型は(多くの環境で)4バイトデータなので、ポインタ変数の値を1増加させると、アドレスは4バイト分増加します。
配列はメモリ上に連続して要素が配置されることが保証されているので、ある要素を指すアドレスに+1のポインタ演算を行うと、次の要素のアドレスを指し示すことになります。
ポインタと配列
配列変数を評価すると配列の先頭要素へのポインタが返されます。
添字演算子は内部的にはポインタ演算を行っており、ポインタ演算を簡易に表記するためのものです。
(シンタックスシュガー)
そのため、ポインタ変数に対して添字演算子を使用することもできます。
int array[] = { 1, 2, 3 };
//配列データのアドレスを取得
//(&記号は必要ない)
int* pointer = array;
//配列先頭の要素を表示
printf("%d\n", array[0]);
printf("%d\n", pointer[0]);
//配列の2番目の要素を表示
printf("%d\n", *(array + 2));
printf("%d\n", *(pointer + 2));
1 1 3 3
ポインタと文字列
文字列はchar型のポインタ変数に格納して使用することができます。
文字列リテラルの評価値はそのアドレスとなるので、ポインタ変数にそのまま代入することで、その文字列の先頭位置のアドレスを得ることができます。
char* str = "ABC";
printf("%s", str);
//別の文字列に差し替える
str = "DEF";
文字列リテラルの型
C言語では文字列リテラル自体はchar*型となっています。
しかし文字列リテラルは不変なので、文字列リテラルを格納するポインタ変数にはconst char*
型を使用することが推奨されます。
const char*型は文字列リテラルを書き換えようとするとコンパイルエラーとなるので安全性が高まります。
char* str1 = "ABC";
const char* str2 = "ABC";
str1[0] = 'Z'; //未定義動作(危険)
str2[0] = 'Z'; //コンパイルエラー(安全)
C++では文字列リテラルはconst char*型となっています。
またC言語ではconst char*型をchar*型変数にそのまま代入可能ですが、C++は型のチェックが厳密なためchar*型変数には格納できません。
ポインタと関数
C言語の関数の引数や戻り値などは、実データがコピーされたものが渡されます。(値渡し)
これをポインタでやり取りすることで、構造体などの大きなサイズのデータのコピーコストを抑えることができます。(ポインタ渡し)
#include <stdio.h>
//3次元情報を扱う構造体
typedef struct {
double x;
double y;
double z;
} Vector3;
//Vector3をmul倍した値を返す
//仮引数と戻り値のVector3はポインタ渡し
Vector3* multiplyVector3(Vector3* v3, double mul)
{
v3->x *= mul;
v3->y *= mul;
v3->z *= mul;
return v3;
}
//Vector3のメンバを表示
//仮引数のVector3はポインタ渡し
void printVector3(Vector3* v3)
{
printf("Vector3(%g, %g, %g)\n",
v3->x, v3->y, v3->z);
}
int main()
{
Vector3 v3 = { 10, 20, 30 };
//仮引数がポインタなのでアドレスを渡す
printVector3(&v3);
//仮引数と戻り値がポインタ
Vector3* v3p = multiplyVector3(&v3, 1.2);
//仮引数がポインタなのでポインタ変数をそのまま渡す
printVector3(v3p);
}
Vector3(10, 20, 30) Vector3(12, 24, 36)
このコードの構造体Vector3は、メンバにdouble型を3つ持つので24バイトのサイズとなります。
これをそのまま引数等に使用すると24バイトのコピーが発生しますが、ポインタでやり取りするとポインタのサイズのコピーコストで済みます。
ポインタのサイズは32ビット環境では4バイト、64ビット環境では8バイトです。
C言語の値のやり取りは、厳密に言えば全て「値渡し」です。
ポインタ渡しは、ポインタ変数が保存するアドレスをコピーして関数に渡しています。
(これも「変数の中身をコピーして渡している」に過ぎず、つまりは値渡し)
ポインタ渡しのことを「参照渡し」と説明しているものもありますが、C言語には参照渡しは存在しません。
C++にはポインタとは別の「参照渡し」の機能が存在します。
混同の可能性があるので注意してください。
引数と配列
関数の引数に配列を指定する事は可能ですが、それは配列の先頭要素へのポインタとして扱われます。
C言語では関数に直接配列を渡すことはできません。
//引数での配列の指定は
void f(int array[]) {}
//こう記述したものとみなされる
void f(int *array) {}
//要素数を指定していても無視される
void f(int array[3]) {}
ポインタから元の配列の要素数を得る手段はないので、関数側で配列として使用するには配列の要素数も同時に引数として渡す必要があります。
#include <stdio.h>
void f(int array[], size_t count)
{
for (int n = 0; n < count; ++n)
printf("%d ", array[n]);
//関数側で引数の配列にsizeofを使用しても
//正しい配列サイズは得られない
//(常にポインタサイズが返される)
size_t s = sizeof(array);
}
int main()
{
int a[] = { 1, 2, 3 };
f(a, sizeof(a) / sizeof(a[0]));
}
アロー演算子
構造体、共用体をポインタで扱う場合、メンバ変数へのアクセスはアロー演算子->
で行います。
アロー演算子を使用しない場合は以下のような形式でメンバにアクセスします。
typedef struct {
double x;
double y;
double z;
} Vector3;
void f(Vector3* v3)
{
//どちらも同じ意味
double a = (*v3).x;
double b = v3->x;
}
ダングリングポインタ
関数内で宣言されたローカル変数の寿命は関数の終了までなので、そのアドレスを返すと不正なメモリ領域への参照となります。
(ダングリングポインタ)
int* f()
{
int n = 2;
//ローカル変数のアドレスを返却
return &n;
}
//関数fの終了時点でローカル変数nの値は使えない
//使用していたメモリ領域は別の用途に使用される可能性がある
int main()
{
int* p = f();
//この時点でポインタpのアドレスが示すデータの内容は保証されない
//何が表示されるかは不定
printf("%d", *p);
}
関数の実行終了の直後はまだローカル変数として使用されたアドレスのデータはそのまま残っている可能性はありますが、いつ消去(上書き)されてもおかしくなく、動作は保証されません。
なお、文字列リテラルはプログラムの終了まで存在することが保証されているため、関数内に記述した文字列リテラルのアドレスを返すことは問題ありません。
const char* f()
{
const char* s = "ABC";
return s;
}
int main()
{
const char* str = f();
//OK
printf("%s", str);
}
関数ポインタ
関数もアドレスを取得してポインタ変数に格納することができます。
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int main()
{
//int型の戻り値、int型を二つ引数に取る
//関数ポインタ
int (*func_ptr)(int, int);
//関数ポインタに同じシグネチャの関数のアドレスを代入
func_ptr = add;
int a = func_ptr(2, 3);
printf("add: %d\n", a);
//別の関数アドレスに差し替え
func_ptr = sub;
int b = func_ptr(2, 3);
printf("sub: %d\n", b);
}
add: 5 sub: -1
関数ポインタの宣言の形式は少し独特ですが、上記の場合はfunc_ptr
がポインタ変数の名前となります。
ポインタのデータ型は関数の戻り値の型を指定し、引数の型と数も同じにします。
関数呼び出し演算子()
を使用しない関数名を評価するとそのアドレスを返すので、同じシグネチャ(戻り値と引数の型および数の形式)の関数ポインタにそのアドレスを保存できます。
後はその関数ポインタに関数呼び出し演算子(および引数)を指定することで、関数を実行することができます。
同じシグネチャであれば別の関数に差し替えることができるので、関数呼び出しのコードを変更することなく、呼び出す関数を変更することができます。
void*型
void*
型は特殊なポインタ型で、データサイズが不明なメモリ位置を保存します。
任意のサイズのメモリを動的確保するmalloc
関数などがvoid*型を返します。
#include <stdio.h>
#include <stdlib.h> //malloc関数などが定義されている
int main()
{
//char型10個分のサイズのメモリを確保
void* p = malloc(sizeof(char) * 10);
if (!p) //メモリ確保失敗
return;
//確保したメモリに文字列をコピー
memcpy(p, "abcdehghi", 10);
printf("%s", (char*)p);
}
void*型はアドレス情報だけを持ち、サイズ情報(データ型の情報)を持たないため、ポインタ演算はできません。
(他のポインタ型へキャストすれば可能です)
NULLポインタ
アドレスがメモリ上のどこも指していないことを示すためにNULLという定数を使用することができます。
(<stdio.h>
などに定義されています)
NULL状態のポインタはNULLポインタと呼びます。
ポインタ変数の初期化に使用したり、一度使用したポインタ変数をNULLポインタにしておくと不用意な値の書き換えを防ぐことができます。
#include <stdio.h>
int main()
{
//ポインタ変数も初期化しない場合は不定値なので
//明示的にNULLポインタにしておく
int* pointer = NULL;
int num = 123;
pointer = #
printf("%d", *pointer);
//これ以上ポインタ変数を使わない場合は
//NULLにしておくと安全
pointer = NULL;
//0を代入しても効果は同じだが
//定数NULLのほうが意図が明確になるのでおすすめ
pointer = 0;
//コンパイラによってはこういうミスに警告を出してくれる
*pointer = NULL;
}
制限付きポインタ(restrictポインタ)
restrict
キーワードで修飾されたポインタは、そのポインタが有効なブロック内で、そのアクセスするメモリに対して他のポインタ等がアクセスしないことをコンパイラに伝えます。
(排他的アクセス)
これにより、コンパイラは動作を最適化して速度の向上を図ることができます。
排他的アクセスを保証するのはプログラマの責任です。
特にrestrictポインタがアクセスするデータが変更される場合、他のポインタ等からのアクセス(読み書き両方)があると未定義動作となります。
このキーワードはC99から標準化されています。
#include<stdio.h>
//srcのアドレスからcount個分をdstのアドレスにコピー
//それぞれのメモリ領域は重複してはならない
void f(int* restrict dst, int* restrict src, size_t count)
{
while (count-- > 0)
*dst++ = *src++;
}
int main()
{
int arr[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
//配列の5番目から、配列の先頭要素に、5つ分コピー
//コピー先とコピー元の領域に重複はないので合法
f(arr + 5, arr, 5);
//配列の2番目から、配列の先頭要素に、5つ分コピー
//コピー先とコピー元の領域が重なるため未定義動作
f(arr + 2, arr, 5);
}
プリプロセッサ
プリプロセッサはコンパイルの直前にコードに対して処理を行います。
(コンパイルに使用するデータが変換されるだけで、コードファイルそのものを書き換えることはありません)
#include
#inclued
ディレクティブは、指定のファイルをソースコード上に展開します。
以下のコードは「stdio.h」ヘッダファイルの内容を「main.c」ファイルの先頭に全て記述したのと同じ意味になります。
これによって「stdio.h」に定義されている関数等が使用可能になります。
#include <stdio.h>
int main() {}
通常は拡張子.hのヘッダファイルを読み込みますが、テキストファイルであれば拡張子および内容を問わず読み込むことができます。
ファイル名を囲う山括弧<>
はシステムにあるファイル(C言語標準のヘッダファイルやコンパイルオプションで指定したディレクトリ内のファイル)を読み込む場合に指定します。
その他ダブルクォーテーション""
で囲うこともでき、これはまずソースコードと同じディレクトリを検索し、見つからなかった場合にシステムを検索します。
こちらは主に自作のヘッダファイルを読み込むときに使用します。
なお、どちらも相対パスの指定が可能です。
絶対パスも可能ですが推奨されません。
#include <stdio.h>
#include "myheader.h"
int main() {}
#define
#define
ディレクティブは指定の識別子を別のコードに置き換えます。
(マクロ)
これはコンパイル時定数やマクロ関数の定義、ユーザー定義型の作成などに使用します。
#include <stdio.h>
//定数の定義
#define BUFFERSIZE 10
#define MYTEXT "Hello."
int main()
{
int arr[BUFFERSIZE] = {0};
printf("%s", MYTEXT);
}
上記コードでは、BUFFERSIZE
という個所は全て10
に、MYTEXT
という個所は全て"Hello."
に置き換えられた上でコンパイルが行われます。
コード中の一箇所を書き換えるだけで全体に修正が反映されるようになるため保守性が良くなり、ただの数値に名前を付けることにもなるのでマジックナンバーの解消に役立ちます。
なお、文字列リテラル中や、他の識別子(変数名や関数名など)の一部として現れるものは置き返されません。
マクロ関数
#define
ディレクティブはコードを置き換えるだけの機能ですが、これを利用することで関数のような処理を作ることができます。
#include <stdio.h>
//マクロ関数
#define SWAP(x, y) do {\
int tmp = x;\
x = y;\
y = tmp;\
} while(0)
int main()
{
int a = 1;
int b = 2;
printf("%d, %d\n", a, b);
SWAP(a, b);
printf("%d, %d\n", a, b);
if(1)
SWAP(a, b);
printf("%d, %d\n", a, b);
}
1, 2 2, 1 1, 2
マクロ定義内で改行する場合は行の終端に\
を指定します。
この直後に改行が必要で、空白やコメントも含めてはなりません。
マクロ関数が複数行に渡る場合、全体をdo{}while(0)
で囲うことが推奨されます。
これはブロック記号を省略したif文などで正しく機能するようにするためです。
マクロ関数は予想しない形でコードに展開される場合があるので、可能なら通常の関数を使用するほうが良いでしょう。
#undef
#undef
ディレクティブは、#define
済みの定数やマクロ関数を削除します。
#include <stdio.h>
#define MYNUMBER 10
int main()
{
printf("%d\n", MYNUMBER);
#undef MYNUMBER
//これ以降はMYNUMBERは使えない
//コンパイルエラー
printf("%d\n", MYNUMBER);
}
定義済みマクロ
C言語にはあらかじめ定義済みのマクロがいくつかあります。
- __STDC__
- コンパイラがANSI C(C89、標準C)に準拠している場合に1として定義されます。
(Visual Studioでは標準では未定義になっています) - __STDC_VERSION__
- 標準Cのバージョンを表す整数値。
199409L (C95)
199901L (C99)
201112L (C11)
201710L (C17) - __STDC_HOSTED__
- OSホスト環境の場合は1、OSなし環境の場合は0。
(C99以降) - __FILE__
- 現在のファイル名の文字列リテラル。
- __LINE__
- ソースファイルの行番号の整数値。
- __DATE__
- コンパイルした日付
"Mmm dd yyyy"形式の文字列リテラル。 - __TIME__
- コンパイルした時刻
"hh:mm:ss" 形式の文字列リテラル。
また、定義済みマクロではありませんが、関数内では__func__
という定義済み変数が使用できます。
これは現在の関数名を保存するconst char型の配列です。
#include <stdio.h>
void my_function()
{
printf("%s", __func__);
}
int main()
{
my_function();
}
my_function
#ifdef
#ifdef
ディレクティブは、指定の識別子が#define
されている場合にコードを有効化します。
#ifndef
ディレクティブは、指定の識別子が#define
されていない場合にコードを有効化します。
こられは#elif
、#else
、#endif
と組み合わせて使用できます。
有効化されなかったコード範囲はコンパイルの対象から除かれます。
#include <stdio.h>
#define MYDEF
int main()
{
#ifdef MYDEF
//MYDEFが定義されていたら
//この範囲のコードが有効になる
printf("'MYDEF' exists.\n");
#endif
#ifdef MYDEF2
//MYDEF2が定義されていたら
//この範囲のコードが有効になる
printf("'MYDEF2' exists.\n");
#elif MYDEF3
//MYDEF3が定義されていたら
//この範囲のコードが有効になる
printf("'MYDEF3' exists.\n");
#else
//上記すべてが定義されていない場合
//この範囲のコードが有効になる
printf("They don't exist.\n");
#endif
#ifndef MYDEF4
//MYDEF4が定義されていなければ
//この範囲のコードが有効になる
printf("'MYDEF4' doesn't exist.\n");
#endif
}
'MYDEF' exists. They don't exist. 'MYDEF4' doesn't exist.
Visual Studioのデバッグモードでは、_DEBUG
という識別子が#define
されているため、これを利用して開発時とリリース時とで必要な処理を容易に振り分けることができます。
ただしVisual Studioのバージョンにより異なる可能性はあります。
int main()
{
int a = f();
#ifdef _DEBUG
//関数の処理結果を確認するためのコード
//製品版(リリースモード)では除外される
printf("a = %d\n", a);
#endif
}
#if
#if
ディレクティブは、指定の条件が真の場合にコードを有効化します。
これは#elif
、#else
、#endif
ディレクティブと組み合わせて使用できます。
有効化されなかったコード範囲はコンパイルの対象から除かれます。
#define LEVEL 5
int main()
{
#if LEVEL > 3
//LEVELの値が3より大きければ
//この範囲のコードが有効になる
printf("'LEVEL' is greater than 3.\n");
#endif
}
#if
ディレクティブではdefined
というプリプロセッサ演算子を使用して、識別子が#define
されているか否かで処理を分けることができます。
#include <stdio.h>
#define MYDEF
int main()
{
#if defined(MYDEF)
//MYDEFがdefineされていれば
//この範囲のコードが有効になる
printf("'MYDEF' exists.\n");
#endif
#if !defined(MYDEF)
//MYDEFがdefineされていなければ
//この範囲のコードが有効になる
printf("'MYDEF' doesn't exist.\n");
#endif
}
#pragma
#pragma
ディレクティブは、処理系定義の動作を制御します。
標準ではSTDC FENV_ACCESS
、STDC FP_CONTRACT
、STDC CX_LIMITED_RANGE
の三つが定義されています。
それぞれON
(有効)、OFF
(無効)、DEFAULT
(既定値)を指定します。
- STDC FENV_ACCESS
- ONにすると、プログラムが浮動小数点環境を変更することを許可します。
fesetround関数(<fenv.h>ヘッダ)で浮動小数点数の丸め方法を変更する場合はONにする必要があります。
(初期値は処理系依存。OFFが多いらしい) - STDC FP_CONTRACT
- ONにすると、浮動小数点式の縮約を許可します。
コンパイラは小数の演算を効率的に行うために、小数を含む式を最適化する場合があり、このとき小数の丸め誤差が変化することがあります。
これを縮約といいます。
(初期値は処理系依存。ONが多いらしい) - STDC CX_LIMITED_RANGE
- ONにすると、複素数の乗算や徐算、絶対値の演算時に発生するオーバーフロー(アンダーフロー)の可能性を無視します。
これは演算を高速化しますが、オーバーフローによる値の変化を考慮しないためオーバーフローしないことはプログラマの責任となります。
(初期値はOFF)
#pragma once
上記以外では#pragma once
というプラグマがよく使用されます。
これは非標準ですが、現行のほとんどのコンパイラが対応しています。
これはヘッダファイル内で使用し、ファイルが一度インクルードされた場合は再度インクルードされても展開されないことを保証します。
つまりインクルードガードの簡易な記法として使用できます。
コードファイルの分割
ソースコードの分割は新しい.cファイル(ソースコード)を作成し、同時に.hファイル(ヘッダファイル)を作成します。
基本的に.cファイルと.hファイルは同名にしますが制限はありません。
ヘッダファイルには外部に公開したい変数や関数を記述します。
通常は二重インクルードを防ぐためにインクルードガードも同時に記述します。
ここでは#pragma once
を使用しています。
分割したソースコードは読み込みたいソースファイル上で#include
ディレクティブを使用して読み込みます。
通常はプロジェクトのディレクトリ内に配置すると思うので、二重引用符を使用します。
(相対パスでの指定も可能です)
分割されたソースコード(.cファイル)は別々にコンパイルが行われます。
これを翻訳単位といいます。
#include "test.h"
int main()
{
printNumber(1);
}
#pragma once
void printNumber(int n);
#include <stdio.h>
void printNumber(int n)
{
printf("%d", n);
}
インクルードガード
ライブラリなどのヘッダファイルは複数のソースからインクルードされる事が想定されるため、二重インクルードを防ぐ処理が必要です。
(二重定義を防ぐため)
現在のコンパイラではほとんどの場合で#pragma once
で十分ですが、以下のような記述でもインクルードガードが可能です。
#ifndef TEST_H
#define TEST_H
void printNumber(int n);
#endif
//この行以降はガードされないので
//何も記述しないこと
#define
を使用する方法は指定の識別子がすでに定義済みの場合は内容を展開しないのに対し(実質的に読み込まれないのと同じ)、#pragma once
はそのファイルがすでに読み込まれている場合は読み込みをスキップするという違いがあります。
オブジェクトの生存期間と有効範囲
記憶域期間
オブジェクトには記憶域期間という概念があります。
これはオブジェクトの寿命と考えることができます。
- 自動記憶域期間
- オブジェクトが宣言されたブロックに処理が移った時点でメモリが確保され、そのブロックを終了した時点で解放されます。
(可変長配列では若干の例外あり) - 静的記憶域期間
- プログラム実行時にメモリが確保され、プログラム終了時に解放されます。
- 割付け記憶域期間
- 動的にメモリを確保/解放する関数によるもので、寿命はプログラマが管理します。
(malloc関数、free関数など) - スレッド記憶域期間
- スレッドが開始されたときにメモリが確保され、スレッドの終了時に開放されます。
(C11以降)
リンケージ
リンケージは、変数名や関数名などの識別子が現在のスコープ外から参照可能かどうかを示します。
- リンケージなし
- 宣言されたスコ-プ内からのみ参照できます。
ブロック内で宣言された変数や関数の仮引数が該当します。 - 内部リンケージ
- 宣言されたファイル内(翻訳単位)から参照できます。
あらゆるブロックの外(グローバル領域)でstaticを使用して宣言された変数や関数が該当します。 - 外部リンケージ
- プログラム全体から参照できます。
あらゆるブロックの外(グローバル領域)で宣言された変数や関数(staticなものは除く)、extern宣言された変数が該当します。
記憶域クラス指定子
記憶域クラス指定子はオブジェクトの記憶域期間やリンケージを指定します。
- auto
- 自動記憶域期間。
ブロック内で宣言されるオブジェクトにのみ使用可能。
全てのローカル変数および関数の引数の初期値です。 - register
- 自動記憶域期間。
ブロック内で宣言されるオブジェクトにのみ使用可能。
値をCPUレジスタに格納するようにコンパイラに要求します。
(実際にそうされるかはコンパイラ次第です)
この変数のアドレスを取得することはできません。
register配列をポインタとして扱うことはできません。 - static
- 静的記憶域期間。
オブジェクトが外部リンケージである場合は内部リンケージにします。
(ブロックスコープの場合は変化なし) - extern
- 静的記憶域期間。
すでに内部リンケージとして宣言されている場合を除き、オブジェクトを外部リンケージにします。
全ての関数、グローバル変数の初期値です。 - _Thread_local
- スレッド記憶域期間。(C11以降)
関数宣言には指定できません。
ブロック内で宣言する場合はリンケージの指定のためにexternまたはstaticのどちらかとの組み合わせが必要です。
(記憶域期間は変更されません)
関数の仮引数にはregister
のみが指定できます。
#include <stdlib.h>
//静的記憶域期間
//外部リンケージ
int a;
extern int b;
//内部リンケージ
static c;
//スレッド記憶期間
//外部リンケージ
_Thread_local int d;
//内部リンケージ
static _Thread_local e;
//通常の関数は暗黙的にextern指定
//つまり外部リンケージ
int main()
{
//自動記憶域期間
//通常のローカル変数は
//autoが省略されたものとみなされる
int f;
auto int g;
//自動記憶域期間
//可能ならCPUレジスタに値を格納することで
//高速化を図る
register int h;
//静的記憶期間
//プログラム実行時に初期化、
//プログラム終了時に開放
static int i;
//静的記憶期間
//外部で定義されている変数へのアクセス
//(定義がないとリンクエラー)
extern int j;
//スレッド記憶域期間
_Thread_local static int k;
//確保されたメモリは割付け記憶域期間
//変数自体はただのローカル変数
//ローカル変数が寿命を迎えても
//割付け記憶域のメモリは削除されない
int* l = malloc(sizeof(int) * 1);
}
//内部リンケージ
static void m() {}
メモリ管理
メモリ領域
プログラムが使用するメモリは大きく分けて4つの領域が存在します。
- テキスト領域
- プログラム本体(実行コード)が保存される領域。
関数もここに保存されます。
読み取り専用。 - 静的領域
- グローバル変数、静的変数(static)が保存される領域。
プログラム開始時に確保され、プログラム終了時に解放されます。 - ヒープ領域
- 動的に確保される領域。
C言語ではmalloc関数などでプログラマが管理します。 - スタック領域
- 関数の引数やローカル変数が保存される領域。
自動的に確保され、自動的に開放されます。
スタックオーバーフロー
関数の引数やローカル変数などはスタック領域に保存されますが、このメモリ領域は数MB程度の大きさしかありません。
大きな配列、構造体などをローカル変数で使用するとメモリが枯渇し、プログラムが強制終了します。
これをスタックオーバーフローといいます。
#include <stdio.h>
typedef struct {
//1MB
char buffer[1024 * 1024];
} MyBigStruct;
int main()
{
printf("MyBigStructは[%zu]バイト\n", sizeof(MyBigStruct));
//8MBのメモリをスタック領域に確保する
//このコードはスタックオーバーフローが発生する可能性が高い
MyBigStruct arr[8];
}
メモリの動的確保
メモリはmalloc
関数で任意のサイズを動的に確保可能です。
このメモリはヒープ領域に確保され、スタック以上の大きなサイズを扱えます。
(実際に使用可能なサイズは環境依存。32bit環境では2~4GBまでの制限あり)
引数は確保したいバイト数を指定します。
確保に成功した場合はそのアドレスを返し、失敗した場合はNULL
を返します。
malloc
関数で確保した領域はfree
関数で解放します。
#include <stdio.h>
#include <stdlib.h>
typedef struct {
//1MB
char buffer[1024 * 1024];
} MyBigStruct;
int main()
{
printf("MyBigStructは[%zu]バイト\n", sizeof(MyBigStruct));
//8MBのメモリをヒープ領域に確保する
MyBigStruct* p = malloc(sizeof(MyBigStruct) * 8);
//NULLならメモリ確保の失敗
if (!p)
{
printf("メモリの確保に失敗しました。");
return 0;
}
p[0].buffer[0] = 'a';
p[0].buffer[1] = 'b';
p[0].buffer[2] = 'c';
p[0].buffer[3] = '\0';
p[1].buffer[0] = 'd';
p[1].buffer[1] = 'e';
p[1].buffer[2] = 'f';
p[1].buffer[3] = '\0';
printf("%s\n", p[0].buffer);
printf("%s\n", p[1].buffer);
//メモリの解放
free(p);
p = NULL;
}
MyBigStructは[1048576]バイト abc def
動的確保したメモリはfree
関数で解放しない限り、プログラムの実行中は確保され続けます。
メモリを解放しないまま、そのメモリ領域へのポインタ変数の寿命が尽きるなどでアクセスする手段がなくなると、解放出来ないメモリ領域が残り続けます。
(メモリリーク)
プログラムを終了すればメモリは解放されますが、長期間稼働し続けるプログラムでは重大な問題になります。
また、一度確保したメモリ領域は別のデータが使用する可能性があるため、メモリの二重解放は不具合の原因になります。
メモリ解放後のポインタにはすぐにNULL
を代入しておくとミスを減らせます。
(NULL
に対するfree
関数の実行は安全です)
アサーション
assert
assert
マクロは、プログラムの実行時に任意の式を検証し、不正な値を検出した場合にプログラムを停止します。
使用するには<assert.h>
ヘッダが必要です。
#include <stdio.h>
#include <assert.h>
int main()
{
int a = 0;
int b = 1;
assert(a == b);
printf("%d %d", a, b);
}
Main: main.c:8: int main(): Assertion `a == b' failed.
assert
マクロに指定した値(式でも良い)が偽(0)と評価される場合、標準エラー出力(stderr
)に、エラーが起きたソースファイル名(__FILE__
)、行番号(__LINE__
)、関数名(__func__
(C99以降))と、0と評価された式を含むエラーメッセージを出力し、abort
関数を呼び出しプログラムを終了します。
(※Visual Studio2022では関数名は出力されないようです)
abort
関数はプログラムの異常終了させます。
NDEBUG
assert
マクロは、<assert.h>
ヘッダがインクルードされるよりも前でNDEBUG
という識別子が#define
されている場合は、その処理が無視されます。
(※Visual Studioのリリースモードでは自動的にNDEBUG
が定義済みになるようです)
#define NDEBUG
#include <stdio.h>
#include <assert.h>
int main()
{
int a = 0;
int b = 1;
assert(a == b);
printf("%d %d", a, b);
}
0 1
_Static_assert
_Static_assert
はコンパイル時に任意の式を検証し、不正な値を検出した場合にプログラムを停止します。
これはC11以降で使用可能です。
<assert.h>
ヘッダをインクルードしている場合はstatic_assert
マクロでも同等の処理が可能です。
_Static_assert
はコンパイル時にエラーチェックを行うので、式の評価値はコンパイル時に決定可能である必要があります。
第一引数には評価する式を、第二引数にはエラー時に標準エラーに出力されるメッセージを指定します。
C23ではメッセージを省略できます。
#include <stdio.h>
//#include <assert.h>
int main()
{
//int型が4バイトでない環境ではコンパイルエラーになる
_Static_assert(
sizeof(int) == 4,
"'int' type is expected that size is 4 byte."
);
}
main.c:7:5: error: static_assert failed due to requirement 'sizeof(int) == 4' "'int' type is expected that size is 4 byte."