C语言常见面试题汇总

gcc的编译过程?

gcc编译过程分为4个阶段:预处理、编译、汇编、链接。
预处理:头⽂件包含、宏替换、条件编译、删除注释
编译:主要进⾏词法、语法、语义分析等,检查⽆误后将预处理好的⽂件编译成汇编⽂件。
汇编:将汇编⽂件转换成 ⼆进制⽬标⽂件
链接:将项⽬中的各个⼆进制⽂件+所需的库+启动代码链接成可执⾏⽂件

static关键字

全局变量存储在静态存储区中,局部变量在堆栈中

  • 静态局部变量:和普通局部变量不同。静态局部变量也是定义在函数内部的,静态局部变量所在的函数在调用多次时,只有第一次才经历变量定义和初始化,以后多次在调用时不再定义和初始化,而是维持之前上一次调用时执行后这个变量的值。下次接着来使用。但是他的作用域仅限于当前函数内。
  • 静态全局变量也只初始化一次,但是作用域在当前文件/模块中。每次调用当前文件时,会使用上一次保存的值。
  • 静态函数:只在本模块或者文件中使用。被限定了范围。

变量/函数的声明和定义之间有什么区别

变量/函数的声明仅声明变量/函数存在于程序中的某个位置也就是后面程序会知道这个函数或者变量的类型,但不分配内存
关于定义,当我们定义变量/函数时,除了声明的作用外,它还为该变量/函数分配内存。

各种指针

  • NULL指针
    NULL用于指示指针未指向有效位置。理想情况下,如果在声明时不知道指针的值,则应将指针初始化为NULL。
    当由它指向的内存在程序中间被释放时,我们应该使指针为NULL。

  • 悬空指针
    悬空指针是没有指向正确内存位置的指针。当删除或释放对象时,如果不修改指针的值或者不置为NULL,就会出现悬空指针。
    这时这个指针指向的内存可能被分配给了其他变量就会造成错误。所以是比较危险的。

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

指针常量与常量指针

int * const p //指针常量:指针在前,常量在后
这个p只能指向一个位置,而不能指向其他位置。指向的变量值可以改变。
const int *p = &a; //常量指针 : 常量在前,指针在后
指向的变量值不能改变,但是可以改变这个指针指向的位置。

“引用”与指针的区别是什么?

引用是C++的概念在C中叫取地址符号

  1. 本质:引用是别名,指针是地址
  2. 指针是独立的可以指向空值,这时我们为指针分配了内存。而引用必须初始化指定的对象自始至终只能依附于同一个变量,他只是别名。标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体随编译器

C语言参数传递方式:

值传递(swap1函数)
地址/指针传递(swap2函数)
引用传递(swap3函数)
void swap1(int,int); //值传递
void swap2(int *p1,int *p2); //地址传递
void swap3(int &a,int &b); //引用传递

这也就证明了地址传递和引用传递都是直接传递的变量所在的地址,函数的主要的作用就是对存储在地址中的变量进行直接的操作
而按值传递则不会修改原值
实际上指针传递也是值传递,他传递的是指针变量的值。
而引用则相当于间接寻址,他直接传递的是地址

c语言没有引用传递,只有c++有

结构体的浅拷⻉与深拷⻉

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

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

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

#include<> 与#include ""的区别?

include<>到系统指定⽬录寻找头⽂件,#include ""先到项⽬所在⽬录寻找头⽂件,如果没有找再到系统指定的⽬录下寻找

宏定义define

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

ifndef/define/endif 的作用?

防止头文件被重复包含和编译。 头文件重复包含会增大程序大小,重复编译增加编译时间。

与内联区别

  1. 内联函数在编译时展开,宏在预处理时展开;
  2. 内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
  3. 内联函数有类型检测、语法判断等功能,而宏没有;
  4. inline函数是函数,宏不是;

与typedef区别

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

  • typedef 仅限于为类型定义符号名称, 定义一种类型的别名,而不只是简单的宏替换 。#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 编译阶段会检查错误,#define 预处理阶段不检查错误。

与const区别

1.数据类型:const修饰的变量有明确的类型,而宏没有明确的数据类型
2.安全方面:const修饰的变量会被编译器检查,而宏没有安全检查
3.内存分配:const修饰的变量只会在第一次赋值时分配内存,而宏是直接替换,每次替换后的变量都会分配内存
4.作用场所:const修饰的变量作用在编译、运行的过程中,而宏作用在预编译中。从编译器的角度讲,最大的优势是简单,方便。因为预处理就可以解决掉#define,不必让编译器来处理这个。
5.代码调试:const方便调试,而宏在预编译中进行所以没有办法进行调试。

在程序语句中使用的常量的地方, 最好是使用const定义,在这方面来说, const只有优势,没有劣势.如果要说const劣势的地方,那就是它不能做上面3中的define的需要在预处理的时候做的事情.其实,这并非它的劣势,而只是不是它所要担负的工作罢了.

c语⾔中有符号和⽆符号的区别?

有符号:数据的最⾼位为符号位,0表示正数,1表示负数
⽆符号:数据的最⾼位不是符号位,⽽是数据的⼀部分,只有正数,所以范围更大

谈谈计算机中补码的意义

计算机只有加法处理方式
将符号位与其他位统⼀处理将减法运算转换成加法运算

描述⼀下指针与指针变量的区别

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

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

描述⼀下内存分区

