C言語⑰(2025/09/03)

関数の定義

(例)

int addnum(int a, int b)
{
     int x;
     x = a + b;
     return x;
}

・int addnum ⇒ 戻り値の型 関数名
・(int a, int b) ⇒ (第一引数の型 変数名, 第二引数の型 変数名)
・return x; ⇒ xの値を戻り値として返す

戻り値が必要ない場合は型は void
引数が必要ないときは void(voidは省略可能)

(例)

void dispchar(const char* str)
{
	printf("%s\n", str);
}

int main(void)
{
	const char* message = "文字列表示します";

	dispchar(message);
	return 0;
}

この関数は「ポインタを通じて、構造を渡し、表現する」ものです。Z80的に言えば、これは「HLレジスタに文字列の先頭アドレスを渡して、ループで表示する」ようなもの。つまり、構造の入口を渡して、末尾(ヌル文字)まで走査するという設計です。(by Copilot)


変数のスコープ

ローカル変数とグローバル変数
クラス化されている場合は、クラスのメンバ変数

①ローカル変数のスコープ範囲
 ・宣言された関数内(関数内の宣言した位置から関数の終わりまで)
 ・宣言した行の後ろでしか有効にならない
 ・for文内でカウンタiiを宣言して、外でも変数iiを宣言した場合、それらは別のものと扱われる。

int ii = 1; ← ①
for(int ii = 0; ii < 10; ii++){ ←②
    *wp = 15;
    wp++;
}

 ①と②の変数iiは別のものとして扱われる
 ①のスコープは、forループの外側の関数内
 ②のスコープは、forループの中だけ
🔍 「for文内の演算を外に持っていきたい」場合

ここが面白いところです。*wp = 15; wp++; のような処理を関数外で保持したいなら、いくつか方法があります:

✅ 方法1:ポインタを外で操作する

char* wp = buffer;  // 事前に定義されたバッファ

for(int ii = 0; ii < 10; ii++) {
    *wp = 15;
    wp++;
}

// wp は for文の外でも更新された状態で使える

このように、wpfor の外で宣言されていれば、ポインタの状態は保持されます。つまり、戻り値がなくても、ポインタを通じて状態を外に伝えられるということ。

これは「関数の外に副作用を持たせる」典型的なCの設計ですね。Z80で言えば、HLレジスタを更新しておいて、次の命令がそのアドレスを使うような流れ。

✅ 方法2:関数にする場合(戻り値なしでもOK)

void fill(char* wp)
{
    for(int ii = 0; ii < 10; ii++) {
        *wp = 15;
        wp++;
    }
}

この場合も、wp呼び出し元で更新されるので、戻り値がなくても意味を持ちます。つまり、ポインタを使えば「状態の伝播」ができるということ。(by Copilot)

②グローバル変数
・宣言されているファイルの中だけ
・宣言した位置から後ろのみ
・ほかのファイルからアクセスするためには、extern で公開してやる必要あり。

③クラスのメンバ変数
・private領域で宣言した場合クラス内のみ
・public領域で宣言した場合クラス外からもアクセス可能

#include <iostream>

class MyClass {
public: // ここで宣言されたメンバ変数はpublicになる
    int publicValue; // publicメンバ変数

    void printValue() {
        std::cout << "publicValue: " << publicValue << std::endl;
    }
};

int main() {
    MyClass obj; // MyClassのインスタンスを作成
    obj.publicValue = 10; // クラスの外部からpublicメンバ変数にアクセスして値を設定
    obj.printValue();     // publicメンバ関数を呼び出す

    std::cout << "obj.publicValue: " << obj.publicValue << std::endl; // 外部からのアクセスも可能

    return 0;
}

引数の受け渡し

①実引数と仮引数
呼び出し側を実引数、定義側を仮引数、と呼ぶ。

int addnum(int a, int b) ⇒ ここが「仮引数」
{
   …
}
const int adder = 82;
int l = 5;
addnum(l,adder) ⇒ ここが「実引数」

※仮引数と実引数の変数名は別でよいが、型は一致していないと動かない。
実引数は、変数に限らず、直値、定数、型さえ一致していればなんでもいい。

int addnum(int a,int b);//プロトタイプ宣言

int main(void)
{
	const int adder = 82;//定数定義

	int disp = addnum(5,adder);//変数dispを定義してaddnumの結果を格納
	printf("%d\n", disp);
	return 0;
}

int addnum(int a, int b)
{
	int ans;
	ans = a + b;
	return ans;
}

