嵌入式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已弃用
- MDK 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 */