# C 语言语法

# static 用法

限制作用域,存储方式变为静态存储

  1. 修饰全局变量,限制作用域。在链接过程中,多文件中的重名变量不会冲突.
  2. 修饰局部变量,延长生命周期,存储方式变为静态存储只进行一次初始化.
  3. 修饰函数,限制函数作用域,局限在文件内部。在连接过程中避免和多文件中同名函数冲突

# #defineconst 的区别

  1. const 定义的常量在程序运行过程中只有一份拷贝 (因为它是全局的只读变量,存放在静态区), 而 #define 定义的宏常量在内存中有若干个拷贝 (没有存空间)
  2. #define 宏是在预编译阶段进行替换,而 const 修饰的只读变量是在编译的时候确定其值
  3. #define 宏没有类型,而 const 修饰的只读变量具有特定的类型
  4. #define 不能被调试, const 可以被调试

# #undef

  • 作用: #undef 用于取消前面的定义的宏标识符
  • 常见用法:
  1. 防止宏定义冲突
#include <stdio.h>
 
int main()
{
#define MAX 200
printf("MAX = %d\n", MAX);
#undef MAX
 
    int MAX = 10;
    printf("MAX = %d\n", MAX);
 
    return 0;
}
  1. 用于限制宏的作用范围
#define TEST_A 1 
#define TEST_CLASS_A clase T1 
#include "TEST.h" 
#undef TEST_A 
#undef TEST_CLASS_A

这段代码表示 TEST_A 1TEST_CLASS_A clase T1 在出 TEST.h 文件后无效
可以有效的减少宏的冲突

# 枚举转字符

相对于 宏定义来说,枚举变量更为利于调试,但是大量的枚举变量,很难让人记住,并且对于枚举值来说,使用 switch case 来实现打印出枚举变量,实在是有些费时费力.
作为一个程序员我们追求的就是高效与简洁的艺术 (就是偷懒), 所以我们要找到一种更高效的方法,宏编程

在这之前需要知道 #k 会把 k 变为字符串

#define MACROSTR(k) #k
// 定义枚举成员
#define X_NUMBERS \
  X(kZero = 10)   \
  X(kOne = 20)    \
  X(kTwo = 30)    \
  X(kThree = 40)  \
  X(kFour = 50)   \
  X(kMax = 60)
// 定义枚举类型
enum
{
#define X(Enum) Enum,
  X_NUMBERS
#undef X
} kConst;
// 定义枚举类型所对应的字符串
const static char *kConstStr[] = {
#define X(String) MACROSTR(String),
    X_NUMBERS
#undef X
};

输出结果如下:

Hello World!

kZero = 10
kOne = 20
kTwo = 30
kThree = 40
kFour = 50
kMax = 60

看结果只能知道 枚举类型被转成了字符,所以需要 使用 gcc -E -o main.c main.i
看一下预编译后的结果
如下

# 1 "enumToStr.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "enumToStr.c"
# 11 "enumToStr.c"
enum
{
  kZero = 10, kOne = 20, kTwo = 30, kThree = 40, kFour = 50, kMax = 60,
} kConst;
static char *kConstStr[] = {
    "kZero = 10", "kOne = 20", "kTwo = 30", "kThree = 40", "kFour = 50", "kMax = 60",
};
...

很明显的看到 枚举被转换成了字符串,但是仍然存在一些问题字符串打印会带上数字,数值不一定的枚举,无法直接作为索引来查找字符串进行打印

# union

union 最大的作用是用于节省空间,例如

而现在配合上结构体和 C99 的位字段,则可以进一步放大优势,例如

typedef union
{
    struct
    {
        unsigned short net_work : 1;   // 1-OneNET 接入成功		0-OneNET 接入失败
        unsigned short err_count : 3;  // 错误计数
        unsigned short heart_beat : 1; // 心跳
        unsigned short get_ip : 1;     // 获取到最优登录 IP
        unsigned short lbs : 1;        // 1 - 已获取到了位置坐标
        unsigned short lbs_count : 3;  // 获取计数
        unsigned short connect_ip : 1; // 连接了 IP
        unsigned short err_check : 1;  // 错误检测
        unsigned short reverse : 4;
    } bit;
    unsigned short all;
} OneNet_Statue_t;

