C语言 - 函数

前言:

  1. 每个C应用程序只有一个,且必须有一个main()主函数。无论主函数写在什么位置,C程序总是从main()函数开始执行。main()函数可以调用其它的子函数,子函数之间可以相互调用任意多次。
  2. 库函数:使用库函数,必须包含 #include 对应的头文件。

函数定义的格式:

函数1

  1. 返回值类型:
    1. 无返回值的类型:针对函数无返回值或明确不需返回值的情况,使用 void (即空类型)表示。
    2. 有返回值的类型:需要指明具体的类型。比如, int、float、char 等。如果省略,默认为int类型。
    3. 对于有返回值类型的,则需要在函数体内写上return 返回值,搭配使用,返回值需要与返回值类型一致。
  2. 函数名:
    1. 函数名,属于标识符。要遵循标识符的命名规则,同时要见名知意,以增强程序的可读性。
  3. 参数列表:
    1. 函数名后面的圆括号里面,可以声明参数的类型和参数名,表示完成函数体功能时需要外部提供的数据列表。
    2. 根据是否有参数,函数可以分为:
    3. 无参函数:在调用无参函数时,主调函数不向被调用函数传递数据。但函数名后的()不能省略。
      • 举例:abort();不接受任何形参。
    4. 有参函数:在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。
      • 函数参数为多个参数时,其间用逗号隔开。
      • 举例:add(int m,int n)
  4. 函数体:
    1. 函数体:是要写在大括号{}里面,是函数被调用后要执行的代码。
  5. return语句:
    1. return语句的作用:① 结束函数的执行 ②将函数运算的结果返回。
    2. return语句后面就不能再写其它代码了,否则会报错。(与break、continue情况类似)
    3. 下面分两种情况讨论:
      • 情况1:返回值类型不是void时,函数体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
      • 情况2:返回值类型是void时,函数体中可以没有return语句。如果要用return语句提前结束函数的执行,那么return后面不能跟返回值,直接写return; 就可以。

函数定义注意事项:

  1. C程序中的所有函数都是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。
  2. 同一个程序中函数不能重名,函数名是用来标识唯一一个函数。即在标准的 C 语言中,并不支持函数的重载。

形参、实参:

  • 形参(formal parameter):在定义函数时,函数名后面括号()中声明的变量称为形式参数,简称形参

  • 实参(actual parameter):在调用函数时,函数名后面括号()中使用的值/变量/表达式称为实际参数,简称实参

  • 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int add(int x, int y) {  //x,y是add()中的形参
    int z;
    z = x + y;
    return (z);
    }

    int main() {
    int c;
    int a = 10,b = 20;
    c = add(a, b); //此时将实参a,b赋值给add()的形参
    printf("sum is %d\n", c);
    return 0;
    }

说明:

1、实参与形参的类型应相同或赋值兼容,个数相等、一一对应。

2、形参只是一个形式,在调用之前并不分配内存。函数调用时,系统为形参分配内存单元,然后将主调函数中的实参传递给被调函数的形参。被调函数执行完毕,通过return语句返回结果,系统将形参的内存单元释放。

函数的调用:

  1. 调用函数时,需要传入实际的参数值。如果没有参数,只要在函数名后面加上圆括号就可以了。

  2. 无参函数调用:函数名();

  3. 有参函数调用:函数名(参数列表);

说明:

  • 1、调用时,参数个数必须与函数声明里的参数个数一致,参数过多或过少都会报错。
  • 2、函数间可以相互调用,但不能调用main函数,因为main函数是被操作系统调用的,作为程序的启动入口。反之,main() 函数可以调用其它函数。
  • 3、函数的参数和返回值类型,会根据需要进行自动类型转换。

main()函数:

  1. C 语言规定, main() 是程序的入口函数,即所有的程序一定要包含一个 main() 函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。
  2. main()函数可以调用其它函数,但其它函数不能反过来调用main()函数。main()函数也不能调用自己。
  3. 正常情况下,如果 main() 函数里面省略 return 0 这一行,编译器会自动加上,即 main() 的默认返回值为0。
  4. 在main()函数中C 语言约定:返回值 0 表示函数运行成功;返回其它非零整数值,表示运行失败,代码出了问题。系统根据 main() 的返回值,作为整个程序的返回值,确定程序是否运行成功。

