深刻理解C语言标准
前言
本文章内容有点长,建议耐心看完,会对C语言的语法有更深的印象。
C语言的起源
C语言起源于B语言,B语言起源于ALGOL(简称:A语言)。可以画图表示:
A语言和B语言已经落伍了,我们可以不用管它们。
C语言的特点
C语言具有高效、强大而灵活,尤其在嵌入式开发。C语言比较靠近底层(除了汇编之外),学习它之后,学习任何语言都将事功半倍。
C语言的标准概述
C语言刚开始诞生的时候没有统一的标准,后来出现了一个K&R标准,但是这个标准并不完善。只定义了C语言而没有定义C库。
C89标准又可以成为ANSI C 或者 ISO C,因为C89标准是在1989年由美国标准协会(ANSI C)发布的,在1990年由国际标准化组织(ISO)在ANSI C的基础之上发布C90标准。但是C90标准很少有人提,一般C89和C90统称为C89,因为改动量并不大。
C99标准在C89之上修订了一些细节以及增加更广的国际字符集支持
C11标准增加了一些新特性,主要多了多线程支持
C18标准没有引入新的语言特性,只对C11进行了补充和修正
K&R标准
比较老的标准了,现在估计找不到编译器支持这个标准了。它与C89标准最大的不同在于函数定义:
1 | // 参数名在圆括号指定 |
C89标准
可以说C89标准非常经典,记得我学的时候就是从C89标准开始的。
C89特点:
- 所有变量都必须先声明后使用,声明必须在语句块开头,任何可执行语句之前。
1 | // 正确的示范 |
- 引入了++运算符和–运算符
1 | main() |
- 外部变量和内部变量,外部变量即全局变量,内部变量即局部变量
1 | //内部变量,只有在函数被调用的时候存在,函数执行完消失 |
1 | //外部变量,意思是在函数外部定义的变量,程序执行期间一直存在,可以被任何函数访问 |
extern 代表声明的意思,声明可以多次声明(说明的意思),但是定义(分配内存空间)只能定义一次
C 语言中用外部变量(全局变量),而在C++中要使用类的静态成员变量代替外部变量。(为了良好的编码习惯)
- 字符串常量可以连接起来
1 | char* s = "hello," |
- const 用来限定指定的内存空间不被修改
我们以编译器分配内存的角度去看
1 | char* const fd = "eee"; |
可以看出如果有指针类型,并且被const修饰,那么指针指向的内存空间不能被修改。
如果const修饰数组类型,那么数组整个内存块不能被修改。
如果const 修饰存放地址的内块,那么就不能修改存放地址的内存块
只要记住,只要有指针类型(除了被两个const修饰),那么编译器会分配两处内存,一个是变量的地址,另一个是变量存储的内容。
const程序地址:https://gitee.com/SFlow/blog-project/tree/master/constC
- 静态static
static 可以修饰外部变量和内部变量,static修饰的变量在程序运行期间一直存在。
static 不能被extern修饰,也不能出现只声明,只能被定义,这个规则限制了使用范围。
当static修饰外部变量时,意味着这个变量只能在该源文件中使用。
1 | //a.c文件 |
当static修饰内部变量时,意味着该变量只能被该函数使用。
1 | //main.c文件,这里不能对s_a进行声明,因为extern不能修饰带有static的变量或函数 |
static可以修饰函数,当修饰函数时,意味着这个函数只能在该源文件中使用。(PS:没有声明你怎么用?)
- 指针数组和数组指针
指针数组,很明显突出的是指针,比如:int* p[3]代表三块连续的内存空间,每个内存空间都存放着一个地址。
数组指针,突出的是数组,比如 int (*p)[3] 代表一块内存空间,这块内存空间存放着一个地址,地址所在位置为三块连续的内存空间。
数组指针让我们想到使用函数指针,比如:void (*fun1)(int a,int b),fun1指向的函数所在的地址。
这块我倒是不常用,我用的话会用到二级指针即 int** p,比较灵活。数组不支持动态分配内存,所以比较死板。
1 |
|
- 命令行参数
main入口函数带有两个参数,第一个参数(argc)的值表示运行程序时参数的个数,第二个参数(argv)指向字符串数组的指针,每个字符串对应一个参数。
argv[0]的值是启动该程序的程序名,因此argc的值至少为1。
假设我们的程序名称叫echoT,那么我们在命令行输入:echoT hello word。那么我们在程序中就能接收到这三个值。
1 |
|
- 函数指针
和函数声明有点类似,但是有不同。比如:
1 | int fun1(int a,int b);//函数声明 |
- typedef
1 | //typedef 把一种类型定义了别名 |
- 联合体
我学习的时候学过,但是工作的时候没有用过。
1 | union tag{ |
- 行宏(__LINE__)和文件宏(__FILE__)
1 | printf("line = %d,file = %s", __LINE__, __FILE__); |
C99标准
- 增加了inline关键字
可以把函数指定为内联函数,这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。
- 增加了restrict关键字
1 | //没有添加restrict之前,可以通过任意指针变量去改变指针所指向的值 |
- 增加了_Complex关键字,来表示复数
这个没有用过,也许有些行业有用吧?
1 |
|
- 增加了_Imaginary关键字,来表示虚数
同上
1 | float _Imaginary a; /* 虚数类型的实部为0 */ |
- 增加了一种新的数据类型:_Bool类型
1 | //_Bool类型长度为1,只能取值范围为0或1 |
支持long long 数据类型
变量声明不必放在语句块开头
1 | int fun() |
- 增加了函数宏(__func__)
1 | printf("fun = %s", __func__); |
取消了不写函数返回类型默认就是 int 的规定
增加和修改了一些标准头文件
定义 bool 的 <stdbool.h>
定义一些标准长度的 int 的 <inttypes.h>
定义复数的 <complex.h>
定义宽字符的 <wctype.h>
有点泛型味道的数学函数 <tgmath.h>
跟浮点数有关的 <fenv.h>
< stdarg.h> 里多了一个 va_copy 可以复制 … 的参数
<time.h> 里多了个 struct tmx 对 struct tm 做了扩展
- 引入了long long int
C99标准中引进了long long int(-(2e63 - 1)至2e63 - 1)和unsigned long long int(0 - 2e64 - 1)。long long int能够支持的整数长度为64位。
C11标准
- 对齐处理
alignof(T)返回T的对齐方式,aligned_alloc()以指定字节和对齐方式分配内存,头文件<stdalign.h>
定义了这些内容。
1 | struct Foo { |
1 | //void *aligned_alloc( size_t alignment, size_t size ); |
- _Noreturn
_Noreturn是个函数修饰符,位置在函数返回类型的前面,声明函数无返回值
1 |
|
- _Generic
_Generic支持轻量级范型编程,可以把一组具有不同类型而却有相同功能的函数抽象为一个接口
1 | _Generic( ‘a’, char: 1, int: 2, long: 3, default: 0) //与switch类似 |
- _Static_assert()
静态断言,在编译时刻进行。
1 |
|
- 匿名结构体、联合体
在 C 语言中,可以在结构体中声明某个联合体(或结构体)而不用指出它的名字,如此之后就可以像使用结构体成员一样直接使用其中联合体(或结构体)的成员。
1 |
|
- 多线程
头文件<threads.h>
定义了创建和管理线程的函数,新的存储类修饰符_Thread_local限定了变量不能在多线程之间共享。
VS2019移除了对threads.h的支持,所以windows下不能用这个。
上面提到VS主要目标是支持C++,对C的支持是次要的。
- quick_exit()
又一种终止程序的方式,当exit()失败时用以终止程序。
- time.h新增timespec结构体,时间单位为纳秒,原来的timeval结构体时间单位为毫秒。
struct timespec 定义:
1 | typedef long time_t; |
C18标准
C18 没有引入新的语言特性,只对 C11 进行了补充和修正。
结尾
至此,对C语言标准的简单总结就完了。简单来看,其实从C89开始,其他并没有太大变化。下一篇将对C++标准做个简单总结。