作者主页:@Fire_Cloud_1
学习社区:烈火神盾
专栏链接:万物之源——C
首先我们来了解一下函数是什么?
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
百度百科中对函数的定义:链接
为什么会有库函数?
① 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上【printf】
② 在编程的过程中我们会频繁的做一些字符串的拷贝工作【strcpy】
③ 在编程是我们也计算,总是会计算n的k次方这样的运算【pow】
我也在上面学习了很多的库函数,总结一下,C语言常用的库函数都有:
接下去,我会参照文档,给大家将两个常用的库函数,来教会大家如何入阅读英文文档
strcpy
char * strcpy ( char * destination, const char * source );
//strcpy在拷贝的时候'\0'也会被拷贝过来
int main(void)
{
char arr1[] = "############";
char arr2[] = "hello bit";
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
接下去再来看一库函数【memset】
memset
void * memset ( void * ptr, int value, size_t num );
char arr[] = "hello bit";
memset(arr, 'x', 5);
printf("%s\n", arr);
注:
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
再给大家介绍两个C语言的官网
en.cppreference.com【英文版】
zh.cppreference.com【中文版】
函数的组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
我们首先来举一个栗子
写一个函数可以找出两个整数中的最大值。
int Get_Max(int x, int y)
{
return (x > y ? x : y);
}
int main(void)
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int max = Get_Max(a, b);
printf("二者中的较大值为:%d\n", max);
return 0;
}
接下去的一个例子,如果对函数的【实参】和【形参】不太了解的小伙伴可以先去看一下下一个模块,而且还会涉及到【传值调用】和【传址调用】的概念,如果有不懂的也先去看一下再来看这个例子。因为我这些都会涉及到
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
int main(void)
{
int a = 10;
int b = 20;
printf("交换前:a = %d, b = %d\n",a, b);
swap(a, b);
printf("交换后:a = %d, b = %d\n", a, b);
return 0;
}
你可以这么认为:形参实例化之后其实相当于实参的一份临时拷贝
void swap(int* px, int* py)
{
int t = *px;
*px = *py;
*py = t;
}
真实传给函数的参数,叫实参
所以我们可以总结出来一句话:
【函数调用时,实参传递给形参,形参是实参的一份临时拷贝,形参的改变不影响实参】
这里我们再来说一下函数的【传值调用】和【传址调用】,这一块在上面的案例中也涉及到了
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
看了这么多,接下来就要来实践一下了,我们通过四道练习题来进行训练
for (int i = 1; i <= 100; ++i)
{
if (IsPrime(i) == 1)
{
printf("%d是素数\n",i);
}
}
/*素数判断*/
int IsPrime(int n)
{
if (n <= 1)
return 0;
else if (n == 2)
return 1;
else
{
for (int i = 2; i < sqrt(n) + 1; ++i)
{
if (n % i == 0)
{
return 0;
}
}
}
return 1;
}
sqrt(n) + 1
,当然这里是开区间,你也可以写成闭区间的形式<= sqrt(n)
,设想一下一个数16 = 2 * 8
,16还可以表示成16 = 4 * 4
,那我们就可以发现,若是找到一个16的公因子【2】,其实就可以不做判断了,直接return 0即可,sqrt(n)
其实就相当于这个【4】,前面的2已经到了,我们就可以这个算数平方根作为分解点,不需要再往下找了9 = 3 * 3
,sqrt(9) = 3
,我们在进行判断的时候,i为3时其实就不需要在向下判断了,因为9对3取余为0,因此【9】就不是一个素数了,这么说你应该可能明白了,素数判断的方法有很多种,大家主要先记住这一种即可for (int i = 2; i < sqrt(n) + 1; ++i)
{
if (n % i == 0)
{
return 0;
}
}
if (n <= 1)
return 0;
else if (n == 2)
return 1;
for (int i = 1000; i <= 2000; ++i)
{
if (Isleap(i) == 1)
{
printf("%d ", i);
}
}
/*闰年判断*/
int Isleap(int year)
{
//Way1
//if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
//{
// return 1;
//}
//return 0;
//Way2
if (year % 4 == 0)
{
if (year % 100 != 0)
{
return 1;
}
}
if (year % 400 == 0)
return 1;
}
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int k = 7;
int pos = BinarySearch(arr, sz, k);
sizeof(arr)
和sizeof(arr[0])
就都是一样的4个字节,因为整型元素都是4个字节的大小,所以求出的sz就为【1】了,此时就会发生问题。最后的话需要查找的值肯定也需要进行一个传入//int BinarySearch(int* a, int n, int k) //指针接收地址
int BinarySearch(int a[], int n, int k) //数组接收数组
{
int left = 0;
int right = n - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (k > a[mid])
left = mid + 1;
else if (k < a[mid])
right = mid - 1;
else
return mid;
}
return -1;
}
void change(int* px)
{
(*px)++;
}
int num = 0;
change(&num);
printf("num = %d\n", num);
(*px)++
,不能写成*px++
,因为【++】的优先级比【*】来得高,所以会先对这个指针变量进行一个++,然后再对其进行一个解引用的操作,但是当这个指针后移的时候,就已经变成了野指针,此时再去访问这个野指针就会出现问题,后面给出运行结果*px
外的括号去掉再看看函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的
void print()
{
printf("haha\n");
}
void three()
{
for (int i = 0; i < 3; ++i)
{
print();
}
}
int main(void)
{
three();
return 0;
}
把一个函数的返回值作为另外一个函数的参数
strlen()
和sizeof()
我会在后面的【面试题】专栏中进行细致讲解char a[] = "hello world";
int len = strlen(a);
printf("len = %d\n", len);
这里要注意一点的是strlen()去求字符串长度的时候是不算’\0’的
char a[10] = "hello";
int len = strlen(strcat(a, "bit"));
printf("len = %d\n", len);
printf("%d", printf("%d", printf("43")));
printf("43")
输出的字符个数有2个,然后再看外层的printf("%d", printf("43"))
便会在输出一个2,它的返回值就是输出了一个字符,那么整体的这个printf("%d", printf("%d", printf("43")))
就会在输出一个1,作为返回值
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的
int Add(int x, int y);
int main(void)
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int sum = Add(a, b);
printf("sum = %d\n", sum);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
函数的定义是指函数的具体实现,交待函数的功能实现
接下去我们来说说函数递归,这也是函数这一块最难理解的内容
程序调用自身的编程技巧称为递归( recursion)
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量
递归的主要思考方式在于:把大事化小
【需求】:输入1234,打印1 2 3 4
void print1(int num)
{
while (num > 0)
{
printf("%d ", num % 10);
num /= 10;
}
}
运行结果如下
print(num)
,即print(1234)
,那既然上面讲到了这个分割数字的思路,这里我们也可以使用这个思路来完成,print(1234)
我们可以拆成print(123) 4
print(123)
又可以拆成print(12) 3
print(12)
又可以拆成print(1) 2
print(1234)
就被拆成了print(1) 2 3 4
,也就是当这个num < 10为一个个位数时,就做一个打印,否则的话就不断将其以十分之一倍得进行缩小。我们将其转化为代码的形式就【一目了然】了void print2(int num)
{
if (num > 9)
{
print2(num / 10);
}
printf("%d ", num % 10);
}
运行结果如下
【需求】:输入abc,输出其长度为3
int main(void)
{
char str[] = "abc";
int len = strlen(str); //利用库函数进行求解
printf("len = %d\n", len);
return 0;
}
int len = my_strlen(str);
,我们传入了字符数组str的首元素地址,那上面有讲到过对于地址要使用指针来进行接收,最后还要返回求出的长度,所以对于函数我们可以定义成这样int my_strlen(char* str)
int my_strlen(char* str)
{
int count = 0;
while ((*str) != '\0')
{
count++; //计数器累加
str++; //字符指针后移
}
return count;
}
my_strlen(abc)
我们可以拆成1 + my_strlen(bc)
my_strlen(bc)
我们可以拆成1 + my_strlen(c)
my_strlen(c)
我们可以拆成1 + my_strlen('\0')
my_strlen(abc)
就相当于是1 + 1 + 1 + 0
。也是使用字符指针去进行一个后移,若其不为【\0】时,就不断对这个字符串进行拆分,然后直到遇到【\0】时便return 0
。接下去就可以写出代码了int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str + 1);
}
return 0;
}
【需求】:输入一个数,输出其求阶乘后的结果
/*阶乘——递归*/
int Func1(int n)
{
if (n <= 1)
return 1;
else
return n * Func1(n - 1);
}
/*阶乘——循环*/
int Func2(int n)
{
int ret = 1;
for (int i = 1; i <= n; ++i)
{
ret *= i;
}
return ret;
}
这里再对上面的递归实现做一个递归展开图的分析
【需求】:输入一个数,输出从1到这个数的斐波那契数
/*斐波那契数列——递归*/
int Fib1(int n)
{
if (n <= 2)
return 1;
else
return Fib1(n - 2) + Fib1(n - 1);
}
接下去我们再来做一个很有意思的小练习,这个和斐波那契数列很像
① 首先来快速了解一下青蛙的跳法
青蛙每次可以跳一个台阶或者两个跳台阶
③ 通过画算法图来分析一下
④ 来看看代码如何书写
//递归
int Jump(int n)
{
if (n <= 2)
return n;
else
return Jump(n - 1) + Jump(n - 2);
}
int main(void)
{
int n = 0;
printf("请输入台阶的数量:");
scanf("%d", &n);
for (int i = 0; i < n; ++i)
{
printf("跳到第%d个台阶时有%d种跳法\n", i + 1, Jump(i + 1));
}
return 0;
}
运行结果
//迭代
int Jump2(int n)
{
int a = 1;
int b = 2;
int c = 1;
int t = n;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
if (t <= 2)
return n;
else
return c;
}
运行结果
汉诺塔是很经典的递归问题,我们一起来看一下
① 首先来了解一下【汉诺塔】是什么东西
② 接下去快速了解一下它的规则
规则一:每次只能移动一片盘片
规则二:每次移动完后要保证小盘片在大盘片的上面
③ 然后通过动画来看看盘片的移动过程
/*汉诺塔*/
void move(char x, char y)
{
printf("%c-->%c\n", x, y);
}
void Hanoi(int n, char a, char b, char c)
{
if (n == 1)
{
move(a, c);
}
else
{
Hanoi(n - 1, a, c, b);
move(a, c);
Hanoi(n - 1, b, a, c);
}
}
int main(void)
{
int n = 0;
int a = 'A';
int b = 'B';
int c = 'C';
scanf("%d", &n);
Hanoi(n, a, b, c);
return 0;
}
【运行结果】
⑤ 马上来讲讲代码的逻辑
本内容请看这篇文章——> 反汇编深挖【函数栈帧】的创建和销毁
好,我们来总结一下本文所学习的内容
形参
与实参
的概念,以及传值调用
与传址调用
的区别,想要在一个函数中真正通过形参的修改使得外部的实参得到一个修改,就需要传入实参的地址,此时就可以与外界建立起一个真正的连接画递归展开图
,可以更好地帮助理解以上就是本文所要展示的所有内容,感谢您的阅读,如有问题请于评论区留言或者私信我