选中内容(绿色)时除了会搜索文章名,还会搜索文章内容
点击结果中的文章名进入文章界面后可以按Ctrl+F在页面内搜索
  • 版权:CC BY-SA 4.0
  • 创建:2019-10-13
  • 更新:2020-07-24
C语言常用容易出错的用法


结构体初始化

  1. typedef struct
  2. {
  3. uint8_t sck;
  4. uint8_t miso;
  5. uint8_t mosi;
  6. uint8_t cs;
  7. uint8_t cpol;
  8. uint8_t cpha;
  9. }spi_simulate_config_typedef;
  10. spi_simulate_config_typedef spi = {
  11. .cpol = 0,
  12. .cpha = 0
  13. }
  14. spi_simulate_config_typedef spi2 = {
  15. cpol: 0,
  16. cpha: 0
  17. }

结构体最后一个成员的用法

  1. typedef struct{
  2. int a;
  3. float b;
  4. int *c;
  5. } a_t;
  1. typedef struct{
  2. int a;
  3. float b;
  4. int c[];
  5. } a_t;
  1. typedef struct{
  2. int a;
  3. float b;
  4. int c[0];
  5. } a_t;

三者等效,可以使用如下方式来分配空间

  1. a_t*p = malloc(sizeof(a_t) + sizeof(int)*8);

另外需要注意,如果最后一个元素定义是如int c[]的形式,则下面的代码无法编译通过

  1. typedef struct{
  2. int a;
  3. int b[];
  4. }a_t;
  5. int main()
  6. {
  7. a_t p;
  8. int c[5];
  9. p.b = c;
  10. return 0;
  11. }

使用int* c则可以

  1. typedef struct{
  2. int a;
  3. int* b;
  4. }a_t;
  5. int main()
  6. {
  7. a_t p;
  8. int c[5];
  9. p.b = c;
  10. return 0;
  11. }

数组的初始化方式

  1. #include "stdio.h"
  2. enum{ENUM1=0,ENUM2,ENUM3};
  3. int main() {
  4. char array0[3]={1,2,3};
  5. char array1[3]={
  6. [0] = 1,
  7. [1] = 2,
  8. [2] = 3
  9. };
  10. char array2[3]={
  11. [ENUM1] = 1,
  12. [ENUM2] = 2,
  13. [ENUM3] = 3
  14. };
  15. for(int i=0;i<3;++i)
  16. printf("%d\n",array0[i]);
  17. for(int i=0;i<3;++i)
  18. printf("%d\n",array1[i]);
  19. for(int i=0;i<3;++i)
  20. printf("%d\n",array2[i]);
  21. return 0;
  22. }

另外,你可能会遇到一种奇怪的写法,建议使用:

  1. char array2[4]={
  2. #define NUMBER0 (10)
  3. ENUM1,
  4. ENUM2,
  5. ENUM3
  6. };

可控储存的结构体,占位符的使用

  1. typedef struct
  2. {
  3. enum
  4. {
  5. TEST_FRAME_VER_0 = 0,
  6. TEST_FRAME_VER_MAX,
  7. } version : 8;
  8. uint8_t gateway : 1;
  9. uint8_t fragment : 1;
  10. enum
  11. {
  12. TEST_FRAME_TYPE_DISCOVER = 0,
  13. TEST_FRAME_TYPE_LINK = 1,
  14. TEST_FRAME_TYPE_DATA = 2,
  15. TEST_FRAME_TYPE_MAX,
  16. } type : 2;
  17. enum
  18. {
  19. TEST_FRAME_SUBTYPE_DISCOVER_REQ = 0,
  20. TEST_FRAME_SUBTYPE_DISCOVER_RESP = 1,
  21. TEST_FRAME_SUBTYPE_DISCOVER_MAX
  22. } subtype : 4;
  23. uint8_t resv : 8;
  24. uint8_t dest_length_type : 4;
  25. uint8_t src_length_type : 4;
  26. void * raw;
  27. } __attribute__((aligned(1),packed)) test_frame_t;
  28. uint8_t frame[4];
  29. test_frame_t test_frame;
  30. memcpy(frame,test_frame,4);

C99中数组定义

C99开始可以使用变量来定义数组长度
eg:

  1. int num=10;
  2. int array[num];

但是也要注意,需要局部变量时使用,空间实在栈上分配的。
在 c99 之前,可以在函数内使用 alloca函数来动态从栈上获取不定长度的空间

keil MDK5也可以使用这个哦,不过在编译选项里面加一个—gnu参数就可以了

keil gnu parameter

强制转换问题

