05-复习C语言核心知识|预处理、头文件、强制类型转换、错误处理、递归、内存管理

一、概述

最近家里有点事,趁在家的这段时间,复习一下C语言核心知识,后的底层开发、音视频开发、跨平台开发、算法等方向的进一步研究与学习埋下伏笔

本篇文章接着上一篇继续对C语言的核心语法知识进行复习

二、C 语言核心语法|预处理、头文件、强制类型转换

1. 预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

例子:

分析下面的实例来理解不同的指令。

#define MAX_ARRAY_LENGTH 20




















这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 替换为 20。使用 #define 定义常量来增强可读性。

#include <stdio.h>




#include "utils.h"




















这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 utils.h,并添加内容到当前的源文件中。

#undef  FILE_SIZE
#define FILE_SIZE 42




















这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif
















这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#ifdef DEBUG
   /* Your debugging statements here */
#endif
















这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。

预定义宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
FILE 这会包含当前文件名,一个字符串常量。
LINE 这会包含当前行号,一个十进制常量。
STDC 当编译器以 ANSI 标准编译时,则定义为 1。

例子:

void main() {

    //这会包含当前文件名,一个字符串常量。
    printf("File :%s\n", __FILE__);
    //当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
    printf("Date :%s\n", __DATE__);
    //当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
    printf("Time :%s\n", __TIME__);
    //这会包含当前行号,一个十进制常量。
    printf("Line :%d\n", __LINE__);
    //当编译器以 ANSI 标准编译时,则定义为 1。
    printf("ANSI :%d\n", __STDC__);
} 

输出:

File :/Users/devyk/Data/ClionProjects/NDK_Sample/day_1/ndk_day1.c
Date :Dec 17 2019
Time :14:23:47
Line :954
ANSI :1 

预处理器运算符

C 预处理器提供了下列的运算符来帮助您创建宏:

宏延续运算符()

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n") 

字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:

#include <stdio.h>














#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")





int main(void){
   message_for(Carole, Debra);
   return 0;
}







当上面的代码被编译和执行时,它会产生下列结果:

Carole and Debra: We love you!




















标记粘贴运算符(##)

宏定义内的标记粘贴运算符##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

#include <stdio.h>














#define tokenpaster(n) printf ("token" #n " = %d", token##n)










int main(void)
{

   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}






当上面的代码被编译和执行时,它会产生下列结果:

token34 = 40 

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

printf ("token34 = %d", token34); 

这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)标记粘贴运算符(##)

defined() 运算符

预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

#include <stdio.h>














#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif


int main(void){
   printf("Here is the message: %s\n", MESSAGE);  
   return 0;
} 

当上面的代码被编译和执行时,它会产生下列结果:

Here is the message: You wish! 

参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

int square(int x) {
   return x * x;
} 

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x)) 

在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

#include <stdio.h>














#define MAX(x,y) ((x) > (y) ? (x) : (y))










int main(void){
   printf("Max between 20 and 10 is %d\n", MAX(10, 20));  
   return 0;
} 

当上面的代码被编译和执行时,它会产生下列结果:

Max between 20 and 10 is 20  

2. 头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

引用头文件的语法

使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:

#include <file> 

这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

#include "file" 

这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

引用头文件的操作

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 char_manger.h,如下:

char *test(void); 

和一个使用了头文件的主程序 char_manager.c,如下:

#include "char_manger.h"
int x;
int main (void)
{
   puts (test ());
} 

编辑器会看到如下的代码信息:

char *test (void);










int x;










int main (void)
{

   puts (test ());
} 

只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

#ifndef HEADER_FILE
#define HEADER_FILE










the entire header file file





#endif  

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:

#if SYSTEM_1
   # include "system_1.h"
#elif SYSTEM_2
   # include "system_2.h"
#elif SYSTEM_3
   ...
#endif 

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,您只需要使用宏名称代替即可:

 #define SYSTEM_H "system_1.h"
 ...
 #include SYSTEM_H  

SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义

3. 强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:

(type_name) expression  

请看下面的实例,使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:

void main(){

    int sum = 20,count = 3;
    double  value,value2;
    value = (double)sum / count;
    value2 = sum / count;
    printf("Value 强转 : %f Value2 wei强转 : %f\n ", value ,value2);
} 

输出:

