LoginSignup
14
20

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

Last updated at Posted at 2023-09-24

次:上級者のための 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
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

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

スクリーンショット 2023-09-24 125828.png

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

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

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

テキストファイルの実行

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

ターミナル
$ touch hello.py && chmod +x $_
hello.py
#!/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
hello.h
#ifndef HELLO_H
#define HELLO_H

void print_hello(void);

#endif
hello.c
#include <stdio.h>
#include "../include/hello.h"

void print_hello(void)
{
    printf("Hello World!\n");
}
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 が単に文字列置換しかしないためだ。プリプロセスでは主に文字列が処理される。

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

ターミナル
$ 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 ディレクトリは必要であることに注目されたい。つまりこのときは下のような区別がディレクトリに存在する。

名称未設定ファイル.drawio (1).png

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

ライブラリ

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

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

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

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

静的ライブラリ

まず静的ライブラリを作り使ってみよう。先ほど作成した hello.oar (アーカイブ)コマンドを使う。

ターミナル
$ 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

-rpathld(リンクさせるコマンド)のオプションであり、ライブラリのあるディレクトリを指定しておく。これらの代わりに -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 だけが少し小さくなっている。

参考

ライブラリについて解説!静的・動的とは?それぞれのメリットは? | だえうホームページ

gcc コンパイルオプション備忘録 - Qiita

gccの概要 - Qiita

Linux環境にてC++で静的ライブラリ、動的ライブラリを作成する方法に関して - Elsaの技術日記(徒然なるままに)

gcc の使い方 - 東京女子大学情報処理センター

C++ - .o、.a、.so ファイルの違いは何ですか? - Stack Overflow

実行時リンカーが検索するディレクトリ

ライブラリとは何なのか? - Qiita

C言語で静的・動的ライブラリを作成・使用する方法 - Qiita

  1. C++ - gcc -R パラメーターは何をしますか? - Stack Overflow

14
20
0

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
14
20