1. const关键字的作用


1)修饰常变量

        const用来修饰只读变量。

        如果一个变量(局部变量或全局变量)被const修饰,那么它的值就不能再被改变。

2) 增强程序的健壮性    

        #define也可以用来定义常量(标识符常量),但#define只是对值进行简单的替换,并不会进行类型检查;

        const可以保护被修饰的东西,防止意外修改,增强程序的健壮性。

3) 节省空间,提高编译效率  

        编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。在程序运行过程中只有一个备份。在编译时确定值。(只读变量,放在静态区)

        #define 定义的宏常量在内存中有若干个备份。在预编译确定其值。

4)使用

        修饰变量,数组,指针,函数的参数,函数的返回值。

        const修饰的变量必须在定义的同时初始化。

2. static关键字的作用

1)修饰局部变量:static修饰局部变量时,会改变其存储位置,将其从栈区转移到静态区,所以出了作用域不销毁。定义在函数体里的变量,只能在函数体中使用。但是出了函数体不销毁,下次仍能使用。

2)修饰全局变量:static修饰全局变量时,存储在静态区。此变量只能在当前源文件内使用,其他源文件中无法使用。

3)修饰函数:static修饰函数时,此函数只能在当前源文件内使用,其他源文件中无法使用。

3. 关键字volatile的作用


       volatile是一个类型修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,编译器不会对该变量进行优化,而是直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

在以下几种情况中会用到volatile:

1) 并行设备的硬件寄存器(如状态寄存器) 

        存储器映射的硬件寄存器通常要加volatile,因为每次对他读写都可能有不同的意义。
2) 中断服务程序中修改的供其他程序检测的变量

        中断服务函数中对某变量进行修改时,若主程序中没有修改该变量,则编译器优化后可能只从内存中读取到寄存器中一次,之后每次只从寄存器中读取变量的副本,导致中断服务程序的操作短路,所以需要使用volatile对变量进行修饰,告诉编译器不对其优化。

3) 多任务环境下个任务间共享标志,应该加volatile      

        多个任务都可以修改共享标志的值,编译器优化后会把变量读取到寄存器中,之后再取变量值时都是从寄存器读取,当内存变量或寄存器变量因别的线程而改变了值时,该寄存器的值不会改变,若不使用volatile修饰,会导致应用程序读取的值与实际的变量值不一致。

5. 关于volatile的面试题
1) 一个参数既可以是const还可以是volatile吗?       

        可以,比如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2) 一个指针可以是volatile吗?

        可以,当一个中断服务子程序修改一个指向buffer的指针时。

3) 下面的函数有什么错误?

int square(volatile int* ptr)
{
    return *ptr * *ptr;    
}
        程序的目的是返回指针ptr指向的值的平方,但ptr指向的是volatile型的变量,所以编译器将产生类似于如下效果:

int square(volatile int* ptr)
{
    int a, b;
 
    a = *ptr;
    b = *ptr;
    return a * b;
}
        由于*ptr的值可能会被意想不到的改变,所以a和b的值可能不同,导致结果与预期不一致,正确的代码如下所示:

long square(volatile int* ptr)
{
    int a;
 
    a = *ptr;
    return a * a;
}

4. typedef和#define的区别

1)  原理不同

        #define是预处理指令,预处理时只进行简单而机械的字符串替换,不做正确性检查;

        typedef是关键字,在编译处理时会做正确性检查,它在自己的作用域内给一个已经存在的类型一个别名,但不能在函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。

2)功能不同

       typedef用来定义类型的别名,起到类型易于记忆的功能。

        #define不只可以为类型取别名,还可以定义常量、变量、编译开关等。
3)作用域不同 

        #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。

5. C语言编译的四个步骤


1) 预处理:展开宏和包含头文件,生成.i文件;(include + #)

2) 编译:编译器对将预处理后的源代码进行词法分析、语法分析、语义分析和优化,最后翻译成汇编码,生成.s文件;

3)汇编:汇编器将汇编码转换为机器码,生成.o文件;

4) 链接:链接器将多个目标和库文件等组合在一起,并解决外部引用,最终生成可执行文件

6. 堆、栈和静态区的区别

栈:保存局部变量;线程共享,栈上的内容只在函数范围内存在,不同函数存在不同栈帧,同一个线程上函数的栈帧共享栈;由编译器分配,效率高;空间较小(一般1MB以内)。

堆:进程共享,其生命周期由free或delete决定,在未释放之前一直存在知道程序结束;由程序员分配,自由使用,空间较大,但是需要整块分配,用以产生内存碎片,效率低。

静态区:保存全局变量和static变量(全局static和局部static);由编译器在编译时分配;整个程序的生命周期内都存在。

7. 栈的作用


        栈的作用有:函数调用、中断处理、程序调试、操作系统中的任务调度,介绍如下:

1) 函数调用:当一个函数被调用时,程序会把该函数的参数、返回地址、局部变量等信息存入栈中,成后当函数执行完,栈会依次弹出这些信息,并恢复调用函数前的状态(栈先进后出);

2) 中断处理:系统发生硬中断或软中断时,操作系统会自动保存当前进程的执行现场(包括CPU状态、程序计数器、寄存器等信息)到栈中,然后将CPU控制权转交给中断处理函数,中断处理函数执行完成后,栈会弹出之前保存的执行现场信息,恢复到中断前的状态;

3) 程序调试:调试程序时,程序员可根据栈中的内容(如变量的值、函数调用栈、异常处理等)来查看当前程序的执行状态;

4) 线程调度:栈是多线程编程的基石,每个线程都必须有一个自己的栈,用来存放本线程运行时各个函数的临时变量,和维护线程中函数调用和函数返回时的函数调用关系以及保存函数运行现场。

8. 调用函数时的入栈顺序


1) 第一个入栈的是主函数中调用函数处的下一条执行语句的地址,也就是函数的返回地址

2) 接着入栈的是函数的各个参数,入栈顺序为从右向左(为了适应可变参函数的参数个数不确定的情况)依次将参数入栈;

3) 然后是函数内部的局部变量(注意,static变量是不入栈的);

4) 函数执行完成后按照先进后出的顺序依次出栈。

        

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