【C/C++】C语言技巧总结

0 概述

  该文章主要是总结一些在编写单片机程序及其他相关实践中学到的实用的C语言技巧以及可能会遇到的一些坑,本文读者应具有一定的C语言基础。

1 位运算

  因为计算机存储数据都是按字节(Byte,B)储存的,一个字节含有8位(bit,b),直接对数据的位进行操作,可以使运算速度加快。下面是常用的运算符

1.1 初级应用

  • 按位取反 ~:用于将数字的每一位二进制翻转,比如 ~(0110 1001) = 1001 0110。
  • 按位与 &:全1为1,其余为0,比如 (1010 0101) & (0001 0111) = (0000 0101)
  • 按位或 |:全0为0,其余为1,比如 (1010 0101) | (0001 0111) = (1011 0111)
  • 按位异或 ^:相同为0,不同为1,比如 (1010 0101) | (0001 0111) = (1011 0010)
  • 左移 <<:将其左边的操作数的值的每一位向左移动,移动的位数由右边的操作数指定,移出的数字舍弃,右边自动补0,比如 (1010 0101) << 3 结果为 (0010 1000),(由于单片机中需要用到的数据大多为无符号数,故此处不讨论有符号数的情况,详情请参考链接左移n位相当于乘以2的n次方(前提是没超过字节范围)
  • 右移 >>:将其左边的操作数的值的每一位向左移动,移动的位数由右边的操作数指定,移出的数字舍弃,左边自动补0,比如 (1010 0101) >> 3 结果为 (0001 0100),同样不考虑有符号数,右移n位相当于除以2的n次方

1.2 进阶应用

1.2.1 给寄存器某一位置1

  单片机的程序当中可能会涉及到寄存器的读写,而寄存器在程序中体现为一个固定字节数的变量,寄存器的某些位控制着某些模块的特定功能,因此对某一位的操作就很有必要了。

P1OUT |= 00000010;   //P1OUT是一个8位寄存器,可看作一个单字节数据
//equal to this: 
P1OUT = P1OUT | 0000010;

  上述程序就是将P1OUT寄存器的第2位置1,其他位保持不变,因为 1 | 0 = 10 | 0 = 0,即一个数或上0,其值保持不变;或上1,则变为1。

1.2.2 给寄存器某一位清0

  有置1的操作,就必然要有清0的操作:

P1OUT &= ~(00000010);  //P1OUT是一个8位寄存器,可看作一个单字节数据
//equal to this: 
P1OUT = P1OUT & ~(00000010);

  上述程序是将寄存器P1OUT的第2位清0,其他位保持不变,因为 0 & 1 = 01 & 1 = 1,即任意数与上1,其值保持不变;与上0,则变为0。

1.2.3 翻转寄存器某一位

  入门程序:LED闪烁就可以使用翻转输出电平的方法实现。

P1OUT ^= (00000010);
//equal to this: 
P1OUT = P1OUT ^ (00000010);

  上述程序是将寄存器P1OUT的第2位进行翻转,其他位保持不变,因为 0 ^ 0 = 0, 1 ^ 0 = 1; 0 ^ 1 = 1, 1 ^ 1 = 0相当于一个数和0异或,保持不变;和1异或,1变成0,0变成1,等价于翻转电平。

1.2.4 数据的字节分解

  在使用单片机的串口传输数据时,一般一次只能传输一个字节,而且传输时是一位一位传输的,因此对于多字节数据,需要先对字节分解,再进行传输,用移位运算符就是一个不错的解决方法。

/****分字节*****/
int a; //假设为2字节
char c1, c2;
c1 = a;  //默认取低8位
c2 = a >> 8;  //取a的高8位


/****按位传输*****/
for(i=0; i<8; i++)
{
     if((zdata << i) & 0x80)  //取最高位,注意此处的zdata的值始终没有改变!
      {
          P1OUT |= BIT1; //SID = 1;
      }
      else
      {
          P1OUT &= ~BIT1;//SID = 0;
      }
}

2 宏定义和预编译

2.1 理论指导

  宏定义语法我们都知道:#define A B,即用A来代替B,需要注意的就是这个替换是纯文本替换。但是其实以#开头的语句还有很多,常见的如下所示。
在这里插入图片描述
这些语句都是预编译代码,即在编译之前先进行处理,处理完成之后得到“简化”的代码,再去编译。
  其中,预编译语句最常用的就是放在头文件开头,来避免头文件的重复调用。设计非常巧妙,如下图所示。

在这里插入图片描述

参考链接

特别注意:如果有#ifdef或者#ifndef,在后面必须要有#endif,就像是一对花括号有其一必须要有其二,否则就会报错:unterminated conditionals。
参考链接

2.2 看到一个宏定义在数据切分字节上的妙用

#define  BYTE0(some)  *((char *)(&some))
#define  BYTE1(some)  *((char *)(&some) + 1)
#define  BYTE2(some)  *((char *)(&some) + 2)
#define  BYTE3(some)  *((char *)(&some) + 3)

这个操作实现的是对数据some进行切分字节操作,非常巧妙。

2.3 define和typedef的使用区分

  最近发现一个小问题,就是一下子忘掉了define的语法,不确定到底是前面代替后面还是后面代替前面,和朋友交流之后确定了,顺便交流了一下typedef的用法,总结一下。

#define A B C

表示用A代替“B C”,即第一个空格后面的内容代替第二个空格后面所有的内容。所以在单片机中,常用短字符代替unsigned intunsigned char,比如:

#define uint unsigned int
#define uchar unsigned char

  但是这样会存在一个问题,那就是C语言中的指针char *,比如

#define CH char *
CH a, b;

等效于char * a, b,这样a就是char * 类型,而bchar类型,因此如果需要定义新的类型,最好使用typedef,其语法如下:

typedef unsigned char uchar;
typedef unsigned int uint;

需要与define语法进行区分,首先是顺序的问题,typedef将前面的数据类型定义为后面自定义的名词,而define用前面自定义的名词去代替后面的数据类型,其次就是typedef语句后面需要加分号。

3. 字符串

3.1 终止符问题

  在初始化一个字符数组(字符串)时,有很多种方式,哪种情况下有终止符?

char str1[7] = "Hello!";   //默认添加终止符
char str2[7] = {'H','e','l','l','0','!'};  //默认添加终止符
char str3[7] = {'H','e','l','l','0','!','\0'}; //显示声明终止符
char str4[] = "Hello!";  //编译器自动为str4分配7个字节
char str5[14] = "Hello!";  //留下空余位置,但仍然有终止符

  一般来说,初始化一个字符数组,系统默认会在最后添加一个终止符 ‘\0’ (ASCII值为0),因此初始化时,必须要多申请一个位置,不然会报错。不过如果使用strlen函数求字符串的长度时,不会算入\0。
  但是程序允许有不带终止符的字符串,比如在程序运行过程中,最后一位不设置为\0,也是可以的,但这样在使用一些函数会出现错误,因为一般内置的函数处理字符串都以最后的\0为分界。

4 数据类型

4.0 记录一个基础问题

int a = 0b01101; //0b 开头,表明为二进制
int b = 0xA5; //0x 开头,表明为十六进制
int c = 12; //单独一个数字,默认为十进制
int d = 012; //0开头表示八进制,即012表示 1*8+2*1=10

4.1 有符号无符号

  首先需要明确的是,有符号和无符号只会影响数据类型的取值范围,而不会影响字节数!(虽然很基础,但对于快要忘了大一C语言的人还是有用的)
  齐次需要注意的是,整形数据类型默认格式不一定是有符号型,这个和编译器有关,可以用下面的程序测试一下:

void char_type()
{
	char c=0xFF;
	if(c==-1)
		printf("signed");
	else if(c==255)
		printf("unsigned");
	else
		printf("error!");
}

参考链接

4.2 数据类型的字节数

  一个整形数据到底占几个字节的问题,可以说是写单片机程序的基础,一开始,我以为这个和单片机的位数有关(是不是8位单片机的数据长度就比16位单片机短?),但后来才知道,单片机的位数的含义是单片机一次能处理多少位数据的意思,并不直接决定单片机的性能。经过查找资料才知道数据类型的长度和编译器直接关联。所以这个东西没有规律可循,只能记忆。

参考链接

4.3 布尔类型

  需要注意的是,C语言较早的版本中,是没有布尔数据类型的,后来C99标准增加了_Bool数据类型,变量只能被赋值为0或者1,其他值会被视为1。C99还提供了一个头文件 <stdbool.h> 定义了 bool 代表 _Bool,true 代表 1,false 代表 0。只要导入 stdbool.h ,就能非常方便的操作布尔类型了。

参考链接

  经过测试发现,Keil中如果设置使用C99版本的话,是能够识别_Bool类型的,包含stdbool.h文件之后,也能使用bool,符合上述表述。但是在使用VS Code(MinGW v8.1)测试时,发现只有在包含stdbool.h文件才能使用_Bool和bool类型。

4.4 数据类型的转换

  在写单片机相关的程序时,我们时常会遇到需要传输非字节数的情况,比如传输浮点数之类的,因此就需要用到数据类型的转换。这里就直接引用这篇博客,权当记录。

  • float转byte
unsigned char temp[4];
unsigned short i=0;  
float floatVariable=value;   
unsigned char *pdata = (unsigned char *)&floatVariable;
for(i=0;i<4;i++)  
{
	temp[i+1] = *pdata++;//float转BYTE
}
  • int 转byte
char temp[4];
unsigned short m=0;
for(m=0;m<4;m++)//int转byte
{
     temp[m+1]=(value>>(24-m*8));
}
  • byte转int
int CountReceiveData=0;
for(int j=0;j<4;j++)
{
	CountReceiveData<<=8;
	Temp_Value1=(buf.data()[1+j])&(0xff);
	CountReceiveData|=Temp_Value1;
}
  • byte转float
float CurrentReceiveData=0;
void *pf;
pf = &CurrentReceiveData;
unsigned char * px;
px=(unsigned char *)buf.data();
for(int j=0;j<4;j++)
{
	*((unsigned char *)pf+j)=*(px+j+1);
}

5 变量修饰关键词

参考链接

5.1 const

  1. 阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化,以后就没有机会改变他了;
  2. 对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值
  4. 对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

5.2 static

  • static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。再次调用该函数可以再次使用。
  • static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以
  • static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为0
  • 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰
  • 考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用static)

  在C++中static关键字除了具有C中的作用还有在类中的使用,在类中,static可以用来修饰静态数据成员和静态成员函数。

  静态数据成员
  (1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
  (2)静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。
  (3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若为对静态数据成员赋初值,则编译器会自动为其初始化为0
  (4)静态数据成员既可以通过对象名引用,也可以通过类名引用。

  静态成员函数
  (1)静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
  (2)非静态成员函数有this指针,而静态成员函数没有this指针。
  (3)静态成员函数主要用来方位静态数据成员而不能访问非静态成员。

5.3 volatile

  先来看看最常见的解释:

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

  不理解?没关系,继续看下去,之后你就会理解上面这段话了。

  要理解volatile这个关键字,首先需要知道什么是编译器优化:

  • 在一次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值,因为直接从内存读取变量会相对较慢。
  • 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
  • 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致

  所以,本质上来说,用volatile修饰一个变量,就是告诉编译器:每次读取这个变量,都要去内存里面读取原始地址,而不要去读取寄存器中的“备份”,不然就可能会读取错误! 。举个例子:

static int i=0;

int main(void) 
{ 
	... 
	while (1) 
	{ 
		if (i) dosomething(); 
	} 
}

/* Interrupt service routine. */ 
void ISR_2(void) 
{ 
	i=1; 
}

  程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
  到此,再去看最初的解释是不是更加清楚了?

下面是volatile变量的几个例子
   1). 并行设备的硬件寄存器(如:状态寄存器)
   2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
   3). 多线程应用中被几个任务共享的变量

