八股文的意义在于,如果你真正理解这些八股,那么你的编程语言才达到了入门级别,如果你不懂,你绝对还没有入门编程语言,也就是说在接下来工作中,受限于基础的薄弱,你的工作进展会非常的慢,你工作高度也会受限于你的基础。这是所有企业都会考察八股原因

目录

1 八股如何回答

2 c语言常见面试题

2.1 GCC编译链接过程

2.2 static 关键字的理解


    基本按照一下三个步骤回答:

               (1)概念什么,他的原理什么本质什么

               (2)应用场景什么需要注意的点?

               (3)你在什么情况下用过,你的心得是什么

        gcc编译过程分为4个阶段预处理编译汇编链接
        预处理:头⽂件替换、宏替换条件编译删除注释使用gcc -E 生成 *.i 预处理文件
        编译:主要进⾏词法语法语义分析等,检查⽆误使用gcc -S 将*.i 文件编译成汇编⽂件。
        汇编使用gcc -c 将汇编⽂件转换成进制⽬标⽂件 *.o
        链接:将项⽬中的各个进制⽂件+所需的库+启动代码链接成可执⾏⽂件,链接分为静态

                接和动态链接,具体可以参考之前写的c语言编码详解动态链接与静态链接的区别

        static 修饰变量作用域只在本文件中使用,对于静态局部变量,只能在函数使用。一般有

        静态全局变量静态局部变量静态函数

        静态局部变量

        和普通局部变量不同静态局部变量也是定义函数内部的,静态局部变量所在的函数在调

        用多次时,只有第一次才经历变量定义初始化,以后多次调用时不再定义和初始化,而

        是维持之前上一次调用执行这个变量的值。下次接着来使用。但是他的作用域仅限于当

        前函数内。存储全局区的静态存储区 .data段。

        静态全局变量

        初始化一次,但是作用域当前文件/模块中。存储在静态存储区 .data段,区别全局变量

        存储在.bss段。

         静态函数只在本模块或者文件使用。被限定范围

        变量/函数声明

        声明变量/函数存在程序中的某个位置也就是后面程序知道这个函数或者变量类型

        但分配内存

        变量/函数的定义:

        我们定义变量/函数时,除了声明作用外,它还为该变量/函数分配内存

        NULL指针
        NULL用于指示指针指向有效位置。一般在初始化时候指针指向任何位置时,设置

        为NULL。当由它指向的内存在程序中间被释放时,我们应该使指针为NULL。

        悬空指针

        悬空指针没有指向正确内存位置指针。一般出现指针指向的内存已经被释放空间,或

        者数组越界时,此时的指针指向了一块已经被释放掉的或者不属于自己空间,就会出现

        空指针,这时对指针的操作比较危险的,尤其在c++中 delete 对象后,c++现在引入智能

        针可以一定程度上有效的解决这个问题

        野指针
        就是只声明没有初始化过的指针,他可能指向任何内存。

        指针常量:

        指的是指针指向的地址是不变的,但是其地址中变量的值是可变的,例如  int * const p;

        常量指针:

        指的是指针指向的地址中的变量的值是不变的,但是指针的地址可以改变,例如 const int *p;

        指向常量的指针常量:

        指的是指针指向的地址地址中的变量的值都是不可以改变的,例如 const int * const p;

        本质:引用别名,本质是一个指针常量;指针是地址,本质是普通指针。

        对比:

        a 指针是独立可以指向空值,这时我们为指针分配了内存。而引用必须初始化指定

            对象自始至终只能依附于同一个变量,他只是别名

        b 指针的大小对于32位系统来说始终是4字节引用大小关联变量的大小

        c 引用是一块内存的别名,不会改变其指向,比较安全,但是指针指向的一块内存地址,可

         以改变其指向,还可以为NULL,会出现野指针和悬空指针的情况。

        值传递需要拷贝效率比较低,不会改变原值
        地址/指针传递传递变量的地址,直接操作内存,指向的空间可以被修改
        引用传递,只在c++中有,本质上也是传递的地址,但是指向的空间不能改变,相当于间接寻址

        当结构体中有指针成员时候容易出现浅拷⻉与深拷⻉的问题

        浅拷贝存在的问题:当出现类的等号赋值时,系统会调用默认拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中两个指针将指向同一个地址,当对象结束时,会调用两次free函数,此时p2已经是野指针,指向的内存空间已经被释放掉,再次free报错;这时,必须采用拷贝

        深拷⻉就是,让两个结构体变量的指针成员分别指向不同堆区空间,只是空间内容拷⻉⼀份,这样在各个结构体变量释放的时候就不会出现多次释放同⼀段堆区空间问题.

        #include <> 是到系统指定目录去寻找头文件;#include “” 先到项目指定目录寻找头文件,再

        到系统指定目录去寻找头文件

        
        宏定义又称为宏代换、宏替换,简称“宏”。

        ifndef/define/endif 的作用
        防止头文件重复包含编译头文件重复包含会增大程序大小重复编译增加编译时间

        内联区别
        a. 内联函数在编译时展开,宏在预处理展开
        b. 内联函数直接嵌入目标代码中,宏是简单的做文本替换;
        c. 内联函数有类型检测语法判断功能,而宏没有
        d. inline函数是函数,宏不是;

        typedef区别
        #define 用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同

        a. typedef 仅限于为类型定义符号名称, 定义一种类型别名,而不只是简单的宏替换

        b. #define 不仅可以为类型定义别名,也能为数值定义别名比如您可以定义 1 为 ONE。
        c. typedef 编译阶段检查错误,#define 预处理阶段不检查错误

        const区别
        a. 数据类型const修饰的变量有明确的类型,而宏没有明确的数据类型
        b. 安全方面:const修饰的变量会被编译器检查,而宏没有安全检查
        c. 内存分配:const修饰的变量只会在第一次赋值时分配内存,而宏是直接替换,每次替换后

                的变量都会分配内存
        d. 作用场所:const修饰的变量作用在编译、运行过程中,而宏作用在预编译中。从编译器

        的角度讲,最大的优势是简单,方便。因为预处理就可以解决掉#define,用不着编译器
        e. 代码调试:const方便调试,而宏在预编译中进行所以没有办法进行调试

        a符号signed ,首位为符号位,0正 1负 ,使用源码和反码表示,无法表示出(对于

        8bit)-128,使用补码表示:10000000 表示-128,一般的机器都是补码表示法范围

         – (2^(n-1)) ~ 2^(n-1) – 1

        b. 无符号数 unsigned,表示的正数的范围会变大 ,范围 为0 ~ 2^n -1

        c. 计算机中的补码将计算机中的减法运算统一加法运算

        指针数组:本质上是数组,数组中的变量为指针,用于二维数组和矩阵的行,int *a[3]; 

        数组指针:本质上是指针,指向数组的指针,指向的是整个数组,数组的首地址,int (*a) [3];

                           int array[3] = {1,2,3}; a = &array;是正确的,a = array;是错误的;

                           取值:*(*a + 1) 等价于 (*a)[1]

        指针:内存中每⼀个字节都会分配编号这个编号就是地址, ⽽指针就是内存单元的编号。一个变量的地址就称为该变量的指针他保存的是一个地址。
