C语言初阶——函数

一、函数是什么

1.1 函数的定义

维基百科中对于函数的定义:子程序

在计算机学科中,子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定的任务,而且相比较于其他的代码,具备相对的独立性;一般会有输入参数并有返回值,提供对过程的封装核对细节的隐藏,这些代码通常被集成为软件库;

C语言中函数的分类包括:库函数和自定义函数。

1.2 库函数

1.2.1 库函数的定义

在C语言中,有一些特定的函数功能会被几乎所有的程序员用到,比如输入输出等,如果让每个程序员自己写相应的函数会变得杂乱不堪,所以为了方便书写,也为了标准的统一,库函数因此而生。

C语言常用的库函数有:IO函数,字符串操作函数,字符操作函数,内存操作函数,时间/日期函数,数学函数,其他库函数;

库函数的使用必须包含#include 对应的头文件。

1.2.2  库函数学习工具

(1)MSDN(Microsoft Developer Network);

(2)www.cplusplus.com;

(3)c与c++参考手册(英文版);

(4)c与c++参考手册(中文版)。

1.3 自定义函数

1.3.1 自定义函数的定义

为了实现各种各样丰富的功能,C语言提供了自定义函数,让程序员能够根据自己的需求创造包含有各种各样功能的函数,而并非如库函数一样是一成不变的。

1.3.2 自定义函数的格式

自定义函数的基本形式如下所示:参数返回类型 函数名(函数参数){函数体;}

ret_type fun_name(para1, *) {//参数返回类型 函数名(函数参数)
	statement;//语句项
}

在函数的参数中,分为实参和形参两种参数,而这两种参数的改变是否会相互影响呢?让我们来做一下的实验:

void Swap(int x, int y) {//此处的x和y是形式参数,简称形参
	int z = 0;
	z = x; x = y; y = z;
}

//当实参的值传输给形参的时候,形参是实参的一份临时拷贝
//修改形参的数值并不会影响原本实参的数值

int main() {
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d,b=%d",a,b);
	Swap(a, b);//此处的a和b是实际参数,简称实参
	printf("交换后:a=%d,b=%d",a,b);
	return 0;
}

当我们运行完如上所示的代码后,可以看到输出结果并没有按照我们所想的a和b的值进行互换,而是保持不变,这是因为函数中形参的改变不会改变实参的值,所以我们需要通过如下方式来进行实参的改变从而实现两个数的交换:

void Swap(int* px, int* py) {
		int z = *px;//使 z = a;
		*px = *py;//使 a = b;
		*py = z;//使 b = z = a;
	}

int main() {
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d,b=%d",a,b);
	Swap(&a, &b);
	printf("交换后:a=%d,b=%d",a,b);
	return 0;
}

四、函数的参数与函数调用

4.1 实际参数和形式参数

4.1.1 实际参数(实参)

真实传给函数的参数叫做实参,实参可以是:常量,变量,表达式,函数等等;例如上述代码中Swap(a,b)中的a,b。(可以粗略的理解为函数调用时进入函数的变量、常量等)

无论实参是何种类型的量,在进行函数调用的时候,它们都必须要有确定的值,以便于把这些值传送给形参;

4.1.2 形式参数(形参)

形式参数指的是函数名后面括号中的变量,因为形式参数只有在函数被调用的时候才能被实例化(分配内存单元),所以叫做形式参数。例如上述函数中void Swap(int x,int y)中的x,y。(也可以粗略的理解为定义函数时函数的参数列表,基本上是以形参的形式存在的)

形式参数在函数调用完成之后就会自动销毁,因此形式参数只有在函数中有效。

在函数进行调用的时候,形参被分配了自己的内存单元,拥有了属于自己的空间,同时也拥有了和实参一模一样的内容,因此可以简单的认为:

形参实例化之后其实相当于实参的一份临时拷贝,除了地址不同,其他一样。

形参和实参的名字可以相同,但是他们会映射到不同的内存单元,并不是完全相同的两个变量。

4.2 函数调用