假设我们 一个变量 OneNet_Statue_t oneNet_SR; 我们可以这样子对变量进行赋值 oneNet_SR.all=0x0010;

当我们要检测变量的某一个位的时候,就可以使用 network = oneNet_SR.net_work 来获取状态

# memmovememcpy 的区别

首先 memcpymemmove 都是 c 语言库函数,位于 string.h 中的函数.
其函数原型分别为

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);

作用都是拷贝一段内存的内容,到目的地址。区别在于,在内存重叠时, memmove 可以保证数据的正确复制,而 memcpy 不可以
情况如图
memmove
对于第一种情况 memcpy 可以完美解决,但是对于后面这两种 memcpy 就无法保证复制结果.
所以 memcpy 实现方法因该如下:

void* my_memcpy(void* dst, const void* src, size_t n)
{
    char *tmp_d = (char*)dst;
    char *tmp_s = (char*)src;
 
    while(n--) {
        *tmp_d++ = *tmp_s++;
    }
    return dst;
}

对 其进行分类讨论则可得到 memmove

void* my_memmove(void* dst, const void* src, size_t n)
{
    char *tmp_d = (char*)dst;
    char *tmp_s = (char*)src;
    if (tmp_d > tmp_s && tmp_s+n > tmp_d)
    { // 内存重叠
      tmp_d = tmp_d+n-1;
      tmp_s = tmp_s+n-1;
      while (n--){ // 先将回被重叠的地方存入目的地址
        *tmp_d-- = *tmp_s--;
      }
    }
    else
    { 
      while(n--) {
          *tmp_d++ = *tmp_s++;
      }
    }
    return dst;
}

# 柔性数组 (0 长数组)

这道题是我唯一不确定的题目,应为用到的太少了 (个人)
题目大致如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Data_t
{
    char value;
    int temp;
    short len;
    char arr[0];
} Flex_Array_t;
int main(void)
{
    int len = 2;
    char *buf = (char *)malloc(sizeof(Flex_Array_t) + len);
    for (int i = 0; i < sizeof(Flex_Array_t) + len; i++)
    {
        buf[i] = i;
    }
    Flex_Array_t *Array = buf;
    printf("array.value = %x\n", Array->value);
    printf("array.temp = %x\n", Array->temp);
    printf("array.len = %x\n", Array->len);
    printf("array.arr[0] = %x\n", Array->arr[0]);
    printf("array.arr[1] = %x\n", Array->arr[1]);
    return 0;
}

这里考了两个知识点,一个是字节对齐,另一个是柔性数组.
在 32 位机的条件下,4 字节对齐,所以大小是 1+4+2 +(3+2)= 12 字节,0 长数组不占空间,
0 长数组,在 ISO 标准里是不支持的,但是 gcc 在 c99 中预先提供了支持。相关文档再此:"Arrays of Length Zero."
我感觉 0 长数组的结构体其实就和 extern int arr[] 极其类似 (不过没有跨文件作用域的效果), 在没有定义变量之前都不会产生存储, sizeof 的结果下他们并不占用空间。对于 Flex_Array_t 也是同一个道理.
其实,严格意义上来说,0 长就类似数组首地址,而数组的首地址仅仅是一个标签,不占用空间,例如

#include <stdio.h>
int arr[10];
int main(int argc, char const *argv[])
{
    arr[9] = 0xff;
    return 0;
}

其汇编代码如下
ASM_Array
arr 存储在 为一个内存标签,和 main 标签作用类似 (.comm 是声明未初始化的内存段空间)
PS: 这也是指针和数组首地址的最大不同

回到题目,这道题目在弄清楚这两个问题和就变简单了, buf 的大小为 14, 那么 buf 被 赋给 Array 的时候,就是从 arr[0] 开始赋值到 arr[13] 为止

那么就可以先得出 Array.arr[0]Array.arr[1] 分别为 0x0a0x0b , 因为结构体的数组不占空间,多出的那 len 必然是给 arr[0] 的.
余下的就很好推断了 Array.value = 0 , 补齐 3 字节, Array.temp = 0x07060504 , int 本为 4 字节不用补齐. Array.len = 0x0908 , short 类型补齐 2 字节.
嗯?!到这里就有一个大问题了既然补齐了 short 两个字节,那么为什么 arr 数值不是未知数呢?

