1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C言語のstaticとexternを図解|.c/.h・グローバル変数・複数ファイルの仕組み

1
Posted at

C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。

プログラムを終了すると、変数や配列の値は基本的に消えます。
商品一覧、設定値、ログのように後で使うデータは、ファイルへ保存します。

また、機能が増えたプログラムを1つの .c ファイルへ書き続けると、修正する場所を探しにくくなります。

この記事では、商品一覧をファイルへ保存する例を使い、次をまとめて学びます。

  • fopenfprintffgetsfclose
  • .c.h の役割
  • 複数の .c ファイルをGCCでコンパイルする方法
  • static が関数・変数・ローカル変数に付いたときの動き
  • extern を付けたとき、付けないときの違い
  • グローバル変数を共有するときの正しい形
  • グローバル変数を使いすぎない判断基準

グローバル変数は便利ですが、どこからでも変更できるため、原因調査を難しくします。

この記事では staticextern を理解するために使います。新しい処理では、まず関数の引数・戻り値・構造体で渡せないかを検討してください。


1. ファイル操作の基本

ファイル操作は、次の順番で行います。

関数・指定 役割
fopen ファイルを開く
fprintf ファイルへ書き込む
fgets ファイルから1行読み込む
fclose ファイルを閉じる
"r" 読み込み用。ファイルがない場合は失敗
"w" 書き込み用。既存の内容を上書き
"a" 追記用。既存ファイルの末尾へ追加

最小の書き込み例

#include <stdio.h>

int main(void)
{
    FILE *file_pointer = fopen("sample.txt", "w");

    if (file_pointer == NULL)
    {
        printf("sample.txt を開けませんでした。\n");

        return 1;
    }

    if (fprintf(file_pointer, "C言語で保存しました。\n") < 0)
    {
        printf("書き込みに失敗しました。\n");
        fclose(file_pointer);

        return 1;
    }

    if (fclose(file_pointer) == EOF)
    {
        printf("ファイルを閉じるときに失敗しました。\n");

        return 1;
    }

    return 0;
}

最小の読み込み例

#include <stdio.h>

int main(void)
{
    char line[256];
    FILE *file_pointer = fopen("sample.txt", "r");

    if (file_pointer == NULL)
    {
        printf("sample.txt を開けませんでした。\n");

        return 1;
    }

    while (fgets(line, sizeof line, file_pointer) != NULL)
    {
        printf("%s", line);
    }

    if (ferror(file_pointer))
    {
        printf("読み込み中にエラーが起きました。\n");
        fclose(file_pointer);

        return 1;
    }

    if (fclose(file_pointer) == EOF)
    {
        printf("ファイルを閉じるときに失敗しました。\n");

        return 1;
    }

    return 0;
}

2. .c.h の役割

C言語では、処理の中身と、他のファイルから使うための宣言を分けます。

ファイル 役割
.c 関数の定義、実際の処理を書く
.h 型、定数、関数宣言を共有する
main.c プログラム全体の流れを書く
stock_app/
├ main.c
├ stock.h
├ stock.c
├ file_store.h
├ file_store.c
├ app_state.h
└ app_state.c

.c ファイルを #include しません。

#include "stock.c"

この書き方は、同じ関数が複数回定義される原因になります。
共有するのは .h ファイルであり、.c ファイルはコンパイルコマンドへ列挙します。


3. ヘッダーファイルにはインクルードガードを付ける

#ifndef STOCK_H
#define STOCK_H

// 宣言を書く

#endif

1回目に stock.h を読み込むと、STOCK_H が定義されます。
2回目以降は STOCK_H がすでに定義されているため、中身を読み飛ばします。

同じ型や関数宣言を複数回取り込む事故を防ぐため、ヘッダーファイルには必ず付けます。


4. static は「見える範囲」または「値が残る期間」を変える

static は、書く場所で意味が変わります。

書く場所 主な動き
関数の前 その .c ファイル内だけで使える関数になる
ファイル直下の変数 その .c ファイル内だけで使えるグローバル変数になる
関数内のローカル変数 関数を抜けても値が残る

4.1 static 関数

static int normalize_stock_count(int stock_count)
{
    if (stock_count < 0)
    {
        return 0;
    }

    return stock_count;
}

この関数は、定義した .c ファイルの中だけで使えます。

stock.c 内
  normalize_stock_count を呼べる

main.c、file_store.c
  normalize_stock_count を呼べない

static を付けない関数は、外部リンケージを持ちます。
別の .c ファイルから使うなら、ヘッダーファイルに関数宣言を書けます。

