はじめに
この記事は42Tokyoや大学の授業でなんとなく、#defineやインクルードガードを使用している方を対象としています。
少しでもプリプロセッサやインクルードガードに対する解像度が上がれば良いかなという気持ちで書きました。
厳密に正しくは無い部分もあるかもしれませんがお手柔らかにお願いします。
42Tokyoについて
プリプロセッサとは
C言語ではコンパイルの前に プリプロセス というという処理を行います。プリプロセスという言葉通り、前処理のことを指します。
このプリプロセスを行うプログラムのことをプリプロセッサと呼びます。
プリプロセッサでは主に、
- マクロ置換(記号定数、引数付きマクロ) 例:
#define BUF_LEN 10
- ファイルの取り込み 例:
#include <stdio.h>
#include "test.h"
を行います。
簡単に言うとプリプロセッサで#defineした定数を変換、includeしたファイルをinclude先に展開しています。
プリプロセッサの予約語
プリプロセッサは処理を行単位で行います。予約語は「#」で始まります。
予約語 | 補足 |
---|---|
#define | 定義 |
#undef | 定義を削除 |
#include | ファイルの内容を取り込む |
#if 条件 | |
#elif 条件 | |
#else | |
#ifdef HOGE | #if defined HOGE (HOGEが定義されているかどうか) |
#ifndef HOGE | #if !defined HOGE |
#endif | 条件分岐の終わりを表す |
#line 行番号 [ファイル名] | 次の行から行番号(ファイル名)を指定 |
#error | エラーメッセージを出力してコンパイルを終了 |
#pragma | 各コンパイラがOS固有の機能を提供、様々なオプションが存在 |
詳しい使い方は以下のサイトを参考にしてみてください
インクルードガード
同じヘッダファイルを複数回includeしてしまうと、その数だけヘッダファイルの内容がinclude先のファイルに展開されてしまいます。よって、同じ関数や構造体が複数定義されます。
その結果多重定義ということで、コンパイルエラーがでます。
例は次セクションの「良くない例」にあります。
つまり、インクルードガードとは、2重のインクルードを回避しコンパイルエラーを防止するための手法のことです。
インクルードガードは以下のように書きます。
#ifndef HOGE_H
#define HOGE_H
void function(void);
// ...
#endif
#ifndef HOGE_H
でHOGE_H
が定義されているかどうかをチェック
- 定義されている場合→何も展開しない
- 定義されていない場合
-
#define HOGE_H
を定義 -
#endif
までの内容も展開
-
よって、一度includeされたら次はHOGE_Hが定義されているので何も展開しません。
このような仕組みでインクルードガードは実現されています。
また、#pragma once
と書くだけでインクルードガードと同様の効果を得ることが出来ます。(後述)
プリプロセッサの例
ccやgccでコンパイルを行う際に-E
オプションをつけるとプリプロセス後の結果を標準出力に出すことが出来ます。
ここではプログラムの実行結果ではなく、-E
オプションをつけてコンパイルを行った際の結果を見ていきます。
紹介している例は以下のリポジトリから使用できます。ぜひ試してみてください。
一般的な例
#define TEST 0
#define BUF_LEN 10
int main(void)
{
char buffer[BUF_LEN];
return TEST;
}
プリプロセッサによって、上の方にその他の出力も含まれますがここでは無視します。
プリプロセッサによってBUF_LEN
やTEST
が置換されていることが分かります
$ cc -E test00.c
// ... 省略
int main(void)
{
char buffer[10];
return 0;
}
二重インクルード(良くない例)
int ft_strlen(char *str);
void ft_putchar(char c);
typedef struct s_list{
int data;
struct s_list *next;
} t_list;
#include "test01.h"
#include "test01.h"
int main(void)
{
return 0;
}
$ cc -E test01.c
インクルードガードを行っていないので同じ内容が2回展開されてしまっています。
# 1 "test01.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 418 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test01.c" 2
# 1 "./test01.h" 1
int ft_strlen(char *str);
void ft_putchar(char c);
typedef struct s_list{
int data;
struct s_list *next;
} t_list;
# 2 "test01.c" 2
# 1 "./test01.h" 1
int ft_strlen(char *str);
void ft_putchar(char c);
typedef struct s_list{
int data;
struct s_list *next;
} t_list;
# 3 "test01.c" 2
int main(void)
{
return 0;
}
この状態でコンパイルをすると、再定義でコンパイルエラーとなってしまいます。
$ cc test01.c
./test01.h:6:3: error: typedef redefinition with different types ('struct (unnamed struct at ./test01.h:3:16)' vs 'struct s_list')
} t_list;
^
test01.c:1:10: note: './test01.h' included multiple times, additional include site here
#include "test01.h"
^
test01.c:2:10: note: './test01.h' included multiple times, additional include site here
#include "test01.h"
^
./test01.h:6:3: note: unguarded header; consider using #ifdef guards or #pragma once
} t_list;
^
2 errors generated.
インクルードガード(#ifndef )
#ifndef TEST02_H
#define TEST02_H
int ft_strlen(char *str);
void ft_putchar(char c);
typedef struct s_list{
int data;
struct s_list *next;
} t_list;
#endif
#include "test02.h"
#include "test02.h"
int main(void)
{
return 0;
}
$ cc -E test02.c
# 1 "test02.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 418 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test02.c" 2
# 1 "./test02.h" 1
int ft_strlen(char *str);
void ft_putchar(char c);
typedef struct s_list{
int data;
struct s_list *next;
} t_list;
# 2 "test02.c" 2
int main(void)
{
return 0;
}
インクルードガードを入れることでこちらは、コンパイルも成功します。
インクルードガード(#pragma once)
#pragma once
と書くだけでインクルードガードと同じ効果があります。
#pragma once
int ft_strlen(char *str);
void ft_putchar(char c);
// 省略
デバック
DEBUGが定義されている時とされていない時で動作を変えることが出来ます。
このような条件付きコンパイルはデバック時に役立ちます。
コンパイル時に-D
オプションを付けることでマクロを定義できます。
#include <unistd.h>
int main(void)
{
#if DEBUG
write(2, "Debug mode\n", 11);
# else
write(1, "Normal mode\n", 12);
#endif
return 0;
}
$ cc test04.c -DDEBUG && ./a.out
Debug mode
$ cc test04.c && ./a.out
Normal mode
複数行マクロ
マクロは基本的に一行で記述しますが、\
をつけることで見やすく書く事ができます。
#include <stdio.h>
#include <stdlib.h>
#define mem_alloc(ptr, type, size) \
do { \
ptr = (type *)malloc(sizeof(type) * size); \
if (ptr == NULL) { \
fprintf(stderr, "Error: malloc() failed.\n"); \
exit(EXIT_FAILURE); \
} \
} while (0)
int main(void)
{
int *ptr;
mem_alloc(ptr, int, 10);
free(ptr);
return 0;
}
$ cc -E test05.c
// ... 省略
int main(void)
{
int *ptr;
do { ptr = (int *)malloc(sizeof(int) * 10); if (ptr == ((void *)0)) { fprintf(__stderrp, "Error: malloc() failed.\n"); exit(1); } } while (0);
free(ptr);
return 0;
}
mem_alloc(ptr, int 10);
という形式で使うため、かならず後ろにセミコロンをつけます。
よって、#define
で定義する際にdo{}while(0)
がないと、if{}
の後ろにセミコロンが来て、予期せぬ動作を起こす可能性があります。
ゆえにdo{}while(0)
をつけることで予期せぬ動作を防止し、呼び出し時に1回だけ実行されるような記述になっています。
実際に使用されている例
以前のGoogleのC++のコード規約で以下の様な例が使用されているようです。
現行ではC++11で導入されたdelete指定が使われているそうです。
参考:Google C++ Style Guide ・ 関数のdefault/delete宣言
C++のクラスではコピーコンストラクタと代入演算子を再定義しないと、デフォルトの設定が使用されてしまうために、メモリリークにつながる可能性があります。
そこでコピーコンストラクタと、代入演算子を再定義しprivateに置くことで誤ってこれらのメソッドを使用することを防いでいます。
この操作を効率化かつ、何を行っているかをわかりやすくするためにマクロが使われていると考えられます。
# define DISALLOW_COPY_AND_ASSIGN(ClassName) \
ClassName(const ClassName &); \
ClassName &operator=(const ClassName &)
class TEST {
private:
DISALLOW_COPY_AND_ASSIGN(TEST);
public :
TEST() {};
~TEST() {};
};
int main() {
TEST test;
return 0;
}
$ c++ -E test06.c
// 省略
class TEST {
private:
TEST(const TEST &); TEST &operator=(const TEST &);
public :
TEST() {};
~TEST() {};
};
int main() {
TEST test;
return 0;
}
注意
42にはC言語の課題において、norminetteというコード規約が存在します。
リテラルや定数以外の定義や複数行マクロなどは禁止です。
使用の際は注意してください。
おわりに
42Tokyoの課題に役立つかは微妙ですが、まとめてみると様々な使い方があることが分かりました。
より詳しい使い方などは以下のサイトなどを参考にしてみてください。
最後まで読んでいただきありがとうございました!
参考にしたサイト・書籍
- 「プログラミング言語C -入門から中級へ- 」 山﨑信行 著
修正
2023/12/31 12:30時点で頂いたコメントについて修正を加えました。