评论

收藏

[C++] C/C++函数与变量前面的标识符的作用

编程语言 编程语言 发布于:2021-07-10 13:46 | 阅读数:554 | 评论:0

DSC0000.png

作者:良知犹存
转载授权以及围观->欢迎添加Wx:Allen-Iverson-me-LYN
 
缅怀逝者,向英雄致敬。
愿山河无恙,国泰民安。

 
    在用C/C++写代码的时候我们经常会使用一些标识符,置于函数或者变量之前,这些标识符有些是限制函数或者变量的使用,有些是提高函数的执行效率,有些则是特殊函数的标志,下面我们来进行介绍。标识符有很多,此处仅介绍常用的标识符,很多知识是边写边边学习,参考了很多前辈的文章。所以这是篇站在巨人肩膀上的文章,希望可以帮到大家。
一、static 静态符号
  概念介绍    static 是一个用来控制变量储存方式以及可见性的关键字。经常用于变量函数的修饰。通过使用的位置分为,静态函数,静态全局变量,静态局部变量。  
    具体使用方法下面详细介绍:
  具体static使用区别1.static函数
 
    普通函数在头文件进行申明,其他文件使用这个函数,只需要在文件的内包含使用函数的头即可。而static函数的作用域仅限于定义函数的文件,其他处不可见,所以static函数在内存中只有一份,普通函数在每个被调用的文件中都维持一份拷贝。
 

  •  
static void fun(void) /*局部函数*/{   ....  }void fun(void) /*普通函数*/{   ....  }   
2.static全局变量

 
      首先在内存的位置,两者都是全局变量,初始化的变量都置于(data)数据段,非初始化的变量置于非初始化数据段(boss)。两者主要区别是,static全局变量限制了作用域,static全局变量的只能作用在定义的源文件中,因此可以避免被其他文件误用。而普通全局变量作用域可以通过extern被整个源文件使用。

  •  
static Robot_t Robot;/*静态全局变量*/Robot_t Robot1;/*普通全局变量*/
 
3.static局部变量


    普通局部变量加上static之后成为静态变量,此变量改变了储存方式,只执行初始化一次,延长了局部变量的声明周期。
    普通局部变量在栈区,使用的时候由编译器自动分配释放,变量用完即释放。static局部变量在全局区(静态区),程序结束后有系统进行释放。
 
     static局部变量只被初始化一次,程序执行的时候变量的下一次值依据上一次结果值

  •  
void fun(void) /*普通函数*/{   static uint8_t robot;/*静态局部变量*/      uint8_t robot1;/*局部变量*/}
关键字使用周期作用域extern程序执行外部(整个程序)static程序执行内部(仅目标文件)auto, register功能执行(没有)
二、volatile与register类型修饰符
  概念介绍      
        volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
        
  一个函数在执行时会使用多个局部变量,包括形参。这些变量使用频度差异较大,编译器会按照使用频度来规划,将常用变量放到闲置寄存器里,使得运算速度得到很大的提高。不常用的那些就放内存里,每次用到就去内存里拿。
     
         register也是一种类型修饰符,但是它声明作用的变量刚好与volatile相反,volatile令程序执行的时候,该变量读取的时候每一次需要从内存读取。但是内存读取的速度和直接在寄存器里面读取速度可以差千百倍,所以volatile相当于为了数据安全,令程序每次从变量初始化的内存进行读取,而register修饰符向编译器建议此变量因为使用非常频繁,将此变量置于寄存器读取,提高程序效率。
 

  •  
volatile  uint8_t robot;register  uint8_t robot1;
 
  volatile的含义就是明确告诉编译器,这个变量在每次访问时,都走内存,而不要用寄存器来缓存。这样在抢占式多任务里,就能确保每次拿到最新的值。而register的意思则相反,告诉编译器这个变量尽可能放到寄存器里,以提高速度。当然如果寄存器不够用,那就还是放内存里。
