2026年8月7日に加筆・統合しました
もともと初級(前編・後編)・中級・上級の4本に分けていた記事を、1本にまとめて読めるようにしました。あわせてコードが正しく表示されていなかった箇所を修正しています(#include の後ろのヘッダ名や、i < 5 の不等号が消えていました)。
この記事について
C言語は「難しい」と言われます。実際、最近の言語が自動でやってくれることを自分で書く必要があります。ただ、いま使われている多くの言語のもとになっているので、ここを通っておくと後がずいぶん楽になります。
この記事では、環境を用意するところから、C言語らしさが出てくるポインタ・動的メモリ・構造体までを一度に扱います。上から順に読めば動くものが書けるようになる、という並びにしました😊
C言語とは
1972年にベル研究所で作られた汎用のプログラミング言語です。OSやミドルウェアといった土台に近いところで広く使われています。
いまでも現役なのは、メモリをどう使うかを自分で決められるからです。限られたメモリで動かす組み込み機器や、1バイトの無駄も惜しい処理では、この「自分で決められる」が効いてきます。その代わり、決めるのを間違えるとそのまま不具合になります。C言語が難しいと言われるのは、面倒を見てくれる範囲が狭いぶん、書く側の責任が大きいからです。
開発環境を用意する
C言語はコンパイル(人が書いた文字を、機械が実行できる形に翻訳すること)が必要な言語です。翻訳するためのコンパイラを用意します。
| OS | 用意するもの |
|---|---|
| Windows | MinGW(gcc)または Visual Studio の MSVC |
| Mac | Xcode Command Line Tools(xcode-select --install で入る) |
| Linux | gcc(多くのディストリビューションで最初から入っている) |
用意できたら、ファイルを hello.c という名前で保存して、次のように翻訳・実行します。
gcc hello.c -o hello # hello.c を翻訳して hello という実行ファイルを作る
./hello # 実行する(Windowsは hello.exe)
Hello World
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
1行ずつ見ていきます。
#include <stdio.h>… 画面に文字を出すprintfを使うための宣言です。「この機能を使います」という申告だと思ってくださいint main()… プログラムはここから始まります。名前は必ずmainですprintf… 文字を出す命令。\nは改行を表しますreturn 0;… 正常に終わったことを伝えます。0以外は「何か問題があった」という意味になります
変数と定数
C言語では、変数を作るときに「どんな種類の値を入れるか」を必ず先に決めます。あとから別の種類は入れられません。
#include <stdio.h>
int main() {
int age = 25; // 整数
float height = 170.5f; // 小数
char grade = 'A'; // 1文字
const int MAX = 100; // 定数(あとから変えられない)
printf("年齢:%d\n", age);
printf("身長:%.1f\n", height);
printf("成績:%c\n", grade);
return 0;
}
| 型 | 入れられるもの | だいたいの大きさ |
|---|---|---|
int | 整数 | 4バイト(約±21億) |
float / double | 小数 | 4 / 8バイト |
char | 1文字(正体は小さい整数) | 1バイト |
printf の中の %d や %c は「ここに値を差し込む」という目印で、型ごとに使う記号が決まっています(整数は %d、小数は %f、1文字は %c、文字列は %s)。ここを間違えても翻訳は通ってしまい、実行したときに変な値が出ます。最初につまずきやすいところです😅
条件分岐
int x = 10;
if (x > 0) {
printf("正の数\n");
} else if (x == 0) {
printf("ゼロ\n");
} else {
printf("負の数\n");
}
// 値がとびとびに決まっているときは switch のほうが読みやすい
switch (x) {
case 1:
printf("one\n");
break; // ★break を忘れると次の case に流れ込む
case 10:
printf("ten\n");
break;
default:
printf("other\n");
}
⚠ switch の break 忘れbreak を書かないと、条件に合った場所から下の case へそのまま流れ込みます。わざとそうする書き方もあるのですが、たいていは書き忘れです。
繰り返し
// for文:回数が決まっているとき
for (int i = 0; i < 5; i++) {
printf("%d回目\n", i + 1);
}
// while文:条件が満たされる間ずっと
int n = 0;
while (n < 5) {
printf("n = %d\n", n);
n++; // ★これを忘れると無限ループになる
}
// do-while文:中身を必ず1回は実行したいとき
int m = 10;
do {
printf("最低1回は通る\n");
} while (m < 10);
3つとも「繰り返す」ための書き方ですが、使い分けの目安があります。
- for … 「5回」のように回数が決まっているとき
- while … 「条件が満たされる間」のように回数が決まっていないとき
- do-while … とにかく1回はやってから条件を見たいとき
関数
同じ処理を何度も書かずに済ませるための仕組みです。C言語の関数は戻り値の型を先に書くのが特徴です。
#include <stdio.h>
// 戻り値の型 関数名(引数の型 引数名, ...)
int add(int a, int b) {
return a + b;
}
// 戻り値がないときは void
void greet(const char* name) {
printf("こんにちは、%sさん\n", name);
}
int main() {
int result = add(3, 5);
printf("%d\n", result); // 8
greet("eight");
return 0;
}
配列
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// ★C言語は配列の範囲をチェックしない
// arr[10] と書いてもエラーにならず、関係ないメモリを読み書きしてしまう
⚠ 範囲の外に触っても止まらない
要素5個の配列に arr[10] と書いても、C言語はエラーにしません。関係ないメモリを読み書きして、しばらく動いてから急に落ちる、といった追いにくい不具合になります。範囲は自分で守る——これがC言語の基本姿勢です。
★ポインタ
ここがC言語の山場です。ただ、「値」ではなく「場所」を持っている変数だと分かってしまえば、そこまで複雑ではありません。
int a = 10;
int* p = &a; // & は「変数のアドレス」を取り出す
printf("%d\n", a); // 10 … 変数そのもの
printf("%p\n", p); // 0x… … a が置かれている場所(アドレス)
printf("%d\n", *p); // 10 … * は「その場所にある値」を取り出す
*p = 20; // ポインタ経由で値を書き換える
printf("%d\n", a); // 20 … a 自体が変わっている
記号が2つ出てきます。& は「住所を教えて」、* は「その住所に行って中身を見て」です。
では何の役に立つのか。いちばん分かりやすいのは、関数の中から呼び出し元の変数を書き換えられることです。
#include <stdio.h>
// 値を渡すと、関数の中で書き換えても呼び出し元は変わらない
void ng(int x) { x = 99; }
// アドレスを渡すと、呼び出し元の変数を書き換えられる
void ok(int* x) { *x = 99; }
int main() {
int a = 1;
ng(a);
printf("%d\n", a); // 1 … 変わらない
ok(&a);
printf("%d\n", a); // 99 … 変わった
return 0;
}
C言語は関数に値を渡すときコピーを渡します。だから ng の中でいくら書き換えても、呼び出し元の a は変わりません。住所を渡した ok だけが、本体に手を届かせることができます。
配列とポインタの関係も押さえておきます。配列の名前は、そのまま先頭の住所として扱えます。
int arr[] = {1, 2, 3, 4, 5};
int* p = arr; // 配列名はそのまま先頭のアドレスとして扱える
for (int i = 0; i < 5; i++) {
printf("%d %d\n", arr[i], *(p + i)); // どちらも同じ値
}
arr[i] という書き方は、実は *(arr + i) の言い換えです。「配列は特別なもの」ではなく住所の計算で動いている、と分かるとすっきりします😊
文字列の扱い
C言語には「文字列型」がありません。char の配列で表します。
#include <stdio.h>
#include <string.h>
char str1[50] = "Hello"; // 50文字ぶんの箱に "Hello" を入れる
char str2[] = "World"; // 必要なぶんだけ確保される(6バイト)
printf("%lu\n", strlen(str1)); // 5 … 終端文字は数えない
strcat(str1, " ");
strcat(str1, str2);
printf("%s\n", str1); // Hello World
⚠ 終端文字ぶんの余裕が要る
C言語の文字列は、最後に \0(ヌル文字)を置いて「ここで終わり」を示します。"World" は5文字ですが6バイト必要です。この1バイトを忘れて確保すると、はみ出して書き込んでしまいます。
ファイル入出力
#include <stdio.h>
FILE* fp = fopen("test.txt", "w"); // "w"=書き込み "r"=読み込み "a"=追記
if (fp == NULL) {
printf("開けませんでした\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp); // ★閉じ忘れると書き込まれないことがある
「開く → 読み書きする → 閉じる」の3段構えです。開けたかどうかを必ず確かめるのが作法で、確かめずに進むと、ファイルが無かったときにその場で落ちます。
関数マクロ
翻訳する前に文字をそのまま置き換える仕組みです。関数に似ていますが、中身は単なる置換です。
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int main() {
printf("%d\n", MAX(3, 5)); // 5
printf("%d\n", SQUARE(4)); // 16
return 0;
}
単なる置換なので、かっこを省くと事故ります。
// ★かっこを省くと、そのまま置き換えられて意図と違う式になる
#define BAD(x) x * x
BAD(1 + 2) // → 1 + 2 * 1 + 2 = 5 になってしまう(期待は 9)
そのため引数と式全体をかっこで囲むのがお約束になっています。最近は、こうした用途にはインライン関数を使うことも多いです。
ビット演算
数値を2進数の桁として直接いじる演算です。組み込みで機器のスイッチを操作するときなどによく出てきます。
int a = 0b1010; // 10
int b = 0b1100; // 12
printf("%d\n", a & b); // AND : 8
printf("%d\n", a | b); // OR : 14
printf("%d\n", a ^ b); // XOR : 6
printf("%d\n", ~a); // NOT : -11
printf("%d\n", a << 1); // 左シフト : 20(2倍)
printf("%d\n", a >> 1); // 右シフト : 5(2分の1)
左に1つずらすと2倍、右に1つずらすと2分の1になります。掛け算・割り算より速いため、昔はよく使われました。いまはコンパイラが自動で最適化するので、速さのために使うより「特定の桁を立てる/落とす」用途で使うことが多いです。
動的メモリ確保
ここまでの変数は、書いた時点で大きさが決まっていました。実行してみないと必要な量が分からないときは、その場で borrow します。
#include <stdio.h>
#include <stdlib.h>
int* arr = (int*)malloc(sizeof(int) * 10); // int 10個ぶんを借りる
if (arr == NULL) {
printf("メモリを確保できませんでした\n");
return 1;
}
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
}
printf("%d\n", arr[9]); // 81
free(arr); // ★借りたら必ず返す
arr = NULL; // 返したあとに使ってしまわないよう、印を付けておく
⚠ 借りたら返すmalloc で借りたメモリは free で返します。返し忘れると、プログラムが動いている間ずっと占有されたままになります(メモリリーク)。長く動かすプログラムでは、少しずつ食いつぶして最後に落ちる、という形で出てきます。
逆に返したあとのポインタを使うのも危険です。返却後に NULL を入れておくと、うっかり使ったときにその場で気づけます。
構造体
関係のあるデータをひとまとめにする仕組みです。「名前・年齢・点数」をバラバラの変数で持つより、まとめたほうが扱いやすくなります。
#include <stdio.h>
#include <string.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
Student s;
strcpy(s.name, "eight");
s.age = 30;
s.score = 95.5f;
printf("%s / %d歳 / %.1f点\n", s.name, s.age, s.score);
// 構造体の配列も作れる
Student class[3];
strcpy(class[0].name, "Aさん");
return 0;
}
C言語には class がありませんが、構造体とポインタを組み合わせることで、あとの言語でいうオブジェクトに近い形を作れます。実際、多くのC言語のライブラリがこの書き方でできています。
まとめ
C言語の基本をひととおり見てきました。要点をまとめます。
- 変数は型を先に決める。
printfの記号も型に合わせる - 配列の範囲外に触ってもエラーにならない。範囲は自分で守る
- ★ポインタは「場所」を持つ変数。
&で住所を取り、*で中身に行く - 関数にはコピーが渡されるので、書き換えたいときは住所を渡す
- 文字列は char の配列で、終端文字ぶんの1バイトが余分に要る
mallocで借りたらfreeで返す。返したらNULLを入れておく- 関数マクロはただの文字の置換なので、かっこを省くと事故る
「面倒を見てくれる範囲が狭い」というC言語の性格は、裏返せば何が起きているかが見えやすいということでもあります。メモリの上でデータがどう置かれているかを一度つかんでおくと、ほかの言語を使うときにも効いてきます😊
それでは、今回はここまで。最後までありがとうございました😊