参考链接

5.4 extern

  在理解extern关键词之前,首先需要理解C语言中变量的声明和定义。众所周知,函数是有声明和定义的,一般结构如下所示。

// 声明
void func(int input);

// 定义
void func(int input)
{
	...
}

即声明只是告诉编译器代码里面有这个函数,而定义则是这个函数的实现。
  那变量为什么还需要声明和定义呢?我们常用的就是int a = 1;这样定义一个变量就能使用了,但是如果这个变量需要被其他文件所调用呢?因为C语言是不允许重复定义变量的,所以其他文件中只能使用声明,来告诉编译器,我这个文件里面用的变量在其他文件里面被定义了,你得去其他文件里面找。而这就需要使用到extern关键词。

char x;     //声明且定义变量x
int y = 1;  //声明且定义变量y,并且初始化变量y
//注意:并不是不赋值就不是定义!

extern char x;     //仅声明变量x
extern int y = 1;  //仅声明变量y

  如果进行模块化编程,必然会涉及到多文件编程,也就会遇到变量声明和定义放在哪里的问题,如果处理不好,可能会出现Symbol XXX multiply defined 或者 “ERROR L104: MULTIPLE PUBLIC DEFINITIONS” 类似的报错,表示变量被重复定义了。出现这种情况,一般是在一个头文件中定义了一个变量,但这个头文件被多次包含,就会导致这个问题。
  正确的做法一般有两种:

  • 在一个模块中定义一个全局变量,然后在其他需要使用该变量的文件中先声明一下。
    module.c文件

    //另一个文件中声明并定义全局变量x,y
    int x;
    int y;
    

    main.c文件:

    #include <stdio.h>
    
    
    void func1(void)
    {
        //声明变量x,y
        extern int x;
        extern int y;
        //给变量x,y赋值
        x = 1;
        y = 2;
        //打印输出变量x,y
        printf("变量x为:%d\n", x);
        printf("变量y为:%d\n", y);
    }
    
    
    int main()
    {
        func1();
    }
    
  • 在头文件中声明变量,即使用extern关键词,在对应的c文件中定义,然后其他需要使用该变量的只需要包含该头文件即可。
    module.c文件

    //另一个文件中声明并定义全局变量x,y
    int x;
    int y;
    

    module.h文件

    #ifndef    __MODULE_H_
    #define    __MODULE_H_
    
    
    //声明变量x,y
    extern int x;
    extern int y;
    
    
    #endif // __DEMO_H_
    

    main.c文件

    #include <stdio.h>
    #include "module.h" //引用头文件
    
    void func1(void)
    {
        x = 1;
        y = 2;
        //打印输出变量x,y
        printf("变量x为:%d\n", x);
        printf("变量y为:%d\n", y);
    }
    
    
    int main()
    {
        func1();
    }
    

