Paepoi

Paepoi » C and GLib Tips » gcc で C/C++

gcc で C/C++

# 最終更新日 2021.02.27

Linux 環境で gcc を使って C 及び C++ 言語のビルド方法と注意点。
macOS の gcc とは別物なので macOS の場合は Clang のほうをご覧ください。
言語そのものは解説しません、本でも買ってください。

あえてコマンドや gedit でコンパイルする方法を解説します。
「ビルダー」等のアプリはコマンドでビルドが普通にできる人が使うものです。

C 言語のビルド
/* testc.c */
#include <stdio.h>

int
main (int argc, char *argv[]) {
	printf("はろーわーるど\n");
	return 0;
}
日本語を出力する何の変哲もない C 言語コードを書き test.c という名で保存。
リテラルに日本語を使う場合は LANG 変数と同じ文字コードで保存されている必要がある。
LANG は下記コマンドで確認、2021 年現在は全部 UTF-8 ですけど一応。
echo $LANG
#=>ja_JP.UTF-8
#
# ja_JP.utf8 の場合もある、内部表記のことなので気にしない
#
エディタの保存文字コードが気になるなら cat や less コマンドで確認。
LANG と一致しているなら文字化けせずに表示できます。
コンパイルするには端末を起動してそのディレクトリに cd コマンドで移動します。
gcc ファイル名
と打ち込むと a.out というファイルが作られます。
out という拡張子に意味は無い、単なるデフォルト名。
既に a.out というファイルが存在する場合は注意、容赦なく上書きされます。

コンパイルが成功すると何も表示されず入力待ちに戻ります。
Success とか ok なんて親切な出力はありません、UNIX ではそれが普通です。

実行パーミッション +x も付いているのでそのまま a.out を実行できる。
ただし当然パスは通っていないので「./」を付加、ドットは現在のパスに変換される。

img2/gcc_1.png

コードに間違いがあるとコンパイルエラーになり行番号付きで stderr が出力されます。
以下は printf の最後にセミコロンを入れ忘れた場合、以前より親切になっています。

img2/gcc_2.png

コマンドラインオプション
この方法だけでは全部 a.out という名前になる、-o オプションで別の名前を付けられます。
拡張子は不要というか無意味です、実行パーミッションがあれば動きます。
たとえば suzuki.gsr という名前にしたければ
gcc -o suzuki.gsr test_c.c
他のオプションについては以下を参照。
Man page of GCC

分割コンパイル
テストとして関数を定義したファイル。
/* func.c */
int plus (int x) {
    return x + x;
}
と main 関数を定義したファイル。
/* main.c */
#include <stdio.h>

int
main (int argc, char *argv[]) {
    printf ( "%d\n", plus(3) );
    return 0;
}
後は gcc func.c main.c のようにソースコードを全部指定する。
同一ディレクトリにまとめているなら gcc *.c のようにワイルドカードを使うのが簡単。
とにかくオブジェクト同士が合体して func.c で定義した関数が利用できるようになる。

ただしこの方法では「暗黙的な宣言」という警告が出ます。
ヘッダを作ってプロトタイプ宣言をしましょう。
Clang のページ のようにするのが正しい方法です。

C++ 言語のビルド
C++ 言語のコンパイルは g++ コマンドを使います。
class, STL 等は C++ 標準なので当然 Linux からも利用できます。
iostream.h と書いている古い本等がありますが C++ ヘッダに拡張子はありません。
// stl.cc
#include <iostream>
#include <vector>
#include <algorithm>

/**
 * みんな大好き C++ のテンプレート
 * C++11 より unary_function は非推奨
 * なんだけど、継承する必要が無くなっただけだったり
 */
template <class T>
struct outputfunc { // : public std::unary_function<T, void> {
    void operator()(T obj) {
        std::cout << obj << " ♪♪♪ ";
    }
};

class App {
public:
    App() {
        std::vector<std::string> vec = {"SUZUKIの", "バイクは", "カッコイイ"};
        // 関数オブジェクトにしたけど今は範囲ベース for のほうがいい
        std::for_each(vec.begin(), vec.end(), outputfunc<std::string>());
    }
    ~App() {
        std::cout << std::endl << "今日も元気でね!" << std::endl;
    }
    static auto init() {
        // auto で this を戻せる
        return new App();
    }
};

int
main(int argc, char* argv[]) {
    // イヂワルな初期化
    auto app = App::init();
    delete app;
    return 0;
}
Fedora 33 では上記で何のオプションも不要でビルド可能です。

img2/gcc_3.png

ところで、C++ ソースの拡張子は GNU 界隈では cc という拡張子が一般的です。
他に大文字の C や cxx でもいい、gcc の man ページ最初のほうに書いています。
Windows で使われる cpp でも一応 C++ と見なしてくれるようです。
ただ g++ は内容が C++ であれば c の拡張子でも問題なくビルドできる。

