一、C语言的函数
C语言程序的基本单位是函数,C语言是面向过程的一门编程语言,采用“自顶向下”的设计思想,采用的方案是把一个大问题拆解为很多个小问题,每个小问题单独进行解决,每个小问题可能需要多条语句才能解决,为了提高效率,所以就把可以解决问题的多条语句构成一个块语句,C语言中把这种块语句就称为函数。
C语言标准在发布的同时也随之发布了标准C库,标准C库中提供了已经封装好的函数接口,目的也是方便用户提高开发效率,不过预先封装好的函数属于
库函数。库函数根据发布者的不同,又分为
标准库和
第三方库,比如标准C库。
函数的本质就是一段可以重复使用的代码块,用户不想每次都复制这段代码,就可以把这段可以重复使用的代码封装为一个函数接口。

-
函数定义
思考:既然函数可以很大程度提高开发效率,应该如何去定义一个函数呢?有没有注意事项?

//函数有参数列表,则应该在函数名称的()中写清楚每个参数的类型,以及每个参数的名称
函数类型 函数名称(参数1类型 参数1名称,参数2类型 参数2名称.........)
{
}
//函数的参数是可以可选的,如果没有参数,则需要在函数名称的()中填写void即可
函数类型 函数名称(void)
{
}
注意:void在C语言标准中是一个关键字,含义具有空的意思,所以如果在参数列表中出现,则表示函数没有参数,同样,如果void是函数类型,则表示函数没有返回值。
注意:函数的类型其实指的是函数的返回值的类型,C语言标准中规定函数类型可以是void或者是对象类型(基本数据类型 int char long float... + 复杂数据类型 struct union.... +指针)
但是函数的返回值类型不允许是数组!!!!
注意:如果函数有返回值类型,则函数内部的需要返回的数据的类型必须要和函数的返回值类型一致,则需要在函数内部调用return语句实现。
int 函数名称(void)
{
return
3.14; //不允许,因为实际返回的数据的类型和定义函数的时候声明类型不一致
}
void 函数名称(void)
{
return 10; //不允许,因为void作为函数的类型,表示函数是没有返回值的!!!!!!!!!
}
int [10] 函数名称(void)
{
int buf[10] = {1,2,3,4,5};
return buf; //不允许,因为函数的返回值类型不允许是数组,但是可以选择传递地址!
}
int * 函数名称(void)
{
int buf[10] = {1,2,3,4,5};
return buf; //允许的,因为函数的返回值类型不允许是数组,但是可以选择传递地址!
}
注意:如果函数的类型是一个指针类型,则表示该函数可以返回一个地址,就把这种函数称为指针函数。

思考:既然C语言程序的基本单位是函数,能否在一个已经存在的函数中定义一个新的函数?
回答:不可以!C语言中函数都是独立的个体,不允许在一个函数内部定义新的函数,但是允许在一个函数内部调用其他的函数!设计函数应该做到
低耦合,高内聚!

-
函数调用
思考:如果用户打算封装一个函数实现某个功能,但是此时用户还没想好函数对应的块语句怎么写,只是把函数的名称和返回值类型以及参数列表写了出来,那能否在一个函数中进行调用?
回答:是可以调用的,但是遵循一个“先定义,后使用”原则,由于C语言中程序都是以函数为单位,并且程序的入口是主函数main(),所以应该把用户自定义的函数定义在main()函数之前,然后在main()函数中进行调用。

但是,有时用户可以在程序设计时是先在main()中调用了某个自定义函数,然后在main()函数后面定义了子函数,此时编译会报错,会提示:子函数未定义,为了避免此类问题,C语言中也是支持“先声明,后定义”。

练习:用户打算设计一个函数,把两个整数传递到函数内部,从而实现计算两个数中较大的数并把较大的整数作为结果输出到终端,请问如何设计程序?
文件注释
/*********************************************************************
* file name :demo.c
* author :2977587124@qq.com
* date :
* fuction :
* note :none
*
* copyright(c):2024 2977587124@qq.com All rights reserved.
*
**********************************************************************/
/************************************************************************
* function name:
* function:
* 函数参数:
* 输入参数: @a :
* 输入参数: @b :
* 返回结果:
* 注意事项:
* 函数作者:
* 创建日期:
* 修改历史:
* 函数版本: V1.0.0
* ***********************************************************************/

