背景
- 高速なアプリケーションを動かすにはC/C++が必須になる
- Pythonで書くと遅くなり、重い腰を上げてC++をやることになったの
- そのためにC++やCmakeの基礎的な疑問をAIにぶつけたので、答えをまとめた
全体像
C/C++ では、プログラムはふつう 1回で全部完成するのではなく、下の3つの流れで作られる。
- 前処理
- コンパイル
- リンク
.h と .c の役割
.h
宣言を書く場所
例:
| |
上記のように、
- 関数名
- 引数の型
- 戻り値の型
を他のファイルに知らせ。
.c
実装を書く場所。
例:
| |
上記のように、実際の処理の中身を書く。
なぜ .h と .c を分けるのか
小さいプログラムなら、1ファイルに全部書いても動く。
でも複数ファイルになると、ある .c から別の .c の関数を使いたくなる。
たとえば
main.cadd.csub.c
があるとき、main.c は add() や sub() の存在を知らないと呼べない。
そのために .h を使って、複数の .c ファイルで共有する宣言をまとめるということ。
つまり .h は、
複数ファイルのための共通の説明書ということ。
#include の意味
| |
は、
その場所に calc.h の中身を貼り付ける
と思えばOK。
なので main.c に #include "calc.h" があると、コンパイラは main.c だけでなく、そこに貼られた calc.h の内容も見る。
< > と " "
#include <stdio.h>標準ライブラリなど、システム側のヘッダを探す#include "calc.h"自分のプロジェクト内のヘッダを探す
#include は前処理
#include はコンパイルそのものではなく、前処理。
前処理の例
#include#define#ifndef
コンパイル
- 各
.cをそれぞれ.oにする
リンク
.o同士をつないで実行ファイルにする
コンパイルとリンクの違い
たとえば
main.cadd.csub.c
があるとき、
| |
のように、各 .c は別々にコンパイルされる。
そのあとで
| |
として、最後に1つの実行ファイルへまとめる。
つまり、
.c同士が自動で1つのソースになるわけではない- まず別々に
.oを作る - 最後にリンクして1つにする
なぜ main.c のコンパイル時に add.c の中身を見ないのか
gcc -c main.c の入力は基本的に main.c と、その中で #include されたものだけ す。
だからコンパイラは、main.c をコンパイルするときに add.c を勝手には見ない。
その代わり、add() の宣言が .h にあれば、
addという関数がある- 引数は
int, int - 戻り値は
int
と分かるので、main.c はコンパイルできる。
コンパイル時に実体はなくてもいいのか
はい、普通の関数ならコンパイル時には宣言があれば進められる。
つまり
- コンパイル時: 宣言が必要
- リンク時: 実装が必要
たとえば main.c が add() を呼んでいても、コンパイル時には
| |
が分かっていればよいということ。
でも最終的にリンクするときに add() の本体がないと、
undefined reference
のようなエラーになる。
つまり、
コンパイラは「呼び方」を確認し、リンカは「本体の所在」を確認する
実装ファイルが自分のヘッダを include する理由
たとえば
| |
のように、sub.c でもヘッダを include することがある。
これは sub.c が sub() を使うためではなく、
ヘッダに書いた宣言と、自分の実装が一致しているか確認するため
つまり、
main.cが include する → 使うためsub.cが include する → 宣言と実装の答え合わせのため
include guard とは何か
ヘッダの先頭によくある
| |
これは include guard 。
これは、
同じヘッダが複数回読み込まれても、1回だけ有効にする
ための仕組み。
これは文法上の絶対ルールではなく、昔からの定番の書き方。
.o, .a, .so, a.out
.o
オブジェクトファイル
各 .c / .cpp をコンパイルした結果
.a
静的ライブラリ
複数の .o をまとめたもの
.so
共有ライブラリ(動的ライブラリ)
これも元は .o から作るが、実行時に読み込まれる
a.out
実行ファイルのデフォルト名
gcc main.c のように -o を省略したときにできることがある
.a と .so の違い
どちらも元は .o から作れる。
.a= 静的ライブラリ.so= 共有ライブラリ
違いは「元の材料」ではなく、できあがったものの使われ方。
.aはリンク時に実行ファイルへ取り込まれる.soは実行時に外から読み込まれる
ar rcs の意味
| |
は、.o をまとめて .a を作るarchiveコマンド。
r= 入れる / 置き換えるc= 新規作成s= シンボル索引を作る
つまり
静的ライブラリを作って、リンクしやすい形に整える
ということ。
PICオプション
PIC とは何か
PIC = Position-Independent Code 位置独立コード。
これは、どのメモリアドレスに置かれても動きやすいコード。
共有ライブラリ .so を作るときによく使う。
| |
PICで大事なこと
-fPICは.oの作り方.soはその.oから作る共有ライブラリ
です。
しかも、PIC な .o から .a を作ることもできます。
つまり -fPIC は .a を禁止するものではなく、単に中身の .o の性質。
フォルダ構成
include / src / third_party
よくあるフォルダ構成は以下:
include/= 公開ヘッダsrc/= 実装(非公開)third_party/= 外部ライブラリ
SDKのパターン
include/= 利用者に見せる APIsrc/= SDK 作者の実装ソース- 配布物 = include/ + ビルド済みライブラリ
- 利用者 = ヘッダを include して、そのライブラリにリンク
Makefile / CMake
今までのmain.c、add.c、sub.c、calc.hを使う場合は以下のようになる。
| |
もしくはもっときれいにかける。
| |
CMakeの場合は以下になる。
| |
まとめ
一言でまとめると、
C/C++ では、ヘッダに宣言、ソースに実装を書き、各ソースを別々にコンパイルし、最後にリンクでつなぐ。
さらにもう少しかみ砕くと、
.hは「こう使ってね」という説明書.c/.cppは実際の中身#includeは説明書を貼り付ける前処理- コンパイルは各ファイルを別々に処理
- リンクはバラバラの結果を最後につなぐ
最後に超短く言うと:
- 宣言を共有するために
.h - 実装を書くために
.c/.cpp - コンパイルで
.o - リンクで実行ファイル
.aと.soはどちらも.oから作れるが、使い方が違う
