C语言从入门到精通 第八章(用函数实现模块化程序设计)

一、函数的定义

1、概述

(1)函数是一个可以独立完成某个功能的语句块,其主要作用是将复杂程序拆成若干易于实现的子程序。

(2)在C语言中,函数分为标准函数(又称为预定义函数)和用户自定义函数。

2、函数的定义形式

<返回类型> <函数名>(<形参列表>)

{

<函数体>

}

(1)函数名一般是标识符,最好能反映函数的功能。

(2)形参列表由逗号分隔,分别说明函数的各个形参,形参将在函数被调用时从调用函数那里获得数据(也就是把实参的数据拷贝到形参中)。形参列表可以为空,但是括号不能省略。

(3)返回类型又称函数类型,表示一个函数所计算(或运行)的结果值的类型,如果一个函数没有结果值,如函数仅用来更新(或设置)变量值、显示信息等,则该函数返回类型为void类型。

①return; ②return <表达式>;

①在返回类型为void的函数体中,若想跳出函数体,将执行流程转移到调用该函数的位置,应使用return语句的第一种格式(也就是return后面不带表达式的一种)。

②在返回类型不是void的函数体中,应使用(应该是必须使用)return语句的第二种格式(也就是return后面带表达式的一种),使执行流程转移到调用该函数的位置,并将<表达式>的值作为函数的返回值。

二、函数的调用

1、概述

(1)C语言中函数调用的一般形式为:

<函数名>(<实参表>)

(2)当调用一个函数时,其实参的个数、类型及排列次序必须与函数定义时的形参相一致,也就是实参与形参应该一对一地匹配。若函数定义时没有形参,则函数调用时实参表亦为空(括号不能省略)。

2、语句调用

(1)语句调用通常用于不带返回值的函数。

(2)举例:

①判断一个数是不是素数:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <math.h>

void func(int a)
{
	int i = 2;
	for (; i <= sqrt(a); i++)
	{
		if (a%i == 0)
		{
			printf("不是素数\n");
			break;
		}
	}
	if (i > sqrt(a))
	{
		printf("是素数\n");
	}
}

int main()
{
	int input;
	scanf("%d", &input);
	func(input);

	return 0;
}

②判断一个年份是不是闰年:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void func(int a)
{
	if ((a % 4 == 0 && a % 100 != 0)|| a % 400 == 0)
	{
		printf("%d是闰年\n", a);
	}
	else
	{
		printf("%d不是闰年\n", a);
	}
}

int main()
{
	int input;
	scanf("%d", &input);
	func(input);

	return 0;
}

3、表达式调用

(1)将被调用函数作为表达式的一部分进行调用,适用于被调用函数带有返回值的情况。

(2)举例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int max(int a, int b)
{
	return a > b ? a : b;
}

int main() {

	printf("%d\n",max(10, 20));  //表达式调用
	//10和20称为实际参数,调用函数时,实际参数的值会传递给形式参数

	return 0;
}

4、参数调用

(1)被调用函数作为另一个函数的一个参数进行调用。

(2)举例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	//把一个函数的返回值作为另外一个函数的参数
	//printf函数的返回值是打印在屏幕上字符的个数
	return 0;
}

5、嵌套调用

(1)被调用函数中调用了另一个函数。

(2)举例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}

三、函数的声明

1、概述

(1)在C++中,函数在使用之前要预先声明,这种声明在标准C++中称为函数原型,函数原型给出了函数名、返回类型以及在调用函数时必须提供的参数的个数和类型。

(2)函数原型的语法为:

<返回类型> <函数名>(<形参列表>); //注意要有分号

2、两种声明形式

(1)直接使用函数定义的头部,并在后面加上一个分号。

(2)在函数原型声明中省略参数列表中的形参变量名,仅给出函数名、函数类型、参数个数及次序。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//代码是从上往下进行编译的
//int max(ine a, int b);  可以用这一行声明告诉计算机有这个被定义的函数,这样max函数的定义可以挪到主函数下面

int max(int, int);
int max(int a, int b);  //声明可以有多次,定义只能有一次

int main()
{

	int num1 = 10;
	int num2 = 20;
	printf("%d\n", max(10, 20));  //表达式调用

	return 0;
}

