深刻理解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
2
3
4
5
6
7
8
9
10
11
12
// 参数名在圆括号指定
//参数类型在左花括号之前声明
//如果没有声明某个参数的类型,则默认为int类型
power(base,n)
int base,n;
{
int i,p;
p=1;
for(i = 1;i<=n;++i)
p = p* base;
return p;
}

C89标准

​ 可以说C89标准非常经典,记得我学的时候就是从C89标准开始的。

​ C89特点:

  1. 所有变量都必须先声明后使用,声明必须在语句块开头,任何可执行语句之前。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 正确的示范
void success()
{
int a,b;
double r;
a = 10;
b = a+10;
}
//错误的示范,编译时会报错
void error()
{
printf("error\n");
int a;
a = 10
}
//如果我这样写,反而能编译通过
void fun()
{
printf("fun\n");
{
int a;
a = 10;
}
}
  1. 引入了++运算符和–运算符
1
2
3
4
5
6
7
8
9
main()
{
int i;
i = 0;
printf("%d\n",i++);
printf("%d\n",i--);
printf("%d\n",++i);
printf("%d\n",i);
}
  1. 外部变量和内部变量,外部变量即全局变量,内部变量即局部变量
1
2
3
4
5
6
//内部变量,只有在函数被调用的时候存在,函数执行完消失
int fun()
{
int i;//内部变量
i=0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//外部变量,意思是在函数外部定义的变量,程序执行期间一直存在,可以被任何函数访问
//例如main.c文件中定义了一个外部变量
int g_y;
int main()
{
g_y = 0;
g_y++;
printf("%d\n",g_y);
}
//假如我在a.c文件中要使用这个变量,那么我可以这样写:
extern int g_y;
void bFun()
{
g_y++;
printf("%d\n",g_y);
}
//该程序示例地址:https://gitee.com/SFlow/blog-project/tree/master/GlobalVariable

extern 代表声明的意思,声明可以多次声明(说明的意思),但是定义(分配内存空间)只能定义一次

C 语言中用外部变量(全局变量),而在C++中要使用类的静态成员变量代替外部变量。(为了良好的编码习惯)

  1. 字符串常量可以连接起来
1
2
3
4
5
char* s = "hello,"
"world";
//等价于
char* s = "hello,world";
//不管引号中间有空格或者换行都可以连接起来
  1. const 用来限定指定的内存空间不被修改

我们以编译器分配内存的角度去看

1
2
3
char* const fd = "eee";
// fd = "eee";// error,不能修改fd那块内存块
*fd = "ddd";// 正确,可以修改指针指向的那块内存空间

可以看出如果有指针类型,并且被const修饰,那么指针指向的内存空间不能被修改。

如果const修饰数组类型,那么数组整个内存块不能被修改。

如果const 修饰存放地址的内块,那么就不能修改存放地址的内存块

只要记住,只要有指针类型(除了被两个const修饰),那么编译器会分配两处内存,一个是变量的地址,另一个是变量存储的内容。

const程序地址:https://gitee.com/SFlow/blog-project/tree/master/constC

  1. 静态static

static 可以修饰外部变量和内部变量,static修饰的变量在程序运行期间一直存在。

static 不能被extern修饰,也不能出现只声明,只能被定义,这个规则限制了使用范围

当static修饰外部变量时,意味着这个变量只能在该源文件中使用。

1
2
3
4
5
6
7
8
9
10
11
12
//a.c文件
static int gs_a = 10;
void func1()
{
gs_a++;
printf("%d\n",gs_a);
}
//b.c文件
void fun2()
{
//printf("%d\n",gs_a);//error,不能这样用
}

当static修饰内部变量时,意味着该变量只能被该函数使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//main.c文件,这里不能对s_a进行声明,因为extern不能修饰带有static的变量或函数
void func()
{
static int s_a = 10;
printf("%d\n",s_a);
}
void func1()
{
//printf("%d\n",s_a);//error,不能这样用
}
int main()
{
func();
}

static可以修饰函数,当修饰函数时,意味着这个函数只能在该源文件中使用。(PS:没有声明你怎么用?)

  1. 指针数组和数组指针

指针数组,很明显突出的是指针,比如:int* p[3]代表三块连续的内存空间,每个内存空间都存放着一个地址。

数组指针,突出的是数组,比如 int (*p)[3] 代表一块内存空间,这块内存空间存放着一个地址,地址所在位置为三块连续的内存空间。

数组指针让我们想到使用函数指针,比如:void (*fun1)(int a,int b),fun1指向的函数所在的地址。

这块我倒是不常用,我用的话会用到二级指针即 int** p,比较灵活。数组不支持动态分配内存,所以比较死板。

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
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
void initData(int* p, int len)
{
int i = 0;
static int startNumber = 0;
for (; i < len; i++)
{
startNumber++;
p[i] = startNumber;
}
}
void printfInt(int* p, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%d\n", p[i]);
}
}
int main()
{
int n = 10;
int p0 = 7;
int p1 = 6;
int i = 1;
int j = 0;
int** p = malloc(n * sizeof(int*));
p[0] = malloc(p0 * sizeof(int));
//初始化p[0]
initData(p[0],p0);

for (; i < n; i++)
{
p[i] = malloc(p1 * sizeof(int));
//初始化
initData(p[i], p1);
}

//打印
printfInt(p[0],p0);
printf("-------上面是0号指针的数据--------\n");
for (i = 1; i < n; i++)
{
printfInt(p[i],p1);
printf("-------上面是%d号指针的数据--------\n",i);
}
return 0;
}
  1. 命令行参数