作者:gashero
  volatile使用场景

  •  中断服务程序里面需要被别的线程或者别的程序读取的变量
  • 多线程或者多任务共享的变量
  • 储存器映射的硬件寄存器通常也要加volatile说明,因为每次对这些寄存器的读写都可能出现不同含义。  
 register使用场景

  •   修饰的变量需要是CPU识别的变量,所以得是个单个的值,长度需要小于等于整型的长度。
  •   因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。
  •   在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以抵消装入寄存器和存储变量所带来的额外开销。 
  ‍
  继续学习      按照与CPU的远近来分,离CPU最近的是寄存器,然后是缓存,最后是内存。寄存器是最贴近CPU的,而且CPU只在寄存器中进行存取。寄存的意思是暂时存放数据,不用每次都从内存中取,它是一个临时的存放数据的空间。

    而寄存器的数据又来源于内存,于是 CPU <-- 寄存器 <-- 内存,这就是它们之间的信息交换。
    那么为什么还需要缓存呢?因为如果频繁地操作内存中同一地址上的数据会影响速度,于是就在寄存器和内存之间设置一个缓存,把使用频繁的数据暂时保存到缓存,如果寄存器需要读取内存中同一地址上的数据,就不用大老远地再去访问内存,直接从缓存中读取即可。
 
DSC0001.png

 
    缓存的速度远高于内存,价格也是如此。注意:缓存的容量是有限的,寄存器只能从缓存中读取到部分数据,对于使用不是很频繁的数据,会绕过缓存,直接到内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。
    从经济和速度的综合考虑,缓存又被分为一级缓存、二级缓存和三级缓存,它们的存取速度和价格依次降低,容量依次增加。购买到的CPU一般会标出三级缓存的容量。
 
三、const extern
  概念介绍      const关键词修饰的数据类型表示数据为不可修改的常量,数据只读不可写,变量不可被初始化。const修饰的变量一定程度增加程序的安全性与可靠性。const可以修饰变量,参数,返回值。
  具体使用方法下面详细介绍:
  具体const使用区别1.const修饰变量

  •  
const int a=0;/*const修饰变量只读不可写*/int const b=0;/*同上*/b = 9;/*error*/a =10;/*error*/
    
    const关键词修饰的变量为常量,只初始化一次,放在ROM中,定义之后后变量的值不可再改变。具体const变量置于内存的位置可以看我的这篇文章

函数内部分配的buffer过大导致堆栈溢出。
     2.const修饰指针

  •  
int const *a; /*表示a指针指向的内容不可被修改*/const int *b;/*表示b指针指向的内容不可被修改*/int *const c;/*表示c指针地址不可被修改*/const int *const d;/*表示d指针地址不可被修改另d指针指向的内容不可修改*/
  const关键词修饰的指针当作为函数参数的时候也是一样的效果。

  •  
void test(int const *a); /*表示a指针指向的内容不可被修改*/void test(const int *b);/*表示b指针指向的内容不可被修改,本来地址也是不可修改,所以无实际意义*/void test(int *const c);/*表示c指针地址不可被修改*/void test(const int *const d);/*表示d指针地址不可被修改另d指针指向的内容不可修改*/
3.const修饰引用与类的成员函数

  •  
void test(const Class& Var); //引用参数在函数内不可以改变void test(const TYPE& Var); //引用参数在函数内为常量不可变class test{public:  int GetCount(void) const ;/*const修表示该函数不可以修改类相关的数据成员*/private:  int num;};int test::GetCount(void) const{  num++;/*error*/}
  继续学习    
       
 
四、__asm汇编标识符
  概念介绍  内嵌汇编标识符,有些时候我们编写程序的时候需要用一些汇编程序执行,这个时候需要用__asm或者asm进行标计。
 
asm能写在任何C++合法语句中,asm还不是C的标准关键字,C11标准未加入正文。
  然asm并不是C的标准关键字(C11标准未加入正文,仅在Annex J中标记为“公共扩展”),但是大多数C实现都将其视为一个关键字asm是C++标准关键字,但是标准没有规定其详细用法。相应的用法为实现定义。
百度百科
 使用方法      asm可以启动内联汇编,但是它不能单独出现,必须接汇编指令、一组被大括号包含的指令或一对空括号。

  •  