Value 强转 : 6.666667 Value2 wei强转 : 6.000000

整数提升

整数提升是指把小于 intunsigned int 的整数类型转换为 intunsigned int 的过程。请看下面的实例,在 int 中添加一个字符:

void main(){ 
    //整数提升
    int i= 17;
    char c = 'c'; //在 ascii 中的值表示 99
    int sum2;


    sum2 = i + c;
    printf("Value of sum : %d\n", sum2 );
}







输出:

 Value of sum : 116 

在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 ‘c’ 的值转换为对应的 ascii 值。

三、C 语言核心语法|错误处理、递归、内存管理

1. 错误处理

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

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

errno、perror() 和 strerror()

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

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

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

例子:

void main(){

    int dividend = 20;
    int divsor = 0;
    int quotient;





    if (divsor == 0){
        fprintf(stderr,"除数为 0 退出运行。。。\n");
        exit(EXIT_FAILURE);
    }

    quotient = dividend / divsor;
    fprintf(stderr,"quotient 变量的值为 : %d\n", quotient);
    exit(EXIT_SUCCESS);
}






输出:

除数为 0 退出运行。。。




















2. 递归

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

语法格式如下:

void recursion()
{

   statements;
   ... ... ...
   recursion(); /* 函数调用自身 */
   ... ... ...
}
 
int main()
{
   recursion();
}








数的阶乘

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

   return i * factorial(i - 1);
}

void main(){
    int i = 15;
    printf("%d 的阶乘 %ld \n",i ,factorial(i));
} 

输出:

15 的阶乘 140732727129776 




















斐波拉契数列

//斐波拉契数列
int fibonaci(int i){
    if (i == 0){
        return 0;
    }
    if (i == 1){
        return 1;
    }
    return fibonaci(i - 1) + fibonaci( i -2);
}

void main(){
    for (int j = 0; j < 10; j++) {
        printf("%d\t\n", fibonaci(j));




    }
}


输出:

0
1	
1	
2	
3	
5	
8	
13	
21	
34	







3. 可变参数

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

int func(int, ... ) 
{

   .
   .
   .
}

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








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

  • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
  • 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
  • 使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
  • 使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
  • 使用宏 va_end 来清理赋予 va_list 变量的内存。

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

例子:

 double average(int num,...){
     va_list  vaList;
     double  sum = 0.0;
     int i ;
     //为 num 个参数初始化 valist
     va_start(vaList,num);
     //访问所有赋给 vaList 的参数
    for (int j = 0; j < num; j++) {
        sum += va_arg(vaList, int);
    }
    //清理为valist 保留的内存
    va_end(vaList);
    return sum/num;
 }


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




输出:

Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000




















4. 内存管理

本章将讲解 C 中的动态内存管理。C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

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

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

动态分配内存

编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

char name[100];




















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

void main() {

    char name[100];
    char *description;










    //将字符串 copy 到 name 中
    strcpy(name, "迎娶白富美!");

    //开始动态分配内存
    description = (char *) malloc(200 * sizeof(char));
    if (description == NULL) {
        fprintf(stderr, "Error - unable to allocate required memory\n");
    } else {
        strcpy(description, "开始添加数据到 description 中");
    }
    printf("Name = %s\n", name );
    printf("Description: %s sizeOf 大小 :%d\n", description , sizeof(description));
//     使用 free() 函数释放内存
    free(description);
}




输出:

Name = 迎娶白富美!
Description: 开始添加数据到 description 中 sizeOf 大小 :8




















5. 命令行参数

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

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

void main(int argc , char *argv[]){
    if (argc ==1){
        printf("argv[%d] == %d",0,*argv[0]);
    }

    else if (argc ==2){
        printf("argv[%d] == %d",1,*argv[1]);
    } else{
        printf("匹配失败...");
    }

}








输出:

argv[0] == 47 

总结

本文篇幅都是复习C语言的基础知识,其它更深入的学习可以参考加强学习资料
不知道大家在看完 C 基础内容之后在对比下 其它高级语言的 语法,是不是大部分都差不多,之前有的人说学了 C 在学其它语言都是小菜一碟,现在看来好像是这么回事。个人觉得其实只要会编程语言中的任何一门在学其它语言都会比较容易上手。

C 语言基础加强学习资料

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYUQt4Np' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片