指针变量::c语言有很多种变量,每种变量都会储存一种数据,而指针变量就是专门来储存指针的变量,本质是变量 只是该变量存放的是空间的地址编号
        二级指针:指针本身也是一个变量,也要占用内存空间,而二级指针就是指向这块变量的指针。一般二级指针用在二维数组中。
        int *p;
        p=&a;

        int *p就是指针变量
        对a取地址,p就是一个指针用来保存地址。

        栈区(stack):存放函数的形参返回值、const 修饰局部变量,局部变量等,由编译器自动分配释放,空间大小

                一般由系统确定,地址由高到低

        堆区heap):程序员自由申请释放,空间比较大,地址一般由低到高增长,使用malloc()

                和free()来管理

        全局/静态区:存放全局变量和静态变量(全局静态变量和局部静态变量),全局区存放未初

                始化的放在全局区(.bss段),初始化的放在静态区(.data段)

        常量区:只读区域.rodata,存放一些常量,比如 const修饰字符串常量 ,const修饰的其

                他变量存储在相应的位置       

        代码区: 为只读区域text段,存放程序二进制代码 .,由操作系统管理,函数存放这里

        

        申请方式
        stack:由系统自动分配。例如声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间
        heap:需要程序员自己申请,并指明大小,在c中malloc函数

        申请大小限制
        栈:栈是向低地址扩展数据结构,是一块连续的内存的区域。这句话的意思

        是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,

        将提示overflow。因此,能从栈获得的空间较小。

        堆:堆是向高地址扩展数据结构,是不连续的内存区域。这是由于系统是用链表存储

        空闲内存地址的,自然是不连续的,而链表遍历方向是由低地址向高地址。堆的大小受限

        于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。堆的访问

        需要访存两次

        申请效率的比较
        栈:由系统自动分配,速度较快。但程序员是无法控制的。
        堆:是由new或者malloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最

        方便.

        堆和栈中的存储内容
        栈:局部变量和形参,函数返回值
        堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容程序员安排。

        a. array的元素存在于栈上, 而vector只有元信息存在于栈上,数据存在于堆上。所以vector不会爆栈
        b. vector顺序容器,其利用连续的内存空间存储元素,但是其内存空间大小是能够改变的。
        c. vector效率偏低,因为当向vector添加元素的时候,内存空间不够,需要重新申请更大的空间,由于vector是连续内存空间的,因此其申请更多空间的时候,可能整个位置发生改变,需要将原来空间里的数据拷贝过去。

        结构体中的成员拥有独⽴的空间,共用体也叫联合体,共⽤体的成员共享同⼀块空间,但是每个共⽤体成员能访问共⽤区的空间⼤⼩是由成员⾃身的类型决定。
共用体使用覆盖技术,成员变量相互覆盖,可以用来求系统的大小端,共用体的大小由它的最大

