閉じる

C言語の変数とは?宣言から初期化・代入・型までやさしく解説

プログラミングを始めると必ず出会うのが変数です。

C言語では、変数は値に名前を付けてメモリ上に保管し、必要なときに読み書きするための基本単位です。

本記事では、宣言から初期化・代入・型選び、スコープや寿命までを、サンプルコードとともに丁寧に解説します。

C言語の変数とは

変数とは、メモリ上の領域に名前(識別子)を付けて値を格納する入れ物です。

C言語では必ずを伴い、intdoubleなどの型が値の表現範囲や演算の振る舞いを決めます。

変数は宣言によって型と名前を定め、初期化や代入によって値を設定します。

変数の宣言・初期化・代入の基本

宣言の書き方

基本の形

変数の宣言は「型 名称;」の形式です。

末尾のセミコロンは必須です。

C言語
#include <stdio.h>

int main(void) {
    int x;          // int型の変数xを宣言(この時点では中身は未定義)
    x = 42;         // 代入で値を設定
    printf("x = %d\n", x);
    return 0;
}
実行結果
x = 42

識別子のルール

識別子(変数名)は英数字とアンダースコアからなり、先頭は英字かアンダースコアで始めます。

大文字小文字は区別されます。

複数宣言と同時初期化

同じ型ならカンマで複数を並べられ、同時に初期化もできます。

ただし1つの宣言内で型は共通で、ポインタの宣言は記号の結びつきに注意が必要です。

C言語
#include <stdio.h>

int main(void) {
    int a = 1, b = 2, c;   // 同じ型で複数宣言。a,bは同時初期化、cは未初期化
    c = a + b;

    int *p, q;             // pはintへのポインタ、qはintそのもの
    int value = 10;
    p = &value;            // pはアドレスを持つ
    q = value;             // qは値を持つ

    printf("a=%d, b=%d, c=%d\n", a, b, c);
    printf("*p=%d, q=%d\n", *p, q);
    return 0;
}
実行結果
a=1, b=2, c=3
*p=10, q=10
注意

int p, q;のような書き方では、は名前に結びつきます。

ポインタを複数宣言するときはint *p, *r;のように記述します。

代入演算子

代入は=で行い、算術と組み合わせた複合代入も使えます。

右結合なのでa = b = 0;のような連鎖代入も可能です。

C言語
#include <stdio.h>

int main(void) {
    int a = 10, b = 3;

    a += b;  // a = 13
    a *= 2;  // a = 26
    int c = a = b = 1; // 右から評価されb=1, a=1, c=1

    printf("a=%d, b=%d, c=%d\n", a, b, c);
    return 0;
}
実行結果
a=1, b=1, c=1

宣言時初期化と代入の違い

宣言と同時に値を与えるのが初期化、宣言後に値を入れるのが代入です。

見た目は似ていますが意味は異なります。

  • 配列やconst変数など、宣言時にしか行えない初期化があります。
  • 静的記憶域期間(ファイルスコープやstatic)のオブジェクトはゼロ初期化されてから定数式で初期化されます。
C言語
#include <stdio.h>

struct Point { int x, y; };

int main(void) {
    int x = 5;     // 初期化
    x = 7;         // 代入(前の値は上書き)

    int arr[3] = {1, 2, 3}; // 配列の初期化
    // arr = {4, 5, 6};     // エラー: 配列全体への代入は不可

    const int ci = 10; // constは宣言時に値が必要
    // ci = 20;        // エラー: 再代入不可

    struct Point p = { .x = 1, .y = 2 }; // 構造体の初期化
    printf("x=%d, arr[0]=%d, p=(%d,%d)\n", x, arr[0], p.x, p.y);
    return 0;
}
実行結果
x=7, arr[0]=1, p=(1,2)

未初期化の危険

ローカル変数(自動記憶域期間)は初期化されません

読み出すと未定義動作になります。

C言語
#include <stdio.h>

int main(void) {
    int y;                // 未初期化(中身は不定)
    printf("y = %d\n", y); // 未定義動作: 実行結果は保証されない
    return 0;
}

出力例(未定義なので環境により異なる):

実行結果
y = 32764

必ず初期化するか、使用前に確実に代入するようにしましょう。