exit()函数:

  1. exit() 函数用来终止**整个程序的运行**。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件 stdlib.h 里面。

  2. exit() 可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数,这两个常量也是定义在 stdlib.h 里面:

    1. EXIT_SUCCESS (相当于 0)表示程序运行成功,正常结束;
    2. EXIT_FAILURE (相当于 1)表示程序异常中止。
  3. 在main()函数结束时也会隐式地调用exit()函数,exit() 等价于使用 return 语句。其它函数使用 exit() ,就是终止整个程序的运行,没有其它作用。

  4. C 语言还提供了一个 atexit() 函数,用来登记 exit() 执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件 stdlib.h

函数的声明:

  1. 函数必须先声明,后使用。由于程序总是先运行 main() 函数,所以其它函数都必须在main()之前声明。

  2. 主调函数和被调函数在同一个 .c 文件当中时:

    1. 被调函数在上,主调函数在下(不需要声明)

      1
      2
      3
      4
      5
      6
      void fun(){
      printf("123");
      }
      int main(){
      fun();//不需要声明
      }
    2. 被调函数在下,主调函数在上(需要声明)

      1
      2
      3
      4
      5
      6
      int main(){
      fun();//需要声明
      }
      void fun(){
      printf("123");
      }
      1. **直接声明法:**将被调用的函数的第一行拷贝过去加分号

        1
        2
        3
        4
        5
        6
        7
        void fun()//声明fun()函数
        int main(){
        fun();
        }
        void fun(){
        printf("123");
        }
      2. **间接声明法:**将函数的声明放在头文件中(文件命名为.h),.c 程序包含头文件即可

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        //这是头文件a.h编写的代码,也就是在头文件里面声明函数
        extern void fun();

        #include <a.h>//包含头文件
        int main(){
        fun();
        }
        void fun(){
        printf("123");
        }
    3. 主调函数和被调函数不在同一个 .c 文件当中时:(必须声明)

      1. **直接声明法:**将被调用的函数的第一行拷贝过去,后面加分号,前面加 extern

        1. 主调函数:假设main.c文件

          1
          2
          3
          4
          5
          6
          extern int Max(int x,int y);
          int main(){
          int num=Max(3,7);
          printf("%d",num);
          return 0;
          }
        2. 被调函数:假设max.c文件

          1
          2
          3
          4
          5
          6
          int Max(int x,int y){
          if(x>y)
          return x;
          else
          return y;
          }
      2. **间接声明法:**将函数的声明放在头文件中(文件命名为.h),.c 程序包含头文件即可

        1. 头文件:han.h

          1
          extern int Max(int x,int y);
        2. 主调函数:han.c

          1
          2
          3
          4
          5
          6
          #include "han.h"
          int main(){
          int num=Max(3,7);
          printf("%d",num);
          return 0;
          }
        3. **注意:**如在Max.c中也用到了其他函数,则主调函数han.c也需要声明相应的函数(或包含相应的头文件),方便的办法是把用到的函数全声明在自己的头文件当中,只样在用的时候只需要包含自己的头文件即可

