关键字&符号&预处理&函数

  无情的搬砖机器

一、关键字

1.1 struct

  该关键字看另外博客:结构体

1.2 union

  该关键字看另外博客:大小端和联合体

1.3 switch

switch注意事项:

  1. case语句中的值只能是整型或字符型的常量或常量表达式(想想字符型数据在内存里是怎么存的)
  2. default语句只能用于处理真正的默认情况
  3. 每个case语句分支必须有break,否则分支重叠

Ps:switch可用于冗长的程序逻辑进程,每一个case代表一个进程,只有其中进程执行完,才对switch(Temp)判断的值进行改动(例如:Temp++);然后进行下一个流程,方便梳理逻辑。

1.4 enum

  相比 #define 标识符常量必须由程序员手工赋值。enum 使程序更容易维护,因为枚举常量是由编译程序自动生成的。

Ps:可搭配switch作为case值方便扩展

1.5 static & extern

1.5.1 static

1
static uint32_t Shatang;

常见用法:屏蔽其他源文件对本源文件static修饰的变量

  1. 修饰局部变量:用来延长变量生命周期,防止再次调用函数时需要再初始化修饰的变量,保留原有数据(常见用法)
  2. 修饰全局变量:用static对全局变量进行修饰改变了其作用域的范围,防止其他源文件对本源文件的变量调用
  • static 全局变量:、
    • static全局变量只初始化一次,防止在其他文件单元中被引用;
    • 只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。
  • static 局部变量
    • static局部变量只被初始化一次,下一次依据上一次结果值;
    • 限制了它的使用范围
  • static 函数
    • static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为(static)内部函数,内部函数应该在当前源文件中说明和定义。
    • static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

Ps:指针是变量

总结:从上面用法描述来看,static不会在 .h文件修饰变量(.c文件都调用不了你,要你何用),正常情况下都会在 .c文件修饰变量、函数定义(声明往往在.h文件),防止其他 .c文件调用;

1.5.2 extern

1
extern uint32_t Shatang;

常见用法:用在变量或者函数的声明前,用来说明“此变量/函数”是在别处定义的,要在此处引用。

  • 问题描述:因为要使用CAN进行数据传输,因此在主程序”test.c”中采用#include “can.h”,调用”can.h”中的函数和变量。结果编译后出现许多 Error L6200E: symbol xxx multiply defined ...

  • 原因:因为在 "can.h"定义(不仅声明还进行定义)了许多变量,"can.c"文件中采用 #include "can.h",调用"can.h"中的变量定义;在主函数"test.c"中也采用 #include "can.h",调用"can.h"中的变量定义,导致”can.h”中的变量被重复定义

  • 解决方法:首先,不应该在”can.h”中定义”can.c”中使用的变量(正常情况都是这样),而是在”can.c”中定义所需的变量。在”can.h”中把变量先进行 extern uint8_t Shatang; 声明,然后 在”can.c”文件再进行定义;此时,主程序”test.c”中将所调用”can.h”中的变量(声明) 将会去”can.c”查找其定义,因此不会存在变量重复定义,问题得到解决。

  • 结论:extern往往用于 .h文件变量声明,然后在相应的 .c文件进行定义,这样防止 .h文件被多个 .c文件调用时产生重复定义的错误

特殊案例:往往图片、字库是按照 .h文件变量定义的,没有.c文件;当你的(lcd.c)文件功能需要调用 某些图片数组.h文件,不能在自身(lcd.h)文件调用 image.h文件,当其他 .c文件调用lcd.h 文件,就会产生上面同样的重复定义;因此对于(只有) .h文件进行变量定义,往往采用直接在(只能在单个) lcd.c 文件调用

1.5.3 static 和 extern 的关系

  static表示是本文件内的变量(在函数中的是静态变量),extern表示是其他文件定义的变量,显然两者是矛盾的;只有全局变量并且没有被static声明的变量才能声明为extern。

1.6 const & 指针

常见用法:const 修饰变量为只读变量(不是常量!!!)。

  • 为了防止传递的函数参数不被修改,在调用函数的形参中用const关键字
  • const可以用来创建数组常量、指针常量、指向常量的指针等

