# 关键字

# 关键字数量

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 Variableg
File Static Variable(native)n
Function Static Variablef
Auto Variablea
Global Functiong
Static Functionn

# 数据类型前缀命名规则

PrefixSuffixData TypeExampleRemark
btbitBit btVariable
bbooleanboolean bVariable
ccharchar cVariable;
iintint iVariable;
sshort[int]short[int] sVariable;
llong[int]long[int] lVariable;
uunsigned[int]unsigned[int] uiVariable;
ddoubledouble dVariable;
ffloatfloat fVariable;
ppointervoid *vpVariable; 指针前缀
vvoidvoid vVariable;
stenumenum A stVariable;
ststructstruct A stVariable;
stunionunion A stVariable;
fpfunction pointvoid(* fpGetModeFuncList_a[])( void )
_aarray ofchar cVariable_a[TABLE_MAX];
_st _psttypedefenum/struct/uniontypedef struct SM_EventOpt { unsigned char unsigned int char }SM_EventOpt_st,*SM_EventOpt_pst;当自定义结构数据类型时使用_st 后缀;当自定义结构数据类型为指针类型时使用_pst 后缀;

# 含义标识命名规则

变量命名使用名词性词组,函数命名使用动词性词组。

变量名目标词动词 (的过去分词)状语目的地含义
DataGotFromSDDataGotFromSD从 SD 中取得的数据
DataDeletedFromSDDataDeletedFromSD从 SD 中删除的数据

PS: 变量含义标识符构成:目标词 + 动词 (的过去分词)+ [状语] + [目的地];

| 变量名 | 目标词 | 动词 (的过去分词) | 状语 | 目的地 | 含义 |
| ------ | ------ | ---------------- | ---- | ------ | ---- ||
| GetDataFromSD | Get | Data | From | SD | 从 SD 中取得数据 |
| DeleteDataFromSD | Delete | Data | From | SD | 从 SD 中删除数据 |
PS: 函数含义标识符构成:动词 (一般现时)+ 目标词 +[状语]+[目的地];

# 其他命名规则

  • 程序中不得出现仅靠大小写区分的相似的标识符
  • 一个函数名禁止被用于其它之处

# 再谈 sizeof

sizeof 不是函数
sizeof 不是函数
sizeof 不是函数
在计算 数据类型的时候不能省略 (), 在计算变量所占用空间的时候可以省略,建议都不要省略

# 思考

PS: 以下情况为 64 位操作系统,gcc-x86-64 环境下

  1. sizeof(int) *p 的含义表示计算 int 型所占字节数,然后再乘以 p

  2. int *p = NULL; 时, sizeof(p) 的值是 8, sizeof(*p) 的值是 [4].{.gap}。

  3. int a[100]; sizeof (a) 的值 400, sizeof(a[100]) 的值 4, sizeof(&a) 的值 [8].{.gap}。

  4. int b[100];

void fun(int b[100])
{
  sizeof(b);
}

sizeof (b) 的值是 [8].{gap}。

# 关于 TRUE 和 FALSE

在 C99 标准中 添加了 bool 数据类型,多了两个宏 trueflase
但是 在单片机中 编译器可能不支持 <stdbool.h> 这个头文件,所以可以考虑使用 宏来替代
例如 #define TRUE 1
当然 如果 状态仅仅有 0 1 两种,下面的方法 会更加好一些

if (!state);  // 为 0 执行
if (state);   // 非 0 执行

上面这种方法无需考虑, TRUEFALSE 被 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 elseswitch 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 的区别

  1. #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值,这里就注定了,宏不可以被调试
  2. 枚举可以一次定义大量的常量,并且视为一种数据类型,无论是在作为返回值还是作为状态参数的时候,都可以使得代码清晰易懂.
  3. enum 定义的枚举类型 视为一类状态的集合。例如 rtthread 的 rt_error 枚举。能清晰的标识
  4. 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 语言深度剖析>> 的看法和个人观点,如有不当,恳请斧正


大道五十,天衍四十九,人遁其一