C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
商品、社員、センサー情報のようなデータは、1つの値だけでは表せません。
また、固定値や状態を数値だけで書くと、意味が分かりにくくなります。
if (status == 2)
{
// 2 が何を表すのか、コードだけでは分かりにくい
}
この記事では、関連するデータを構造体にまとめ、変更しない値、状態を表す値、コンパイル前に置き換える名前を使い分けます。
-
structとtypedef -
constが付くと何が変わるか -
enumが作る値と、できないこと -
#defineが行う文字列置換 - 定数マクロと関数形式マクロの注意点
- 既存の
#defineと名前がぶつかった場合 - 構造体配列を関数へ渡す方法
この記事は、VS Code + WSL + Ubuntu + GCC を前提にしています。
コンパイル時は、毎回次のオプションを付けます。
gcc -std=c17 -Wall -Wextra -Wpedantic main.c -o app
1. 構造体で関連する値を1件にまとめる
商品を表すために、商品名、単価、在庫数、販売状態を使うとします。
別々の変数として持つこともできます。
char name[] = "りんご";
int unit_price = 120;
int stock_count = 10;
int status = 1;
ただし、商品が増えると、どの名前・単価・在庫数が同じ商品なのかを管理しにくくなります。
構造体を使うと、関連する値を1件として扱えます。
typedef struct
{
int id;
char name[32];
int unit_price;
int stock_count;
} Product;
typedef を付けると、以降は struct Product ではなく、短い Product を型名として使えます。
Product apple = { 101, "りんご", 120, 10 };
2. const は「この経路から変更しない」を表す
const は、値を変更しない意図をコードに表します。
const int max_stock = 100;
この変数へ代入すると、コンパイル時にエラーになります。
max_stock = 200;
error: assignment of read-only variable 'max_stock'
const は、値を変更してはいけないことをコンパイラへ伝えるためのものです。
関数の引数で使う const
表示するだけの関数では、構造体を変更しないことを明示できます。
void print_product(const Product *product)
{
printf("%s\n", product->name);
// product->stock_count = 0;
// const が付いているため、ここでは変更できない
}
const Product *product は、このポインタを通しては Product の中身を変更しないという意味です。
| 書き方 | 変更できないもの |
|---|---|
const Product *product |
product が指す構造体の中身 |
Product * const product |
ポインタ変数そのもの。別の場所を指し直せない |
const Product * const product |
構造体の中身とポインタ変数の両方 |
初心者のうちは、まず次の使い方を覚えれば十分です。
// 読み取り専用で渡す
void print_product(const Product *product);
// 変更するために渡す
void add_stock(Product *product, int amount);
const は、値が常に正しいことまで保証する機能ではありません。
たとえば、const int max_stock = -1; と書いても、値の妥当性は検証されません。範囲の確認は、条件分岐などで別に書きます。
const と #define の違い
const int max_stock = 100;
#define MAX_STOCK 100
どちらも 100 を表せますが、動きは違います。
| 観点 | const |
#define |
|---|---|---|
| 正体 | 型を持つ変数・オブジェクト | コンパイル前の文字列置換 |
| 型チェック | ある | ない |
| アドレスを取る | できる | できない |
| デバッガーで確認 | しやすい | 置換後の値になる |
#if の条件 |
原則として使えない | 使える |
| 向く場面 | 読み取り専用の値、関数引数 | 配列サイズ、条件付きコンパイル、ヘッダガード |
C言語の const は、C++ の constexpr のような「必ずコンパイル時定数」という意味ではありません。
配列サイズ、case ラベル、#if の条件など、コンパイル時定数が必要な場面では、#define または enum を使う方が分かりやすい場合があります。
3. enum は数値へ名前を付ける
販売状態を数値で管理する例です。
typedef enum
{
PRODUCT_STATUS_STOPPED,
PRODUCT_STATUS_SELLING,
PRODUCT_STATUS_SOLD_OUT
} ProductStatus;
値を省略した場合、先頭は 0、以降は1ずつ増えます。
| 名前 | 値 |
|---|---|
PRODUCT_STATUS_STOPPED |
0 |
PRODUCT_STATUS_SELLING |
1 |
PRODUCT_STATUS_SOLD_OUT |
2 |
コードでは、数値ではなく意味のある名前を使えます。
ProductStatus status = PRODUCT_STATUS_SELLING;
if (status == PRODUCT_STATUS_SOLD_OUT)
{
printf("売り切れです。\n");
}
値を固定したい場合
ファイルへ保存する値、通信で送る値、他システムと合わせる値は、番号を明記します。
typedef enum
{
PRODUCT_STATUS_STOPPED = 0,
PRODUCT_STATUS_SELLING = 10,
PRODUCT_STATUS_SOLD_OUT = 20
} ProductStatus;
enum は、実行時に不正な値を自動で拒否しません。
次のような値も、明示的なキャストをすると入れられます。
ProductStatus status = (ProductStatus)99;
外部ファイル、通信、ユーザー入力から値を受け取る場合は、範囲や値を自分で確認します。
int is_valid_product_status(ProductStatus status)
{
return status == PRODUCT_STATUS_STOPPED
|| status == PRODUCT_STATUS_SELLING
|| status == PRODUCT_STATUS_SOLD_OUT;
}
4. #define はコンパイル前に文字を置き換える
#define は、Cコンパイラが文法を確認する前に、指定した名前を文字列として置き換えます。
#define PRODUCT_NAME_LENGTH 32
この後に書かれた PRODUCT_NAME_LENGTH は、コンパイル前に 32 へ置き換えられます。
char name[PRODUCT_NAME_LENGTH];
イメージは次のとおりです。
コンパイル前
char name[PRODUCT_NAME_LENGTH];
↓ 置換後
char name[32];
#define は、変数を作りません。メモリを確保せず、型も持ちません。
定数としての #define
配列サイズや固定の上限には、名前付きの定数マクロを使えます。
#define PRODUCT_NAME_LENGTH 32
#define LOW_STOCK_LIMIT 3
#define TAX_RATE 0.10
マクロ名は、通常は大文字とアンダースコアで書きます。
#define MAX_PRODUCT_COUNT 100
プロジェクトがすでに命名規約を持つ場合は、そちらを優先してください。
5. 関数形式マクロは、関数のように見えて関数ではない
引数を取る #define は、関数形式マクロと呼ばれます。
#define SQUARE(value) ((value) * (value))
見た目は関数呼び出しに近いですが、実際は文字列置換です。
int result = SQUARE(1 + 2);
置換後は、次の形になります。
int result = ((1 + 2) * (1 + 2));
そのため、result は 9 になります。
かっこが不足したマクロの問題
次の定義では、期待と違う計算になることがあります。
#define BAD_SQUARE(value) value * value
int result = BAD_SQUARE(1 + 2);
置換後は、次の形です。
int result = 1 + 2 * 1 + 2;
結果は 9 ではなく 5 になります。
i++ を渡すと危険な理由
#define SQUARE(value) ((value) * (value))
int index = 1;
int result = SQUARE(index++);
置換後、index++ が2回評価されます。
int result = ((index++) * (index++));
このような書き方は、意図しない動きや未定義動作につながるため、使いません。
関数形式マクロより、通常の関数を優先してください。
int square_int(int value)
{
return value * value;
}
| 比較 | 関数形式マクロ | 通常の関数 |
|---|---|---|
| 型チェック | ない | ある |
| 引数の評価回数 | 複数回になることがある | 1回 |
| デバッグ | 追いにくい | 追いやすい |
| 初心者向け | 原則として避ける | 優先する |
6. すでにある #define との付き合い方
#define は、宣言された場所からファイル終端まで、または #undef されるまで有効です。
ヘッダーファイルから読み込まれたマクロも、その後のコードへ影響します。
ヘッダガードも #define を使う
#ifndef PRODUCT_H
#define PRODUCT_H
// Product に関する宣言
#endif
#ifndef PRODUCT_H は、PRODUCT_H がまだ定義されていない場合だけ、中身を読み込むという意味です。
1回目の読み込みで PRODUCT_H を定義します。
2回目以降は PRODUCT_H がすでにあるため、中身を読み飛ばします。
これが、同じヘッダーファイルの二重読み込みを防ぐ仕組みです。
同じマクロ名を再定義しない
#define MAX_PRODUCT_COUNT 100
#define MAX_PRODUCT_COUNT 200
異なる内容で同じ名前を定義すると、GCCは warning: "MAX_PRODUCT_COUNT" redefined のような警告を出します。
マクロは名前空間を持たないため、短すぎる名前は他のヘッダーファイルとぶつかりやすくなります。
#define MAX 100
#define STATUS 1
より、用途を含めた名前にします。
#define PRODUCT_MAX_COUNT 100
#define PRODUCT_STATUS_SELLING 1
ただし、状態値には enum を使う方が読みやすい場合が多いです。
予約済みの名前は使わない
先頭がアンダースコアの名前や、アンダースコアが連続する名前は、処理系や標準ライブラリが使うため予約されています。
#define __PRODUCT_MAX 100
#define _ProductMax 100
このような名前は、新しく作らないでください。
7. 使い分けの判断基準
| 目的 | 第一候補 | 理由 |
|---|---|---|
| 関連する値を1件として持つ | struct |
データの対応関係を保てる |
| 型名を短くする | typedef |
struct の記述を減らせる |
| 読み取り専用の引数にする | const |
関数が値を変更しないことを示せる |
| 状態・結果・分類を表す | enum |
数値へ意味のある名前を付けられる |
| 配列の長さ、ヘッダガード、条件付きコンパイル | #define |
プリプロセッサで扱う値だから |
| 計算処理 | 関数 | 型チェックとデバッグがしやすい |
| 関数形式マクロ | 原則として使わない | 引数の多重評価などの事故を防ぐ |
8. 完成例
#include <stdio.h>
#define PRODUCT_NAME_LENGTH 32
#define LOW_STOCK_LIMIT 3
typedef enum
{
PRODUCT_STATUS_STOPPED = 0,
PRODUCT_STATUS_SELLING = 1,
PRODUCT_STATUS_SOLD_OUT = 2
} ProductStatus;
typedef struct
{
int id;
char name[PRODUCT_NAME_LENGTH];
int unit_price;
int stock_count;
ProductStatus status;
} Product;
const char *get_product_status_text(ProductStatus status)
{
switch (status)
{
case PRODUCT_STATUS_STOPPED:
return "販売停止";
case PRODUCT_STATUS_SELLING:
return "販売中";
case PRODUCT_STATUS_SOLD_OUT:
return "売り切れ";
default:
return "不明";
}
}
void print_product(const Product *product)
{
if (product == NULL)
{
return;
}
printf("商品ID: %d\n", product->id);
printf("商品名: %s\n", product->name);
printf("単価: %d円\n", product->unit_price);
printf("在庫数: %d個\n", product->stock_count);
printf("状態: %s\n", get_product_status_text(product->status));
if (product->stock_count <= LOW_STOCK_LIMIT)
{
printf("在庫が少なくなっています。\n");
}
}
int main(void)
{
Product products[] = {
{ 101, "りんご", 120, 10, PRODUCT_STATUS_SELLING },
{ 102, "みかん", 100, 0, PRODUCT_STATUS_SOLD_OUT },
{ 103, "牛乳", 210, 2, PRODUCT_STATUS_SELLING }
};
size_t product_count = sizeof products / sizeof products[0];
for (size_t index = 0; index < product_count; index++)
{
print_product(&products[index]);
printf("\n");
}
return 0;
}
実行結果です。
商品ID: 101
商品名: りんご
単価: 120円
在庫数: 10個
状態: 販売中
商品ID: 102
商品名: みかん
単価: 100円
在庫数: 0個
状態: 売り切れ
在庫が少なくなっています。
商品ID: 103
商品名: 牛乳
単価: 210円
在庫数: 2個
状態: 販売中
在庫が少なくなっています。
9. 理解確認
課題1: const の追加
print_product の引数から const を外し、関数内で stock_count を変更できることを確認してください。
その後、const を戻し、変更しようとするとコンパイルエラーになることを確認してください。
課題2: 新しい状態を追加する
PRODUCT_STATUS_RESERVED を追加し、状態表示関数にも 予約中 を追加してください。
値は 30 に固定してください。
課題3: マクロを関数へ置き換える
次の関数形式マクロを、通常の関数へ置き換えてください。
#define DISCOUNT_PRICE(price, rate) ((price) * (1.0 - (rate)))
関数名は calculate_discount_price とし、引数と戻り値の型を自分で決めてください。
まとめ
-
structは、関連する値を1件にまとめる -
typedefは、型名を使いやすくする -
constは、変更しない意図をコンパイラと読み手へ伝える -
enumは、状態や結果に意味のある名前を付ける -
enumは不正な値を自動で拒否しない -
#defineは、コンパイル前に文字列を置き換える - 関数形式マクロは、引数の多重評価などの事故があるため、通常の関数を優先する
-
#defineの名前は、既存のマクロとぶつからないように用途を含める