avatar

zian

A text-focused Halo theme

  • Java
  • 面试
  • 首页
  • C语音
  • liunx
  • 数据结构与算法
  • 控制台
Home C 语言 函数 (封装)
文章

C 语言 函数 (封装)

Posted 2024-12-25 Updated 2025-01- 13
By Administrator
36~46 min read

函数入门

在C语言中,函数意味着功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体。也因为如此,C语言被称为模块化语言。

对于函数的使用者,可以简单地将函数理解为一个黑箱,使用者只管按照规定给黑箱一些输入,就会得到一些输出,而不必理会黑箱内部的运行细节。
wKgA3V-3ZP2ANZk3AABeWMwc2q0047.png

日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如开关、音量、频道等,使用者不需要理会其内部电路,更不需要管电视机的工作原理,只需按照规定的接口操作接口得到结果。

对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节。对于一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的。

理解:

  1. 对外部提供接口, (明确参数、返回值、函数的作用)
  2. 对内部封装,尽量做到一个函数是实现一个功能。

函数的定义

函数头:函数对外的公开接口

  • 函数名称:命名规则与跟变量一致,一般取与函数实际功能相符合的、顾名思义的名称。(命名规则要一样)
  • 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数。
  • 返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据。(如果不确定返回值,可以先写 void )
char 	*strcpy (char *__restrict, const char *__restrict);

size_t	 strlen (const char *);

函数体:函数功能的内部实现

语法说明:

返回类型 函数名称(参数1, 参数2, ……)
{
    函数体 // 代码实现
}

例子: 一般情况下我们把自己封装的函数写在main函数的上方,此时函数被创建并自动的对函数进行了声明

#include <stdio.h>

// 函数创建
int max(int a, int b)
{
    return a > b ? a : b;
}

int main(int argc, char const *argv[])
{
    int maxnum = max(99, 17);

    printf("%d \n", maxnum);

    return 0;
}

若我们将函数的定义写在main的下面,这时代码编译时会有警告,此时你需要在main函数的上方先对函数进行声明

#include <stdio.h>

// 函数声明
int max(int a, int b);

int main(int argc, char const *argv[])
{
    int maxnum = max(99, 17);

    printf("%d \n", maxnum);

    return 0;
}

int max(int a, int b)
{
    return a > b ? a : b;
}

注意:

  1. 函数没有返回值时,函数声明时返回类型设置为 void;
  2. 函数不需要接收参数时,参数列表也需要设置为 void但是参数中的 void可以省略
#include <stdio.h>

void sayhello1(void){
    printf("hello1! \n");
    return; // return; 不带任何参数就是没有返回值, 没有返回值的return可以省略
}

void sayhello2(){
    printf("hello2! \n");
}


int main(int argc, char const *argv[])
{
    sayhello1();
    sayhello2();

    return 0;
}

实参与形参

概念:

  • 函数调用中的参数,被称为实参,即 arguments
  • 函数定义中的参数,被称为形参,即 parameters
    实参与形参的关系:
  • 实参于形参的类型和个数必须一一对应。
  • 形参的值由实参初始化。
  • 形参与实参位于不同的内存区域,彼此独立。
#include <stdio.h>

int max(int num1, int num2, int num3)
{ // num1,num2,num3 都被成为形参, 形参只有声明没有初始化

    return num1 > num2 ? (num1 > num3 ? num1 : num3) : (num2 > num3 ? num2 : num3);
}

int main(int argc, char const *argv[])
{
    printf("%d \n", max(7, 8, 9)); // 7 8 9 就是实参, 形参是由实参初始化(赋值)

    int a = 11, b = 12, c = 9;

    max(a, b, c); // a b c 就是实参
    printf("%d \n", max(a, b, c));

    return 0;
}

练习: 实现返回反转字符串的函数.

#include <stdio.h>
#include <string.h>

