【嵌入式C语言自由】知识点备忘录

嵌入式C语言自由!!!

优先级

  • 有时候会不可避免的在一行使用多种运算符,分开写和加小括号不一定总是最合适的,此时还是要掏出八股秘籍------优先级表。。:
    在这里插入图片描述
  • 强制转换优先级等同小括号,向右结合

关键字

  • volatile
    易变值,防止编译器优化使用原值的临时备份,每次使用必须要重读原值。临时备份可能被放在工作寄存器中临时使用,而原值可能被中断修改,通常与中断相关变量使用

  • switch

    • case可以为整型负数,不能为浮点数
    • 如果没有break将接着往下执行,即使case值不符合(c#不会这样),方便多个case执行一个代码块
  • struct

    • 结构的外部引用extern:如果使用typedef定义则不需要声明struct,直接声明新类型(c++允许使用结构类型时省略struct关键字,即使不用typedef声明)
    • 结构内包含指向自己的指针:如果使用typedef定义则需要在结构指针前再次添加struct声明,需要说明的时,包含指向自己的指针是C的一个未声明先使用的特例,仅此一例(此时该结构未定义)
    • c99允许结构包含不定长度的数组
  • enum
    可以用枚举代替的魔法数尽量用枚举代替,这不光有利于代码的可读性和移植性,还将在调试时带来可见的好处

数组

  • []结合方向:从左向右,如[2][3]表示两个元素,每个里面又包含三个子元素
  • 数组下标不仅允许上溢出,还允许下溢出,分别指向数组内存的后面和前面,这里充分展示了一个C设计理念,那就是用户清楚的知道自己在干什么

指针

  • 指针自增自减的单位是指向数据类型的字节数,也就是指向后一个或前一个同类型内存块,加减一个常数也是一样,但同类型指针不能进行一般数值运算,可相减,结果为单位指针指向类型内存块的个数
  • 和变量不太一样,指针在引用前必须初始化,否则其指向的地址未知,这是致命的。比如,stm32开发,强行引用将会导致硬件级总线访问错误引发系统崩溃,
  • 关于一般变量的使用,初始化就是要在使用前赋值,默认其地址由编译器自动分配(可以通过编译器指定)。多级指针变得比较复杂。
    • 在函数参数传入时,一般来讲,修改变量需要传入一级指针,修改指针需要传二级指针,也就是说,二级指针是修改指针的指针
    • 指针是特殊的变量,其使用方式其实是有点奇怪的:
      • 指针的引用修改的并不是该指针的值,该指针自己的地址和其指向的地址都没有发生改变
      • 指针存在一个级别和一个基本类型,都匹配才能正常赋值
      • 指针的引用修改的是其保存的地址指向的变量,该变量可以是普通变量或者是其他指针
      • 如果指针是指向其他指针的话,情况变得更加奇怪,也更加危险。比如一级指针能指向二级指针,三级指针也能指向一级指针,编译器只给出类型不一样的警告,类型转换后甚至是允许的使用方法,比如下面的代码不会有任何错误和警告(使用GCCv11.3)。
char ip[] = "hello";

int main(void)
{
	char** a;
	char* c;
	char* d;
	char*** g;

	c = ip; 			 //一级指针初始化
	a = &c;				 //二级指针初始化
	d = (char*)&a;       //一级指向二级
	g = (char***)&*d;    //三级指向一级

	printf("%s\r\n",*a);
	printf("%s\r\n",**g);
	return 0;
}
  • 指针的声明
    • 指针的声明对*号的位置并不敏感
    • 需要注意的是单行声明多个变量,如果想声明一个类型的多个变量,则每个变量前都要加除了基本变量类型外的其他符号
//以下定义等效
char* a;
char *a;
char * a;
char*a;
//单行声明多个指针变量
char *a,*b;
char **a,**b;
  • const
    • 当指针声明中有const, 则const修饰的是其向左结合的类型
    • 如果const左边为基本数据类型,则在一条语句内声明多个数据时该const对所有参数都生效,结合方式依旧向左
    • 对多个const的情况也一样,比如
//声明只读的二级指针pp,其指向的一级指针和其一级指针指向的内容均为只读
//声明一个可读写的二级指针cc,其指向的一级指针可读写,其指向的一级指针指向的内容只读
//ss为任意的合法值初始化只读内容
char const*const*const pp=ss, **cc=ss;
  • 特别的
    • 对一些arm核,比如cortex-m3,double类型和对应指针的访问要求四字节对齐,否则将产生硬件错误,在使用按字节对齐结构时容易发生

  • 变参宏示例,简化RTT的打印,可以用__VA_ARGS__替换参数列表:
#define rtt_print(...)  RTT_DEBUG_PRINTF(0, __VA_ARGS__)

内联函数 inline

  • 内联函数是个高级宏,也就是定义的函数实际上会进行代码替换而不是入栈,MDK中配合static和extern使用,否则可能报错

强制转换

  • 强制转换通常配合指针使用,但在对非指针变量强制转换需要仔细考虑是否使用得当。在强转后数据结构变大的情况下,吞并数据需要注意一些问题,而在强转后数据结构变小的情况下,舍弃数据则不容易出问题。
    • 指针强转,内存向高位结合(向右)
    • 普通变量强转,内存向低位结合(向左)
    • 所以在涉及到接收字节序的时候需要特别注意。接收字节序增长顺序基本都是内存向高,所以比较适合使用指针强转。下面是一个例子,假设接收缓冲为ii,如果只用变量强转,解析一个uint16_t类型,则需要使用接收顺序的后一个字节;如果使用指针强转,则需要使用接收顺序的前一个字节,我感觉使用指针更符合常规思维方式。
uint8_t ii[] = {0x00,0x00,0x02,0x00};
int main() {
	printf("%d\r\n",(uint16_t)ii[1]);  //向左结合
	printf("%d\r\n",*(uint16_t*)&ii[1]);  //向左结合
	return 0;
}

在这里插入图片描述

  • 因为不同类型强转的结合方向不同,所以需要特别注意大小端的问题。

大小端转换

2/4字节大小端转换和自动检测1/2/4字节大小端转换:

#define SWAP_B2_ENDIANESS(s) \
    (((s) & 0xFF00) >> 8) | \
    (((s) & 0x00FF) << 8)

#define SWAP_B4_ENDIANESS(value) \
    ( \
        ((value << 24) & 0xFF000000) | \
        ((value <<  8) & 0x00FF0000) | \
        ((value >>  8) & 0x0000FF00) | \
        ((value >> 24) & 0x000000FF) \
    )

#define AUTO_B1B2B4_SWAP(x)	((sizeof(x)==1)?x:((sizeof(x)==2)?SWAP_B2_ENDIANESS(x):((sizeof(x)==4?SWAP_B4_ENDIANESS(x):x))))

工程结构

  • 头文件可以相互包含(正点原子式编程),缺点是改动一个被包含的头文件将导致所有相关源文件的重新编译,这在一些老电脑上是很难以忍受的(make支持多线程编译还挺好)
  • 头文件中包含定义时不允许相互包含

声明

允许重复声明,不建议这样做,应该没有任何好处

编码

  • ASCII和gb2312编码的识别:从起始位置开始,第一个字节第一位为1则用gb2312解释,为0则用ASCII解释

  • GBK兼容gb2312,是gb2312字符集的扩充

  • keil对中文编码支持不太好,有几种处理方式:

    • 可以用如下编译器指令忽略警告
    -Wno-invalid-source-encoding				#ac6
    --locale --multibyte_chars 					#ac5可以尝试这个?
    

库函数拾遗

printf

  • %.*s*表示可用一个额外参数指定最大输出宽度,如3,hello表示hello仅输出3位,*也可直接用数值代替
  • %f,double类型不需要使用%lf,但默认输出会损失精度,需要使用格式输出参数指定小数位数
  • PC端编程printf("\033[2J")这个可以清空终端内容类似system("clear")
  • printf 家族其他成员:
    • sprintf() //输出到缓冲
    • snprintf()
    • vprintf() //变参函数参数列表处理,printf底层调用的接口
    • vsprintf()
    • vsnprintf()
    • fprintf() //输出到文件
    • vfprintf()

scanf

使用方式和printf很想,因为要给数据赋值,所以传参的时候传地址

  • scanf()
  • sscanf() //输入到缓冲

字符串

  • 字符串的换行,当一个字符串太长的时候,在字符串""内部添加\换行符是不行的,可以把一个""换成两个,""\""这样就可以换行了,两个字符串也会自动拼接

对齐

  • c支持使用编译器指令进行字节对齐,gcc和keil这些编译器的对齐指令也都不一样,互不兼容,功能强大,内容也比较多,用什么百什么即可。。。主要用于数据传输
    • MDK v6,放#pragma pack(push, 1)#pragma pack(pop)中间即可,这个例子是按一个字节对齐
    • MDK v5, 可以使用__packed,v6已弃用
  • 大小端转换可以配合union,比如浮点转换

预设值

  • c预设了一些格式如__xxxx__的宏,包含了行号、函数名、日期、编译时间等信息,会方便一些调试信息的打印
    • __LINE__
    • __fun__
    • __TIME__

断言assert

  • 调试时用来定位错误,不同的C库有不同的定义,目的都是一样的。以stm32HAL库为例,下面是HAL库中大量使用的assert_param()断言定义,可以看到要使用断言需要定义USE_FULL_ASSERT,assert_param()传入的参数会进行条件判断,为false则进入assert_failed()进行用户处理
#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
    /* USER CODE BEGIN 6 */
    /* User can add his own implementation to report the file name and line number,
       ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
    /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

#ifdef  USE_FULL_ASSERT
/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr: If expr is false, it calls assert_failed function
  *         which reports the name of the source file and the source
  *         line number of the call that failed.
  *         If expr is true, it returns no value.
  * @retval None
  */
  #define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值