-
函数参数
思考:既然一个函数可以对数据进行处理,请问如何把要处理的数据传递给函数?应该如何操作?
回答:需要在设计函数的时候说清楚函数需要传递的参数的类型以及参数名称,都是在定义函数的时候通过函数的参数列表传递。
函数的参数列表是在后缀运算符()里面进行填写,()中的参数只是一个函数的助记符, 只是为了描述需要传递给函数的参数,所以函数的参数一般称为形式参数,简称为形参。
而
定义函数的时候函数参数列表中的形参是不占内存的,只是为了提醒用户参数的数量和类型!
用户在调用函数接口时,需要按照函数的参数列表来向函数提供对应的数据,数据的数量和数据的类型必须和形参一致。
注意:当一个函数被调用之后,函数的形参才会得到对应的内存,并且函数的形参的内存只会在函数内部生效,当函数调用完成后,则函数形参的内存会被系统自动释放。
注意:当用户调用一个函数时,如果函数有参数列表,则用户需要提供对应的数据给函数,而用户提供的数据的类型必须和函数参数类型一致,用户实际提供的数据被称为实际参数,简称为实参,而实参是必须存在的,实参的形式可以是表达式、常量、变量、地址........
-
单向传递

单向传递:只是把实参的值传递给函数作为参数,在函数内部对数值进行修改是不会影响外部实参的!
思考:一个函数的参数列表中的参数有对应的类型和名称,那另一个函数在调用该函数的时候,传递给函数的需要处理的数据(实参)的类型和名称是否需要和参数列表(形参)完全一致?
回答:实参的名称和函数形参的名称不需要一致,只需要确保实参的类型和函数形参的类型一致即可,如果类型不一致,则会出现数据精度异常。

思考:既然函数数据传递的过程是单向的,请问用户如何获取函数内部对数据的处理结果呢?
回答:可以通过函数的返回值获取函数的处理结果,函数中调用return语句可以把结果返回给被调用的位置。

-
双向传递
如果不打算调用return语句,则可以选择把实参的地址作为参数传递给函数内部,这样函数内部对地址中的数据进行修改,则函数外部的实参地址下的值也会变化,只不过此时函数参数类型应该是指针才可以。

-
生命周期
思考:程序中全局变量和局部变量在使用的时候是否有区分?有哪些使用细节需要注意??
回答:对于生命周期是指变量的生命周期,也就是变量从得到内存到释放内存的时间就是变量的生命周期,程序中变量如果按照存储单元的属性分类,可以分为变量和常量,也可以按照生命周期进行划分,可以分为全局变量和局部变量。
局部变量:在函数内部定义的变量或者在某个复合语句中定义的变量都称为局部变量!
全局变量:在所有的函数外部(在所有复合语句外部)定义的变量就被称为全局变量!

-
作用范围
作用范围指的是定义的变量的作用域,也就是变量的有效使用范围,对于全局变量而言,作用域是针对整个程序,所以程序中任何一个函数都有访问权限。对于局部变量而言。作用域只针对当前局部变量的复合语句有效。

注意:当全局变量的名称和局部变量名称相同时,则应该遵循
“就近原则”,也就是应该优先使用同一个作用域内的变量,如果该作用域中没有该变量,则可以扩大作用域。
-
数组传递
思考:通过学习已经知道可以把变量的值或者地址当做参数传递给函数进行处理,但是有时用户需要连续处理多个相同类型的数据,但是不想定义多个形参,请问如何解决该问题???
回答:可以选择把多个类型相同的数据构造为一个数组,然后把数组作为参数传递给函数,本质就是把数组的地址传递过去,此时分为两种方案:


思考:如果把数组的地址当做参数传递给函数,那用户如何知道实参数组的长度是多少???
回答:如果打算把数组作为参数传递给函数,则应该连同数组的长度一同作为参数传递给函数,而数组长度应该使用sizeof进行计算。

思考:既然可以把一维数组的地址作为参数传递,请问能否把多维数组传递给函数处理???
回答:一维数组和多维数组其实没有区别,因为都是把数组的首地址传递过去,只不过在函数内部访问数组元素的时候有一些区别。