参考链接

  • 拓展

extern "C":可以把这个看作是两个修饰符,即一个extern,一个"C",前者告诉编译器修饰的这个模块是extern类型的,而"C"告诉编译器这个要用C的方式来编译、链接。因为在编译阶段,C和C++的处理方式是不一样的,这样会导致链接阶段发生错误。

参考链接

6 结构体(struct)&联合(union)&枚举(enum)

6.1 参考链接

6.2 结构体语法总结

  结构体是将不同类型的数据按照一定的功能需求进行整体封装,封装的数据类型与大小均可以由用户指定。
  声明一个结构体的语法如下:

struct A
{
	int a;
	char b;
};

  需要注意的是,在声明一个结构体时,不能包含本身,但是可以包含本身的指针【链表的构成基础】(因为指针所占的字节数是固定的),而且还可以嵌套其他的结构体:

struct A
{
	int a;
	char b;
	struct A *B;   //结构体本身的指针
	struct S S1;   //另一个结构体S定义的变量S1
};

  声明结构体类型仅仅是声明了一个类型,系统并不为之分配内存,就如同系统不会为类型 int 分配内存一样。只有当使用这个类型定义了变量时,系统才会为变量分配内存。所以在声明结构体类型的时候,不可以对里面的变量进行初始化还有一个资料显示:“结构体名”的命名规范是全部使用大写字母。
  如果需要定义一个结构体变量,可以直接在声明时最后的分号前加上。