C语言开发对内存使用有区域划分,分别是栈区(stack)、堆区(heap)、BSS、数据段(data)、代码段(text)。

BSS段 未初始化全局变量,未初始化全局静态变量
数据段 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据
代码段 可执行代码、字符串常量
在这里插入图片描述

解释堆和栈的区别

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

  2. 申请大小的限制
    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

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

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

堆和栈的速度

https://blog.csdn.net/blunt1991/article/details/14005995?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1.pc_relevant_default&spm=1001.2101.3001.4242.2&utm_relevant_index=4

  1. 分配和释放,堆在分配和释放时都要调用函数(MALLOC,FREE),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成空洞),这些都会花费一定的时间,具体可以看看MALLOC和FREE的源代码,他们做了很多额外的工作,而栈却不需要这些。栈效率更高,因为堆分配分配了之后还要与存储器之间建立映射,而栈分配则是之前就建立好了。
  2. 访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。
  3. 栈有专门的寄存器,压栈和出栈的指令效率很高,而堆需要由OS动态调度,堆内存可能被OS调度在非物理内存中,或是申请内存不连续,造成碎片过多等问题。

vector和array区别

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

结构体与共⽤体的区别

结构体中的成员拥有独⽴的空间,共⽤体的成员共享同⼀块空间,但是每个共⽤体成员能访问共⽤区的空间⼤⼩是由成员⾃身的类型决定。
共用体使用覆盖技术,成员变量相互覆盖。

extern关键字

  1. extern修饰变量的声明。
    举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。能够被其他模块以extern修饰符引用到的变量通常是全局变量。还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。
  • c语言函数调用过程
    一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。
    总结起来整个过程就三步:
    1)根据调用的函数名找到函数入口;
    2)在栈中申请调用函数中的参数及函数体内定义的变量的内存空间
    3)函数执行完后,释放栈中的申请的参数和变量的空间,最后返回值(如果有的话)

关键字const

意味着只读。防止被修饰的成员的内容被改变。明确的告诉使用者不要修改这个变量。

  1. 在函数声明时修饰参数,表示在函数访问时参数(包括指针和实参)的值不会发生变化。

  2. 对于指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,const修饰的都是后面的值
    const int *b = &a; b不能修改
    或者int
    const b = &a;,b不能修改

数组特点

同⼀个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的
不初始化:如果是局部数组 数组元素的内容随机 ,如果是全局数组,数组的元素内容自动赋为0
完全初始化:如果⼀个数组全部初始化 可以省略元素的个数 数组的⼤⼩由初始化的个数确定

谈谈数组名作为类型、作为地址、对数组名取地址的 区别?

数组名作为类型:代表的是整个数组的⼤⼩
数组名作为地址:代表的是数组⾸元素的地址
对数组名取地址:代表的是数组的⾸地址

  • 数组首地址与数组首元素地址
    char arr[]={‘1’,‘2’,‘3’,‘4’,‘5’,‘6’};
    char * b = arr; //只能写成这样而不能写成 = &arr

arr //获得数组首元素地址等同于&arr[0]
arr、arr[0]、(arr+0) //获得数组首元素值
&arr //获得数组地址
arr+1 //获得数组第二个元素地址
arr[1]、*(arr+1) //获得数组第二个元素值

字节对齐规则

公式1:前面的地址必须是后面的地址正数倍,不是就补齐
公式2:整个Struct的地址必须是最大字节的整数倍
int 4
double 8
char 1
占用字节为24

int 4
char 1
double 8
这样占用就是16节省了内存空间

violate关键字

https://blog.csdn.net/ijn842/article/details/81273232
volatile是一个类型修饰符(type specifier), 防止编译器对代码进行优化。

在编译期间,编译器可能对代码进行优化
当编译器看到此处的n被const修饰,从语义上来讲,n是不期望被修改的
所以优化的时候把n的值存放到寄存器中,以提高访问的效率
只要以后使用n的地方都去寄存器中取,即使n在内存中的值发生变化,寄存器也不受影响,所以输出的n的值为10。也就是多线程其他线程可能读到的值是修改之前的。

使用volatile修饰之后,就不会将此值拷贝到寄存器中,那么其他线程可以读到修改后的值,也就是可见性。
volatile的作用就是被设计用来修饰被不同线程访问和修改的变量

attribute

https://www.jianshu.com/p/29eb7b5c8b2d
attribute:属性,主要是用来在函数或数据声明中设置其属性,与编译器相关
可以设置多种属性:比如

  1. attribute((unused))如果定义了一个静态函数,而没有去使用,编译时会有一个告警:而使用attribute((unused))可以告诉编译器忽略此告警
  2. attribute((always_inline))的意思是强制内联,所有加了attribute((always_inline))的函数再被调用时不会被编译成函数调用而是直接扩展到调用函数体内,比如我定义了函数
    attribute((always_inline)) void a()和
    void b()

    a();}

函数调用过程

 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、移动ebp、esp形成新的栈帧结构。
2、压栈(push)形成临时变量并执行相关操作。
在一个栈中,依据函数调用关系,发起调用的函数(caller)的栈帧在下面(高地址方向),被调用的函数的栈帧在上面。
每发生一次函数调用,便产生一个新的栈帧,当一个函数返回时,这个函数所对应的栈帧被清除(eliminated)
3、return一个值。
这些操作在Add函数中进行。

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

猜你喜欢

转载自blog.csdn.net/chongbin007/article/details/121828838