次:上級者のための Hello World! 2 ~C/C++ プログラミング~
はじめに
今回は C/C++ を使ったプログラミングについて書きためていく。コンパイラは gcc を、OS は Ubuntu22.04 を使う。なお文中でのコンピュータとは、一つのハードウェアの上で一つの OS が動いているという状況をまとめてあらわす。また、文中でのコンパイルとは、かなり曖昧な意味を表す。
蛇足:詳しい仕様
$ 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.
C/C++ といえば、コンパイル型言語の代表である。しかし、初心者向けの解説は多いけれど中級、上級の解説は少ない。これは、ある程度は仕方ないことではあるが、あまり面白くない。今回は Hello World を題材に C 言語について書いていく。
必要なものをインストールしておく。
$ sudo apt install tree build-essential cmake pkg-config -y
実行可能ファイルの作成
とりあえず書いてみる
ではまず、普通に C 言語で書いてみよう。
適当な場所に hello_ws
ディレクトリを作り 移動して、その中に hello.c
を作成してみよう。ws はワークスペースの略である。touch
はファイルがいつ編集されたかを更新するが、空ファイルをつくるときによく使われる。以下をコピペしてほしい。
$ mkdir hello_ws && cd $_
$ touch hello.c
$ tree
.
└── hello.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
コンパイル
では、ここで -o
オプションで名前を指定しコンパイルした後、ls -l
コマンドを打ってみよう。ls
コマンドより詳しくファイルの情報を見ることができる。
$ 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
ここで左端の部分を見てほしい。下に詳細な表を書いたので赤字の部分を見てほしい。
読み、書き、実行とは、OS が各ユーザに与える権限である。もともと OS というのは多くの人間でつかうものである。r
や w
や x
がある部分は権限があり、-
の部分に権限はない。たとえば、赤の他人は hello.c
ファイルを読むことはできても上書きすることはできない。
ところで、読みと書きはともかく実行とは何だろうか?
コンピュータを使う人にとって実行可能とは、ファイル名を指定するだけでプログラムが動くということである。そのファイルがテキストファイルかバイナリファイルかは関係ない。
テキストファイルの実行
たとえば、Pythonを使ってみよう。hello.pyというファイルを作って実行権限を与える。chmod
は権限を変更するが、具体的な使い方は省略する。以下をコピペしてほしい。
$ touch hello.py && chmod +x $_
#!/usr/bin/env python3
print("Hello Python!")
hello.py
ファイルの 1 行目はシェバンと呼ばれる。省略しないでほしい。では実行してみよう。
$ ./hello.py
Hello Python!
実行
とにかく、今 実行可能な hello
ファイルが存在することがわかった。では実行してみよう。
$ ./hello
Hello World!
ディレクトリの整理とプリプロセス
つねに単一のファイルにソースコードを書くわけではない。ファイルを分割して整理しよう。
とりあえず、すべてのファイルを削除しておこう。
$ rm *
ソースコードとヘッダファイルを置くディレクトリを作成して、ファイルを置いておこう。以下をコピペしてほしい。
$ mkdir src include
$ touch main.c src/hello.c include/hello.h
$ tree
.
├── include
│ └── hello.h
├── main.c
└── src
└── hello.c
#ifndef HELLO_H
#define HELLO_H
void print_hello(void);
#endif
#include <stdio.h>
#include "../include/hello.h"
void print_hello(void)
{
printf("Hello World!\n");
}
#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
が単に文字列置換しかしないためだ。プリプロセスでは主に文字列が処理される。
ではコンパイルし実行してみよう。
$ gcc main.c src/hello.c -o hello
$ ./hello
Hello World!
インクルードディレクトリの指定
ヘッダファイルを探す場所をコンパイラにコマンドラインから教えてみよう。上記の .c
ファイル内の hello.h
のインクルードをすべて #include <hello.h>
に変更して以下のようにコンパイルしてみよう。
$ gcc main.c src/hello.c -o hello -I./include
$ ./hello
Hello World!
-I
オプションはヘッダファイルの存在するディレクトリを指定する。-I
と ./include
の間に空白が開いていないが誤字ではない。
ライブラリの作成
大規模な開発では、毎回すべてのファイルをコンパイルするのは大変である場合が多い。そこで、最初からよく使うモノをコンパイルしておくと便利である。
オブジェクトファイル
gcc では -c
オプションで中間ファイル ――正確には(リロケータブル)オブジェクトファイル―― を作成する。拡張子は o
である。lib
ディレクトリを作ってその中にライブラリや中間ファイルを置く。
$ mkdir lib && cd $_
$ gcc -c ../src/hello.c -I../include
これで hello.o
ができた。main.c
といっしょにコンパイルしてみよう。
$ 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
ディレクトリは必要であることに注目されたい。つまりこのときは下のような区別がディレクトリに存在する。
作成した hello.o
の中身を見ていると、バイナリファイルになっており文字化けしていることがわかる。変更した .c
ファイルだけを中間ファイルにコンパイルしておくと最後にまとめてくっつけることができる。しかし、.o
ファイル自体は単なる中間ファイルであり、人間が扱うことは少ない。
ライブラリ
バイナリの関数を呼び出すには、一般に静的ライブラリと共有ライブラリ(動的ライブラリ)の 2 種類の方法がある。
静的ライブラリは最終的なコンパイルのときに実行可能ファイル内に含まれるようになる。一方共有ライブラリは、プログラムの実行時に別の場所のファイルが呼ばれる。
静的ライブラリを使うと実行可能ファイルを別のコンピュータへコピーするだけで実行できるが、ファイルが大きくなる。一方共有ライブラリを使うと、ライブラリが存在しないと実行できないがファイルは小さくなる。
ライブラリは命名規則や呼び出しが初見ではわかりにくいが、落ち着いて見ればわかるはずだ。
静的ライブラリ
まず静的ライブラリを作り使ってみよう。先ほど作成した hello.o
と ar
(アーカイブ)コマンドを使う。
$ cd lib
$ ar r libhello.a hello.o
ar: creating libhello.a
これで libhello.a
というファイルができた。静的ライブラリは lib<ライブラリ名>.a
という名前にしなければいけない。
$ 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:<ファイル名>
を使う必要がある。
$ gcc main.c -o hello_a -I./include -L./lib -l:libhello.a
あるいは以下のようにする方法もある。-Wl,
オプションは続くオプションをリンカに渡す。-Bstatic
や -Bdynamic
は後ろに続くライブラリについて、静的ライブラリを使うか、動的ライブラリを使うか決める。-Bdynamic
の後ろには何もないように見えるが、これは自作ではないライブラリのためである。
$ 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
共有ライブラリ
次に共有ライブラリを作り使ってみよう。
$ cd lib
$ gcc ../src/hello.c -o libhello.so -I../include -shared
-shared
オプションを使って libhello.so
という共有ライブラリができた。 共有ライブラリは lib<ライブラリ名>.so
という名前にしなければならない。ただし libhello.so.1.0
のようにバージョンをファイル末尾に書いて、libhello.so
へのシンボリックリンク(Windowsにおけるショートカット)を張ることもある。それではコンパイルしてみよう。
$ 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
を実行してみてほしい。下のようなエラーを吐くはずだ。
$ ./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
ファイルを思い出してほしい。あれはリンクが終わっていない状態である。
実行するときだけライブラリのあるディレクトリを指定することも可能だ。新しいほうのターミナルで以下を実行してほしい。
$ LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH ./hello_so
Hello World!
プログラムがどの共有ライブラリを必要としているかは ldd
コマンドでわかる。ぜひ両方のターミナルで実行してほしい。
$ ldd hello_so
しかし、実は gcc
でのコンパイル時に共有ライブラリの場所を教えることもできる。
$ gcc main.c -o hello_so -I./include -L./lib -lhello -Wl,-rpath=$PWD/lib
-rpath
は ld
(リンクさせるコマンド)のオプションであり、ライブラリのあるディレクトリを指定しておく。これらの代わりに -R
オプションを使うという記事もあるが GCC では無効になっているようだ。1
比較
では比較してみよう。先ほどの ldd
コマンドで実行可能ファイルについて調べてみよう。
$ 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番目がファイルの大きさ(単位はバイト)である。
$ 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
だけが少し小さくなっている。
参考
ライブラリについて解説!静的・動的とは?それぞれのメリットは? | だえうホームページ
Linux環境にてC++で静的ライブラリ、動的ライブラリを作成する方法に関して - Elsaの技術日記(徒然なるままに)
C++ - .o、.a、.so ファイルの違いは何ですか? - Stack Overflow
C言語で静的・動的ライブラリを作成・使用する方法 - Qiita