struct A
{
	int a;
	char b;
}A_a;

/**与下面程序等效**/
struct A
{
	int a;
	char b;
};

struct A A_a;

  需要注意与带有typedef的语句进行区分。

typedef struct A
{
	int a;
	char b;
}AAA;    //相当于声明一个结构体A并将其重命名为AAA

AAA A_a;  //定义一个结构体A类型的变量A_a

  补充:有时候定义结构体会直接省略结构体的类型名:

typedef struct
{
	int a;
	char b;
}AAA;

  因为本身结构体名只不过是一个标签罢了,定义变量时还是得加上关键词struct
  结构体内部成员的访问有两种方式:

typedef struct A
{
	int a;
	char b;
}AAA;

AAA A1;
d = A1.a;     //访问内部成员用符号”.“

AAA *A2;
c = A2->b;   //定义一个指针,访问内部成员使用”->“

结构体内存对齐问题
  我们可能在学C语言的时候,会依稀记得一个结论:结构体占有的内存大小就是其内部所有成员占有内存之和。但是实际上,某些硬件平台要求数据的存储必须从特定的地址开始,这就有了结构体成员内存对齐的要求,也就是成员之间可能会存在一些“空位置”,这就导致结构体实际占据的内存大小要比内部成员占有内存之和要大一些。