-Wall -Wextra等の警告オプションの活用も有効です。

const変数の宣言

const「変更不可」を型に与える修飾子です。

Cではconst変数は原則コンパイル時定数ではありません(列挙子や#defineマクロは別)。

ファイルスコープのconstはCでは外部リンケージのままです(内部リンケージにしたい場合はstaticを付与)。

ポインタとの組み合わせも重要です。

C言語
#include <stdio.h>

int main(void) {
    const int ci = 42;   // 値は変更不可
    // ci = 7;           // エラー

    int v = 10;
    const int *pc = &v;  // 「先がconst」: *pcの変更不可、pcの付け替えは可
    // *pc = 20;         // エラー
    pc = &ci;            // OK

    int *const cp = &v;  // 「ポインタ自体がconst」: cpの付け替え不可、*cpの変更は可
    *cp = 77;            // OK
    // cp = &ci;         // エラー

    printf("v=%d, *pc=%d, *cp=%d\n", v, *pc, *cp);
    return 0;
}
実行結果
v=77, *pc=42, *cp=77

変数の型と選び方

整数型

整数型はsigned/unsigned char, short, int, long, long longなどがあります。

ビット幅は処理系依存ですが、最小幅は規格で保証されています。

範囲は<limits.h>で確認できます。

C言語
#include <stdio.h>
#include <limits.h>

int main(void) {
    printf("char:        %zu bytes, CHAR_MIN=%d, CHAR_MAX=%d\n", sizeof(char), CHAR_MIN, CHAR_MAX);
    printf("signed char: %zu bytes, SCHAR_MIN=%d, SCHAR_MAX=%d\n", sizeof(signed char), SCHAR_MIN, SCHAR_MAX);
    printf("short:       %zu bytes, SHRT_MIN=%d, SHRT_MAX=%d\n", sizeof(short), SHRT_MIN, SHRT_MAX);
    printf("int:         %zu bytes, INT_MIN=%d, INT_MAX=%d\n", sizeof(int), INT_MIN, INT_MAX);
    printf("long:        %zu bytes, LONG_MIN=%ld, LONG_MAX=%ld\n", sizeof(long), LONG_MIN, LONG_MAX);
    printf("long long:   %zu bytes, LLONG_MIN=%lld, LLONG_MAX=%lld\n", sizeof(long long), LLONG_MIN, LLONG_MAX);
    printf("unsigned:    0..%u\n", UINT_MAX);
    return 0;
}

出力例(典型的なLP64環境):

実行結果
char:        1 bytes, CHAR_MIN=-128, CHAR_MAX=127
signed char: 1 bytes, SCHAR_MIN=-128, SCHAR_MAX=127
short:       2 bytes, SHRT_MIN=-32768, SHRT_MAX=32767
int:         4 bytes, INT_MIN=-2147483648, INT_MAX=2147483647
long:        8 bytes, LONG_MIN=-9223372036854775808, LONG_MAX=9223372036854775807
long long:   8 bytes, LLONG_MIN=-9223372036854775808, LLONG_MAX=9223372036854775807
unsigned:    0..4294967295

浮動小数点型

float, double, long doubleがあり、精度や範囲は<float.h>で取得できます。

丸め誤差に注意が必要です。

C言語
#include <stdio.h>
#include <float.h>

int main(void) {
    float f = 1.0f / 3.0f;
    double d = 1.0 / 3.0;
    printf("float  1/3 = %.9f (FLT_DIG=%d)\n", f, FLT_DIG);
    printf("double 1/3 = %.17f (DBL_DIG=%d)\n", d, DBL_DIG);

    double a = 0.1, b = 0.2;
    printf("0.1 + 0.2 = %.17f\n", a + b); // 0.30000000000000004 など
    return 0;
}
実行結果
float  1/3 = 0.333333343 (FLT_DIG=6)
double 1/3 = 0.33333333333333331 (DBL_DIG=15)
0.1 + 0.2 = 0.30000000000000004

文字型(char)と文字列

charは1バイトの整数で、実装により符号あり/なしが異なることがあります。

Cの文字列は‘\0′(ヌル)終端のchar配列です。

文字列リテラルは静的領域に置かれ書き換え不可です。

C言語
#include <stdio.h>
#include <string.h>

int main(void) {
    char s1[] = "Hello";     // 書き換え可能な配列
    s1[0] = 'h';

    const char *s2 = "World"; // 文字列リテラル(書き換え禁止)を指す
    // s2[0] = 'w';           // エラーにすべき。未定義動作の原因

    printf("%s %s! (len=%zu)\n", s1, s2, strlen(s1));
    return 0;
}
実行結果
hello World! (len=5)

多言語文字にはUTF-8等のエンコーディングを用います。

コードポイント単位の処理が必要な場合は<wchar.h>wchar_tや、C11の<uchar.h>char16_t/char32_tも検討します。

真偽値

C99以降で<stdbool.h>を使うとbool, true, falseが利用できます(実体は_Bool)。

C言語
#include <stdio.h>
#include <stdbool.h>

int main(void) {
    bool ok = (5 > 3);
    printf("ok=%d, !ok=%d\n", ok, !ok); // printfでは整数(0/1)として出力
    return 0;
}
実行結果
ok=1, !ok=0

列挙型

列挙型は意味のある整数定数の集合を定義します。

列挙子は整数で、指定がなければ0から連番です。

C言語
#include <stdio.h>

enum Color { RED = 1, GREEN, BLUE }; // GREEN=2, BLUE=3

int main(void) {
    enum Color c = GREEN;
    printf("c=%d, RED=%d, GREEN=%d, BLUE=%d\n", c, RED, GREEN, BLUE);
    return 0;
}
実行結果
c=2, RED=1, GREEN=2, BLUE=3

構造体(struct)と共用体

structは複数の値を一つにまとめ、unionは同じメモリ領域を別の型として再解釈します。

C言語
#include <stdio.h>
#include <string.h>

struct Person {
    char name[16];
    int age;
};

union Number {
    int i;
    float f;
};

int main(void) {
    struct Person p = { "Taro", 20 };
    printf("Person{name=\"%s\", age=%d}, sizeof=%zu\n", p.name, p.age, sizeof p);

    union Number n;
    n.i = 0x3f800000; // IEEE754でおおよそ1.0fに相当するビットパターン
    printf("as int=0x%x, as float=%f, sizeof=%zu\n", n.i, n.f, sizeof n);
    return 0;
}
実行結果
Person{name="Taro", age=20}, sizeof=24
as int=0x3f800000, as float=1.000000, sizeof=4
注意

共用体で最後に書き込んだメンバと異なるメンバを読む挙動は処理系定義です。

配列型

配列は連続した要素の集合です。

多くの式で先頭要素へのポインタに暗黙変換(配列→ポインタの崩壊)が起きます。

C言語
#include <stdio.h>

size_t count_via_param(int a[], size_t n) {
    // ここでのaはint*に等価。sizeof(a)はポインタのサイズになる
    return n;
}

int main(void) {
    int a[] = {1,2,3,4};
    size_t n = sizeof a / sizeof a[0];
    printf("length=%zu\n", n);
    printf("length via param=%zu\n", count_via_param(a, n));

    int m[2][3] = {{1,2,3}, {4,5,6}}; // 二次元配列
    printf("m[1][2]=%d\n", m[1][2]);
    return 0;
}
実行結果
length=4
length via param=4
m[1][2]=6

ポインタ型

ポインタはアドレスを保持します。

配列とポインタは異なる型ですが密接に関係します。

算術演算で要素単位に移動できます。

C言語
#include <stdio.h>

int main(void) {
    int arr[3] = {10, 20, 30};
    int *p = arr;  // arrは式中で&arr[0]に崩壊
    printf("*p=%d, *(p+1)=%d, *(p+2)=%d\n", *p, *(p+1), *(p+2));
    printf("p=%p, p+1=%p\n", (void*)p, (void*)(p+1)); // アドレスは要素サイズ分進む
    return 0;
}
実行結果
*p=10, *(p+1)=20, *(p+2)=30
p=0x7ffcc0012340, p+1=0x7ffcc0012344

型修飾子

signed/unsigned/short/longは整数のサイズや符号を、constは不変性を、volatileは最適化抑制(メモリマップトI/O等)を、restrictは別名(alias)がない前提で最適化を促します。

C言語
#include <stddef.h>

void add_arrays(size_t n,
                int * restrict dst,
                const int * restrict a,
                const int * restrict b) {
    for (size_t i = 0; i < n; ++i) {
        dst[i] = a[i] + b[i];
    }
}
/* restrictを満たすよう、dst/a/bが重ならないように呼び出します。*/

サイズと範囲

処理系によりデータモデル(ILP32/LP64など)は異なり、サイズも変わります。

実際の環境ではsizeofで確認してください。

以下は代表的な目安です。

ILP32の典型サイズLP64の典型サイズ主な範囲(符号付き)printf書式例
char8bit8bit-128..127/0..255%c, %d
short16bit16bit約±3e4%hd
int32bit32bit約±2e9%d
long32bit64bit約±9e18(LP64)%ld
long long64bit64bit約±9e18%lld
size_t32bit64bit0..%zu
ポインタ32bit64bitアドレス幅%p

サイズ確認の例:

C言語
#include <stdio.h>

int main(void) {
    printf("sizeof(int)=%zu, sizeof(long)=%zu, sizeof(void*)=%zu\n",
           sizeof(int), sizeof(long), sizeof(void*));
    return 0;
}

出力例(LP64):

実行結果
sizeof(int)=4, sizeof(long)=8, sizeof(void*)=8

64bit整数を確実に出力するには<inttypes.h>のマクロが便利です。

C言語
#include <stdio.h>
#include <inttypes.h>

int main(void) {
    int64_t v = 1234567890123;
    printf("int64_t = %" PRId64 "\n", v);
    return 0;
}
実行結果
int64_t = 1234567890123

型変換

Cでは多くの式で暗黙の型変換が行われます。

整数同士の除算や、符号付き・符号なしの混在に注意します。

C言語
#include <stdio.h>
#include <limits.h>

int main(void) {
    int a = 1, b = 2;
    double r1 = a / b;          // 整数同士の除算 → 0
    double r2 = (double)a / b;  // 片方をdoubleに変換 → 0.5
    printf("r1=%.1f, r2=%.1f\n", r1, r2);

    unsigned int u = 4000000000u;
    int s = -1;
    // 比較ではsがunsignedに変換されるため予想外の結果になることがある
    printf("s < u ? %s\n", (s < u) ? "true" : "false");

    // 明示的キャストは慎重に
    signed char sc = (signed char)200; // 実装依存の結果
    printf("cast to signed char: %d\n", sc);
    return 0;
}
実行結果
r1=0.0, r2=0.5
s < u ? false
cast to signed char: -56
重要

constを無理に外すキャストで本当に不変なオブジェクトを書き換えると未定義動作になります。

スコープ・寿命・記憶域クラス

スコープ

スコープはその名前が有効な範囲です。

ブロックスコープ、ファイルスコープ、関数プロトタイプスコープなどがあります。

C言語
#include <stdio.h>

int main(void) {
    int x = 10;         // 外側のx
    {
        int x = 20;     // 内側のx(外側を隠す)
        printf("inner x=%d\n", x);
    }
    printf("outer x=%d\n", x);
    return 0;
}
実行結果
inner x=20
outer x=10

寿命

寿命(ストレージ期間)はオブジェクトが存在する期間です。

  • 自動(automatic): ブロックに入ると生成、出ると消滅。ローカル変数。
  • 静的(static): プログラム開始から終了まで。ファイルスコープやstatic修飾。
  • 動的(動的確保): malloc/free等で手動管理。
  • スレッド局所(thread): 各スレッドごとに独立。
C言語
#include <stdio.h>
#include <stdlib.h>

int *leak_example(void) {
    int *p = malloc(sizeof *p); // 動的確保(呼び出し側でfreeが必要)
    *p = 123;
    return p;
}

int *dangling_pointer(void) {
    int local = 42;
    // return &local; // エラー: ローカルの寿命終了後はダングリングポインタ
    return NULL;
}

int main(void) {
    int *p = leak_example();
    printf("*p=%d\n", *p);
    free(p); // 解放を忘れない
    return 0;
}
実行結果
*p=123

記憶域クラス

記憶域クラス指定子としてauto, register, static, extern, C11の_Thread_localがあります。

  • auto: 既定のローカル変数。通常は明示不要。
  • register: 最適化ヒント(現代では効果はほぼない)。
  • static: 静的記憶域期間を与えるか、リンケージ(内部)を制御。
  • extern: 別の翻訳単位にある定義を参照。
  • _Thread_local: スレッドごとに独立したオブジェクト。

外部変数とextern宣言

複数ファイルで変数を共有するには外部変数を使います。

C言語
/* counter.c */
#include <stdio.h>

int counter = 0;      // 定義(ストレージを確保)
void inc(void) { counter++; }
void show(void) { printf("counter=%d\n", counter); }
C言語
/* main.c */
#include <stdio.h>

extern int counter;   // 参照用の宣言(定義ではない)
void inc(void);
void show(void);

int main(void) {
    inc();
    inc();
    show();
    printf("mainからも参照: counter=%d\n", counter);
    return 0;
}
実行結果
counter=2
mainからも参照: counter=2

内部リンケージ(static)とファイルスコープ

ファイルスコープのstatic内部リンケージを与え、その翻訳単位内でのみ見えます。

モジュール内の隠蔽に使います。

C言語
/* module.c */
#include <stdio.h>

static int hidden = 42; // 他ファイルからは参照不可
int public_data = 7;    // 外部リンケージ

void show_hidden(void) { printf("hidden=%d\n", hidden); }
C言語
/* other.c */
extern int public_data;
// extern int hidden; // リンクエラー: hiddenは内部リンケージ

int main(void) {
    extern void show_hidden(void);
    show_hidden();
    printf("public_data=%d\n", public_data);
    return 0;
}
実行結果
hidden=42
public_data=7

ローカルstaticと初期化タイミング

ブロック内のstatic変数は静的記憶域期間を持ち、関数をまたいで値を保持します。

Cでは静的オブジェクトの初期化子は定数式に限られ、プログラム開始前に行われます。

C言語
#include <stdio.h>

void counter(void) {
    static int c = 0; // 初回は0で初期化。以降は値を保持
    c++;
    printf("c=%d\n", c);
}

int main(void) {
    counter();
    counter();
    counter();
    return 0;
}
実行結果
c=1
c=2
c=3

スレッド局所

C11の_Thread_localスレッドごとに独立した変数を提供します。

各スレッドで同じ名前の変数が別々のインスタンスを持ちます。

C言語
/* C11スレッドの例 */
#include <stdio.h>
#include <threads.h>    // C11
#include <stdlib.h>

_Thread_local int tls_var = 0; // スレッド局所

int thread_func(void *arg) {
    int id = *(int*)arg;
    tls_var = id; // 各スレッドが独自に値を設定
    printf("thread %d: tls_var=%d, &tls_var=%p\n", id, tls_var, (void*)&tls_var);
    return 0;
}

int main(void) {
    thrd_t t1, t2;
    int a = 1, b = 2;
    thrd_create(&t1, thread_func, &a);
    thrd_create(&t2, thread_func, &b);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);
    printf("main thread: tls_var=%d, &tls_var=%p\n", tls_var, (void*)&tls_var);
    return 0;
}
実行結果アドレスは各スレッドで異なる
thread 1: tls_var=1, &tls_var=0x7f8b8d5fe6dc
thread 2: tls_var=2, &tls_var=0x7f8b8cdfd6dc
main thread: tls_var=0, &tls_var=0x7ffd3b1b26dc
実装メモ

一部の環境では<threads.h>のサポートが弱い場合があります。

その際はPOSIXのpthreadとコンパイラ拡張の__threadなどを検討します。

まとめ

本記事では、C言語の変数について、宣言・初期化・代入の基本から、型の選び方スコープと寿命記憶域クラスまでを体系的に解説しました。

特に、未初期化の危険や配列とポインタの違い、符号付きと符号なしの混在などは、実際の不具合につながりやすい要点です。

日々のコーディングでは「必ず初期化」「型と範囲を意識」「constで意図を明示」「スコープを最小に」を心がけてください。

サイズや挙動が処理系依存となる部分はsizeof<limits.h>/<float.h>で確認し、必要に応じて<stdint.h>/<inttypes.h>を活用すると堅牢なプログラムになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!