函数与模块化程序设计

目录

函数的分类

标准库函数

自定义函数

函数的定义

自定义函数格式

函数调用

二分查找

函数原型与声明

变量的作用域和生存期

变量的作用域

全局变量与局部变量

变量的生存期

自动变量

静态变量

外部变量

寄存器变量

数据存储与栈溢出

函数的嵌套调用与链式访问

函数封装与防御性程序设计

函数设计基本原则

递归函数

基本介绍

实例

模块化程序设计

条件编译


函数的分类

标准库函数

在使用某些函数前,必须在程序的开头把该函数所在的头文件包含起来

C语言库函数
 IO函数:输入输出函数,printf scanf getchar putchar
 字符串操作函数:strcmp strlen
 字符操作函数:toupper(小写转大写)
 内存操作函数:memcpy memcmp memset
 时间/日期函数:time
 数学函数:sqrt pow
 其他库函数

自定义函数

如果库函数不能满足自己的需要时,就需要我们自己编写自己所需要的函数来完成自己所需要的功能,我们称这一类函数为自定义函数

函数的定义

自定义函数格式

返回值类型  函数名  (类型  形式参数1,  类型  形式参数2,...)

{

    声明语句序列;

    可执行语句序列;

}

函数名是函数唯一标识,用于说明函数的功能,为了便于区分,通常变量名小写字母开头的单词组合而成,函数名则用大写字母开头的单词组合单词组成

函数体必须用一对花括号包围,{}就是函数体的界定符,在函数体内部定义的变量只能在函数体内访问,成为内部变量

函数头部参数表里的变量,成为形式参数,简称形参,形参表是函数的入口,而函数的返回值就是函数的结果,若函数没有返回值,则需用void定义返回值的类型;若函数值不需要入口参数,则需用void代替形参表中的内容,表示该函数不需要任何外部数据

注意,在函数定义前面写上一段注释来描述函数功能及其形参是一个非常好的习惯

函数调用

函数必须被main()函数直接或间接调用才能发挥作用,在主函数中调用其它函数时,必须提供一个称谓实际参数(简称实参)的表达式给被调用的函数

调用其他函数的函数简称为主调函数,被调用的函数简称为被调函数,主调函数把实参的值复制给形参的过程,称为参数传递

int swap(int a, int b)//传值调用
{
	a += b;
	b = a - b;
	a = a - b;
}
int main()
{
	int a = 1;
	int b = 2;
	swap(a, b);
	printf("a=%d,b=%d\n", a, b);
	return 0;
}

运行结果:
a=1,b=2

我们会发现,明明已经调用了函数,为什么没有实现两个值的交换呢?

原因是在该函数被调用时,实参传给形参,其实形参是实参的一份临时拷贝,改变形参不能改变实参的值 ,我们通过debug可以看到其实形参和实参的地址是不一样的,所以改变形参并无法起到改变实参的作用

那么我们要怎么写才能实现我们想要的效果呢?在这里需要提前了解指针的一些内容

int main()
{
	int a = 10;//生成四个字节的空间
	int* pa = &a;//pa就是一个指针变量,可以通过*pa来找到a
	*pa = 20;//此时a的值就通过找a的地址进行定位实现改变了
	printf("a=%d\n", a);
	return 0;
}

运行结果:
a=20

 所以,我们可以做出以下更改

void swap(int* pa, int* pb)//传址调用
{
	*pa += *pb;
	*pb = *pa - *pb;
	*pa = *pa - *pb;
}
int main()
{
	int a = 1;
	int b = 2;
	swap(&a, &b);
	printf("a=%d,b=%d\n", a, b);
}

运行结果:
a=2,b=1

那么,我们什么时候需要用到传值调用,什么时候又要用到传址调用呢?

当改变函数外部变量的时候,需要写地址,如果生成了新变量,则不需要写地址,但是需要返回值和外部接收 

void add(int* pa)
{
	*pa += 1;
}
int main()
{
	int a = 0;
	add(&a);
	printf("a=%d\n", a);
	add(&a);
	printf("a=%d\n", a);
	return 0;
}

运行结果:
a=1
a=2

注意,函数的返回值只能有一个,函数返回值的类型可以是除数组以外的任何类型,函数中的return语句可以有多个,但不表示函数可以有多个返回值

当函数的返回值类型为void型时,表示函数没有返回值,函数可以没有return语句,程序会一直运行到函数的最后一条语句后再返回,如果程序不是运行到函数的最后一条语句才返回,那么必须使用return语句返回,无须返回任何值的return语句可以写成:return;

二分查找