如何辨别const是修饰什么?方法:无括号的情况下,可以先忽略类型名。

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 int *const p;这种都不可变的有什么用?只不过是教语法时用到的花里胡哨。
诚然,指针p和p指向的对象都不可变,但是p指向的对象还可以是个指针啊!因此 const int *const p;往往是用于二级指针。思路反过来,当别人代码这样写的时候:你就应该明白,这里是二级指针。

1
2
3
4
//特殊情况
const (int*)p;
* const int p;
int* const p;

  以上的三种情况是等效的。对于const (int *),因为int *是一个整体,相当于一个类型(如 char),因此,这个const是限定指针不可变。

Ps:只读变量 和 常量 是有区别的,内存存放区域都不同。详情看内存分配一章。

1.7 void

void 常与 (函数)指针 搭配使用:

  1. C语言规定只有相同类型的指针才可以相互赋值
  2. void*指针作为左值用于“接收”任意类型的指针
  3. void指针作为右值赋值给其他指针时需要强制类型转换

1.8 volatile

volatile用于告诉编译器必须每次去内存中取变量值:

  1. volatile主要修饰可能被多个线程访问的变量
  2. volatile也可以修饰可能被(硬件)未知因数更改的变量

Ps:volatile往往只用于最底层驱动(防止数值被修改,例如通信协议);最好不要用于封装其他高级应用层的代码,用处不大,而且不利于其他人对你的程序进行移植拓展(极端情况下可能取值问题出错)。

举例如下:

1
2
3
4
int square(volatile int *ptr)
{
return (*ptr)*(*ptr);
}

由于*ptr 的值可能被意想不到地该变,这段代码可能返不是你所期望的平方值!

1.9 sizeof

1
sizeof(int) //计算(int类型)内存大小

常见用法:计算内存大小。
在嵌入式c代码中,用于计算数组等大小;它往往会和 struct、#define、#pragma pack 连用,用来计算结构体的偏移量,方便调用结构体成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
块注释:打印参数列表
*/
typedef struct
{
uint32_t ulPrintAuto; // 0 自动打印开关
uint32_t ulPrintFormat; // 1 打印格式
uint32_t ulPrintLang; // 2 打印语言
uint32_t ulPrintRow; // 3 打印走纸行数

uint32_t ulPrintAcc; // 4 打印总累计数据
uint32_t ulPrintRecipeSetting; // 5 打印配方设置表
uint32_t ulPrintRecipeAcc; // 6 打印配方累计表

}_strPrintParam;
/*
块注释:获取打印参数结构体成员的偏移量
*/
#define GET_PRINTF_PARAM_OFFSET(member) (((uint32_t)(&(((_strPrintParam *)0)->member))) / sizeof(uint32_t))
/*
块注释:获取打印参数数量
*/
#define PRINT_PARAM_NUM (sizeof(_strPrintParam)/sizeof(uint32_t))
  • PRINT_PARAM_NUM 可用于实现结构体成员的(for循环)赋值;
  • GET_PRINTF_PARAM_OFFSET(member) 计算出来的结构体偏移量可方便 结构体扩展

1.10 typedef

  typedef用于给一个已经存在的数据类型重命名。

下边是一个能够说明typedef的语法例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// simple typedef
typedef unsigned long ulong;

// the following two objects have the same type
unsigned long l1;
ulong l2;

// more complicated typedef
typedef int int_t, *intp_t, (&fp)(int, ulong), arr_t[10];

// the following two objects have the same type
int a1[10];
arr_t a2;

// common C idiom to avoid having to write "struct S"
typedef struct {int a; int b;} S, *pS;

