上級者のための Hello World! ~C/C++ プログラミング~

[次:上級者のための Hello World! 2 ~C/C++ プログラミング~](https://qiita.com/KaoruHosokawa00/items/53586c30439cac54e0b6)

# はじめに

今回は C/C++ を使ったプログラミングについて書きためていく。コンパイラは gcc を、OS は Ubuntu22.04 を使う。なお文中でのコンピュータとは、一つのハードウェアの上で一つの OS が動いているという状況をまとめてあらわす。また、文中でのコンパイルとは、かなり曖昧な意味を表す。

<details><summary>蛇足：詳しい仕様</summary>

```bash
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```

</details>

C/C++ といえば、コンパイル型言語の代表である。しかし、初心者向けの解説は多いけれど中級、上級の解説は少ない。これは、ある程度は仕方ないことではあるが、あまり面白くない。今回は Hello World を題材に C 言語について書いていく。

必要なものをインストールしておく。

```bash: ターミナル
$ sudo apt install tree build-essential cmake pkg-config -y
```

# 実行可能ファイルの作成

## とりあえず書いてみる

ではまず、普通に C 言語で書いてみよう。

適当な場所に `hello_ws` ディレクトリを作り 移動して、その中に `hello.c` を作成してみよう。ws はワークスペースの略である。`touch` はファイルがいつ編集されたかを更新するが、空ファイルをつくるときによく使われる。**以下をコピペ**してほしい。

```bash: ターミナル
$ mkdir hello_ws && cd $_
$ touch hello.c
$ tree
.
└── hello.c
```

```c: hello.c
#include <stdio.h>

int main(void)
{
    printf("Hello World!\n");
    return 0;
}
```

## コンパイル

では、ここで `-o` オプションで名前を指定しコンパイルした後、`ls -l` コマンドを打ってみよう。`ls` コマンドより詳しくファイルの情報を見ることができる。

```bash: ターミナル
$ gcc hello.c -o hello
$ ls -l
total 20
-rwxr-xr-x 1 user user 15968 Sep 24 12:36 hello
-rw-r--r-- 1 user user    82 Sep 24 12:36 hello.c
```

ここで左端の部分を見てほしい。下に詳細な表を書いたので**赤字の部分**を見てほしい。

![スクリーンショット 2023-09-24 125828.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986389/9f8185b4-6120-51e2-aa20-2560be4e0274.png)

読み、書き、実行とは、OS が各ユーザに与える権限である。もともと OS というのは多くの人間でつかうものである。`r` や `w` や `x` がある部分は権限があり、`-` の部分に権限はない。たとえば、赤の他人は `hello.c` ファイルを読むことはできても上書きすることはできない。

ところで、読みと書きはともかく**実行とは何だろう**か？

コンピュータを使う人にとって実行可能とは、ファイル名を指定するだけでプログラムが動くということである。そのファイルがテキストファイルかバイナリファイルかは関係ない。

## テキストファイルの実行

たとえば、Pythonを使ってみよう。hello.pyというファイルを作って実行権限を与える。`chmod` は権限を変更するが、具体的な使い方は省略する。以下をコピペしてほしい。

```bash: ターミナル
$ touch hello.py && chmod +x $_
```

```python: hello.py
#!/usr/bin/env python3

print("Hello Python!")
```

`hello.py` ファイルの 1 行目はシェバンと呼ばれる。**省略しないで**ほしい。では実行してみよう。

```bash: ターミナル
$ ./hello.py
Hello Python!
```

## 実行

とにかく、今 実行可能な `hello` ファイルが存在することがわかった。では実行してみよう。

```bash: ターミナル
$ ./hello
Hello World!
```

# ディレクトリの整理とプリプロセス

つねに単一のファイルにソースコードを書くわけではない。ファイルを分割して整理しよう。

とりあえず、すべてのファイルを削除しておこう。

```bash: ターミナル
$ rm *
```

ソースコードとヘッダファイルを置くディレクトリを作成して、ファイルを置いておこう。**以下をコピペ**してほしい。

```bash: ターミナル
$ mkdir src include
$ touch main.c src/hello.c include/hello.h
$ tree
.
├── include
│   └── hello.h
├── main.c
└── src
    └── hello.c
```

```c: hello.h
#ifndef HELLO_H
#define HELLO_H

void print_hello(void);

#endif
```

```c: hello.c
#include <stdio.h>
#include "../include/hello.h"

void print_hello(void)
{
    printf("Hello World!\n");
}
```

```c: main.c
#include "include/hello.h"

int main()
{
    print_hello();
    return 0;
}
```

`hello.h` 内の `#` から始める行はインクルードガードと呼ばれ、同じヘッダファイルが何度も呼ばれないようにしている。if not defined つまり、`HELLO_H` が定義されていないときには、`#endif` までを読み込む。その途中で `HELLO_H` を定義しておく。これらは本来 重複読み込みを防止するためのものではないが、伝統的に用いられている。

ただし、最近のコンパイラでは、これらの書き方の代わりに先頭に `#pragma once` とだけ書き込んでおくことでも代替可能であることが多い。

ヘッダファイル（`.h` ファイル）には通常、関数名とその引数・返り値の型をそれぞれ書く。引数は変数名を書いてもいい。これをプロトタイプ宣言という。

興味がある人は `/usr/include/stdio.h` を見てみよう。`printf()` が宣言されているはずだ。

`hello.c` 内の `#include "../include/hello.h"` は厳密には不要だが、読み込んでおくことが推奨される。

ヘッダファイルをインクルードするときの `<>` `""` ではヘッダファイルを探す場所が異なっている。カレントディレクトリからの相対パスで書く場合は `""` を使う。

`#` から始まる行はプリプロセッサ ディレクティブ（前処理指令）と呼ばれる。これらはコンパイルの前の、プリプロセスのときに必要になる。プリプロセスは以下のようなことをする。

- `#include` によるヘッダファイルをソースコードに再帰的に挿入する
- コメントを消す 
- `#define` による文字列置換をする  

C言語は明示的な型宣言を必要とするのに、`#define` では型を宣言しないことを不思議に思ったことはないだろうか？これは `#define` が単に文字列置換しかしないためだ。プリプロセスでは主に文字列が処理される。

ではコンパイルし実行してみよう。

```bash: ターミナル
$ gcc main.c src/hello.c -o hello
$ ./hello
Hello World!
```

## インクルードディレクトリの指定

ヘッダファイルを探す場所をコンパイラにコマンドラインから教えてみよう。上記の `.c` ファイル内の `hello.h` のインクルードをすべて `#include <hello.h>` に**変更して**以下のようにコンパイルしてみよう。

```bash: ターミナル
$ gcc main.c src/hello.c -o hello -I./include
$ ./hello
Hello World!
```

`-I` オプションはヘッダファイルの存在するディレクトリを指定する。`-I` と `./include` の間に空白が開いて**いない**が誤字ではない。

# ライブラリの作成

大規模な開発では、毎回すべてのファイルをコンパイルするのは大変である場合が多い。そこで、最初からよく使うモノをコンパイルしておくと便利である。

## オブジェクトファイル

gcc では `-c` オプションで中間ファイル ――正確には（リロケータブル）オブジェクトファイル―― を作成する。拡張子は `o` である。`lib` ディレクトリを作ってその中にライブラリや中間ファイルを置く。

```bash: ターミナル
$ mkdir lib && cd $_
$ gcc -c ../src/hello.c -I../include
```

これで `hello.o` ができた。`main.c` といっしょにコンパイルしてみよう。

```bash: ターミナル
$ cd ..
$ gcc main.c lib/hello.o -o hello_o -I./include
$ tree
.
├── hello
├── hello_o
├── include
│   └── hello.h
├── lib
│   └── hello.o
├── main.c
└── src
    └── hello.c
$ ./hello_o
Hello World!
```

このコンパイル時には `src` ディレクトリが不要であるが、`include` ディレクトリは必要であることに注目されたい。つまりこのときは下のような区別がディレクトリに存在する。

![名称未設定ファイル.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986389/66b93b39-f98a-f7e1-f0fc-b850c1441b07.png)


作成した `hello.o` の中身を見ていると、バイナリファイルになっており文字化けしていることがわかる。変更した `.c` ファイルだけを中間ファイルにコンパイルしておくと最後にまとめてくっつけることができる。しかし、`.o` ファイル自体は単なる中間ファイルであり、人間が扱うことは少ない。

## ライブラリ

バイナリの関数を呼び出すには、一般に静的ライブラリと共有ライブラリ（動的ライブラリ）の 2 種類の方法がある。

静的ライブラリは最終的なコンパイルのときに実行可能ファイル内に含まれるようになる。一方共有ライブラリは、プログラムの実行時に別の場所のファイルが呼ばれる。

静的ライブラリを使うと実行可能ファイルを別のコンピュータへコピーするだけで実行できるが、ファイルが大きくなる。一方共有ライブラリを使うと、ライブラリが存在しないと実行できないがファイルは小さくなる。

ライブラリは命名規則や呼び出しが初見では**わかりにくい**が、落ち着いて見ればわかるはずだ。

## 静的ライブラリ

まず静的ライブラリを作り使ってみよう。先ほど作成した `hello.o` と `ar` （アーカイブ）コマンドを使う。

```bash: ターミナル
$ cd lib
$ ar r libhello.a hello.o
ar: creating libhello.a
```

これで `libhello.a` というファイルができた。静的ライブラリは `lib<ライブラリ名>.a` という名前にしなければいけない。

```bash: ターミナル
$ cd ..
$ gcc main.c -o hello_a -I./include -L./lib -lhello
$ tree
.
├── hello
├── hello_a
├── hello_o
├── include
│   └── hello.h
├── lib
│   ├── hello.o
│   └── libhello.a
├── main.c
└── src
    └── hello.c
$ ./hello_a
Hello World!
```

`-L` オプションはライブラリのあるディレクトリを指定する。 今回は `-L./lib` である。ライブラリは `-l<ライブラリ名>` で指定する。つまりライブラリのファイル名から`lib`、`.a`を削除して先頭に `-l` をつける。今回は `-lhello` である。

今回は静的ライブラリしかなかったためこれでよかったが、共有ライブラリも存在する場合はそちらのほうが優先される。明示的にライブラリを指定するには `-l:<ファイル名>` を使う必要がある。

```bash: ターミナル
$ gcc main.c -o hello_a -I./include -L./lib -l:libhello.a
```

あるいは以下のようにする方法もある。`-Wl,` オプションは続くオプションをリンカに渡す。`-Bstatic` や `-Bdynamic` は後ろに続くライブラリについて、静的ライブラリを使うか、動的ライブラリを使うか決める。`-Bdynamic` の後ろには何もないように見えるが、これは自作ではないライブラリのためである。

```bash
$ gcc -o hello main.c -I./include -L./lib -Wl,-Bstatic -lhello -Wl,-Bdynamic
# または
$ gcc -o hello main.c -I./include -L./lib -Wl,-Bstatic,-lhello,-Bdynamic
```

## 共有ライブラリ

次に共有ライブラリを作り使ってみよう。

```bash: ターミナル
$ cd lib
$ gcc ../src/hello.c -o libhello.so -I../include -shared
```

`-shared` オプションを使って `libhello.so` という共有ライブラリができた。 共有ライブラリは `lib<ライブラリ名>.so` という名前にしなければならない。ただし `libhello.so.1.0` のようにバージョンをファイル末尾に書いて、`libhello.so` へのシンボリックリンク（Windowsにおけるショートカット）を張ることもある。それではコンパイルしてみよう。

```bash: ターミナル
$ cd ..
$ gcc main.c -o hello_so -I./include -L./lib -lhello
$ export LD_LIBRARY_PATH=$PWD/lib:$LD_LIBRARY_PATH
$ tree
.
├── hello
├── hello_a
├── hello_o
├── hello_so
├── include
│   └── hello.h
├── lib
│   ├── hello.o
│   ├── libhello.a
│   └── libhello.so
├── main.c
└── src
    └── hello.c
$ ./hello_so
Hello World!
```

ここで、**別の**ターミナルを立ち上げて `hello` を実行してみてほしい。下のようなエラーを吐くはずだ。

```bash: 新規ターミナル
$ ./hello_so
./hello: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
```

じつは Linux においては、環境変数 `LD_LIBRARY_PATH` を指定しないと、実行可能ファイルの実行時に、自作の共有ライブラリを探すことができない。それを指定しているのが `export` から始まる一行である。コンパイル時の `-L./lib` は、あくまでコンパイル時にライブラリを探しているだけであり、実行時には関与しない。

`$PWD` は print working directory の略でカレントディレクトリへのフルパスを保存している環境変数である。

このように機械語のファイルどうしの関連付けをリンクと呼び、関連付けを行うものをリンカと呼ぶ。`.o` ファイルを思い出してほしい。あれはリンクが終わっていない状態である。

実行するときだけライブラリのあるディレクトリを指定することも可能だ。新しいほうのターミナルで以下を実行してほしい。

```bash: 新規ターミナル
$ LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH ./hello_so
Hello World!
```

プログラムがどの共有ライブラリを必要としているかは `ldd` コマンドでわかる。ぜひ両方のターミナルで実行してほしい。

```bash: ターミナル
$ ldd hello_so
```

<!---

<details><summary>蛇足：LD_LIBRARY_PATHを自動指定する方法</summary>

もし `~/.profile` などに書き込む場合は `$PWD` の部分を具体的なディレクトリに書き換える必要がある。`~/.profile` や `$PWD` や `export` は、やや本質を外れた話になるので、何を言っているかわからない場合かつ、毎回 `export` から始まる一行を打ちたくない人は、以下を実行されたい。以下を実行せずに毎回打っても構わない。

```bash: ターミナル
$ echo export LD_LIBRARY_PATH=$PWD/lib:\$LD_LIBRARY_PATH >> ~/.profile
$ source ~/.profile
```

</details>

--->

しかし、実は `gcc` でのコンパイル時に共有ライブラリの場所を教えることもできる。

```bash: ターミナル
$ gcc main.c -o hello_so -I./include -L./lib -lhello -Wl,-rpath=$PWD/lib
```

`-rpath` は `ld`（リンクさせるコマンド）のオプションであり、ライブラリのあるディレクトリを指定しておく。これらの代わりに `-R` オプションを使うという記事もあるが GCC では無効になっているようだ。[^gccr]

[^gccr]: [C++ - gcc -R パラメーターは何をしますか? - Stack Overflow](https://stackoverflow.com/questions/7860024)

## 比較

では比較してみよう。先ほどの `ldd` コマンドで実行可能ファイルについて調べてみよう。

```bash
$ ldd hello_*
hello_a:
        linux-vdso.so.1 (0x00007ffe51985000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa2befbb000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa2bf1f1000)
hello_o:
        linux-vdso.so.1 (0x00007ffd4e7f9000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3ad86fa000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3ad8930000)
hello_so:
        linux-vdso.so.1 (0x00007ffea1788000)
        libhello.so => /home/user/hello_ws/lib/libhello.so (0x00007f3d17429000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3d171fa000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3d17435000)
```

`hello_so` だけが `libhello.so` を必要としている。

次に `ls` コマンドで大きさを見てみよう。左から5番目がファイルの大きさ（単位はバイト）である。

```bash
$ ls -l hello_*
-rwxr-xr-x 1 user user 16024 Oct 14 14:24 hello_a
-rwxr-xr-x 1 user user 16024 Oct 14 13:20 hello_o
-rwxr-xr-x 1 user user 15952 Oct 14 13:20 hello_so
```

共有ライブラリを使った `hello_so` だけが少し小さくなっている。


# 参考

[ライブラリについて解説！静的・動的とは？それぞれのメリットは？ | だえうホームページ](https://daeudaeu.com/library/)

[gcc コンパイルオプション備忘録 - Qiita](https://qiita.com/seriru13/items/c2f5192615162c4c3f47/)

[gccの概要 - Qiita](https://qiita.com/keitean/items/28a519ec1caa8c15abcf/)

[Linux環境にてC++で静的ライブラリ、動的ライブラリを作成する方法に関して - Elsaの技術日記(徒然なるままに)](https://elsammit-beginnerblg.hatenablog.com/entry/2021/11/03/230934/)

[gcc の使い方 - 東京女子大学情報処理センター](https://www.cis.twcu.ac.jp/~asakawa/chiba2002/gcc_usage/)

[C++ - .o、.a、.so ファイルの違いは何ですか? - Stack Overflow](https://stackoverflow.com/questions/30186256/)

[実行時リンカーが検索するディレクトリ](https://docs.oracle.com/cd/E19253-01/819-0391/chapter3-10898/index.html)

[ライブラリとは何なのか？ - Qiita](https://qiita.com/false-git@github/items/4b531467788b446a18d2)

[C言語で静的・動的ライブラリを作成・使用する方法 - Qiita](https://qiita.com/Pirlo/items/8261361f2c153e104054)