int binary(int arr[], int key, int sz)
 //数组不传大小?因为并非真正创建数组,所以不用传大小,想要传递数组大小只能在函数外部传递
{
	int left = 0;
	int right = sz - 1;
	while(left<=right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > key)
		{
			right = mid - 1;
		}
		else if (arr[mid] < key)
		{
			left = mid + 1;
		}
		else
		{
			return mid;
		}
	}
	return -1;
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };

	int key = 7;//查找对象
	//printf("%d\n", arr[15]);
	int sz = sizeof(arr) / sizeof(arr[0]);//总字节长度/单个元素字节长度=元素个数
	//找到就返回下标位置,找不到就返回-1
  //能不能在函数里使用sizeof让主函数中不计算长度呢?不可以,在函数中使用sizeof会导致长度为1
  //数组传参,传递的实际不是数组本身,只穿过去了数组第一个元素的地址
	int ret = binary(arr, key, sz);
	if (-1 == ret)
	{
		printf("找不到\n");
	}
	else
	{
		printf("下表位置为%d\n", ret);
	}
	return 0;
}

函数原型与声明

有时,我们可能会习惯的把自定义函数写在主函数的下面,这时程序可能就会无法运行,原因是运行到主函数时,没有读取到自定义函数,所以会报错,这时,我们就需要在全局中将自定义函数的函数原型进行声明,便可以运行了

void test(int a);//函数原型声明
int main()
{
	int a = 1;
	test(a);
	return 0;
}

void test(int a)
{
	NULL;       //函数原型
}

变量的作用域和生存期

变量的作用域

程序中被花括号括起来的区域,叫做语句块,函数体,分支语句,循环语句都是语句块,变量的作用域的规则是:每个变量仅在定义它的语句块(包含下级语句块)内有效,并且拥有自己的存储空间

不在任何语句块内定义的变量,称为全局变量,全局变量在程序的所有位置均有效;相反的,在除整个程序以外的其他语句块内定义的变量,称为局部变量

全局变量与局部变量

全局变量从程序的运行开始就占据内存,仅在程序结束时才释放,所谓释放内存,就是将内存中的值恢复为随机值(即乱码)

全局变量在不指定初值时会自动初始化为0

变量的生存期

变量的存储类型一般声明方式如下:

存储类型    数据类型    变量名表;

自动变量

标准定义格式:

auto  类型名  变量名; 

如果没有指定变量的存储类型,那么变量的存储类型就缺省为auto

自动变量的“自动”体现在进入语句块时自动申请内存,退出语句块时自动释放内存,在退出语句块以后不能再访问,所以自动变量也被称为动态局部变量 

(1)自动变量在定义时不会自动初始化

(2)自动变量在退出函数后,其分配的内存立即被释放,再次进入语句块时,该变量被重新分配内存,所以不会保存上一次退出函数前所拥有的值

静态变量

定义格式:

static  类型名  变量名;

静态变量是与程序“共存亡”的,而自动变量是与程序块“共存亡”的。静态变量的值可以保持到下一次函数调用,是因为静态变量是在静态存储区分配内存的,在静态存储区分配的内存在程序运行期间是不会被释放的,其生存期是整个程序运行期间

long Func(int n)
{
	long p = 1;
	p = p * n;
	return p;
}
int main()
{
	int i, n=10;
	for (i = 1; i <= n; i++)
	{
		printf("%d!=%ld\n", i,Func(i));
	}
	return 0;
}
运行结果:
1!=1
2!=2
3!=3
4!=4
5!=5
6!=6
7!=7
8!=8
9!=9
10!=10
long Func(int n)
{
	static long p = 1;
	p = p * n;
	return p;
}
int main()
{
	int i, n=10;
	for (i = 1; i <= n; i++)
	{
		printf("%d!=%ld\n", i,Func(i));
	}
	return 0;
}
运行结果:
1!=1
2!=2
3!=6
4!=24
5!=120
6!=720
7!=5040
8!=40320
9!=362880
10!=3628800

静态局部变量与自动变量都是在函数内部定义的,因此它们的作用域都是局部的

外部变量

 定义格式:

extern  类型名  变量名;

外部变量也是在静态存储区分配内存的,其生存期是整个程序的运行期,没有显示初始化的外部变量由编译程序自动初始化为0

静态变量与全局变量的异同点

同:都是在静态存储区分配内存的,都只分配一次存储空间并且仅被初始化一次,都能自动初始化为0,其生存期都是整个程序运行期间

不同:作用域可能是不同的,这取决于静态变量是在哪里定义的

在函数内部定义的,称为静态局部变量,只能在定义它的函数内被访问

静态全局变量,可以在定义它的文件内的任何地方被访问

寄存器变量

定义格式:

register  类型名  变量名;

