C言語のプログラムは、変数に「どのくらいの大きさの値を入れるのか」「どの範囲と精度が必要なのか」を型で約束しながら書き進めます。
本稿では、整数・浮動小数点・文字・真偽値の基本から、サイズや範囲の確認方法、32bit/64bit差、printfの書式、型変換と安全な選び方まで、入門者向けに丁寧に解説します。
C言語のデータ型の基本とサイズ
データ型の役割とビット幅
C言語のデータ型は、メモリ上の値の解釈方法を決めます。
例えば整数型は2の補数表現(多くの処理系で採用)により負数も扱え、浮動小数点型はIEEE 754(多くの処理系)で小数を近似します。
ビット幅が大きくなるほど表現できる範囲は広がりますが、必要以上に大きい型はメモリやキャッシュ効率、演算速度に影響する場合があります。
従って、用途に応じて「十分な範囲」「必要な精度」を満たす最小限の型を選ぶのが基本です。
sizeofとlimits.h/float.hで範囲確認
コンパイル環境での実際のサイズや範囲は、sizeof
演算子やヘッダ定数で確認できます。
次のサンプルは代表的な型のサイズと最小値・最大値を表示します。
// 型のサイズと範囲を調べるサンプル
// コンパイル例: gcc -std=c11 -Wall -Wextra -O2 types_info.c -o types_info
#include <stdio.h>
#include <limits.h> // 整数の範囲(CHAR_MIN, INT_MAX など)
#include <float.h> // 浮動小数点の範囲(FLT_MIN, DBL_MAX など)
#include <stdint.h> // SIZE_MAX など
#include <stddef.h> // ptrdiff_t
int main(void) {
printf("== サイズ(バイト) ==\n");
printf("char : %zu (CHAR_BIT=%d)\n", sizeof(char), CHAR_BIT);
printf("signed char : %zu\n", sizeof(signed char));
printf("unsigned char : %zu\n", sizeof(unsigned char));
printf("short : %zu\n", sizeof(short));
printf("int : %zu\n", sizeof(int));
printf("long : %zu\n", sizeof(long));
printf("long long : %zu\n", sizeof(long long));
printf("size_t : %zu\n", sizeof(size_t));
printf("ptrdiff_t : %zu\n", sizeof(ptrdiff_t));
printf("void* : %zu\n", sizeof(void*));
printf("float : %zu\n", sizeof(float));
printf("double : %zu\n", sizeof(double));
printf("long double : %zu\n\n", sizeof(long double));
printf("== 範囲(整数) ==\n");
printf("char (plain) : %d .. %d\n", CHAR_MIN, CHAR_MAX); // 実装依存(符号付き/なし)
printf("signed char : %d .. %d\n", SCHAR_MIN, SCHAR_MAX);
printf("unsigned char : %u .. %u\n", 0u, UCHAR_MAX);
printf("short : %d .. %d\n", SHRT_MIN, SHRT_MAX);
printf("unsigned short : %u .. %u\n", 0u, USHRT_MAX);
printf("int : %d .. %d\n", INT_MIN, INT_MAX);
printf("unsigned int : %u .. %u\n", 0u, UINT_MAX);
printf("long : %ld .. %ld\n", LONG_MIN, LONG_MAX);
printf("unsigned long : %lu .. %lu\n", 0ul, ULONG_MAX);
printf("long long : %lld .. %lld\n", LLONG_MIN, LLONG_MAX);
printf("unsigned long long: %llu .. %llu\n", 0ull, ULLONG_MAX);
printf("size_t (max) : 0 .. %zu\n", SIZE_MAX);
printf("ptrdiff_t : %td .. %td\n\n", (ptrdiff_t)PTRDIFF_MIN, (ptrdiff_t)PTRDIFF_MAX);
printf("== 範囲(浮動小数点, 正規化数の最小) ==\n");
printf("float : min=%.9g max=%.9g (digits=%d)\n", FLT_MIN, FLT_MAX, FLT_DIG);
printf("double : min=%.17g max=%.17g (digits=%d)\n", DBL_MIN, DBL_MAX, DBL_DIG);
printf("long double : min=%Lg max=%Lg (digits=%d)\n", LDBL_MIN, LDBL_MAX, LDBL_DIG);
printf("epsilon : float=%g double=%g long double=%Lg\n", FLT_EPSILON, DBL_EPSILON, LDBL_EPSILON);
return 0;
}
== サイズ(バイト) ==
char : 1 (CHAR_BIT=8)
signed char : 1
unsigned char : 1
short : 2
int : 4
long : 4
long long : 8
size_t : 8
ptrdiff_t : 8
void* : 8
float : 4
double : 8
long double : 16
== 範囲(整数) ==
char (plain) : -128 .. 127
signed char : -128 .. 127
unsigned char : 0 .. 255
short : -32768 .. 32767
unsigned short : 0 .. 65535
int : -2147483648 .. 2147483647
unsigned int : 0 .. 4294967295
long : -2147483648 .. 2147483647
unsigned long : 0 .. 4294967295
long long : -9223372036854775808 .. 9223372036854775807
unsigned long long: 0 .. 18446744073709551615
size_t (max) : 0 .. 18446744073709551615
ptrdiff_t : -9223372036854775808 .. 9223372036854775807
== 範囲(浮動小数点, 正規化数の最小) ==
float : min=1.17549435e-38 max=3.40282347e+38 (digits=6)
double : min=2.2250738585072014e-308 max=1.7976931348623157e+308 (digits=15)
long double : min=1.06581e-312 max=1.06581e-312 (digits=18)
epsilon : float=1.19209e-07 double=2.22045e-16 long double=1.06581e-312
32bit/64bitの違いと環境依存
Cの型サイズは処理系に依存します。
代表的なデータモデルは次の通りです。
- ILP32(多くの32bit Unix系): int/long/ポインタのうち、int=32bit, long=32bit, ポインタ=32bit
- LP64(多くの64bit Unix系: Linux, macOS): int=32bit, long=64bit, ポインタ=64bit
- LLP64(64bit Windows/MSVC): int=32bit, long=32bit, long long=64bit, ポインタ=64bit
ILP32 では、int
・long
・ポインタはいずれも 32bit 幅を持ちます。
これは 32bit CPU 上の Unix 系 OS で一般的に使われるデータモデルです。
メモリ空間が 4GB に制限されるため、大規模データ処理には不利です。
LP64 では int
は 32bit のままですが、long
とポインタは 64bit に拡張されます。
これにより、大きなメモリ空間を扱える一方で、long
のサイズが 64bit になる点に注意が必要です。
Linux や macOS の 64bit 環境で広く採用されています。
LLP64 では int
と long
は 32bit のままに保たれ、long long
とポインタが 64bit になります。
これは Windows が C 言語の long
の互換性を重視した設計によるものです。
64bit Windows 環境での Visual C++ で採用されています。
次の表は典型的なサイズ(バイト)の目安です。
データモデル | short | int | long | long long | void* | size_t |
---|---|---|---|---|---|---|
ILP32(32bit) | 2 | 4 | 4 | 8 | 4 | 4 |
LP64(Unix系64bit) | 2 | 4 | 8 | 8 | 8 | 8 |
LLP64(Windows 64bit) | 2 | 4 | 4 | 8 | 8 | 8 |
同じソースでも、long
のサイズがOSで違うなどの差があり得ます。
実際の値は前節のプログラムやsizeof
で必ず確認するのが安全です。
基本の型グループ(整数/浮動小数点/文字)
- 整数型:
signed/unsigned char, short, int, long, long long
など。個数やインデックス、ビットフラグに向いています。 - 浮動小数点型:
float, double, long double
。測定値、割合、統計量などの小数に使います。 - 文字・文字列:
char
(1バイト文字の単体)、char[]
(ヌル終端文字列)。テキスト処理の基本です。
整数型の種類(int/short/long)と選び方
符号付きとunsignedの違い
符号付きは負の値を表現できますが、符号なしは0以上の値のみを表します。
表現範囲はビット数に比例し、符号なしの方が最大値は約2倍になります。
負数を取り得ないカウントやビット演算にはunsigned
が適しますが、符号付きと符号なしを混在させた比較や演算は意図しない型変換を招くため注意が必要です。
特に符号付き整数のオーバーフローは未定義動作です。
long longや大きな整数の扱い
32bit環境ではlong
が32bitのことが多く、64bitの整数が必要ならlong long
を使います。
リテラルにはLL
(例: 1234567890123LL
)を付け、printf
では%lld
(符号付き)や%llu
(符号なし)を使います。
環境差を避ける目的で、正確なビット幅が必要な場合は後述のstdint.h
の固定幅整数型を優先します。
固定幅整数型(stdint.h)の利点
stdint.h
はビット幅が明確な型を提供します。
例として、int32_t
は常に32bitの符号付き整数、uint64_t
は64bitの符号なし整数です。
これにより、ファイル形式や通信プロトコルなどで「何bitか」が仕様の一部である場合に安全です。
printf
にはinttypes.h
のマクロ(例: PRId32
, PRIu64
)を使うと移植性が高まります。
// 固定幅整数とprintfマクロの例
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
int32_t a = INT32_MAX;
uint64_t b = 12345678901234567890ull;
printf("a(int32_t) = %" PRId32 "\n", a);
printf("b(uint64_t) = %" PRIu64 "\n", b);
return 0;
}
出力例:
a(int32_t) = 2147483647
b(uint64_t) = 12345678901234567890
size_t/ptrdiff_tの使いどころ
メモリサイズや配列の添字にはsize_t
(非負の大きな整数)を使います。
ポインタ同士の減算結果にはptrdiff_t
(符号付き)が用いられます。
これらはアーキテクチャに合わせて十分な幅を持つため、int
で代用しない方が安全です。
// size_t と ptrdiff_t の基本
#include <stdio.h>
#include <stddef.h>
int main(void) {
int a[10] = {0};
int *p = &a[2];
int *q = &a[8];
size_t n = sizeof(a) / sizeof(a[0]); // 要素数
ptrdiff_t d = q - p; // 要素単位の距離
printf("配列の要素数(size_t): %zu\n", n);
printf("q - p の距離(ptrdiff_t): %td\n", d);
return 0;
}
出力例:
配列の要素数(size_t): 10
q - p の距離(ptrdiff_t): 6
printfの書式(%d/%u/%ld)と整数リテラル
整数の主な書式指定子は次の通りです。
型に合わない指定子は未定義動作になり得るため、必ず合わせます。
型 | 書式 | 例 |
---|---|---|
int | %d | printf(“%d”, (int)x); |
unsigned int | %u | printf(“%u”, (unsigned)x); |
long | %ld / %lu | printf(“%ld”, l); |
long long | %lld / %llu | printf(“%lld”, ll); |
short | %hd / %hu | printf(“%hd”, s); |
size_t | %zu | printf(“%zu”, n); |
ptrdiff_t | %td | printf(“%td”, d); |
固定幅(int32_t等) | %”PRId32″ 等 | printf(“%” PRId32, v); |
リテラルの接尾辞も重要です。
例 | 意味 |
---|---|
42U | unsigned int |
3000000000UL | unsigned long |
10LL / 10ULL | long long / unsigned long long |
0xFFu | 16進のunsigned int |
‘A’ | 文字リテラル(型はint) |
// 整数のリテラルとprintfの組み合わせ例
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
int i = 42;
unsigned u = 42U;
long l = 100000L;
long long ll = 10000000000LL;
int32_t i32 = -123;
uint64_t u64 = 18446744073709551615ull;
printf("int=%d, unsigned=%u, long=%ld, long long=%lld\n", i, u, l, ll);
printf("i32=%" PRId32 ", u64=%" PRIu64 "\n", i32, u64);
return 0;
}
int=42, unsigned=42, long=100000, long long=10000000000
i32=-123, u64=18446744073709551615
浮動小数点・文字・真偽値のデータ型
float/double/long doubleの選び方
double
はデフォルトの小数型として推奨されます。精度(約15~17桁)と速度のバランスが良好です。float
はメモリや帯域が厳しい場合や大量配列での省メモリ化に有効ですが、精度(約6~9桁)は低下します。long double
は更に高精度ですが、処理系によりdouble
と同じ場合もあります(MSVC)。必要性が明確なときに選びます。
リテラルでは1.23f
がfloat
、1.23
がdouble
、1.23L
がlong double
です。
charとsigned/unsigned charの違い
char
は文字の格納に使いますが、char
が符号付きかどうかは実装依存です。
一方、signed char
とunsigned char
は明確に別型で、バイト列処理で符号問題を避けるにはunsigned char
が便利です。
テキストとして扱うときはchar
(プレーン)を使い、数値として計算するならsigned/unsigned char
を明示します。
ビット数はCHAR_BIT
で取得します(典型的には8)。
文字列(char配列)の基本
Cの文字列は'\0'
(ヌル文字)で終端されたchar
配列です。
配列の長さは終端を含めて十分に確保し、書き込みは境界を超えないように注意します。
// 文字・文字列の基本
#include <stdio.h>
#include <string.h>
int main(void) {
char c = 'C'; // 文字(型はcharだが、文字リテラルはintとして扱われる場面もある)
const char *p = "C language"; // 文字列リテラルは静的領域(書き換え禁止)
char s[16]; // 十分な大きさのバッファを用意
// 安全なコピー: snprintfは終端も保証
snprintf(s, sizeof(s), "%s", p);
printf("char: %c, 文字コード: %d\n", c, (int)c);
printf("文字列: %s (長さ: %zu)\n", s, strlen(s));
return 0;
}
出力例:
char: C, 文字コード: 67
文字列: C language (長さ: 10)
_Boolとstdbool.hのbool
C99では_Bool
が導入され、stdbool.h
をインクルードするとbool
/true
/false
を使えるようになります。
bool
は整数型の一種で、printf
では%d
で出力します。
#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool a = true;
bool b = (5 > 10);
printf("a=%d, b=%d\n", a, b); // 1と0で表示
return 0;
}
出力例:
a=1, b=0
printfの書式(%f/%c/%s)
浮動小数点の主な書式は%f
(固定小数)、%e
(指数)、%g
(適応)です。
long double
には%Lf
等を使います。
%c
は文字、%s
は文字列です。
// 浮動小数点と文字/文字列のprintf書式
#include <stdio.h>
int main(void) {
float f = 3.1415926f;
double d = 2.718281828459045;
long double ld = 1.234567890123456789L;
char ch = 'A';
const char *str = "hello";
printf("float %%f=%.3f %%e=%.3e %%g=%.3g\n", f, f, f);
printf("double %%f=%.6f %%e=%.6e %%g=%.6g\n", d, d, d);
printf("long double %%Lf=%.9Lf\n", ld);
printf("char= %c, string= %s\n", ch, str);
return 0;
}
出力例:
float %f=3.142 %e=3.142e+00 %g=3.14
double %f=2.718282 %e=2.718282e+00 %g=2.71828
long double %Lf=1.234567890
char= A, string= hello
型変換と安全な選び方ガイド
代入と演算の型変換(整数昇格)
Cでは、char
やshort
で演算すると自動でint
に昇格(整数昇格)し、両辺の型に応じて「通常の算術変換」が行われます。
符号付きと符号なしを混ぜると、符号なし側に引っ張られて意図しない結果になることがあるため注意します。
// 符号混在と整数昇格の落とし穴
#include <stdio.h>
int main(void) {
int i = -1;
unsigned int u = 1u;
// 比較ではuがunsignedのため、iがunsignedに変換されて非常に大きな値になる
printf("i < u ? %d\n", (i < u)); // 多くの処理系で0(false)
signed char sc1 = 100, sc2 = 50;
printf("sc1 + sc2 = %d\n", sc1 + sc2); // intに昇格して計算
unsigned char uc = 250;
signed char sc = 10;
// uc(250) + sc(10)=260 -> 昇格後intで計算。格納時の型で再び丸め/オーバーフローに注意
int sum = uc + sc;
printf("uc + sc = %d\n", sum);
return 0;
}
出力例:
i < u ? 0
sc1 + sc2 = 150
uc + sc = 260
明示的キャストの使い方
キャストは型を強制変換しますが、情報が失われたり未定義動作を見えにくくしたりするため、必要最低限にします。
安全なキャストは「境界チェック」を伴います。
// double -> int への安全なキャスト(飽和)
#include <stdio.h>
#include <limits.h>
int safe_cast_double_to_int(double x, int *out) {
if (x > (double)INT_MAX) { *out = INT_MAX; return 1; } // 1: 飽和
if (x < (double)INT_MIN) { *out = INT_MIN; return 1; }
*out = (int)x; // 範囲内ならキャスト
return 0; // 0: 正常
}
int main(void) {
double v1 = 1e20, v2 = -1e20, v3 = 123.9;
int r;
safe_cast_double_to_int(v1, &r); printf("v1->%d\n", r);
safe_cast_double_to_int(v2, &r); printf("v2->%d\n", r);
safe_cast_double_to_int(v3, &r); printf("v3->%d\n", r);
return 0;
}
出力例:
v1->2147483647
v2->-2147483648
v3->123
オーバーフロー/丸めの回避
符号付き整数のオーバーフローは未定義動作です。
加算・乗算の前に限界をチェックするか、より広い型で計算し、結果が範囲内か確かめてから狭い型に格納します。
// intの安全な加算チェック
#include <stdio.h>
#include <limits.h>
int add_int_safe(int a, int b, int *out) {
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) {
return 1; // オーバーフロー
}
*out = a + b;
return 0;
}
int main(void) {
int r;
if (add_int_safe(INT_MAX, 1, &r)) {
printf("オーバーフロー検出\n");
}
if (!add_int_safe(100, 23, &r)) {
printf("100 + 23 = %d\n", r);
}
return 0;
}
出力例:
オーバーフロー検出
100 + 23 = 123
浮動小数点では丸め誤差が避けられません。
比較は「許容誤差(epsilon)」を設けます。
// 浮動小数点の近似比較
#include <stdio.h>
#include <float.h>
#include <math.h>
int nearly_equal(double a, double b, double eps) {
double diff = fabs(a - b);
double scale = fmax(1.0, fmax(fabs(a), fabs(b)));
return diff <= eps * scale;
}
int main(void) {
double x = 0.1 + 0.2; // 0.30000000000000004 になることが多い
printf("x=%.17f, 0.3=%.17f, equal? %d\n", x, 0.3, nearly_equal(x, 0.3, 10 * DBL_EPSILON));
return 0;
}
出力例:
x=0.30000000000000004, 0.3=0.29999999999999999, equal? 1
精度・速度・メモリのバランス
- 範囲が明確なら固定幅整数(
int32_t
等)で仕様を固定化します。汎用計算にはint
が最適化されやすい傾向があります。 - 大規模データの配列では
float
やuint16_t
などの軽量型でメモリ帯域を節約できますが、精度や上限に余裕があるかを事前に検証します。 - 64bit環境では
long
が64bit(LP64)の場合、インデックスやサイズはsize_t
のまま扱い、long
に安易にキャストしない方が安全です。 - 性能が問題になる箇所は、型選択がキャッシュ効率やSIMDに与える影響も考慮します。
初心者向けチェックリスト
- 値が負になる可能性があるかどうかで
signed
かunsigned
を決めます。 - 配列の長さやメモリサイズは
size_t
、ポインタ差はptrdiff_t
を使います。 - 仕様でビット幅が決まるデータは
stdint.h
の固定幅整数を使います。 printf
の書式指定子は型に合わせ、固定幅にはinttypes.h
のマクロを使います。- 32bit/64bit差を意識し、
long
のサイズ差に注意します。 - 浮動小数点の比較は許容誤差を使います。
- キャストは必要最小限に留め、境界チェックを伴います。
- 範囲やサイズに迷ったら
sizeof
とlimits.h
/float.h
で確認します。
まとめ
C言語の型は、プログラムの正しさと効率を左右する重要な設計要素です。
まずは実行環境ごとのサイズと範囲をsizeof
やlimits.h
/float.h
で押さえ、整数・浮動小数点・文字の基本特性を理解しましょう。
整数は符号とオーバーフロー、浮動小数点は丸め誤差、文字列は終端とバッファサイズが肝要です。
移植性が求められる場面では固定幅整数とinttypes.h
の書式マクロを活用し、サイズやポインタ関連はsize_t
/ptrdiff_t
を用いて安全に扱います。
最後に、型変換の自動規則を理解し、必要に応じて明示的なチェックとキャストで守りを固めることで、環境に依存せず堅牢なCプログラムを組み立てられるようになります。