注意
本稿はわかりやすさを重視したため,ヘッダファイルでの関数の定義を行なっています.
(2018/5/6 13:16 修正しました)
ヘッダファイルでの関数の定義はコンパイル時間やファイルの実行速度などの観点から推奨されていません.
詳しいヘッダファイルの書き方は次の記事が参考になりますのでご参照ください.
https://qiita.com/MoriokaReimen/items/7c83ebd0fbae44d8532d
記載通りのコンパイル方法でコンパイルが通ることは執筆時に確認していますが,
環境に依存した書き方かもしれません.
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.12.6
BuildVersion: 16G29
$ gcc --version
gcc-7 (Homebrew GCC 7.3.0) 7.3.0
Copyright (C) 2017 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.
本稿で掲載しているコードはあくまで説明用なので効率的ではない・冗長である場合が多いです.
例えば,配列の初期化なら本稿のようにポインタを渡して初期化するのではなく,
単に初期化したものをポインタで返す関数の方が良さそうです.
本題
例えば以下のようなソースコードarrayplus.cがあったとします.
# include <stdio.h>
int main(){
int a[5] = { 1, 2, 3, 4, 5}; // このコードが通らない場合は
int b[5] = { 1, 0, -1, -2, -3}; // "for文を用いた初期化"へ
int c[5];
for(int i = 0; i < 5; i++){
c[i] = a[i] + b[i];
}
printf("c = [");
for(int i = 0; i < 5; i++){
printf("%d,", c[i]);
}
printf("\b]\n");
return 0;
}
$ gcc -O2 -o arrayplus.out arrayplus.c
$ ./arrayplus.out
c = [2,2,2,2,2]
上記を例に変数,関数,ヘッダファイルの利用について解説します.
変数を使う
まず,配列の要素数が全て同じなので変数sizeで定義できるようにしましょう.
# include <stdio.h>
int main(){
int size; size = 5; //これは私の習慣であってint size = 5;でもOKです
int a[size] = { 1, 2, 3, 4, 5};
int b[size] = { 1, 0, -1, -2, -3};
int c[size];
for(int i = 0; i < size; i++){
c[i] = a[i] + b[i];
}
printf("c = [");
for(int i = 0; i < size; i++){
printf("%d\n", c[i]);
}
printf("\b]\n");
return 0;
}
これで一箇所変えるだけで全ての配列の宣言における要素数及びfor文を書き換える手間を省けました.
初期化の際,このように配列の要素数などはできるだけ数値をそのまま5などと打つのではなく変数を使う方が良いです.
また,int a[size*4%3]のように[]内で演算を行うのも避けた方が身のためです1.
これは要素数を負で宣言してしまうなどのバグの温床になりやすいからです.
関数を使う
配列を初期化する:等差数列
まず,配列a, bですが,等差数列で初期化しています.
int a[size] = { 1, 2, 3, 4, 5}; //初項1, 交差1の等差数列
int b[size] = { 1, 0, -1, -2, -3}; //初項1, 交差-1の等差数列
これは,そのままmain関数内でfor文で回すことがいちばん簡単です.
int a[size];
int b[size];
for(int i = 0; i < size; i++){
a[i] = 1 + i * 1;
b[i] = 1 + i * (-1);
}
ただしこれは配列a,bが両方同じ要素数を持つためであって,
例えばそれぞれが異なる要素数size, size2を持つなら
int a[size];
int b[size2];
for(int i = 0; i < size; i++){
a[i] = 1 + i * 1;
}
for(int i = 0; i < size2; i++){
b[i] = 1 + i * (-1);
}
のようにfor文を2つ書かなければいけません.
そこで,配列を等差数列に初期化してくれる関数を考えます.
void range(int *a, int size, int start, int step){
// 等差数列で初期化
for(int i = 0; i < size; i++){
a[i] = start + i*step;
}
}
これにより,配列の初期化は
int a[size]; range(a, size, 1, 1);
となり,たとえ配列の要素数が異なっていても
int a[size]; range(a, size, 1, 1);
int b[size2]; range(b, size2, 1, -1);
のようにシンプルに記述することが可能になりました.
配列を初期化する:全部を同じ要素に
次に配列cですが,初期化していない配列を扱うのはなにかと危険なので,大抵の場合は0などで初期化しておくと良いとされています.
そこでもう少し一般化して「与えられたint型の数字で初期化する」関数を定義しましょう.
void init(int *a, int size, int num){
// 全ての要素を特定の数字で初期化
for(int i = 0; i < size; i++){
a[i] = num;
}
}
もちろん,全零配列に特化して作るのも有効です.
void zeros(int *a, int size){
// 全ての要素を0で初期化
for(int i = 0; i < size; i++){
a[i] = 0;
}
}
これにより,汎用性は失われましたが関数の引数を減らすことができます.
また,全部0で初期化するには
int a[size] = {0};
とすればよいそうです.
range,zeros,initについて
お気づきの方もいらっしゃるかと思いますが,zerosとinitはrangeで実現できます.
//zerosと同じ
int a[size]; range(a, size, 0, 0);
//initと同じ
num = 1
int b[size]; range(a, size, num, 0);
本稿ではnumpyなどを参考にして関数の例として示させていただきました.
実装の際にはまとめてしまうのも良い手だと思います.
配列を出力する
これで本当にrange,init/zerosを用いてa,b,cは所望の要素を持つ配列に初期化されたのでしょうか?
それを確かめる方法としてよく用いられるのは配列の中身を全て出力するものです.
# include <stdio.h>
void print(int *a, int size){
// 配列の中身を出力
printf("[");
for(int i = 0; i < size; i++){
printf("%d,", a[i]);
}
printf("\b]\n");
}
この関数はデバッグ目的でよく使います.
動作確認
さて,これまでを踏まえて初期化及び配列の要素と動作の確認をしてみましょう.
# include <stdio.h>
void range(int *a, int size, int start, int step){
// 等差数列で初期化
for(int i = 0; i < size; i++){
a[i] = start + i * step;
}
}
void init(int *a, int size, int num){
// 全ての要素を特定の数字で初期化
for(int i = 0; i < size; i++){
a[i] = num;
}
}
void zeros(int *a, int size){
// 全ての要素を0で初期化
for(int i = 0; i < size; i++){
a[i] = 0;
}
}
void print(int *a, int size){
// 配列の中身を出力
printf("[");
for(int i = 0; i < size; i++){
printf("%d,", a[i]);
}
printf("\b]\n");
}
int main(){
int size; size = 5;
int a[size]; range(a, size, 1, 1);
int b[size]; range(b, size, 1, -1);
int c[size]; init(c, size, 0); //zeros(c, size);でも可
printf("a = ");print(a, size);
printf("b = ");print(b, size);
printf("c = ");print(c, size);
for(int i = 0; i < size; i++){
c[i] = a[i] + b[i];
}
printf("\n");
printf("c = ");print(c, size);
return 0;
}
$ gcc -O2 -o arrayplus.out arrayplus.c;./arrayplus.out
a = [1,2,3,4,5]
b = [1,0,-1,-2,-3]
c = [0,0,0,0,0]
c = [2,2,2,2,2]
どうやらきちんとrange,init,printは動作してくれたようです.
そして,関数を定義したことによりmain関数のなかがスッキリしました.
一方で,main関数の記述がソースコードの中で下の方になってしまいました.
今回はまだマシですが,更に関数を増やすとmainに辿り着くのが大変になりそうです.
解決手段の一つはプロトタイプ宣言です.
プロトタイプ宣言
関数を宣言だけしておいて中身を後で書くものです.
# include <stdio.h>
// 等差数列で初期化
void range(int *a, int size, int start, int step);
// 全ての要素を特定の数字で初期化
void init(int *a, int size, int num);
// 全ての要素を0で初期化
void zeros(int *a, int size);
// 配列の中身を出力
void print(int *a, int size);
int main(){
//省略
}
void range(int *a, int size, int start, int step){
// 等差数列で初期化
for(int i = 0; i < size; i++){
a[i] = start + i*step;
}
}
void init(int *a, int size, int num){
// 全ての要素を特定の数字で初期化
for(int i = 0; i < size; i++){
a[i] = num;
}
}
void zeros(int *a, int size){
// 全ての要素を0で初期化
for(int i = 0; i < size; i++){
a[i] = 0;
}
}
void print(int *a, int size){
// 配列の中身を出力
printf("[");
for(int i = 0; i < size; i++){
printf("%d,", a[i]);
}
printf("\b]\n");
}
ただ幾分かマシになっただけで根本的な解決には至ってません.
ヘッダファイルを使う
そこで本稿が推奨するのがもう一つの方法であるヘッダファイルへの関数の切り出しです.
関数の切り出し
新たにarray.h,array.cを作成します.
全てのプロトタイプ宣言をarray.hに,全ての関数の記述をarray.cに移行します.
# ifndef ARRAY_H //二重でincludeされることを防ぐ
# define ARRAY_H
// 等差数列で初期化
void range(int *a, int size, int start, int step);
// 全ての要素を特定の数字で初期化
void init(int *a, int size, int num);
// 全ての要素を0で初期化
void zeros(int *a, int size);
// 配列の中身を出力
void print(int *a, int size);
# endif
# include <stdio.h>
# include "array.h" // ここで必ず"array.h"をincludeする
void range(int *a, int size, int start, int step){
// 等差数列で初期化
for(int i = 0; i < size; i++){
a[i] = start + i*step;
}
}
void init(int *a, int size, int num){
// 全ての要素を特定の数字で初期化
for(int i = 0; i < size; i++){
a[i] = num;
}
}
void zeros(int *a, int size){
// 全ての要素を0で初期化
for(int i = 0; i < size; i++){
a[i] = 0;
}
}
void print(int *a, int size){
// 配列の中身を出力
printf("[");
for(int i = 0; i < size; i++){
printf("%d,", a[i]);
}
printf("\b]\n");
}
移行した関数を使うにはarray.hをincludeします.
# include <stdio.h>
# include "array.h" // <>ではなく ""で囲う
int main(){
// 省略
}
.
├── array.h
├── array.c
└── arrayplus.c
早速コンパイルして実行してみましょう.
コンパイルするには新たにarray.cを指定する必要があります.
$ gcc -O2 -o arrayplus.out arrayplus.c array.c
切り出す前と同じ結果が得られるはずです.
$ ./arrayplus.out
a = [1,2,3,4,5]
b = [1,0,-1,-2,-3]
c = [0,0,0,0,0]
c = [2,2,2,2,2]
利点
改めて,関数及びヘッダファイルへの切り出しの利点について解説します.
1. main関数の処理がみやすくなる
例えば等差数列で初期化された100個の要素をもつ配列が欲しい場合,関数がない場合は
int a[100] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100};
あるいは
int a[100];
for(int i = 0; i < 100; i++){
a[i] = i + 1;
}
と書かなければいけなかったものが,
int a[100]; range(a, 100, 1, 1);
この1行で終わります,
このように配列の初期化を簡潔に指定して行うことができます.
初期化の仕方に合わせて関数を予め作っておけば,
あらゆる配列の初期化が簡単に行えるわけです.
2. 他のソースコードでもincludeすれば定義されている関数が使える
例えば新たにarraysub.cというファイルを書いたとします.
# include <stdio.h>
# include "array.h"
int main(){
int size; size = 5;
int a[size]; range(a, size, 1, 1);
int b[size]; range(b, size, -1, -1);
int c[size]; init(c, size, 0);
for(int i = 0; i < size; i++){
c[i] = a[i] - b[i];
}
printf("c = ");print(c, size);
return 0;
}
$ gcc -O2 -o arraysub.out arraysub.c array.c; ./arraysub.out
c = [0,2,4,6,8]
このようにarray.hをincludeするだけで他のプログラムでも再利用ができます.
つまり,車輪の再発明をしなくても済むわけです.
3. main関数をいじることなくパラメータを変える
前準備において配列の要素数をsizeとしてmain関数内で定義してました.
よく使われるのはmain関数の外側で#defineで定義する方法です.
新たにarray_test.cを用意します.
main関数内で初期化される配列aの大きさ,初項,等差を全て#defineで定義します.
# include <stdio.h>
# include "array.h"
# define ARRAY_SIZE 5
# define ARRAY_START 1
# define ARRAY_STEP 1
int main(){
//私の場合は習慣としてローカル変数へ代入してから使います.
int size; size = ARRAY_SIZE;
int start; start = ARRAY_START;
int step; step = ARRAY_STEP;
int a[size]; range(a, size, start, step); //引数が全て変数に
printf("a = ");print(a, size);
return 0;
}
さらに,define.hを作成し全ての#define構文を切り出します.
# ifndef DEFINE_H
# define DEFINE_H
# define ARRAY_SIZE 5
# define ARRAY_START 1
# define ARRAY_STEP 1
# endif
# include <stdio.h>
# include "define.h" // `define.h`をinclude
# include "array.h"
int main(){
//省略
}
.
├── array_test.c
├── array.c
├── array.h
└── define.h
コンパイルして実行してみましょう.
$ gcc -O2 -o array_test.out array_test.c array.c; ./array_test.out
a = [1,2,3,4,5]
これにより,define.hの数値を書き換えてるだけで配列aの初期化方法を定義できるようになりました.
ソースコードに手を加えるというのはそれだけでバグを生む要因となります.
なので完成してうまく動作するものにはできるだけ手を触れない方が良いです.
そこで,関数として切り出して隔離する手段が有効なわけです.
欠点とその回避法
1. 命名
自作ヘッダファイルを用いる場合,自作した関数の振る舞いを
main関数を読んだだけでは(未来の自分を含む)他人には把握しづらいかもしれません.
例えば,初めて見た人に下記のコードが配列aを等差数列で初期化しているとはわからないはずです
(Pythonに馴染みのある方なら察しがつくかもしれないですが).
int a[10]; range(a, 10, 1, 1);
そのため,main関数で初出の関数についてはコメントなどで補足することが手助けとなります.
// range:配配列を等差数列で初期化する関数
// 引数:列のポインタ,要素数,初項,差
// 定義しているヘッダファイル:array.h
int a[10]; range(a, 10, 1, 1);
たいていの場合は1文目だけで十分です.
他人が見る場合があるのであれば関数を定義しているヘッダも記載しておくと親切でしょう.
また,ヘッダファイル内の記述もコメントを多く残しておくことが大切です.
あるいはもっと明示的な命名,例えば上記の例でしたらinitArithmeticSequence()とするのも良いです.
2. ヘッダファイルの読み込み順
これはheader.hをうまく使うことである程度緩和できます.
また,header.hにプロトタイプ宣言を記載しておくと個々のヘッダファイルにアクセスせずとも
関数の一覧が把握できるため有用です.
# ifndef HEADER_H
# define HEADER_H
# include "define.h"
// 等差数列で初期化
void range(int *a, int size, int start, int step);
// 全ての要素を特定の数字で初期化
void init(int *a, int size, int num);
// 全ての要素を0で初期化
void zeros(int *a, int size);
// 配列の中身を出力
void print(int *a, int size);
# endif
番外編:Makefileで一括コンパイル
この先どんどんヘッダが増えていった場合,コンパイルで指定するのも億劫になります.
$ gcc -O2 -o main.out main.c array.c dist.c func.c ... (続)
そこでMakefileを使って管理しましょう.
例として,headerfilesディレクトリ内にarray.c, dist.c, func.cがあり,
親フォルダにそれらをまとめるheader.hがあるとします.
.
├── Makefile
├── header.h
├── headerfiles
│ ├── array.c
│ ├── dist.c
│ └── func.c
└── main.c
array.c, dist.c, func.cの中身は以下の通りです.
//このコード内で用いるヘッダのみinclude
# include <stdio.h>
# include <math.h>
//必ずinclude
# include "../header.h"
void range(int *a, int size, int start, int step){
// 省略
}
void init(int *a, int size, int num){
// 省略
}
void zeros(int *a, int size){
// 省略
}
void print(int *a, int size){
// 省略
}
//このコード内で用いるヘッダのみinclude
# include <stdlib.h>
# include <math.h>
//必ずinclude
# include "../header.h"
double binom(){
// 省略
}
double normal(){
// 省略
}
//このコード内で用いるヘッダのみinclude
# include <stdio.h>
//必ずinclude
# include "../header.h"
void printout(){
// 省略
}
header.hの中身はarray.c, dist.c, func.cの関数を列挙します.
# ifndef HEADER_H
# define HEADER_H
// define.hの内容をここに書く
# define ARRAY_SIZE 5
# define ARRAY_START 1
# define ARRAY_STEP 1
// from array.c //出自を書く
// 等差数列で初期化
void array_range(int *a, int size, int start, int step);
// 全ての要素を特定の数字で初期化
void array_init(int *a, int size, int num);
// 全ての要素を0で初期化
void array_zeros(int *a, int size);
// 配列の中身を出力
void array_print(int *a, int size);
// from dist.c //出自を書く
// 二項分布
double dist_binom();
// 正規分布
double dist_normal();
// from func.c //出自を書く
// 出力
double func_printout();
# endif
あとはmain.cでheader.hをincludeします.
# include <stdio.h>
# include <stdlib.h>
# include "header.h"
int main(){
// 省略
}
準備が出来たらMakefileを作成します2.
PROGRAM = main
OBJS = headerfiles/array.o headerfiles/dist.o headerfiles/func.o
CC = gcc
CFLAGS = -O2
$(PROGRAM): $(PROGRAM).o $(OBJS)
$(CC) -o $(PROGRAM) $(PROGRAM).o $(OBJS) -lm
$(OBJS) $(PROGRAM).o: header.h
clean:
rm -f *.o headerfiles/*.o
PROGRAM, OBJS, CC, CFLAGSはCで言うところの#DEFINEに近いです.
これで準備は整ったので,makeを実行しましょう.
$ make
gcc -O2 -c -o main.o main.c
gcc -O2 -c -o headerfiles/array.o headerfiles/array.c
gcc -O2 -c -o headerfiles/dist.o headerfiles/dist.c
gcc -O2 -c -o headerfiles/func.o headerfiles/func.c
gcc -o main main.o headerfiles/array.o headerfiles/dist.o headerfiles/func.o -lm
これで実行ファイルmainが生成されます.
.oファイルが邪魔になったらmake cleanで消せます.
補足
関数名に出自をつけるとわかりやすいかもしれないです.
その場合のheader.hは以下のようになります.
# ifndef HEADER_H
# define HEADER_H
// define.hの内容をここに書く
# define ARRAY_SIZE 5
# define ARRAY_START 1
# define ARRAY_STEP 1
// from array.c //出自を書く
// 等差数列で初期化
void array_range(int *a, int size, int start, int step);
// 全ての要素を特定の数字で初期化
void array_init(int *a, int size, int num);
// 全ての要素を0で初期化
void array_zeros(int *a, int size);
// 配列の中身を出力
void array_print(int *a, int size);
// from dist.c //出自を書く
// 二項分布
double dist_binom();
// 正規分布
double dist_normal();
// from func.c //出自を書く
// 出力
double func_printout();
# endif
補足2
インクルードガードには#pragma once を使う方法があります.
先ほどのheader.hの例でいうと,
ファイル頭の#ifndef HEADER_H,#define HEADER_H,ファイル末の#endifを消して
#pragma onceをファイルの1行目に書くだけです.
# pragma once
...