寄存器是CPU内部的一种容量有限但速度极快的存储器

数据存储与栈溢出

void test(int a)
{
	if (a < 10000)
	{
		test(a++);
	}
}
int main()
{
	test(1);
	return 0;
}

程序运行时不会报错,但是一直不会结束,原因是这个程序进入了死递归,但是如果我们debug的话,可以发现程序会报错:Stack overflow,意味栈溢出,不断的调用test,不断的分配空间,却没有释放空间(执行完成这个栈区)直到栈区空间不足,造成栈溢出 

 内存划分
栈区:局部变量,函数形参,调用函数时返回值等临时变量
堆区:动态内存分配区:malloc/free,calloc,realloc
静态区:全局变量,静态变量

函数的嵌套调用与链式访问

 函数的嵌套调用

 void a()
 {
     NULL;
 }
 void b()
 {
     a();
 }
 int main()
 {
     b();
     return 0;
 }

链式访问: 将一个函数的返回值,作为另一个函数的参数

int main()
{
    int len = strlen("abc");
	printf("%d\n", len);
	//链式访问
	printf("%zd\n", strlen("abc"));
    return 0;
}
int main()
{
	printf("%d\n", printf("%d", printf("%d", 4)));
	printf("%d\n", printf("%d", printf("%d", 43)));
	//printf()的返回值类型是int,返回字符打印的个数,所以先打印43,之后返回2,打印2,返回1,打印1
	printf("%d\n", printf("%d", printf("%d\n", 43)));//转义字符也算入
	printf("%d\n", printf("%d", printf("%d", 54)));
	return 0;
}
运行结果:
411
4321
43
31
5421

函数封装与防御性程序设计

为了增加程序的健壮性,使函数具有防弹功能,需要在函数的入口增加对函数参数合法性的检查和对函数返回值的检验

函数设计基本原则

 (1)函数规模要小,控制在50行以内,容易维护,出错几率小

(2)功能单一

(3)只有一个入口和一个出口,尽量不要使用全局变量传递信息

(4)应当尽可能多地考虑一些可能出错的情况,定义好函数接口后,轻易不要改动

(5)在函数入口,对参数的有效性进行检查

(6)对执行某些敏感性操作时(如执行除法,开放,取对数,赋值,函数参数传递等)之前,应检验操作数及其类型的合法性

(7)要考虑到如果调用失败,应该如何处理

(8)通常通过返回值来报告错误,因此调用函数时要校验函数地返回值,以判断函数调用是否成功

(9)确保函数的实参和形参类型相匹配

(10)确保函数中所有控制分支都有返回值

递归函数

基本介绍

如果一个对象部分地由它自己组成或按它自己定义,就成它是递归的

long Fcct(int n)
{
	if (n < 0)
	{
		return -1;//处理非法数据
	}
	else if (n == 0 || n == 1)
	{
		return 1;//基线情况,即递归终止条件
	}
	else         //一般情况
	{
		return (n * Fact(n - 1));//递归调用
	}
}

写递归代码的时候:
1.不能死递归,要有跳出条件,每次递归逼近跳出条件
2.递归层次不能太深

递归函数必须包含:

1.由其自身定义的与原始问题类似的更小规模的子问题,称为一般情况

2.递归调用的最简形式,用来结束递归调用,称为基线情况

实例

编写函数不允许创建临时变量,求字符串长度-模拟实现strlen函数

创建临时变量:

int slen(char* str)
{
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;//指向下一个字符地址,因为str其实是形参*str
	}
	return count;
}

不创建临时变量:

思路:'abc\0'=1+'bc\0'=1+1+'c\0'=1+1+1+'\0'=1+1+1+0=3

int slen(char* str)
{
	if (*str != '\0')
	{
		return 1 + slen(str+1);
	}
	else
	{
		return 0;
	}
}
int main()
{
	char arr[] = "abc";
	//['a'] ['b'] ['c'] ['/0']
	//传地址,先传a,再传b,以此类推
	printf("%d\n", slen(arr));
	return 0;
}

/不能用str++?

原因是前者是传入str+1,留下str(没变),后者是传入str,留下str+1,导致死递归,栈溢出,改为++str也可以解决,但是留下的同样也+1,如果保留原值,则不能这么写

求第n个斐波那契数列

int Fib(int n)
{
	if (n == 1 || 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", ret);
	return 0;
}

这个程序在运行时,会发现随着n的增加,运行时间会呈指数爆炸式增长,原因是在每次调用Fib(n)的时候,都要计算一次Fib(n-1)和Fib(n-2),以此类推,效率低

这时,我们不妨考虑使用循环的方式实现这个问题

int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 0;
	int ret = 1;
	int a = 1;
	int b = 1;
	if (n == 1 || n == 2)
	{
		printf("%d",ret);
	}
	else
	{
		for (i = 3; i <= n; i++)
		{
			ret = a + b;
			a = b;
			b = ret;
		}
		printf("%d",ret);
	}
	return 0;
}

 实现字符串的反向输出,如:abc ->cba