参数的传递:

  1. 值传递:

    1. 概念:是将实参的值传递给被调用的形参
    2. 值传递,是单向传递,修改形参的值,不会影响到实参
    3. **默认传递值的类型:**基本数据类型 (整型类型、浮点类型,字符类型)、结构体、共用体、枚举类型。
    4. 体会:形参、实参各占独立的存储空间。函数在被调用时,给形参动态分配临时存储空间,函数返回释放。
  2. 地址传递:

    1. 概念:实参将地址传递给形参,二者地址值相同

    2. 地址传递,是双向传递,修改形参则会影响到实参

    3. **默认传递地址的类型:**指针、数组

    4. 指针作为形参:

      1. 当函数的形参类型是指针类型时,使用该函数时,需要传递指针,或者地址,或者数组给该形参。函数内以指针的方式操作变量(*指针)。
      2. 因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部变量的值。
    5. 数组作为形参:

      1. 数组名本身就代表该数组首地址,传数组的本质就是传地址。因此,把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。

      说明:

      void f(int x[], int n){

      ​ …………;

      }

      **参数1:**数组的定义,只需写出中括号即可,不需要限定数组长度

      **参数2:**数组作为参数的习惯操作。将函数中要操作的数组元素的长度传入(并不是指数组的总长度)。

      由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入。

      • 对于二维数组做形参:

      int maxValue(int array[][4], int n)

      那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。也就是说,在定义二维数组时,必须指定列数(即一行中包含几个元素) ,由于形参数组与实参数组类型相同,所以它们是由具有相同长度的一维数组所组成的。所以必须指定第2维(列数)。

      **注意:**在传递时列数必须一致,而行数可以不一样

      1
      2
      3
      4
      void f(int x[5][5]){}
      //1、int a[3][5]; 可以传给f函数吗? 可以!
      //2、int b[10][3];可以传给f函数吗? 不可以!
      而行数要保证实参不能比形参的大,不然会丢失数据
    6. 指针和数组作形参小结:

      函数2

    7. 字符串(字符指针)作为形参:

      • 字符串(或字符指针)作为函数的参数,与数组或指针作为函数参数没有本质的区别,传递的都是地址值,所不同的仅是指针指向对象的类型不同而已。
    8. 指针数组作为形参:

      • 指针数组的元素是指针变量,用指针数组能够实现一组字符串的处理。

