函数
完成特定任务的独立程序代码单元,作为组成大型程序的构件
语法规则定义函数的结构和使用方式
执行某些动作:printf() 把数据打印到屏幕上
找出一个值并返回:strlen()返回字符串长度
函数可以同时具备以上两种功能
函数可以使程序更模块化
正确定义函数
调用函数
建立函数间的通信:(传参,返回值)
每一个功能作为一个函数
单独实现每一个函数,设计和测试
函数名需要清楚表达函数用途和组织结构
函数可以作为根据传入信息、生成值、响应动作定义的黑盒,不需要了解内部代码
重点在程序的整体设计,而不是函数的实现细节
无参无返回值
#define NAME "GIGATHINK,INC."
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis,CA 94904"
#define WIDTH 40
void starbar(void);
/*函数原型,告诉编译器函数返回值和传参类型,这些信息称为签名
*返回值类型 函数名(参数类型);
*声明一个名为starbar 没有返回值没有参数的函数
*定义需要在调用之前,或者声明放在函数调用之前,最好统一在最前,声明也可以放在.h文件,然后include
*函数声明有分号
*函数返回值类型也称函数类型
*/
starbar();
/*函数调用
*找到函数定义并执行相关内容
*执行完成后返回主调函数继续执行下一行
*/
printf("%s \n", NAME);
printf("%s \n", ADDRESS);
printf("%s \n", PLACE);
starbar();
void starbar(void)
{ /*定义函数,明确指定函数需要做什么
* 函数类型 函数名 (函数参数列表) 函数定义没有分号
* { 变量声明 函数表达式语句 返回值}
* 如果需要把函数放在单独文件中,需要用到extern include define
* 没有返回值时void*/
int count;
/*局部变量,只属于函数,
*在其他地方定义使用count不会出现名称冲突*/
for (count = 1; count <= WIDTH; count++)
putchar('*');
putchar('\n');
}
带参无返回值
#include <string.h>/*为strlen ()提供原型*/
void show_n_char(char ch, int num);
/*告知编译器使用两个参数,
*一个char类型ch,一个int类型num,称为形参
*通过传参每次调用重新赋值,局部变量,函数私有
*函数声明每个变量都必须有类型
*void dibs(int x,y,z)错误的形式
*void dibs(x,y,z)
*int x,y,z;有效但不建议
*void show_n_char(ch,num)
*char ch;
*int num;有效但不建议,变量必须在左括号之前声明
*void show_n_char(char,int)函数声明可以省略变量名,函数定义不可以省略*/
// 居中显示
int spaces;
show_n_char('*', WIDTH);
/*用符号常量作为参数
*实参提供ch和num的值
*实参是具体值:'*'和40,赋给对应的形参(被调函数的变量)ch和num
*实参可以是常量,变量或者表达式,取值结果必须符合形参类型
*函数并不会影响主调函数的实参值
*调用时会声明形参变量并初始化为实际参数的值*/
putchar('\n');
show_n_char(SPACE, 12); /*用符号常量作为参数*/
printf("%s\n", NAME);
spaces = (WIDTH - strlen(ADDRESS)) / 2; /*计算要跳过多少个空格*/
show_n_char(SPACE, spaces); /*用一个变量作为参数*/
printf("%s\n", ADDRESS);
show_n_char(SPACE, (WIDTH - strlen(PLACE)) / 2);
printf("%s\n", PLACE); /*用一个表达式作为参数*/
show_n_char('*', WIDTH);
putchar('\n');
void show_n_char(char ch, int num)
{ // 打印num个ch字符
/*从黑盒视角只需要关注输入和输出,和功能
函数形参和局部变量和主调函数互不影响*/
int count;
for (count = 1; count <= num; count++)
putchar(ch);
}
带参带返回值
函数类型和返回值类型相同
没有返回值的函数类型为void
int imin(int, int);//一个返回int,需要2个int参数的imin函数,声明必须在调用之前,可以在函数内
// 设计用于测试函数的程序被称为驱动程序,在驱动程序中确认函数正确
int evil1, evil2;
printf("Enter a pair of integers (q to quit) : \n");
while (scanf("%d %d", &evil1, &evil2) == 2)
{
printf("The lesser of %d and %d is %d.\n", evil1, evil2, imin(evil1, evil2));
/*函数返回值可以赋值给变量
*可以作为表达式的一部分
*imin (evil1, evil2))本身表示返回值*/
printf("Enter a pair of integers (q to quit):\n");
}
printf("Bye.\n");
int imin(int n, int m)
{ // 返回值类型和声明的类型必须匹配,不匹配会导致类型转换,结果不正确
int min;//局部变量和形参外部不可见
if (n < m)
min = n;
else
min = m;
return min;
/*把信息从被调函数传回主调函数,min是int,返回min所以int imin(...);
*可以返回表达式的值
*return 可以终止函数并把控制返回给主调函数的下一条语句
*可以提前return,可以有多句return,在void函数中可以return;*/
}
函数原型
ANSI标准之前声明只需要声明函数类型、不需要声明参数
会导致的问题:
主调函数会把参数存储在栈,根据实参决定传递的类型
被调函数从栈中读取,根据形参类型读值
函数个数和类型不匹配时不能读取正确的参数
解决方案:
函数声明时指明函数类型,参数个数和参数类型,即函数原型
int imax(int,int);//可以只有参数类型,没有形参名
int imax(int a,int b);//函数声明的变量名和函数定义的变量名可以不一样,类型必须一样,函数内部使用函数定义的变量名
编译器会根据函数原型检查函数调用是否与函数原型匹配;
类型不匹配:编译器会报警告并把实参的类型转化为形参的类型
数量不匹配:编译器会报参数太少的错误信息
错误:无法编译
警告:允许编译
void print_name(); //不检查参数
void print_name(void); // 表明不接受任何参数
int printf(const char *,…); // 第一个参数是字符串,可能有其他未指定的参数 ,通过stdarg.h提供定义形参数量不固定的函数的标准方法
使用函数原型是为了让编译器第一次执行到该函数前就知道如何使用,把函数定义放在使用之前也是相同的效果
递归
函数调用自己
必须有终止递归的条件测试部分
优点:简单的解决方案
缺点:浪费资源,不方便阅读维护,低效率
eg:
unsigned long Fibonacci (unsigned n)
{//第一级调用需要调用2次,其中在第二级调用也会调用2次,到第3级调用总共存在1+2+4=7个变量,变量数量呈指数增长,占大量内存会导致程序崩溃
if (n > 2)
return Fibonacci (n-1) +Fibonacci(n-2) ;
else
return 1;
}
每个函数都是平等的
每个函数都可以调用其他函数或被其他函数调用
过程可以嵌套在另一个过程中,嵌套在不同过程中的过程之间不能调用
void up_and_down (int) ;
up_and_down (1);//第一级递归
void up_and_down(int n)
{/*每级递归的变量都属于本级递归私有,即第1级的n与第2级的n不同
*同一级递归的变量是同一个地址,第一级的n前后相同*/
printf("Level %d: n location %p\n", n, &n); // #1 进递归前打印
if (n < 4) // 必须包含测试能让递归停止调用的语句
up_and_down(n + 1);
/*第2级递归,3,4,每次函数调用都会返回一次
*当函数执行完后,控制权被传回上一级递归,执行下一句
*必须按顺序逐级返回递归
*在递归调用之前的语句,按被调函数的顺序执行,1234
*在递归调用之后的语句,按被调函数相反的顺序执行,4321
*每次递归都会从头开始执行,相当于循环*/
printf("LEVEL %d: n location %p\n", n, &n); // #2 递归退出后打印,%p转换说明打印地址
}
尾递归
递归调用置于函数末尾,正好在return之前,相当于循环
long fact (int n) ;
long rfact (int n) ;//12!接近5亿,超过12的阶乘需要用long long 或double
int num;
printf("This program calculates factorials.\n");
printf("Enter a value in the range 0-12 (q to quit) : \n");
while (scanf("%d", &num) == 1)
{
if (num < 0)
printf("No negative numbers, please.\n");
else if (num > 12)
printf("Keep input under 13.\n");
else
{
printf("loop : %d factorial = %ld\n", num, fact(num));
printf("recursion: %d factorial = %ld\n", num, rfact(num));
}
printf("Enter a value in the range 0-12 (q to quit) :\n");
}
printf("Bye.\n");
long fact(int n) // 使用循环的函数
{ // 能够循环使用的循环更快,内存占用更少
long ans; // 从1*n*(n-1)一直到2
for (ans = 1; n > 1; n--)
ans *= n;
return ans;
}
long rfact(int n) // 使用递归的函数
{ /*每次递归都会创建变量,使用更多内存,占栈空间
函数调用花费时间*/
long ans;
if (n > 0) // n==0时阶乘=1
ans = n * rfact(n - 1); // n!=n*((n-1)!)
else
ans = 1;
return ans;
}
倒序计算
递归适合处理倒序
/* binary.c --以二进制形式打印制整数*/
void to_binary (unsigned long n) ;
unsigned long number;
printf("Enter an integer (q to quit ) : \n");
while (scanf("%lu", &number) == 1)
{
printf("Binary equivalent : ");
to_binary(number);
putchar('\n');
printf("Enter an integer (q to quit) : \n");
}
printf("Done . \n");
void to_binary(unsigned long n)/*递归函数*/
{//取余可以获取二进制但倒序,递归输出
int r;
r = n % 2;//第一次取余获取末尾二进制位
if (n >= 2)
to_binary(n / 2);//二进制左移一位
putchar (r == 0 ? '0': '1');//取余算出来倒序,递归再倒序,则正序
return;
}
多源代码文件的程序
多个函数放在一个文件
Unix
编译两个文件:
cc file1.c file2.c //生成一个a.out文件
cc file1.c file2.o
make命令可以自动管理多文件程序
DOS 命令原理和cc类似,使用不同的名称,对象文件的扩展名为.obj
Linux
gcc file1.c file2.c
gcc file1.c file2.o
IDE
面向项目
项目描述特定程序使用的资源
资源包括源代码文件
创建项目,使用菜单命令,把源代码文件加入一个项目中
需要确保源代码文件要在项目列表
许多IDE不需要在项目列表列出头文件,include指令管理头文件
Xcode要在项目添加头文件
头文件
头文件:函数原型、预处理定义符号常量 #define
需要调用函数的文件include相应的头文件
命令行中gcc编译涉及到-c编译单个文件、-o链接多个文件
IDE 中涉及到把头文件和源文件加入到项目
vscode涉及到使用gcc编译、cmake管理项目
指针 函数改变主调参数的值
值为内存地址的变量
char类型变量的值为字符,指针变量的值为地址
声明指针必须指定 指针所指向变量 的类型
不同变量占用不同的存储空间,指针操作需要知道操作对象的大小,或者区分同样大小的long和float
变量属性:名称、值、地址
地址是变量在计算机内部的名称
变量把值作为基本量,把地址作为 &变量 获得的派生量
指针把地址作为基本量,把值作为 *指针 获得的派生量
scanf使用地址作为参数
如果主调函数不使用函数return返回的值,则必须通过地址才能修改主调函数的值
地址运算符 &
访问变量地址
&后跟一个变量名,给出该变量的地址,可以赋值给指针
间接运算符 *
获取地址上的值
*后跟一个指针名或地址,找出储存在地址中的值,解运算符
/* loccheck.c --查看变量被储存在何处*/
#include <stdio.h>
void mikado(int);/*函数原型*/
void interchange(int *u, int *v)
int pooh = 2, bah = 5; /* main()的局部变量*/
printf("In main (), pooh = %d and &pooh = %p\n", pooh, &pooh);
/* 一元运算符&:给出变量的存储地址 eg:&pool
* %p 输出地址的转换说明*/
printf("In main (),bah = %d and &bah = %p\n", bah, &bah);
mikado(pooh);//传参只传值
int * pi;
/* int表明指针指向int类型的对象
* * 表明变量是一个指针
* pi是指向int类型变量的指针,空格可有可无*/
char * pc;
float *pf,*pg;
pi=&pooh;//把pooh的地址赋值给pi,ptr是指向pooh的指针,&pooh表示pooh的地址
pi=&bah;//指针是可修改的左值,pi可以指向另外一个int类型的变量
pooh=*pi;//找出pi指向的地址的值,即bah的值
//更改主调函数的变量
int x=5,y=10;
printf("Originally x= %d and y = %d.\n",x,y);
interchange(&x,&y);//把地址发给函数
printf("Now x= %d and y = %d.\n");
void mikado(int bah)
{ // 必须是形参实参类型相同的变量,变量做参数适合计算或处理值,定义函数
int pooh = 10; /* mikado ()的局部变量*/
printf("In mikado (), pooh = %d and &pooh = %p\n", pooh, &pooh);
// 与main函数的pooh变量的地址不相同,分别是两个函数的局部变量,是独立的
printf("In mikado (), bah = %d and &bah = %p\n", bah, &bah);
// 会把main函数中实参(pooh的值)传给当前函数的形参(bah),只传递值,不会改变实参,可以防止原始变量被修改
}
void interchange(int *u, int *v)
{ // 指针作为参数,&x,必须是指向正确类型的指针,适合用于改变主调函数的变量
int temp; // 临时存放u的值,避免执行完u=v之后,u值丢失
temp = *u; // temp获得u指向的对象值,即x,u=&x *u=x
*u = *v; // 把v指向地址的值赋值给u指向的地址,即x=y
*v = temp; // 把v指向的地址的值更新为temp,即y=temp
/*interchange(x,y)
temp=u;//只会交换u,v值,并不会影响主调函数的x,y
u=v;
v=temp;*/
}