循环:

void reverse_string(char* str)
{
	int left = 0;
	int right = strlen(str) - 1;
	while (left < right)
	{
		char tmp = str[left];
		str[left] = str[right];
		str[right] = tmp;
		left++;
		right--;
	}
}

int main()
{
	char arr[] = "abcdef";
	reverse_string(arr);
	printf("%s\n", arr);
	return 0;
}

递归:

了解一下:

int main()
{
	char arr[] = "abcd";
	char *tmp = arr + 1;
	printf("%s\n", tmp);
	return 0;
}
运行结果:
bcd
void reverse_string(char* str)
{
	char tmp = *str;
	int len = strlen(str);
	*str = *(str + len - 1);//第一个更换为最后一个  fbcdef
	*(str + len - 1) = '\0';
    //如果直接将a填进去,由于字符串的读取到\0结束,所以读取fbcda时,不容易更改内部四个,所以将a 
      的位置放上\0,方便更改内部,之后再加上去
	//fbced\0(\0)
	if (strlen(str + 1) >= 2)//
	{
		reverse_string(str + 1);//bdce\0(\0)---dce\0(\0\0 )   ce\0(\0\0)---e\0(\0\0\0)
		//由于使用的是地址+1,所以读取的时候只会读取比上回后一位的字符串,再进行更改,并不会影响 
          之前更改过的
	}
	*(str + len - 1) = tmp;//将最近的\0更改为字母
}
int main()
{
	char arr[] = "abcdef";
	reverse_string(arr);//数组名arr是数组arr首元素的地址
	printf("$s\n", arr);
	return 0;
}

利用递归,输入一个非负整数,输出每位数字加和,168-15

int DigitalSum(int a) 
{
	if (a > 9)
	{
		return a % 10 + DigitalSum(a / 10);
	}
	else
	{
		return a;
	}
	return a % 10 + DigitalSum(a / 10);
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	int sum=DigitalSum(a);
	printf("%d\n", sum);
	return 0;
}

利用递归,实现n^k的计算,即pow函数

float Pow(float a, float b)
{
	if (b > 1)
	{
		b -= 1;
		return a * Pow(a, b);
	}
	else if (b == 0)
	{
		return 1;
	}
	else if (b < 0)
	{
		b = -b;
		b -= 1;
		return 1/(a * Pow(a, b));
		//直接写为  return 1/Pow(a,-b);
	}
	else
	{
		return a;
	}
}
int main()
{
	float n, k;
	scanf("%f,%f", &n, &k);
	float result = Pow(n, k);
	printf("%f\n", result);
	return 0;
}

模块化程序设计

模块化处理:将不同的功能写进不同的模块,每个模块各有一个头文件和源文件

原则:高聚合,低耦合,保证每个模块的相对独立性

如何将代码分成多个文件?如何在多个文件中共享函数?

在模块分解后,每个模块均由一个扩展名为.c的源文件和一个扩展名为.h的头文件构成,其中main函数所问的文件称为主模块

把需要共享的函数放在一个单独的.c文件中,把共享函数的函数原型,宏定义和全局变量声明等放在一个单独的.h头文件中,其他需要共享这个函数的程序用#include包含这个头文件后,就可以调用这个函数了

需要注意的是,头文件里对全局变量的声明要加上extern关键字,用以声明该变量为外部变量。变量声明与变量定义不同的是:对于变量声明,编译器并不对其分配内存

计算器中的加法模块

源文件:

int Add(int a, int b)
{
	return a + b;
}

头文件:

int Add(int a, int b);

主模块:

#include "Add.h"
int main()
{
	int a = 10, b = 20;
	printf("%d", Add(a, b));
	return 0;
}

条件编译

在编写程序的时候,我们可能会出现以下情况:编写了a.h,b.h,但是b.h中包含了a.h,但是在源文件中我们又需要同时包含a.h,b.h,这样做的话,我们会发现,a.h被多次包含了,这样会引起头文件中宏常量的多次定义,这时我们就需要使用被称为条件编译的编译预处理命令

#ifndef A

#define A

...//头文件内容

#endif

这样操作的话,就可以当头文件被再次包含的时候,因为已经被A已经被定义,所以不再执行#ifndef和#endif之间的内容

条件编译由#if,#ifdef,#ifndef,#else,#elif和#endif组合而成

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值