C语言学习教程(八)

C语言学习教程(八):本系列教程第26-30章。

本篇是本系列教程的最后一篇文章,希望大家能够通过本系列教程的学习有所收获。

完结,撒花。。。

26-Error Handling

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许你访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

(1)perror()和strerror()

C 语言提供了 perror()strerror() 函数来显示与 errno 相关的文本消息。

  • perror() 函数输出显示你传给它的字符串。并自动在后面跟一个冒号、一个空格和当前 errno 值的文本表示形式。
  • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。

让我们来模拟一种错误情况,尝试打开一个不存在的文件。你可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,你应该使用 stderr 文件流来输出所有的错误。

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

extern int errno;

int main(){

	FILE *fp = NULL;
	int errnum;

	fp = fopen("unexist.txt", "rb");
	if(fp == NULL){
		fprintf(stderr, "Value of errno : %d\n", errno);
        
        errnum = errno;
		perror("Error printed by perror");
		fprintf(stderr, "Error opening file : %s", strerror(errnum));
	}else{
		fclose(fp);
	}

	return 0;
}

运行结果:

1
2
3
4
5
$ gcc -o test1 test1.c
$ ./test1
Value of errno : 2
Error printed by perror: No such file or directory
Error opening file : No such file or directory%

(2)除以零错误

一个常见的问题是,在除任何数时,程序员不检查除数是否为零,最后会产生运行时错误。

下面的代码通过在除法之前检查除数是否为零来解决此问题:

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

int main()
{
	int dividend = 20;  //被除数
	int divisor = 0;    //除数
	int quotient;       //商

	if(divisor == 0){
		fprintf(stderr, "Division by zero! Exiting...\n");
		exit(-1);
	}
	quotient = dividend / divisor;
	printf("Value of quotient : %d\n", quotient);

	exit(0);
}

运行结果:

1
2
3
$ gcc -o test2 test2.c
$ ./test2
Division by zero! Exiting...

(3)程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值EXIT_SUCCESS。在这里,EXIT_SUCCESS是宏,它被定义为 0。如果程序中存在一种错误情况,当退出程序时,会带有状态值EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:

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

int main()
{
	int dividend = 20;  //被除数
	int divisor = 5;    //除数
	int quotient;       //商

	if(divisor == 0){
		fprintf(stderr, "Division by zero! Exiting...\n");
		exit(EXIT_FAILURE);
	}
	quotient = dividend / divisor;
	printf("Value of quotient : %d\n", quotient);

	exit(EXIT_SUCCESS);
}

运行结果:

1
2
3
$ gcc -o test3 test3.c
$ ./test3
Value of quotient : 4

27-Recursion

递归(Recursion)指的是在函数的定义中使用函数自身的方法。

递归的语法格式如下:

1
2
3
4
5
6
7
8
void recursion()
{
   recursion();  //function calls itself
}

int main() {
   recursion();
}

C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。

递归函数对于解决许多数学问题非常有用,例如计算数字的阶乘、生成斐波那契数列等。

(1)数的阶乘

下面的示例使用递归函数计算一个给定的数的阶乘:

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

int factorial(unsigned int i){
	if(i <= 1){
		return 1;
	}
	return i * factorial(i-1);
}

int main(){

	int i = 15;

	printf("Factorial of %d is %d\n", i, factorial(i));

	return 0;
}

运行结果:

1
2
3
$ gcc -o test1 test1.c
$ ./test1
Factorial of 15 is 2004310016

(2)斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为"兔子数列",指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波那契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波那契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

image-20221018112001440

示例代码:

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

int fibonaci(int i){
	if(i == 0){
		return 0;
	}
	if(i == 1){
		return 1;
	}
	return fibonaci(i-1) + fibonaci(i-2);
}