函数的高级应用:

  1. 递归函数:

    1. 递归函数 调用:函数自己调用自己的现象就称为递归。
    2. 直接递归:函数自身调用自己。
    3. 间接递归:可以理解为A()函数调用B()函数,B()函数调用C()函数,C()函数调用A()函数。
    4. 解释说明:
      1. 递归函数包含了一种隐式的循环
      2. 递归函数会重复执行某段代码,但这种重复执行无须循环控制。
      3. 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出
      4. C语言支持函数的递归调用。
    5. 总结:
      1. 使用递归函数大大简化了算法的编写。
      2. 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
      3. 在要求高性能的情况下尽量避免使用递归,递归调用既花时间耗内存。考虑使用循环迭代
  2. 可变函数:

    1. 有些函数的参数数量是不确定的,此时可以使用C语言提供的可变参数函数(Variadic Functions)。

    2. 声明可变参数函数的时候,使用省略号 … 表示可变数量的参数。

      1
      int printf(int form, ...);
    3. 这里的 … 表示可以传递任意数量的参数,但是它们都需要与form的格式化标志相匹配。

    4. 注意:… 符号必须放在参数序列的结尾,否则会报错

    5. 可变参数的使用:

      1. 为了使用可变参数,你需要引入<stdarg.h>头文件。

      2. 在函数中,需要声明一个va_list类型的变量来存储可变参数。它必须在操作可变参数时,首先使用。

      3. 使用va_start函数来初始化va_list类型的变量。它接受两个参数,参数1是可变参数对象,参数2是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。

      4. 使用va_arg函数来逐个获取可变参数的值。每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,参数1是可变参数对象,参数2是当前可变参数的类型。

      5. 使用va_end函数来结束可变参数的处理。

      6. 举例:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        #include <stdio.h>
        #include <stdarg.h>

        // 可变参数函数,计算多个整数的平均值
        //count是表示需要输入几个,如count=4,也就是他后面有4个数
        double average(int count, ...) {
        va_list args; // 声明一个va_list变量,存储可变参数
        va_start(args, count); // 初始化va_list,指向可变参数的位置
        double sum = 0;
        for (int i = 0; i < count; i++) {
        int num = va_arg(args, int); // 逐个获取整数参数
        sum += num;
        }
        va_end(args); // 结束可变参数的处理
        return sum / count;
        }

        int main() {
        // 调用可变参数函数输入5,表示后面跟5个数
        double avg = average(5, 10, 20, 30, 40, 50);
        printf("Average: %lf\n", avg);
        return 0;
        }
      7. 总结:

        • 可变参数函数,在编写各种工具函数和格式化输出函数时非常有用。但要小心确保传递的参数数量和类型与函数的预期相匹配,以避免运行时错误。
  3. 指针函数:(返回值是指针)

    1. C语言允许函数的返回值是一个指针(地址),这样的函数称为指针函数

    2. 指针函数是指 返回值是指针的函数。它的本质是一个函数,只不过返回值是一个指针。

    3. 定义格式:

      1
      2
      3
      返回值类型 *函数名(形参列表) {
      函数体

      • 函数体中的 return 命令须返回一个地址。
    4. 特点:

      1. 返回值是一个指针。
      2. 通常用于动态内存分配、返回数组或字符串等场景。
  4. 函数指针:(指向函数的指针)

    1. 函数指针是指向函数的指针。它的本质是一个指针,只不过这个指针指向的是函数。

    2. C 语言规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说, print 函数和 &print 是一回事。

    3. 定义形式:

      1. 返回类型 (*指针变量名)(参数列表);
      2. 如:int (*max)(参数列表);
      3. 其中,参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称。
      4. int (* max)(int i);或int (* max)(int);
    4. 例子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 普通函数
      int add(int a, int b) {
      return a + b;
      }
      int subtract(int a, int b) {
      return a - b;
      }
      int main() {
      // 定义函数指针
      int (*funcPtr)(int, int);
      // 让函数指针指向 add 函数
      funcPtr = &add;//funcPtr = add;是一样的
      printf("Add: %d\n", funcPtr(10, 5)); // 输出:15
      // 让函数指针指向 subtract 函数
      funcPtr = subtract;
      printf("Subtract: %d\n", funcPtr(10, 5)); // 输出:5return 0;
      }
      • 变量 funcPtr 是一个函数指针,它可以指向函数返回值类型为 int 且有2个整型参数的函数
      • funcPtr 指向函数 add() 的地址。函数 add() 的地址可以用 & 获得,也可以用add函数名获得
      • (*funcPtr)的小括号一定不能省略
      • 有了函数指针,通过它也可以调用函数:
      • (*funcPtr)(10,5)等同于add(10,5)以及funcPtr(10,5)是一样的
    5. 特点:

      1. 函数指针是一个变量,存储的是函数的地址。
      2. 可以通过函数指针调用不同的函数,常用于回调函数、函数表等场景。
    6. 注意:

      1. 对指向函数的指针变量不能进行算术运算,如p+n,p++,p–等运算是无意义的。
      2. 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
  5. 回调函数:

    1. 指向函数a的指针变量(函数指针)的一个重要用途是把函数a的入口地址作为参数传递到其它函数b中,此时的函数b就称为回调函数。在此基础上,我们就可以在回调函数b中使用实参函数a。

    2. 原理解释:有一个函数(假设函数名为fun),它有两个形参(x1和x2),定义x1和x2为指向函数的指针变量。在调用函数fun时,实参为两个函数名f1和f2,给形参传递的是函数f1和f2的入口地址。这样在函数fun中就可以调用f1和f2函数了。

      函数3

      **例子:**有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      #include <stdio.h>

      int fun(int x, int y, int (*p)(int, int)); //fun函数声明
      int max(int, int); //max函数声明
      int min(int, int); //min函数声明
      int add(int, int); //add函数声明

      int main() {
      int a = 10, b = 20, n;
      printf("please choose 1,2 or 3:");
      scanf("%d", &n); //输入1,2或3之一
      switch(n){
      case 1:
      fun(a, b, max); //输入1时调用max函数
      break;
      case 2:
      fun(a, b, min); //输入2时调用min函数
      break;
      case 3:
      fun(a, b, add); //输入3时调用add函数
      break;
      }

      return 0;
      }

      int fun(int x, int y, int (*p)(int, int)){ //定义fun函数
      int result;
      result = (*p)(x, y);
      printf("%d\n", result); //输出结果
      }

      int max(int x, int y){ //定义max函数
      int z;
      if (x > y)
      z = x;
      else
      z = y;

      printf("max=");
      return z; //返回值是两数中的大者
      }

      int min(int x, int y){ //定义min函数

      int z;
      if(x < y)
      z = x;
      else
      z = y;

      printf("min=");
      return z; //返回值是两数中的小者
      }

      int add(int x, int y){ //定义add函数
      int z;
      z = x + y;
      printf("sum=");
      return z; //返回值是两数之和
      }
  6. 函数说明符:

    1. 内部函数:(静态函数)static关键字修饰

      1. 如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数称为内部函数。此时,内部函数需要使用 static 修饰。

      2. 定义形式:static 返回值类型 函数名(形参){}

      3. 举例:

        1
        2
        3
        static int f(int a,int b){

        }
        • 说明:f()函数只能被本文件中的函数调用,在其他文件中不能调用此函数。
        • 但此处 static 的含义并不是指存储方式,而是指对函数的调用范围只局限于本文件。因此在不同的源文件中定义同名的内部函数互不影响。
    2. 外部函数:

      1. 外部函数在整个源程序中都有效,只要定义函数时,在前面加上extern关键字即可。

      2. C语言规定,可以省略extern,默认函数前面加上 extern 关键字

      3. 定义形式:extern 返回值类型 函数名(形参表){}

      4. 举例:

        1
        2
        3
        extern int f(int a,int b){

        }
        • 如果定义为外部函数,则它不仅可被定义它的源文件调用,而且可以被其他文件中的函数调用,即其作用范围不只局限于其源文件,而是整个程序的所有文件。
        • 在一个源文件的函数中调用其他源文件中定义的外部函数时,通常使用extern说明被调函数为外部函数。

字符串常用函数:

  1. 这些函数都在#include <string.h>头文件当中

  2. **strlen()**函数:

    • 作用:返回字符串的长度,不包括末尾的空字符 ‘\0’ 。

      1
      2
      char str[10] = "China";
      printf("%d\n", strlen(str));//5
  3. **sizeof:**这是个关键字运算符,并不是函数

    • 作用:计算数据类型或变量所占用的内存大小(以字节为单位)包含“\0”

      1
      2
      3
      4
      int a;
      sizeof(a);//4
      char a[10]="huan";
      sizeof(a);//输出10,返回的是整个数组的大小
  4. **strcpy()**函数:

    1. 格式:strcpy(字符数组1, 字符数组2)

    2. 作用:字符串的复制,直接将字符数组2的字符串复制到字符数组1中。

      1
      2
      3
      4
      5
      6
      char str1[10]="111111234", str2[] = "China";
      strcpy(str1, str2);
      printf("%s\n",str1); //China 遇到\0停止输出
      for(int i = 0;i < 10;i++){
      printf("%c",str1[i]); //China\0234\0
      }

      **注意:**复制时将字符串China和其后的′\0′一起复制到字符数组str1中,取代字符数组str1中前面的字符,未被取代的字符保持原有内容。

  5. **strncpy()**函数:

    1. 格式:strncpy(字符串1, 字符串2, n);
    2. 作用:将字符串2中前面n个字符复制到字符数组1中去。
  6. **strcat()**函数:

    1. 格式:strcat(字符数组1, 字符数组2)

    2. 作用:把两个字符数组中的字符串连接起来,把字符串2接到字符串1的后面,结果放在字符数组1中,函数调用后得到一个函数值——字符数组1的地址。

    3. 注意:连接前两个字符串的后面都有′\0′,连接时将字符串1后面的′\0′取消,只在新字符串最后保留′\0′。

      1
      2
      3
      4
      char str1[30] = {"People′s of "};
      char str2[] = {"China"};
      printf("%s\n", strcat(str1, str2)); //People′s of China
      printf("%s\n", str1); //People′s of China
  7. **strncat()**函数:

    1. 格式:strncat(str1, str2, n);
    2. 作用:将字符串2中前面n个字符连接到字符数组1中去
  8. **strcmp()**函数:

    1. 格式:strcmp(字符串1, 字符串2)

    2. 作用:比较字符串1和字符串2。

    3. **字符串比较的规则是:**将两个字符串自左至右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到′\0′为止。

      (1) 如全部字符相同,则认为两个字符串相等。返回值为0

      (2) 若出现不相同的字符,如果返回值为正数,则字符串1大;反之,返回值为负数,则字符串2大。

      1
      2
      3
      4
      5
      6
      char *str1 = "abxy";
      char *str2 = "abmn";
      printf("%d\n",strcmp(str1, str2)); // 1

      int compare1 = strcmp("China", "Korea");
      printf("%d\n",compare1); // -1
  9. strlwr()/strupr()

    1. strlwr(字符串):将字符串中大写字母换成小写字母。

    2. strupr(字符串):将字符串中小写字母换成大写字母。

      1
      2
      3
      4
      5
      6
      char str[] = "HelloWorld";
      strlwr(str);
      puts(str); //helloworld

      strupr(str);
      puts(str); //HELLOWORLD

基本数据类型和字符串的转换函数:

  1. 基本数据类型 -> 字符串:

    1. **sprintf()**函数可以将其他数据类型转换成字符串类型。此函数声明在stdio.h头文件中。

    2. sprintf()和平时我们常用的printf()函数的功能相似,只是sprintf()函数输出到字符串中,而printf()函数输出到屏幕上。

      1
      2
      3
      4
      5
      char str[20];
      int a = 111
      char c = 'a';
      sprintf(str, "%d%c", a, c);//把a c 加入到字符数组str中
      printf("str=%s\n", str); //111a
  2. 字符串 -> 基本数据类型:

    1. 调用头文件 <stdlib.h> 的函数atoi()atof() 即可。

      1
      2
      3
      4
      5
      #include <stdlib.h>
      char str1[10] = "123456";
      char str2[4] = "111";
      int i = atoi(str1);//123456
      int j = atof(str1);//111

日期和时间相关函数

  1. 头文件是 <time.h>
  2. 举例说明:
  • 返回一个值,即格林尼治时间1970年1月1日00:00:00到当前时刻的时长,时长单位是秒。
1
time_t time(time_t *t)
  • 获取当前时间,返回一个表示当地时间的字符串(当地时间是基于参数timer的)。
1
char *ctime(const time_t *timer)
  • 计算time1和time2之间相差的秒数(time1-time2)
1
double difftime(time_t time1, time_t time2)
  • 例子举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    #include <stdio.h>
    #include <time.h> //该头文件中,声明日期和时间相关的函数

    // 运行test函数,看看执行花费时间
    void test() {
    int i = 0;
    int sum = 0;
    int j = 0;
    for (i = 0; i < 10000000; i++) {
    sum = 0;
    for (j = 0; j < 100; j++) {
    sum += j;
    }
    }
    }

    int main() {
    printf("程序启动...\n");

    time_t start_t;
    //先得到执行test前的时间
    time(&start_t); //获取当前时间

    test(); //执行test

    time_t end_t;
    //再得到执行test后的时间
    time(&end_t); //获取当前时间

    double diff_t; //存放时间差
    diff_t = difftime(end_t, start_t); //时间差,按秒 ent_t - start_t


    //然后得到两个时间差就是耗用的时间
    printf("%d\n",start_t); //1697026306
    printf("%d\n",end_t); //1697026308
    printf("执行test()函数 耗用了%.2f 秒\n", diff_t); //执行test()函数 耗用了2.00 秒

    //获取时间对应的字符串的表示
    char * startTimeStr = ctime(&start_t);
    printf("%s\n",startTimeStr); //Wed Oct 11 20:11:48 2023

    return 0;
    }

数学运算相关函数:

  1. math.h头文件定义了各种数学函数。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。
  • double exp(double x) :返回 e 的 x 次幂的值。

  • double log(double x) :返回 x 的自然对数(基数为 e 的对数)

  • double pow(double x, double y) :返回 x 的 y 次幂。

  • double sqrt(double x) :返回 x 的平方根。

  • double fabs(double x) :返回 x 的绝对值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
    #include <math.h>

    int main() {
    double d1 = pow(2.0, 3.0);
    double d2 = sqrt(5.0);

    printf("d1=%.2f\n", d1); //d1=8.00
    printf("d2=%f\n", d2); //d2=2.236068

    return 0;
    }

内存管理相关函数:

  1. C程序的内存分配:

    1. 内存从大到小排序:栈–堆–静态区–常量区–代码区
    2. 非静态的局部变量——内存中的动态存储区:stack 栈
    3. 临时使用的数据——建立动态内存分配区域,需要时随时开辟,不需要时及时释放——heap 堆
    4. 全局变量和静态局部变量——内存中的静态存储区/全局区
    5. const修饰常量、字符串常量——常量区
    6. 自己编写的代码、指令等——代码区
    7. 根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,只能通过指针来引用)
  2. void指针(无类型指针)

    1. C 语言提供了一种不定类型的指针,叫做 void 指针。它只有内存块的地址信息没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。

    2. void 指针可以指向任意类型的数据,但是不能解读数据

    3. void 指针与其他所有类型指针之间是互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。

      1
      2
      3
      4
      5
      6
      int x=10;
      int *p=&x;
      void *q=&x;
      q=p;//无类型的指针变量可以与其他类型的指针变量相互转换
      int y=*q;//无法解读出void类型指针指向的数据
      int z=*((int *)q);//可以先转换为int *类型的,再取值

      由于不知道 void 指针指向什么类型的值,所以不能用*运算符取出它指向的值。

  3. 内存动态分配函数:

    1. 头文件<stdlib.h>声明了四个关于内存动态分配的函数。所谓动态分配内存,就是按需分配,申请才能获得。

    2. 掌握:malloc()

      1. 函数原型:void *malloc(unsigned int size); //size的类型为无符号整型

      2. 作用:在内存的动态存储区(堆区)中分配一个长度为size连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。

      3. 由于返回的指针的基类型为 void,应通过显式类型转换后才能存入其他基类型的指针变量,否则会有警告。如果分配不成功,返回空指针(NULL)。

      4. 举例1:

        1
        2
        int *p;
        p=(int *)malloc(sizeof(int));//因为返回的是void *,所以转换为int *类型
      5. 举例2:动态申请数组空间

        1
        2
        3
        4
        int *p;
        p = (int *)malloc(n * sizeof(int));
        for (int i = 0; i < n; i++)
        p[i] = i * 5;

        得到一个元素类型为int型,长度为n的数组。取元素方式与之前相同,如获取第2个元素:p[1]。

      6. 举例3:动态申请结构体空间

        1
        2
        struct node *p;
        p = (struct node *) malloc(sizeof(struct node)); //(struct node*)为强制类型转换
      7. 关于返回值为NULL:

        • malloc() 分配内存有可能分配失败,这时返回常量 NULL。Null 的值为0,是一个无法读写的内存地址,可以理解成一个不指向任何地方的指针。
        • 由于存在分配失败的可能,所以最好在使用 malloc() 之后检查一下,是否分配成功。
    3. 掌握:**free()malloc()**联用

      1. 函数原型:**void free(void *p);**函数无返回值

      2. 作用:释放指针变量p所指向的内存空间,使这部分内存能重新被其它变量使用。否则这个内存块会一直占用到程序运行结束。

        1
        2
        3
        4
        5
        6
        int *p;
        p=(int *)malloc(sizeof(int));

        //...各种操作...

        free(p); //千万不要忘了使用free()释放内存!

        注意:

        1、指针 p 必须是经过动态分配函数 malloc 成功后返回的首地址。

        2、分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。

        3、如果忘记调用free()函数,同时p所在的函数调用结束后p指针已经消失了,导致无法访问未回收的内存块,构成内存泄漏。

    4. 了解:calloc()

      1. 函数原型:void *calloc(unsigned int n,unsigned int size);

      2. 作用:在内存的**动态存储区(堆区)*中分配n个,单位长度为size的连续空间,这个空间一般比较大,总共占用nsize 个字节。其内容初始化为0,并将该空间的首地址作为函数的返回值。如果函数没有成功执行,返回NULL。

      3. calloc()函数适合为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。

        1
        2
        3
        4
        5
        6
        7
        int *p;
        p = (int *)calloc(10,sizeof(int)); //开辟空间的同时,其内容初始化为零

        //等同于
        int* p;
        p = (int *)malloc(10 * sizeof(int));//未初始化
        memset(p, 0, sizeof(int) * 10);//使用memset函数初始化

        上面示例中, calloc() 相当于 malloc() + memset() 。

    5. 了解:realloc()

      1. 函数原型:void realloc(void p, unsigned int size)

      2. 作用:重新分配malloc()或calloc()函数获得的动态空间大小,即调整大小的内存空间。将先前开辟的内存块的指针p指向的动态空间大小改变为size,单位字节。返回值是一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。分配失败返回NULL。

      3. realloc() 优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。

      4. 如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用 memset() )。

        1
        2
        3
        int* b;
        b = (int *)malloc(sizeof(int) * 10);
        b = (int *)realloc(b, sizeof(int) * 2000);

        指针 b 原来指向10个成员的整数数组,使用 realloc() 调整为2000个成员的数组。