__asm volatile("BKPT #01");__asm void SystemReset(void)/*arm中的汇编复位函数*/{ MOV R0, #1       //;  MSR FAULTMASK, R0  //; 清除FAULTMASK 禁止一切中断产生 LDR R0, =0xE000ED0C  //; LDR R1, =0x05FA0004  //; STR R1, [R0]     //; 系统软件复位  deadloop  B deadloop    //; 死循环使程序运行不到下面的代码}
五、 __attribute__((at(address))) 指定地址
  概念介绍     这是__attribute__组合的一个另一个关键词,通过使用该修饰符修饰则变量可被指定为绝对地址。
 使用操作     变量放在ROM的内存中

  •  
__align(32) volatile CPU_INT08U \External_RamMemp[EXTERNAL_MEM_NUM][EXTERNAL_MEMBLOCK_SIZE] \ __attribute__((at(0x08004000)));
 
     变量放在RAM的内存中

  •  
__attribute__((at(0x20000000)))  static Isr isrs[46] = {  Reset_Handler,  NMI_Handler,  HardFault_Handler,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  NULL,  SVC_Handler,  NULL,  NULL,  PendSV_Handler,  SysTick_Handler,}
 
六、__packed与 __attribute__((__packed__))
  概念介绍      这两个限定符都是一个作用,将修饰的结构体或者联合体变量中所有的成员变量为对齐边界设置为一字节对齐,用这两个关键词修饰之后,变量存放在内存的时候,空间压缩至最小,可以最大限度的节省空间。但是空间节省,CPU执行读取效率会下降,因为CPU读取数据的,是有按照整倍数读取CPU最小单位读取,如果数据按照一字节排列,就会导致CPU读取变量的时,无法整倍数读取数据,只能被分割读取。所以使用的时候我们也要注意,数据对齐的时候,考虑程序执行的速率。
 
       具体这两个关键词的使用方法大家可以看我之前的这篇文章,结构体、联合体的成员内存对齐的情况,这篇文章比较详细描述字节对齐的关键字,以及对齐的具体排列方式。

  •  
__packed__ typedef {        uint8_t  a :1;/*强制对齐*/     uint8_t  b :1;     uint8_t  c :1;     uint8_t  d :1;     uint8_t  e :1;       uint8_t  res :3; }StructTypef;typedef struct __attribute__((__packed__)){     uint8_t  a;     char   b;} StructTypef;
 
 

七 、inline内联函数修饰符
  概念解释
  内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。
维基百科
  使用方法    1.内联函数主要用在一些大量访问,并且操作简单的函数上使用。使用内联函数实际是在编译器编译的时候,在内联函数调用的时候像宏一样展开,以代码膨胀为代价,省去了函数调用的开销,从而提高函数的执行效率。 
    
    2.内联函数一般都是置于.h头文件申明,因为内联函数使用时候类似宏定义,编译器会在调用的时候进行展开,所以编译器必须随处可见内联函数的定义,要不然无法实现内联函数的调用。

  •  
/*一般在函数名称的最左边加上inline,函数就变成了内联函数*/inline Robot_t* Robot_Get_Base(void){  return &Robot;}
  优劣       3.内联函数使用需要主要,函数如果过于复杂 或者包含有while循环 for循环的代码部分,不建议使用内联函数。下面分别介绍此两种情况。
 
         代码复杂的函数本身调用执行的时间就多,如果函数进行内联,此时函数进行了内存扩展,但是程序调用的执行速率也没有变快,所以此时用内联函数除了增加内存之外并没有对程序执行速率提升。
 
        有while或者for循环的代码,随着内联函数执行,代码将不停扩展,导致内存消耗代价过高。
  继续学习    
        inline 一般会与static联合起来使用,用于限制static inline 修饰的函数使用范围,就是static限制作用加 inline的内联标识,内联函数一般都是置于.h头文件申明,因为内联函数使用但是具体inline与 static inline 区别可以参照stack overflow的一篇文章,我就不多赘述了。

 

https://stackoverflow.com/questions/7762731/whats-the-difference-between-static-and-static-inline-function。

 

具体参考示例:在ST等LL库的代码中使用了很多的内联函数

DSC0002.png

DSC0003.png

 
八 、__irq中断函数标志
  概念解释        __irq为一个中断函数标识,用来标记此函数为中断函数,这样编译器编译调用此函数的时候,先保护函数入口现场,再执行中断函数,函数执行完毕,恢复中断现场。

  •  
__irq void USART2_IRQHandler(void)if (USART2->ISR & USART_ISR_RXNE){  USART2->ISR &= ~USART_ISR_RXNE;  cyclic_buffer_push(&s_comm.s_cyclic_rx_, USART2->RDR);}
    
    对于不同编译器,__irq在函数名的位置有些区别,为了方便使用高级语言编写异常处理函数,ARM编译器对异常处理函数做了特定扩展,只要使用关键字_irq,这样编译出来的函数就满足异常响应对现场保护和恢复的需要。
 
    目前在ARM系列的芯片,都配备了中断循环嵌套,所以我们在编写中断服务函数的时候一般不需要再次添加__irq标识符。
  使用场景    但是如果我们通过中断向量地址进行函数跳转,此时我们需要添加__irq中断标志。示例如下:


  •  
#define ISR_INT0 (*(unsigned*) (_ISR_STARTADDRESS+0x74))//_ISR_STARTADDRESS+0x74强制转换为指针,指向RAM__irq void EintIsr(void){    ISR=rEXTINTPND; //如果EINT产生时,which_int =1}int main(void){      ISR_INT0 =(unsigned)EintIsr;/*初始化中断函数*/}
  详细步骤    第一步:进行指定中断地址进行内存的分配;
    第二步:进行编写中断服务函数的编写;
    第三步:初始化中断函数,等待中断执行。
 
 
总结如下
1、若不想自己编写中断入口现场保护代码,而且使用中无中断嵌套,在中断函数中用 __irq 来标识我们的中断函数,否则出错;
2、若程序中要使用中断嵌套,对于无中断嵌套功能的ARM来说,一定要自己编写中断入口现场保护代码,而且不能用 __irq 标识我们的中断函数,否则出错。
九。__attribute__((weak))  弱符号标识  
  概念解释
  弱符号(Weak symbol)是链接器在生成ELF文件的过程中使用的一种特殊属性符号。默认情况下,如果没有特别声明,目标文件里面的符号都是强符号。在链接过程中,一个强符号会优先于一个同名的弱符号。相比之下,两个同名的强符号一起链接会出现链接错误。当链接一个可执行文件,弱符号可以不定义。但对于强符号,如果没有定义,连接器会产生一个“符号未定义”错误。
维基百科
DSC0004.png

    在C语言中,函数和初始化的全局变量(包括显示初始化为0)是强符号,未初始化的全局变量是弱符号。

  •  
void __attribute__((weak)) f();int main(void){    if (f)    f();    return 0;}
 

  使用方法    如果这个关键字用在函数定义上面,一般情况下和一般函数没有两样。但是当有一个同名函数但是不带__weak被定义时,所有对这个函数的调用都是指向后者(不带__weak那个), 如果有两个一样的函数都用了__weak,那么真正调用那个,就要看链接器了。
    弱符号是目标文件和动态库中的符号定义,可能会被其他符号定义覆盖,如果加载程序未找到任何定义,则其值为零。换句话说,我们可以定义个链接时不需要解释的符号。
  继续学习
  在一个程序中出现问题还算好,毕竟代码都在一起。如果你使用的动态库或者静态库中有未初始化的全局变量,并且恰好也和你定义的重名,结果如何?我尝试过,和上面一样,冲突的两个变量地址也相同。而这个时候你如果没有库的源码,当发生了问题,变量被修改,你估计要走很多弯路才能想到是库改了你的变量。这是我曾经解决过的一个问题。从那之后,我要求我们公司所有库的源码中不可以出现非static全局变量。
常高伟
而弱符号如此多的不便,为什么还要用呢?
    使用弱符号的目的是,当不确定这个符号是否被定义的情况下,链接器也可以成功链接出ELF文件,适用于某些模块还未实现的情况下,其他模块的先行调试。 弱符号在C语言和C++语言的规范里面没有被提及,所以使用弱符号的代码,移植性不是非常好。
 
 
DSC0005.png
 
 

  
关注下面的标签,发现更多相似文章