// the following two objects have the same type
pS ps1;
S* ps2;
  1. typedef 定义结构体类型

    1
    2
    3
    4
    5
    6
    7
    8
    typedef struct
    {
    int iNum;
    long lLength;
    }MyStruct;
    MyStruct stu1;
    MyStruct *stu1;
    MyStruct class[50];
  2. typedef 定义数组类型

    1
    2
    3
    4
    5
    6
    7
    8
    typedef int(AINT5)[5];
    typedef float(AFLOAT10)[10];
    typedef char(ACHAR9)[9];

    AINT5 a1;
    AFLOAT10* pf = &fArray;
    ACHAR9 cArray;
    AINT5 i = {0,1,2,3,4};
  3. typedef 定义(常见)指针类型

    1
    2
    3
    typedef char* pstr;

    pstr p;
  4. typedef 定义函数指针类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    using namespace std;

    typedef void(*pFunc)();

    void myFunc()
    {
    cout << "Hello World!" << endl;
    }

    int main()
    {
    pFunc func;
    unc = &myFunc;
    func();
    return 0;
    }

进阶:typedef 定义函数指针类型后,可以再用 该函数指针类型 定义 数组/结构体成员,让 每个元素 或 结构体成员 为函数指针。

综合例程,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h> 
typedef char arr[2][5]; // 数组
typedef char *name[5]; // 指针数组
typedef char (*lan)[5]; // 数组指针

int main()
{
arr age;
name named;
lan land;
char i;
char j;
for(i=0;i<2;i++){
for(j=0;j<5;j++){
age[i][j]=i*j+1;
}
}

for(i=0;i<2;i++){
named[i]=age[i];
}

land=&age;

for(i=0;i<2;i++){
for(j=0;j<5;j++){
printf("aged[%d][%d]=%d named[%d][%d]=%d land[%d][%d]=%d\n",i,j,age[i][j],i,j,named[i][j],i,j,land[i][j]);
}
}
}

1.11 enum & const & typedef & #define

  • #define:标识符常量必须由程序员手工赋值
  • enum:枚举常量是由编译程序自动生成的,使程序更容易维护
    • 这两个都是可以用来定义常量
  • const:修饰 只读变量,不会变的变量。从内存分配区上来看,就已经不一样了。
  • typedef:跟#define极其相似,但实际有很大区别

举例区分typedef & #define

例子1. 以下p1,p2,p3,p4有什么区别?

1
2
3
4
5
6
//第一段
typedef char* PCHAR;
PCHAR p1,p2;
//第二段
#define PCHAR char*
PCHAR p3,p4;

答案:p4是一个char类型

例子2. 以下是 指针为只读变量 还是 指针指向的内容不可变?

1
2
typedef char* pstr;
const pstr p;

