- 技術 💻
C言語とは|入門から環境構築・ポインタ・mallocまで実行例で解説
結論
C言語とは、1972年に生まれた、コンパイルして機械語で動かすプログラミング言語です。OSや組み込み機器など、速さとハードに近い操作が要る所で使われます。
この記事でわかること
- gccの4段階(-E・-S・-c・リンク)の中間ファイル
- sizeof 14型の実測とポインタのアドレスの実物
- エラー原文15件の直し方とvalgrind・ASanの出力
C言語のプログラムは、gcc などのコンパイラが4つの段階を経て、CPU が直接実行できるファイルに変えます。
flowchart TD
A[hello.c<br>書いたソース] -->|gcc -E 前処理| B[hello.i<br>820行に展開]
B -->|gcc -S コンパイル| C[hello.s<br>アセンブリ]
C -->|gcc -c アセンブル| D[hello.o<br>機械語の部品]
D -->|リンク| E[hello<br>実行ファイル]
最初によく使う書き方とコマンドです。
| やりたいこと | 書き方・コマンド | 補足 |
|---|---|---|
| コンパイルして実行 | gcc hello.c -o hello → ./hello |
Windows は hello.exe ができる |
| 警告をすべて出す | gcc -Wall -Wextra hello.c -o hello |
初めは必ず付ける |
| 型の大きさを調べる | sizeof(int) |
64ビット Linux で4バイト |
| 変数のアドレスを表示 | printf("%p", (void *)&x) |
実行のたびに変わる |
| メモリを確保・解放 | malloc → 使う → free |
NULL かどうかを確かめる |
| メモリの誤りを探す | gcc -g -fsanitize=address |
valgrind でも探せる |
| 数学関数を使う | gcc calc.c -lm |
無いと undefined reference to `sqrt' |
| Windows で始める | Visual Studio の cl、MSYS2 の gcc |
環境構築の節に手順 |
確認環境:Ubuntu 24.04.4・GCC 13.3.0(既定の規格は gnu17)・GNU ld 2.42・glibc 2.39・Valgrind 3.22.0・GDB 15.1・Clang 18.1.3(エラー文の比較)・OpenJDK 21.0.10・Python 3.11.15(速さの比較)。この記事のC言語のコードは、すべて gcc でコンパイルして実行し、出力・エラー文・ファイルサイズを載せています。gcc のオプションは GCC 公式マニュアル、GCC 14・15 の変更は GCC のリリースノート、規格は ISO のページ、Windows の手順は Microsoft Learn と VS Code のドキュメント、Mac の手順は Apple Developer のドキュメント、AddressSanitizer は Clang のドキュメント、Valgrind は Valgrind のマニュアル、言語の順位は TIOBE Index で確かめました(確認日 2026年10月22日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C言語とは|入門から環境構築・ポインタ・mallocまで実行例で解説」
C言語とは(意味・歴史・規格)
この節では、C言語がどんな言語で、いつ・誰が作り、どんな規格があるかが分かります。
C言語(シーげんご)は、1972年にベル研究所のデニス・リッチーが作ったプログラミング言語です。 前身の「B言語」を改良したことから「C」と名付けられ、UNIX とともに広まりました。書いたソースコードを、コンパイラで機械語(CPU が直接読む命令)に変えてから実行する「コンパイル型」の言語です。
1手続き型
処理を上から順に書き、関数にまとめて組み立てる。クラス(オブジェクト指向)は無い。
2コンパイル型
実行前に機械語へ変える。実行時に翻訳しないぶん速い。
3ハードに近い
ポインタでメモリの番地を直接扱える。ビット単位の操作もできる。
4移植しやすい
多くの CPU・OS にコンパイラがある。同じソースを別の機械用にコンパイルし直せる。
C言語の規格(C89〜C23)
C言語の文法は、国際規格 ISO/IEC 9899 で決められています。版ごとに通称があり、gcc では -std=c99 のように選べます。__STDC_VERSION__ は、コンパイラがどの規格でコンパイルしているかを表す値で、GCC 13.3.0 で確かめた値を載せています。
| 通称 | 規格 | 発行 | __STDC_VERSION__ |
|---|---|---|---|
| C89・C90 | ANSI X3.159-1989・ISO/IEC 9899:1990 | 1989年・1990年 | 定義なし |
| C99 | ISO/IEC 9899:1999 | 1999年 | 199901L |
| C11 | ISO/IEC 9899:2011 | 2011年 | 201112L |
| C17(C18) | ISO/IEC 9899:2018 | 2018年 | 201710L |
| C23 | ISO/IEC 9899:2024 | 2024年10月 | 202311L(GCC 15) |
2026年10月22日時点で、ISO のページでは ISO/IEC 9899:2024(C23)が発行済み(Published)です。GCC 13 の既定は gnu17(C17 に GNU の拡張を足したもの)でした。GCC 15 のリリースノートには「C のコンパイルの既定を -std=gnu17 から -std=gnu23 に変えた」とあります。
$ echo | gcc -dM -E - | grep __STDC_VERSION__
#define __STDC_VERSION__ 201710L
C言語の読み方と「C」の名前
読み方は「シーげんご」です。英語では単に C(シー)、または C programming language と呼びます。後で比べる C++(シープラスプラス)と C#(シーシャープ)は、名前は似ていますが別の言語です。
C言語でできること・使われている場所
この節では、C言語が今どこで使われているかと、今も使われる理由が分かります。
C言語は、速さ・メモリの少なさ・ハードウェアの直接操作が求められるソフトウェアで使われています。
| 分野 | 例 | C言語が選ばれる理由 |
|---|---|---|
| OS | Linux カーネル | メモリ・CPU を直接扱える |
| 組み込み機器 | 家電・自動車の制御、マイコン | 小さいメモリで動き、実行が速い |
| データベース・サーバー | SQLite・PostgreSQL・nginx | 速さとメモリの効率 |
| 言語の処理系・ライブラリ | Python の拡張モジュール | 他の言語から呼び出しやすい |
| デバイスドライバ | ネットワーク・ストレージの制御 | ハードウェアのレジスタを操作する |
| ゲームエンジン・画像処理 | 処理の重い部分のライブラリ | 計算が速い |
公式の文書に書かれている例です。
- Linux カーネルのドキュメント:「The Linux kernel is written in the C programming language」(gcc の
-std=gnu11でコンパイルされることが多い、とあります。Rust もCONFIG_RUSTで使えるようになっています) - SQLite の公式サイト:「SQLite is a C-language library」
- Python の公式ドキュメント:C で「C API extension module(拡張モジュール)」を作る手順が載っている
これらの分野の開発者は、組み込みエンジニア・ミドルウェアの開発者・ネットワーク機器の開発者などと呼ばれます。
TIOBE Index(プログラミング言語の人気の指標)の2026年10月の順位では、C は Python に次ぐ2位(11.33%)でした。
C言語の特徴(メリットとデメリット)
この節では、C言語の良い点と気を付ける点を、この記事の実測と合わせて見ます。
メリット
実行が速い(素数を数える処理で Python の約30倍。後の節で実測)
使うメモリが少ない(同じ処理で約11MB)
メモリやハードウェアを直接扱える
多くの CPU・OS で動く。他の言語から呼び出しやすい
文法が小さい。コンピューターの仕組みを学べる
デメリット
メモリの確保と解放を自分で書く
配列の範囲外を読んでもエラーにならないことがある
誤りがクラッシュ(Segmentation fault)として出る
文字列・リストなどの便利な型が少ない
クラスが無く、大きなプログラムは設計の工夫が要る
「エラーにならない」ことがある:未定義動作
C言語の規格には「未定義動作(undefined behavior)」という考え方があります。規格が「どうなるかを決めない」操作で、配列の範囲外の読み書きや、int のあふれ(オーバーフロー)などが当たります。 コンパイラも実行時の仕組みも、多くの場合は止めてくれません。
int big = INT_MAX;
int arr[3] = {1, 2, 3};
int idx = 3;
printf("INT_MAX + 1 = %d\n", big + 1);
printf("arr[3] = %d\n", arr[idx]);
INT_MAX + 1 = -2147483648
arr[3] = -106207488
どちらもエラーにならず、意味のない値が出ました。同じことを Java と Python で行うと、実行時に止まります。
要素3個の配列で arr[3] を読んだとき
C言語では範囲を守るのは書く人の役目。検出には後の節の -fsanitize が使える
-fsanitize=undefined,address を付けてコンパイルし直すと、実行時に2つとも見つかりました。
ub.c:9:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
ub.c:10:32: runtime error: index 3 out of bounds for type 'int [3]'
C言語とJava・Python・C++の違い
この節では、他の言語と何が違うかを、同じ処理を動かした結果と表で比べます。
Java・Pythonとの違い
1千万以下の素数の個数を数える同じ処理(エラトステネスのふるい)を、C・Java・Python で書いて5回ずつ実行し、真ん中の時間を取りました。答えはどれも 664579 でした。
時間はプロセスの起動から終了までで、処理の内容や書き方で大きく変わります。ここでは「C は速い」ことの目安として載せています。
| 項目 | C言語 | Java | Python | JavaScript |
|---|---|---|---|---|
| 実行のしかた | 機械語にコンパイルして直接実行 | バイトコードにして JVM で実行 | インタプリタが読みながら実行 | ブラウザ・Node.js のエンジンが実行 |
| メモリの解放 | 自分で free を書く |
GC(自動) | 自動(参照カウントと GC) | GC(自動) |
| 型 | 静的(宣言が要る) | 静的 | 動的(宣言しない) | 動的 |
| 文字列 | char の配列+最後に '\0' |
String クラス |
str 型 |
string 型 |
| 配列の範囲外 | 検査しない(未定義動作) | 例外で止まる | 例外で止まる | undefined を返す |
| クラス | 無い(構造体と関数) | ある | ある | ある |
| 実行ファイル | OS・CPU ごとに作り直す | .class は JVM があれば動く |
ソースのまま動く | ソースのまま動く |
範囲外の欄は、要素3個の配列の [3] を読んだ結果です(Java 21 は ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3、Python は IndexError: list index out of range、Node.js 22 は undefined)。
C++・C#との違い
| 項目 | C言語 | C++ | C# |
|---|---|---|---|
| 関係 | もとの言語 | C を拡張して作られた | 名前は似ているが別系統 |
| オブジェクト指向 | 無い | ある(クラス・継承) | ある |
| メモリ管理 | 手動 | 手動+スマートポインタ | GC(自動) |
| 主な用途 | OS・組み込み・ライブラリ | ゲーム・アプリ・高速な処理 | Windows アプリ・Unity のゲーム |
| C のコード | ― | 多くはそのままコンパイルできる | そのままは使えない |
Objective-C も C を拡張した言語で、Apple の macOS・iOS のアプリ開発で使われてきました。C言語を学んでおくと、C++ の土台・Java や Go のポインタや参照の考え方・Rust がメモリの安全を守る理由がつかみやすくなります。
C言語の環境構築(Windows・Mac・Linux)
この節では、OS ごとにC言語をコンパイルできるようにする手順が分かります。
C言語を動かすには「コンパイラ」が要ります。 代表は gcc(GNU)、clang(LLVM)、MSVC(Microsoft の cl)の3つです。
| OS | 入れるもの | 確かめるコマンド |
|---|---|---|
| Windows(手軽さ重視) | Visual Studio の「C++ によるデスクトップ開発」 | 開発者コマンド プロンプトで cl |
| Windows(gcc を使う) | MSYS2 の MinGW-w64 | gcc --version |
| Windows(Linux と同じ環境) | WSL の Ubuntu に build-essential | gcc --version |
| Mac | Command Line Tools for Xcode(clang) | clang --version |
| Linux(Ubuntu・Debian) | sudo apt install build-essential |
gcc --version |
Windows:Visual Studio(cl)
Microsoft Learn のチュートリアルの手順です。
Visual Studio を入れる
Visual Studio インストーラーで「C++ によるデスクトップ開発」ワークロードを選んでインストールします。
開発者コマンド プロンプトを開く
スタートメニューの「Visual Studio 2022」フォルダーから「開発者コマンド プロンプト for VS 2022」を開き、cl と打ってコンパイラの版が出るか見ます。
コンパイルして実行する
cl hello.c で hello.exe ができます。hello と打つと実行されます。
Windows:MSYS2 の gcc
VS Code のドキュメントにある手順です。MSYS2 をインストールし、MSYS2 のターミナルで次を実行します。
pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain
続けて、Windows の環境変数 Path に C:\msys64\ucrt64\bin(既定の場所の場合)を足し、新しく開いたコマンドプロンプトで gcc --version を実行して版が出れば準備完了です。
Mac:Command Line Tools for Xcode
Apple のドキュメントの手順です。ターミナルで次を実行し、表示されたダイアログで「インストール」を選びます。
xcode-select --install
clang などのコマンドが /Library/Developer/CommandLineTools に入ります。Xcode 本体を入れている場合は、別に入れなくても使えます。コンパイルは clang hello.c -o hello、実行は ./hello です。
Linux(Ubuntu・WSL)
Ubuntu の build-essential は、gcc・g++・make・libc6-dev(標準ライブラリのヘッダー)をまとめて入れるパッケージです。
sudo apt update
sudo apt install build-essential
gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
エディターは VS Code・Vim など何でも使えます。VS Code の日本語化と入れ方は、関連記事「VSCode日本語化とインストール手順|Win/Mac/Linuxと直し方」で扱っています。
Hello Worldを書いて動かす
この節では、最初のプログラムを書いて、コンパイル・実行するまでが分かります。
hello.c という名前で保存します。
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
| 行 | 意味 |
|---|---|
#include <stdio.h> |
入出力の関数(printf など)の宣言を読み込む |
int main(void) |
プログラムが始まる関数。名前は必ず main |
printf("Hello, World!\n"); |
文字を表示する。\n は改行。文の最後に ; |
return 0; |
正常に終わったことを OS に伝える(終了コード 0) |
コンパイルして実行しました。$? は直前のコマンドの終了コードです。
$ gcc hello.c -o hello
$ ./hello
Hello, World!
$ echo $?
0
-o hello は「出来上がるファイルの名前を hello にする」指定です。省くと a.out(Windows の MinGW では a.exe)という名前になります。
コンパイルの仕組み(gccの-E・-S・-c・リンク)
この節では、gcc hello.c -o hello の1行の中で何が起きているかを、途中のファイルを開いて確かめます。
gcc は「前処理 → コンパイル → アセンブル → リンク」の4段階で実行ファイルを作ります。 オプションを付けると、好きな段階で止められます。GCC のマニュアルにある説明と、実際にできたファイルです。
| 段階 | 止めるオプション | できるもの | 大きさ(実測) |
|---|---|---|---|
| ソース | ― | hello.c |
84 バイト |
| 1. 前処理 | -E |
hello.i(#include を展開したソース) |
820 行・21,316 バイト |
| 2. コンパイル | -S |
hello.s(アセンブリ) |
668 バイト |
| 3. アセンブル | -c |
hello.o(オブジェクトファイル) |
1,504 バイト |
| 4. リンク | (何も付けない) | hello(実行ファイル) |
15,960 バイト |
1. 前処理(-E):#include が展開される
#include <stdio.h> の行が、stdio.h の中身に置き換わります。7行のソースが820行になりました。その中に printf の宣言があります。
$ gcc -E hello.c -o hello.i
$ wc -l hello.i
820 hello.i
$ grep -n "extern int printf" hello.i
476:extern int printf (const char *__restrict __format, ...);
$ tail -6 hello.i
int main(void)
{
printf("Hello, World!\n");
return 0;
}
2. コンパイル(-S):アセンブリになる
CPU の命令を人が読める形で書いたもの(アセンブリ)ができます。主な部分を抜き出しました。
$ gcc -S hello.c -o hello.s
$ cat hello.s
.LC0:
.string "Hello, World!"
.text
.globl main
.type main, @function
main:
...
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
ret
printf と書いたのに、puts を呼ぶ命令になっていました。 書式(%d など)の無い文字列を改行付きで出すだけなので、gcc がより単純な puts に置き換えています。最適化を指定しない(-O0)状態でも、この置き換えが行われていました。
3. アセンブル(-c):オブジェクトファイルになる
機械語になりましたが、まだ実行できません。nm で中身の名前を見ると、puts は U(undefined=このファイルには無い)でした。
$ gcc -c hello.c -o hello.o
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ nm hello.o
0000000000000000 T main
U puts
4. リンク:足りない部品をつないで実行ファイルにする
リンカー(ld)が、puts の本体がある標準ライブラリ(libc)とつなぎます。
$ gcc hello.o -o hello
$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
$ ldd hello
linux-vdso.so.1 (0x00007f93af0a9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f93aee00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f93af0ab000)
dynamically linked は「libc を実行のときに読み込む」という意味です。-static を付けると libc の中身を実行ファイルに入れるため、大きさは 15,960 バイトから 785,360 バイトになりました。途中のファイルを全部残したいときは gcc -save-temps hello.c -o hello で、hello.i・hello.s・hello.o が同じフォルダにできます。gcc -v を付けると、実際に呼ばれている cc1(コンパイラ本体)・as(アセンブラ)・collect2(リンクの入口)が表示されます。
ファイルを分けてコンパイルする(分割コンパイル)
プログラムが大きくなったら、関数ごとにファイルを分けます。宣言はヘッダーファイル(.h)に書き、使う側で #include "calc.h" します。
/* calc.h */
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
#endif
$ gcc -Wall -c main.c
$ gcc -Wall -c calc.c
$ gcc main.o calc.o -o app
$ ./app
2 + 3 = 5
$ gcc main.o -o app2
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x13): undefined reference to `add'
collect2: error: ld returned 1 exit status
calc.o を渡し忘れると、リンクの段階で undefined reference to `add' になります。ファイルが増えたときの自動化は、関連記事「Makefileの書き方|自動変数一覧とタスクランナー用テンプレート」で扱っています。
変数と型(sizeofの実測)
この節では、C言語の基本の型と、それぞれが何バイトかを実際に測った結果が分かります。
C言語の変数は、使う前に型を決めて宣言します。 型の大きさは規格で完全には決まっておらず、OS と CPU で変わります。sizeof で実際の大きさを測りました(64ビットの Ubuntu・GCC 13.3.0)。
printf("int : %zu バイト (%d 〜 %d)\n", sizeof(int), INT_MIN, INT_MAX);
char : 1 バイト (-128 〜 127)
short : 2 バイト (-32768 〜 32767)
int : 4 バイト (-2147483648 〜 2147483647)
unsigned int: 4 バイト (0 〜 4294967295)
long : 8 バイト (-9223372036854775808 〜 9223372036854775807)
long long : 8 バイト
float : 4 バイト (有効桁 6 桁)
double : 8 バイト (有効桁 15 桁)
long double : 16 バイト
bool : 1 バイト
int32_t : 4 バイト
size_t : 8 バイト
int * : 8 バイト
char * : 8 バイト
| 型 | 64ビット Linux(実測) | Windows(MSVC) | 主な使い道 |
|---|---|---|---|
char |
1 | 1 | 1文字・1バイト |
int |
4 | 4 | ふつうの整数 |
long |
8 | 4 | 大きめの整数(OS で違う) |
long long |
8 | 8 | 大きな整数 |
double |
8 | 8 | 小数 |
long double |
16 | double と同じ |
精度の高い小数 |
ポインタ(int * など) |
8 | ― | アドレス |
Windows の値は Microsoft Learn の「データ型の範囲」の表です。long は Linux で8バイト、Windows で4バイトと違います。 大きさを決めたいときは <stdint.h> の int32_t・int64_t を使います。2進数・16進数での表し方は、関連記事「16進数とは|変換・計算のやり方と0〜255早見表を3言語で実演」で扱っています。
計算・if・for の基本
int a = 7, b = 2;
printf("7 / 2 = %d\n", a / b);
printf("7 %% 2 = %d\n", a % b);
printf("(double)7 / 2 = %.1f\n", (double)a / b);
unsigned char u = 255;
u = u + 1;
printf("unsigned char 255 + 1 = %d\n", u);
char c = 'A';
printf("'A' は %%c で %c、%%d で %d\n", c, c);
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
printf("1〜10 の合計 = %d\n", sum);
7 / 2 = 3
7 % 2 = 1
(double)7 / 2 = 3.5
unsigned char 255 + 1 = 0
'A' は %c で A、%d で 65
1〜10 の合計 = 55
- 整数どうしの割り算は小数点以下が切り捨て(7 / 2 = 3)。小数がほしいときは
(double)で型を変えてから割る。 - 符号なし(unsigned)の型はあふれると0に戻る(255 + 1 = 0)。符号ありの
intのあふれは、前の節のとおり未定義動作。 charの中身は数。'A'は文字コードの 65。
printf の書式(よく使うもの)
| 書式 | 型 | 例 |
|---|---|---|
%d |
int |
printf("%d", 42) → 42 |
%ld・%lld |
long・long long |
大きな整数 |
%u |
unsigned int |
符号なし |
%f・%.2f |
double |
3.500000・3.50 |
%c |
1文字 | A |
%s |
文字列(char *) |
hello |
%p |
ポインタ(アドレス) | 0x7fff0c1e5efc |
%zu |
size_t(sizeof の結果) |
4 |
%x |
16進数 | ff |
%% |
% そのもの |
% |
よく使う標準ライブラリのヘッダー
C言語の言語そのものは小さく、入出力や文字列の処理は標準ライブラリの関数を #include して使います。
| ヘッダー | 主な中身 | この記事で使った例 |
|---|---|---|
<stdio.h> |
入出力 | printf・fprintf・snprintf・scanf |
<stdlib.h> |
メモリ・変換・終了 | malloc・free・realloc・atoi |
<string.h> |
文字列・メモリの操作 | strlen・strcpy・strcmp |
<math.h> |
数学関数(リンクに -lm) |
sqrt |
<stdint.h> |
大きさの決まった整数型 | int32_t |
<stdbool.h> |
bool・true・false |
bool |
<limits.h>・<float.h> |
型の最大値・最小値 | INT_MAX・DBL_DIG |
<stddef.h> |
size_t・NULL・offsetof |
offsetof |
ネットワーク(<sys/socket.h>)などは OS が用意するヘッダーで、Linux・Mac と Windows で中身が違います。テストには Unity・CppUTest などの C 向けのテスト用ライブラリが使われます。
配列と文字列
この節では、配列がメモリにどう並ぶかと、C言語の文字列の正体が分かります。
配列はメモリに隙間なく並ぶ
要素のアドレスを表示すると、int(4バイト)ずつ番地が増えていました。
int nums[4] = {10, 20, 30, 40};
int *p = nums; /* 配列名は先頭要素のアドレスになる */
for (int i = 0; i < 4; i++) {
printf("nums[%d] = %d アドレス %p *(p+%d) = %d\n",
i, nums[i], (void *)&nums[i], i, *(p + i));
}
printf("sizeof(nums) = %zu、要素数 = %zu\n", sizeof(nums), sizeof(nums) / sizeof(nums[0]));
nums[0] = 10 アドレス 0x7fff74ff3430 *(p+0) = 10
nums[1] = 20 アドレス 0x7fff74ff3434 *(p+1) = 20
nums[2] = 30 アドレス 0x7fff74ff3438 *(p+2) = 30
nums[3] = 40 アドレス 0x7fff74ff343c *(p+3) = 40
sizeof(nums) = 16、要素数 = 4
nums[i] と *(p + i) は同じ要素を指します。 p + 1 は「1バイト先」ではなく「int 1個分(4バイト)先」です。要素数は sizeof(配列) / sizeof(要素) で求めます(関数に渡した後は使えません。関数の節で実測)。
文字列は「最後が \0 の char の配列」
C言語には文字列の型がありません。文字列は char の配列で、最後に '\0'(値が0の文字)が付きます。 "hello" は5文字ですが、配列の大きさは6バイトでした。
char s[] = "hello";
printf("s = %s strlen = %zu sizeof = %zu\n", s, strlen(s), sizeof(s));
s = hello strlen = 5 sizeof = 6
s[0] = 'h' (104)
s[1] = 'e' (101)
s[2] = 'l' (108)
s[3] = 'l' (108)
s[4] = 'o' (111)
s[5] = '\0' (0)
"あ" の strlen = 3、sizeof = 4
strlen は '\0' の手前までの「バイト数」を数えます。UTF-8 で保存したソースでは、日本語の「あ」は3バイトなので、strlen は3でした。文字数ではない点に気を付けます。
| やりたいこと | 関数(<string.h>) |
注意 |
|---|---|---|
| 長さ(バイト数) | strlen(s) |
'\0' は数えない |
| コピー | strcpy(dst, src) |
dst の大きさを検査しない |
| つなげる | strcat(dst, src) |
同上 |
| 比べる | strcmp(a, b) |
同じなら 0。== では比べられない |
| 大きさを守って書く | snprintf(buf, sizeof(buf), ...) |
はみ出す分は切り捨てる |
buf = abcdef、strcmp("abc","abd") = -1
snprintf の結果 = item-42
strcpy は書き込み先の大きさを確かめません。8バイトの配列に19バイトをコピーすると、後の節の stack smashing detected で止まります。大きさが決まっている場所に書くときは snprintf を使うと、はみ出しを防げます。
ポインタとは(アドレスを表示して理解する)
この節では、ポインタが何を入れる変数かを、実際のアドレスを表示して確かめます。
ポインタは「メモリのアドレス(番地)」を入れる変数です。 &x で変数 x のアドレスを取り出し、*p で p が指す先の値を読み書きします。
int x = 10;
int *p = &x; /* p に x のアドレスを入れる */
printf("x の値 : %d\n", x);
printf("x のアドレス : %p\n", (void *)&x);
printf("p の値 : %p\n", (void *)p);
printf("p が指す値 *p : %d\n", *p);
printf("p 自身のアドレス: %p\n", (void *)&p);
*p = 20; /* p を通して x を書き換える */
printf("*p = 20 の後の x: %d\n", x);
x の値 : 10
x のアドレス : 0x7fff0c1e5efc
p の値 : 0x7fff0c1e5efc
p が指す値 *p : 10
p 自身のアドレス: 0x7fff0c1e5f00
*p = 20 の後の x: 20
p の値は x のアドレスと同じ 0x7fff0c1e5efc でした。 p を通して 20 を書くと、x が 20 に変わりました。図にすると次のとおりです。
flowchart TD
P[変数 p<br>場所 0x7fff0c1e5f00<br>中身 0x7fff0c1e5efc] -->|指す| X[変数 x<br>場所 0x7fff0c1e5efc<br>中身 10]
S[*p = 20] -->|p を通して書く| X
| 書き方 | 意味 | この例の値 |
|---|---|---|
int *p; |
int を指すポインタ p を宣言する | ― |
&x |
x のアドレス | 0x7fff0c1e5efc |
p |
p に入っているアドレス | 0x7fff0c1e5efc |
*p |
p が指す先の値 | 10 |
*p = 20; |
p が指す先に書く | x が 20 になる |
p + 1 |
次の要素のアドレス | 4バイト先(int の場合) |
NULL |
どこも指していない印 | 読むと Segmentation fault |
もう一度実行すると、アドレスは 0x7ffcad50d8ac に変わりました。Linux などの OS は、攻撃を難しくするために、実行のたびにメモリの配置を変えています(ASLR)。アドレスの値そのものではなく、「p と &x が同じ」という関係を見ます。
プログラムのメモリの配置
関数・文字列・グローバル変数・malloc の領域・ローカル変数のアドレスを並べると、置き場所が分かれていました。
関数 main : 0x55a71764c1a9
文字列リテラル : 0x55a71764d004
グローバル変数 : 0x55a71764f01c
ヒープ(malloc): 0x55a754e552a0
ローカル変数 : 0x7fffbe6f45ac
flowchart TD
A[コード・文字列リテラル<br>0x55a71764c1a9 付近] --> B[グローバル変数<br>0x55a71764f01c]
B --> C[ヒープ<br>malloc で確保<br>0x55a754e552a0]
C --> D[空き]
D --> E[スタック<br>ローカル変数<br>0x7fffbe6f45ac]
| 領域 | 置かれるもの | いつ消えるか |
|---|---|---|
| コード | 関数の機械語、文字列リテラル | プログラムの終了まで(書き換え不可) |
| 静的領域 | グローバル変数・static 変数 |
プログラムの終了まで |
| ヒープ | malloc で確保したもの |
free したとき |
| スタック | ローカル変数・関数の引数 | 関数を抜けたとき |
Java の変数がどこに置かれるかとの比較は、関連記事「Java ヒープとスタックの違い|変数の保存場所とメタスペースを図解」で、Go のポインタの書き方は関連記事「Goのポインタの使い方|*と&の違い、値渡しとポインタ渡し、レシーバ」で扱っています。
関数(値渡しとポインタ渡し)
この節では、関数の書き方と、引数がコピーされることによる落とし穴が分かります。
C言語の関数の引数は「値のコピー」で渡されます。 呼び出した側の変数を書き換えたいときは、アドレス(ポインタ)を渡します。
#include <stdio.h>
int add(int a, int b); /* プロトタイプ宣言 */
void swap_value(int a, int b);
void swap_pointer(int *a, int *b);
int sum_array(int arr[], int n);
int main(void)
{
printf("add(3, 4) = %d\n", add(3, 4));
int x = 1, y = 2;
swap_value(x, y);
printf("swap_value の後 : x=%d y=%d\n", x, y);
swap_pointer(&x, &y);
printf("swap_pointer の後: x=%d y=%d\n", x, y);
int nums[5] = {1, 2, 3, 4, 5};
printf("main での sizeof(nums) = %zu\n", sizeof(nums));
printf("sum_array = %d\n", sum_array(nums, 5));
return 0;
}
int add(int a, int b)
{
return a + b;
}
void swap_value(int a, int b) /* 値のコピーを受け取る */
{
int t = a; a = b; b = t;
}
void swap_pointer(int *a, int *b) /* アドレスを受け取る */
{
int t = *a; *a = *b; *b = t;
}
int sum_array(int arr[], int n)
{
printf("関数の中での sizeof(arr) = %zu\n", sizeof(arr));
int s = 0;
for (int i = 0; i < n; i++) s += arr[i];
return s;
}
add(3, 4) = 7
swap_value の後 : x=1 y=2
swap_pointer の後: x=2 y=1
main での sizeof(nums) = 20
関数の中での sizeof(arr) = 8
sum_array = 15
x と y を入れ替える関数
関数の外の変数を変えたいときは、アドレスを渡して *a で書き換える
配列を関数に渡すと、先頭のアドレス(ポインタ)として渡ります。 main では 20 バイトだった sizeof が、関数の中では 8(ポインタの大きさ)になりました。gcc もコンパイルのときに警告を出していました。そのため、配列を渡す関数には要素数を別の引数で渡します。
func.c:41:54: warning: 'sizeof' on array function parameter 'arr' will return size of 'int *' [-Wsizeof-array-argument]
関数を main より後ろに書くときは、先頭にプロトタイプ宣言(int add(int a, int b);)を書きます。宣言が無いと、GCC 14 以降ではコンパイルエラーになります(エラーの節)。
構造体(struct)
この節では、複数の値をまとめる構造体の書き方と、メモリ上の並び方が分かります。
構造体は、型の違う値を1つにまとめた型です。 Java や Python のクラスから、メソッドを除いたものに近い形です。
#include <stdio.h>
#include <stddef.h>
#include <string.h>
typedef struct {
char name[16];
int age;
double height;
} Person;
struct Mixed {
char c;
int i;
};
void birthday(Person *p)
{
p->age++; /* (*p).age++ と同じ */
}
int main(void)
{
Person taro = {"Taro", 20, 171.5};
Person copy = taro; /* 構造体は代入でまるごとコピーされる */
birthday(&taro);
strcpy(copy.name, "Copy");
printf("%s: %d 歳, %.1f cm\n", taro.name, taro.age, taro.height);
printf("%s: %d 歳\n", copy.name, copy.age);
printf("sizeof(Person) = %zu\n", sizeof(Person));
printf("offsetof name=%zu age=%zu height=%zu\n",
offsetof(Person, name), offsetof(Person, age), offsetof(Person, height));
printf("sizeof(struct Mixed) = %zu (char 1 + int 4 なのに)\n", sizeof(struct Mixed));
printf("offsetof c=%zu i=%zu\n", offsetof(struct Mixed, c), offsetof(struct Mixed, i));
return 0;
}
Taro: 21 歳, 171.5 cm
Copy: 20 歳
sizeof(Person) = 32
offsetof name=0 age=16 height=24
sizeof(struct Mixed) = 8 (char 1 + int 4 なのに)
offsetof c=0 i=4
| 書き方 | 意味 |
|---|---|
taro.age |
構造体の変数からメンバーを読む |
p->age |
構造体へのポインタからメンバーを読む((*p).age と同じ) |
Person copy = taro; |
全メンバーをコピーした別の変数ができる |
typedef struct {...} Person; |
struct を付けずに Person と書けるようにする |
char 1バイトと int 4バイトを並べた構造体(struct Mixed)は、合計5バイトではなく8バイトでした。offsetof で見ると、int は4バイト目から始まっています。CPU が読みやすい位置にそろえるため、コンパイラが間に隙間(パディング)を入れるためです。ファイルや通信でバイト列をそのまま読み書きするときは、この隙間に気を付けます。
malloc・freeでメモリを確保する
この節では、実行中に必要な分だけメモリを確保して、返す書き方が分かります。
malloc はヒープからメモリを確保し、free で返します。 配列の大きさが実行してから決まるときに使います。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n = 5;
int *arr = malloc(n * sizeof(int)); /* int 5 個分を確保 */
if (arr == NULL) {
fprintf(stderr, "メモリを確保できませんでした\n");
return 1;
}
for (int i = 0; i < n; i++) arr[i] = (i + 1) * 10;
int *tmp = realloc(arr, 10 * sizeof(int)); /* 10 個に広げる */
if (tmp == NULL) {
free(arr);
return 1;
}
arr = tmp;
for (int i = n; i < 10; i++) arr[i] = (i + 1) * 10;
for (int i = 0; i < 10; i++) printf("%d ", arr[i]);
printf("\n");
free(arr); /* 使い終わったら返す */
arr = NULL; /* 返した後に使わないよう NULL にしておく */
return 0;
}
10 20 30 40 50 60 70 80 90 100
Valgrind で確かめると、確保と解放の数がそろい、漏れ(リーク)はありませんでした。
==1525== HEAP SUMMARY:
==1525== in use at exit: 0 bytes in 0 blocks
==1525== total heap usage: 3 allocs, 3 frees, 4,156 bytes allocated
==1525==
==1525== All heap blocks were freed -- no leaks are possible
(3 allocs のうち1つは、printf が内部で使う領域です。)
| 関数 | すること | 失敗したとき |
|---|---|---|
malloc(バイト数) |
確保する(中身は不定) | NULL を返す |
calloc(個数, 1個の大きさ) |
確保して0で埋める | NULL を返す |
realloc(ポインタ, 新しいバイト数) |
広げる・縮める(場所が変わることがある) | NULL を返し、元の領域は残る |
free(ポインタ) |
返す | ― (2回呼ぶと異常終了) |
- 1malloc の戻り値が NULL でないか確かめた確保に失敗すると NULL が返る
- 2大きさを「個数 × sizeof(型)」で書いたmalloc(5) は5バイト。int 5個なら 5 * sizeof(int)
- 3realloc の結果を別の変数で受けた失敗して NULL が返ると、元のアドレスを失ってリークする
- 4確保したものを1回ずつ free した忘れるとリーク、2回だと double free
- 5free の後はポインタを NULL にした解放後の領域を使う誤り(use after free)を防ぐ
よくあるコンパイルエラーと直し方
この節では、gcc が出すエラー・警告の原文と、何を直せばよいかが分かります。すべて GCC 13.3.0 で実際に出たものです。
文法のエラー(コンパイルが止まる)
セミコロンが抜けたときと、宣言していない変数を使ったときです。
e1.c: In function 'main':
e1.c:5:22: error: expected ';' before 'return'
5 | printf("Hello\n")
| ^
| ;
6 | return 0;
| ~~~~~~
e2.c: In function 'main':
e2.c:5:5: error: 'count' undeclared (first use in this function)
5 | count = 1;
| ^~~~~
e1.c:5:22 は「5行目の22文字目」です。セミコロンが無いエラーは、次の行(return)の手前として報告されます。
リンクのエラー(undefined reference)
コンパイルは通ったのに、部品をつなぐ段階で見つからないときのエラーです。最後の collect2: error: ld returned 1 exit status が目印です。
$ gcc e4.c -o e4
e4.c:5:5: warning: implicit declaration of function 'prinf'; did you mean 'printf'? [-Wimplicit-function-declaration]
/usr/bin/ld: /tmp/cciJO71s.o: in function `main':
e4.c:(.text+0x18): undefined reference to `prinf'
collect2: error: ld returned 1 exit status
$ gcc e5.c -o e5
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
$ gcc e6.c -o e6
/usr/bin/ld: /tmp/ccgfGn69.o: in function `main':
e6.c:(.text+0x2f): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
$ gcc e6.c -o e6 -lm
$ ./e6
1.414214
-Wall で初めて出る警告
次のコードは、-Wall を付けないと2件しか警告が出ませんでした。-Wall -Wextra を付けると6件出ました。
double price = 120.5;
int n;
char name[8] = "abc";
printf("%d\n", price);
printf("%d\n", n);
if (n = 3) { printf("3\n"); }
if (name == "abc") { printf("same\n"); }
int age;
scanf("%d", age);
e7.c:9:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
e7.c:11:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
e7.c:14:14: warning: comparison with string literal results in unspecified behavior [-Waddress]
e7.c:18:13: warning: format '%d' expects argument of type 'int *', but argument 2 has type 'int' [-Wformat=]
e7.c:10:5: warning: 'n' is used uninitialized [-Wuninitialized]
e7.c:18:5: warning: 'age' is used uninitialized [-Wuninitialized]
警告でも実行ファイルはできます。 できたからといって正しく動くとは限らないので、警告が0件になるまで直します。
エラー・警告の原文と直し方の一覧
| エラー・警告(原文) | 原因 | 直し方 |
|---|---|---|
error: expected ';' before 'return' |
前の行の ; が無い |
指された位置の前の行末に ; |
error: 'count' undeclared (first use in this function) |
宣言していない変数・打ち間違い | int count = 0; のように宣言する |
warning: implicit declaration of function 'printf' |
#include <stdio.h> が無い |
注記どおり #include を足す |
undefined reference to `prinf' |
関数名の打ち間違い・本体が無い | 名前を直す。別ファイルなら一緒にリンク |
undefined reference to `main' |
main が無い(mian など) |
int main(void) を書く |
undefined reference to `sqrt' |
数学ライブラリをつないでいない | 最後に -lm を付ける |
format '%d' expects argument of type 'int', but argument 2 has type 'double' |
書式と型が合わない | double は %f |
format '%d' expects argument of type 'int *' |
scanf に & が無い |
scanf("%d", &age) |
suggest parentheses around assignment used as truth value |
if (n = 3) は代入 |
比較は == |
comparison with string literal results in unspecified behavior |
文字列を == で比べた |
strcmp(name, "abc") == 0 |
'n' is used uninitialized |
値を入れずに使った | 宣言のときに初期化する |
makes integer from pointer without a cast |
int n = "123"; など型の取り違え |
数にするなら atoi・strtol |
'sizeof' on array function parameter |
関数の中で配列の sizeof |
要素数を引数で渡す |
fatal error: nothing.c: No such file or directory |
ファイル名・場所の誤り | ls で名前と今いるフォルダを確かめる |
gcc: fatal error: no input files |
ソースファイルを指定していない | gcc ファイル名.c |
GCC 14 以降で警告からエラーに変わったもの
GCC 14 の移行ガイドには、C で次の診断が警告からエラーに変わったと書かれています。GCC 13 では警告で済んでいたコードが、新しい gcc ではコンパイルできなくなります。
| 診断 | 例 | GCC 13(実測) |
|---|---|---|
| 宣言していない関数の呼び出し(implicit-function-declaration) | #include 忘れ・打ち間違い |
警告 |
| 型を書かない宣言(implicit-int) | main() の戻り値の型なし |
警告 |
| 整数とポインタの取り違え(int-conversion) | int n = "123"; |
警告 |
| 型の違うポインタの代入(incompatible-pointer-types) | int * に char * |
警告 |
void の関数で値を返す(return-mismatch) |
return 1; |
警告 |
Clang 18 では、宣言していない関数の呼び出しは既にエラーでした(error: call to undeclared function 'prinf'; ISO C99 and later do not support implicit function declarations)。どのコンパイラでも通るように、警告の段階で直しておきます。
Segmentation faultの原因と直し方
この節では、実行中にプログラムが落ちる「Segmentation fault」が何で、どこで起きたかを調べる方法が分かります。
Segmentation fault(セグメンテーション違反)は、使ってはいけないメモリを読み書きしたため、OS がプログラムを止めたことを表します。 終了コードは 139 でした。
int *p = NULL;
printf("これから NULL を読みます\n");
printf("%d\n", *p);
$ bash -c './segv; echo "exit=$?"'
これから NULL を読みます
bash: line 1: 5633 Segmentation fault ./segv
exit=139
出力をファイルやパイプに流したときは、これから NULL を読みます も表示されませんでした(./segv > out.txt の out.txt は0バイト)。端末以外への出力はためてから書かれる(バッファリング)ため、書く前に落ちると消えます。どこまで進んだかを printf で探すときは、fprintf(stderr, ...) を使うと消えません。
よくある原因
| 原因 | 例 | 直し方 |
|---|---|---|
| NULL を読み書きした | int *p = NULL; *p |
使う前に if (p != NULL) |
| 文字列リテラルを書き換えた | char *s = "abc"; s[0] = 'X'; |
char s[] = "abc"; と配列にする |
| 配列の範囲を大きく超えた | arr[100000] |
ループの条件を i < n にする |
| free した後に使った | free(p); *p |
free 後は p = NULL |
| 再帰が止まらない | 終わる条件の無い再帰 | 終わる条件を書く |
scanf に & が無い |
scanf("%d", age) |
&age |
配列からはみ出して書いた:stack smashing detected
8バイトの配列 name に、19バイト(18文字+'\0')を strcpy でコピーしました。gcc はコンパイルのときに警告を出し、実行すると、表示の後に異常終了しました(終了コード 134)。
smash.c:7:5: warning: '__builtin_memcpy' writing 19 bytes into a region of size 8 overflows the destination [-Wstringop-overflow=]
$ bash -c './smash; echo "exit=$?"'
Yamada Taro Hanako
*** stack smashing detected ***: terminated
bash: line 1: 5636 Aborted ./smash
exit=134
関数の終わりに、スタックの見張りの値(カナリア)が書き換えられていないかを確かめる仕組みが働き、プログラムを止めました。コピー先の大きさを超えない snprintf(name, sizeof(name), "%s", 元の文字列) に変えると、はみ出しは起きません。
文字列リテラルを書き換えた
文字列リテラルの書き換えも実行しました。配列にコピーした t は書き換えられ、リテラルを指す s に書いたところで落ちました。
t = Xbc
bash: line 1: 12174 Segmentation fault ./lit
gdb で落ちた行を見つける
-g(デバッグ情報を入れる)を付けてコンパイルし、gdb で実行すると、落ちた行が表示されます。
$ gcc -g segv.c -o segv
$ gdb -q -batch -ex run -ex bt ./segv
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555190 in main () at segv.c:7
7 printf("%d\n", *p);
#0 0x0000555555555190 in main () at segv.c:7
メモリの誤りをvalgrind・AddressSanitizerで見つける
この節では、目に見えないメモリの誤りを、道具で見つける方法と実際の出力が分かります。
メモリの誤りの多くは、その場では落ちずに進みます。 4種類の誤りを入れたプログラムを、何も付けずに実行した結果です。
| 誤り | 何も付けずに実行 | valgrind | -fsanitize=address |
|---|---|---|---|
| 解放し忘れ(リーク) | 正常終了 | definitely lost: 40 bytes |
detected memory leaks |
| 確保した範囲の外に書く | 99 と表示(正常終了) | Invalid write of size 4 |
heap-buffer-overflow |
| free の後に使う | 1510646364 と表示 | Invalid read of size 4 |
heap-use-after-free |
| 2回 free する | free(): double free detected in tcache 2(終了コード 134) |
Invalid free() |
attempting double-free |
valgrind
コンパイルし直さずに使えます。Valgrind のマニュアルは、-g を付け、-O0 でコンパイルすることを勧めています。範囲外に書いた例では、確保した20バイトの「0バイト後ろ」に書いたことと、その行(14行目)が出ました。
$ gcc -g -O0 bugs.c -o bugs
$ valgrind --leak-check=full ./bugs 2
==1624== Invalid write of size 4
==1624== at 0x10923A: main (bugs.c:14)
==1624== Address 0x4a7e054 is 0 bytes after a block of size 20 alloc'd
==1624== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1624== by 0x10922D: main (bugs.c:13)
解放し忘れの例では、9行目で確保した40バイトが返されていないと出ました。
==1616== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1616== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1616== by 0x1091EE: main (bugs.c:9)
AddressSanitizer(-fsanitize=address)
gcc・clang に入っている検出の仕組みで、コンパイルのときにオプションを付けます。誤りが見つかると、その場で止まり、終了コードは 1 でした。
$ gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer bugs.c -o bugs_asan
$ ./bugs_asan 3
==1679==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0x5591546604b6 bp 0x7ffcc9825a20 sp 0x7ffcc9825a10
READ of size 4 at 0x502000000010 thread T0
#0 0x5591546604b5 in main bugs.c:21
(中略)
freed by thread T0 here:
#0 0x7fce6f6fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
#1 0x55915466047e in main bugs.c:20
(中略)
previously allocated by thread T0 here:
#0 0x7fce6f6fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55915466042d in main bugs.c:18
「21行目で読んだ領域は、20行目で free され、18行目で確保されたもの」と、3つの行が一度に分かります。
| 道具 | 使い方 | 速さ(Clang・Valgrind の文書) | 向いている場面 |
|---|---|---|---|
| AddressSanitizer | -fsanitize=address でコンパイルし直す |
典型で約2倍遅い | ふだんのテスト・CI |
| UndefinedBehaviorSanitizer | -fsanitize=undefined |
― | 整数のあふれ・範囲外の添字 |
| valgrind | valgrind --leak-check=full ./prog |
20〜30倍遅いことがある | コンパイルし直せないとき |
| gdb | gdb ./prog |
― | 落ちた行・変数の値を見る |
この記事の素数のプログラムでは、-O1 で 0.14 秒、AddressSanitizer 付きも 0.14 秒、valgrind では 1.43 秒でした。メモリを手で管理しない言語のしくみとしては、Rust の所有権があります。関連記事「Rust入門|所有権からCLI作成までコピペで動くコード付き」で扱っています。
C言語の学習の順番
この節では、入門からどの順で学ぶと進めやすいかが分かります。
本や学習サイトで文法を読み、手元の gcc で1つずつ動かして出力を確かめながら進めます。
環境を作り Hello World を動かす
コンパイラを入れ、gcc -Wall -Wextra でコンパイルする習慣を付けます。
変数・型・if・for
整数の割り算の切り捨て、printf の書式と型の対応を押さえます。
配列と文字列
文字列は最後に '\0' がある char の配列、と理解します。
関数
引数はコピーで渡る。配列は先頭のアドレスで渡る。
ポインタ
& と * を、アドレスを printf しながら確かめます。
構造体と malloc・free
動的な配列や、構造体をつないだリストを作ります。
分割コンパイルと道具
ヘッダーファイル、Makefile、gdb を使って、複数のファイルのプログラムを作ります。
よくある質問
Q. C言語とは何ですか? A. 1972年にベル研究所のデニス・リッチーが作った、コンパイルして機械語で実行するプログラミング言語です。OS・組み込み機器・データベースなど、速さとハードウェアに近い操作が要る所で使われています。
Q. C言語の読み方は? A. 「シーげんご」です。英語では C(シー)と呼びます。
Q. C言語で何ができますか? A. OS(Linux カーネル)、組み込み機器の制御、データベース(SQLite・PostgreSQL)、Web サーバー(nginx)、他の言語から呼ぶライブラリなどが作られています。「C言語でできること・使われている場所」の節に表があります。
Q. C言語は難しいですか?
A. 文法は小さい言語ですが、ポインタとメモリの管理を自分で行う点が難しいと言われます。誤りがエラーにならずに進むことがあるので、-Wall -Wextra と -fsanitize=address を付けて確かめながら学ぶと、つまずきを見つけやすくなります。
Q. C言語とC++の違いは? A. C++ は C を拡張して作られた言語で、クラスなどのオブジェクト指向の機能が加わっています。C のコードの多くは C++ としてもコンパイルできます。C# は名前が似ていますが別系統の言語で、GC でメモリを自動で管理します。
Q. C言語を始めるには何が必要ですか?
A. コンパイラです。Windows は Visual Studio の「C++ によるデスクトップ開発」か MSYS2 の gcc、Mac は xcode-select --install で入る clang、Linux は sudo apt install build-essential で入る gcc を使います。
Q. コンパイルとは何ですか? A. 人が書いたソースコードを、CPU が実行できる機械語のファイルに変えることです。gcc は前処理(-E)・コンパイル(-S)・アセンブル(-c)・リンクの4段階で行います。
Q. ポインタとは何ですか?
A. メモリのアドレス(番地)を入れる変数です。&x で x のアドレスを取り、*p で指す先の値を読み書きします。この記事の実行例では、p の値と x のアドレスが同じ 0x7fff0c1e5efc でした。
Q. Segmentation fault とは何ですか?
A. 使ってはいけないメモリ(NULL・解放済み・範囲外・書き換え不可の領域)を読み書きしたため、OS がプログラムを止めたことです。gcc -g でコンパイルして gdb で実行すると、落ちた行が分かります。
Q. C言語の最新の規格は?
A. 2026年10月22日時点で、ISO/IEC 9899:2024(通称 C23)です。GCC 15 から、gcc の既定は gnu23 になりました。
Q. C言語は今でも使われていますか? A. 使われています。Linux カーネルは C で書かれており、2026年10月の TIOBE Index では C は2位でした。新しいシステムの一部では Rust も使われ始めています(Linux カーネルも Rust に対応)。
Q. printf と puts の違いは?
A. printf は %d などの書式で値を埋め込めます。puts は文字列をそのまま出し、最後に改行を付けます。書式の無い printf("...\n") は、gcc が puts に置き換えることがあります(コンパイルの仕組みの節で確認)。
まとめ
- C言語は1972年生まれのコンパイル型の言語。OS・組み込み・データベースなどで使われ、最新の規格は C23(ISO/IEC 9899:2024)。
- gcc は前処理(-E)・コンパイル(-S)・アセンブル(-c)・リンクの4段階で実行ファイルを作る。7行のソースは前処理で820行になった。
- 型の大きさは環境で変わる。64ビット Linux の
intは4バイト、longは8バイト(Windows は4バイト)。 - 文字列は最後に
'\0'があるcharの配列。ポインタはアドレスを入れる変数で、&で取り、*で指す先を読み書きする。 - 関数の引数はコピーで渡る。呼び出し元を変えたいときはアドレスを渡す。配列は先頭のアドレスで渡る。
mallocで確保したら NULL を確かめ、1回だけfreeする。- 警告は
-Wall -Wextraで出し、0件にする。メモリの誤りは-fsanitize=addressか valgrind で見つける。
参考・出典
確認日はいずれも 2026年10月22日です。
- ISO「ISO/IEC 9899:2024 Information technology — Programming languages — C」:https://www.iso.org/standard/82075.html
- GCC 公式マニュアル「Options Controlling the Kind of Output」:https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
- GCC 15 Release Series「Changes, New Features, and Fixes」:https://gcc.gnu.org/gcc-15/changes.html
- GCC 14「Porting to GCC 14」:https://gcc.gnu.org/gcc-14/porting_to.html
- Microsoft Learn「チュートリアル: コマンド ラインでの C プログラムのコンパイル」:https://learn.microsoft.com/ja-jp/cpp/build/walkthrough-compile-a-c-program-on-the-command-line
- Microsoft Learn「データ型の範囲」:https://learn.microsoft.com/ja-jp/cpp/cpp/data-type-ranges
- Visual Studio Code「Using GCC with MinGW」:https://code.visualstudio.com/docs/cpp/config-mingw
- Apple Developer「Installing the command-line tools」:https://developer.apple.com/documentation/xcode/installing-the-command-line-tools
- Clang「AddressSanitizer」:https://clang.llvm.org/docs/AddressSanitizer.html
- Valgrind「The Valgrind Quick Start Guide」:https://valgrind.org/docs/manual/quick-start.html
- The Linux Kernel documentation「Programming Language」:https://docs.kernel.org/process/programming-language.html
- SQLite:https://sqlite.org/index.html
- Python ドキュメント「Extending Python with C or C++」:https://docs.python.org/3/extending/extending.html
- TIOBE Index:https://www.tiobe.com/tiobe-index/
- Wikipedia「C言語」:https://ja.wikipedia.org/wiki/C言語
- IT用語辞典 e-Words「C言語」:https://e-words.jp/w/C言語.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C言語とは|入門から環境構築・ポインタ・mallocまで実行例で解説」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。