main入口函数带有两个参数,第一个参数(argc)的值表示运行程序时参数的个数,第二个参数(argv)指向字符串数组的指针,每个字符串对应一个参数。

argv[0]的值是启动该程序的程序名,因此argc的值至少为1。

假设我们的程序名称叫echoT,那么我们在命令行输入:echoT hello word。那么我们在程序中就能接收到这三个值。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc,char* argv[])
{
printf("argc = %d", argc);
printf("argv[0] = %s", argv[0]);
printf("argv[1] = %s", argv[1]);
printf("argv[2] = %s", argv[2]);
return 0;
}

  1. 函数指针

和函数声明有点类似,但是有不同。比如:

1
2
int fun1(int a,int b);//函数声明
int (*fun1)(int a,int b);//定义函数指针
  1. typedef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//typedef 把一种类型定义了别名
typedef char* PCHAR;//将char* 起了一个别名叫PCHAR
struct point
{
int x;
int y;
};
//将struct point 起了别名叫POINT
typedef struct point
{
int x;
int y;
}POINT;

//可以简化函数指针的使用
typedf int (*fun1)(int a,int b);
fun1 m_fun;//定义了一个函数
//省略给函数赋值,一般是加载dll的时候动态定位函数地址
m_fun();//调用函数
  1. 联合体

我学习的时候学过,但是工作的时候没有用过。

1
2
3
4
5
6
7
union tag{
int x;
float m;
char* b;
};
//相当于储物盒,里面分成一个个小格子,每个格子大小一致。
//这样意味着,格子的大小要和最大的数据类型所占的空间一样,每个格子有可能塞不满
  1. 行宏(__LINE__)和文件宏(__FILE__)
1
printf("line = %d,file = %s", __LINE__, __FILE__);

C99标准

  1. 增加了inline关键字

可以把函数指定为内联函数,这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。

  1. 增加了restrict关键字
1
2
3
4
5
6
7
8
//没有添加restrict之前,可以通过任意指针变量去改变指针所指向的值
int* p = (int*)malloc(10*sizeof(int));
int* p1 = p;
p1[0] = 10;
p1[1] = 20;
//添加之后只能通过这个变量去改变指针所指向的值
int* restrict p = (int*)malloc(10*sizeof(int));
p[0] = 30;
  1. 增加了_Complex关键字,来表示复数

这个没有用过,也许有些行业有用吧?

1
2
3
4
5
6
7
8

float _Complex a; /* 该变量包含两个float值,实部和虚部 */
double _Complex b; /* 与上述类似 */
long double _Complex c;
//使用<complex.h>头文件可以更方便的完成复数问题(虚数单位i写作大写的I):
double complex a = 1.3; /* 1.3 */
double complex b = 2.3 + 4 * I; /* 2.3+4i */
double complex c = 5.3 * I; /* 5.3i */
  1. 增加了_Imaginary关键字,来表示虚数

同上

1
2
3
4
5
6
float _Imaginary a;       /* 虚数类型的实部为0 */
double _Imaginary b; /* 与上述类似 */
long double _Imaginary c;

//使用<complex.h>头文件可以更方便的完成复数问题(虚数单位i写作大写的I):
double imaginary c = 5.3 * I; /* 5.3i */
  1. 增加了一种新的数据类型:_Bool类型
1
2
3
4
5
//_Bool类型长度为1,只能取值范围为0或1
_Bool a = 1;
_Bool b = 2; /* 使用非零值,b的值为1 */
_Bool c = 0;
_Bool d = -1; /* 使用非零值,d的值为1 */
  1. 支持long long 数据类型

  2. 变量声明不必放在语句块开头