我们常常使用各种指针指过去指过来、强制转换过去转换过来、飞过去飞过来~~~然后就飞不见了!!!!!
案例1:强制转换导致越界访问,这种情况下如果编译器检查不严格很难发现,如果检查严格,会出现非法访问地址错误

  1. #include "stdio.h"
  2. #include "stdint.h"
  3. int main()
  4. {
  5. uint8_t a = 10;
  6. void* p = (void*)&a;
  7. uint16_t b = (uint16_t)(*((uint8_t*)p));
  8. uint16_t c = *((uint16_t*)&p);
  9. printf("a=%d,b=%d,c=%d\n",a,b,c);
  10. }

正确的是b的写法,c是错误的
输出结果

一种函数的神奇写法

  1. int plus (a,b)
  2. int a;
  3. int b;
  4. {
  5. return a+b;
  6. }

在一些老版程序中会出现,现在不建议这样用了,但是有可能会遇到别人这样写!!

内存越界访问问题

比如

  1. #include <stdio.h>
  2. #include <stdbool.h>
  3. #include <stdint.h>
  4. int main()
  5. {
  6. uint64_t a = 1024;
  7. uint16_t b;
  8. uint32_t* c = &b;
  9. *c = a;
  10. printf("%d %d %d\r\n", sizeof(a), sizeof(b), sizeof(*c));
  11. return 0;
  12. }

这里将一个使用了强制转换,在编译的时候会报警告,如果不理会强制运行则会导致内存越界访问,程序崩溃,当然,在PC上可能会优化而不会崩溃,但是在一些单片机中就不会这么好了

编译优化以及 volatile

在某些情况下,代码有可能会被优化,比如:

  1. bool flag = false;
  2. void on_interrupt()
  3. {
  4. flag = true;
  5. }
  6. main()
  7. {
  8. flag = false;
  9. exec_with_interrupt(on_interrupt);
  10. while(!flag){}
  11. printf("end");
  12. }

这里exec_with_interrupt执行过程中会调用参数传入的回调函数,回调函数里面会将标记置位真,但在某些情况下(不一定会出现,但可能出现),编译器优化会认为flag需要优化,于是死循环变成了while(false){},最终导致永远在死循环内不会出来!

可能又会发现,在死循环里面加一个打印函数或者改一下编译优化等级程序又正常了!

这种情况下告诉编译器不要优化flag就好了,使用volatile修饰flag即可

  1. volatile bool flag = false;

变量分配的地址对齐(aligned)

有时为了避免出现一些奇怪的错误, 或者某些特定的语句或者硬件需要,我们需要指定某些变量申请的地址在内存中对齐(比如有些 DMA 要求操作128字节对齐)

比如

  1. int a;

可能分配到的地址是0x800127b8, 如果我们改成

  1. int a __attribute__((aligned(64)));

则分配到的地址就会是0x80012780

比如我们用强制转换的时候就会十分在意地址,如果不是强制转换成的类型的长度的整数倍,就会导致程序崩溃, 如;

  1. uint8_t a[10];
  2. // 比如这里a[0]的地址是8的整数倍
  3. uint64_t temp = *((uint64_t*)a);
  4. temp = *((uint64_t*)(a+1));//这里可能会崩溃

这里有可能会崩溃,也有可能不会,不会是因为编译器做了处理

结构体的对齐(aligned) 和 紧凑型(packed)

类似, 如果__attribute__((align(64))) 用到结构体声明上,就是指明结构体需要多少字节对齐,比如64字节对齐,比如:

  1. typedef struct
  2. {
  3. bool a;
  4. } aaa_t;

就算这个结构体的大小只有一个字节,最后一个变量也会占用64字节

packed就是相反

  1. typedef struct
  2. {
  3. bool a;
  4. } aaa_t __attribute__((packed));

用这个结构体定义的变量只会占用一个字节, 如果不熟练,尽量少用这种方式,这种方式一般用在定义通信协议的时候

宏定义妙用

: 取字符,比如#define A(x) #x, 调用A(aaa)得到字符串“aaa”

: 连接,比如

  1. #define A(x) a##x
  2. int a10=100;
  3. int main()
  4. {
  5. printf("%d\n",A(10));
  6. }

最后输出100,注意这里传入的10只能是在编译前能够确定值的数,比如这里的常数10,不能传入变量,比如下面这个就不行

  1. #define A(x) a##x
  2. int a10=100;
  3. int main()
  4. {
  5. int b = 10;
  6. printf("%d\n",A(b));
  7. }

因为宏定义展开是在预处理阶段,是不能得到变量具体的值的,上面A(b)只能得到ab,而且不是字符串,是变量ab,然而并不存在这个变量,所以编译阶段就会报错

单引号字符串

  1. uint32_t data = 0x41424344; // D在内存低字节, A在高字节
  2. int ret2 = (data == 'ABCD');

此处, ret2 的值为 1!!
虽然编译会报警告

文章有误?有想法想讨论?查看或者发起勘误/讨论 主题
(发起评论需要先登录 github)

/wallpaper/wallhaven-rdyewm.jpg