的变量决定,当然要满足对齐原则利用联合体实现大小端的判断参考我之前写的详解大端序和小端序(c语言实现

union un_big_little_endian{

        int a;

        char b;

}; // 4个字节大小,变量a 和b 共享一块空间

        extern 是c语言的一个关键字,可以用于修饰全局变量和函数,可以表示申明一个变量和函数

        ,可以放在任意位置,后续就可以引用该变量或者函数;extern 也用于文件编程,在其他

        文件申明一个变量和函数,为了在该文件引用,申明是不分配内存的,只有定义才会分配

        内存。

        extern 申明时不要初始化值,否则会变成定义。

        register作用局部变量,不能作用于全局变量;

        register 定义的类似必须是cpu寄存器可以接收的类型;

        register 只是申请为寄存器变量,并不一定成功;

        register 修饰的变量c语言不能用取地址来获取,因为不在内存中;但是c++可以,取地址就表

                示register失效

        volatile修饰的变量表示只能从内存中取值,不能被优化,由于程序在编译运行中,会对变量

        做优化比如会将值拿到寄存器中,volatile修饰的变量就是不允许这种优化,以防止多个

        线程共享此变量时,取到的值不同步

        c99 中的关键字,编译使用 std=c99使用, restrict 修饰指针,表示该指针指向的地址中的变

        量的值,只能通过该指针进行修改,这样程序运行时,得到的结果具有一致性程序就会

        对代码进行一定的优化。可以参考之前我写的C99中的restrict关键字详解

        数组中元素数组中的数据类型是一致的,数组的存储是在一块连续的空间中,因此数组的

        访问复杂度为O(1)。

        一维数组:int a[3]; 

                数组名数组名代表数组首元素的地址,a 与 &a[0]等价,sizeof(a) = 12,代表数组的大

                                小

                取值:a[1] 等价 *(a+1)

                地址:&a[0] + 1等价于 a + 1,加1指的是数组中元素地址的加1个元素大小的地址;

                           &a 表示数组地址,&a + 1表示整个数组的地址加1个数组大小的地址;

        二维数组:int a[3][3];

                二维数组的本质是一维数组,二维数组可以利用二维指针,或者指针数组来操作

                数组名:本质上是首行元素的地址,表示上于首元素的地址是一样的,sizeof(a) = 36 ,

                        代表整个二维数组的大小;sizeof(a[0]) = 12 ,代表一行元素的大小,此时a代表

                        个数组,a[0]代表第一行

                取值:a[1][1] 等价 *(&a[0][0] +1)

                地址:&a[0][0] + 1 ,单个元素的地址加1个元素大小的地址

                           &a + 1,表示整个数组地址加1个二维数组大小的地址

                           &a[0] + 1,表示首行元素的地址加1行大小的地址 

        attribute:属性,主要是用来在函数或数据声明设置属性,与编译器相关可以设置多种属

        性。包括对齐自定义段、格式、有无内联、有无使用等。

        例如int unint_val __attribute__((section(“.data”))); 定义该变量在.data段

                   Header_hand_herder __attribute__ ((aligned(16)));定义该变量16字节对齐

                   struct Header_t

                  {

                        int version[16];

                        uint8_t lhi;

                  }__attribute__((packed)); //定义结构体减少对齐

        (1)第一个成员的首地址为0.

        (2)每个成员的首地址是自身大小的整数

        (3)结构体的总大小,为其成员中所含最大类型的整数倍。

        具体可以参考我之前写的字节对齐原则

 1 int Add(int x,int y)
 2 {
 3     int sum = 0;
 4     sum = x + y;
 5     return sum;
 6 }
 7 
 8 int main ()
 9 {
10     int a = 10;
11     int b = 12;
12     int ret = 0;
13     ret = Add(a,b);
14     return 0;
15 }

        1、参数拷贝(参数实例化)分配内存。
        2、保存当前指令的下一条指令,并跳转到被调函数。

  main数中操作

        接下来是调用Add函数并执行的一些操作,包括:
        1、移动ebpesp形成新的栈帧结构。
        2、压栈(push)形成临时变量并执行相关操作
         在一个栈中,依据函数调用关系,发起调用的函数(caller)的栈帧在下面(高地址方向),

         被调用的函数的栈帧在上面。每发生一次函数调用,便产生一个新的栈帧,当一个函数返回

         时,这个函数所对应的栈帧被清除eliminated)
        3、return一个值。

  Add数中操作

        被调函数完成相关操作后需返回到原函数中执行刚才保存的下一条指令,操作如下
        1、出栈pop)。
        2、恢复main函数的栈帧结构。(pop
        3、返回main函数
        这些操作也在Add函数中进行。 至此,在main函数中调用Add函数的整个过程已经完成
        总结起来整个过程就三步:
                1)根据调用的函数名找到函数入口;
                2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
                3)函数执行完后,释放函数在栈中的申请的参数和变量的空间,最后返回值(如果有的话)

        详细参考函数调用深入剖析

        分析函数调用中的x86架构的寄存器参考x86寄存器

                      

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注