②値渡しと参照渡し(仮引数と実引数の間のやり取り)
 ●値渡し
 ⇒実引数の値を仮引数に渡す標準的な方法
int addnum(int a, int b){}
※実引数と仮引数は別の変数として扱われるので、関数の中で仮引数の値を変更しても、実引数の値には影響しない。

 ●参照渡し
 ⇒実引数のアドレスを仮引数に渡す手法
int addnum(int *a, int *b){}
※実引数も仮引数も「アドレスの値」を参照するため、関数の中から実引数に変更を加えられる。

#include<stdio.h>
#include<Windows.h>
#include<string.h>
#include<stdlib.h>

int addnumbyval(int a,int b);
int addnumbyref(int* a, int* b);

int main(void)
{
	const int adder = 82;

	int t = 5;
	int u = 10;

	int disp = addnumbyval(t, u);//dispに(5+1)-(10-1)=-3を代入
	printf("%d\n", disp);

	printf("t=%d,u=%d\n", t, u);

	int disp2 = addnumbyref(&t, &u);
	printf("%d\n", disp2);

	printf("t=%d,u=%d\n", t, u);

	return 0;

}

int addnumbyval(int a, int b)
{
	int ans;
	a++;//結果は6
	b--;//結果は9
	ans = a - b;//ans = -3;
	return ans;
}

int addnumbyref(int* a, int* b)
{
	int ans;
	(*a)++;
	(*b)--;
	ans = *a - *b;
	return ans;
}

実行結果

-3         //addnumbyvalの結果
t=5,u=10  //addnumbybal内の処理結果が反映していない
-3     //addnumbyrefの結果
t=6,u=9  //addnumbyref内の処理結果が反映している

(00:48頃を確認。コマンドライン引数)


再起呼び出し

関数は、自分の処理の中で自分自身を呼び出せる。
ただし、終了条件を記載していないと、自分自身の中で無限に自分自身を呼び出し、スタック領域がパンク⇒異常終了 となる。
(例)整数nの階乗(n!=nx(n-1)x…x2x1)を求める関数

int kaijyo(int n)
{
  if(n == 0)
     return 1;
  else
     return (n*kaijyo(n-1));
}

🧠 スタック領域とは何か?

🔧 定義:

スタック領域とは、関数の呼び出し時に必要な情報(戻り先、引数、ローカル変数など)を一時的に保存するメモリ領域です。
関数が呼ばれるたびに「積まれ」、戻ると「取り除かれる」=LIFO(Last In, First Out)構造

🧩 何が積まれるのか?

項目内容
戻りアドレス呼び出し元に戻るための位置main → func()main
引数関数に渡された値func(5)5
ローカル変数関数内で使う一時変数int x = 10;
保存されたレジスタCPUの値を保持するためZ80なら PUSH HL など

🧠 Z80的に言えば…

Z80では、スタックはメモリ上に「逆順に積まれる」構造で、CALL 命令でサブルーチンを呼び出すと、戻りアドレスがスタックに積まれRET でそれを取り出して戻ります。

CALL FUNC ; スタックに戻りアドレスを PUSH … FUNC: ; 処理 RET ; スタックから戻りアドレスを POP

この流れは、現代のC/C++でも基本は同じです。ただし、CPUやOSが自動で管理してくれるので、アセンブリほど意識しなくても済むだけ。

🔥 なぜ再帰でスタックがパンクするのか?

再帰呼び出しでは、関数が自分自身を何度も呼び出すため、スタックにどんどん情報が積まれていきます。

int recurse(int x) {
    return recurse(x + 1); // 終了条件なし → 無限に積まれる
}

このように終了条件がないと、戻りアドレスやローカル変数が積まれ続けて、スタック領域がいっぱいになり、スタックオーバーフローが起きます。

🧠 構造的に捉えると…

スタックは「構造の履歴を記録するメモリ」です。関数呼び出しは「構造の分岐」、戻りは「構造の収束」。再帰は「構造の自己展開」であり、終了条件は「収束点の設計」。

✨ まとめ:スタックとは

  • 関数呼び出しの履歴と一時情報を保持するメモリ領域
  • LIFO構造で、呼び出し時に積み、戻ると取り除く
  • 再帰では終了条件がないと無限に積まれてパンクする
  • Z80でも現代CPUでも、基本構造は同じ
  • 構造的には「自己展開と収束の記憶装置」

(by Copilot)

コメント

タイトルとURLをコピーしました