一、函数的介绍
① 函数是一段封装特定功能的代码,用于实现代码复用和模块化开发。
② 优势:避免重复编码、提高代码复用性、提供现成功能支持(通过库函数)、实现模块化开发。
③ 设计原则:
- 采用模块化思维:将复杂问题分解为多个小问题
- 遵循高内聚 低耦合原则
- 保持功能单一性
- 减少外部依赖
④ 使用规范:
- 使用前必须先定义(包括声明)
- 定义需在调用之前完成
二、函数的定义
函数类型 函数名(参数列表)
{
函数体;
return 返回值;
}
参数列表格式: (数据类型 形式参数1,数据类型 形式参数2,数据类型 形式参数3…)
函数类型:函数运行结果的类型 int void char double float,函数在调用完毕后,一般情况都是有结果值。 这个值就有数据的类型 。 如果函数的返回值设计为void ,那么函数调用(使用)后,没有结果,不返回任何数据。
注意:
① 在设计函数时,若未显式指定返回值类型,默认返回 int 类型。若无返回值需求,应声明为 void 类型。
② 当函数执行流程遇到 return 语句时,会立即返回到函数调用处。return 语句后的代码不会被执行。
③ 函数调用时,传递的参数必须满足:参数个数一致,且类型匹配(类型相同或可进行隐式类型转换)。
三、函数的调用
函数调用前,必须先定义后使用
① 函数调用方式
1)无参函数:函数名(); // fun1();
2)带参函数:函数名(arg1,arg2); // fun2(a,b); 其中a,b是实参(实际参数)
② 函数后面的圆括号(),不可以省略。
③ 传递参数要求: 参数个数一致, 数据类型也一致。
④ 主调函数是程序执行的入口点,在本例中 main 就是主调函数,负责发起函数调用。
⑤ 被调函数是被其他函数调用的功能模块,在这里 add 函数作为被调函数,由 main 函数调用执行具体功能。
函数的调用过程
#include <stdio.h>
void fun3()
{
printf("this is fun3…\\n");
return ;
}
void fun2()
{
fun3();
printf("this is fun2…\\n");
return ;
}
void fun1()
{
fun2();
printf("this is fun1…\\n");
return ;
}
int main()
{
fun1();
printf("this is main…\\n");
return 0;
}
求最大公约数、最小公倍数、闰年
#include <stdio.h>
int maxyue(int a,int b)
{
int min = a;
if(a>b)
{
min =b;
}
int i = 0 ;
for(i = min;i>= 1;i–)
{
if(0 == a%i && 0 == b%i)
{
break;
}
}
return i;
}
int minbei(int a,int b)
{
int max = a;
if(a<b)
{
max = b;
}
int mul = a*b;
int i = 0 ;
for(i= max;i<=mul ;i++)
{
if(0 == i%a && 0 == i%b)
{
break;
}
}
return i;
}
int is_leap_year(int year)
{
if((0 == year%4 && 0!=year%100) || 0 == year%400)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int num1=0,num2=0;
printf("input 2 num:");
scanf("%d%d",&num1,&num2);
int ret = maxyue(num1,num2);
printf("%d %d 最大公约%d\\n",num1,num2,ret);
ret = minbei(num1,num2);
printf("%d %d 最小共倍%d\\n",num1,num2,ret);
printf("input year:");
scanf("%d",&num1);
ret = is_leap_year(num1); // IsLeapYear
printf("year:%d , ",num1);
if(ret)
{
printf(" 是润年\\n");
}
else
{
printf(" 不是润年\\n");
}
return 0;
}
四、函数的声明
- 在实际编程过程中,特别是在多文件编程场景下,无法保证函数的定义一定出现在调用之前。这种情况下,编译器在编译时会遇到未声明的函数调用,从而产生警告。为避免此类问题,我们需要预先对函数进行声明。
- 如果被调函数在主调函数的下方定义需要再主调函数上方声明
- 如果被调函数在主调函数的上方定义,定义时已经完成函数的声明
int add(int a,int b) ; 函数的原型:函数头。 包含 函数名,参数列表(参数的个数,每个参数的数据类型) ,返回值的数据类型。
#include <stdio.h>
int div(int arg1,int arg2); // 函数的声明
//函数的定义 ,实现具体
int add()
{
int a =20;
int b =10;
int ret = a+b;
return ret; // 返回结果,返回函数调用的地方。
}
int add2(int arg1,int arg2) // arg1 ,arg2 形参,形式参数
{
int ret = arg1+arg2 ;
return ret;
}
int sub(int arg1,int arg2) // arg1 ,arg2 形参,形式参数
{
int ret = arg1-arg2 ;
return ret;
}
int mul(int arg1,int arg2) // arg1 ,arg2 形参,形式参数
{
int ret = arg1*arg2 ;
return ret;
}
int main()
{
int result = add(); // 函数的使用,() 表示调用函数, int result = ret; int result =30;
printf("result is %d\\n",result);
int num1=0,num2=0;
printf("input 2 num:");
scanf("%d%d",&num1,&num2);
//result = add2(int num1,int num2); //不需要加类型
result = add2(num1,num2); // 直接传递参数,不需要类型 num1,num2 都是实参,实际参数
printf(" add result2 is %d\\n",result);
result = sub(num1,num2); // 直接传递参数,不需要类型 num1,num2 都是实参,实际参数
printf("sub result2 is %d\\n",result);
result = mul(num1,num2); // 直接传递参数,不需要类型 num1,num2 都是实参,实际参数
printf("mul result2 is %d\\n",result);
result = div(num1,num2); // 直接传递参数,不需要类型 num1,num2 都是实参,实际参数
printf("div is %d\\n",result);
return 0;
}
int div(int arg1,int arg2) // arg1 ,arg2 形参,形式参数
{
int ret = arg1/arg2 ;
return ret;
}
五、变量的作用域、生命周期及存储类型
5.1 变量的作用域
① 作用域: 变量定义后,可以被访问的区域。
② 局部作用域: 局部变量
变量作用域在离定义该变量最近的大括号内。当发生函数调用后,作用域发生了变化,主调函数中定义的变量在被调函数内不可访问
③ 全局作用域: 全局变量
定义在所有函数之外的变量。可在同一工程的所有源文件(.c文件)中的任意位置直接访问
5.2 变量的生命周期
① 变量的生命周期是指从 开辟内存空间开始 到 变量的内存空间的回收结束 的这个时间段。
② 局部变量
1)定义在函数内部的变量
2)函数参数
注意:
- 函数内部定义的变量必须初始化,否则其值为随机数
- 内存分配从变量定义处开始,函数执行结束时回收内
③ 全局变量
1)定义在所有函数之外的变量
注意:
- 可以初始化,若未初始化则系统默认初始化为0
- 在a.out 正式运行起来前就要把内存空间开辟好,在程序结束后空间释放 。全局变量有全局生命周期。
④ 命名规则
1)同一作用域内不允许出现同名标识符(包括变量名、数组名、函数名)
2)当不同作用域存在同名标识符时,会发生变量隐藏现象
#include <stdio.h>
/*
int add()
{
return a+b; // 不能访问到主调的 a,b 变量
}
int main()
{
int a = 10; //局部作用域
int b =20; //局部作用域
int c = add();
return 0;
}
*/
int a = 10; // 全局变量, 整个工程中,任意位置都可以被访问
int b =20; // 全局变量, 整个工程中,任意位置都可以被访问
int add()
{
return a+b;
}
int main()
{
int a =1;
int b =3;
int c = add();
int d = a+b; // 这个地方使用的是 局部变量。全局变量被隐藏
printf("c is %d\\n",c);
return 0;
}
5.3 变量的存储类型
格式:存储类型 数据类型 变量名;
① auto int a; 用的相对少
auto 是自动存储类型的关键字,用于声明局部变量。该关键字通常可以省略。使用 auto 定义的变量会在声明时自动分配内存空间(位于栈区 stack),并在离开作用域时自动释放。这类变量包括函数内部定义的变量和函数参数。
② register int b; 用的相对少
register 用于建议编译器将变量存储在 CPU 寄存器中,以提升访问速度
#include <stdio.h>
int main()
{
int a; // auto int a;等价的
auto int b;
register int c;
}
③ extern int c;
extern 用于声明外部变量,其作用是让其他源文件能够访问该变量。
1.c
#include <stdio.h>
extern int a; // 声明,表示有这个变量,在其他的.c 中
extern int b;
extern void fun();
int main()
{
int c = a+b;
printf("a+b %d\\n",c);
fun();
}
2.c
int a =10;// 定义,开辟内存空间
int b =20;
void fun()
{
}
gcc 1.c 2.c
④ static int d;
static 是静态修饰符,可用于修饰变量或函数。用 static 修饰的变量会被分配在内存的数据区。
static 修饰全局变量的话,表示这个变量只能在本模块(本源文件中 依然全局变量)中使用。其他的源文件不可以使用。 static 的主要作用是限制作用域。
static 修饰函数的话,将函数作用域限制在本源文件内,static 的主要作用是限制作用域, 避免函数重名。
static 修改局部变量
如果局部变量前加 static ,变量的生命周期是整个程序运行的周期。
第一运行fun函数时, i 分配空间+初始化。如果不是第一次运行,这个变量就不会再分配空间和初始化。相当于 static int i = 0,这段没有 了。
#inlcude <stdio.h>
int fun()
{
static int i = 0 ;// 全局生命周期
i++;
return i;
}
int main()
{
int num = 0;
printf("input num");
scanf("%d",&num);
int j = 0 ;
int count = 0 ;
for(j=0;j<num;j++)
{
count = fun();
}
// i = 20; //error ,不能访问i。 i本身是存在,但是没在i的作用域范围内。
printf("count is %d\\n",count);
return 0;
}
5.4 代码运行后计算机内部内存空间分配
程序运行时的内存分区主要分为内核、栈区、堆区、数据区、文本区
① 文本区(Text):存储程序的机器指令(可执行代码,二进制形式)。该区域通常是只读的,防止程序意外修改自身指令。
② 数据区:
1)已初始化数据段(.data):存放显式赋值的全局变量、静态局部变量、静态全局变量(已初始化过的变量)
2)未初始化数据段(.bss段):存放未初始化或初始化为0的全局/静态变量,程序启动时由操作系统按位自动将其清零,不占用编译时磁盘空间
3)字符串字面量、const修饰的全局/局部常量
③ 堆区(Heap):由程序员手动申请和释放空间,使用最灵活。通过malloc/calloc/realloc手动申请的动态内存块
④ 栈区(Stack):存放函数的局部变量、函数形参、函数调用的返回地址、寄存器上下文信息。栈区空间上限默认为8M,由编译器自动管理分配和释放,遵循先进后出原则
⑤ 内核:进行文件、进程、内存、硬件管理
六、函数参数的传递