思考:如果用户打算在一个函数中定义一个数组用来存储已经处理好的数据,但是C语言中规定不允许返回一个数组类型,当函数调用完成后函数内部的内存会被内核释放掉,也就意味着处理好的数据都会丢失,请问应该如何处理?
回答:由于C语言不支持函数的返回值类型是一个数组,但是可以选择把数组的地址作为返回值,此时函数的返回值类型就应该是指针才对。

思考:程序中数组的类型和函数的返回值类型是一致的,为什么编译程序会报警告,运行时也出错,是什么原因导致的?应该怎么解决?


出现段错误的原因:因为子函数中的变量buf的生命周期是在函数内部的,所以当函数调用完成后,则数组buf的内存会被系统自动释放,此时数组buf的地址对应的存储单元就没有访问权限了。
虽然得到了数组buf的地址,但是由于用户没有该地址的访问权限,所以访问时会出现段错误。
解决方案:可以选择把函数内部的数组定义为全局变量,此时程序中任意函数都可以访问,并且数组内存是在程序终止后才会被释放。
解决方案 :可以选择把函数内部的局部变量的生命周期延长,此时需要使用C语言中的存储类修饰符,就是C语言关键字之一的static关键字,static具有静态的含义,可以把局部变量的生命周期进行延长。
从内存角度分析:如果在函数内部定义一个局部变量,则系统会从内存分区中的
栈空间中分配一块内存给局部变量,栈空间是由系统自动管理,所以当函数调用结束时系统会自动释放该局部变量的内存。
如果函数中定义的局部变量使用static关键字进行修饰,则系统会从
全局数据区分配内存空间给该局部变量,全局数据区的生命周期是跟随程序的,不会因为函数结束而释放。

static除了可以修饰局部变量外,也可以用于修饰函数,如果一个函数在定义的时候使用static关键字进行修饰,则可以限制函数的作用域为文件内部有效。



-
内存分布
思考:请问什么是栈空间以及什么是全局数据区?两者之间有什么联系?如何区分变量是在栈空间还是全局数据区?

如果采用的是32bit的linux系统,则每个运行的程序都会得到4G大小的内存空间,只不过每个程序得到的4G大小的内存都是虚拟内存,而物理内存才只有4G,物理内存是真实存在的,而虚拟内存是通过映射得到的。
虚拟内存是由物理内存映射而来,所以都需要计算机中的重要部件:MMU 内存管理单元!
保留区
保留区也可以称为不可访问区域,用户是没有权限访问的,对于Linux系统而言,保留区的地址范围是0x0000_0000 ~ 0x0804_8000,所以保留区的大小是128M,一般用户定义的指针变量在初始化的时候就可以指向这块空间,由于这块空间任何程序都没有权限访问,所以可以确保指针不会被误用,所以可以防止野指针出现,宏定义NULL其实就是指向0x0000_0000

代码段
程序由数据以及指令组成,代码段存储的是编译器对程序编译之后生成的二进制指令,代码段分为两部分,分别是.text段和.init段。

.text段用于存储用户程序生成的指令,.init段用于存储系统初始化的指令,这两部分的属性是只读的,在程序运行之后代码段中的数据就不应该再被修改。在程序运行之前代码段的内存空间就已经被内核计算完成。


数据段
程序由数据以及指令组成,根据数据的生命周期和数据类型的不同,一般把数据存储在两部分,一个部分是栈空间,另一个部分是数据段。
数据根据数据类型(变量or常量,全局or局部)以及根据数据是否被初始化(已初始化or未初始化)把数据存储在三个不同的位置:.rodata段 .bss段 .data段。
- .rodata段:被称为只读常量区,程序中的常量(整型常量、字符串常量)都是存储在该区域,对于该区域的属性是只读的,当程序结束后该区域的内存会被释放。


-
.data段:用于存储程序中的已经被初始化的全局变量和已经被初始化的静态局部变量,另外注意初始化的值不能为0!

-
.bss段:用于存储程序中未被初始化的全局变量以及未被初始化的静态局部变量以及初始化为0的全局变量和初始化为0的静态局部变量。

