一、语言中常用关键字
- C语言关键字又称为保留字,就是已被C语言本身使用,不能作其它用途使用的字。
- 关键字是C语言提供的,不能自己创建关键字。
- 变量名不能是关键字。
C语言的关键字共有32个,根据关键字的作用,可分其为数据类型关键字(12个,红色)、控制语句关键字(12个,橙色)(循环控制5个,条件语句3个,开关语句3个,返回语句1个), 存储类型关键字(5个,蓝色,存储类型关键字在使用时只能存在一个,不能共存)和其它关键字(3个,黑色)。
1、enum
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
//颜色
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;
//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
枚举,顾名思义就是一一列举,以上定义的 enum Day, enum Color 都是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
- 枚举的优点:
- 增加代码的可读性和可维护性。
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)。
- 便于调试。
- 使用方便,一次可以定义多个常量, 而#define 宏一次只定义一个。
- #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
2、union
联合体类型,也是一种特殊的自定义类型。 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
union和struct结构较为类似。但是,union内部成员共享一个存储空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
同时,联合体总空间大小至少是最大成员的大小;要能整除其任意成员的空间大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算联合变量的大小
printf(“%d\n”, sizeof(un));
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
注意:要判断当前计算机的大小端存储。
3、auto
每个局部变量都是auto修饰的,但是一般都会省略
int main()
{
auto int a = 10;
//自动创建,自动销毁 -自动变量
// auto 省略掉了
}
4、register
计算机中数据可以存储到寄存器、高速缓存、内存、硬盘、网盘。
int main()
{
// 大量、频繁被使用的数据,放在寄存器中,提升效率
register int num = 100 ;
//建议num的值存放在寄存器中,是否存入,编译器说了算。
//即使不加该关键字,编译器一般也会放。
}
5、typedef
类型重命令,包含:
- 对一般类型进行重命名
- 对结构体类型进行重命名
- 对指针进行重命名
- 对复杂结构进行重命名
typedef unsigned int u_int;
int main()
{
unsigned int num = 100;
u_int num2 = 90;
return 0;
}
6、extern
extern 用来声明外部符号,可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
//a.c:
static int val = 2022;
//test.c:
extern int val;
int main()
{
printf(“%d\n”,val);
return 0;
}
7、static
在C语言中:用来修饰变量和函数。
7.1 修饰局部变量
改变了局部变量的生命周期。(本质上改变了变量的存储类型)
7.2 修饰全局变量
全局变量在整个工程中都i可以使用,全局变量在其它源文件内部可以被使用,是因为全局变量具有外部链接属性,但是被static修饰后,就变成了内部链接属性,其它源文件就不能链接到这个静态的全局变量了。
//a.c:
static int val = 2022;
//test.c:
extern int val;
int main()
{
printf(“%d\n”,val);
return 0;
}
7.3 修饰函数
使函数只能在自己所在的源文件内部使用,不能在其它源文件内部使用。
本质上:static是将函数的外部链接属性变成了内部链接属性!(和static修饰全局变量一样)
//a.c:
static int Add(int x,int y)
{
return x+y;
}
//test.c:
extern int Add(int x,int y);
int main()
{
int a = 10;
int b = 20;
int val = Add(a,b);
printf(“%d\n”,val);
return 0;
}
8、const
8.1 const修饰变量
变量不可直接被修改。但是却可以通过指针的解引来进行修改。
const修饰指针变量分为几种情况:
const int *p; // p 可变,p 指向的值不可变
int const *p; // p 可变,p 指向的值不可变
int *const p; // p 不可变,p 指向的值可变
const int *const p; //指针 p 和 p 指向的值都不可变
8.2 const修饰函数
const可以修饰函数参数,const 修饰符也可以修饰函数的返回值,返回值不可被改变。如:
const int Fun (void);
也可以在另一链接文件中引用 const 只读变量:
extern const int i; //正确的声明
extern const int j=10; //错误!只读变量的值不能改变
9、volatile
volatile 关键字和 const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问,因此,该关键字可以在定义硬件寄存器时使用。
在中断中使用的变量,如果要在主函数中进行调用,需要对该变量使用该关键字。
int i=10;
int j = i;
int k = i;
不加volatile时,这时候编译器对代码进行优化,因为在(1)、(2)两条语句中,i 没有被用作左值。这时候编译器认为 i 的值没有发生改变,所以在(1)语句时从内存中取出 i 的值赋给 j 之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给 k 赋值。这是一种内存被“覆盖”的情况。编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。
但要注意:(1)、(2)语句之间 i 没有被用作左值才行。
编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。
volatile int i=10;
int j = i;
int k = i;
volatile 关键字告诉编译器 i 是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从 i 的地址处读取数据放在k中。这样看来,如果 i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
二、C语言中常用预处理命令
预处理定义:
预处理是指在进行编译(词法扫描和语法分析)之前所作的工作。预处理是C语言区别于其他高级语言的特征之一, 它由预处理程序负责完成。当对一个源文件进行编译时, 系统自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。
C语言的预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件内容、定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译。
预处理分类:
文件包含、宏定义、条件编译
预处理作用:
合理地使用预处理功能编写的程序便于阅读、修改、 移植和调试,也有利于模块化程序设计。
1、文件包含
1.1 文件包含的定义
文件包含就是在预处理阶段,将#include 包含的文件,替换成原有的文件的内容。可以实现文件分离,声明和实现相分离,是程序更加的模块化。
#include指令使另一个文件的内容被编译,就好像它实际出现在#include指令出现的位置一样。这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。如果一个头文件被包含到十个源文件中,那它实际上被编译了十次。
1.2 文件包含格式
C语言编译器支持两种不同类型的#include文件包含:函数库文件和本地文件
a)函数库文件包含
#include <filename> 对于filename(文件名),并没有任何的限制,不过根据规定,标准库文件以一个.h后缀结尾。 编译器通过观察由编译器定义的“一系列标准位置”查找函数库头文件。所使用的编译器文档会说明这些标准的位置是什么,以及怎样修改它们或者在列表中添加其他位置。
b) 本地文件包含
#include "filename" 标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。可以先对本地头文件使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对待它们进行处理。
2、宏定义
2.1 宏定义的定义
#define叫做宏定义命令,它是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串。之后再出现该标识符,则全部替换成指定的字符串。
2.2 基本的宏定义语法
#define 宏名 字符串
例: #define PI 3.14159 // 定义常量PI的宏
注意:
- # 表示是预处理指令,C语言里面凡是以#开头的,都是预处理指令
- 宏名,一般使用大写,跟普通的变量区别
- 被替换的字符串,可以是常量、字符串、表达式
- 宏定义后面不要加 ;
- 程序中反复表达的表达式就可以使用宏定义
2.3 宏定义的注意事项和细节
- 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这是一种简单的替换。字符串中可以含任何字符,它可以是常数、表达式、if语句、函数等,预处理程序对它不作任何正确性检查。
- 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换
- 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止,其定义域可以使用#undef命令(#undef 宏名 )取消宏。
- 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
- 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换
- 可用宏定义表示数据类型(与typedef的区别?)
宏定义表示数据类型和用typedef定义数据说明符的区别:
宏定义只是简单的字符串替换,由预处理器来处理;而typedef是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
2.4 带参数的宏定义
- C语言允许带宏参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”
- 对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
- 带参宏定义的一般形式为:#define 宏名(形参列表)字符串 在字符串中可以含有各个形参
- 带参宏调用的一般形式为:宏名(实参列表)如:#define MAX(a, b) (a > b)? a : b //计算两数最大值的宏
//说明
//1.MAX 就是带参数的宏
//2.(a,b) 就是形参
//3.(a>b) ? a : b是带参数的宏对应字符串,该字符串中可以使用形参
#define MAX(a,b) (a>b) ? a : b
int main()
{
int x , y, max;
printf("input two numbers: ");
scanf("%d %d", &x, &y);
//说明
//1. MAX(x, y); 调用带参数宏定义
//2. 在宏替换时(预处理,由预处理器), 会进行字符串的替换,同时会使用实参, 去替换形参
//3. 即MAX(x, y) 宏替换后 (x>y) ? x : y
max = MAX(x, y);
printf("max=%d\n", max);
getchar();
getchar();
return 0;
}
注意事项和细节:
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现
- 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型
- 字符串内的形参通常要用括号括起来以避免出错
2.5 带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么在使用这个宏时就可能出现危险,导致不可预料的后果。副作用就是在表达式求值时出现永久性的后果。如下:
x + 1; 这个表达式无论执行几百次都是一样的,所以它没有副作用;
x++; 但是这个表达式就不同了,每次执行都会改变x的值,每一次执行都是一个不同的结果。所以,这个表达式是具有副作用的。
#define MAX(a, b) ((a) > (b) ? (a) : (b))
x = 5;
y = 8;
z = MAX(x++, y++);
printf(“x =%d, y = %d, z = %d\n”, x, y, z);
x = 6, y = 10, z = 9
产生这个结果的原因是那个较小的值只增加了一次,而那个较大的值却增加了两次——第一次是在比较的时候,第二次是在执行后面的表达式时。
这就是一个具有副作用的宏参数,我们在使用的时候一定要注意。
2.6 带参宏定义和函数的区别
宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数就是执行这块内存中的代码。
2.7 预定义符号(预定义的宏)
下表为C语言预处理器定义的符号。它们的值有的是字符串常量,有的是十进制数字常量。
#include<stdio.h>
#include<assert.h>
int main()
{
printf("%s\n",__FILE__);
printf("%d\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%s",__func__);
return 0;
}
2.8 预处理命令
#运算符
通常称为stringifie(字符串化)运算符,使得在它后面的变量转换成带双引号的串
#include <stdio.h>
#define mkstr(s) #s
int main(void)
{
printf(mkstr(I Like C));
return 0;
}
##运算符
称为 pasting(粘贴,链接)运算符,用于连接两个符号
#include <stdio.h>
#define concat(a,b) a##b
int main(void) {
int xy = 10;
printf("%d",concat(x,y));
// printf("%x",concat("hello","xa"));
return 0;
}
@运算符
功能是将其后面的变量进行字符化,VS环境下有效
#include<stdio.h>
#define TO_CHAR(x) #@x
int main()
{
printf("%c", TO_CHAR(1));
printf("%c", TO_CHAR(3));
printf("%c", TO_CHAR(4));
printf("%c", TO_CHAR(5));
return 0;
}
#undef
解除定义标识符 ,即它取消先前 #define 对标识符的定义。如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。若标识符无与之关联的宏,则忽略此指令。
#undef NAME
#line 与 #error
#include<stdio.h>
#include<assert.h>
#include <stdlib.h>
#line 100 "jie.c"
#error "编译出错"
#define ASSERT(exp) (void)((exp) || (my_assert(#exp,__FILE__,__LINE__,__func__),0))
void my_assert(const char * _Message,char * _file,int _line,char const* func)
{
printf("%s:",_file);
printf("%d ",_line);
printf("%s: ",func);
printf("Assertion \'%s\' failed \n",_Message);
abort();
}
void print( int * arr,int n)
{
ASSERT(arr!=NULL);
// assert(arr!=NULL);
for(int i = 0;i<n;i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
#ifdef ASSERT
printf("%d ",1);
#endif
int arr[] = {1,2,3,4,5,6,7,8};
int n = 8;
int * ip = NULL;
print(ip,n);
return 0;
}
编译时:
jie.c:100:2: error: #error "编译出错"
jie.c:111 print: Assertion 'arr!=NULL' failed 进程已结束,退出代码134
3、条件编译
3.1 条件编译的定义
条件指令允许你对程序源代码的各部分有选择地进行编译,这个过程称为条件编译。商业软件公司广泛应用条件编译来提供和维护某一程序的许多定制版本。比如说我们在项目工程中,会有一些调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);
//为了观察数组是否赋值成功。
#endif
}
return 0;
}
3.2 常见的条件编译指令
最常用的条件编译指令为#if,#else,#elif和#endif。这些指令允许根据常量表达式的结果,有条件地选择代码指令。
1. #if 常量表达式
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2. 多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
3.3 #ifdef和 #ifndef
条件编译的另一种方法使用#ifdef和#ifndef指令。它们分别表示 " 如果有定义 " 和 " 如果无定义"。
#include <stdio.h>
#define TED 10
int main (void)
{
#ifdef TED
printf("Hi Ted \n");
#else
printf("Hi anyone \n");
#endif
#ifndef RALPH
printf("RALPH not defined\n");
#endif
return 0;
}
三、C语言中常用修饰符
修饰符的用法与关键字相同,_Thread_local在C语言中会用到。