【C语言】关于我回头学的那些错误处理等(六)

本文介绍了C语言中的错误处理方法,包括返回值和错误码、异常处理(使用setjmp和longjmp)以及断言函数的使用。此外,还详细阐述了C语言的动态内存管理,如malloc、calloc、realloc和free的使用,并给出了相关示例。文章还涉及了可变参数的概念,展示了如何使用va_list处理可变数量的参数。
摘要由CSDN通过智能技术生成

前言

我的第一门语言就是C,但是学艺不精,中途跑去学了C#和Java后,感觉到了C的重要性,毕竟是最接近底层的语言,又跑回来学C。

毕竟前两门的控制语句,变量什么的都是类似的,回到C后只需要学习一些特定C的语法,比如宏,预编译指令等等,这些对我来说都是陌生的词汇。

所以边学边记录一下以前的知识。



一、错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。

在发生错误时,大多数的 C 或 UNIX 函数调用返回 1NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

C 语言中常用的错误处理技术包括:

  1. 返回值检查:可以通过函数的返回值来判断函数执行是否成功,如果返回的状态码表示出现了错误则需要进行相应的处理。
  2. 异常处理:C 语言没有内置的异常处理机制,但是可以通过 setjmp() 和 longjmp()、signal()
    等函数来实现类似异常处理的功能。
  3. 断言函数:断言函数(如 assert())可以在程序运行时检查某个条件是否成立,如果不成立就会终止程序并输出错误信息。
  4. 日志记录:可以使用日志库(如 syslog、log4c)等工具来记录程序运行时的信息和错误,便于排查问题和调试程序。
  5. 调试器:可以使用调试器(如 GDB)来动态地跟踪程序运行过程,并定位和解决错误。调试器通常提供单步执行、断点设置、变量监视等功能。
  6. 多级处理:多级处理可以针对不同的错误类型采取不同的处理方法,例如先尝试恢复错误,不能恢复再报告错误并退出程序,或者打开一个错误窗口提示用户选择下一步操作等。

下面主要讲解前三者,因为后面三个我也没必要讲吧。。

1)返回值和错误码

C语言中的函数通常会返回一个值来表示函数执行成功还是失败。如果执行成功,该函数通常返回0或其他非负数;如果执行失败,则返回一个负数作为错误码。

例如,标准库函数fopen()用于打开文件。

  • 成功:返回一个指向文件的指针
  • 失败:返回NULL

另外,在errno.h头文件中定义了一些常量来表示不同类型的错误码。

使用这种方式进行错误处理时,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作,开发人员应该在程序初始化时,把 errno 设置为 0。

这种方式比较简单但也比较繁琐,容易出错。

2)异常处理

除了使用传统的返回值和错误码来进行错误处理外,C语言也可以通过一些异常处理机制来实现更加灵活和清晰的代码结构。

例如,在C++中就提供了try-catch机制来捕获和处理异常。

另外,在C语言中也可以使用setjmp()longjmp()函数组合来模拟异常处理机制。

  • setjmp():用于保存当前程序状态,并将控制流转移到指定位置
  • longjmp():用于恢复保存的程序状态并跳转到之前设置的位置

使用这种方式进行错误处理时,可以将所有可能引起异常的代码放在try块中,并在catch块中处理异常。

这种方式可以使代码更加简洁和易于维护,同时也是现代C程序设计中的一种主流做法。

总之,C语言的错误处理方式有很多,具体选择哪一种需要根据具体情况而定。对于简单的程序,使用返回值和错误码即可;对于复杂的程序,使用异常处理会更加方便和灵活。

3)断言函数

断言函数(Assertion function)是一种程序设计中用于进行错误检查和调试的技术。

断言函数是一个布尔表达式,用于指示在运行时某个条件是否成立。如果这个条件不成立,程序就会抛出异常或者中断运行,并输出相关的错误信息。

在 Android 程序中,断言函数通常被用于进行参数合法性检查、空指针检查等操作,以防止程序因为非法输入或者其他原因而导致崩溃或者异常行为。

在 C++ 中,常见的断言函数包括 assert() 函数和 static_assert() 函数;
在 Java 中,常见的断言函数包括 assert 关键字和 JUnit 断言函数等。

4)三种错误处理的实例

以下是几个C语言错误处理的示例:

1. 返回值和错误码示例

假设我们要打开一个文件,如果文件打开成功,就输出文件内容;否则输出错误信息。

在这种情况下,我们可以使用fopen()函数返回的指针来判断是否发生了错误,并在发生错误时设置errno变量。

#include <stdio.h>
#include <errno.h>

int main()
{
    FILE *fp = fopen("test.txt", "r");
    if(fp == NULL)
    {
        printf("Error opening file: %s", strerror(errno));
        return 1;
    }
    else
    {
        char buffer[100];
        while(fgets(buffer, sizeof(buffer), fp) != NULL)
            printf("%s", buffer);
        fclose(fp);
        return 0;
    }
}

解析strerror

2. 异常处理示例

假设我们要进行一系列复杂的操作,并且有可能会出现各种不同类型的异常。

在这种情况下,我们可以使用setjmp()longjmp()来模拟异常处理机制,并将所有可能出现异常的代码放在try块中。

#include <stdio.h>
#include <setjmp.h>

jmp_buf exception_buffer;

void catch_exception(const char *msg)
{
    printf("Exception caught: %s", msg);
    longjmp(exception_buffer, 1);
}

void complex_operation()
{
    int a = 5, b = 0;
    if(b == 0)
        catch_exception("Division by zero");
    else
        printf("Result: %d", a/b);  
}

int main()
{
   if(setjmp(exception_buffer) == 0)
        complex_operation();
    else
        printf("Program terminated due to exception");

    return 0;
}