答案: const pstr p;<==> char* const p;

  错误的原因在于将 typedef 当做文本扩展了(#define 才是真正的文本扩展!)。声明 const pstring 时,const修饰的是pstring的类型,这是一个指针。因此,该声明语句应该是把cstr定义为指向string 类型对象的const指针。

1
2
3
4
5
6
typedef char* pstr;

const pstr p;
const (char*) p;
* const char p;
char* const p;

  以上的四种情况是等效的。


二、符号

2.1 单引号、双引号

  • 单引号引起来的都是字符常量
  • 双引号引起来的都是字符串常量

举例:1 ,’1’ , “1”

第1个是整数常量,32位系统下占4字节;
第2个是字符常量,占1字节;
第3个是字符串常量,占2字节。

  字符在内存里是以 ASCII码 存储的,所以字符常量还可以与整形常量或变量进行运算,如:'A' + 1

2.2 逻辑运算符使用分析

案例分析:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
void main()
{
int i=0;
int j=0;
if(++i>0 || ++j>0)
{
printf("%d\n",i);
printf("%d\n",j);
}
}

输出的结果为1,0。

2.3 逻辑运算符 & 按位运算符

  举例:以下例子函数的DATAx为宏定义stm32各个IO管脚,封装函数实现8pin并口数据输出;以下函数均能在C编译器编译通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//error
void DATA_Process(uchar com)
{
DATA0=((com &(1<<0))==1) ? 1 : 0 ;
DATA1=((com &(1<<1))==1) ? 1 : 0 ;
DATA2=((com &(1<<2))==1) ? 1 : 0 ;
DATA3=((com &(1<<3))==1) ? 1 : 0 ;
DATA4=((com &(1<<4))==1) ? 1 : 0 ;
DATA5=((com &(1<<5))==1) ? 1 : 0 ;
DATA6=((com &(1<<6))==1) ? 1 : 0 ;
DATA7=((com &(1<<7))==1) ? 1 : 0 ;
}
//correct
void DATA_Process(uint8_t com)
{
DATA0=(com &(0x01<<0)) ? 1 : 0 ;
DATA1=(com &(0x01<<1)) ? 1 : 0 ;
DATA2=(com &(0x01<<2)) ? 1 : 0 ;
DATA3=(com &(0x01<<3)) ? 1 : 0 ;
DATA4=(com &(0x01<<4)) ? 1 : 0 ;
DATA5=(com &(0x01<<5)) ? 1 : 0 ;
DATA6=(com &(0x01<<6)) ? 1 : 0 ;
DATA7=(com &(0x01<<7)) ? 1 : 0 ;

当调用该函数时,错误案例是无法实现相应的输出的

1
DATA_Process(0xff);

提示:因为是 按位& ,所以得出来的值 并非 1 或0 ;只有 && 条件判断 才是得出来的值 1或0。

2.4 优先级

三、预处理

  预处理是在编译环节中最早开始执行的,并且后面的代码(因条件变化)都不会影响到任何预处理的一些操作(例如:#define)

3.1 宏定义

1
2
#define       //定义一个预处理宏
#undef //取消宏的定义

举例如下

1
#define SREG   (*(volatile unsigned char*)0x5F)

  嵌入式系统编程,要求程序员能够利用C语言访问固定的内存地址。

  • 既然是个地址,那么按照C语言的语法规则,这个表示地址的量应该是指针类型。
    • 所以,知道要访问的内存地址后,比如0x5F,第一步是要把它强制转换为指针类型(unsigned char*)0x5F,AVR的SREG是八位寄存器,所以0x5F强制转换为指向unsigned char类型。
    • volatile(可变的)这个关键字说明这变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了。这种“意想不到地改变”,不是由程序去改变,而是由硬件去改变——意想不到。
  • 第二步,对指针变量解引用,就能操作指针所指向的地址的内容了*(volatile unsigned char*)0x5F
  • 第三步,小心地把#define宏中的参数用括号括起来,这是一个很好的习惯,所以
    • #define SREG ((volatile unsigned char)0x5F)

类似的,如果使用一个32位处理器,要对一个32位的内存地址进行访问,可以这样定义:

1
#define RAM_ADDR    (*(volatile unsigned long *)0x0000555F)

  然后就可以用C语言对这个内存地址进行读写操作了.

读:tmp = RAM_ADDR;
写:RAM_ADDR = 0x55;


3.2 条件编译

1
2
3
4
5
6
7
8
9
10
#ifdef        //判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef //与#ifdef相反,判断某个宏是否未被定义

#if //编译预处理中的条件命令,相当于C语法中的if语句
#elif //若#if,或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else //与#if对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else

#endif //#if,#ifdef,#ifndef,这些条件命令的结束标志

#error //用于生成一个编译错误的消息,并停止编译

条件编译的意义,实际工程中条件编译主要用于以下情况:

  1. 不同的产品线公用一份代码
  2. 区分编译产品的调试版和发布版
  3. 方便变动程序,例如不同的软件驱动方式

举例:

1
2
3
#if Test_Version_Enable
#error "The version is a test version,please try it to be unable"
#endif

四、函数的设计技巧

  • 参数名要能够体现参数意义
  • 如果说传递的参数为指针,且仅仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被恶意修改
  • 不要省略返回值的类型,如果函数没有返回值,那么应当声明为void
  • 在函数体的“入口处”,对参数的有效性进行检查,对指针的检查尤为重要
  • 语句不可返回指向“栈内存”的“指针”,因为该内存会在函数结束后销毁
  • 相同的输入应当产生相同的输出,尽量避免函数带有“记忆”功能少用static
  • 避免函数有太多的参数,参数个数应当控制在4个以内
  • 有时候函数不需要返回值,但是增加灵活性,可以附加返回值
-------------本文结束感谢您的阅读-------------