4.2.1传值调用

函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。(简单来说就是,只把数值传进去了,而函数无论怎么操作都不会影响实参)

4.2.2 传址调用

(1)传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。(简单来说就是把实参的地址传递给形参,通过解引用操作符来直接修改实参的值)

(2)这种传参方式可以让函数与函数外面的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量;

4.3 经典练习题

4.3.1 写一个函数判断一个数是不是素数(输出100到200之间的素数)

#include
#include

int is_prime(int n) {//是素数返回1,不是素数返回0;
	for (int j = 2; j <= sqrt(n); j++) {//sqrt是开平方函数,需要引用头文件math.h
		if (n % j == 0) {
			return 0;
		}
	}
	return 1;
}

int main() {
	int count = 0;
	for (int i = 101; i <= 200; i += 2) {
		if (is_prime(i)) {
			printf("%d ", i);
			count++;
		}
	}
	printf("\ncount - %d\n",count);
}

4.3.2 写一个函数判断某一年是不是闰年(打印1000到200年之间的闰年)

#include
//判断一年是不是闰年:(以下条件满足其一)
//1.能被4整除且不能被100整除;
//2.能被400整除
int is_leap_year(int n) {
	if (((n % 4 == 0) && (n % 100 != 0)) || (n % 400 == 0))
		return 1;
	else
		return 0;
}

int main() {
	int year = 0, count = 0;
	for (year = 1000; year <= 2000; year++) {
		if (is_leap_year(year)) {
			printf("%d ", year);
			count++;
		}
	}
	printf("\ncount - %d\n", count);
	return 0;
}

4.3.3 写一个函数实现一个整型有序数组的二分查找

#include
//此处定义的形参和实参没有关系,不是一个变量和数组
int binary_search(int arr[], int k, int sz) {//形参arr看起来是数组,本质上是指针变量
	//数组传参实际上传递的是数组首元素的地址,而不是整个数组。
	//因此在函数内部计算一个函数参数部分的数组的元素个数是不靠谱的,应该在主函数中计算。
	int left = 0;
	int right = sz - 1;
	while (left <= right) {
		int mid = left + (right - left) / 2;
		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 = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1) {
		printf("找不到\n");
	}
	else {
		printf("找到了,下标是:%d\n", ret);
	}
	return 0;
}

4.3.4 写一个函数,每调用一次这个函数,都会将num的值增加1

#include

void ADD(int* p) {
	(*p)++;
}

int main() {
	int num = 0;
	ADD(&num);
	printf("%d\n", num);
}

五、函数的嵌套使用和链式访问

函数与函数之间可以根据实际的需求进行组合,可以相互调用;

5.1 嵌套调用

简单来说,就是两个定义好了的函数,一个函数可以在函数体部分调用另一个函数。

但是函数可以嵌套调用,不可以嵌套定义!!!即函数内部不能再次定义一个函数。

5.2 链式访问

把一个函数的返回值作为另一个函数的参数,就叫做链式访问。

#include
#include

int main() {
	int len = strlen("abcdef");
	printf("%d\n", len);
	//普通的打印方式如上
	printf("%d\n", strlen("abcdef"));
	//将一个函数的返回值作为另一个函数的参数,形成类似链条的访问过程。
	printf("%d", printf("%d", printf("%d", 43)));
	//以上代码结果为4321,可以自行推断一下,巩固链式访问的知识。
}

六、函数的声明和定义

6.1函数的声明

(1)函数的声明就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但不是具体存不存在,函数的声明解决不了;

(2)函数的声明一般出现在函数出现之前,要满足先声明后使用的条件;

(3)函数的声明一般都是放在头文件中的——建立一个.c文件和.h文件,文件名称是函数名称,在.h文件中进行函数的声明,在.c文件中进行函数的定义;

注意:这个注意事项是因为在学习过程当中一个源文件当然是可以的,但是在公司工作的时候并不是一个人进行所有代码的书写,而是按照模块功能进行拆分的。