// stock.h
int normalize_stock_count(int stock_count);

ただし、他のファイルから使わせる必要がない補助関数まで公開すると、プロジェクト全体で名前がぶつかりやすくなります。

その .c の内部処理だけに使う関数は static にするのが基本です。

4.2 static を付けないとどうなるか

module_a.cmodule_b.c に、同じ名前の外部関数を作るとします。

// module_a.c
int calculate_total(int value)
{
    return value + 1;
}
// module_b.c
int calculate_total(int value)
{
    return value + 2;
}

両方をコンパイルすると、同じ外部関数が2つあるため、リンク時に次のようなエラーになります。

multiple definition of 'calculate_total'

一方、両方を static 関数にすると、各 .c の内部だけの別関数として扱われるため、名前はぶつかりません。

static int calculate_total(int value)
{
    return value + 1;
}

ただし、static を付けた関数は他の .c ファイルから呼び出せません。

4.3 static ローカル変数

int get_next_sequence(void)
{
    static int sequence = 1;

    return sequence++;
}

呼び出すたびに、値が残ります。

printf("%d\n", get_next_sequence());
printf("%d\n", get_next_sequence());
printf("%d\n", get_next_sequence());

実行結果です。

1
2
3

通常のローカル変数なら、関数を呼ぶたびに初期化されます。

int get_next_sequence(void)
{
    int sequence = 1;

    return sequence++;
}

この場合、毎回 1 が返ります。

static ローカル変数は状態を持つため、テストしにくくなります。

単純な連番や内部キャッシュなど、値を残す理由が明確な場面だけで使います。


5. extern は「実体は別の .c にある」と知らせる

extern は、変数の実体を作りません。

extern int g_saved_product_count;

これは、次の意味です。

g_saved_product_count という int 型の変数は、別の場所で1回だけ定義されている。

実体は、1つの .c ファイルだけに書きます。

int g_saved_product_count = 0;

正しい配置

// app_state.h
#ifndef APP_STATE_H
#define APP_STATE_H

extern int g_saved_product_count;

#endif
// app_state.c
#include "app_state.h"

int g_saved_product_count = 0;
// file_store.c
#include "app_state.h"

void increment_saved_count(void)
{
    g_saved_product_count++;
}

app_state.h を読み込んだ .c ファイルは、同じ1つの変数を参照します。

app_state.c
  int g_saved_product_count = 0;
  ↑ 実体は1つ

main.c / file_store.c
  extern int g_saved_product_count;
  ↑ 同じ実体を使う

extern を付けないとどうなるか

パターン1: 宣言がないまま使う

void increment_saved_count(void)
{
    g_saved_product_count++;
}

この .c から変数の宣言が見えていなければ、コンパイルエラーになります。

error: 'g_saved_product_count' undeclared

パターン2: ヘッダーファイルに実体を書いてしまう

// app_state.h
int g_saved_product_count = 0;

このヘッダーファイルを main.cfile_store.c の両方で読み込むと、変数の実体が複数作られます。

multiple definition of 'g_saved_product_count'

ヘッダーファイルには extern による宣言だけを書き、実体は .c に1回だけ書きます。

パターン3: 複数の .cstatic で書く

// main.c
static int g_saved_product_count = 0;
// file_store.c
static int g_saved_product_count = 0;

これはエラーになりません。

ただし、別々の変数が2個作られます

main.c の g_saved_product_count
file_store.c の g_saved_product_count

片方を増やしても、もう片方の値は変わりません。

static は「共有を防ぐ」、extern は「1つの実体を共有する」と覚えると整理しやすくなります。

グローバル変数は static を付けなくても生存期間が長い

ファイル直下に書いた変数は、static がなくてもプログラム開始から終了まで存在します。

int g_saved_product_count = 0;
static int s_saved_product_count = 0;

違いは、主に他の .c から参照できるかどうかです。

書き方 プログラム終了まで値が残る 他の .c から参照
int g_count = 0; 残る extern を使えばできる
static int s_count = 0; 残る できない

6. 完成例: 商品一覧を保存する

stock.h

#ifndef STOCK_H
#define STOCK_H

#define PRODUCT_NAME_LENGTH 32

typedef struct
{
    int id;
    char name[PRODUCT_NAME_LENGTH];
    int unit_price;
    int stock_count;
} Product;

void print_product(const Product *product);

#endif

stock.c

#include <stdio.h>

#include "stock.h"