① 值传递 : 实参通过赋值运算符将参数数据内容传递给形参。形参是实参的副本,两者数据内容相同但占据不同的内存空间。由于形参和实参的内存相互独立,无法通过修改形参来改变实参的值。值传递方式只能读取实参的值,无法修改实参本身。
② 地址传递:实参传递的是变量的内存地址值。这种方式既可以读取实参的数据值,也能修改实参的内容。一维整形数组作为参数时 , 本质是地址传递。 一维字符数组作为参数时 , 本质是地址传递。
#include <stdio.h>
void fun(int arg)
{
arg++;
printf("fun arg:%d\\n",arg);
}
void swap(int a,int b)
{
int t = 0 ;
t = a;
a = b;
b =t;
printf("swap a:%d b:%d\\n",a,b);
}
int main()
{
int i =10;
fun(i);
printf("i:%d\\n",i);
int a = 20;
int b =1;
swap(a,b);
printf("main a:%d,b:%d\\n",a,b);
return 0;
}
一维整形数组作为参数时 , 本质是地址传递。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void fill_array(int a[],int size)// a的类型为int [];// 如果是整形数组的话,还需要数组的大小
{
// 如果实参是数组类型,在被调内部会降级为指针( 8 byte ,是数组在内存中的一个编号)
// int size = sizeof(a)/sizeof(a[0]);
int i = 0 ;
for(i=0;i<size;i++)
{
// 这个不是值传递, 所以可以修改数组。
a[i] = rand()%30;
}
// printf("sizeof is a %lu\\n",sizeof(a));
// printf("sizeof is a[0] %lu\\n",sizeof(a[0]));
}
void show_array(int a[],int size)
{
int i = 0 ;
for(i=0;i<size;i++)
{
//a[i] = rand()%30;
if(0 == i%5)
{
printf("\\n");
}
printf("%5d\\t",a[i]);
}
printf("\\n");
}
//逆序
int revert_array(int a[],int size)
{
int i = 0 ;
for(i=0;i<size/2;i++)
{
int t = a[i];
a[i] = a[size-1 -i];
a[size-1-i] = t;
}
return 0;
}
//冒泡排序
int bubble_sort(int a[],int size)
{
int i = 0 ;
int j = 0 ;
for(j=size-1;j>0;j–)
{
for(i=0;i<j;i++)
{
if(a[i]>a[i+1])
{
int t = a[i];
a[i] = a[i+1];
a[i+1] = t;
}
}
}
return 0;
}
//选择排序
int select_sort(int a[],int size)
{
int i = 0 ;
int j = 0 ;
for(i=0;i<size-1;i++)
{
for(j=i+1;j<size;j++)
{
if(a[i] >a[j])
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
}
return 0;
}
int main()
{
srand(time(NULL));
int array[10]={0};
int size = sizeof(array)/sizeof(array[0]);
fill_array(array,size);// int []
show_array(array,size);
//revert_array(array,size);
//bubble_sort(array,size);
select_sort(array,size);
show_array(array,size);
return 0;
}
一维字符数组作为参数时 , 本质是地址传递。
#include <stdio.h>
#include <string.h>
// 对于字符数组(字符串)而言, 约定以\\0 结尾。可以通过判断\\0 的位置,可以得出字符串的长度。
// 所以不需要传入 size
int mystrlen(char a[]) // 依然是地址传递,可以对数组对写操作
{
// sizeof(a) == 8byte
int len = 0 ;
while(1)
{
if('\\0'!=a[len])
{
len++;
}
else
{
break;
}
}
return len;
}
int show_string(char a[]/*,int size*/)
{
int len = mystrlen(a);
int i = 0 ;
for(i=0;i<len;i++)
{
putchar(a[i]);
}
printf("\\n");
}
int mystrcat(char dst[],char src[])
{
int len = mystrlen(dst);
int j = 0;
for(j=0;'\\0'!=src[j];j++,len++)
{
dst[len] = src[j];
}
dst[len]='\\0';
}
int main()
{
char str[100]={0}; // char []
char str2[100]={0};
printf("input string1:");
gets(str); // hello\\0
printf("input string2:");
gets(str2); // hello\\0
mystrcat(str,str2);
show_string(str);
return 0;
}
#include <stdio.h>
void intcpy(int dst[],int src[],int len)
{
int i = 0 ;
for(i=0;i<len;i++)
{
dst[i] = src[i];
}
return ;
}
void show_array(int a[],int size)
{
for(int i = 0 ;i<size;i++)
{
printf("%d\\n",a[i]);
}
}
int main()
{
int a[5]={1,2,3,4,5};
int b[10]={0};
intcpy(b,a,sizeof(a)/sizeof(a[0]));
show_array(b,10);
return 0;
}
七、函数的递归
① 在函数内部调用自身的行为称为递归,它是循环的一种特殊实现方式。与普通循环不同,递归会使用栈空间存储每次调用的上下文。
② 递归适用于解决回溯类问题或需要逆向思维处理的问题。
③ 使用递归时需注意:
1)必须设置合理的终止条件,否则会导致无限递归;
2)递归深度不宜过大,否则可能因内存消耗过多引发程序崩溃。
实现阶乘

