一、函数概述
1、函数
函数:一堆代码的集合,用一个标签(函数名)去描述它
目的:复用化
函数与数组:都是一段连续空间。
区别:函数具备3要素。
指针、数组2要素:大小、读取方式
int *p;
int a[100];
函数具备3要素:
1、函数名(标签、地址)
2、输入参数(输入):承上 / 启下(反向修改)
3、返回值(输出):启下
在定义函数时,必须将3要素告知编译器。
int fun(int, int, char) // 输入参数的顺序是有严格要求的
{ xxx }
2、如何用指针描述函数?
char *p; // 指针
char (*p)[10]; // 数组
int (*p)(int, int, char); // 函数
// 必须与函数的输入参数、输出参数是一样的,函数名替换为指针
3、定义函数与调用函数
int fun(int a,char b) // 定义函数
{
xxxx
}
int main()
{
fun(10,2); // 调用函数
}
// 类似:
char buf[100]; // 定义数组
buf[10]; // 调用
例1:
// 001.c
#include <stdio.h>
int main()
{
int (*myshow)(const char *, ...); // 这样定义的话,myshow读内存的方法和printf是一致的了
printf("hello world!\n");
myshow = printf; // 它们读内存的方法是一样的,因此不进行强制类型转换
myshow("=========\n");
return 0;
}
// man 3 printf
int printf(const char *format, ...);
// 注意:不一定非要用printf这样的函数,只要效果一样,可以自己定义!(这个在嵌入式开发中可能会很常用)
二、输入参数
1、调用者与被调者、实参与形参
函数调用时有2个对象:调用者和被调者。
调用者:
函数名(要传递的数据) //实参:要传递的真实的数据
被调者:函数的具体实现
函数的返回值 函数名(接收的数据) //形参:函数的具体实现/被调者中,接收数据的形式
{
xxx
}
传递过程:实参传递数据给形参
传递形式:拷贝(按位逐一赋值的过程)
例2:
// 002.c
#include <stdio.h>
void myswap(int buf) // 在myswap函数中预留了4个字节来等待接收数据
{
printf("buf is %x\n", buf);
}
int main()
{
int a = 20;
myswap(0x123); // 整型常量,占4B(32bit)
return 0;
}
例3:
// 003.c
#include <stdio.h>
void myswap(int buf) // 在myswap函数中预留了4个字节来等待接收数据
{
printf("buf is %x\n", buf);
}
int main()
{
int a = 20;
myswap(a); // 对变量来说,也是逐一赋值拷贝的过程
return 0;
}
例4:
// 004.c
#include <stdio.h>
void myswap(char buf) // 在myswap函数中预留了1个字节来等待接收数据
{
printf("buf is %x\n", buf);
}
int main()
{
int a = 20;
myswap(0x1234); // 发送了4个字节(32bit)的数据
return 0;
}
例5:
// 005.c
#include <stdio.h>
void myswap(int buf)
{
printf("buf is %x\n", buf);
}
int main()
{
int a = 20;
char *p = "hello world!";
printf("p is %x\n", p);
myswap(p);
return 0;
}
2、值传递与地址传递
3、连续空间的传递
为了解决内存空间而提出的解决方案。
场景:
在主函数中有大量连续空间,若是通过值传递,则在形参中也要分配一样大的空间,才能拷贝过去,这对空间的占用会非常大。
若只需要传递连续空间的首地址,就可以进行操作。
实际开发中使用的更多。
(1)数组
数组名 --- 标签
int abc[10];
// 实参
fun(abc) // 直接用地址传递,abc是一个标签/地址。
// 形参:
void fun(int *p)
void fun(int p[10]) // 这种写法也可以!
// 10只是给人看的,告诉人有10个int大小的空间;
// C语言编译器不会管这个10,C语言编译器只是把p当做一个地址。
数组的函数与函数之间的调用,使用地址传递。
(2)结构体
结构体变量
struct abc{int a; int b; int c;};
struct abc buf;
// 值传递
fun(buf); // 实参
void fun(struct abc a1) // 形参
// 地址传递
fun(&buf) // 实参
void fun(struct abc *a2) // 形参
结构体的函数与函数之间的调用,使用地址传递。
4、字符空间与非字符空间的操作
三、返回值
1、它是提供启下功能的其中一种表现形式。
(1)启下功能的2大表现形式:
- 返回值
- 输入参数的地址传递
(2)效果不同:
通过函数返回值返回一个值,该值与返回值类型一样;
通过输入参数“启下”,根据被调函数内部处理的不同,可能反向修改后的值很不同。
(3)通过返回值的“启下”方式,可以改成通过输入参数的地址传递“启下”的方式!
案例1:返回一个基本数据类型
// 通过返回值“启下”
int fun1(void);
int a = 0;
a = fun1(); // a会改变
// 通过输入参数的地址传递来“启下”
void fun2(int *p); // 想返回一个值,就把值的地址*写在输入参数处
int a = 0;
fun2(&a); // a会改变
案例2:想要返回2个值(“启下”2个值):
int[2] fun(void); // 错误!没有这种写法。
int fun(int *); // 返回值只能返回1个,另1个使用输入参数的地址传递来反向修改!
案例3:返回一个地址(指针)
// 通过返回值“启下”
int *fun1(void); // 返回值为1个地址(指针)
int *p;
p = fun1();
// 通过输入参数的地址传递来“启下”
void fun2(int **p); // 想返回一个指针*,就把指针*的地址**写在输入参数处
总结:
输入参数中若是int *p
,则反向修改一个值。
输入参数中若是int **p
,则反向修改一个地址。
2、基本语法
返回类型:基本数据类型、地址(指针)类型(只能返回连续空间类型,不能返回数组)。
返回值只能是1个。
调用者:
a = fun(); // 调用者用 = 去接收这个值
// 调用者如果不用 = 去接收被调者的返回值的话,被调者的返回值就消失掉了。 当然,可以通过汇编语言的一些方式去找回它。
被调者:
int fun()
{
return num; // 被调者返回一个值
}
被调者返回一个值,调用者接收了这个值,过程本质:拷贝。
例6:
// 006.c
#include <stdio.h>
int fun(void)
{
return 0x123; // 返回一个int值
}
int main()
{
int ret;
ret = fun();
printf("ret is %x\n", ret);
return 0;
}
例7:
// 007.c
#include <stdio.h>
char fun(void)
{
return 0x123; // 返回一个char值:低8位:23
}
int main()
{
int ret;
ret = fun();
printf("ret is %x\n", ret);
return 0;
}
例8:
// 008.c
#include <stdio.h>
int fun(void)
{
int a = 0x123;
int *p = &a;
return p; // 指针也是int,32位,也可以返回
}
int main()
{
int ret;
ret = fun();
printf("ret is %x\n", ret);
return 0;
}
3、函数返回地址(指针)类型
返回地址,返回的就是连续空间。
int *fun(); // 返回一个地址(指针)
需要先考虑一个问题:地址指向的合法性。
例9:
// 009.c
#include <stdio.h>
char *fun(void)
{
char buf[] = "hello world"; // buf是局部变量
return buf; // fun函数一旦return后,buf就消失了
}
int main()
{
char *p; // 申请一个一模一样的地址类型,去接收
p = fun();
printf("p is %s\n", p); // 看一下指针是啥
return 0;
}
作为函数的设计者(程序员),必须保证函数返回的地址所指向的空间是合法的,比如常量区、数据段区和堆区(不是局部变量一般就没问题)。
例10:
// 010.c
#include <stdio.h>
char *fun(void)
{
return "hello world"; // 双引号字符串在常量区,常量区生命周期不会随着函数的返回而消失,其地址在return后不会被改变
}
int main()
{
char *p;
p = fun();
printf("p is %s\n", p);
return 0;
}
作为函数的使用者,定义一个与函数返回类型一模一样的地址类型去接收就可以了。
int *fun(); // 函数声明
int *p = fun(); // 定义一个一模一样的类型去接收
4、函数返回类型内部实现
(1)返回基本数据类型
第一种:返回一个具体的基本数据类型值
基本数据类型 fun(void)
{
基本数据类型 ret; // 定义一个一模一样类型的变量
// 对ret做了一定的赋值操作等处理
return ret; // 将变量返回
}
int fun()
{
int ret;
ret = 5;
return ret; // 返回一个具体的int值:5
}
第二种:返回一个函数状态标识(成功与否的标识)
(2)返回地址(指针)类型
作为函数的设计者(程序员),必须保证函数返回的地址所指向的空间是合法的,比如常量区、数据段区和堆区(不是局部变量一般就没问题)。
例10:第一种:返回常量区(整个程序结束,其生命周期才结束)
这种方式在工程上意义不大。因为通过调用函数获取一个常量的话,还不如直接将该常量赋值呢。
例11:第二种:static局部变量,再返回静态化后的数据段区
// 011.c
#include <stdio.h>
char *fun(void)
{
static char buf[] = "hello world"; // 把局部变量静态化,该变量就到了数据段
return buf;
}
int main()
{
char *p; // 申请一个一模一样的地址类型,去接收
p = fun();
printf("p is %s\n", p); // 看一下指针是啥
return 0;
}
例12:第三种:返回堆区(malloc申请、free释放)
// 012.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char *fun(void)
{
char *s = (char *)malloc(100); // 1、申请空间(多大空间、强转类型)
// malloc申请空间,返回的是void *,因此使用时必须要强制转换成具体的类型
strcpy(s, "hello world"); // 2、申请完后初始化,初始化后去用
return s;
}
int main()
{
char *p;
p = fun();
printf("p is %s\n", p);
free(p); // 3、用完后释放掉
// free的是p,不是s。因为s在fun函数return时就消失了,而p存的是s中的值
return 0;
}
// man malloc:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
// malloc使用三部曲:
// 1、申请空间(多大空间、强转类型)
// 2、申请完后初始化,初始化后去用
// 3、用完后释放掉
小结:
- 输入参数中的
*
具有四义性(传递值、地址、字符空间、非字符空间),这种多义性可以通过不同的修饰符来区分。
- 返回值中的
*
具有二义性(静态数据段区、堆区)。