函数入门
在C语言中,函数意味着功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体。也因为如此,C语言被称为模块化语言。
对于函数的使用者,可以简单地将函数理解为一个黑箱,使用者只管按照规定给黑箱一些输入,就会得到一些输出,而不必理会黑箱内部的运行细节。
黑箱的输入和输出
日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如开关、音量、频道等,使用者不需要理会其内部电路,更不需要管电视机的工作原理,只需按照规定的接口操作接口得到结果。
对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节。对于一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的。对于所有的模块封装都应该做到高内聚低耦合。
高内聚低耦合:
“高内聚”和“低耦合”是软件设计中的两个概念,它们之间存在一定的交互关系。下面是对这两个概念的简要解释:
- 内聚(High Cohesion):代码模块的内聚是指模块内部功能紧密集成,没有或很少有外部依赖。这意味着,模块应该只负责一种功能,并且这个功能应该尽可能地close。如果模块功能过于宽泛,它可能会变得难以维护和测试。
- 耦合(Low Coupling):代码模块之间的耦合是指模块之间的依赖关系。耦合越高,表示两个模块之间越紧密,这可能会导致代码难以维护和扩展。理想情况下,应该尽可能地减少模块之间的耦合(相互依赖的成都),以提高代码的可维护性。
在实际的项目中,通常需要根据具体情况来权衡内聚和耦合。例如,如果一个模块的功能过于单一,可能需要增加其职责,以提高代码的内聚性。如果两个模块之间有大量的数据交换,可能需要考虑引入一个新的数据层,以降低它们之间的耦合。
总之,高内聚意味着代码模块应该尽量close(自闭),低耦合意味着代码模块之间的依赖应该尽量low。在实际的项目中,应该根据具体情况来平衡这两个指标,以提高代码的可维护性和可扩展性。
函数的定义
- 函数头:函数对外的公开接口
-
- 函数名称:命名规则与跟变量一致,一般取与函数实际功能相符合的、顾名思义的名称。
- 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数。
- 返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据。
- 函数体:函数功能的内部实现
- 语法说明:
返回类型 函数名称(参数1, 参数2, ……) ///函数头
{
函数体
}
函数声明:
概念:只是告知编译器函数的参数列表以及返回值类型,并不是把函数进行实现。
作用: 当我们把函数的实现与调用分开在不同的文件中出现时就需要在调用的文件写明函数的模型,可以给编译器提供检测函数调用过程中是否出现不匹配。
// 函数声明,用于告知编译器以下这些函数的模型
// 函数声明语句中 可以省略便令的名字,但是变量的类型不可以省略
int TieDan(int , int );
int ErGou (int , int );
int CaiGou(int , int );
int DaGou (int , int );
int XiGou (int , int );
- 写出下面所描述的各个函数的声明语句:
-
- 函数 f1 接受一个 int 类型的参数,没有返回值。
- void f1 (int) ;
- 函数 f2 接受两个 int 类型的参数,并返回一个 int 型数据。
- int f2 (int , int );
- 函数 f3 不接收任何参数,也不返回任何数据。
- void f3 (void) ;
- 函数 f1 接受一个 int 类型的参数,没有返回值。
函数调用的流程
函数调用时,进程的上下文会切换到被调函数,当被调函数执行完毕之后再切换回去。
局部变量与栈内存
- 局部变量概念:凡是被一对花括号包含的变量,称为局部变量
- 局部变量特点:
- 某一函数内部的局部变量,存储在该函数特定的栈内存中
- 局部变量只能在该函数内可见,在该函数外部不可见
- 当该函数退出后,局部变量所占的内存立即被系统回收,因此局部变量也称为临时变量
- 函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
- 栈内存特点:
- 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量
- 每当一个函数退出时,系统将自动回收其栈内存
- 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配的原则
int max(int x, int y) // 变量 x 和 y 存储在max()函数的栈中
{
int z; // 变量 z 存储在max()函数的栈中
z = x>y ? x : y;
return z; // 函数退出后,栈中的x、y 和 z 被系统回收
}
int main(void)
{
int a = 1; // 变量 a 存储在main()函数的栈中
int b = 2; // 变量 b 存储在main()函数的栈中
int m; // 变量 m 存储在main()函数的栈中,未赋值因此其值为随机值
m = max(a, b);
}
- 技术要点:
- 栈内存相对而言是比较小的(默认8M),不适合用来分配尺寸太大的变量。
- return 之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的(该函数退出后他的栈会被系统回收,如果返回他的地址则属于野指针)。
函数的参数传递
函数的形参与实参的关系
形参:形式上的参数,出现函数头中的参数列表称为形参。一下实例中 a ,b , c 都称为函数Max的形参。
int Max( int a , int b , int c)
{
}
实参:实际被传递的参数,出现在函数调用语句中。以下例子中 j , k ,l 称为实参
char j = 'A';
int k = 567 ;
int l = 123 ;
int max = Max( j , k , l );
实参用于初始化形参的值, 形参与实参所在的栈空间内存是互不干扰的。
因此从以下示例代码中可以看出在主函数和func函数中jkl的值是一样的,因为实参用于初始化形参,但是jkl的地址是不同的,因为在主函数中有三个变量 jkl , 在func也有三个独立的变量jkl他们互不干扰。
#include <stdio.h>
void func(int j , int k , int l)
{
printf(" [func] &j:%p : %d \n" , &j , j );
printf(" [func] &k:%p : %d \n" , &k , k );
printf(" [func] &l:%p : %d \n" , &l , l );
}
int main(int argc, char const *argv[])
{
int j = 123 ;
int k = 456 ;
int l = 345 ;
printf(" [main] &j:%p : %d \n" , &j , j );
printf(" [main] &k:%p : %d \n" , &k , k );
printf(" [main] &l:%p : %d \n" , &l , l );
func( j , k , l );
return 0;
}
参数的值传递
概念: 把参数中的数据值作为传递的实体。
int main(int argc, char const *argv[])
{
int j = 123 ;
int k = 456 ;
int l = 345 ;
func( j , k , l );
return 0;
}
参数的地址传递
概念: 把参数的地址作为传递的实体。
int main(int argc, char const *argv[])
{
int j = 123 ;
int k = 456 ;
int l = 345 ;
func( &j , &k , &l );
return 0;
}
数组作为形参出现
概念: 形参中定义了一个数组func( int arr[5] ) 实际上arr 他只是一个指针并非一个数组。
// int (*ptr)[5] = &arr
void func( int arr [ 5 ] , int * p , int (* ptr) [5])
{
printf("sizeof(arr):%ld\n" , sizeof(arr) );
printf("sizeof(p):%ld\n" , sizeof(p) );
// *ptr = arr
for (int i = 0; i < 5; i++)
{
printf("(*ptr)[%d]:%d\n" ,i , (*ptr)[i]);
}
}
int arr [5] ={1,2,3,4,5};
func( arr , arr , &arr );
函数的返回值
概念: 函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过return语句返回
-
- 没有返回值的函数为空类型,用void表示,一旦函数被定义被void,就不能再接收它的值了。
- return语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数都只能有且只有一个return语句被执行,所以函数最多只有一个返回值。
- 函数一旦执行return语句就立即返回,后面的语句都不会再执行,所以return语句还有强制结束函数的作用
一般的来说,函数是可以返回局部变量的,但是要注意几种情况。 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了。函数返回局部变量的值其实是对返回局部变量的值的拷贝然后返回。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错。但是如果返回的是局部变量的地址(指针)的话,程序运行后会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放了,这样指针指向的内容就是不可预料的内容,调用就会出错。准确的来说,函数不能通过返回指向栈内存的指针(注意这里指的是栈,返回指向堆内存的指针是可以的)。
int func1(void)
{
int a = 456 ;
return a ; // 返回的是a的值
}
int * func2(void)
{
int a = 456 ;
int * p = malloc(4);
static int b = 567 ; // 被static 修饰后的变量b 的内存分配与数据段中,
// 数据段中的所有数据都被称为静态数据
// return &a ; // [错误]返回的是a的地址
// return p ; // [正确]返回的是p的值 (堆内存地址)
return &b ; // [正确] 返回的虽然是局部变量b的地址,但是由于b是静态变量他的内存不会因为该函数退出而被释放
}
指针函数:
概念:这是一个函数,该函的返回值是一个指针。
void * func(void){}
int * func(void){}
double * func(void){}
函数指针函数 [拓展]:
typedef int (*Func_t) (int , float) ;
int func (int a , float f )
{
}
int * (TestFunc ( char c , int i )) (int , float)
Func_t TestFunc ( char c , int i )
{
...... ....
return func;
}
函数指针:
概念:他是一个指针, 该指针指向的类型是一个函数的类型
语法:
返回值类型 (*ptr) ( 参数列表 ) ;
#include <stdio.h>
typedef int int32_t ;
typedef int * int32_pt ;
typedef int (FuncPtr_t) (int , float) ;
typedef int (*FuncPtr_pt) (int , float) ;
int func(int a ,float f )
{
printf("[%s]\n" , __FUNCTION__);
}
int main(int argc, char const *argv[])
{
// 以下两个语句都是用于定义整型指针
int32_t * Ptr1_int ;
int32_pt Ptr2_int ;
// 定义了一个指针变量ptr_func ,它专门用于存储返回值为int 参数列表为 int ,float的函数类型的地址。
int (*ptr_func) (int , float) = func ;
FuncPtr_t * ptr = func ;
FuncPtr_pt ptr1 = func ;
// 通过函数的名字来调用函数,而函数的名字实际上是该函数的入口地址
func(123, 345.345);
// ptr_func = func
ptr_func(345 , 678.678);
ptr(123123, 345.4567);
return 0;
}
函数指针数组:
概念: 他是一个数组, 该数组中存储了指针,而指针所指向的类型是函数类型的。
语法:
函数的返回值类型 (* arr[3]) ( 函数的形参列表 )
示例:
#include <stdio.h>
typedef int (*P_Func_t) (int , int) ;
int TieDan(int a , int b)
{
printf("[%s]\n" , __FUNCTION__ );
return 0 ;
}
int ErGou(int a , int b)
{
printf("[%s]\n" , __FUNCTION__ );
return 0 ;
}
int CaiGou(int a , int b)
{
printf("[%s]\n" , __FUNCTION__ );
return 0 ;
}
int DaGou(int a , int b)
{
printf("[%s]\n" , __FUNCTION__ );
return 0 ;
}
int XiGou(int a , int b)
{
printf("[%s]\n" , __FUNCTION__ );
return 0 ;
}
int main(int argc, char const *argv[])
{
P_Func_t arr [5] = { TieDan , ErGou , CaiGou , DaGou , XiGou };
for (int i = 0; i < 5; i++)
{
// 通过数组来循环调用每一个函数
arr[i](123,456) ;
}
return 0;
}
结语:
在本篇博客中,我们一起探讨了C语言中的函数,包括函数的定义、声明和调用方式,以及如何利用函数提高代码的可读性和重用性。从了解函数的基本概念到学习如何构建自己的函数,希望你能掌握这一关键编程技能。
函数不仅是C语言编程的基石,更是构建组织良好、易于维护的程序的重要工具。通过将代码分解为小的、功能明确的部分,我们能够有效地管理复杂性,并确保代码的灵活性。