記事ディレクトリ
関数
1: 関数とは何ですか?
ウィキペディアでの関数の定義:サブルーチン 特定のタスクを完了する責任があり、他のコードから比較的独立しています。(2): 一般に、プロセスのカプセル化と詳細の非表示を提供する入力パラメーターと戻り値があります。これらのコードは通常、ソフトウェア ライブラリとして統合されます。
その2:C言語における関数の分類
(1) ライブラリ関数:C 言語内で提供される関数。
(2) 自己定義関数: 自分で作成した関数。
1: ライブラリ関数
(1): ライブラリ関数の存在意義:
- C 言語プログラミングを学ぶと、コードを書いた後の結果が待ちきれず、結果を画面に出力して確認したくなることを知っています。現時点では、関数を頻繁に使用します。
- 情報を特定の形式 (printf) で画面に出力します。
- プログラミングの過程で、文字列のコピー作業 (strcpy) を頻繁に行います。
- プログラミングでは、n の k 乗の演算 (pow) も計算し、常に計算します。**
- 作業の効率化とバグの発生を防ぐために、ライブラリ関数を導入しました
(2): ライブラリ関数の学習と使用
上で説明した基本的な関数と同様に、これらはビジネス コードではありません。C言語の基本ライブラリには、移植性をサポートし、プログラムの効率を向上させるために、プログラマがソフトウェアを開発するのに便利な一連の同様のライブラリ関数が提供されています。
ライブラリ関数の使い方を覚える必要はありません。どのように使われているかを知ることができます。
おすすめのサイトとアプリはこちら
(1) www.cplusplus.com
(2) MSDN (Microsoft Developer Network)
(3) http://en.cppreference.com (英語版)
(4) http://zh.cppreference.com (中国語版)
合格これらの方法で、関数名、仮パラメーター、必要なヘッダー ファイル、戻り値、その他の必要な情報などの情報を見つけることができます。
これらのツールの言語はすべて英語です. プログラミングを学ぶプロジェクトでは、英語を学ぶ必要があり
ます. ライブラリ関数の例を2つ示します.
strcpy
char * strcpy ( char * destination, const char * source );
上記の英語では、宛先が戻り値であると簡単に説明していますが、
より深い理解は、
strcpy 関数がソース ソースのデータを宛先スペースにコピーした後、宛先スペースの開始アドレスを返すことです。
//strcpy的用法
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = {
"xxxxxxxxxxxxxxx" };
char arr2[] = {
"hello bit" };
strcpy(arr1, arr2);//arr1为目的地,arr2为源头,为了将arr2数组里面的内容拷贝到arr1里面
printf("%s\n", arr1);
return 0;
//arr1与t对齐的后面的x,也就是数组下标为9的x,被改成\0拷贝了过来
//实际上打印的结果为hello bit(\0不显示但存在)
}
memset(メモリー設定機能)
void * memset ( void * ptr, int value, size_t num );
補足: この値は、num のバイト数を指します。
//memset的用法
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = {
"hello bit" };
memset(arr, 'x', 5);
printf("%s\n", arr);
return 0;
}
//memset中间一定是unsigned char类型,5则表示arr数组里面前五个东西被x所取代
注:
ただし、ライブラリ関数が知っておく必要がある秘密は、ライブラリ関数を使用するには、#include に対応するヘッダー ファイルをインクルードする必要があるということです。
ここでは、上記のライブラリ関数をドキュメントに従って学習し、ライブラリ関数の使い方をマスターすることを目的としています。
2: カスタム機能
(1): カスタム関数の構成
ユーザー定義関数はプログラマーによって設計され、通常の関数と同様に関数名、戻り値の型、仮パラメーターなどを持ちます。
基本的な構造は次のとおりです。
ret_type fun_name(para1, * )
{
statement;//语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
(2): 質問例
例 1: 2 つの整数の最大値を見つける関数を作成する
//写一个函数找出两个整数的最大值
#include<stdio.h>
int get_max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
//简便的代码,一行代替上面函数体里面的代码
//return(x > y ? x : y);
}
//从之前笔记一:函数是什么里面的定义得知-
//上述函数就是一个语句块,它负责完成某项特定的任务—求两个数的最大值
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int c = get_max(a, b);
printf("%d\n", c);
return 0;
}
//输入:10 20
//输出:20
例 2: 2 つの整数変数の内容を交換する関数を作成する
//写一个函数交换两个整型变量的内容
//错误示范
#include<stdio.h>
void swap(int a, int b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(a, b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
//输入:10 20
//输出:
//交换前:a=10,b=20
//交换后:a=10,b=20
//没有达到相应的效果
正しいコード:
//正确代码
#include<stdio.h>
void swap(int* pa, int* pb)
{
int temp = 0;
temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
//输入:10 20
//输出:
//交换前:a=10,b=20
//交换后:a=20,b=10
//
エラーの理由は、転送アプリケーションのページに直接移動でき、明確に説明されます
3: 関数内のパラメーター
1:実パラメータ(実パラメータ)
関数に渡される実パラメータは、実パラメータと呼ばれます。
引数は、定数、変数、式、関数などです。
関数が呼び出されると、それらの値を仮パラメーターに渡すために、それらはすべて明確な値を持っている必要があります。
たとえば、上記のコードの get_max(a,b) と ab は実パラメーターであり、関数内の仮パラメーター xy にそれぞれ渡されます。
2: 仮パラメータ (formal parameters)
仮パラメータは、関数名の後の括弧内の変数です。
仮パラメーターは、関数が呼び出されたときにのみインスタンス化される (メモリ単位が割り当てられる) ため、仮パラメーターと呼ばれます。したがって、仮パラメータは関数内でのみ有効です。
次の文は非常に重要です。! ! ! !
関数が呼び出されると、実パラメーターが仮パラメーターに渡され、仮パラメーターは実パラメーターの一時的なコピーになるため、仮パラメーターの変更は実パラメーターに影響しません。
パラメータは実際のパラメータの一時的なコピーになるという言葉を覚えておいてください! ! !
4: 関数呼び出し
関数は呼び出されることで機能します。呼び出されることは、他のコードで使用されることです
1: 値による呼び出し
関数の仮パラメータと実パラメータはそれぞれ異なるメモリ ブロックを占有し、仮パラメータを変更しても実パラメータには影響しません。
したがって、関数の引数を変更せずに値渡しを使用できます。
2 つの整数の和を計算するプログラムを書く例を見てみましょう:
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int c = add(a, b);
printf("%d\n", c);
return 0;
}
このプログラムでは、a と b は実パラメータです. a と b のプロパティを変更せずに、a と b だけを使用して操作します. このとき、値による呼び出しを使用して、操作によって得られた値を返すことができます.
2: アドレスで呼び出す
参照渡しとは、関数外で作成した変数のメモリアドレスを関数の引数に渡すことで関数を呼び出す方法です。
パラメータを渡すこの方法により、関数は関数外の変数との実際の接続を確立できます。つまり、関数は関数外の変数を直接操作できます。
例として、2 つの数値を交換するために上記のコードを使用してみましょう。
#include<stdio.h>
void swap(int* pa, int* pb)
{
int temp = 0;
temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
このプログラムでは、a と b の値を変更してから、参照渡しを使用する必要があります。これは、値渡しの仮パラメーターの変更が実際のパラメーターに影響を与えないためです。
次に、なぜそれが間違っているのかを知るために
、Swap 関数の 2 つのコードを詳細に解釈してみましょう。
//错误代码
#include<stdio.h>
void swap(int a, int b)//返回类型为void表示不返回,
//此处的int a与int b表示形式参数和它们的类型
{
int temp = 0;//定义一个临时变量
temp = a;//把a的值赋给temp
a = b;//把b的值赋给a
b = temp;//把temp的值赋给b,完成交换操作
//注意,因为形参只是实参的一份临时拷贝,
//在整个函数中我们改变的只是实参,
//出函数后形参被销毁无法改变实参
}
int main()
{
int a = 0;//创建变量a
int b = 0;//创建变量b
scanf("%d %d", &a, &b);//输入数值
printf("交换前:a=%d,b=%d\n", a, b);//展示
swap(a, b);//交换函数,将a,b传进去
printf("交换前:a=%d,b=%d\n", a, b);//实参依旧是a和b的原始值,没有达到我们的目的
return 0;
}
//正确代码
#include<stdio.h>
void swap(int* pa, int* pb)//void表示不返回,此处的int* pa与int* pb表示形式参数和它们的类型
{
int temp = 0;//定义临时变量
temp = *pa;//用地址找到实参a并赋给temp
*pa = *pb;
//把用地址找到的实参b赋给用地址找到的实参a
*pb = temp;//用地址找到实参b并赋给temp
//跳出函数时,被销毁的形参只是两个指针变量,此时实参的交换已经完成
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);//传入地址
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
3: 練習
(1): ある数が素数かそうでないかを判定する関数を書きなさい。
//写一个函数可以判断一个数是不是素数。
#include<stdio.h>
#include<math.h>
int is_primer(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
{
return 0;
//如果经过return 0,
//则下一步往函数int is_primer(int n)的大括号走,
//然后进入主函数is_primer(i)那里进行判断i是否等于1
}
}
//j没有任何一个数能整除n,说明它一定是素数,则返回1,否则返回0
return 1;
}
int main()
{
//打印100~200之间的素数
int i = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数
if (is_primer(i) == 1)//如果是素数返回的结果是1,则令函数==1,将素数打印出来
{
printf("%d ", i);
}
//如果这个函数返回值不等于1
//则继续进入主函数的for循环,然后for循环再进入if中判断,
//判断完后,将i传到形式参数里面去
}
return 0;
}
Note: If the function does not write a return value, the function will return a value by default. 一部のコンパイラは、最後の命令の結果を返します。
(2): ある年がうるう年かどうかを判定する関数を書きなさい。
//写一个函数判断一年是不是闰年。
int is_leap_year(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return 1;
else
return 0;
}
//有一种更简单的方法,不需要用if...else
//int is_leap_year(int year)
//{
// return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
//}
int main()
{
int y = 0;
int count = 0;
for (y = 1000; y <= 2000; y++)
{
//判断y是不是闰年
if (is_leap_year(y) == 1)
{
count++;
printf("%d ", y);
}
}
printf("\ncount = %d\n", count);
return 0;
}
(3): 順序付けられた整数配列の二分探索を実装する関数を書きなさい。
//写一个函数,实现一个整形有序数组的二分查找
//错误代码
//#include<stdio.h>
//int binary_search(int* arr, int a)//地址传过去,得要指针接收,所以本质还是一个指针
//int binary_search(int arr[], int a)
//{
//int left = 0;
//int right = sizeof(arr)/sizeof(arr[0]) - 1;//right一直是0
//上面的那一行代码是有逻辑错误的,
//因为数组传参,传递的是数组首元素的地址
//所以sizeof(arr)/sizeof(arr[0])=1,所以1-1=0
//int mid = 0;
//while (left <= right)
//{
//mid = left + (right - left) / 2;//找中间的元素,防止越界
//if (arr[mid] > a)//中间元素大于查找值,就从右缩小一半的范围
//{
//right = mid - 1;//可以使用--mid,但不推荐
//}
//else if (arr[mid] < a)//中间元素小于查找值,就从左缩小一半的范围
//{
//left = mid + 1;//可以使用++mid,但不推荐
//}
//else
//{
//return mid;//找到了,返回下标
//}
//}
//if (left > right) //正常情况下不会出现
//{
//return -1;//找不到,返回-1
//}
//}
//int main()
//{
//int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//int k = 0;
//scanf("%d", &k);
//找到了返回下标,0~9
//找不到返回 -1(这里不能返回0,因为数组的第一个数字下标是0,如果return 0很有可能会对结果造成影响)
//传数组,一个元素4个字节,数组中10个元素40个字节,
//把40个字节全都移过去,如果int arr[]为了接收也创建了一个40字节的空间
//这样空间会有大量的浪费,所以c语言有了以下规定:
//数组传参,传递的是数组首元素的地址
//int ret = binary_search(arr, k);
//if (-1 == ret)
//printf("找不到\n");
//else
//printf("找到了,下标是:%d\n", ret);
//return 0;
//}
//正确代码(在主函数里面定义一个sz变量)
#include<stdio.h>
//地址传过去, 得要指针接收, 所以本质还是一个指针
//int binary_search(int* arr, int a, int sz)//数组传参,传递的是数组首元素的地址
int binary_search(int arr[], int a, int sz)//形参为数组、需要查找的整数、数组的元素个数
{
int left = 0;
int right = sz - 1;
int mid = 0;
while (left <= right)
{
mid = left + (right - left) / 2;//找中间的元素,防止越界
if (arr[mid] > a)//中间元素大于查找值,就从右缩小一半的范围
{
right = mid - 1;//可以使用--mid,但不推荐
}
else if (arr[mid] < a)//中间元素小于查找值,就从左缩小一半的范围
{
left = mid + 1;//可以使用++mid,但不推荐
}
else
{
return mid;//找到了,返回下标
}
}
if (left > right) //正常情况下不会出现
{
return -1;//找不到,返回-1
}
}
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
//只能在主函数里面定义sz变量,sizeof(arr)在主函数里使用不需要传递地址
int sz = sizeof(arr) / sizeof(arr[0]);
int k = 0;
scanf("%d", &k);
//找到了返回下标,0~9
//找不到返回 -1(这里不能返回0,因为数组的第一个数字下标是0,如果return 0很有可能会对结果造成影响)
//数组传参,传递的是数组首元素的地址
int ret = binary_search(arr, k, sz);//再多加一个sz变量,传到形参上去
if (-1 == ret)
printf("找不到\n");
else
printf("找到了,下标是:%d\n", ret);
return 0;
}
(4): 関数が呼び出されるたびに num の値を 1 ずつ増やす関数を作成する
#include<stdio.h>
void Add(int* p)//在主程序内定义一个变量储存调用的次数,因为需要改变变量的值,所以进行传址调用
{
printf("hehe\n");
(*p)++;//解引用找到变量再加1,注意这个括号不能忘
//否则,*p++就表示每次这个指针先向后移动4个字节,然后解引用
}
int main()
{
int num = 0;
Add(&num);
return 0;
}
5: 関数のネストされた呼び出しと連鎖アクセス
1.ネストされた呼び出し
関数は、必要に応じて相互に呼び出すことができます。
2つの例を挙げてください
#include<stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
//这是每一个初学者都会写的代码,我们先调用了main函数,
//然后在main函数的内部又调用了printf函数,这就是嵌套调用.(可以理解为复合函数)
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
注: 関数は、ネストされた定義を呼び出すことはできますが、ネストされた定義を呼び出すことはできません。
2.チェーンアクセス
ある関数の戻り値を別の関数の引数として受け取ります。
#include<stdio.h>
#include<string.h>
int main()
{
int len = strlen("abcdef");
printf("%d\n", len);
//可以利用链式访问简化代码,如下:
printf("%d\n", strlen("abcdef"));
return 0;
}
strlen関数の戻り値がprintf関数のパラメータとなり、2つの関数をチェーンのようにつなぐ、つまりチェーンアクセスです。
(1): 連鎖アクセスの典型例
次のプログラムの出力は何ですか?
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
答え: 4321
重要な知識のポイント: printf 関数の戻り値は、印刷される文字数です。
最初に最も外側のprintf関数を入力します
関数のこのレイヤーには、 2 番目のレイヤー関数printf("%d", printf("%d", 43))の戻り値が必要です。
第 2 レベルのprintf関数には、第 3 レベルの関数printf("%d", 43)の戻り値が必要です
最初に第 3 レベルの関数を実行し、第 3 レベルのprintf("%d", 43)関数を実行した後、返される印刷文字数は2です。
したがって、次のようになりますprintf("%d", printf("%d", 2))
2 番目のレイヤーは戻り値 2 を取得し、2 を出力します。このとき、2 番目のレイヤー関数も、入力した文字数1を返します。
したがって、printf("%d", 1)になり、最終的に 1 が出力されます。
また、4321 の出力を形成します。
6: 関数の宣言と定義
1: 関数の宣言
1. コンパイラーに、関数の呼び出し、パラメーター、および戻り値の型を伝えます。しかし、それが存在するかどうかは、関数
宣言によって決定されません。
2. 関数の宣言は、通常、関数の使用前に表示されます。使用前の宣言を満たすために。
3. 関数宣言は、通常、ヘッダー ファイルに配置されます。
2: 関数の定義
1. 関数の定義は、関数の特定の実装を指し、関数の関数実装が説明されています。これは、カスタム関数を作成するために通常行う手順と同じです。
2. 関数を入れ子に定義できない
3: プログラムのブロック書き込み
コードを書くとき、次のように考えるかもしれません: ソース ファイルにすべてのコードを書いているので、それを見つけるのは便利ではありません。
実際、そのような習慣は将来のプログラム開発に悪影響を及ぼします。
私たちの社会には独自の分業があり、プログラムを開発するとき、メインのプログラムを 1 人が書き、関数を 1 人が書くなど、大規模なプロジェクトの一部だけを担当する必要があることがよくあります。プロジェクトのパーツを分割すると、バグを見つけてそれに応じて修正するのが速くなります。
したがって、関数を記述するときは、次のようなファイル割り当てが必要です。
関数の宣言、型の定義、ヘッダー ファイルのインクルード — ヘッダー ファイル.h関数
定義 — 関数実装のソース ファイル
.c 各関数をこれら 2 つのファイルに記述するか、複数の関数を記述することができます。ファイル内で 2 つに分けられます。
たとえば、2 つの数値の合計であるプロジェクトを作成する場合、多くのプログラマーは分担して完成させます.
A は、ヘッダー ファイル add.h を作成しました。
#pragma once
//头文件中放:函数的声明,类型的定义,头文件的包含
int Add(int x,int y)
B によって作成された小さなソース ファイル add.c
int Add(int x,int y)
{
return x + y;
}
test.cの内部
#include "add.h"//引用头文件
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a. & b);
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
モジュールでコードを書くことの利点は, ファイルを実装するために必要なコードを隠すことができることです. たとえば
, Zhang San というプログラマーがいます.
彼は余暇にゲームエンジンを書きました. 会社は彼のゲームエンジンと
Zhangを購入したいと考えています.サンはそれを売ることに同意した.彼にそれを渡す,しかし私は彼にソースコードを見せたくないので,彼はどうすればよいでしょうか?
彼は次のようにしました:
Zhang San はこれらの 2 つのコード
を書きました.彼は VS 環境で動作すると仮定します. , ヘッダーファイルをクリック, 既存のアイテムを追加, 設定 上記の2つのコードファイルが追加されました.
このゲームエンジンはたまたまLi Siが購入したいプロジェクトでした.
Zhang Sanは考えました: 使用させてもいいが,それがどのように実装されているかをあなたに見せたくない
ので、彼はクリックしました
[適用] をクリックし
ます。使用後、add.c のコードは実行されません。
この add.lib ファイルは Zhang San によって作成されたゲーム エンジンの静的ライブラリです
. Li Si が購入した後、彼はそれを開いて add.lib ファイルを見ました. それは文字化けの束だった
ので, Zhang San は彼に取扱説明書
.
test.c のコードは、最終的に次のようになります。
#include "add.h"//引用头文件
#pragma comment(lib, "add.lib")//加载引用静态库
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a. & b);
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
7: 関数の再帰
1: 再帰とは
自分自身を呼び出すプログラムのプログラミング スキルを再帰と呼びます (再帰とは、自分自身を呼び出すプログラムです)
再帰は、アルゴリズムとしてプログラミング言語で広く使用されています。手続きまたは関数は, その定義または仕様において, 直接的または間接的に
自身を呼び出す方法を持ってい
ます. 通常, 大きくて複雑な問題を, 元の問題に似た小さな問題に変換して解決します.
再帰的戦略
は少数のプログラムのみ可能です.問題解決プロセスに必要な繰り返し計算を記述するために使用され、プログラムのコード量を大幅に削減します。
再帰についての主な考え方は次のとおりです。大きなものを小さくする
ここに単純な再帰があります
#include<stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
//死递归hehe,到最后程序因为栈溢出而自动跳出(挂掉了)
スタック オーバーフローはいつ発生しますか?
各関数呼び出しはスタック領域のメモリ空間に適用され、上記のコードは無限回再帰的に適用され、最終的にスタック オーバーフローにつながります。
2: 再帰に必要な2つの条件
制約があります。この制約が満たされると、再帰は続行されません。
再帰呼び出しのたびに、この制限にどんどん近づいていきます。
(1): 演習 1: (図面の説明)
整数値 (符号なし) を取り、そのビットを順番に出力します
例: 入力: 1234、出力: 1 2 3 4
#include<stdio.h>
void print(unsigned int n)
{
if (n > 9)//如果n是两位数的话
{
print(n / 10);//满足递归的两个必要条件
}
printf("%d", n % 10);
}
int main()
{
unsigned int num = 0;
//无符号整型用%u
scanf("%u", &num);//1234
print(num);//按照顺序打印num的每一位
return 0;
}
それでは、再帰プロセスを分析しましょう
//递归的思考方式:大事化小
//
//print(1234)
//print(123) 4
//print(12) 3 4
//print(1) 2 3 4
//1 2 3 4
//
print(1234);//这个函数从上到下,先递进后回归
//1234大于9,进入if语句,第一层
print(1234)
{
if (n > 9)//n=1234,满足条件,进入if
{
print(123);
}
printf("%d ", n % 10);//第一层,n%10=4
}
//print(123)展开,n=123满足条件,继续进入下一层,接着递归
print(123)
{
if (n > 9)//n/10=123,满足条件,进入if
{
print(12);
}
printf("%d ", n % 10);//第二层,n%10=3
}
//print(12)展开,n/10=1此时不满足条件,不会继续进入下一层的if语句
print(12)
{
if (n > 9)//n=12,不满足条件,不进入if
{
print(1);
}
printf("%d ", n % 10);//第三层,n%10=2
}
print(1)
{
if (n > 9)//n=1,不满足条件,不进入if
{
print(0);
}
printf("%d ", n % 10);//第三层,n%10=1
}
//递归的“递”此时已经完成,我们将这个代码整理一下,查看它时如何“归”的
print(1234)
{
{
{
{
printf("%d ", n % 10);//第四层,n%10=1
}
printf("%d ", n % 10);//第三层,n%10=2
}
printf("%d ", n % 10);//第二层,n%10=3
}
printf("%d ", n % 10);//第一层,n%10=4
}
//代码从第四层开始向外执行,故可以实现数字的按位打印
//输出:1 2 3 4
ファンクションスタックフレームの知識ポイントを少し足してみましょう(内部スキルの練習)(ブロガーさんの記事が詳しく書かれていて分かりやすいのでまとめられます)
関数スタック フレームと破棄
ここで、前述のレジスターの種類について簡単に説明します。ここでそれらを思い出してみましょう。
eax: 汎用レジスタ、一時データを保存、戻り値によく使用 |
---|
ebx: 汎用レジスタ、一時データを保持 |
ebp: スタック ボトム レジスタ |
edp: スタックのトップ レジスタ |
eip: 現在の命令の次の命令のアドレスを保持する命令レジスタ |
ebp と edp は、関数スタック フレームを維持するために使用されます。
関数はどのように正確に呼び出されますか? まず、VS で F10 キーを押して、逆アセンブリをクリックすると、アセンブリ コードが表示されます。メモリアドレスの特定のレイアウトを観察したいので、「Show Symbol Names」をキャンセルします
ヒント:
1. dword は 4 バイトです
2. 4 バイトが毎回初期化され、ecx の合計回数が初期化され、最後に eax の内容、0CCCCCCCCch が初期化されます
3. プッシュ: スタックをプッシュ: スタックの一番上
に要素を置きますスタック pop: スタックをポップ: スタックの一番上から要素を削除します
このことから、実際には、仮パラメータをまったく定義していないことがわかります
. パラメータを mov, push 命令に渡します (b と c の仮パラメータを [ebp+8], [ebp+0Ch( 12)]. )
Add(a,b) は右に渡され、次に左に渡されます。
a' と b' (x と y) は、実際には a と b の実パラメータの一時的なコピーです。
return z の後、関数は破棄を開始します. 逆アセンブルされたコードを見てみましょう.
00C213F1 5F pop edi
00C213F2 5E pop esi
00C213F2 5B pop ebx
//pop三次 相当于之前连续push三次的逆过程,连续在栈顶弹出值分别存放到edi,esi,ebx中
//pop完后,这些空间没必要存在了,应该被销毁
00C213F4 8B E5 mov esp,ebp
//mov将ebp赋给了esp,这个时候esp与ebp在同一个位置上
00C213F6 5D pop ebp
//把ebp pop出来之后ebp回到main函数的栈底,esp回到main函数的栈顶
図に示すように、esp は main 関数のスタックの先頭に戻ります。
00C213F7 C3 ret
//ret指令的执行,跳到栈顶弹出的值就是call指令的下一条指令的地址
//此时esp+4(pop了一下),栈顶的地址也弹出去了,esp指向了00C21450(call)的底部
Add 関数を呼び出した後、main 関数に戻ると、実行を続行します。
00C21450 83 C4 08 add esp,8
//回到call指令的下一条地址,esp直接+8,把x,y形参的空间释放,esp这个时候指向edi的顶端
00C21453 89 45 E0 mov dword ptr [ebp-20h],eax
//把eax中的值,存放到[ebp-20h(也就是c)]里面去
//函数的返回值是由eax寄存器带回来的
読んだ後、次の質問に答えることができます
- ローカル変数はどのように作成されますか?
回答: 最初に関数用のスタック フレーム スペースを割り当て、そのスペースの一部を初期化してから、ローカル変数のスタック フレーム スペースに少しスペースを割り当てます。 - 初期化されていない場合、ローカル変数の値がランダムな値になるのはなぜですか?
回答: 初期化されていない場合、ローカル変数はメイン関数のスタック フレームから取得されるため、その値はランダムに入れられ、ランダムな値は初期化後に上書きできます。 - 関数はどのようにパラメータを渡しますか? パラメータが渡される順序は何ですか?
- 仮パラメータと実パラメータの関係は?
回答: 仮パラメーターは、実パラメーターの一時的なコピーです。 - 関数が呼び出されるとどうなりますか?
(3, 5) 回答: 関数を呼び出したい場合、呼び出される前にプッシュされ、2 つのパラメーター (a、b) が右から左にプッシュされます。実際、仮パラメーター関数を入力すると、Add 関数のスタック フレームで、関数の仮パラメーターがポインターのオフセットを介して取得されます。 - 関数呼び出しの終了後、関数はどのように戻りますか?
A: 呼び出しの前に、call 命令の次の命令のアドレスを格納し、ebp によって呼び出された関数の前の関数スタック フレームの ebp を格納します。関数呼び出しの後に関数が戻る必要がある場合は、 ebp 、前の関数呼び出しの ebp を見つけることができます。ポインターが下がったとき (上位アドレス)、esp の先頭に移動して、main 関数のスタック フレーム空間に戻ることができます。その後、呼び出し命令を覚えています次の命令のアドレス、戻ると、呼び出し命令の次の命令のアドレスにジャンプできるため、関数呼び出しの後に関数が戻ることができ、戻り値はeax レジスタを介して戻されます。
(2): 演習 2: (説明用の図)
文字列の長さを調べるための 3 つの方法を以下に説明します。
方法 1:
//方法一
#include<stdio.h>
#include<string.h>
int main()
{
char arr[] = {
"abc" };
int len = strlen(arr);
printf("%d", arr);
return 0;
}
方法 2: 関数とループを使用する
//方法二
#include<stdio.h>
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = {
"abc" };
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
方法 3: 再帰 (簡単、一時変数を作成する必要はありません)
//strlen("abc");
//1+strlen("bc");
//1+1+strlen("c");
//1+1+1+strlen("");
//1+1+1+0=3
#include<stdio.h>
int my_strlen(char* str)
{
if (*str != '\0')
{
//如果第一个字符不是"\0",则字符串至少包含一个字符
//可以剥出来个1,str+1是下一个字符所指向的地址
//空字符第一个是"\0"
return 1 + my_strlen(str + 1);
//注意:这里str++并不能代替str+1的作用
//我们把str+1之后的地址传下去了,而留下来str还是原来的str.
//因为str++是先使用再++,
//那么根据原理传进去的str还是原来的str(原来是a的地址,传进去还是a的地址)
//所以按照原理:++str能代替str+1的作用,但并不推荐这样做,
//因为如果递归回来之后使用str的话,留下来的str不是原来的str了.
}
else
return 0;
}
int main()
{
char arr[] = {
"abc" };
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
方法 3 はコードが非常に少ないように見えますが、実際にはプログラム内で多くの計算が繰り返されます。
3: 関数の再帰と反復
(1): イテレーションとは
繰り返しは実際には繰り返しであり、ループは特別な種類の繰り返しです。
(2):メリットとデメリット
関数再帰では、関数をレイヤーごとに呼び出します。これには、必要なコードが少なくて済み、簡潔であるという利点があります。しかし、主な欠点が 2 つあります. 一方では、多数の計算が繰り返されるため、プログラムの実行速度が遅くなります; 他方では、関数が呼び出されるたびに、スタック内の対応するスペースを開く必要があります。スタック オーバーフローが発生しました。(スタック領域が使い果たされており、プログラムを続行できません)
反復を使用すると、ループで多くの関数を呼び出す必要がなくなり、繰り返し計算がはるかに少なくなり、このプログラムの実行速度ははるかに速くなりますが、このプログラムのコード量ははるかに大きくなります。
n 番目のフィボナッチ数を見つけます。(オーバーフローに関係なく)
//斐波那契数列
//1 1 2 3 5 8 13 21 34 55 ....
//方法一:递归法
// n<=2 1
//Fib1(n)
// n>2 Fib1(n-1)+Fib1(n-2);//第三个数加第二个数
#include<stdio.h>
int Fib1(int n)
{
//如果想知道某个斐波那契数究竟计算了多少次,可以设置一个全局变量count
//if (n == k)//算第k个斐波那契数被计算了多少次
//count++;
if (n <= 2)
return 1;
else
return Fib1(n - 1) + Fib1(n - 2);
}
//int count = 0;
int main()
{
int n = 0;
scanf("%d", &n);
int ret1 = Fib1(n);//定义一个ret来接受上面函数的返回值
printf("%d", ret1);
//printf("count=%d\n", count);
return 0;
}
n の階乗を求める
//求n的阶乘
//方法一:递归法
//1*2*3=Fac(3)
//1*2*3*4=Fac(3)*4
//
// n<=1 1
//Fac(n)
// n>1 n*Fac(n-1);
#include<stdio.h>
int Fac1(int n)
{
if (n <= 1)
return 1;
else
return n * Fac1(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret1 = Fac1(n);//定义一个ret来接受上面函数的返回值
printf("%d", ret1);
return 0;
}
上記の 2 つのコードは非常に単純に見えますが、実際に実行すると大きな問題が見つかります。
Fib1 関数を使用する場合、50 番目のフィボナッチ数を計算する場合は特に時間がかかります。
Fac1 関数を使用して 10000 の階乗を見つけます (結果の正確性を考慮せずに)。プログラムがクラッシュします。
なぜ?
Fib 関数を使用する場合、n=50 の場合、光 n=46 が 3 回計算 (出現) されるため、プログラムの実行が非常に遅くなります。
Fac1 関数を使用する場合、10000 の階乗を計算すると、スタック オーバーフロー( stack overflow ) の情報が表示されます. システムによってプログラムに割り当てられたスタック領域は制限されていますが、無限ループがある場合、または (デッドrecursion), これにより、スタック領域が常に開かれ、最終的にスタック領域が枯渇する可能性があります. この現象はスタックオーバーフローと呼ばれます.
では、この問題をどのように修正すればよいでしょうか。
再帰を非再帰に変更
修正後のコードは以下の通り
//斐波那契数列
//1 1 2 3 5 8 13 21 34 55 ....
//方法二:迭代
#include<stdio.h>
int Fib2(int n)
{
int a = 1;//
int b = 1;//前两个数都是1
int c = 1;//对斐波那契数列中前两个数之和的第三个数初始化
//不能令c=0,如果输入的n小于2,那直接return 0了,很显然不行
while (n > 2)//第三个斐波那契数的时候开始循环
{
//1 1 2 3 5 8 13 21 34 55 ....
//
//
// a b c.....(斐波那契数前两个数之加赋给第三个数,以此类推...)
// a b c....
// a b c....
c = a + b; //斐波那契数前两个数之加赋给第三个数
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret2 = Fac2(n);//定义一个ret来接受上面函数的返回值
printf("%d", ret2);
return 0;
}
//求n的阶乘
//方法二:迭代法
int Fac2(int n)
{
int i = 0;
int ret2 = 1;//阶乘初始化必然为1
for (i = 1; i <= n; i++)
{
ret2 = ret2 * i;
}
return ret2;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret2 = Fac2(n);//定义一个ret来接受上面函数的返回值
printf("%d", ret2);
return 0;
}
ヒント:
- 多くの問題は、非再帰的な形式よりも明確であるという理由だけで、再帰的な形式で説明されています。
- ただし、これらの問題の反復実装は、再帰実装よりも効率的である傾向がありますが、コードはわずかに読みにくくなります。
- 問題が複雑すぎて繰り返し実装できない場合、再帰的な実装の単純さによって、それが課す実行時のオーバーヘッドを補うことができます。
4: 関数再帰の古典的な問題
(1): ハノイの塔問題
1。起源:
ハノイの塔 (別名ハノイの塔) は、古代インドの伝説に由来する知育玩具です。ブラフマーが世界を創造したとき、彼は 3 つのダイヤモンドの柱を作り、その上に 64 個の黄金の円盤を下から上に順に積み上げました。ブラフマーはバラモンに、下から始めて、サイズの順に別の柱にディスクを再配置するように命じました。また、ディスクを小さなディスクに拡大することはできず、3 つの列の間で一度に移動できるディスクは 1 つだけであると規定されています。
2。抽象化は数学的問題です。
下の図に示すように、左から右に A、B、C の 3 つの柱があり、そのうち柱 A には小さいものから大きいものまで n 個の円盤が積み上げられています。柱 A から C の円盤 柱が上昇するとき、その期間中の原則は 1 つだけです: 一度に移動できるのは 1 つのプレートだけであり、大きなプレートは小さなプレートに乗ることはできません. 移動の手順と移動回数。
(1)n == 1
1st Disk 1 A---->C合計=1回
(2) n == 2
第1セットNo.1 A---->B
第2セットNo.2 A---->C
第3セットNo.1 B---->C 合計=3回
(3)n == 3
1 セット目 No.1 A---->C
2 セット目 No.2 A---->B
3 セット目 No.1 C---->B
4 セット目 No.3A---- >C
5 セット目 No . 1 B---->A
6 番目のセット No. 2 B---->C
7 番目のセット No. 1 セット A---->C サム = 7 回
法則を見つけるのは難しくありません: ディスクの 2 が 1 の 1 乗に減る回数
2 枚の円盤の 2 の次数は 1 の 2 乗に
減ります。3 枚の円盤の 2 の次数は 1 減り
ます。. . . .
n 個のディスクの次数 2 - 1 の n 乗
したがって: 移動回数: 2^n - 1
アルゴリズムの分析
(手順1) プレートの場合
a 列のプレートを a から c に直接移動します
それ以外は
(ステップ 2) まず、列 a の n-1 プレートを c を使用して b に移動します (図 1)。
確かに、移動できない c 列はなく、既知の関数パラメーターは hanoi(int n, char a, char b, char c) です。
列 c の助けを借りて、列 a のプレートを列 b に移動することを表します。ここで関数を呼び出すと、列 a の n-1 が移動します。
プレートは、C ピラーの助けを借りて B ピラーに移動します。ここで位置 hanoi(n-1,a,c,b) を変更する必要があります。
(手順 3) この時点で、図 1 のように移動が完了しますが、移動はまだ終わっていません. まず、列 a の最後のプレート (n 番目) のプレートを c に移動します (写真 2)
(手順 4) 最後に, 列 b を移動 a の助けを借りて n-1 プレートを c に移動します (図 3)
// 汉诺塔
#include<stdio.h>
void hanoi(int n, char a, char b, char c)//这里代表将a柱子上的盘子借助b柱子移动到c柱子
{
if (1 == n)//如果是一个盘子直接将a柱子上的盘子移动到c
{
printf("%c-->%c\n", a, c);
}
else
{
hanoi(n - 1, a, c, b);//将a柱子上n-1个盘子借助c柱子,移动到b柱子
printf("%c-->%c\n", a, c);//再直接将a柱子上的最后一个盘子移动到c
hanoi(n - 1, b, a, c);//然后将b柱子上的n-1个盘子借助a移动到c
}
}
int main()
{
int n = 0;
printf("请输入需要移动的圆盘个数:");
scanf("%d", &n);
hanoi(n, 'A', 'B', 'C');//移动A上的所有圆盘到C上
return 0;
}
(2):カエルのジャンプステップ
(1) 問題解説
カエルは一度に 1 段か 2 段跳び上がることができます。カエルが n 歩ジャンプできる回数の合計を求めてください。
タスク: カエルが n 歩飛び上がる方法は何通りありますか?
ルール: カエルは一度に 1 つまたは 2 つのステップをジャンプできます。
(2) 問題分析
n = 1 の場合、ジャンプの方法は 1 つであり
、n = 2 の場合、カエルは 1 つのレベルをジャンプしてから別のレベルをジャンプし、カエルは直接 2 つのレベルにジャンプします。ジャンプには 2 つの方法があります.
n = 3 の場合, カエルは 1 つのレベルを 3 回ジャンプします. カエルは 1 つのレベルをジャンプしてから 2 つのレベルをジャンプします. カエルは 2 つのレベルをジャンプしてから 1 つのレベルをジャンプします.
n = 3 の場合, 3 つのジャンプ方法があります. 4, 5 あります 8 種類のジャンプがあります;
n = 5 の場合、8 種類のジャンプがあります; …
法則はフィボナッチ数列(3)再帰の終わりの判断に似ている
たとえば、カエルは 5 段ジャンプする必要があります。
5 歩の数を求めるには、4 歩と 3 歩の値を求める必要があります。
4 つのステップの数を見つけるには、3 つのステップと 2 つのステップの値を見つける必要があり、そのうちの 1 つの値は再帰を必要としません。
3 ステップの数を見つけるには、2 番目のステップと 1 番目のステップの値を見つける必要があり、他の値はこの時点で再帰を必要としません。
この時点で、再帰を中断できるので、再帰終了の判断基準を取得します。列 1 のディスクの数が 1 以下の場合、再帰から飛び出します。
//青蛙跳台问题
#include<stdio.h>
//或者运用void函数,但之前要定义一个全局变量count
//int count = 0;//创建全局变量来统计个跳法个数
//void frog_jump(int n)
//{
// if (n == 0)//当台阶数为0是跳法个数加1
// count++;
// else if (n < 0);
// else
// {
// frog(n - 1);
// frog(n - 2);
// }
int frog_jump(int n)
{
//int sum = 0;
if (1 == n)//等于1时,sum=1
{
return 1; //sum += 1
}
else if (2 == n)//等于2时,sum=2
{
return 2;//sum += 2;
}
else//大于3时,开始递归
{
return frog_jump(n - 1) + frog_jump(n - 2);//sum = frog_jump(m - 1) + frog_jump(m - 2);
}
//return sum;
}
int main()
{
int n = 0;
printf("请输入台阶的个数:");
scanf("%d", &n);
int ret = frog_jump(n);
printf("一共有%d种跳法\n", ret);
return 0;
}
(3):カエルのジャンプステップ(上級編)
質問 (1): 昔々、頂上にたどり着くために階段をジャンプしたいカエルがいました. カエルが一度に 1 歩までジャンプできるなら、2 段までジャンプすることもできます. (n-1) レベル、n レベル。次に、カエルが n 番目の階段を跳び上がるとき、合計で何回の跳躍方法を持っているか。(n段のn段ジャンプ法が存在する前提です)
(1) 問題解決のアイデア:
n が 1 ステップの場合: f(1) = 1 (ジャンプは 1 回のみ可能)
n が 2 ステップの場合: f(2) = f(2 - 1) + f(2 - 2) (2 つの A メソッドがあります) n が 3ステップの
場合: f(3) = f(3 - 1) + f(3 - 2) + f(3 - 3) ( 3 段跳びの方法、1 段目、2 段目、3 段目)
……
……
n が (n - 1) 段の場合:
f(n-1) = f((n-1)-1) + f((n-1)-2) + … + f((n-1)-(n-2)) + f((n -1)-(n-1))
f(n-1) = f(0) + f(1)+f(2)+f(3) + … + f((n-1)-1)
f (n-1) = f(0) + f(1) + f(2) + f(3) + … + f(n-2)
n が n ステップの場合:
f(n) = f(n-1) + f(n-2) + f(n-3) + … + f(n-(n-1)) + f(nn)
f(n) = f( 0) + f(1) + f(2) + f(3) + … + f(n-2) + f(n-1)
f(n-1) と f(n) のケースを組み合わせると、f(n) = f(n-1) + f(n-1) となるため、次のようになります。 f(n) = 2*f (n -1)。
#include<stdio.h>
int frog_jump_step(int n)
{
if (n <= 1)
{
return 1;
}
else
return 2 * frog_jump_step(n - 1);
}
int main()
{
int n = 0;
printf("请输入青蛙应该跳的台阶个数:");
scanf("%d", &n);
int sum = frog_jump_step(n);
printf("一共有种%d种跳法\n", sum);
return 0;
}
問題 (2): カエルは一度に 1 歩まで跳ぶことができ、2 歩まで跳ぶこともできます。また、n 歩まで跳ぶことができます。カエルは m 段の階段を何通り上ることができますか?
(1) 問題解決のアイデア
上記と異なる状況は、カエルのジャンプの最後のステップが n ステップではないため、スキップする現象が発生する可能性があることです。
前置多項式:
f(n) = f(n-1) + f(n-2) + f(n-3) + … + f(nm)
f(n-1) = f(n-2) + f(n-3) + … + f(nm) + f(nm-1)は次の
ように簡略化されます: f(n) = 2f(n-1) - f(nm-1)
#include<stdio.h>
int frog_jump_step(int n,int m)
{
if (n > m)
{
return 2 * frog_jump_step(n - 1, m) - frog_jump_step(n - 1 - m, m);
}
else
{
if (n <= 1)
{
return 1;
}
else
return 2 * frog_jump_step(n - 1);
}
}
int main()
{
int n = 0;
int m = 0;
printf("请输入青蛙应该跳的台阶个数:");
scanf("%d", &n);
int sum = frog_jump_step(n, m);
printf("一共有种%d种跳法\n", sum);
return 0;
}