# 关键字
# 关键字数量
C89 因该是 32 个,C99 多出了 5 个,多出的 5 个如下
关键字 | 作用 |
---|---|
inline | 定义一个类的内联函数,引入它的主要原因是用它替代 C 中表达式形式的宏定义 |
restrict | 只用于限定指针,所有修改该指针所指向内容的操作全部都是基于 (base on) 该指针的,即不存在其它进行修改操作的途径 |
_Bool | 用于表示布尔值,引入 <stdbool.h> , 可以用 bool 代替 |
_Complex 和 Imaginary | 添加了复数类型 |
# 关于 sizeof 不得不说的那些事
其实挺致命的 sizeof 平常都是 sizeof () 这么用的,一直以为它是个函数,到了今天才知道它是个关键字
sizeof 不带括号也可以使用,sizeof (int) 和 sizeof int 没有什么区别,但是由于 sizeof () 用的多,建议用着一种形式
至于为什么 sizeof () 用的多我估计是和 不用 #define max(a,b) a>b?a:b
, 而用 #define max(a,b) (a)>(b)?(a):(b)
的原因是一致的
# 关于 声明 和 定义 不得不说的二三事
声明和定义是由明显区别的,体现在两个地方:内存 和 出现次数
- 在内存上,
定义必然占用内存空间
声明不一定占用内存空间,只有当被声明的对象被定义后才会占用空间
这一块的区别可以去看以先 汇编 上对内存的分配
测试代码如下:#include <stdio.h>
//extern int small = 0; /* extern 声明的 同时不能被定义 */
extern int big;
extern int x;
int main(void)
{
int x = 0;
int y;
for (int i = 0; i < 5; i++)
{
x += i * x;
}
return 0;
}
- 出现次数,
定义只能出现一次,要不然就是重定义,鬼知道编译器会给你优化成什么东西
声明能出现多次,编译器会认为当前的这写声明都是一个变量,一般用于多文件编译跨文件的变量
# register
这个关键字目的在于告诉编译器 把变量存放到 CPU 内部寄存器中,减少变量从内存中的读取次数来提升效率
当然,你定义是你定义,编译器听不听这你就管不着了。人家怎么开心怎么来
有个要注意的地方,register 变量可能不存放在内存中,所以不能用取址运算符 “&” 来获取 register 变量的地址
对了,寄存器是无法存入自定义类型的,所以传参记得传指针
# 寄存器
这玩意可亲切了,毕竟干嵌入式出身,CPU 内部有寄存器,这是唯一一个和 CPU 处理速度能搭上拍的存储器
但是由于造价高,所以数量少。但凡数据读取都会经过寄存器,哪怕在内存上读取,也是从内存到高速缓存,再到寄存器
至于为什么快,一是小,而是距离近,其他原因我也没法解释了
# static
# 修饰变量
变量又分为局部和全局变量,但它们都存在内存的静态区
- 静态全局变量,作用域仅限于变量被定义的文件中,从哪定义的就从哪开始,在定义之前的地方调用,就需要 用
extern
来声明 - 静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了.
由于被 static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值
# 修饰函数
修饰函数。函数前加 static 使得函数成为静态函数。但此处 “static” 的含义
不是指存储方式,而是指对函数的作用域仅局限于本文件
# 基本数据类型
# 什么是数据类型
数据类型其实就是一个模子,每定义一个变量就是用模子在内存里打一个相应大小的空间.
数据类型当然不仅如此,数据结构的定义就是数据及在数据上的操作,简单来说,除了有空间,还得有使用方法
# 变量命名规则
c 语言是一个精简高效的语言,所以我们变量名也要简洁有力,但是非常用英语词汇最好不要使用缩写,尤其是专业英语
# 作用域前缀命名规则
标识符类型 | 作用域前缀 |
---|---|
Global Variable | g |
File Static Variable(native) | n |
Function Static Variable | f |
Auto Variable | a |
Global Function | g |
Static Function | n |
# 数据类型前缀命名规则
Prefix | Suffix | Data Type | Example | Remark |
---|---|---|---|---|
bt | bit | Bit btVariable | ||
b | boolean | boolean bVariable | ||
c | char | char cVariable; | ||
i | int | int iVariable; | ||
s | short[int] | short[int] sVariable; | ||
l | long[int] | long[int] lVariable; | ||
u | unsigned[int] | unsigned[int] uiVariable; | ||
d | double | double dVariable; | ||
f | float | float fVariable; | ||
p | pointer | void *vpVariable; 指针前缀 | ||
v | void | void vVariable; | ||
st | enum | enum A stVariable; | ||
st | struct | struct A stVariable; | ||
st | union | union A stVariable; | ||
fp | function point | void(* fpGetModeFuncList_a[])( void ) | ||
_a | array of | char cVariable_a[TABLE_MAX]; | ||
_st _pst | typedefenum/struct/union | typedef struct SM_EventOpt { unsigned char unsigned int char }SM_EventOpt_st,*SM_EventOpt_pst; | 当自定义结构数据类型时使用_st 后缀;当自定义结构数据类型为指针类型时使用_pst 后缀; |
# 含义标识命名规则
变量命名使用名词性词组,函数命名使用动词性词组。
变量名 | 目标词 | 动词 (的过去分词) | 状语 | 目的地 | 含义 |
---|---|---|---|---|---|
DataGotFromSD | Data | Got | From | SD | 从 SD 中取得的数据 |
DataDeletedFromSD | Data | Deleted | From | SD | 从 SD 中删除的数据 |
PS: 变量含义标识符构成:目标词 + 动词 (的过去分词)+ [状语] + [目的地];
| 变量名 | 目标词 | 动词 (的过去分词) | 状语 | 目的地 | 含义 |
| ------ | ------ | ---------------- | ---- | ------ | ---- ||
| GetDataFromSD | Get | Data | From | SD | 从 SD 中取得数据 |
| DeleteDataFromSD | Delete | Data | From | SD | 从 SD 中删除数据 |
PS: 函数含义标识符构成:动词 (一般现时)+ 目标词 +[状语]+[目的地];
# 其他命名规则
- 程序中不得出现仅靠大小写区分的相似的标识符
- 一个函数名禁止被用于其它之处
# 再谈 sizeof
sizeof 不是函数
sizeof 不是函数
sizeof 不是函数
在计算 数据类型的时候不能省略 (), 在计算变量所占用空间的时候可以省略,建议都不要省略
# 思考
PS: 以下情况为 64 位操作系统,gcc-x86-64 环境下
sizeof(int) *p
的含义表示计算 int 型所占字节数,然后再乘以 p。当
int *p = NULL;
时,sizeof(p)
的值是 8,sizeof(*p)
的值是 [4].{.gap}。当
int a[100];
sizeof (a) 的值 400,sizeof(a[100])
的值 4,sizeof(&a)
的值 [8].{.gap}。当
int b[100];
void fun(int b[100]) | |
{ | |
sizeof(b); | |
} |
sizeof (b) 的值是 [8].{gap}。
# 关于 TRUE 和 FALSE
在 C99 标准中 添加了 bool 数据类型,多了两个宏 true
和 flase
但是 在单片机中 编译器可能不支持 <stdbool.h> 这个头文件,所以可以考虑使用 宏来替代
例如 #define TRUE 1
当然 如果 状态仅仅有 0 1 两种,下面的方法 会更加好一些
if (!state); // 为 0 执行 | |
if (state); // 非 0 执行 |
上面这种方法无需考虑, TRUE
和 FALSE
被 Keil 或者其他 编译器定义了,并且不是正常的数值
# 浮点的比较
在计算机中,数字都是离散的,所以浮点数的精度也是有限的,浮点变量在进行几次运算后,数值可能就产生了误差
这时候,对其进行 等价判别必然会产生判。例如:
#include <stdio.h> | |
#include <stdlib.h> | |
int main() | |
{ | |
float d1, d2, d3, d4; | |
d1 = 194268.02; | |
d2 = 194268; | |
d4 = 0.02; | |
d3 = d1 - d2; | |
if (d3 > d4) | |
printf(">0.02\n"); | |
else if (d3 < d4) | |
printf("<0.02\n"); | |
else | |
printf("=0.02\n"); | |
printf("%f - %f = %f \n", d1, d2, d3); | |
} |
输出结果:
< 0.02
194268.015625 - 194268.000000 = 0.015625
可以看出数据发生了明显的变化,所以在对浮点数进行比较的时候,务必设定一个精度范围
误差在这个范围即为相等.
例如:
if ((x >= - EPSINON) && (x <= EPSINON) |
核心就是: 浮点数都是有精度限制的,所以你存的数,不一定就是你要数
# 另一个条件分支: switch case
其实在很大程度上 if else
已经够用了,但是为了让逻辑更加清晰,以及便于编译器更好的优化代码
在看 <深入理解操作系统> 这本书的时候 我对 if else
和 switch case
分别生成的分支语句进行了反汇编
发现 后者 只有在 case
数量超过 4 的时候才会生成条件 转跳表 这个数据结构
所以建议是,当条件分支数 > 5 时,设置连续的 case 值,进行分支执行
# 高深莫测的 const
定义 const 只读变量,具有不可变性。
const 的出现是为了节省空间,取代 预编译指令。但是很显然,取代失败了。毕竟 宏函数 还是很香的
言归正传,
通常来说,编译器不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使
得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高
# const 修饰数组
const int arr[10]; | |
int const ary[10]; |
const 修饰的数组为,只读数组,不可修改数组内的元素
当然 数组名本身也是一个常量指针,所以 const 位于什么位置,对于 数组来说并没有什么区别
# const 修饰指针
对于 指针来说,const 的位置就很关键
const int *p; //p 可变,p 指向的对象不可变 | |
int const *p; //p 可变,p 指向的对象不可变 | |
int *const p; //p 不可变,p 指向的对象可变 | |
const int *const p; // 指针 p 和 p 指向的对象都不可变 |
这一块我就不细讲了,之前记录过,具体细节看这篇博客:( •̀ ω •́ )✧
const int *p; //const 修饰 * p,p 是指针,*p 是指针指向的对象,不可变 | |
int const *p; //const 修饰 * p,p 是指针,*p 是指针指向的对象,不可变 | |
int *const p; //const 修饰 p, p 不可变,p 指向的对象可变 | |
const int *const p; // 前一个 const 修饰 * p, 后一个 const 修饰 p, 指针 p 和 p 指向的对象 | |
都不可变 |
# const 修饰函数
# const 修饰函数参数
const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。例如:
void Fun(const struct DATE); |
告诉编译器 DATE
在函数体中的不能改变,从而防止了使用者的一些无意的或错误的修改
# const 修饰返回值
const 修饰符也可以修饰函数的返回值,返回值不可被改变.
例如:
const int Fun (void); | |
在另一连接文件中引用 const 只读变量: | |
extern const int i; // 正确的声明 | |
extern const int j = 10; // 错误!只读变量的值不能改变 |
# 反优化大师 volatile
这个关键字在 非嵌入式平台上十分少用. volatile 的作用是告诉 编译器不对这个变量进行优化,并提供该变量稳定的内存空间.
一般来说,在嵌入式中,会存在软延时,例如:
static void delay(volatile uint32_t timeout) | |
{ | |
while(timeout--); | |
} |
这时候如果删除 volatile
可能就会导致 timeout 无法访问,这个函数直接被优化为空,然后延时异常,尤其是在 CCS 上编写 MSP 系列单片机的时候,所有变量都需要加上 volatile
.
对于这种情况建议是直接 typedef 一下 typedef volatile uint8_t vu8_t;
方便使用,避免重复劳动
# 跨国护照 extern
为什么说是跨国护照呢?extern 用于扩大 函数,变量的作用域,当一个变量需要跨文件的时候,就需要,用 extern 来声明,一般放在头文件中.
这不就是护照,每个要使用的文件就得包含整个声明,这部就像是签证?
例如:
extern struct GPIO_TypeDef GPIO1; // 作用域为包含了这个 头文件的所有文件 | |
void GPIO_WritePin(GPIO_TypeDef *base, uint8_t pin, uint8_t state); | |
extern void GPIO_WritePin(GPIO_TypeDef *base, uint8_t pin, uint8_t state); | |
// 函数默认为 自带 extern 属性,都为跨文件作用域. |
需要注意的是,extern 是声明,而非定义。这时候对变量进行增删查改,任何一个操作都会导致程序异常,所以在 extern 某个变量后需要手动 定义一下这个变量,才能对变量进行操作
# struct
在我看来,如果 C 的灵魂是指针,那么 struct 就是灵魂的载体,有了 strcut 之后,我们就无需局限在基本数据类型上,可以实现自己的数据类型,实现相关结构体,当然要注意的是,这玩意一般传指针,传结构体过于浪费运存,不建议使用
# 空结构体大小
这里有一个很有意思的问题,如果一个结构体如下:
struct stu{ | |
}st |
那么 sizeof(st)
的大小是多少呢?书中说是 1 , 但我在 vscode + 64 位的 GCC 编译器下 编译出是 0
arm-linux-gnueabihf-gcc-6.2.1 也是 0
所以我觉得可能是编译器版本不同,以及 VC6.0 编译器的自带的一些配置才出现这个问题
PS: 这个问题得先放一放有空再去深究
# 柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员
例如:
// 声明 | |
typedef struct st_type | |
{ | |
int i; | |
int a[0]; | |
} type_a; | |
// 使用 | |
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); |
这个操作像极了 int *a = new int [100];
, (间接证明了 C 语言是可以实现 高级语言的一些操作的,只不过会繁琐一些)
# 类?不类!
struct 和 class 的最大区别在于 Class 对数据进行了封装,非 public 下的成员函数和成员变量,无法进行调用或访问
在 structurt 中,成员函数还是可以使用 函数指针来模拟
# 勤俭持家小能手 union
union 与 struct 的用法非常类似。
union 维护足够的空间来置放多个数据成员中的 一种,而不是为每一个数据成员配置空间
在 union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
例子如下:
union StateMachine | |
{ | |
char character; | |
int number; | |
char *str; | |
double exp; | |
}; |
一个 union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大
长度是 double 型态,所以 StateMachine 的空间大小就是 double 数据类型的大小.
union 一般用于 缩减内存大小,当某些数据不会被同时访问的时候可以考虑使用 union
# 大小端问题
- 大端模式(Big_endian): 字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。
- 小端模式(Little_endian): 字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中
如何检测大小端:
#include <stdio.h> | |
#include <stdlib.h> | |
int main(void) | |
{ | |
union | |
{ | |
short n; | |
char c[sizeof(short)]; | |
} un; | |
un.n = 0x0102; | |
if ((un.c[0] == 1 && un.c[1] == 2)) | |
// 判断这种情况是大端还是小端 | |
printf("大端模式!\n"); | |
else if ((un.c[0] == 2 && un.c[1] == 1)) | |
// 判断这种情况是大端还是小端 | |
printf("小端模式!\n"); | |
else | |
printf("error!\n"); | |
return 0; | |
} |
# enum: 要我有何用?
一开始学枚举的时候,我也觉得枚举并没有什么作用,因为 宏可以干它能干的事情,后来学了状态机。开始整改代码风格的时候,算是理解了 enum
的作用.
# #define 和 enum 的区别
- #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值,这里就注定了,宏不可以被调试
- 枚举可以一次定义大量的常量,并且视为一种数据类型,无论是在作为返回值还是作为状态参数的时候,都可以使得代码清晰易懂.
- enum 定义的枚举类型 视为一类状态的集合。例如 rtthread 的 rt_error 枚举。能清晰的标识
- sizeof () 枚举变量是整型大小
# Nvidia 的同门师兄弟 typdef
当年的 Nvidia 最喜欢干的就是 套马甲,typedef 也是如此.
typedef 在官方 的定义上是不会创造新的数据类型的,那么疑问就来了
typedef struct { | |
uint16_t ODR; | |
uint16_t IDR; | |
... | |
} GPIO_TypeDef; |
这种又是什么情况呢?其实这属于定义以一个没有名字的结构体,然后重命名为 GPIO_TypeDef, 故称为 套马甲
# 又双叒叕是 #define
#define
总是 喜欢和 其它关键字抢饭吃。所以大部分 C 语言程序员都离不开 宏定义.typedef
和 #define
区别还是那句话, #define
是等效替代.typedef
, static
, auto
, register
都属于存储类关键字,所以不能重叠使用,这个时候考虑使用 #define
.
例如:
#define INT32 int | |
unsigned INT32 i = 10; |
有些时候必须得使用 typedef
. 例如,在重命名指针数据类型的时候#define
格式如下:
#define PCHAR char* | |
PCHAR p3,p4; //p4 不是指针 |
typedef
格式:
typedef char* pchar; | |
pchar p1,p2; |
到这重要的关键字总结完毕,以后想起或者遇到什么有趣的应用,再继续完善
以上 都是本人基于 <<C 语言深度剖析>> 的看法和个人观点,如有不当,恳请斧正
大道五十,天衍四十九,人遁其一