C LANGUAGE TECHNOLOGY

【C言語】関数とは【プロトタイプ宣言,引数,記憶クラス指定子とスコープ,関数内外の変数の初期化】

2021年7月12日

悩んでいる人

C言語の関数を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOS(Linuxカーネル)の授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
  • プログラミング歴15年以上,習得している言語: C/C++PythonSolidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開
  • 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
  • 最近は自然言語処理AIイーサリアムに関する有益な情報発信に従事.
    • (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
    • (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.

こういった私から学べます.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

関数とは

関数とは,一般的には,ある値(引数)を入れるとその値を利用して演算し,その結果(返り値)を返すものです.

C言語のコードは,多くの関数で構成されています.

C言語の標準ライブラリ関数(printf/scanf関数等)は,既に用意されていたものです.

それでは,main関数以外の関数を作ってみましょう.

関数定義の書式は,以下のようになります.

このうち,関数名以外,つまり,記憶クラス指定子,型名,引数宣言については,必要ない場合は省略することができます.

記憶クラス指定子は,後述します.

型名とは,その関数が返す値(返り値)の型のことです.

関数名は,その関数の名前であり,その関数を参照する時に利用されます.

関数名の命名規則は,変数名の命名規則に準じます.

変数名の命名規則を知りたいあなたは,データ型とはの記事を読みましょう.

引数宣言には,その関数への入力を変数として記述します.

この関数への入力のための変数のことを引数と呼びます.

関数の返り値は,return文の後に記述します.

return文が呼び出されると,その関数は終了し,返り値を呼び出した関数に返します.

値を返さない関数の型は,void型といいます.

また,値を返す型名を省略した場合はint型だとみなされます.

つまり,以下の2つは同じ意味になります.

main関数は,何に値を返すのかというと,OS(WindowsやLinux等)に値を返します.

より正確には,そのプログラムを起動した親プロセス(たいていの場合はシェル)に値を返します.

返す値が0の場合は正常終了したという意味で,0以外の場合は異常終了したという意味です.

main関数を終わりに以下のように書くことは,親プロセスに0という値を返していたということです.

プログラムの実行後に「echo $?」と実行すると,main関数の返り値が設定される特殊変数「$?」の値を表示できます.

main関数の返り値が0のコードを以下に示します.

実行結果は以下になります.4行目で0が表示されていることがわかります.

他の返り値で試してみたいあなたは,return.cの7行目の「return 0;」の0を他の値に変更してみて下さい.

関数のコード例

関数のコード例としてadd関数を紹介します.

add関数は,入力に2つのint型の引数をとり,出力としてその2つの引数の足し算の結果を返します(7~14行目).

7行目のadd関数の定義の最初のintは,この関数がint型の値を返すということを意味します.

別の言い方をすると,この関数はint型であると言えます.

次のaddというのは,この関数の名前です.

add関数のカッコ()の中では,引数を宣言します.

引数が2つあるときは,このように別々に宣言をします.

つまり,「int add(int a, b)」のように記述できません.

8行目の「{」からadd関数のブロックが始まります.

ここから6行下の14行目の「}」までがadd関数の定義部になります.

add関数の中では,変数sumを定義しています.

この変数sumには引数a,bの足し算の結果を代入し,変数sumをadd関数の返り値「return sum;」として返しています.

ここで,returnというのは予約語です.

この場合,add関数はint型を返すので,returnの後には,int型の変数,定数または式でなければなりません.

add関数で利用している変数a,b,sumはこのadd関数の内部変数であることに注意して下さい.

つまり,main関数で定義されているint型の変数a,b,sumと,add関数で定義している変数a,b,sumは別の変数となります.

C言語では変数は基本的に内部変数(ローカル変数)であるということを覚えて下さい.

外部変数(グローバル変数)は,後述します.

また,単に引数と書いていますが,渡す側の引数と渡される側の引数を区別したい場合,以下のように区別します.

  • 実引数(じつひきすう):渡す側の引数
  • 仮引数(かりひきすう):渡される側の引数

add関数の実引数は23行目,仮引数は7行目になります.

main関数の中でadd関数を利用するためには,23行目のように「sum = add(a, b);」と呼び出します.

このようにadd関数を呼ぶことで,main関数の変数a,bの値がadd関数のa,bに渡され(値がコピーされ)ます.

add関数で計算した結果(add関数の変数sum)が返されて,main関数の変数sumに代入(値がコピー)されます.

実行結果は以下になります.

3行目の行末で「2 3」を入力したら,その足し算の結果「2 + 3 = 5」が4行目に表示されていることがわかります.

関数のプロトタイプ宣言

関数のプロトタイプ宣言とは,その関数の名前,型,引数,返り値をあらかじめ宣言することです.

これらの情報は,コンパイラが関数をチェックする際に利用します.

プロトタイプ宣言は,関数を作る場合,その関数よりも前に,または,その関数を呼び出す関数よりも前に必ず宣言しなければなりません.

通常はコードの最初で宣言するか,ヘッダファイル内で宣言してインクルードします .

add.cのadd関数のプロトタイプ宣言は以下になります.

引数付きで書く場合,引数の名前はコンパイラに無視されるので,任意の名前を付けたり省略することが可能です.

例えば,add関数のプロトタイプ宣言は以下の2つのように書けます.

また,できるだけ関数定義部と同じ名前の引数にした方がよいです.

add.cのadd関数は呼び出されるmain関数よりも前に定義されていたので,定義と同時に宣言も行われたことになり,プロトタイプ宣言は必要ありませんでした.

このような場合には,プロトタイプ宣言をしてもしなくてもどちらでもよいです.

もし関数が呼び出されるよりも後にその関数の定義がある場合は,必ずその関数を呼び出す前にプロトタイプ宣言が必要です.

add関数のプロトタイプ宣言をして,main関数の後にadd関数を定義するコードは以下になります.

実行結果は以下になります.同じですね.

もし,このプロトタイプ宣言をし忘れてしまった場合,コンパイル時に「add関数が暗黙的な宣言になっている」旨の警告が出るので注意しましょう.

関数の引数

ここで,関数の引数について考えてみましょう.

C言語では,引数は値渡し(call by value)になります.

値渡しでは,ある関数を呼んでその関数に実引数を渡す時に,呼ばれる関数の仮引数には実引数の値をコピーして受け取ります.

つまり,呼ばれる関数の仮引数と呼ぶ関数の実引数は同じものを指すのではなく,仮引数には実引数のコピーを渡します.

仮引数と実引数は,別の場所(メモリ)に配置します.

このことが何も意味するのかというと,呼ばれた関数で仮引数を変更しても呼んだ関数の実引数は変化しないということです.

呼ばれた関数で呼ぶ関数の実引数を変更したい場合はポインタを利用する必要があります.

引数として渡せるのは,以下になります.

配列そのものをコピーして渡すことはできません.

ここで,関数の引数がない場合は,以下のように明示的にvoidを書いていました.

また,以下のようにvoidを省略できます.

C11の規格書「ISO/IEC 9899:2011」のセクション6.11.6項には以下のように書かれています.

The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.
空のカッコを伴う関数宣言子(関数原型形式の仮引数並びでない)の利用は,廃止予定である.

https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf

したがって,自分で書いたプログラムを将来書き直すことにならないように,引数がない場合は明示的にvoidを入れることをおすすめします.

記憶クラス指定子とスコープ

記憶クラス指定子には,以下のものがあります.

  • auto
  • static
  • extern
  • register
  • typedef

この中で,typedefはデータ型の別名を定義するものです.

typedefは,他の記憶クラス指定子と性質が異なるので,本記事では紹介しません.

typedefを知りたいあなたはこちらからどうぞ.

記憶クラスとは,簡単にいうと記憶(変数等)が格納されているメモリ領域(記憶域)の区別のことです.

記憶クラスは,動的と静的の2種類に分類できます.

動的な記憶クラスの変数は,その関数が呼ばれた時に生成され,呼ばれ終わったら破棄します.

これに対して,静的な変数は,プログラムが始まってから終了するまで存在し続け,常に値を保持します.

変数定義

今まで変数定義は以下でした.

厳密には変数定義は,記憶クラス指定子を利用して以下になります.(記憶クラス指定子は省略可能です.)

変数の分類

一般に変数は,以下の2つの表のように,動的な変数と静的な変数,内部変数と外部変数のようにカテゴリーの異なる分類をできます.

種類意味
動的な変数関数の呼び出し時に生成,終了時に破棄
静的な変数プログラム開始から終了まで存在,常に値を保持

種類意味
内部変数関数の内部で定義されている変数
外部変数関数の外部で定義されている変数

自動変数(auto変数)

自動変数(auto変数)は,これまで関数内で定義して利用した動的な記憶クラスの変数です.

自動変数は,記憶クラス指定子にautoを指定します.

動的な記憶クラスの変数は,内部変数に限られます.

しかし,これまでは記憶クラスを指定していませんでした.

これは内部変数のデフォルトの記憶クラスはautoなので,記憶クラス指定子を省略すると自動的にautoの記憶クラスになります.

したがって,autoという記憶クラス指定子を明示的にコード中に記述することは,めったにありません.

つまり,以下の2つは同じ意味になります.

auto変数で明示的に定義するコードを以下に示します.

ここでは,記憶クラス指定子autoを明示的に指定して変数定義をしています.

例えば,add関数の変数a,b,sumは動的な記憶クラスです.

main関数の15行目と20行目でそれぞれadd関数を呼び出した時,同様に変数a,b,sumの記憶域が生成されます.

15行目と25行目でadd関数を呼びだした時の記憶域のアドレスは別になります .(ただし,この場合はたまたま同じになる可能性が高いです.)

このように生成と破棄を繰り返すことが動的な記憶クラスの変数です.

実行結果は以下になります.

register変数

記憶クラス指定子registerを指定すると,CPUのレジスタに直接記憶域を生成します.

したがって,register変数は,一般のメモリ上に記憶域をとる変数と比較して,高速に演算をすることができます.

また,register変数も動的な記憶クラスを持ちます.

register変数で指定できる型は,CPUの自然長(レジスタ長)に収まるサイズの型でなければなりません.

現在のCPUのレジスタ長は64ビット(8バイト)がほとんどです.

したがって,register変数で定義できる型は以下になります.

  • 整数型
  • 文字型
  • ポインタ型

ここで注意しなければならないのは,CPUのレジスタには数に限りがあるということです.

例えば,Intel x64シリーズ(XeonやCore i系等)にはregister変数に割当可能なレジスタはわずか数本しかありません.

ARM64(AARCH64)の場合は30本程度ありますが,コンパイラにより予約されているレジスタもありますので,実際に自由に利用できるレジスタは少ないです.

もしregister変数を利用しすぎてレジスタに割り当てられなくなった場合は,ただのauto変数になります.

このようにregister変数を多く利用してもエラーにはなりませんので,本当にCPUのレジスタに割り当て高速に演算処理したい変数のみにregisterを付けましょう.

register変数を利用したadd関数のコードは以下になります.

実行結果は以下になります.

静的な変数(static変数)

静的な変数(static変数)とは,固定アドレスの記憶域を持っていて,プログラムの開始から終了まで存在している変数のことです.

静的な記憶域を持つ変数は以下になります.

  • static宣言をした内部変数
  • 全ての外部変数

内部変数に記憶クラス指定子staticを指定して変数定義をすると,静的なローカル変数を作成できます.

これまでの動的な変数は,関数が呼ばれるたびに生成して破棄しましたが,静的な変数は関数の呼び出しが終了しても破棄することなく,値を保持します.

つまり,再びその関数を呼び出した時にも以前の値を保持しています.

したがって,その関数の内部状態を保持することに便利です.

例えば,ある関数内で以下のように変数定義すると,その関数内だけで有効なint型の静的な変数sumを定義したことになります.

main関数でfunc_auto関数とfunc_static関数をMAX回(値は5)呼び出して返り値を表示するコードは以下になります.

func_auto関数は,関数内で自動変数numを0に初期化し,次にインクリメントしてその値(1)を返します.

func_auto関数の変数numは,動的な記憶クラスを持っているので,func_auto関数が呼ばれるたびに変数numを生成して0に初期化します.

したがって,この関数は何回呼んでも返り値は1になります.

func_static関数は,プログラムの実行前に静的な変数numを0に初期化します.

そしてインクリメントし,その値(1)を返します.

関数が終了した後も変数numの記憶域を破棄せず値を保持しています.

したがって,2回目に呼び出された時には,変数numの値は1であり,その値をインクリメントした値(2)が関数の返り値になります.

同様にして,5回呼ばれた時のfunc_static関数の返り値は5になります.

実行結果は以下になります.

呼び出し毎に,func_static関数で定義された静的な変数は値が1から5まで増えます.

これに対して,func_auto関数で定義された動的な変数は値が1のままであることがわかります.

外部変数(グローバル変数)

全ての関数の外側で変数を定義すると,異なる関数間で利用できる外部変数(グローバル変数)を定義できます.

外部変数は,全て静的な記憶クラスを持っていて,プログラムの開始から終了まで存在し続けます.

ここで,変数の宣言と定義の違いを説明します.

変数の宣言とは,その変数を利用できると宣言しているだけで,実際にその変数用の記憶域を確保するわけではありません.

つまり,他の場所で定義された変数を参照する時には,変数宣言のみを行います.

これに対して,変数の定義とは,実際にその変数用の記憶域を確保します.

また,変数を定義すると同時に変数を宣言したことになります.

外部変数を利用してint型の加算をするadd関数を作成し,main関数でadd関数を呼び出すコードは以下になります.

7行目でint型の外部変数A,B に初期値0で定義しています.

また,同時に変数宣言をしたことにもなります.

外部変数の有効範囲(スコープ)は,関数の外側で外部変数の定義が行われている場合は,その宣言をした場所からファイルの最後までです.

外部変数でstatic定義していない場合は,他のファイルから参照できます(詳細は後で説明).

このコードの場合,main関数及びadd関数の両方で,外部変数A,Bを利用できます.

main関数では,15~16行目で外部変数A,Bにそれぞれ2,3を代入します.

次に,17行目で「a = add();」とadd関数を呼び出します.

add関数では,それらの足し算をした値を返します.

外部変数を無駄に多く利用すると関数が外部変数に依存してしまい,コードの保守性の観点でデバッグしにくくなるため,望ましくはありません.

実際,このコードのadd関数は,外部変数A,Bに依存しているので,良くない例です.

add4cのadd関数の方が優れています.

このコードは,外部変数の動作例を示すものだと考えて下さい.

内部変数のスコープ:ブロッグと変数の関係

これまで,内部変数は関数が始まるブロックの先頭で定義していました.

これに加えて,内部変数は,任意のブロックの先頭で定義します.

この場合,その内部変数の有効範囲(スコープ)は,そのブロック内に限定されます.

外側のブロックと内側のブロックで同じ名前の変数が定義されている場合は,内側のブロックの変数が有効になります.

内部変数のスコープを理解するコードは以下になります.

main関数の9行目の最初のブロックで変数a,b,cを定義しています.

この変数a,b,cはもちろんmain関数内全体で利用できます.

これらの変数の値を11行目のprintf関数で出力した後,12行目のブロック(複文)の開始の後で,このブロック内でのみ有効なローカル変数(局所変数)を定義しています(13行目).

この変数b,cは,ブロック外で定義されている変数b,cとはまったく別物です.

変数aに関しては,このブロック内でもブロック外で定義されているaを参照します.

したがって,このブロック内の変数aはブロック外で定義されたaであり,変数b,cはこのブロック内で定義されたb,cです.

このブロック内でb,cを変更しても,ブロック外のb,cとは別物なので,ブロック外の変数b,cは変更されません.

このように内部変数は,定義されたブロック内でのみ有効なローカル変数になります.

実行結果は以下になります.理解した通りの結果になっているか確認して下さい.

外部変数のスコープ:extern宣言で参照

次に,外部変数のスコープを考えてみましょう.

外部変数の有効範囲は,定義された場所からファイルの最後までです.

staticで定義されていない外部変数は,他のファイルから参照することができます.

global.cを以下の2つをファイルに分けたコードは以下になります.

extern_variables.cの5行目で「int A = 0, B = 0;」と外部変数A,Bの定義を行っています.

この外部変数をアクセスするために,extern_variables2.cの9行目で「extern int A, B;」と外部変数の宣言をしています.

記憶クラス指定子externを指定すると,変数は宣言のみで定義しません.

このように記述することで,extern_variables.cで定義されている外部変数A,Bを参照することができます.

記憶クラス指定子externは,一般にスコープ外の変数を参照したくて変数の宣言のみをしたいときに利用されます.

この場合では,ファイル外で定義されている(つまりスコープの外にある)外部変数を参照するために利用されます.

実行結果は以下になります.正常に動作していることがわかります.

extern_variables2.cは以下に示すextern_variables3.cのように書けます.

この2つファイルの違いは,外部変数のextern宣言の場所です.

extern_variables3.cでは,main関数の中の12行目でextern宣言をしているので,変数A,Bのスコープはmain関数内のみになります.

これに対して,extern_variables2.cの9行目のextern宣言は,ファイルの終わりまで有効です.

この場合は,main関数しかないので違いはありません.

しかし,もしmain関数の後ろにsub関数がある場合,外部変数A,Bをextern_variables2.cのsub関数では利用できますが,extern_variables3.cのsub関数では利用できません.

この場合,sub関数の中でもextern宣言をする必要があります.

実行結果は以下になります.

ここで,extern宣言は他のファイルで定義された外部変数を参照するためにのみ利用されると勘違いしやすいので注意して下さい.

あくまでスコープ外の変数を参照する時の変数宣言を行う時に利用します.

例えば,以下のextern_variables4.cでは,同一ファイル内でスコープ外にある外部変数A,Bを参照するために,main関数内でextern宣言をしています.

この場合,もちろんmain関数より上(main関数の外)でextern宣言しても構いません.

実行結果は以下になります.extern_variables4.cファイルのみでコンパイルして実行します.

通常の外部変数は,他のファイルでextern宣言をすれば参照することができます.

これに対して,外部変数を記憶クラス指定子staticで定義すると,そのファイル内でのみ有効な外部変数を作成できます.

この場合,外部変数は,そのファイル内の関数間でのみ利用可能なグローバル変数となり,他のファイルから参照することができないので注意して下さい.

関数のスコープ

通常の関数は,他のファイルで関数のプロトタイプ宣言をすれば参照できます.

これに対して,関数を記憶クラス指定子staticとともに定義すると,そのファイル内でのみ有効な関数を作成できます.

staticで定義した関数は,他のファイルから参照できません.

他のファイルから参照できる関数を作るためには,記憶クラス指定子externを指定します.

externは関数のデフォルトの記憶クラスなので,内部変数の場合のautoと同様に省略される場合が多いです.

関数のスコープを理解するためのコード例は以下になります.

extern_func.cの7行目で,このファイル内のみで有効な外部変数A,Bを定義しています.

また,このファイル内でのみ有効なprint_variables関数を定義しています(9~12行目).

これらの外部変数と関数は,このファイル内(extern_func.c)でのみ有効であり,他のファイルから参照することはできません.

同じ名前の外部変数A,Bとprint_variables関数をextern_func2.cでも定義していますが,これらはextern_func.cのものとは別物です.

func2関数で参照している外部変数A,Bとprint_variables関数はextern_func2.cで定義されているもので,func関数で参照している外部変数AとBはextern_func.cで定義されているものです.

これらを考慮して,extern_func.cとextern_func2.cを実行し,理解した通りの結果になるか確認してみて下さい.

記憶クラス指定子のまとめ

これまでに見てきたように,記憶クラス指定子は,内部変数に利用する場合と外部変数に利用する場合,関数に利用する場合でそれぞれ意味が異なります.

以下に簡単にまとめておきます.

C言語では記憶クラス指定子の使い分けは重要ですので,勘違いしないように正確に覚えましょう.

  • 内部変数で利用する場合
    • auto:自動変数(動的な記憶クラス,省略可能)
    • static:静的な変数
    • register:レジスタ変数(動的な記憶クラス)
    • extern:スコープ外の変数の参照(変数の宣言のみ)
  • 外部変数で利用する場合
    • static:そのファイル内でのみ有効な外部変数の定義
    • extern:スコープ外(他のファイルが多い)の外部変数の参照(宣言のみ)
  • 関数定義で利用する場合
    • static:そのファイル内でのみ有効な関数の定義
    • extern:他のファイルから参照可能な関数の定義(デフォルト,省略可能)
  • 関数のプロトタイプ宣言で利用する場合
    • static:そのファイル内でのみ有効な関数のプロトタイプ宣言
    • extern:他のファイルから参照可能な関数のプロトタイプ宣言(デフォルト,省略可能)

関数内外の変数の初期化

これまでのコードにも何度も出てきましたが,関数内外の変数は定義時に初期化することができます.

  • 動的な変数(自動変数,レジスタ変数)
    • 初期値を記述すると,実行時に初期化されます.初期値を与えないで変数を定義した場合には,初期値は不定になります.
  • 静的な変数(外部変数,static宣言した内部変数)
    • 初期値を記述すると,コンパイル時に初期化されます.初期値を与えないで変数を定義した場合には,初期値は自動的に0になります.
  • 配列
    • 配列の初期化も記憶クラスにより上記に準じます.

初期化について理解するために,以下のコードを実行してみて下さい.

実行結果は以下になります.

コンパイル時に16行目の変数aが初期化されていないと警告されています.

そのため,aの値が「347730048」と未定義の値になっていることがわかります.(実行毎に値は変動します.)

変数や配列の初期化について深掘りしたいあなたは,データ型とはの記事を読みましょう.

まとめ

C言語の関数を紹介しました.

具体的には,関数のプロトタイプ宣言,引数,記憶クラス指定子とスコープ,関数内外の変数の初期化について解説しました.

C言語の関数では多くの用語が出てきて大変ですが,少しずつ確実に習得していきましょう.

関数の再帰呼び出しも合わせて読むことをおすすめします.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , , , , , , , ,