函数(C语言)

一、函数概述

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这样的函数,只要效果一样,可以自己定义!(这个在嵌入式开发中可能会很常用)

《函数(C语言)》

二、输入参数

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;
}

《函数(C语言)》

例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;
}

《函数(C语言)》

例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;
}

《函数(C语言)》 接收端根据其能力,取其能接收的最多的字节

例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;
}

《函数(C语言)》 不管是传递值也好,地址也好,拷贝的过程都是逐一赋值

2、值传递与地址传递

见:《值传递与地址传递(C语言)》

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、字符空间与非字符空间的操作

见:《字符空间与非字符空间的操作(C语言)》

三、返回值

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,则反向修改一个地址。

字符空间与非字符空间的操作(C语言)

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;
}

《函数(C语言)》 结果是123

例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;
}

《函数(C语言)》 返回char值(低8位:23)

例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;
}

《函数(C语言)》 指针也是int,也可以返回

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;
}

《函数(C语言)》 警告:函数返回了一个局部变量。p指向了一个乱七八糟的东西,而不是hello world

作为函数的设计者(程序员),必须保证函数返回的地址所指向的空间是合法的,比如常量区、数据段区和堆区(不是局部变量一般就没问题)。

例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;
}

《函数(C语言)》 成功打印出hello world

作为函数的使用者,定义一个与函数返回类型一模一样的地址类型去接收就可以了。

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;
}

《函数(C语言)》 成功返回hello world

例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;
}

《函数(C语言)》 成功返回hello world

// man malloc:
#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);

// malloc使用三部曲:
// 1、申请空间(多大空间、强转类型)
// 2、申请完后初始化,初始化后去用
// 3、用完后释放掉

小结:

  • 输入参数中的*具有四义性(传递值、地址、字符空间、非字符空间),这种多义性可以通过不同的修饰符来区分。

《字符空间与非字符空间的操作(C语言)》

  • 返回值中的*具有二义性(静态数据段区、堆区)。
    原文作者:李行之
    原文地址: https://www.jianshu.com/p/e6c961b27cd0
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