前面说过 柔性数组 大小为 0, 我们开辟的空间大小为 结构体大小大再多上 2 字节.
我们使用 malloc 函数开辟出的空间是连续的所以对于 柔性数组来说,不管前面补没补齐,都从上一个数据类型结束初开始计算,算是一个 c99 的特性吧.
具体原因我以后在琢磨琢磨.

# do{..}while(0) 宏定义中的作用

------------20230615----------------

曾经见到过这样的宏

#define __set_task_state(tsk, state_value)      \
    do { (tsk)->state = (state_value); } while (0)

一度不知道为什么要用 do{}while(0) 这种结构,在我看来它也可以这样实现

#define __set_task_state(tsk, state_value) (tsk)->state = (state_value);

但是今天再找文献的时候发现一篇有意思的文章原文链接刷新的我对这个写法的认知

来自 Google 的一位 linux 内核开发工程师是这么说的

do{...}while(0) is the only construct in C that lets you define macros that always work the same way

大致意思是 do {...} while (0) 是 C 中唯一允许您定义始终以相同方式工作的宏的构造

来看个例子,如果有这么个宏

#define foo(x)  bar(x); baz(x)
// 这样使用
foo(wolf);

这会被转译为:

bar(wolf); baz(wolf);

能够正常执行,但是这种情况

if (!feral)
    foo(wolf);

将会被解释为

if (!feral)
    bar(wolf);
baz(wolf);

很显然,这是不符合我们的逻辑的,它没有办法和 do {} while (0) 一样正常工作

如果我们把这个宏重用 do {...} while (0), 重新定义

#define foo(x)  do { bar(x); baz(x); } while (0)

该语句功能上等价于前者,do 能确保大括号里的逻辑能被执行,而 while (0) 能确保该逻辑只被执行一次,即与没有循环时一样
语句被解释为

if (!feral)
    do { bar(wolf); baz(wolf); } while (0);
Semantically, it's the same as:

从本质上的效果来说,上面等价于下面这段代码的逻辑

if (!feral) {
    bar(wolf);
    baz(wolf);
}

那么为啥不用 {} 直接括起来呢

#define foo(x)  { bar(x); baz(x); }

这对于上面举的 if 语句的确能被正确扩展,但是针对下面这段代码就会出错

if (!feral)
    foo(wolf);
else
    bin(wolf);

对应拓展为

if (!feral) 
{
    bar(wolf);
    baz(wolf);
};
else
    bin(wolf);

很显然,存在语法错误.
至于循环的问题,其实不用担心,一般来说,while 都会被编译器优化

# 一些思考

其实认真一想,如果我只有一条语句也给 if 或者 while 加上括号
那么,对于 #define foo(x) { bar(x); baz(x); } 这个宏
在下面这段代码也是正常运行的

```C
if (!feral) 
{
    {
        bar(wolf);
        baz(wolf);
    }   ;
}
else
{ 
    bin(wolf);
}

是不是突然在想这个东西的意义是啥了 (狗头), 其实认真想一想,咱们写代码除了自己用,还会给别人使用
你永远不知道你的队友使用哪种风的编码方式,这也是 Google 那位大佬想要表示的本意:
无论你怎么调用这种风格的宏它都不会出现语法错误

# 编译器特性

# 条件分支和循环分支 (可能传谣)

工作的时候听到前辈说: while 不加括号只有分号会导致重起,if 语句也是
就类似这样的代码

/* while 部分 */
//...
while(condition)
  ;
/* if 部分 */
//...
if (condition)
  ;

我去追问了一下,但是他们说没办法复现,是之前遇到的问题,上 google 和 Bing 逛了一圈没发现相对应的 bug
不过还是先记录下来,毕竟公司用的 ADS 1.2, 可能是编译器的问题,一切检测还是村寻 ANSI C
自己再 Keil 的 ARMCCV5 和 ARMCLANGV6 中都暂时未发现对应的问题,等后面如果遇到,那就用对应的编译器生成汇编看看

------------20230615----------------


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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

黑羊 支付宝

支付宝