在这里插入图片描述
  这个“几字节边界上对齐”是指从结构体内存起点开始算的,比如int只能在第0,4,8字节处开始存储,所以结构体成员定义顺序的不同会导致最后结构体占有的内存大小不一样。

结构体中的位域
  在结构体中定义成员变量,有时候不需要完整的一个数据类型的宽度,比如一个布尔量只需要1位来表示,为了节省内存空间,可以采用位域的语法。

  所谓 “位域” 是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

  其语法如下所示:

struct bs
{
	int a:8;
	int b:2;
	int c:6;
}data;

  这几句代码表示 “data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位”。但是位域的使用需要注意三点:

  • 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。比如:
struct bs 
{
	unsigned a:4;
	unsigned :0; /*空域*/
	unsigned b:4; /*从下一单元开始存放*/ 
	unsigned c:4;
} 
//这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
  • 位域可以没有域名,但是这样的位域不能使用。比如:
struct k 
{
	int a:1;
	int :2; /*该2位不能使用*/ 
	int b:3; 
	int c:2;
};
  • 位域的位数不能超过数据类型,比如51单片机中的int为16位,则位域不能超过16位。

综上所述,其实位域也算是一种数据类型,只不过是二进制位定义的罢了。


结构体的构造函数
  值得一提的是,在c++中,结构体也具有构造函数,即可以通过类似于初始化列表的方式对结构体内部的变量进行初始化。如下所示。【C语言中就没有构造函数这种概念】

#include <stdio.h>
//同时用默认构造函数和自定义构造函数 
struct student {
	int id;
	char gender;
	student(){};  //显示默认构造函数 
	
	//自定义构造函数 
	//可以同时初始化id和gender 
	student(int _id, char _gender) : id(_id), gender(_gender){} 
	//也可以单独初始化gender
	student(char _gender) : gender(_gender){} 
}pig,Pig;  //能不经初始化就定义变量 

int main(){
	pig = student(23,'F');//直接使用构造函数,同时赋值
	Pig = student('M'); //直接使用构造函数,单独赋值 
	printf("pig ID = %d\npig Gender = %c\n\n",pig.id,pig.gender);
	printf("Pig ID = %d\nPig Gender = %c",Pig.id,Pig.gender);
	return 0;
}