堆空间
堆空间属于用户可以随意支配的内存,用户想要支配堆空间的内存的前提是需要向内核申请,可以通过库函数malloc()、calloc()申请堆内存,注意堆空间需要用户手动申请以及手动进行释放,通过库函数free()释放堆内存。堆内存属于匿名内存,只能通过指针访问!!!
❗
-
malloc()

利用malloc申请堆内存,则申请成功的堆内存是未被初始化的,所以用户应该对申请的堆内存进行初始化,同样,malloc只需要一个参数,该参数指的是需要申请的堆内存的大小,以字节为单位。
该函数的返回值是申请成功的堆内存的首地址,但是该地址的类型是void*,则用户应该对该地址进行强制转换,如果申请失败,则函数返回NULL,所以用户应该进行错误处理!

-
calloc()

calloc函数可以申请堆内存,calloc有两个参数,第一个参数是要申请的内存块的数量,第二个参数是内存块的大小,所以申请的内存的总大小 = 内存块数量 * 内存块大小,相当于是数组结构。
该函数的返回值是申请成功的堆内存的首地址,但是该地址的类型是void*,则用户应该对该地址进行强制转换,如果申请失败,则函数返回NULL,所以用户应该进行错误处理!

-
free()

注意:由于堆空间是由用户进行支配,所以用户申请成功之后,使用完成后需要及时释放堆空间,并且必须手动释放,并且必须只能释放一次,如果不释放,则会导致内存泄漏!
另外,当把申请的堆内存释放之后,则应该同样把指向堆内存首地址的指针的地址指向NULL!

栈空间
栈空间主要用于存储程序的命令行参数、局部变量、函数的参数值、函数的返回地址,当函数被调用期间,内核会分配对应大小的栈空间给函数使用,当函数调用完成则栈空间就会内核释放。
栈空间的内存存储是随机值,所以用户得到栈空间之后,应该把变量进行初始化,目的是防止变量中存储的值是不确定的。
对于栈空间的地址分配是
向下递增,所以栈空间使用的越多,则分配的内存地址越低,栈空间的数据遵循“
先进后出”原则,一般内核都会提供两个指针,一个指针指向栈顶,一个指针指向栈底,数据进入栈空间的动作就叫做入栈/压栈(PUSH),数据从栈空间出去的动作就叫做出栈/弹栈(POP)。
注意:Linux系统中栈空间的容量是有限的,如果超过容量,则会发生栈溢出,导致程序出现段错误。

思考:既然linux内存的栈空间大小是有限制的,请问栈空间的大小是多少?能否修改大小?
回答:linux系统的栈空间的大小默认是8M,是允许修改的,可以利用linux系统的ulimit命令来查询栈空间的大小以及修改栈空间的大小。注意:栈空间的大小的修改是临时性,是针对当前终端的,不是永久有效的!



.data段: 存初始化的。初始过的化的又分为全局的和静态局部的
.bss段:存未初始化的。全局变量以及静态局部变量以及初始化为0的全局变量以及静态局部变量

const常量关键字
思考:可以看到C语言标准中对于main函数的第二个参数的约束是 char * argv[],但是代码编辑器在定义main函数的时候为什么是 char const *argv[] ? 为什么多了一个const,这个词表示什么意思,有什么作用?


回答:const是C语言的关键字之一,其实是英文constant的缩写,具有常量的含义,const关键字在C语言标准中是类型限定符,一般用于修饰变量的,可以用于降低变量的访问权限,相当于把变量的属性变为只读,变量的存储单元只能读,不能写。
举例: int data = 10; data = 20; //变量是可读可写的 const int data; //只读变量
思考:C语言中明明支持定义常量,为什么还需要定义一个变量,再把变量的权限变为只读?
回答:程序的中的常量都是存储在只读常量区(.rodata段),但是由于这个段的属性是只读的,并且没有办法通过名称来访问常量,所以就可以定义变量,通过变量名称来间接访问在程序运行期间不需要修改的常量的值!