// 函数注意点:
//  1. 函数的名字不能重复
//  2. 函数的形参和实参必须一致(数据类型、参数个数)
//  3. 函数的形参是由调用处的实参赋值的,  比如 void add(int num )  --->   int a = 10; add(a) 
//  调用处将 a 的值 ,赋值给函数的 num 变量,  ( 形参的值由实参初始化。  )
//  4.形参与实参位于不同的内存区域,彼此独立。(每一个函数都在栈中有一块自己独立的空间,不受其他函数的影响) 
void reversalStr(char* str);
void reversalStr2(char* str, char* resStr);

char* reversalStr3(char* str); // 错误的
void add(int* num) {
  // *num = *num + 1;
  // ++ 的优先级比 ,* 号, 所以需要将 *num 括起来
  // *num++; 
  (*num)++;
}

int main(int argc, const char* argv[]) {
  // int num = 10;
  // add(&num);
  // printf("%d \n", num);

  char str[20] = "hello";
  // reversalStr(str);
  // printf("%s \n", str);

  // char resStr[20];
  // reversalStr2(str, resStr);
  // printf("str %s \n", str);
  // printf("resStr %s \n", resStr);

  char* resStr;
  resStr = reversalStr3(str);
  printf("%s \n", resStr);
  
  // 指针指向的地址一旦被回收,就指向零地址,也就是NULL
  /* char* testStr = str;
  printf("%p \n", testStr);
  testStr = NULL;
  printf("%p \n", testStr);
  printf("%s \n", testStr); */


  return 0;
}



void reversalStr(char* str) {
  int len = strlen(str);
  for ( int i = 0; i < len / 2; i++ ) {
    // char c = str[i];
    // str[i] = str[len - i - 1];
    // str[len - i - 1] = c;
    char c = *(str + i);
    *(str + i) = *(str + len - i - 1);
    *(str + len - i - 1) = c;
  }

}

/**
 * 注意点:
 *  1. 函数一旦执行完成,函数中内存都会回收
 *  2.
 */
void reversalStr2(char* str, char* resStr) {
  int len = strlen(str);
  for ( int i = 0; i < len; i++ ) {
    resStr[i] = str[len - i - 1];
  }

}


/**
 * 注意点:
 *  1. 函数一旦执行完成,函数中内存都会回收
 *  2. 当一个函数返回的是一个指针的时候,函数结束了,指针指向的内存会就被回收
 *  3. 如果要返回指针,可以考虑两种方法:
 *     1. 形参多定义一个返回的值的指针 ,指针在调用处声明,在函数中赋值 (推荐)
 *     2. 使用 static 修饰指针,将指针指向的内存放在 .data 段中,不受栈的影响
 */
char* reversalStr3(char* str) {
  static char resStr[20];
  int len = strlen(str);
  for ( int i = 0; i < len; i++ ) {
    resStr[i] = str[len - i - 1];
  }
  return resStr;

}

总结:

  1. 函数形参和实参必须一致(数据类型 、数量)(C 语言不支持函数重载)

  2. 函数的形参是由调用处的实参赋值的, 比如 void add(int num ) ---> int a = 10; add(a) 调用处将 a 的值 ,赋值给函数的 num 变量, ( 形参的值由实参初始化。 )

  3. 形参与实参位于不同的内存区域,彼此独立。(每一个函数都在栈中有一块自己独立的空间,不受其他函数的影响)

  4. 如果需要修改调用处的数据,需要将对应的地址作为函数的参数

  5. 函数一旦执行完成,函数中内存都会回收

  6. 当一个函数返回的是一个指针的时候,函数结束了,指针指向的内存会就被回收

  7. 如果要返回指针,可以考虑两种方法:

    1. 形参多定义一个返回的值的指针 ,指针在调用处声明,在函数中赋值 (推荐)
    2. 使用 static 修饰指针,将指针指向的内存放在 .data 段中,不受栈的影响
    3. 使用堆内存,比如在 函数作用是使用 malloc 在函数中声明一个指针,这个指针指向的内存是由开发者决定是什么时候释放,不受