(4)函数声明存在的原因:在写代码的过程当中,如果main函数在前面,自定义函数在后面,程序是按照顺序执行的,所以main函数中扫描到自定义函数的时候,会发出一个警告(不是错误);所以通过在main前进行函数的声明,可以避免警告的产生。

6.2 函数的定义

函数的定义指的是函数的具体实现,交代函数的功能实现(函数体);

 拓展:静态库设置:右击项目名称——属性——配置属性——常规——将配置类型(应用程序改为静态库)——编译除了.lib文件(静态库文件)——卖出去

七、函数递归

7.1 递归的定义

程序调用自身的编程技巧称为递归;(比如:函数内部调用自身);

递归作为一种算法在程序设计语言中应用广泛,一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化成余元问题相似的规模较小的问题以求解;只需要少量的程序就可以描述出解题过程中所需要的多次重复计算,大大减少了程序的代码量

#include

void print(unsigned int n) {
	if (n > 9) {
		print(n / 10);
	}
	printf("%d ", n % 10);
}

int main() {
	unsigned int num = 0;
	scanf("%u", &num);
	print(num);
	//接受一个无符号整型,按顺序打印他的每一位
	return 0;
}

7.2 递归的两个必要条件

(1)存在限制条件:当满足这个限制条件的时候,递归便不再继续;

(2)每次递归调用之后会越来越接近这个限制条件,直到达到这个限制条件。

 7.3 练习

编写函数求字符串的长度,且不允许创建临时变量

(1)创建临时变量的解决办法:

//求字符串的长度——模拟实现strlen
#include
//int my_strlen(char str[])//参数部分写成数组的形式
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\n", len);
	return 0;
}

(2)未创建临时变量的解决办法:

#include

int my_strlen(char* str) {
	if (*str != '\0')
		return 1 + my_strlen(str + 1);
	else
		return 0;
}

int main() {
	char arr[] = "abc";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

7.4 递归与迭代

7.4.1 迭代:

(1)定义:从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。这里可以看为 for (i=1 , i<100 , i++);

(2)简要说明:迭代就是自己执行很多次,每次都能排除一个,直到找到结果。

用迭代的方法求斐波拉契数列(1,1,2,3,5,8,13,21,34,55......) 

#include

int Fib(int n) {
	if (n == 1 || n == 2){
		return 1;
	}else {
		int a = 1, b = 1, c = 0;
		while (n >= 3) {
			c = a + b;
			a = b;
			b = c;
			n--;
		}
		return c;
	}
}

int main() {
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

7.4.2 递归:

(1)递归的定义:想象成一个树结构,从字面可以其理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。

(2)简要说明:递归就是每次循环都包含自己的请求,子问题必须携带原始问题,且一次一次缩小范围,最终找到结果

用递归的方法求斐波拉契数列(1,1,2,3,5,8,13,21,34,55......) 

#include

int Fib(int n) {
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}

int main() {
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

缺陷:用递归的方式求斐波拉契数列总是会进行大量重复的计算,这个过程当中会浪费很多的资源和时间。 

7.4.3 注意事项

(1)在调试斐波拉契数列函数的时候,如果给予的参数比较大,那么就好报错:stack overflow(栈溢出)现象;

原因:系统分配给程序的栈空间是有限的,但是如果出现了死循环或者死递归的现象,可能会导致一直开辟栈空间,导致栈空间耗尽的情况,这种现象被称为栈溢出。

(2)针对出现栈溢出的现象进行的调整:

  1. 将递归改成非递归;
  2. 使用static对象替代nostatic局部对象;

(3)对于递归函数的提示:

  1. 许多问题是用递归的形式进行解释的,这是因为它比非递归的形式更加的清晰;
  2. 但这些问题的迭代实现往往比递归实现的效率要高得多,虽然代码的可读性会差些;
  3. 当一个问题相当复杂,难以用迭代实现,此时递归实现的简洁性可以补偿它带来的运行时的开销。

你可能感兴趣的:(C语言笔记,c语言,开发语言,学习)