目录
1.函数是什么?
1.C语言中的函数与数学中的函数所有不同。C语言中的函数,其实就是一个子程序,是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
2.函数的分类
1. 库函数:C语言本身提供的可以直接使用的函数
2. 自定义函数:需要我们自己去创造实现的一种函数
库函数
早期没有库函数,但是C语言为了避免出现不标准,代码冗余,开发效率低等问题,把常用的功能实现成函数,集成为库,由C语言直接提供。
只要各种编译器按照C语言的标准去定义这些函数,这样的话无论是在VS还是在ggc,功能差异都不会太大
常见的库函数:
IO函数
字符串操作函
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
库函数这么多?如何学习?
进入http://www.cplusplus.com或者使用msdn等软件,进行查询学习。
例如:我想查stcpy的用法
从字面上看就是copy字符串。把source的数据放到destination,而且null( \0 )也会被放进去。
例如:
#include<stdio.h> //使用头函数
int main()
{
char arr1[] = "abcdef";
char arr2[20] = "0";
//能把arr1中的abcdef拷贝到arr2中
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
结果是abcdef
说明copy成功了
我们再来看这一个例子,来看看有没有拷贝‘\0’
#include<string.h>
int main()
{
char arr1[] = "abcdef"; //a b c d e f \0
char arr2[20] = "XXXXXXXXXXX";//X X X X X X X XXXX\000
//能把arr1中的abcdef拷贝到arr2中
strcpy(arr2, arr1);
//string copy
printf("%s\n", arr2);
return 0;
}
结果是:abcdef
说明'\0'也被拷贝过来了。
再来看一个
sizeof(a) 返回的是4(个字节)
sizeof的返回的值的类型是size_t
memset是什么函数呢?
int main()
{
char arr[] = "hello world";
memset(arr, 'X', 3);
printf("%s\n", arr);
return 0;
}
结果是 XXXlo world
//1. 设置内存的时候是以字节为单位的
//2. 每个字节的内容都是一样value
int main()
{
char arr[] = "hello bit";
memset(arr+6, 'X', 3);
//1. 设置内存的时候是以字节为单位的
//2. 每个字节的内容都是一样value
printf("%s\n", arr);
return 0;
}
结果是XXXXX bit
此涉及指针的运算,以后详说。
使用库函数,必须包含 #include 对应的头文件。
自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数,需要我们自己来设定。
函数就类似于一座工厂,参数就是原材料,返回值就是处理后的商品
例1:写一个求两个数最大的那一个
*例2:写一个函数可以交换两个整形变量的内容:
可见,这样写,程序出问题。
我们通过调试后可见:
1.我们确实把值传入了ab,说明输入没有问题,只可能是在函数里出了问题。
2.可见x和y有了新的地址,也就是说也确实收到了传入的值,但是x和y都有自己的地址,ab又有自己的地址。
3.最后确实交换了x和y的值,但是并不会交换a与b的值。所以a和b没有任何影响。
4.传给的a和b叫实参,函数的x和y叫形参
总结:当实参传给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参。
如何更改?
我们有一种通过地址来更改的方法,例如:
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
printf("%d\n", a);
return 0;
}
我们通过可以*p20这个地址,来间接更改a的值
利用这种方法,我们就可以这样改:
将swap(a,b)改成swap(&a,&b),这样我就传入了地址
函数参数从 void swap(int a,int b)改为 void swap(int * a,int * b)
这样之间就建立了一种联系。
void swap2(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap2(&a, &b);
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
由此可见,交换正常。
当然,我们也可以这样写:
函数的参数:
1.真实传给函数的参数,叫实参。
2.实参可以是:常量、变量、表达式、函数等。
3.无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。
1.形式参数是指函数名后括号中的变量
2.因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
3.形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
为什么我交换ab的时候需要取地址,比两个数的大小就不用取地址呢?
就是因为我get_max我只需求ab这两个的最大值,而不需要与ab建立联系。
而交换ab,就必须用地址建立联系,才有能力去进行交换
总结:如果要与main函数里的变量建立联系的话才需要传指针变量,其他情况传值就行了,想传地址的话也可以。传地址的功能更强大。
传值调用&&传址调用
传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用:1.传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
2.这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。
本质上都是传值,一个是传地址,一个是传值。
练习:
1. 写一个函数可以判断一个数是不是素数。
2. 写一个函数判断一年是不是闰年。
3. 写一个函数,实现一个整形有序数组的二分查找。
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
1.判断素数
//2~n-1试除
//或者用 2~sqrt(n)试除 开平方n
int sspd(int x)
{
int i = 0;
for (i = 2; i < x - 1; i++)
{
if (x % i == 0)
{
return 0;//return 0执行的时候是不会执行return1的 这函数直接就返回结束了
}
}
return 1;
}
//return的功能比break更强大,他直接就可以返回一个值然后就走了,就结束函数了,所以不需要break
int main()
{
int a = 0;
int b = 0;
scanf("%d", &a);
if (sspd(a) == 1)
{
printf("是素数");
}
else
{
printf("不是素数");
}
return 0;
}
2. 写一个函数判断一年是不是闰年。
int runnian(int x)
{
if ((x % 4 == 0 && x % 100 != 0) || x % 400 == 0)
{
return 1;
}
return 0;
}
int main()
{
int a = 0;
while ((scanf("%d", &a)) != EOF)
{
if (runnian(a) == 1)
{
printf("是闰年\n");
}
else
{
printf("不是闰年\n");
}
}
return 0;
}
//切记,函数的功能要单一
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void test(int* p)
{
*p = *p + 1;
}
int main()
{
int num = 0;
test(&num);
printf("%d\n", num);
test(&num);
printf("%d\n", num);
test(&num);
printf("%d\n", num);
return 0;
}
//
*p = *p + 1是否可以简化为 *p++ (×)
++的优先级更高,作用于p的,而不是*p
(*p)++就可以
3. 写一个函数,实现一个整形有序数组的二分查找。
先给大家来一个错误的示范:
int search(int arr[], int k)
{
int sz = sizeof(arr) / sizeof(arr[0]);
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;//找不到
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
scanf("%d", &k);
int xb = search(arr, k);
if (xb == -1)
printf("找不到");
else
printf("找到了,下标是%d", xb);
return 0;
}
原因:
1.数组在传参的时候,传递的不是整个数组,而是传递的是数组首元素的地址。
所以看起来search(int arr[ ],int k),int arr[ ]传递的是一个数组,
实际上传的是 int*arr 一个指针。
所以
指针变量的大小是4,所以4/4=1
sz为什么等于1,就是这个原因。
正确写法:
int search(int arr[], int k,int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;//找不到
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
scanf("%d", &k);
int xb = search(arr, k,sz);
if (xb == -1)
printf("找不到");
else
printf("找到了,下标是%d", xb);
return 0;
}
函数就是对过程的封装和细节的隐藏
函数的嵌套访问和链式访问
嵌套访问:我A可以调用B,B可以调用C
#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;
}
函数可以嵌套调用,但是不能嵌套定义
int main()
{
void test()
{
}
return 0;
}
❌❌这样写就错了
链式访问
把一个函数的返回值作为另外一个函数的参数
把这样的代码:
int main()
{
int len = strlen("abcdefg");
printf("%d", len);
return 0;
}
写成:
int main()
{
printf("%d", strlen("abcdefg"));
return 0;
}
把这个strlen这个函数的返回值作为printf这个函数的参数,叫做链式访问
再来看看这个代码的结果是什么?
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
1.第一步会打印最后的43
2.printf的返回值是输入字符的个数
3.所以第二个printf打印的是2,第一个printf打印的是1
所以结果是 4321
函数的声明和定义
写出来test.c的文件,要进行编译,链接,才能生成.exe文件
扫代码时,他是一行一行的扫描的下来扫描的,当第一次见到Add的时候,他之前没有见到过add函数,所以会报一行警告,实际上是有这个函数的。
怎样才能让他不警告呢?我就声明一下没有任何的警告
函数的声明时,形参可以省略,也就是说
int Add(int x,int y); 可以写成
int Add(int ,int );
1. 函数的声明告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的
4.函数的定义就是函数如何实现。
5.一般情况函数写前面就好,就可以不用声明了
6.企业一般写成一个add.c, add.h
函数的声明写到add.h,函数的定义写在add.c
add.h和add.c叫一个加法模块。
正常运行。
各自写各自的文件模块,多人开发,这样就不会冲突
愿意别人使用自己的代码,又不想让别人使用,我就只需要把声明告诉别人,函数的实现不告诉别人。
我就只需要把.lib和.h文件给别人用就行了。
把.h文件添加现有项进去,
没有任何问题,就可以正常使用了
函数递归
什么是递归?
程序调用自身的编程技巧称为递归( recursion)。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法。必须在函数体里调用自己。
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略:
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
例:一个最简单递归:
#include<stdio.h>
int main()
{
printf("hehe");
main();
return 0;
}
但是他跑到最后会报错:
执行的顺序在递归里显得尤其重要,他走的每一步,都会直到执行完成后才会执行下一步
*练习与讲解
练习1.
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4
法一:除以10再取余,存到数组里,在倒过来打印√
法二:把1 2 3 4一步一步拆成,(1234),(123)4 ,(12)3 4,(1)2 3 4
递归:
#include<stdio.h>
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);//希望他能依次打印出来
return 0;
}
1.黑线是走,红线是回。
2.调用了四次函数,每一次函数都要彻彻底底的走完了,才会继续下一步。
3.执行的顺序都显得尤其重要。
4.函数一定要执行完才算结束,顺序很重要。
5.每一次调用函数都会在内存里开辟一段空间
6.无休无止的调用,会导致空间被沾满,导致栈溢出。所以最开始的那个递归会报错。
递归的必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件
练习二:
编写函数不允许创建临时变量,求字符串的长度
先不管限制条件
法一:strlen是数\0之前的字符长度。
法二:自己写个函数传参传的是第一个字符的地址。
当不是\0时,*str++
于是我可以写成:
#include<string.h>
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
*str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int a=my_strlen(arr);
printf("%d", a);
return 0;
}
法三:编写函数不允许创建临时变量,运用递归,求字符串的长度
我可以直接retrun我要的值。
#include<string.h>
int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str + 1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcdef";
int a=my_strlen(arr);
printf("%d", a);
return 0;
}
假如拿abcd作例子
红出蓝回
对于str这个指针变量:
递归与迭代
迭代:重复执行。就有点类似于循环,循环就是一种迭代。
练习三: 求n的阶乘。(不考虑溢出)
n!=n*(n-1)!
所以我可以写成:
int fac(int x)
{
if (x <= 1)
{
return 1;
}
else
{
return x * fac(x - 1);
}
}
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d", fac(a));
return 0;
}
练习四:求第n个斐波那契数
斐波那契数:1、1、2、3、5、8、13、21、34、……
斐波那契数的特点是,前两个数相加等于第三个数。
用fib(n)来表示第N个斐波那契数所以可以写成:
int fib(int x)
{
if (x <= 2)
{
return 1;
}
else
{
return fib(x - 1) + fib(x - 2);
}
}
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d", fib(a));
return 0;
}
但是假如我要算第50个,速度就会非常的慢
当我要知道50,我就得知道49 48,49又得知道48 47,48又得知道47 46,这么以此类推。
所以速度非常的慢。
所以咋们写成非递归的:
求第一个a+b=c时,把b赋给a,c赋给b,以此循环。
int fib(int x)
{
int a = 1;
int b = 1;
int c = 1;
while (x > 2)
{
c = a + b;
a = b;
b = c;
x--;
}
return c;
}
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d", fib(a));
return 0;
}
当用递归写完比较简单且没有明显的缺陷就可以写成递归,当有明显缺陷时就不要写成递归。
思考:
1. 汉诺塔问题
2. 青蛙跳台阶问题