static int normalize_stock_count(int stock_count)
{
    if (stock_count < 0)
    {
        return 0;
    }

    return stock_count;
}

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", normalize_stock_count(product->stock_count));
}

app_state.h

#ifndef APP_STATE_H
#define APP_STATE_H

extern int g_saved_product_count;

#endif

app_state.c

#include "app_state.h"

int g_saved_product_count = 0;

file_store.h

#ifndef FILE_STORE_H
#define FILE_STORE_H

#include <stddef.h>

#include "stock.h"

int save_products_to_file(
    const char *file_path,
    const Product products[],
    size_t product_count);

#endif

file_store.c

#include <stdio.h>

#include "app_state.h"
#include "file_store.h"

static int write_csv_header(FILE *file_pointer)
{
    return fprintf(file_pointer, "ID,商品名,単価,在庫数\n") >= 0;
}

int save_products_to_file(
    const char *file_path,
    const Product products[],
    size_t product_count)
{
    FILE *file_pointer = fopen(file_path, "w");

    if (file_pointer == NULL)
    {
        return 0;
    }

    if (!write_csv_header(file_pointer))
    {
        fclose(file_pointer);

        return 0;
    }

    g_saved_product_count = 0;

    for (size_t index = 0; index < product_count; index++)
    {
        if (fprintf(
                file_pointer,
                "%d,%s,%d,%d\n",
                products[index].id,
                products[index].name,
                products[index].unit_price,
                products[index].stock_count) < 0)
        {
            fclose(file_pointer);

            return 0;
        }

        g_saved_product_count++;
    }

    if (fclose(file_pointer) == EOF)
    {
        return 0;
    }

    return 1;
}

main.c

#include <stdio.h>

#include "app_state.h"
#include "file_store.h"
#include "stock.h"

int main(void)
{
    Product products[] = {
        { 101, "りんご", 120, 10 },
        { 102, "みかん", 100, 8 },
        { 103, "牛乳", 210, 5 }
    };
    size_t product_count = sizeof products / sizeof products[0];

    for (size_t index = 0; index < product_count; index++)
    {
        print_product(&products[index]);
        printf("\n");
    }

    if (!save_products_to_file("products.csv", products, product_count))
    {
        printf("products.csv を保存できませんでした。\n");

        return 1;
    }

    printf("%d件を products.csv へ保存しました。\n", g_saved_product_count);

    return 0;
}

コンパイルと実行

gcc -std=c17 -Wall -Wextra -Wpedantic \
    main.c stock.c file_store.c app_state.c \
    -o stock_app

./stock_app

実行後、products.csv が作成されます。

ID,商品名,単価,在庫数
101,りんご,120,10
102,みかん,100,8
103,牛乳,210,5

7. グローバル変数を使う判断基準

状況 第一候補
処理へ値を渡す 関数の引数
処理結果を返す 戻り値、出力用の構造体
複数の値をまとめて渡す 構造体
その .c 内部だけで状態を持つ static 変数
アプリ全体で本当に1つだけの状態 extern を使うグローバル変数

extern を使うグローバル変数は、次の場合に限定すると安全です。

  • アプリ全体の設定
  • 初期化済みかどうかの状態
  • 明確に1つだけである必要がある共有状態
  • 組み込み環境のハードウェアレジスタなど、設計上共有が避けられないもの

8. 理解確認

課題1: static 関数

stock.cnormalize_stock_count から static を外し、stock.h に関数宣言を追加してください。

その後、main.c から呼び出してみてください。

次に、static を戻し、main.c から呼び出す設計が不要か考えてください。

課題2: extern の確認

g_saved_product_count の実体を app_state.h へ移動し、main.cfile_store.c の両方で読み込んでください。

どのようなリンクエラーが出るか確認した後、元の正しい形へ戻してください。

課題3: 追記モード

save_products_to_file を変更し、既存の内容を消さずに追記する関数 append_product_to_file を作ってください。

ファイルを開く指定は "a" を使います。


まとめ

  • ファイル操作は、開く、確認する、読む・書く、閉じるの順に進める
  • .h には共有する型・関数宣言、.c には処理の実装を書く
  • .c#include せず、コンパイルコマンドへ列挙する
  • static 関数は、その .c 内だけで使える
  • static ローカル変数は、関数を抜けても値が残る
  • extern は実体を作らず、別の .c にある変数を参照する宣言
  • グローバル変数の実体は、必ず1つの .c にだけ書く
  • 複数の .c に同名の static 変数を書くと、共有されず別々の変数になる
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?