cの#includeについてよくわかんなかったことメモ

cのヘッダファイルというものについて理解が浅くて、よくコンパイルエラーやリンクエラーをお見舞いされるので整理してみる。

スクリプト言語だと、どのソースファイルを使うか、プログラマがrequire等々で指定する仕組みが普通だと思うけど、
cの場合は少し違っていて、基本的にはソースファイルはすべてコンパイル時にコンパイラに引数として渡して知らせる。そしてプログラマがソースを参照するコード書かなくても、コンパイラがリンクをしてくっつけてくれる。ということのようだ。
ではなぜ#includeが必要かというと、コンパイル中に別のファイルで定義している名前に出くわしたとき、それが何か知る必要があるから。だから#includeは、最低限の宣言だけを含んだヘッダファイルを対象にするのが普通。ということのようだ。
#includeというのも、その名のとおりで、ただ指定したファイルを展開しているだけなのだった。

kawaii.h

#ifndef __kawaii__
#define __kawaii__

void kawaii();

#endif /* defined(__kawaii__) */

sample_include.c

#include "kawaii.h"

int main(int argc, char **argv) {
    return 0;
}

sample_include2.c

#include "kawaii.h"

これらのソースの#includeを展開してみる。

$ gcc -E sample_include.c sample_include2.c
# 1 "sample_include.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "sample_include.c"
# 1 "kawaii.h" 1



void kawaii();
# 2 "sample_include.c" 2

int main(int argc, char **argv) {
    return 0;
}
# 1 "sample_include2.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "sample_include2.c"
# 1 "kawaii.h" 1



void kawaii();
# 2 "sample_include2.c" 2

void hoge(); が書いてあるファイルをインクルードすることで、sample_include.c、sample_include2.c 両方で、void hoge();
という宣言をするのと同じ意味になる。

ということは、void hoge();みたいな宣言は、別々のファイルにそれぞれ同じ記述があったとしても、リンク時にエラーにならないようになってるということになる。
そこで、実験してみたところ、別々のファイルどころか、同じソースに複数あってもエラーにならなかった。

// コンパイル通った
void hoge();
void hoge();
void hoge();
void hoge();
void hoge();
void hoge();
void hoge();


int main(int argc, char **argv) {
    return 0;
}

ところが関数の実装は、複数あるとエラーになる

// エラーになる
void hoge() {}
void hoge() {}

int main(int args, char **argv) {
    return 0;
}

さらに、実装は、別々のファイルに同じ関数が存在することができない。リンク時にエラーになる。

sample_include.c

void hoge() {}

int main() {
    return 0;
}

sample_include2.c

void hoge() {}
$ gcc sample_include.c sample_include2.c
ld: duplicate symbol _hoge in /var/folders/c1/rg29l13n7mbgsvdj9z8mzcs00000gn/T//ccH7WdQJ.o and /var/folders/c1/rg29l13n7mbgsvdj9z8mzcs00000gn/T//ccrb3ou7.o for architecture x86_64
collect2: ld returned 1 exit status

以上がエラーになるということは、ヘッダファイルに関数の実装が書いてある場合、問題になることがあるということになる。
複数のソースから、実装を含むファイルを#includeするとリンク時にエラーになる。

kawaii.h

#ifndef __dame__
#define __dame__

static void dame() {
}

#endif /* defined(__dame__) */

sample_include.c

#include "dame.h"

int main(int argc, char **argv) {
    return 0;
}

sample_include2.c

#include "dame.h"

エラーになる。

$ gcc sample_include.c sample_include2.c
duplicate symbol _dame in:
    /var/folders/q2/v5jmn6hn6715x8n3v0kh9z6c0000gn/T//ccht1ehW.o
    /var/folders/q2/v5jmn6hn6715x8n3v0kh9z6c0000gn/T//ccqzLElm.o
ld: 1 duplicate symbol for architecture x86_64
collect2: ld returned 1 exit status

これを回避するにはstaticを使う。
static宣言した関数は、そのファイル内でしか公開されなくなる。
このstatic宣言した関数を#includeすると、#includeはファイルを展開するだけのマクロなため、
#include先のファイルでローカルな関数になる。

#ifndef __dame__
#define __dame__

static void dame() {
}

#endif /* defined(__dame__) */

上のように修正すると、さっきの例でコンパイル通るようになる。

$ gcc sample_include.c sample_include2.c

これをやると、#include先のファイル全てに同じ関数が存在することになって、プログラムのサイズが増えるから、inlineと併用するのが良いのかも。