int max(int a, int b)  //倘若函数的实现在函数被调用之前,那么可以不需要对该函数进行声明
{
	return a > b ? a : b;
}

四、函数返回类型

1、四类函数的定义形式

(1)带参数的有返回值函数:

<返回类型> <函数名>(<形参列表>)

{

<语句序列>

return <表达式>;

}

(2)不带参数的有返回值函数:

<返回类型> <函数名>( )

{

<语句序列>

return <表达式>;

}

(3)带参数的无返回值函数:

void <函数名>(<形参列表>)

{

<语句序列>

return; //如果这是最后一条语句,那么可有可无

}

(4)不带参数的无返回值函数:

void <函数名>( )

{

<语句序列>

return; //如果这是最后一条语句,那么可有可无

}

2、举例

(1)有参有返------每调用一次函数,就会将num的值增加1。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int func(int num) //有参有返
{
	return num + 1;
}

int main()
{
	int num;
	scanf("%d", &num);
	
	num = func(num);
	num = func(num);
	printf("%d\n", num);

	return 0;
}

(2)无参有返------返回当前全局变量的值。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int a = 0;

int return_a()  //无参有返
{
	return a;
}

int main()
{
	a++;
	printf("%d\n", return_a());
	a++;
	printf("%d\n", return_a());
	a++;
	printf("%d\n", return_a());

	return 0;
}

(3)有参无返------有序数组的二分查找。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void func(int a[] ,int b ,int len) //有参无返
{
	int left = 0;
	int right = len - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (a[mid] > b)
		{
			right = mid - 1;
		}
		if (a[mid] < b)
		{
			left = mid + 1;
		}
		if (a[mid] == b)
		{
			printf("找到了,下标为%d\n", mid);
			break;
		}
	}
	if (left > right)
	{
		printf("未找到!\n");
	}
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int input;
	scanf("%d", &input);
	int len = sizeof(arr) / sizeof(arr[0]) - 1;
	func(arr, input, len);

	return 0;
}

(4)无参无返------打印一个游戏菜单选择界面。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void menu()  //无参无返
{
	printf("************************************\n");
	printf("******* 1.开始游戏 0.退出游戏 ******\n");
	printf("************************************\n");
}

int main()
{
	menu();

	return 0;
}

五、函数参数

1、参数的传递方式

(1)值传递:将实参值的副本传递(复制)给被调用的形参,函数运作过程中不会也不能影响实参

(2)地址传递:参数类型是指针,将实参值的指针副本传递(复制)给被调用的形参,函数运作过程通过对指针解引用可以访问实参,进而可以影响实参。(下一章将会详细介绍指针)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//交换两个实参中的数据------错误的函数
void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}
//交换两个实参中的数据------正确的版本
void Swap2(int *px, int *py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
	int num1 = 1;
	int num2 = 2;
	Swap1(num1, num2);
	printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
	Swap2(&num1, &num2);
	printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

2、默认参数

(1)在C语言中,可以为形参指定默认值,在函数调用时没有指定与形参相对于的实参时就自动使用默认值

(2)默认参数通常在函数名第一次出现在程序中的时候指定,如在函数定义时的参数列表中指定默认参数,从语法上看指定默认参数和变量初始化类似。

(3)如果一个参数中有多个参数,则默认参数应从右至左逐个定义(如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值)。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int func(int a, int b = 20, int c = 30)  //函数默认参数
{
	return a + b + c;
}
//如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
//如果函数声明有默认参数,函数实现就不能有默认参数(二者最多只能有一者有默认参数)
/*
int fun(int a = 10, int b = 10);

int fun(int a = 10, int b = 10)    这一行不应该再写"= 10"了,否则会出现"二义"
{
	;
}
*/

int main()
{
	printf("%d\n", func(12));
	printf("%d\n", func(12, 30));

	return 0;
}

六、递归函数

1、概述

(1)如果一个函数在其函数体内直接或间接地调用了自己,该函数就称为递归函数。

(2)使用递归需要注意以下几点:

①用递归编写代码往往较为间接,但一般要牺牲一定的效率,因为系统处理递归函数时都是通过压栈/退栈的方式实现的。

②无论哪种递归调用,都必须有递归出口,即结束递归调用的条件。

③编写递归函数时需要进行递归分析,既要保证正确使用了递归语句,还要保证完成了相应的操作。

2、举例

(1)接受一个整型值(无符号),按照顺序打印它的每一位。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}