定义格式: const int data; // 不行,因为变量的存储单元已经变为只读 data = 10; 会报错
注意: 如果需要利用变量来存储一个常量,则需要在定义变量的时候利用const关键字修饰,并且一定要完成初始化!
思考:既然const可以修饰普通变量,那是否可以修饰指针变量呢?如果可以修饰指针变量,那请问 int *const p; 和 const int *p; 是否有区别?有什么区别?
回答:const关键字是可以修饰指针变量,但是 int *const p; 和 const int *p;是有很大区别的!
int *const p;
可以看到const离变量名称更近,const是修饰变量p的,而变量p是一个指针变量,变量p用于存储一个地址,但是变量p本身也可以得到存储单元,相当于降低了变量p的存储单元的属性,也就是变量p中存储的地址就不能发生变化,所以这个指针变量就称为
指针常量。
const int *p;
可以看到const关键字离指针变量p指向的地址下的数据类型更近,所以const是用于修饰指针变量p指向的地址的,所以也可以写成 int const *p;变量p的存储单元是没有受到影响的,而是变量p中存储的地址的权限降低为只读了,可以为变量p是美杜莎之眼,被称为
常量指针。

注意:只有通过指针变量p来间接访问地址下的值时,才会出现错误,因为变量p中存储的地址被变量p影响了,权限降低为只读了,但是只要不通过变量p来间接访问,则就不会受到变量p的影响。


递归思想的应用
思考:C语言程序的基本单位是函数,每个函数都可以解决一个问题,但是如果此时一个程序中有n个相同的问题出现,则就需要调用对应的函数n次,这样会导致程序冗杂,可读性较差,请问是否有更为简单的方案来解决对应的问题呢?
回答:可以使用递归函数解决,当然注意递归函数不是万能的,一般用于解决数学问题,递归函数指的是在一个函数内部反复调用自己的函数,递归函数具有递进和回归的过程,就相当于把一个类似的大问题拆分为很多类似的小问题,再把每个小问题的结果作为上一个问题的答案,一层一层进行解决。
注意:使用递归函数的时候要谨慎,
必须要提前写清楚终止条件,如果不写终止条件,就会变为死循环,相当于一直调用自己,由于每调用一次函数,内核都会提供一块栈空间,就会栈溢出,从而发生段错误,导致程序崩溃。
笔试题:用户打算设计程序,用户通过键盘输入一个正整数n,然后设计一个递归函数,用来求1 * 2* 3* 4 *....*n的结果,请问如何设计程序。



笔试题:用户设计一个程序,通过scanf函数输入一个字符串,在主函数中申请一块堆内存,把输入的字符串存储到该堆内存中,然后利用递归的方案实现字符串的字符逆序输出,请问如何设计程序? “hello” -> “olleh”


练习:申请一块堆内存,利用scanf函数实现通过键盘输入一个字符串,并存储到堆内存中,要求利用递归的方案实现计算字符串的实际长度,并把计算结果输出到终端,请问如何设计程序?


注意:想要计算字符串的实际长度,可以调用库函数的方式实现:strlen() 计算字符串实际长度




从参数数量的方面说:ma有一个参数,ca有两个参数
从内存有没有初始化,ma没有,ca有

从内存的角度说,局部变量在栈里,全局变量在数据段里

常量指针
常量指针
指针常量
常量指针常量
01:利用递归思想实现设计一个程序,完成斐波那契数列的函数设计,利用递归实现!
1 1 2 3 5 8 13.....
int func(int n)
{
//终止条件
if( n<=2 )
{
return 1;
}
else
{
return func(n-1)+func(n-2);
}
}
02:

#include <stdio.h>
// 函数用于找到数组中第二大的数
int secondLargest(int arr[], int n) {
if (n < 2) {
// 如果数组元素小于2个,无法找到第二大的数
return -1;
}
int first, second;
if (arr[0] > arr[1]) {
first = arr[0];
second = arr[1];
} else {
first = arr[1];
second = arr[0];
}
for (int i = 2; i < n; i++) {
if (arr[i] > first) {
second = first;
first = arr[i];
} else if (arr[i] > second && arr[i] < first) {
second = arr[i];
}
}
return second;
}
int main() {
int arr[] = {12, 35, 1, 10, 34, 1};
int n = sizeof(arr) / sizeof(arr[0]);
int second = secondLargest(arr, n);
if (second == -1) {
printf("数组元素小于2个,无法找到第二大的数\n");
} else {
printf("数组中第二大的数是: %d\n", second);
}
return 0;
}