#include <stdio.h>
// 递归需要结束条件
void fun_error()
{
fun_error(); // 程序会崩溃
}
// 阶乘
int fun(int n)
{
if(1 == n || 0==n ) //0! = 1,1! = 1
{
return 1;
}
else
{
return fun(n-1) * n;
}
}
// 斐波那契数列规律 1,1,2,3,5,8
int fib(int n)
{
if(n ==1 || n ==2)
{
return 1;
}
return fib(n-1)+fib(n-2);
}
int main()
{
int ret = fun(30);
printf(" 5 的阶乘 %d\\n",ret);
int i = 7;
int ret = fib(i);
printf("第%d项 值%d\\n",i,ret);
return 0;
}
汉诺塔


#include <stdio.h>
void han(int n,char a, char b,char c )
{
if(1 == n)
{
printf("%c->%c\\n",a,c);
}
else
{
// a->b a->c b->c
han(n-1,a,c,b);
printf("%c->%c\\n",a,c);
han(n-1,b,a,c);
}
}
int main()
{
han(3,'a','b','c');
return 0;
}
八、预处理指令
在c语言中,所有 # 开头的指令,被称为预处理指令。
gcc 编译步骤中的预处理,所有的预处理指令,都要在这步处理完
8.1 #include
① 头文件包含的内容包括:全局变量声明、函数声明、自定义构造类型声明、类型别名、宏定义。本质上,包含头文件就是将指定文件的内容复制到当前位置。
② 头文件包含的两种形式:
1)#include <stdio.h>:使用尖括号时,编译器只在系统头文件目录(如/usr/include)中查找。
2)#include "myheader.h":使用双引号时,编译器会首先在当前目录(a.out所在目录)查找,若未找到再到系统目录查找。
③ 注意事项:通常只包含.h头文件;不要包含.c源文件
main.c中负责主函数编写
功能代码.c中主要负责功能函数的编写
功能代码.h中主要负责声明功能代码.c中编写的功能函数
使用时,调用函数的文件只需加对应函数的.h头文件即可使用该函数