(2)编写一个函数,不允许创建临时变量,求字符串的长度。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int strlen(const char* str)
{
	if (str[0] != '\0')
	{
		str++;
		return 1 + strlen(str);
	}

	return 0;
}

int main()
{
	char str[] = "lalalalalala";
	
	printf("%d\n", strlen(str));

	return 0;
}

(3)求n的阶乘。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int factorial(int n)
{
	if (n > 0)
	{
		return n * factorial(n - 1);
	}
	return 1;
}

int main()
{
	int n;
	scanf("%d", &n);
	printf("%d\n", factorial(n));

	return 0;
}
//使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃
//系统分配给程序的栈空间是有限的,但是如果出现了死循环或者死递),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

(4)求第n个斐波那契数。(不考虑溢出)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int fib(int n)
{
	if (n > 1)
	{
		return fib(n - 1) + fib(n - 2);
	}
	if (n == 1 || n == 0)
	{
		return 1;
	}
}

int main()
{
	int n;
	scanf("%d", &n);
	printf("%d\n", fib(n));

	return 0;
}
//在使用 fib 这个函数的时候,如果要计算第50个斐波那契数字,会特别耗费时间
//这时可以将递归改为非递归,防止出现栈溢出的现象,同时效率也会提高

七、代码的分文件编写

1、分文件编写的目的和步骤

(1)分文件编写的目的:在实际开发中,代码量往往不止几十行,为了便于管理和检查错误,通常需要将不同功能的代码写在不同的文件中。

(2)函数的分文件编写步骤:

①将函数的实现写在其它源文件(也就是.cpp文件)中。

②将函数的声明写在与其源文件同名的头文件(也就是.h文件)中,使用"#include"预处理指令将函数实现需要包含的头文件(比如stdio.h)添加在该头文件中。

③在函数实现所在的源文件中使用"#include"预处理指令将函数声明所在的头文件添加进来。

④需要调用函数的文件中使用"#include"预处理指令将函数声明所在的头文件添加进来即可。

(3)举例:

test.h的内容:放置函数的声明。

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__    防止头文件被重复定义

test.c的内容:放置函数的实现。

cpp 复制代码
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
	return x + y;
}

2、头文件被包含的方式

(1)本地文件(自己写的)包含------ #include "filename.h"

①查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误。

②不同环境的标准头文件路径:

[1]Linux环境的标准头文件的路径:/usr/include

[2]VS环境的标准头文件的路径:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include (按照安装路径去找)

(2)库文件(C语言提供的)包含------ #include <filename.h>

①查找策略:直接去标准路径下去查找,如果找不到就提示编译错误。

②对于库文件也可以使用" "的形式包含,但是这样做查找的效率就低些。

(3)程序在编译时实际上只需编译一次就可以"录入"所用的函数,但如果在不同的分文件中同时包含了同一个头文件,可能会导致一个文件被编译两次,这是完全没有必要的,对此可以采取以下两个措施之一,使每个头文件最多被编译一次:

①每个头文件的开头写:

#ifndef TEST_H

#define TEST_H

//头文件的内容

#endif //TEST_H

②每个头文件的开头写:

#pragma once

3、使用在其它源文件中定义的全局变量或函数

(1)要想使用在其它源文件中定义的全局变量或函数,++++使用extern关键字在需要使用该全局变量或函数的文件中对那个全局变量或函数进行声明++++即可,这样的话即使不包含头文件也能使用在其它源文件中定义的全局变量或函数。

(2)使用在其它源文件中定义的全局变量(无论是否包含对方的头文件都需要使用extern声明):

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//add.c文件:
int g_val = 2018;
//test.c文件:
int main()
{
extern int g_val;
	printf("%d\n", g_val);
	return 0;
}

(3)使用在其它源文件中定义的函数(如果包含了对方的头文件则无需使用extern声明):

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//add.c文件:
int Add(int x, int y)
{
	return x + y;
}
//test.c文件:
int main()
{
	extern int Add(int, int);
	printf("%d\n", Add(2, 3));
	return 0;
}