UNICODE
Windows しか知らない人は UNICODE を扱う wchar_t は UTF-16LE で 2byte だと思っている。
gcc で wchar_t は UTF-32(UCS-4) で 4byte である、_t のサフィックスがある変数は環境依存。
UTF-32 と UCS-4 は定義した団体が違うだけで同じものと思ってください。
正確には広義が UTF-32 ですが GLib の関数が UCS-4 の名前を使っているので Linux 必須の知識になる。
リテラルの場合は文字数分になりますが最低が 4 バイトのようです。
#include <stdio.h>
#include <wchar.h> // wchar_t
#include <uchar.h> // char32_t

// C11 対応の gcc にて

int
main (int argc, char const* argv[]) {
    /**
     * char16_t char32_t の確認
     */
    printf("リテラルのサイズは %lu バイト\n", sizeof('a'));
    printf("char のサイズは %lu バイト\n", sizeof(char));
    printf("wchar_t のサイズは %lu バイト\n", sizeof(wchar_t));
    printf("UTF-16(UCS-2) のサイズは %lu バイト\n", sizeof(char16_t));
    printf("UTF-32(UCS-4) のサイズは %lu バイト\n", sizeof(char32_t));
    return 0;
}

/* output
リテラルのサイズは 4 バイト
char のサイズは 1 バイト
wchar_t のサイズは 4 バイト
UTF-16(UCS-2) のサイズは 2 バイト
UTF-32(UCS-4) のサイズは 4 バイト
*/

x86_64
64bit アプリケーションではポインタサイズが 64bit(8byte) になる。
wchar_t のサイズが Windows とは違う他に 64bit では long も変わる。
#include<stdio.h>
#include<stdlib.h>

int
main (int argc, char *argv[]) {
    int * p;
    printf ( "int     = %lu\n", sizeof(int) );
    printf ( "long    = %lu\n", sizeof(long) );
    printf ( "pointer = %lu\n", sizeof(p) );
    printf ( "char    = %lu\n", sizeof(char) );
    printf ( "wchar_t = %lu\n", sizeof(wchar_t) );
    printf ( "size_t  = %lu\n", sizeof(size_t) );
    return 0;
    
    /* output
    Linux x86_64 | Windows x64
                 |
    int     = 4  | int     = 4
    long    = 8  | long    = 4
    pointer = 8  | pointer = 8
    char    = 1  | char    = 1
    wchar_t = 4  | wchar_t = 2
    size_t  = 8  | size_t  = 8
    */
}
なので Linux なら long 変数にポインタを突っ込...
バグの元なのでヤメて注意しましょう。