封装函数的好处

  • 增加程序的可读性
  • 降低代码的重复率,实现代码复用,以及模块化设计。
  • 使我们的程序模块化,开发和维护都变得更简单。

函数名冲突问题

  1. 使用宏进行替换
  2. 如果是自定义的更换名字

函数的特点

  • 函数调用的内存是在栈里面,那个栈空间随着函数的调用而诞生,随着函数的调用结束而释放
  • 在函数内部定义的局部变量,随着函数调用的结束而释放
  • 在函数调用的时候,不能调用其它函数的局部变量
  • 我们在自己定义的函数内部改变外部函数的变量的值时,需要传递这个变量的地址

地址传递 和值传递

  • 如果 实参 是 变量的名字 或者 某个具体的值 ,称之为值传递 ,普通数据类型 (深拷贝 --> 开辟了新的内存空)
  • 如果想要在某个函数中改变外部变量的值,需要进行地址传递 , 引用数据类型(地址值)(浅拷贝 ---> 利用 )
int add(int a) {

}

int num = 10;

add(num); // add(10)
/*
int add(int a = 10) {

}
*/

观察下面的代码,笔试面试必考题

必考题1.png

上面代码正确,任何变量需要被其它函数内部修改,都需要将当前变量地址作为实参传递给函数,
函数根据实参类型声明对应类型的指针,基本数据类型就是普通指针.如果是指针类型那就声明声明二级指针.

总结:如果要修改外部函数的数据,必须要将其的地址作为参数进行传递(地址)

指针地址传递.png

image-jlrr.png

数组与指针的转换

int show(int (*arr)[5]){} // int show(int arr[][5]){}
int setData(int *a){} //int setData(int a[]){}
int setStr(char *p){} //int setStr(char p[]){}
int main()
{
    int array[4][5];
    show(array); // &array[0]

  
    int a;
    setData(&a);
    int a1[10] = {0};
    setData(a1); // setData(&a1[0]);
    char str[10] = "hello";
    setStr(str);
}

数组作为实参传递时,其实传递的只是数组首元素的地址不是整个数组 , (数组作为参数无需使用 & 符号)
形参接收数组变量时,无法使用sizeof计算数组大小的 , (传递过来的是一个指针)

int func(char p[100]) //还可以写成p[]  //如果函数的形参是数组的写法,其实还是一个指针变量 相当于char *p
{
  printf("p size:%lu\n",sizeof(p));
  printf("%s\n",p);
}
int main(int argc,char*argv[])
{
  char str[100] = "hello";
  func(str);
  printf("%s\n",str);
  return 0;
}

函数指针与函数指针数组

函数指针就是一个指向函数的指针

  • 函数指针,是一种特殊指针指向是函数的入口地址
  • 将函数名赋值给函数指针时,编译器会自动将函数的入口地址赋值给函数指针

例:

int func(int a, int b){};

int (*p)(int, int);

p = func;
func(1,2)
p(3,4);

函数指针数组,每一项元素是函数指针类型的数组

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int minus(int a, int b)
{
    return a + b;
}

int max(int a, int b)
{
    return a > b ? a : b;
}

int main(int argc, char const *argv[])
{
    int (*fun1)(int, int) = add;
    int (*fun2)(int, int) = minus;
    int (*fun3)(int, int) = max;

    int (*arr[3])(int, int) = {fun1, fun2, fun3};

    printf("%d \n", arr[2](7,8));

    /* code */
    return 0;
}

回调函数

函数A接受一个函数B作为参数,函数A内部调用函数B,这时函数B就被成为回调函数

当函数作为其他函数的参数就叫回调函数。

#include <stdio.h>
#include <math.h>

int is_zhishu(int num)
{
    for (int i = 2; i <= sqrt(num); i++)
    {
        if (num % i == 0)
        {
            return 0;
        }
    }
    return 1;
}