int main(){

	int i;

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

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ gcc -o test2 test2.c
$ ./test2
0
1
1
2
3
5
8
13
21
34

28-Variable Arguments

有时,你可能会碰到这样的情况,你希望函数带有可变数量的参数,而不是预定义数量的参数。C 语言为这种情况提供了一个解决方案,它允许你定义一个函数,能根据具体的需求接受可变数量的参数。下面的示例演示了这种函数的定义。

1
2
3
4
5
6
7
8
9
int func(int, ... )
{
. . .
}

int main() {
   func(1, 2, 3);
   func(1, 2, 3, 4);
}

请注意,函数 func() 最后一个参数写成省略号,即三个点号(),省略号之前的那个参数是 int,代表了要传递的可变参数的总数。为了使用这个功能,您需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。

具体步骤如下:

步骤1:定义一个函数,最后一个参数为省略号(),省略号前面可以设置自定义参数。

步骤2:在函数定义中创建一个 va_list 类型变量。

步骤3:使用 int 参数和 va_start 宏来初始化 va_list 类型的变量为一个参数列表。

步骤4:使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。

步骤5:使用宏 va_end 来清理赋予 va_list 变量的内存。

数据类型 va_list 和宏 va_startva_argva_end都是在 stdarg.h 头文件中定义的。

现在让我们按照上面的步骤,来编写一个带有可变数量参数的函数,并返回它们的平均值:

 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
#include <stdio.h>
#include <stdarg.h>

double average(int num, ...){    //步骤1

    int i;
    double sum = 0.0;
    va_list valist;              //步骤2

    va_start(valist, num);       //步骤3

    for (i = 0; i < num; i++) {
        sum += va_arg(valist, int);  //步骤4
    }

    va_end(valist);                  //步骤5

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

    return 0;
}

建议本段程序通过IDE来编写,放置鼠标在相应的函数名(eg:va_start)上面来查看函数的详细信息。

运行结果:

1
2
3
4
$ gcc -o test1 test1.c
$ ./test1
Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000

29-Memory Management

本章将解释 C 中的动态内存管理。 C 编程语言提供了多种内存分配和管理功能。这些函数可以在 <stdlib.h> 头文件中找到。

函数名 描述
void *calloc(int num, int size); 在堆中,分配一块 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
void *malloc(int num); 在堆中,分配一块 num 字节数组大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是随机数据。
void free(void *address); 释放由 address 指针所指向的一块内存块。
void *realloc(void *address, int newsize); 重新分配 address 指针所指向的内存块,把内存块扩展到 newsize 大小。

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

(1)动态分配内存

编程时,如果预先知道数组的大小,那么定义数组时就比较容易。

例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

1
char name[100];

但是,如果预先不知道需要存储的文本长度,例如你想存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

 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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main(){

	char name[100];
	char *description;

	strcpy(name, "Zara Ali");

	description = malloc(200 * sizeof(char));

	if(description == NULL){
		fprintf(stderr, "Error - unable to allocate required memory\n");
	}else{
		strcpy(description, "Zara Ali a DPS student in calss 10th");
	}

	printf("Name = %s\n", name);
	printf("Description : %s\n", description);

	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test1 test1.c
$ ./test1
Name = Zara Ali
Description : Zara Ali a DPS student in calss 10th

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

1
calloc(200, sizeof(char));

当动态分配内存时,你有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。

(2)重新调整内存的大小和释放内存

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议在不需要内存时,都应该调用函数 free() 来释放内存。或者,通过调用函数 realloc() 来增加或减少已分配的内存块的大小。下面让我们使用 realloc() 和 free() 函数,再次查看上面的实例:

 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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main(){

	char name[100];
	char *description;

	strcpy(name, "Zara Ali");

	description = 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 = 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(description);

	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test2 test2.c
$ ./test2
Name = Zara Ali
Description : Zara Ali a DPS student.She is in class 10th

30-Command Line Arguments

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当你想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。

命令行参数是使用 main() 函数参数来处理的。其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。下面是一个简单的示例,它会检查命令行是否有提供参数,并根据参数执行相应的动作:

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

int main(int argc, char *argv[]){

	if(argc == 2){
		printf("The argument supplied is : %s\n", argv[1]);
	}else if(argc > 2){
		printf("Too many arguments supplied.\n");
	}else{
		printf("One argument expected.\n");
	}

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ gcc -o test1 test1.c

$ ./test1
One argument expected.

$ ./test1 hello
The argument supplied is : hello

$ ./test1 hello Alice
Too many arguments supplied.

需要指出的是,argv[0] 默认存储当前程序的名称,argv[1] 是一个指向第一个命令行参数的指针,*argv[n] 是最后一个参数。如果没有提供任何参数,argc 将被设置为 1。如果只传递了一个参数,argc 将被设置为 2。

多个命令行参数之间应该用空格分隔开,但是如果参数本身带有空格,那么传递参数的时候就应该把参数放置在双引号""或单引号 '' 内部。

让我们重新编写上面的示例,我们将打印当前程序名称,并且我们还通过在双引号内输入命令行参数:

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


int main(int argc, char *argv[]){

	printf("Program name is : %s\n", argv[0]);

	if(argc == 2){
		printf("The argument supplied is : %s\n", argv[1]);
	}else if(argc > 2){
		printf("Too many arguments supplied.\n");
	}else{
		printf("One argument expected.\n");
	}

	return 0;
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ gcc -o test2 test2.c

$ ./test2
Program name is : ./test2
One argument expected.

$ ./test2 hello
Program name is : ./test2
The argument supplied is : hello

$ ./test2 hello Alice
Program name is : ./test2
Too many arguments supplied.

$ ./test2 "hello Alice"
Program name is : ./test2
The argument supplied is : hello Alice
updatedupdated2022-10-182022-10-18