在上述示例中,我们定义了一个catch_exception()函数来处理异常,并使用longjmp()跳转到之前保存的位置。

complex_operation()函数中,我们进行了除法操作并判断除数是否为0。如果除数为0,则调用catch_exception()函数抛出异常。

最后,在main函数中使用setjmp()保存当前程序状态,并在发生异常时跳转到之前设置的exception_buffer位置。

如果没有发生异常,则执行完complex_operation()函数后正常退出程序。


3. 断言函数示例

下面是一个使用 assert() 函数进行调试的示例代码:

#include <stdio.h>
#include <assert.h>

int main()
{
    int x = 5;
    int y = 10;

    assert(x < y);  // 检查 x 是否小于 y

    printf("x + y = %d\n", x + y);

    return 0;
}

在上述代码中,使用了 assert() 函数来检查变量 x 是否小于变量 y。由于 x < y 的条件成立,程序不会抛出异常,因此可以正常运行并输出 "x + y = 15" 的结果。

如果我们将 x 和 y 的值交换一下,使x > y,则程序会在执行到 assert() 函数时抛出一个异常,并输出相关的错误信息:

Assertion failed: x < y, file test.c, line 9

这个错误提示告诉我们,在文件 test.c 的第 9 行处,断言 x < y 失败了,即 x 的值大于等于 y 的值。

这种方式可以帮助我们快速找到代码中的问题,并进行修复。


二、可变参数

简单来说就几句话,你调用函数的时候是不是可以传参数?

那我一般的函数有几个参数我就穿几个,可变参数你传几个都行,100个都可以。

#include <stdio.h>
#include <stdarg.h>
 
double average(int num,...)//num表示总数量
{
 	//用于存储可变参数列表
    va_list valist;
    //累加num个参数
    double sum = 0.0;
 
    /* 为 num 个参数初始化 valist */
    va_start(valist, num);
 
    /* 访问所有赋给 valist 的参数 */
    for (int i = 0; i < num; i++)
    {
       //通过va_arg宏逐个访问valist中的值,并转换为int类型,再加到sum中
       sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);
 
 	//返回所有输入整数的平均数
    return sum/num;
}
 
int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

结果如下:

结果

下面是对该函数中每一步的解析:

  1. 首先,在函数头部声明了一个可变参数的函数average,它接收两个参数:num和省略号(…)。其中num代表后续要计算平均数的整数数量,而省略号表示可以包含任意多个整数作为计算平均数的输入。

  2. 在函数内部定义一个va_list类型的变量valist,用于存储可变参数列表。

  3. 后面通过调用va_start宏来初始化valist。这个宏可以设置valist指向参数列表中第一个可变参数所在位置。

  4. 然后,使用for循环遍历所有传递进来的整数,并将它们累加到sum总和中去。这里通过va_arg宏逐个访问valist中存储的参数值,并将其转换为int类型,再加到sum中。

  5. 循环结束后,使用va_end宏清理valist所占用的内存空间。注意,在调用完va_end之后就不能再使用valist了。

  6. 最后返回所有输入整数的平均数。

需要注意一下点:

  • 必须在包含stdarg.h头文件之后才能编译此代码。
  • 必须在调用average函数时至少传递一个整数作为参数。

三、动态内存管理

C 语言提供了一些函数和运算符,使得程序员可以对内存进行操作,包括分配、释放、移动和复制等,这些函数可以在 <stdlib.h> 头文件中找到。

内存是通过指针变量来管理的。指针是一个变量,它存储了一个内存地址,这个内存地址可以指向任何数据类型的变量,包括整数、浮点数、字符和数组等。

序号函数和描述(从分配到释放,依次往下)
1void *malloc(int num);
在堆区分配一块指定大小的内存空间,用来存放数据。
这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
2void calloc(int num, int size);
在内存中动态地分配 num 个长度为 size 的连续空间,
并将每一个字节都初始化为 0。
所以它的结果是分配了 num
size 个字节长度的内存空间,
并且每个字节的值都是 0。
3void *realloc(void *address, int newsize);
该函数重新分配内存,把内存扩展到 newsize。
4void free(void *address);
该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

calloc和malloc区别:

  • calloc会被初始化
  • malloc不会被初始化

注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。

1)分配内存

学过数组的人,都知道在定义的时候能给定一个大小,比如:

//代码1
int arr1[10];
char arr2[10];
float arr3[1];
double arr4[20];
//代码2
//用宏定义的方式
#define X 3
int arr5[X];

但是,如果您不知道需要存储的长度。那么我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

//定义指针,该指针指向未定义所需内存大小的字符
char *description;
...
//动态分配内存
description = (char *)malloc( 200 * sizeof(char) );

上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:

calloc(200, sizeof(char));

那些从一开始就定义了大小的数组,一旦定义则无法改变大小。

而当动态分配内存时,您有完全控制权,可以传递任何大小的值。

2)调整内存

您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。

char *description;
...
//动态分配内存
description = (char *)malloc( 30 * sizeof(char) );
...
//假设您想要存储更大的描述信息
description = (char *) realloc( description, 100 * sizeof(char) );

您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

3)释放内存

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。

//使用 free() 函数释放内存
free(description);

4)综合案例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");//复制
 
   //动态分配内存
   description = (char *)malloc( 30 * sizeof(char) );
   if( description == NULL )
   {
   	  //把一个字符串写入到文件中
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student.");
   }
   //假设您想要存储更大的描述信息
   description = (char *) realloc( description, 100 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
   	  //字符串追加
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
 
   /* 使用 free() 函数释放内存 */
   free(description);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七qi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值