八、变量的生存周期

1、作用域

(1)通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

(2)在花括号内定义的变量称为局部变量(比如函数体内部定义的变量),其作用域是变量所在的局部范围,也就是其最近的外层花括号所限定的范围;不在花括号内定义的变量称为全局变量,其作用域是整个文件。

cpp 复制代码
#include<stdio.h>

int num = 20;   //定义(全局)变量【定义在{}-代码块之外的变量】
int age = 100;  //全局变量和静态变量在未初始化的时候会默认初始化为0

int main()
{
	//定义(局部)变量【定义在{}-代码块内部的变量】
	char ch = 'A';
	int age = 20;   //局部变量和全局变量的名字尽量不要重复,否则容易产生误会(如果重复,局部变量优先级更高)

	{
		int b = 10;
	}

	printf("%c\n", ch);
	printf("%d\n", age);
	printf("%d\n", num);
	//printf("%d\n", b);     //会报错

	return 0;
}

2、生存周期

(1)变量由编译程序在编译时给其分配存储空间(称为静态存储分配),并在程序执行过程中始终存在,这类变量的生存周期与程序的运行周期相同,当程序运行时,该变量的生存周期随即存在,程序运行结束,变量的生存周期随即终止。

(2)变量由程序在运行时自动给其分配存储空间(称为自动存储分配),这类变量为函数(或块)中定义的自动变量,它们在程序执行到该函数(或块)时被创建,在函数(或块)执行结束时释放所占用的空间(也就是被销毁)。

cpp 复制代码
#include<stdio.h>

int global = 2020;

int main()
{
	int num1 = 0;
	{
		int num2 = 0;
		printf("%d\n", num1);
		printf("%d\n", num2);
	}

	printf("%d\n", num1);
	//printf("%d\n", num2);   局部变量在哪定义就只能在哪使用
	printf("%d\n", global);   //全局变量在哪都能用
	
	extern int num3;      //在其它源文件中定义的全局变量经过声明后也可以使用
	printf("%d\n", num3);

	return 0;
}

3、关键字static

(1)被static修饰的局部变量称为静态局部变量,它的生命周期会延长至程序结束。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void test()
{
	//static修饰局部变量
	static int i = 0;
	i++;
	printf("%d ", i);   //static修饰局部变量改变了变量的生命周期
	                    //让静态局部变量出了作用域依然存在,直到程序结束生命周期才会结束
}
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		test();
	}
	return 0;
}

(2)被static修饰的全局变量称为静态全局变量,静态全局变量只能在本源文件内使用,不能在其它源文件内使用。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//add.c文件:
static int g_val = 2018;  //一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用
//test.c文件:
int main()
{
//extern int g_val;
	//printf("%d\n", g_val);
	return 0;
}

(3)被static修饰的函数称为静态函数,静态函数只能在本源文件内使用,不能在其它源文件内使用。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//add.c文件:
static int Add(int x, int y)
{
	return x + y;
}
//test.c文件:
int main()
{
	//extern int Add(int, int);
	//printf("%d\n", Add(2, 3));
	return 0;
}
相关推荐
Kisorge37 分钟前
【C语言】指针数组、数组指针、函数指针、指针函数、函数指针数组、回调函数
c语言·开发语言
爱吃西瓜的小菜鸡4 小时前
【C语言】判断回文
c语言·学习·算法
FeboReigns7 小时前
C++简明教程(文章要求学过一点C语言)(1)
c语言·开发语言·c++
FeboReigns7 小时前
C++简明教程(文章要求学过一点C语言)(2)
c语言·开发语言·c++
_小柏_8 小时前
C/C++基础知识复习(43)
c语言·开发语言·c++
yoyobravery8 小时前
c语言大一期末复习
c语言·开发语言·算法
落羽的落羽12 小时前
【落羽的落羽 C语言篇】自定义类型——结构体
c语言
Kisorge12 小时前
【C语言】代码BUG排查方式
c语言·开发语言·bug
yoyo勰13 小时前
sqlite3
c语言·sqlite
就爱学编程14 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(上)
c语言·数据结构