我只是一个码字小白,打一遍过过脑子,面试题出自下面的博客:
C语言常见面试题汇总_Charles Ren的博客-CSDN博客_c语言面试题
1、gcc的编译过程是怎样的?
gcc编译过程分为四个阶段:预处理、编译、汇编、链接
预处理:头文件包含、宏替换、条件编译、删除注释
编译:检查语法错误、语义错误、进行代码优化、检查无误后将预处理好的文件编译成汇编文件。
汇编:将汇编文件转换成二进制目标文件
链接:将项目中的各个二进制文件、所需的库、启动代码链接成可执行文件
2、static关键字
全局变量存储在静态存储区中,局部变量存储在堆栈中。
- 静态局部变量:和普通变量不同。静态局部变量也是定义在函数内部的,静态局部变量所在的函数在调用多次时,只有第一次才经历变量定义和初始化,以后多次在调用时不再定义和初始化,而是维持之前上一次调用时执行后这个变量的值。下次接着来使用,但是他的作用域仅限于当前函数中。
- 静态全局变量也只初始化一次,但是作用域在当前文件/模块中。每次调用当前文件时,会使用上一次保存的值。
- 静态函数只在本模块或者文件中使用,被限定了范围。
3、变量/函数的声明和定义之间有什么区别?
变量/函数的声明仅声明变量/函数存在于程序中的某个位置也就是后面程序会知道这个函数或者变量的类型,但不分配内存。
关于定义,当我们定义变量/函数时,除了声明的作用外,他还为该变量/函数分配内存。
4、各种指针
NULL指针:
NULL指针用于指示指针未指向有效位置。理想情况下,如果在声明时不知道指针的值,则应将指针初始化为NULL
悬空指针:
悬空指针是没有指向正确内存位置的指针。当删除或释放对象时,如果不修改指针的值或者不置为NULL,就会出现悬空指针。
野指针:
野指针就是只声明没有被初始化过的指针,他可能指向任何内存。
5、指针常量与常量指针
int * const p = &a; //指针常量:p只能指向一个位置,而不能指向其他位置,指向的变量值(即*p)可以改变
const int *p = &a; //常量指针:指向的变量的值不能改变,但是可以改变这个指针指向的位置
6、引用与指针的区别?
引用是c++的概念,在c中叫取地址符号。
本质:引用是别名,指针是地址
指针是独立的可以指向空值,这是我们为指针分配了内存。而引用必须初始化指定的对象自始至终只能依附于同一个变量,他只是别名。标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体跟随编译器。
7、c语言的参数传递方式?
值传递(swap1函数)
地址/指针传递(swap2函数)
引用传递(swap3函数)
void swap1(int, int); //值传递
void swap2(int *p1, int *p2); //地址传递
void swap3(int &a, int &b); //引用传递
这也就证明了地址传递和引用传递都是直接传递的变量所在的地址,函数的主要作用就是对存储在地址中的变量进行直接的操作。
而按值传递则不会修改原值。
而引用相当于间接寻址,他直接传递的是地址。(c语言没有引用传递,只有c++有)
8、结构体的深拷贝和浅拷贝
当结构体中有指针成员的时候容易出现浅拷贝与深拷贝的问题。
浅拷贝存在的问题:当出现类的等号赋值时,系统会调用默认的拷贝函数----即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次free函数,此时ptr2已经是野指针,指向的内存空间已经被释放掉,再次free会报错,这时,必须采用深拷贝。
深拷贝就是,让两个结构体变量的指针成员分别指向不同的堆区空间,只是空间内容拷贝一份,这样在各个结构体变量释放的时候就不会出现多次释放同一堆区空间的问题。
9、#include<>与#include""的区别?
#include<>到系统指定目录寻找头文件,#include" "先到项目所在的目录寻找头文件,如果找不到再到系统指定的目录下寻找。
10、关于宏定义#define?
宏定义又称宏代换、宏替代,简称“宏”
11、ifndef/define/endif的作用?
防止头文件被重复包含和编译。头文件重复包含会增加程序的大小,重复编译增加编译时间。
12、宏定义与内联的区别?(c++ inline)
- 内联函数在编译时展开,宏在预处理时展开。
- 内联函数直接嵌入到目标代码中,宏是简单的做文本替换。
- 内联函数有类型检测、语法判断等功能
- inline函数是函数,宏不是
13、宏定义与typedef区别
#define用于为各种数据类型定义别名,与typedef类似,但是他们有以下几点不同
- typedef仅限于为类型定义符号名称,定义一种类型的别名,而不是简单的宏替换。#define不仅可以为类型定义别名,也能为数值定义别名,比如可以定义1为ONE
- typedef编译阶段会检查错误,#dedfine预处理阶段不检查错误
14、与const区别
- 数据类型:const修饰的变量有明确的类型,而宏没有明确的数据类型
- 安全方面:const修饰的变量会被编译器检查,而宏没有安全检查
- 内存分配:const修饰的变量只会在第一次赋值时分配内存,而宏是直接替换,每次替换后的变量都会分配内存。
- 作用场所:const修饰的变量作用在编译、运行过程中,而宏作用在预编译中。从编译器的角度讲,最大的优势是简单,方便。因为预编译就可以解决掉#define,不必让编译器来处理这个。
- 代码调试:const方便调试,而宏在预编译中进行所以没有办法调试。
在程序语句中使用常量的地方,最好是使用const定义。
15、c语言中有符号和无符号的区别?
有符号:数据的最高位为符号位,0表示正数,1表示负数
无符号:数据的最高位不是符号位,而是数据的一部分,只有正数,所以范围更大
16、谈谈计算机中补码的意义?
计算机只有加法处理方式,将符号位与其他位统一处理将减法运算转换成加法运算。
17、描述以下指针与指针变量的区别?
指针:内存中每一个字节都会分配编号,这个编号就是地址,而指针就是内存单元的编号。一个变量的地址就称为该变量的指针,他保存的是一个地址。
指针变量:c语言有很多种变量,每种变量都会存储一种数据,而指针变量就是专门来存储指针的变量,本质是变量,只是该变量存放的是空间的地址编号。
二级指针:指针本身也是一个变量,也要占用内存空间,而二级指针就是指向这块变量的指针。一般二级指针用在二维数组中。
int *p; //int *p是一个指针变量
p = &a; //对a取地址,p就是一个指针用来保存地址
18、描述一下内存分区?
c语言开发对内存使用有区域划分,分别是栈区(stack)、堆区(heap)、BSS、数据段(data)、代码段(text)。
BSS段:未初始化全局变量、未初始化全局静态变量
数据段:已初始化全局变量、已初始化全局静态变量、局部静态变量、数据常量
代码段:可执行代码、 字符串常量
解释堆和栈的区别:
申请方式:
stack:由系统自动分配。例如,声明在函数中一个局部变量int b; 系统自动在栈中为b开辟空间。
heap:需要程序员自己申请,并指明大小,在c中malloc函数
申请大小限制:
stack:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的的地址和栈的最大容量是系统预先定好的,在Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是用链表来存储的空闲的内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址,堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较:
stack:由系统自动分配,速度较快。但程序员是无法控制的
heap:是由new(malloc)分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
堆和栈中的存储内容:
stack:局部变量和形参
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
19、结构体和共用体的区别?
结构体中的成员拥有独立的空间,共用体的成员共享同一块空间,但是每个共用体成员能访问共用区的空间大小是由成员自身的类型决定。共用体使用覆盖技术,成员变量相互覆盖。
20、extern关键字
- extern修饰变量的声明:举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量。能够被其他模块以extern修饰符引用到的变量通常是全局变量。还有一点很重要的是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。
- c语言函数调用过程:一个c语言函数的执行过程可以认为是多个函数之间的互相调用的过程,他们形成了一个或简单或复杂的调用链条。这个链条的起点是main(),终点也是main()。总结起来整个过程就三步:(1)根据调用的函数名找到函数入口;(2)在栈中申请调用函数中的参数及函数体内定义的变量的内存空间;(3)函数执行完成后,释放栈中申请的参数和变量的空间,最后返回值。
20、关键字const
意味着只读。防止被修饰的成员的内容被改变。明确的告诉使用者不要改变这个变量。
21、数组特点
同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
不初始化:如果是局部数组数组元素的内容随机,如果是全局数组,数组的元素内容自动赋值为0。
完全初始化:如果一个数组全部初始化,可以省略元素的个数,数组的大小由初始化的个数决定。
22、谈谈数组名作为类型、作为地址、对数组名取地址的区别?
数组名作为类型:代表的是整个数组的大小
数组名作为地址:代表的是数组首元素的地址
对数组名取地址:代表的是数组的首地址
数组首地址与数组数组首元素地址
char arr[] = {'1', '2', '3', '4', '5', '6'};
char *b = arr; //只能写成这样而不能写成=&arr
23、字节对齐规则
公式1:前面的地址必须是后面的地址的整数倍,不是就补齐
公式2:整个struct的地址必须是最大字节的整数倍
int 4
double 8
char 1
占用字节24
int 4
char 1
double 8
占用字节16,节省了大量的空间
24、volatile关键字
volatile是一个类型修饰符,防止编译器对代码进行优化。
25、函数调用过程
int Add(int x, int y)
{
int sum = 0;
sum = x + y;
return sum;
}
int main()
{
int a = 10;
int b = 12;
int ret = 0;
ret = Add(a, b);
return 0;
}
(1) 参数拷贝分配内存
(2) 保存当前指令的下一条指令,并跳转到被调函数,
这些操作均在main中执行
接下来是调用Add函数并执行的一些操作,包括:
移动ebp、esp形成新的栈帧结构--->压栈形成临时变量并执行相关操作--->在一个栈中,依据函数调用关系,发起调用的函数(caller)的栈帧在下面(高地址方向),被调用的函数的栈帧在上边。每发生一次函数调用,便产生一个新的栈帧,当一个函数返回时,这个函数所对应的栈帧被清除。--->return一个值。
被调用函数完成相关操作后需返回到原函数中执行刚才保存的下一条指令,操作如下:
(1)出栈 (2)恢复mian函数的栈帧结构 (3)返回main函数
这些操作也在Add函数中进行。至此,在main函数中调用Add函数的整个过程已经完成。
总结起来就三步:
1)根据调用的函数名找到函数入口
2)在栈中申请调用函数中的参数及函数体内定义的变量的内存空间
3)函数执行完后,释放函数在栈中的申请的参数和变量的空间,最后返回值。
这些操作在Add函数中进行