参考链接

6.3 联合union语法总结

  union即为联合,其最大的特征就是其内部成员共用一片内存,且其各自首地址相同。因此,对于结构体内部的成员,有一个特殊的性质:那就是访问任意一个变量,其他的变量都会自动更改。

union A
{
	int a;
	int b;
};

int main()
{
	union A AAA;  //定义一个联合
	AAA.a = 10;
	printf("%d", AAA.b);  //此时输出的b的值就是a所被赋的值
]

  从这个程序可以看出,所谓的联合实际是开辟出了一个空间,然后确定了空间的首地址,其内部的各个变量就像是这小片内存中访问的“接口”一样,可以随意访问。这也体现出union这个数据类型的灵活之处。至于这个union到底占据多大的内存,是按内部变量占据内存最大的来算。
  下面举一个利用联合实现二进制位连接的妙用

typedef union 
{
	struct
	{
		unsigned short Data : 8;
		unsigned short Addr : 4;
		unsigned short : 4;
	}B;
	unsigned short R; 
}T; 

  这个程序需要处理一个双字节的数据,但是这个双字节包含了三个部分,如果一起赋值较为麻烦,因此考虑使用位域的数据类型,同时利用联合将三个打散的部分连接起来,即赋值的时候各部分单独赋值,读取的时候一次性读取。妙!

6.4 枚举类型(enum)

参考链接

7 case语句使用

  case语句是不需要大括号的,但一定要记得加上break语句,但是之前有遇到case语句编译无法通过的情况,经过查找资料发现可能是 错在case语句中有定义局部变量,这个在C语言中是不允许的。那有没有办法能够实现在case语句中定义局部变量呢?还是有的,那就是加上大括号。如下

#include "stdio.h"

int main()
{
    int a = 10;
    //错误的代码
    switch (a)
    {
    case 1:
        int c = 10;
        break;
    case 2:
        break;
    default:
        break;
    }
    //正确的代码
    switch (a)
    {
    case 1:
    {
        int c = 10;
        break;
    }
    case 2:
        break;
    default:
        break;
    }
}

大括号在C语言中有 划分域 的作用,域内和域外相对独立。众所周知,C语言不允许变量重复定义,但如果一个在域内一个在域外,那是可以的,类似于局部变量和全局变量:

#include "stdio.h"

int main()
{
    int a = 1;
    {
        int a = 2;
        printf("%d\n", a);
    }
    printf("%d\n", a);
}

参考链接

8 函数返回一个数组类型的局部变量

  我们都知道,如果要返回一个数组变量,其实只能返回一个地址,但是函数执行完毕后,它返回的地址实际上就不能访问了,或者其对应的内容就不是原本想要的了,这个时候就会有报错:address of stack memory associated with local variable ‘xxx‘ returned,那怎么办呢?

  其中有一种解决办法是,将局部数组变量定义为static,这样返回的地址就是有意义的了。

  但这样其实还存在一个问题,那就是当有这个函数被同时调用,那么这个地址指向的内存就会被反复读写,容易造成内存泄漏,而且该位置的值的准确性也无法确定。

  因此建议:如果有数组输出,建议是作为参数传入,这样在调用该函数时,就先定义一个输出数组,作为参数传入函数,这样函数的作用就是给这个数组进行重新赋值,或者说修改。

记住不要定义指针,因为指针声明时其实只是一个常量,它没有分配地址空间。

  但这样其实很不优雅,因为这个函数的作用就是读写一个全局变量,且每次调用之前都要确保该全局变量已定义,非常麻烦。还有一种更优雅的方式,就是将数组变量放进一个结构体内部,这样只需要定义一个结构体即可,而且后期还可以进行维护,比如添加字段,改变数据类型等,要方便得多。

9 函数传参过程中的隐式数据转换

  最近在编程的过程中遇到一个不应该遇到的问题,那就是将不符合参数类型的参数传给函数,比如这个:

