プログラミングを始めると必ず出会うのが変数です。
C言語では、変数は値に名前を付けてメモリ上に保管し、必要なときに読み書きするための基本単位です。
本記事では、宣言から初期化・代入・型選び、スコープや寿命までを、サンプルコードとともに丁寧に解説します。
C言語の変数とは
変数とは、メモリ上の領域に名前(識別子)を付けて値を格納する入れ物です。
C言語では必ず型を伴い、int
やdouble
などの型が値の表現範囲や演算の振る舞いを決めます。
変数は宣言によって型と名前を定め、初期化や代入によって値を設定します。
変数の宣言・初期化・代入の基本
宣言の書き方
基本の形
変数の宣言は「型 名称;」の形式です。
末尾のセミコロンは必須です。
#include <stdio.h>
int main(void) {
int x; // int型の変数xを宣言(この時点では中身は未定義)
x = 42; // 代入で値を設定
printf("x = %d\n", x);
return 0;
}
x = 42
識別子のルール
識別子(変数名)は英数字とアンダースコアからなり、先頭は英字かアンダースコアで始めます。
大文字小文字は区別されます。
複数宣言と同時初期化
同じ型ならカンマで複数を並べられ、同時に初期化もできます。
ただし1つの宣言内で型は共通で、ポインタの宣言は記号の結びつきに注意が必要です。
#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;
のような連鎖代入も可能です。
#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
)のオブジェクトはゼロ初期化されてから定数式で初期化されます。
#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)
未初期化の危険
ローカル変数(自動記憶域期間)は初期化されません。
読み出すと未定義動作になります。
#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
を付与)。
ポインタとの組み合わせも重要です。
#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>
で確認できます。
#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>
で取得できます。
丸め誤差に注意が必要です。
#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
配列です。
文字列リテラルは静的領域に置かれ書き換え不可です。
#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
)。
#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から連番です。
#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
は同じメモリ領域を別の型として再解釈します。
#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
共用体で最後に書き込んだメンバと異なるメンバを読む挙動は処理系定義です。
配列型
配列は連続した要素の集合です。
多くの式で先頭要素へのポインタに暗黙変換(配列→ポインタの崩壊)が起きます。
#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
ポインタ型
ポインタはアドレスを保持します。
配列とポインタは異なる型ですが密接に関係します。
算術演算で要素単位に移動できます。
#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)がない前提で最適化を促します。
#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書式例 |
---|---|---|---|---|
char | 8bit | 8bit | -128..127/0..255 | %c, %d |
short | 16bit | 16bit | 約±3e4 | %hd |
int | 32bit | 32bit | 約±2e9 | %d |
long | 32bit | 64bit | 約±9e18(LP64) | %ld |
long long | 64bit | 64bit | 約±9e18 | %lld |
size_t | 32bit | 64bit | 0.. | %zu |
ポインタ | 32bit | 64bit | アドレス幅 | %p |
サイズ確認の例:
#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>
のマクロが便利です。
#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では多くの式で暗黙の型変換が行われます。
整数同士の除算や、符号付き・符号なしの混在に注意します。
#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
を無理に外すキャストで本当に不変なオブジェクトを書き換えると未定義動作になります。
スコープ・寿命・記憶域クラス
スコープ
スコープはその名前が有効な範囲です。
ブロックスコープ、ファイルスコープ、関数プロトタイプスコープなどがあります。
#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): 各スレッドごとに独立。
#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宣言
複数ファイルで変数を共有するには外部変数を使います。
/* counter.c */
#include <stdio.h>
int counter = 0; // 定義(ストレージを確保)
void inc(void) { counter++; }
void show(void) { printf("counter=%d\n", counter); }
/* 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
は内部リンケージを与え、その翻訳単位内でのみ見えます。
モジュール内の隠蔽に使います。
/* module.c */
#include <stdio.h>
static int hidden = 42; // 他ファイルからは参照不可
int public_data = 7; // 外部リンケージ
void show_hidden(void) { printf("hidden=%d\n", hidden); }
/* 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では静的オブジェクトの初期化子は定数式に限られ、プログラム開始前に行われます。
#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
はスレッドごとに独立した変数を提供します。
各スレッドで同じ名前の変数が別々のインスタンスを持ちます。
/* 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>
を活用すると堅牢なプログラムになります。