动态分配内存的基本原则:

  1. 避免分配大量的小内存块:分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大
  2. 仅在需要时分配内存:只要使用完堆上的内存块,就需要及时释放它,否则可能出现内存泄漏。这里需要遵守原则:谁分配,谁释放。
  3. 总是确保释放以分配的内存:在编写分配内存的代码时,就要确定在代码的什么地方释放内存。

常见的内存错误及其对策:

  1. 内存分配未成功,却使用了它:

    • 解决办法是,在使用内存之前检查指针是否为NULL。比如,如果指针p是函数的参数,那么在函数的入口处应该用if(p==NULL)if(p!=NULL)进行防错处理。
  2. 内存分配虽然成功,但是尚未初始化就引用它:

    1
    2
    3
    4
    5
    int * p = NULL;
    p = (int*)malloc(sizeof(int));
    if (p == NULL){/*...*/}
    /*初始化为0*/
    memset(p, 0, sizeof(int));

    题外话,无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  3. 内存分配成功并且已经初始化,但操作时提示内存越界:

    • 在使用数组时经常发生下标“+1”或者“-1”的操作,特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
  4. 忘记了释放内存,造成内存泄漏:

    • 动态内存的申请与释放必须配对,程序中malloc()free()的使用次数一定要相同,否则肯定有错误。
  5. 未正确的释放内存,造成内存泄漏:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>
    #include <stdlib.h>

    void getMemory(int *p) {
    p = (int *) malloc(sizeof(int)); // 在这里修改的是局部指针 p,不会影响 main 函数中的原始指针 ptr
    //....
    }

    int main() {
    int *ptr = NULL;
    getMemory(ptr); // 将 ptr 的值传递给 getMemory,但是在函数内部修改的是 p,而不是 ptr
    printf("ptr = %d\n", *ptr); // 这里的 *ptr 是未定义行为,因为 ptr 没有指向有效的内存
    free(ptr); // 这里试图释放未分配的内存,会导致问题
    }

    在本例中,getMemory()中的p申请了新的内存,只是把 p所指的内存地址改变了,但是ptr丝毫未变。getMemory()中的p也始终没有进行内存的释放。事实上,因为没有用free释放内存,每执行一次getMemory()就会泄漏一块内存。

  6. 释放了内存却继续使用它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    long *p;
    void addr() {
    long k;
    k = 0;
    p = &k;
    }
    void port() {
    long i, j;
    j = 0;
    for (i = 0; i < 10; i++) {
    (*p)--;
    j++;
    }
    }
    int main() {
    addr();
    port();
    }

    由于addr函数中的变量k在函数返回后就已经不存在了,但是在全局变量p中却保存了它的地址。在下一个函数port中,试图通过全局指针p访问一个不存在的变量,进而出错。

参考链接: