関数の定義
(例)
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文の外でも更新された状態で使える
このように、wp
が for
の外で宣言されていれば、ポインタの状態は保持されます。つまり、戻り値がなくても、ポインタを通じて状態を外に伝えられるということ。
これは「関数の外に副作用を持たせる」典型的な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)
コメント