8.2 #define
宏定义指令:
宏的本质是代码替换,不会执行任何计算操作。
定义宏时,建议为参数和整体表达式添加圆括号。
|
带参宏没有参数类型、返回值、函数类型相关概念 |
函数有参数类型、函数类型、返回值相关 |
|
带参宏只是代码的替换,没有调用传参过 |
函数有调用、传参过程 |
|
带参宏是在程序编译时展开,代码体积增大 |
函数是在程序运行过程中加载,代码体积不会增大 |
|
带参宏没有传参过程,执行效率高 |
函数有传参过程,需要调用、传参产生资源开销 |
|
简单的代码想要封装模块,优先带参宏 |
需要严谨的代码逻辑,多行代码,考虑使用函数 |
#include <stdio.h>
// 不带参宏
#define PI 3.14 // PI 宏名 建议名字全部大写
#define M 50 // # 开头的指令,后面不要加分号;
// #define 宏名 宏值
// 宏值如果是一个表达式的话 ,最好都使用圆括号包起来
#define N ((M)+(M))
// 带参宏
#define MAX(x,y) ((x)>(y)? (x):(y))
#define ADD(a,b) ((a)+(b))
#define LENTH(r) (2*(PI)*(r))
// 需要写在一行, 如果一行写不下 使用续行符 \\
#define PRINT(a,b,c) do{\\
printf("a is %d\\n",a);\\
printf("b is %d\\n",b);\\
printf("c is %d\\n",c);}while(0) //执行一次
int main()
{
double r = 7.1234;
double area = r*r *PI;
printf("area is %f\\n",area);
int num = 0 ;
num= N*N; // 50+50 * 50+50 不加括号就会这样,
printf("num is %d\\n",num);
int a = 10;
int b = 20;
int ret = MAX(a,b);
printf("max is %d\\n",ret);
ret = ADD(3>4,5); // a+b 3>4+5
printf("add is %d\\n",ret);
PRINT(1,2,3);
return 0;
}
8.3 条件编译,宏开关
#if // 当满足条件时执行
#elif // 当不满足if条件但满足当前条件时执行
#else // 当前面所有条件都不满足时执行
#endif // 结束条件判断块
#ifdef // 当指定宏已定义时执行
#ifndef // 当指定宏未定义时执行
①
#if 0 #if 条件 3.0 >VERSION 判断条件的真假 ,决定#if 和#endif 之间的代码是否要预处理
#endif
②
#define DEBUG 1
#if DEBUG
调试相关的代码 ,在程序调试过程中,需要显示的。 如果是正式程序不需要显示
#endif
③
#if DEBUG //如果DEBUG 这个为真值 ,在#if 和#else 之间,就要被预处理
#else // 否则 #else 和#endif 要被预处理
#endif
④
#if DEBUG
预处理1
#elif 条件2
预处理2
#else
预处理3
#endif
⑤
// 一般出现在头文件,用于保证#ifndef 和#endif 之间的预处理,在a.out 只会预处理一次
如果__ADD_H_ 没有被定义过,则定义这个宏,预处理语句就会被执行。
如果__ADD_H_ 被定义过,则#ifndef 和#endif 之间的就不会被预处理。
#ifndef __ADD_H_
#define __ADD_H_
预处理
#endif
如果 这个宏被定义过__ADD_H_ ,则预处理 会被执行。
一般用于,代码中的一些选配功能。
#ifdef __ADD_H_
预处理
#endif

