C 语言 函数 (封装)
函数入门
在C语言中,函数意味着功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体。也因为如此,C语言被称为模块化语言。
对于函数的使用者,可以简单地将函数理解为一个黑箱,使用者只管按照规定给黑箱一些输入,就会得到一些输出,而不必理会黑箱内部的运行细节。
日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如开关、音量、频道等,使用者不需要理会其内部电路,更不需要管电视机的工作原理,只需按照规定的接口操作接口得到结果。
对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节。对于一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的。
理解:
- 对外部提供接口, (明确参数、返回值、函数的作用)
- 对内部封装,尽量做到一个函数是实现一个功能。
函数的定义
函数头:函数对外的公开接口
- 函数名称:命名规则与跟变量一致,一般取与函数实际功能相符合的、顾名思义的名称。(命名规则要一样)
- 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数。
- 返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据。(如果不确定返回值,可以先写 void )
char *strcpy (char *__restrict, const char *__restrict);
size_t strlen (const char *);
函数体:函数功能的内部实现
语法说明:
返回类型 函数名称(参数1, 参数2, ……)
{
函数体 // 代码实现
}
例子: 一般情况下我们把自己封装的函数写在main函数的上方,此时函数被创建并自动的对函数进行了声明
#include <stdio.h>
// 函数创建
int max(int a, int b)
{
return a > b ? a : b;
}
int main(int argc, char const *argv[])
{
int maxnum = max(99, 17);
printf("%d \n", maxnum);
return 0;
}
若我们将函数的定义写在main的下面,这时代码编译时会有警告,此时你需要在main函数的上方先对函数进行声明
#include <stdio.h>
// 函数声明
int max(int a, int b);
int main(int argc, char const *argv[])
{
int maxnum = max(99, 17);
printf("%d \n", maxnum);
return 0;
}
int max(int a, int b)
{
return a > b ? a : b;
}
注意:
- 函数没有返回值时,函数声明时返回类型设置为
void
; - 函数不需要接收参数时,参数列表也需要设置为
void
但是参数中的void
可以省略
#include <stdio.h>
void sayhello1(void){
printf("hello1! \n");
return; // return; 不带任何参数就是没有返回值, 没有返回值的return可以省略
}
void sayhello2(){
printf("hello2! \n");
}
int main(int argc, char const *argv[])
{
sayhello1();
sayhello2();
return 0;
}
实参与形参
概念:
- 函数调用中的参数,被称为实参,即 arguments
- 函数定义中的参数,被称为形参,即 parameters
实参与形参的关系: - 实参于形参的类型和个数必须一一对应。
- 形参的值由实参初始化。
- 形参与实参位于不同的内存区域,彼此独立。
#include <stdio.h>
int max(int num1, int num2, int num3)
{ // num1,num2,num3 都被成为形参, 形参只有声明没有初始化
return num1 > num2 ? (num1 > num3 ? num1 : num3) : (num2 > num3 ? num2 : num3);
}
int main(int argc, char const *argv[])
{
printf("%d \n", max(7, 8, 9)); // 7 8 9 就是实参, 形参是由实参初始化(赋值)
int a = 11, b = 12, c = 9;
max(a, b, c); // a b c 就是实参
printf("%d \n", max(a, b, c));
return 0;
}
练习: 实现返回反转字符串的函数.
#include <stdio.h>
#include <string.h>
// 函数注意点:
// 1. 函数的名字不能重复
// 2. 函数的形参和实参必须一致(数据类型、参数个数)
// 3. 函数的形参是由调用处的实参赋值的, 比如 void add(int num ) ---> int a = 10; add(a)
// 调用处将 a 的值 ,赋值给函数的 num 变量, ( 形参的值由实参初始化。 )
// 4.形参与实参位于不同的内存区域,彼此独立。(每一个函数都在栈中有一块自己独立的空间,不受其他函数的影响)
void reversalStr(char* str);
void reversalStr2(char* str, char* resStr);
char* reversalStr3(char* str); // 错误的
void add(int* num) {
// *num = *num + 1;
// ++ 的优先级比 ,* 号, 所以需要将 *num 括起来
// *num++;
(*num)++;
}
int main(int argc, const char* argv[]) {
// int num = 10;
// add(&num);
// printf("%d \n", num);
char str[20] = "hello";
// reversalStr(str);
// printf("%s \n", str);
// char resStr[20];
// reversalStr2(str, resStr);
// printf("str %s \n", str);
// printf("resStr %s \n", resStr);
char* resStr;
resStr = reversalStr3(str);
printf("%s \n", resStr);
// 指针指向的地址一旦被回收,就指向零地址,也就是NULL
/* char* testStr = str;
printf("%p \n", testStr);
testStr = NULL;
printf("%p \n", testStr);
printf("%s \n", testStr); */
return 0;
}
void reversalStr(char* str) {
int len = strlen(str);
for ( int i = 0; i < len / 2; i++ ) {
// char c = str[i];
// str[i] = str[len - i - 1];
// str[len - i - 1] = c;
char c = *(str + i);
*(str + i) = *(str + len - i - 1);
*(str + len - i - 1) = c;
}
}
/**
* 注意点:
* 1. 函数一旦执行完成,函数中内存都会回收
* 2.
*/
void reversalStr2(char* str, char* resStr) {
int len = strlen(str);
for ( int i = 0; i < len; i++ ) {
resStr[i] = str[len - i - 1];
}
}
/**
* 注意点:
* 1. 函数一旦执行完成,函数中内存都会回收
* 2. 当一个函数返回的是一个指针的时候,函数结束了,指针指向的内存会就被回收
* 3. 如果要返回指针,可以考虑两种方法:
* 1. 形参多定义一个返回的值的指针 ,指针在调用处声明,在函数中赋值 (推荐)
* 2. 使用 static 修饰指针,将指针指向的内存放在 .data 段中,不受栈的影响
*/
char* reversalStr3(char* str) {
static char resStr[20];
int len = strlen(str);
for ( int i = 0; i < len; i++ ) {
resStr[i] = str[len - i - 1];
}
return resStr;
}
总结:
-
函数形参和实参必须一致(数据类型 、数量)(C 语言不支持函数重载)
-
函数的形参是由调用处的实参赋值的, 比如 void add(int num ) ---> int a = 10; add(a) 调用处将 a 的值 ,赋值给函数的 num 变量, ( 形参的值由实参初始化。 )
-
形参与实参位于不同的内存区域,彼此独立。(每一个函数都在栈中有一块自己独立的空间,不受其他函数的影响)
-
如果需要修改调用处的数据,需要将对应的地址作为函数的参数
-
函数一旦执行完成,函数中内存都会回收
-
当一个函数返回的是一个指针的时候,函数结束了,指针指向的内存会就被回收
-
如果要返回指针,可以考虑两种方法:
- 形参多定义一个返回的值的指针 ,指针在调用处声明,在函数中赋值 (推荐)
- 使用 static 修饰指针,将指针指向的内存放在 .data 段中,不受栈的影响
- 使用堆内存,比如在 函数作用是使用
malloc
在函数中声明一个指针,这个指针指向的内存是由开发者决定是什么时候释放,不受
封装函数的好处
- 增加程序的可读性
- 降低代码的重复率,实现代码复用,以及模块化设计。
- 使我们的程序模块化,开发和维护都变得更简单。
函数名冲突问题
- 使用宏进行替换
- 如果是自定义的更换名字
函数的特点
- 函数调用的内存是在栈里面,那个栈空间随着函数的调用而诞生,随着函数的调用结束而释放
- 在函数内部定义的局部变量,随着函数调用的结束而释放
- 在函数调用的时候,不能调用其它函数的局部变量
- 我们在自己定义的函数内部改变外部函数的变量的值时,需要传递这个变量的地址
地址传递 和值传递
- 如果 实参 是
变量的名字
或者某个具体的值
,称之为值传递 ,普通数据类型 (深拷贝 --> 开辟了新的内存空) - 如果想要在某个函数中改变外部变量的值,需要进行地址传递 , 引用数据类型(地址值)(浅拷贝 ---> 利用 )
int add(int a) {
}
int num = 10;
add(num); // add(10)
/*
int add(int a = 10) {
}
*/
观察下面的代码,笔试面试必考题
上面代码正确,任何变量需要被其它函数内部修改,都需要将当前变量地址作为实参传递给函数,
函数根据实参类型声明对应类型的指针,基本数据类型就是普通指针.如果是指针类型那就声明声明二级指针.
总结:如果要修改外部函数的数据,必须要将其的地址作为参数进行传递(地址)
数组与指针的转换
int show(int (*arr)[5]){} // int show(int arr[][5]){}
int setData(int *a){} //int setData(int a[]){}
int setStr(char *p){} //int setStr(char p[]){}
int main()
{
int array[4][5];
show(array); // &array[0]
int a;
setData(&a);
int a1[10] = {0};
setData(a1); // setData(&a1[0]);
char str[10] = "hello";
setStr(str);
}
数组作为实参传递时,其实传递的只是数组首元素的地址不是整个数组 , (数组作为参数无需使用 & 符号)
形参接收数组变量时,无法使用sizeof计算数组大小的 , (传递过来的是一个指针)
int func(char p[100]) //还可以写成p[] //如果函数的形参是数组的写法,其实还是一个指针变量 相当于char *p
{
printf("p size:%lu\n",sizeof(p));
printf("%s\n",p);
}
int main(int argc,char*argv[])
{
char str[100] = "hello";
func(str);
printf("%s\n",str);
return 0;
}
函数指针与函数指针数组
函数指针就是一个指向函数的指针
- 函数指针,是一种特殊指针指向是函数的入口地址
- 将函数名赋值给函数指针时,编译器会自动将函数的入口地址赋值给函数指针
例:
int func(int a, int b){};
int (*p)(int, int);
p = func;
func(1,2)
p(3,4);
函数指针数组,每一项元素是函数指针类型的数组
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int minus(int a, int b)
{
return a + b;
}
int max(int a, int b)
{
return a > b ? a : b;
}
int main(int argc, char const *argv[])
{
int (*fun1)(int, int) = add;
int (*fun2)(int, int) = minus;
int (*fun3)(int, int) = max;
int (*arr[3])(int, int) = {fun1, fun2, fun3};
printf("%d \n", arr[2](7,8));
/* code */
return 0;
}
回调函数
函数A接受一个函数B作为参数,函数A内部调用函数B,这时函数B就被成为回调函数
当函数作为其他函数的参数就叫回调函数。
#include <stdio.h>
#include <math.h>
int is_zhishu(int num)
{
for (int i = 2; i <= sqrt(num); i++)
{
if (num % i == 0)
{
return 0;
}
}
return 1;
}
int max(int a, int b, int (*cb)(int num))
{
// return cb(a > b ? a : b);
int max = a > b ? a : b;
int res = cb(max);
return res;
}
int main(int argc, char const *argv[])
{
// int a = max(7, 11);
printf("%d", max(14, 27, is_zhishu));
return 0;
}
使用回调函数实现系统方法 qsort
数组排序方法
#include <stdio.h>
int asc_order(int a, int b)
{
// 升序
// 前者大于后者交换
if (a > b)
{
return 1;
}
return 0;
}
int desc_order(int a, int b)
{
// 降序
// 前者小于后者交换
if (a < b)
{
return 1;
}
return 0;
}
int *qsort(int *arr, int len, int (*order)(int a, int b))
{
// 我们这里用的是冒泡排序
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
if (order(arr[j], arr[j + 1]))
{
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main(int argc, char const *argv[])
{
int arr[10] = {1, 25, 23, 17, 22, 9, 8, 55, 42, 31};
qsort(arr, 10, asc_order);
for (int i = 0; i < 10; i++)
{
printf("%d \n", arr[i]);
}
return 0;
}
内联函数
- 概念:类似于
宏替换
,是由inline
修饰的函数.意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程(开栈的过程),加快程序运行速度。 - 什么时候用到内联函数:
- 函数的代码简短
- 函数被频繁地调用
- 注意
- 内联函数实际上是用空间代价(程序尺寸增大)来换取时间效率(不再需要切换函数 (开辟栈空间))
- 内联函数定义一般放在头文件中
- 内联函数的代码块尽量不超过5行
- 内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。
- 内联函数要在函数被调用之前声明。关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。而且内联函数在声明阶段也要加上inline关键字 , 声明定义都要使用
inline
修饰函数。
静态函数
概念: 由static修饰的函数,称之为静态函数
作用:防止一个工程中不同文件因函数名冲突,静态函数仅限于本文件有效
// [test.c]
#include<stdio.h>
//extern int func();
//静态函数只能在本文件中有效
static int static_func()
{
printf("test.c static_func\n");
}
int main()
{
func();
static_func();
return 0;
}
// [src.c]
#include <stdio.h>
//该函数不能在test.c中被调用
static int static_func()
{
printf("src.c static_func\n");
}
int func()
{
static_func();
printf("func\n");
}
总结:
- 静态修饰的东西只能在本文件中使用 (static )
- 作用:保护变量,保护函数。
递归函数(重点)
概念:
自己调用自己,称之为递归递归函数设计:
- 问题模型本身要符合递归模型(递推模型) 递归通常用于解决可以被分解为相似子问题的问题,这些问题的解决方案可以通过解决更小的相同问题来构建。
- 问题的解,当递归到一定层次时,答案是显而易见的,且能结束函数。
- 先明确函数要实现的功能与参数的关系,暂不管功能具体的实现。
- 呈现第n层与第n-1层的递推关系。
例子: 求任意数的阶乘
#include <stdio.h>
int jiecheng(int num)
{
if (num == 2)
{
return 2;
}
return num * jiecheng(num - 1); // 拆分问题 8的阶乘 = 8 * 7的阶乘
}
int main(int argc, char const *argv[])
{
printf("%d \n", jiecheng(6));
return 0;
}
递归函数注意的地方:
- 自己调用自己(递归函数), 自己调用别的函数(函数嵌套)
- 一定有一个终止条件
- 不要递归得太深,否则导致栈空间奔溃
作业:
- 使用递归实现求第n位 Fibonacci 数列对应的值.
#include <stdio.h> // 斐波那契, 当前数 = 前面两个数的和 int fibonacci(int n) { if ( n == 1 || n == 2 ) { return 1; } else { return (fibonacci(n - 1) + fibonacci(n - 2)); } // return (fibonacci(n - 1) + fibonacci(n - 2)); } int main(int argc, const char* argv[]) { int n = 10; int res = fibonacci(3); printf("%d \n", res); return 0; }
- 猴子吃桃子,有一只猴子他有一堆桃子,他每天吃桃子一半多一个,到第N天早上想再吃时,见只剩下一个桃子了.要求实现一个函数根据N的值求一开始这个猴子有几个桃子
#include <stdio.h> void eat(int day, int *m) { if (day == 1) { return; } // day3 1 , day 2 (1+1) * 2 , day1 ( 4 + 1 ) * *m = (*m + 1) * 2; eat(day - 1, m); } int main(int argc, const char *argv[]) { // 2. 猴子吃桃子,有一只猴子他有一堆桃子, // 他每天吃桃子一半多一个, 到第N天早上想再吃时, // 见只剩下一个桃子了.要求实现一个函数根据N的值求一开始这个猴子有几个桃子 , 3 int num = 1; eat(10, &num); printf("%d \n", num); return 0; }
- 递归实现n的k次方
#include <stdio.h> int power(int n, int k) { if ( k == 1 ) { return n; } return (n * power(n, k - 1)); } int main(int argc, const char* argv[]) { int res = power(8,3); printf("%d \n", res); return 0; }
写递归小技巧:
- 先明确结束递归的条件 (先写结束递归的判断)
- 确定调用递归的参数(每一次调用函数都应该,更接近结束递归)
- 如果需要在递归中获取每次函数计算结果
- 每次函数调用都返回值
- 使用全局变量或者静态变量
- 在调用处将值的地址往下传递。
C语言源程序的基本单位就是函数
c语言程序就是由函数构成的,一个c语言程序中仅有一个main函数,除了main函数外由若干个其他的函数组成程序某一部分功能.