1
2
3
4
5
6
7
8
9
10
11
int fun()
{
printf("fun\n");
int a;
printf("a\n");
a = 10;
}
for(int i=0;i<10;i++)
{
//i在括号内有效,出了括号作用域无效
}
  1. 增加了函数宏(__func__)
1
printf("fun = %s", __func__);
  1. 取消了不写函数返回类型默认就是 int 的规定

  2. 增加和修改了一些标准头文件

定义 bool 的 <stdbool.h>

定义一些标准长度的 int 的 <inttypes.h>

定义复数的 <complex.h>

定义宽字符的 <wctype.h>

有点泛型味道的数学函数 <tgmath.h>

跟浮点数有关的 <fenv.h>

< stdarg.h> 里多了一个 va_copy 可以复制 … 的参数

<time.h> 里多了个 struct tmx 对 struct tm 做了扩展

  1. 引入了long long int

C99标准中引进了long long int(-(2e63 - 1)至2e63 - 1)和unsigned long long int(0 - 2e64 - 1)。long long int能够支持的整数长度为64位。

C11标准

  1. 对齐处理

alignof(T)返回T的对齐方式,aligned_alloc()以指定字节和对齐方式分配内存,头文件<stdalign.h>定义了这些内容。

1
2
3
4
5
6
7
8
struct Foo {
int a;
float b;
char c;
};

alignof(Foo) //值为4,对齐长度
sizeof(Foo) //结构体的总大小:12
1
2
3
4
5
6
//void *aligned_alloc( size_t alignment, size_t size );
//分配 size 字节未初始化的存储空间,按照 alignment 指定对齐。 size 参数必须是 alignment 的整数倍。
//aligned_alloc 是线程安全的
int *p2 = aligned_alloc(1024, 10*sizeof *p2);
printf("1024-byte aligned addr: %p\n", (void*)p2);
free(p2);
  1. _Noreturn

_Noreturn是个函数修饰符,位置在函数返回类型的前面,声明函数无返回值

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

// 在 i <= 0 时导致未定义行为
// 在 i > 0 时退出
noreturn void stop_now(int i) // 或 _Noreturn void stop_now(int i)
{
if (i > 0) exit(i);
}

int main(void)
{
puts("Preparing to stop...");
stop_now(2);
puts("This code is never executed.");
}
  1. _Generic

_Generic支持轻量级范型编程,可以把一组具有不同类型而却有相同功能的函数抽象为一个接口

1
_Generic( ‘a’, char: 1, int: 2, long: 3, default: 0) //与switch类似
  1. _Static_assert()

静态断言,在编译时刻进行。

1
2
3
4
5
6
7
8
9
10
#include <assert.h>
int main(void)
{
// 测试数学是否正常工作
static_assert(2 + 2 == 4, "Whoa dude!"); // 或 _Static_assert(...

// 这会在编译时产生错误。
static_assert(sizeof(int) < sizeof(char),
"this program requires that int is less than char");
}
  1. 匿名结构体、联合体

在 C 语言中,可以在结构体中声明某个联合体(或结构体)而不用指出它的名字,如此之后就可以像使用结构体成员一样直接使用其中联合体(或结构体)的成员。

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

struct person
{
char *name;
char gender;
int age;
int weight;
struct
{
int area_code;
long phone_number;
};
};

int main(void)
{
struct person jim = {"jim", 'F', 28, 65, {21, 58545566}};
printf("%d\n", jim.area_code);
}
  1. 多线程

头文件<threads.h>定义了创建和管理线程的函数,新的存储类修饰符_Thread_local限定了变量不能在多线程之间共享。

VS2019移除了对threads.h的支持,所以windows下不能用这个。

引用于使用C11新增的多线程支持库-threads.h进行多线程编程

上面提到VS主要目标是支持C++,对C的支持是次要的。

  1. quick_exit()

又一种终止程序的方式,当exit()失败时用以终止程序。

  1. time.h新增timespec结构体,时间单位为纳秒,原来的timeval结构体时间单位为毫秒。

struct timespec 定义:

1
2
3
4
5
6
7
8
typedef long time_t;
#ifndef _TIMESPEC
#define _TIMESPEC
struct timespec {
time_t tv_sec; // seconds
long tv_nsec; // and nanoseconds
};
#endif

C18标准

C18 没有引入新的语言特性,只对 C11 进行了补充和修正。

结尾

至此,对C语言标准的简单总结就完了。简单来看,其实从C89开始,其他并没有太大变化。下一篇将对C++标准做个简单总结。

评论