/*定时器延时ms*/
void delay_ms(char n)
{
	while(n--)
	{
		time_delay = 0;
		while(time_delay <= 10)
			_nop_();
	}
}

函数逻辑写得没问题,但是如果传大于char类型范围的数:如1000,就开始出问题了,因为函数在被调用时,会自动将传入的数据转换为设定的类型,这是隐式数据类型转换的一种。类似的例子还有就是参数设定为unsigned类型,但是传入的却是一个负数,这种可能会出现一个很大的值。

  为了避免这种问题,建议采用typeof关键词来获取数据类型,先进行数据处理之后再作为函数参数传入。这里举一个例子。源码如下:

void shift_bits(int num_of_595, uint64_t data)
{
	RCK_SET(1);
	SCK_SET(1);
	for(uint8_t i=0; i<num_of_595*8; i++)
	{
		DIO_SET(data >> (num_of_595*8-1));  //先取最高位
		data <<= 1;     //左移一位,次高位变最高位
		SCK_SET(0);
		for_delay_us(10);
		SCK_SET(1);
	}
	RCK_SET(0);    //一起输出锁存的数字
	for_delay_us(10);
	RCK_SET(1);
}

这个函数实现的功能就是将传入的参数依次移位,但是为了提高兼容性,这里允许传入不同长度的数据,有8位,16位,最多兼容64位,但是因为参数类型是64位的,那么其他更短的数据会进行数据转换,也就是在高位补零,那这样就会影响到移位操作。解决办法就是使用typeof关键词来获取数据类型,然后再传入函数,如下所示。

#include <stdio.h>

#define FUNC(x) { \   //用宏定义代替函数调用
	typeof(x) tmp = 0xffff; \  //先得到全为1且数据长度一致的变量
	int bits = 1; while (tmp>>=1) bits++; \ //得到移位的长度
	func(bits, x); \  //依次调用,将需要移位的长度也作为参数传入
}
void func(int bits, unsigned short x) {
	while ((--bits)>=0) printf("%d", (x&(1<<bits))!=0);
	puts("");
}
int main() {
	unsigned char a = 9; FUNC(a)
	unsigned short b = 10; FUNC(b)
	return 0;
}

10 函数指针

  在一些比较大的C语言SDK中,经常可以看到函数指针的代码:

11 其他

11.1 sizeof关键词

  没错!sizeof是关键词!不是函数!使用方法可以参考下面这个链接。

11.2 assert

  assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。

库函数: assert.h
原型定义: void assert( int expression );

  assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
  通俗来讲,程序中可以通过assert这个宏来断言。如果断言非真,程序停止,标准错误输出一条具体的错误信息。
  assert()的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。因此可以考虑使用预编译语法来实现仅在调试时包含

参考链接

11.3 __asm

  __asm关键字用于调用内联汇编程序,并且可在 C 或 C++ 语句合法时出现。 类似可能会有的宏定义如_asm, __ASM都是这个意思

  使用方法:

  • __asm后跟一个程序集指令、一组括在大括号中的指令或者至少一对空大括号。
  • __asm与大括号一起使用,则该关键字表示大括号之间的每一行都是一条汇编语言语句。
    _asm { mov al, 2  mov dx, 0xD007 out al, dx }
    
  • __asm不与大括号一起使用,放在代码行首部,则 __asm关键字表示此行的其余部分是一条汇编语言语句。
    __asm mov al, 2
    __asm mov dx, 0xD007
    __asm out al, dx
    
  • __asm做语句分隔符,可将汇编语句放在同一行代码中。
    __asm mov al, 2 __asm mov dx, 0xD007 __asm out al, dx
    

参考链接

  volatile 或volatile是可选的,你可以用它也可以不用它。如果你用了它,则是向GCC声明“不要动我所写的Instruction List,我需要原封不动的保留每一条指令”,否则当你使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。

参考链接

  • 8
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记录无知岁月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值