C99|C11
現行 Fedora の gcc v10 は標準で C99 に対応しています(// コメント等)
以前は -std=c99 オプションを付けることによりフル対応可能でした。
c89|c99 というラッパースクリプトもある。
#include<stdio.h>

// 以下のコマンドにすると C89 準拠になりビルドできない
// gcc -std=c89 hoge.c
// c89 hoge.c

int
main (int argc, char *argv[]) {
    // 変数宣言の簡易化
    //int i;
    //for (i=0; i<3; i++) {
    for (int i=0; i<3; i++) {
        printf("%d\n", i);
    }
    return 0;
}
C11 は一部対応、-std=c11 というオプションは無い。
#include <stdio.h>

// C11 test

int main (int argc, char const* argv[]) {
    /**
     * C11: fopen のモードに x 追加
     * 存在するファイルを開けないように指定
     * ちなみに fopen_s は Visual Studio だけ
     */
    FILE *fp = fopen("スズキ.txt", "w");
    if (fp == NULL) {
        printf("書き込みできません\n");
        return -1;
    } else {
        fputs("カッコイイ!", fp);
        fclose(fp);
    }
    fp = fopen("スズキ.txt", "wx");
    if (fp == NULL) {
        printf("存在するので書き換えできません\n");
        // gets の実験はこの return をコメントアウトしてね
        return -1;
    } else {
        fputs("ダサイ!", fp);
        fclose(fp);
    }
    /**
     * gets は削除されていない、警告は出るがビルドは可能
     * gcc や Clang では fgets 推奨
     * ちなみに gets_s は Visual Studio だけ
     */
    char buf[1024];
    printf( "何か入力してください > " );
    gets(buf);
    printf("入力されたのは「%s」です\n", buf );
    return 0;
}

C++11|C++14|C++17
現行 Fedora の gcc v10 は標準で C++14 まで標準対応しています。
#include <iostream>
#include <vector>
#include <algorithm>

int main (int argc, char const* argv[]) {
    /**
     * C++11 auto による型憶測や範囲ベース for ループ
     */
    const char suzuki[3][16] = {"SUZUKIの", "バイクは", "カッコイイ"};
    for (auto x : suzuki) {
        std::cout << x << std::endl;
    }
    /**
     * C++14 ラムダ式の拡張
     */
    std::vector<std::string> v = {"SUZUKIは", "クルマも", "カッコイイ!"};
    std::for_each( v.begin(), v.end(), [] (auto s) -> void {
        std::cout << s << std::endl;
    });
    return 0;
}
C++17 は一部対応、オプションが必要。
#include <iostream>

// C++ 17 構造化束縛、std::pair や std::tuple を Python のように受け取れる
// g++ -std=c++17 test_cc17.cc

std::pair<std::string, int> suzuki() {
  return {"GSR", 400};
}

int main (int argc, char const* argv[]) {
    auto [bike, cc] = suzuki();
    std::cout << bike << cc << std::endl;
    return 0;
}

Trigraph, Digraph
Trigraph は昔コンピューターで表記できる文字が少なかった時代に規定された三文字表記。
日本語キーボードは恵まれていますがコレが無いと困る言語圏もあったのです。
トライグラフ - Wikipedia
??=include <stdio.h>

/**
 * -trigraphs オプションにて利用可能
 * gcc -trigraphs hoge.c
 */
 
main() ??<
    printf("%s??/n", "はろー Trigraphs");
??>

Digraph は C99 で規定されたチョッピリ洗練版。
gcc はオプション無しで使える。
%:include <stdio.h>
 
main() <%
    printf("%s\n", "はろー Digraph");
%>

現在はキーボードにブレース等が無い言語圏でもすべて右 Alt キー経由で入力できるようです。
ということで歴史の長い C 以外の言語ではンなことできませんのであしからず。

Makefile
現行 Fedora 33 では make コマンドが入っていません。
C 言語をやるならそのうち必要になるので入れておきましょう。
sudo dnf install make
make コマンドは Makefile というファイルを読み取り各種コマンドを実行します。
以下の内容で拡張子を付けない Makefile という名前のファイルを作成。
注意としてインデントは \t で行うのを忘れないように。
# Makefile
hayabusa: suzuki.c
    gcc -o hayabusa suzuki.c
それを suzuki.c というソースがあるディレクトリに置きましょう。
そして端末から make コマンド、hayabusa という実行ファイルが作成されます。

また Gedit の外部ツールを有効にして「ビルド」を実行(Ctrl+F8)なんてこともできます。
Makefile の書き方については以下のサイトが解りやすかった。
Makefileの解説

GLib
GNOME 関連アプリのソースコードを見ると printf や char なんて使われていません。
GLib によるマクロ g_printf や gchar で記述されている。
様々な理由がありますが GLib に少しでも係るアプリを作るならこちらを使いましょう。

GLib ヘッダを include するには pkg-config 指定が必要です。
`pkg-config --cflags --libs glib-2.0`
GLib には日本語を扱うのに便利な関数も用意されている。
#include <glib.h>
#include <glib/gprintf.h>
 
/* gcc test.c `pkg-config --cflags --libs glib-2.0` */
 
int
main (int argc, char *argv[]) {

    gchar *c;
    gchar *n;
    GError *error;
    gunichar *ucs;
    glong len;
    gint i = 0;

    c = g_strdup("バーグマン 400");
    /* 漢字も一文字として長さを得る */
    g_printf ("%s は %ld 文字です\n", c, g_utf8_strlen(c, 1024));
    /* 漢字も一文字として切り取り */
    n = g_utf8_substring(c, 0, 5);
    g_printf ("%s 200\n", n);
    g_free(n);
    /* アルファベットのみ大文字化 */
    n = g_utf8_strup("Suzuki Hayabusa カッコイイ", -1);
    g_printf("%s\n", n);
    g_free(n);

    /* UTF-32(UCS-4) に変換 */
    ucs = g_utf8_to_ucs4 (c, -1, NULL, &len, &error);
    g_free(c);
    /* エラーチェック */
    if (!ucs) {
        g_printf (error->message);
        gint retval = error->code;
        g_error_free (error);
        return retval;
    }
    /* 一文字づつ書き出し */
    for (i; i<len; i++) {
        n = g_malloc0(4);
        g_unichar_to_utf8 (ucs[i], n);
        g_printf ("%s\n", n);
        g_free(n);
    }
    g_free(ucs);
    return 0;
}
これ以上細かいことはリファレンスマニュアルを参照ください(英語)。
GLib Reference Manual

GTK+
GTK+ ヘッダを include するにはやはり pkg-config 指定が必要です。
コンパイル毎に打ち込みなんてアホクサイので Makefile を利用しましょう。
# Makefile
gtkexe: test_gtk.c
	gcc -o gtkexe test_gtk.c `pkg-config --cflags --libs gtk+-3.0`
※gtk+ により glib, gio はインクルードされるので別途指定は不要

そして肝心なコード、ここでは GtkWindow を表示するだけの最小限サンプル。
#include <gtk/gtk.h>

int
main(int argc, char *argv[]) {
    GtkWidget *window;
    /* 初期化必須 */
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (G_OBJECT (window), "delete_event", gtk_main_quit, NULL);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}
C 言語なので PyGtk のようなメソッドではなく関数をひたすら利用していきます。
それと GTK+ 初期化関数の呼び出しは必須なので忘れないように。
これ以上細かいことはリファレンスマニュアルを参照ください。
GTK+ 3 Reference Manual
Copyright(C) sasakima-nao All rights reserved 2002 --- 2024.