一、概述

最近家里有点事,趁在家的这段时刻,温习一下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言语中心常识

  • 01-温习C言语中心常识|综述
  • 02-温习C言语中心常识|基本语法、数据类型、变量、常量、存储类、基本句子(判别句子、循环句子、go to句子)和运算
  • 03-温习C言语中心常识|函数、作用域规矩、数组、枚举、字符与字符串、指针
  • 04-温习C言语中心常识|结构体、共用体、位域、输入&输出、文件读写
  • 05-温习C言语中心常识|预处理、头文件、强制类型转化、过错处理、递归、内存办理

总结

本系列文章都是温习C言语的中心根底常识,其它更深化的学习能够参阅加强学习材料
不知道我们在看完 C 根底内容之后在对比下 其它高档言语的 语法,是不是大部分都差不多,之前有的人说学了 C 在学其它言语都是小菜一碟,现在看来好像是这么回事。个人觉得其实只要会编程言语中的任何一门在学其它言语都会比较简单上手。

C 言语根底加强学习材料

  • C 言语 排序算法
  • C 言语 根底实例代码
  • C 言语 100 道练习题

参阅

  • 菜鸟教程runoob