int max(int a, int b, int (*cb)(int num))
{
    // return cb(a > b ? a : b);
    int max = a > b ? a : b;
    int res = cb(max);
    return res;
}

int main(int argc, char const *argv[])
{
    // int a = max(7, 11);
    printf("%d", max(14, 27, is_zhishu));

    return 0;
}

使用回调函数实现系统方法 qsort数组排序方法

#include <stdio.h>

int asc_order(int a, int b)
{
    // 升序
    // 前者大于后者交换
    if (a > b)
    {
        return 1;
    }
    return 0;
}

int desc_order(int a, int b)
{
    // 降序
    // 前者小于后者交换
    if (a < b)
    {
        return 1;
    }
    return 0;
}

int *qsort(int *arr, int len, int (*order)(int a, int b))
{
    // 我们这里用的是冒泡排序
    for (int i = 0; i < len - 1; i++)
    {
        for (int j = 0; j < len - 1 - i; j++)
        {
            if (order(arr[j], arr[j + 1]))
            { 
                // 交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main(int argc, char const *argv[])
{
    int arr[10] = {1, 25, 23, 17, 22, 9, 8, 55, 42, 31};

    qsort(arr, 10, asc_order);

    for (int i = 0; i < 10; i++)
    {
        printf("%d \n", arr[i]);
    }

    return 0;
}

内联函数

  1. 概念:类似于 宏替换,是由 inline修饰的函数.意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程(开栈的过程),加快程序运行速度。
  2. 什么时候用到内联函数:
  • 函数的代码简短
  • 函数被频繁地调用
  1. 注意
    • 内联函数实际上是用空间代价(程序尺寸增大)来换取时间效率(不再需要切换函数 (开辟栈空间))
    • 内联函数定义一般放在头文件中
    • 内联函数的代码块尽量不超过5行
    • 内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。
    • 内联函数要在函数被调用之前声明。关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。而且内联函数在声明阶段也要加上inline关键字 , 声明定义都要使用 inline 修饰函数。

静态函数

概念: 由static修饰的函数,称之为静态函数
作用:防止一个工程中不同文件因函数名冲突,静态函数仅限于本文件有效

// [test.c]
#include<stdio.h>
//extern int func();
//静态函数只能在本文件中有效
static int static_func()
{
    printf("test.c static_func\n");
}
int main()
{
    func();
    static_func();
    return 0;
}
// [src.c]
#include <stdio.h>
//该函数不能在test.c中被调用
static int static_func()
{
    printf("src.c static_func\n");
}
int func()
{
    static_func();
    printf("func\n");
}

总结:

  1. 静态修饰的东西只能在本文件中使用 (static )
  2. 作用:保护变量,保护函数。

递归函数(重点)

概念:
自己调用自己,称之为递归递归函数设计:

  • 问题模型本身要符合递归模型(递推模型) 递归通常用于解决可以被分解为相似子问题的问题,这些问题的解决方案可以通过解决更小的相同问题来构建。
  • 问题的解,当递归到一定层次时,答案是显而易见的,且能结束函数。
  • 先明确函数要实现的功能与参数的关系,暂不管功能具体的实现。
  • 呈现第n层与第n-1层的递推关系。

例子: 求任意数的阶乘

#include <stdio.h>

int jiecheng(int num)
{

    if (num == 2)
    {
        return 2;
    }

    return num * jiecheng(num - 1); // 拆分问题 8的阶乘 = 8 * 7的阶乘 
}

int main(int argc, char const *argv[])
{
    printf("%d \n", jiecheng(6));
    return 0;
}

递归函数注意的地方:

  1. 自己调用自己(递归函数), 自己调用别的函数(函数嵌套)
  2. 一定有一个终止条件
  3. 不要递归得太深,否则导致栈空间奔溃
    递归求阶乘案例.png

作业:

  1. 使用递归实现求第n位 Fibonacci 数列对应的值.
    #include <stdio.h>
    
    // 斐波那契,   当前数 =  前面两个数的和
    int fibonacci(int n) {
      if ( n == 1 || n == 2 ) {
        return 1;
      } else {
        return  (fibonacci(n - 1) + fibonacci(n - 2));
      }
      // return  (fibonacci(n - 1) + fibonacci(n - 2));
    }
    
    int main(int argc, const char* argv[]) {
      int n = 10;
      int res = fibonacci(3);
      printf("%d \n", res);
      return 0;
    }
    
    
  2. 猴子吃桃子,有一只猴子他有一堆桃子,他每天吃桃子一半多一个,到第N天早上想再吃时,见只剩下一个桃子了.要求实现一个函数根据N的值求一开始这个猴子有几个桃子
    #include <stdio.h>
    
    void eat(int day, int *m) {
        if (day == 1) {
            return;
        }
        // day3 1  , day 2 (1+1) * 2    , day1 ( 4 + 1 ) *
        *m = (*m + 1) * 2;
        eat(day - 1, m);
    }
    
    int main(int argc, const char *argv[]) {
        // 2. 猴子吃桃子,有一只猴子他有一堆桃子,
        // 他每天吃桃子一半多一个, 到第N天早上想再吃时,
        // 见只剩下一个桃子了.要求实现一个函数根据N的值求一开始这个猴子有几个桃子  , 3
        int num = 1;
        eat(10, &num);
        printf("%d \n", num);
        return 0;
    }
    
    
  3. 递归实现n的k次方
    #include <stdio.h>
    
    int power(int n, int k) {
      if ( k == 1 ) {
        return n;
      }
      return (n * power(n, k - 1));
    }
    
    
    int main(int argc, const char* argv[]) {
      int res = power(8,3);
      printf("%d \n", res);
      return 0;
    }
    
    

写递归小技巧:

  1. 先明确结束递归的条件 (先写结束递归的判断)
  2. 确定调用递归的参数(每一次调用函数都应该,更接近结束递归)
  3. 如果需要在递归中获取每次函数计算结果
    1. 每次函数调用都返回值
    2. 使用全局变量或者静态变量
    3. 在调用处将值的地址往下传递。

C语言源程序的基本单位就是函数

c语言程序就是由函数构成的,一个c语言程序中仅有一个main函数,除了main函数外由若干个其他的函数组成程序某一部分功能.

C语音
License:  CC BY 4.0
Share

Further Reading

Jan 7, 2025

C语言 其他

作用域 关键字 register 在C语言中,register 关键字是一种存储类说明符,用于建议编译器将变量存储在CPU寄存器中,而不是内存中。这样做可以提高访问变量的速度,因为寄存器的访问速度通常比内存快得多。然而,这个关键字只是一个提示,编译器可以选择忽略它。 使用register关键字的变量

Jan 6, 2025

C语言 结构体

结构体 问题的引入 多个相同数据类型的数据可以用数组表示,那么,如果多个不同数据类型的数据如何用一个集合表示呢?? 前面我们所介绍的普通数据类型实际上远远未能满足我们对实际应用中的要求,比如说一个学生,可能包含的属性有年龄、姓名、分数等等,不可能用一个基本数据类型(int 、float 、char)

Dec 28, 2024

C语言 内存

内存 什么是内存 软件在运行是,临时用来存储数据的 操作系统会将内存按照字节划分为 N 多个小格子 什么是内存地址 其实就是格子的编号 32 位操作系统:以 32 位的二进制表示 64 位操作系统:以 64 位的二进制表示 内存地址的作用 快速操作内存中存储的数据 C 语言中如何获取内存地址 &变量

OLDER

ubuntu系统

NEWER

02.类型转换

Recently Updated

  • 其他
  • Elasticsearch 面试
  • Spring 面试
  • RabbitMQ 面试
  • Redis 面试

Trending Tags

ruoyi docker java

Contents

©2025 zian. Some rights reserved.

Using the Halo theme Chirpy