0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.c/.cpp に書くか .h に書くかは、外に公開するかで決まる

0
Last updated at Posted at 2026-09-29

イントロ

C を初めて触ったあの日からおおよそ 9年。やっとヘッダファイルのあるべき姿を本質から理解できました。それを言語化して自分の頭に定着させようということで、筆を取った次第です。

あまり厳密でアカデミックな解説でないかもなのでそこは注意。

また、インクルードガードなど、C/C++ ヘッダの基本は知っている前提で進めます。

2026/09/30 追記 - @SaitoAtsushi 様よりコメントを受け

C の仕組みとしてリンケージという考え方があるそうです。それを踏まえると、公開/非公開という呼び方はあまり適切でないかもしれません。

「あまり厳密でアカデミックな解説でない」スタンスとして用語は一旦そのままにしていますが、C のより深い内容を理解する際は注意が必要になります。

ヘッダの意味を超ざっくり

my_lib.c というライブラリがあるとして、main.c がそれを使いたいとします。何もない状態では、main.c は my_lib.c に何が定義されているかを知りません。

my_lib.c
#include "my_lib.h"

int my_lib(int a, int b) {
  return a + b;
}
main.c
int main() {
  int ret = my_lib(2, 3);
  //        ~~~~~~~~~~~~ これが存在しているのを知らない/見えない → コンパイルエラー
  // かしこい環境だと「暗黙 (implicit) の宣言をしてるよ」の
  // ワーニング止まりで結果上手くコンパイルしてくれることも
  return 0;
}

2026/09/30 追記 - @SaitoAtsushi 様よりコメントを受け

かしこい環境、というより古い C 規格への互換として融通をきかせてくれている、という方が正確なようです。

いずれにせよ、すぐ解消できるワーニングに変わりはないので、放置せず解消しましょう。

このとき、ライブラリに付属する my_lib.h を経由して、main.c は my_lib.c に定義されているものを知ることができます。

my_lib.h
#ifndef MY_LIB_H
#define MY_LIB_H

int my_lib(int a, int b);

#endif /* MY_LIB_H */
main.c
#include "my_lib.h"  // my_lib() の存在を知る

int main() {
  int ret = my_lib(2, 3);
  //        ~~~~~~~~~~~~ コンパイルが通る
  return 0;
}

2026/09/30 編集 - @SaitoAtsushi 様よりコメントを受け

インクルードガードのマクロを __MY_LIB_H__ というような名前にするのはルール違反のようです。前後に __ を置く形式は、標準ライブラリなどシステム内のみにとどめるべきで、普通のコードで真似するのは避けるべきなようです。

上のソースも以降のソースも前後の _ を無くして修正しました。

外に公開/非公開にする、とは例えば

ヘッダファイルに書くということは、main.c など他ファイルから使えるようにするということ。つまり上の例で、my_lib() は他ファイルから使えるよう公開しているということになります。

で、何でもかんでも公開していると良いかというと、そうではありません。

使う側からしたら、ライブラリ内部の細々した、本来知らなくていい余計なものが見えていると邪魔です。ライブラリを作る側からしたら、勝手に触られたらイヤなものがユーザから見えていたら怖いお話です。

ですから、一般的にヘッダファイルには「使う側から見て必要最低限のことだけ書く」のが正しいということになります。具体的に例を考えてみます。

関数・変数・マクロ - パターン1

スタックデータ構造 (LIFO) を例に考えてみます。

stack.c
#include "stack.h"

static int stack[STACK_SIZE];
static int stack_count_var = 0;

static int is_stack_full();

void stack_push(int value) {
  if(is_stack_full()) return;  // Do nothing
  stack[stack_count_var] = value;
  stack_count_var++;
}

int stack_pop() {
  if(stack_count_var == 0) return -1;
  stack_count_var--;
  return stack[stack_count_var];
}

int stack_count() {
  return stack_count_var;
}

static int is_stack_full() {
  return stack_count_var >= STACK_SIZE;
}

良い例が思いつかなくて、非公開関数の例として is_stack_full() を無理やり用意しました。正直このケースだと関数に分けてある意味はあまり無いです。

このソースへのヘッダファイル、つまり公開すべきものは、例えば以下のようになります。

stack.h
#ifndef STACK_H
#define STACK_H

#define STACK_SIZE 8

void stack_push(int value);
int stack_pop();
int stack_count();

#endif /* STACK_H */

stack[] と stack_count_var が外から自由に触れるようでは、使う側の故意または事故で狂わせることができてしまいます。触らせたくありませんから、これはヘッダファイルに書かず、また static をつけてスコープをファイル内に絞ることで、外へは非公開にします。

(is_stack_full() は公開でも全く問題ないが) 同様にして非公開にしたい関数も、ヘッダに書かず、static をつけて宣言します。

一方、スタックが満たんかを使う側が確かめられるようにするため、stack_count() と STACK_SIZE はヘッダに書いて外から読めるようにしています。

他、スタックの操作を外からできないと何にもなりませんから、stack_push() と stack_pop() はもちろん公開しています。

