深刻理解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标准最大的不同在于函数定义:

// 参数名在圆括号指定
//参数类型在左花括号之前声明
//如果没有声明某个参数的类型,则默认为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. 所有变量都必须先声明后使用,声明必须在语句块开头,任何可执行语句之前。
// 正确的示范
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. 引入了++运算符和–运算符
main()
{
	int i;
	i = 0;
	printf("%d\n",i++);
	printf("%d\n",i--);
    printf("%d\n",++i);
	printf("%d\n",i);
}
  1. 外部变量和内部变量,外部变量即全局变量,内部变量即局部变量
//内部变量,只有在函数被调用的时候存在,函数执行完消失
int fun()
{
    int i;//内部变量
    i=0;
}
//外部变量,意思是在函数外部定义的变量,程序执行期间一直存在,可以被任何函数访问
//例如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. 字符串常量可以连接起来
char* s = "hello,"
    	  "world";
//等价于
char* s = "hello,world";
//不管引号中间有空格或者换行都可以连接起来
  1. const 用来限定指定的内存空间不被修改

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

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修饰外部变量时,意味着这个变量只能在该源文件中使用。

//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修饰内部变量时,意味着该变量只能被该函数使用。

//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,比较灵活。数组不支持动态分配内存,所以比较死板。

#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。那么我们在程序中就能接收到这三个值。

#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. 函数指针

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

int fun1(int a,int b);//函数声明
int (*fun1)(int a,int b);//定义函数指针
  1. typedef
//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. 联合体

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

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

C99标准

  1. 增加了inline关键字

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

  1. 增加了restrict关键字
//没有添加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关键字,来表示复数

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


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关键字,来表示虚数

同上

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类型
//_Bool类型长度为1,只能取值范围为0或1
_Bool a = 1;
_Bool b = 2;    /* 使用非零值,b的值为1 */ 
_Bool c = 0;
_Bool d = -1;   /* 使用非零值,d的值为1 */
  1. 支持long long 数据类型

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

int fun()
{
	printf("fun\n");
	int a;
	printf("a\n");
	a = 10;
}
for(int i=0;i<10;i++)
{
    //i在括号内有效,出了括号作用域无效
}
  1. 增加了函数宏(__func__)
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>定义了这些内容。

struct Foo {
     int a;
     float b;
     char c;
};

alignof(Foo)    //值为4,对齐长度
sizeof(Foo) //结构体的总大小:12
//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是个函数修饰符,位置在函数返回类型的前面,声明函数无返回值

#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支持轻量级范型编程,可以把一组具有不同类型而却有相同功能的函数抽象为一个接口

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

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

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

 #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 定义:

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++标准做个简单总结。

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值