関数・変数・マクロ - パターン2

もう一つ、同じく LIFO でちょっと違う設計を例にしてみます。

stack.c
#include "stack.h"

#define STACK_SIZE 8

static int stack[STACK_SIZE];
static int stack_count_var = 0;

void stack_push(int value) {
  if(is_stack_full()) return;  // Do nothing
  stack[stack_count_var] = value;
  stack_count_var++;
}

int stack_pop() {
  if(stack_count_var == 0) return -1;
  stack_count_var--;
  return stack[stack_count_var];
}

int is_stack_full() {
  return stack_count_var >= STACK_SIZE;
}
stack.h
#ifndef STACK_H
#define STACK_H

void stack_push(int value);
int stack_pop();
int is_stack_full();

#endif /* STACK_H */

今度は is_stack_full() をヘッダに書いて公開。一方で STACK_SIZE を .c に書いて非公開にしました。使う側がスタックサイズを意識することがない設計です (意識したくてもできないのは、それはそれで問題だが)。

stack[] と stack_count_var は変わらず、勝手にいじられたくないので非公開にしています。

#include - 公開するケース

#include を公開する・しないというのは、ちょっと分かり辛いかもしれません。

まず公開する場合、stdbool.h が割と説明しやすいのでこれを例にします。テスト点数に対し、合格・不合格を返す関数を例に考えてみます。

exam_util.c
#include "exam_util.h"

const static unsigned PASS_BORDER = 90;

bool is_passed(unsigned score) {
  return score >= PASS_BORDER;
}
exam_util.h
#ifndef EXAM_UTIL_H
#define EXAM_UTIL_H

#include <stdbool.h>

bool is_passed(unsigned score);

#endif /* EXAM_UTIL_H */

公開している関数の引数に bool 型を使っています。つまり使う上では stdbool.h が必要になるということです。

もしここで #include <stdbool.h> を .c に書いて外から隠した場合、使う側が自力で #include を書いて、bool の定義を得にいかなければなりません。そのうえ #include の順番を考えさせられたりします。これでは不便です。

よってこのシチュエーションでは、#include <stdbool.h> を外に向けて明かす必要があるということになります。

#include - 非公開にするケース

逆に、公開しなくていい場合の例として、三平方の定理 (ピタゴラスの定理) を計算する関数を例に考えてみます。

pythagorean.c
#include <math.h>

double pythagorean(double a, double b) {
  return sqrt(a * a + b * b);
}
pythagorean.h
#ifndef PYTHAGOREAN_H
#define PYTHAGOREAN_H

double pythagorean(double a, double b);

#endif /* PYTHAGOREAN_H */

math.h にある sqrt() をライブラリ内で使っています。一方でライブラリ外からすると、sqrt() について意識する必要はありません。ただ返ってくる double 値を受け取るだけですから。

よってこのシチュエーションでは、#include <math.h> を外に公開する必要がないということになります。

なお、#include を適切に書くことは、依存関係の正しい明示・整理になり、ビルド速度の悪化防止につながります。

型定義

ここで型定義とは、typedef での型定義、および enum や struct での型定義を意味しています。

#include <stdbool.h> の話を応用します。enum でブール型を自力で定義するのを例に考えてみます。

exam_util.c
#include "exam_util.h"

const static unsigned PASS_BORDER = 90;

BOOLEAN is_passed(unsigned score) {
  return score >= PASS_BORDER;
}
exam_util.h
#ifndef EXAM_UTIL_H
#define EXAM_UTIL_H

typedef enum {
  B_FALSE,
  B_TRUE
} BOOLEAN;

BOOLEAN is_passed(unsigned score);

#endif /* EXAM_UTIL_H */

stdbool.h のときと同じく、is_passed() を使うために、使う側はその返り値型である BOOLEAN を知っている必要があります。ですからヘッダに書いて公開します。

逆に、ライブラリ内部の非公開関数とのやりとりでしか使わない enum や struct であったら、それは公開する必要がないので非公開にします。

ちなみにモダン言語では

モダンな言語の例として JavaScript。数値や関数の実態を書いてある .js ファイルで、公開するものには export を書きます。

my_lib.js
export function my_lib() {
  // ...
}

これで外から読めるようになります。ヘッダファイルはありません。

import { my_func } from "my_lib.js";

my_func();

Python など他のモダン言語でも同様に、C/C++ のヘッダファイルと同じ形式でやってるのを私は見たことがありません。実際、.c/.cpp にある関数の実態の名前や引数などに変更加えたら、ヘッダファイルの宣言も適応するように変えないといけないわけです。面倒です。

その思いは世界のエンジニアもきっと同じだったのでしょう。上の例の通りモダン言語ではヘッダシステムは排除されているわけです。

おわり

ちなみに私は、#include や #define をほぼ無条件で全てヘッダに書いてしまっていました。定義ものを全部まとめる場所だと間違っていました。

ただそれでもワーニングも出ないし動いてしまうんだな・・・。問題になるのがある程度の規模になってきた頃で、なかなか体感し辛いのが難しい。

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?