2. C 语言基础
常考面试题
-
int main(int argc, char ** argv)函数中,参数argc和argv分别代表什么意思?⭐⭐⭐⭐
第一个参数,
int
型的argc
,为整型,用来统计程序运行时发送给main
函数的命令行参数的个数。第二个参数,
char*
型的argv[]
,为字符串数组,用来存放指向字符串的指针元素,每一个指针元素指向一个字符串参数。各成员含义如下:-
argv[0]
指向程序运行的全路径名 -
argv[1]
指向在DOS命令行中执行程序名后的第一个字符串 -
argv[2]
指向执行程序名后的第二个字符串。。。。。。
-
argv[argc-1]
指向执行程序名后的最后一个字符串 -
argv[argc]
为NULL
-
-
结构体和共用体的区别⭐⭐⭐⭐⭐
- struct和union都是由多个不同的数据类型成员组成。 struct的所有成员都存在;但在任何同一时刻, union中只存放了一个被选中的成员。
- 在不考虑字节对齐的情况下,struct变量的总长度等于所有成员长度之和。Union变量的长度等于最长的成员的长度。
- struct的不同成员赋值是互不影响的;而对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了。
-
使用共用体读写成员时需要注意什么?⭐⭐⭐⭐
共用体是共用内存空间,所以每个成员都是读写同一个内存空间,那么内存空间里面的内容不停的被覆盖,而同一时刻,都只能操作一个成员变量。否则会出现读错误。
-
do…while(0)的作用⭐⭐⭐⭐
- do…while(0)使复杂的宏在展开时,能够保留初始的语义,从而保证程序正确的逻辑。
- 避免使用goto控制程序流。由于goto不符合软件工程的结构化,而且有可能使得代码难懂,不倡导使用,这个时候我们可以使用do{…}while(0)来做同样的事情。
常考面试题
-
简述C++有几种传值方式,之间的区别是什么?⭐⭐⭐⭐
传参方式有这三种:值传递、引用传递、指针传递
-
值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
-
引用传递:形参在函数体内值发生变化,会影响实参的值;
-
指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
-
-
为什么值传递不改变实参的值?⭐⭐⭐⭐
因为在函数传参的过程中,函数会为形参申请新的内存空间,并将实参的值复制给形参。形参的改变当然不会影响实参的值。
要想影响实参的值,可以使用指针传递。在C++中,可以使用引用传递。
-
全局变量和局部变量的区别⭐⭐⭐⭐
-
作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
-
内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
-
生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了
-
使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量。
-
-
全局变量和局部变量如何初始化?⭐⭐⭐⭐
当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值:
数据类型 初始化默认值 int 0 char ‘\0’ float 0 double 0 pointer NULL 正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果。
-
请说说原码、反码、补码⭐⭐⭐⭐
整型数值在计算机的存储里,最左边的一位代表符号位,0代表正数,1代表负数。
(1)原码:为二进制的数,如:10 原码为0000 1010
(2)反码:正数的反码与原码相同:如:10 原码为0000 1010,反码为0000 1010
负数为原码0变1,1变0,(符号位不变):如:-10 原码为1000 1010,反码为1111 0101
(3)补码:正数的补码与原码相同:如:10 原码为0000 1010,补码为0000 1010
负数的补码为反码加1:如:-10 反码为1111 0101,补码为1111 0110
-
32位机器下,sizeof (char *)的大小是多少?64位机器下呢?⭐⭐⭐⭐
4个字节。
8个字节。
常考面试题
-
说说数组与指针⭐⭐⭐⭐⭐
-
数组是相同类型数据的集合。
引入数组就不需要在程序中定义大量的变量,大大减少了程序中变量的数量,使程序精炼,而且数组含义清楚,使用方便,明确地反映了数据间的联系。
许多好的算法都与数组有关,如洗牌算法、冒泡排序等。同时数组也是一种数据结构,它的特点就是可以常数时间复杂度O(1)地访问元素,但是插入与删除元素是O(n)的时间复杂度,所以当需要频繁插入删除元素时,尽量不用数组,或对数组进行一些改进优化,比如C++ vector容器就是在数组的基础上进行改进优化,提高了数组操作效率。
-
指针也是一种变量,但它和普通的变量的区别是,普通的变量存放的是实际的数据,而指针变量包含的是内存中的一块地址,这块地址指向某个变量或者函数。
指针是C/C++语言的核心的概念,大大提高了程序的灵活性,但是同时也隐藏着危机,如内存泄露、非法内存访问、野指针等。所以为了规避这些问题,在后来的C++11引入了智能指针帮助程序员。
-
-
说说数组和指针的区别⭐⭐⭐⭐⭐
-
概念:
(1)数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
(2)指针:指针相当于一个变量,但是它和普通变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。
-
区别:
(1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)存储方式:
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。
指针:指针本身就是一个变量,作为局部变量时存储在栈上。
(3)求sizeof:
数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
(4)初始化:
//数组 int a[5] = {0}; char b[]={"Hello"};//按字符串初始化,大小为6. char c[]={'H','e','l','l','o','\0'};//按字符初始化 int *arr = new int[n];//创建一维数组 //指针 //指向对象的指针 int *p=new int(0) ; delete p; //指向数组的指针 int *p=new int[n]; delete[] p; //指向类的指针: class *p=new class; delete p;
-
-
数组指针与指针数组的区别⭐⭐⭐⭐⭐
数组指针是一个指针变量,指向了一个一维数组, 如
int (*p)[4]
,(*p)[4]
就成了一个二维数组,p也称行指针;指针数组是一个数组,只不过数组的元素存储的是指针变量, 如int *p[4]
。 -
指针函数与函数指针的区别⭐⭐⭐⭐⭐
(1)定义不同
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。(2)写法不同
指针函数:int *fun(int x,int y); 函数指针:int (*fun)(int x,int y);
(3)用法不同
指针函数返回一个指针。
函数指针使用过程中指向一个函数。通常用于函数回调的应用场景。
常考面试题
如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆栈段组成。
-
代码段:存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)。
-
数据段data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
-
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
-
可执行程序在运行时又会多出两个区域:堆区和栈区。
**堆区:**动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
-
最后还有一个文件映射区(共享区),位于堆和栈之间。
-
堆和栈的区别⭐⭐⭐⭐⭐
- 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等,栈有着很高的效率;堆一般由程序员分配释放,堆的效率比栈要低的多。
- 堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
- 空间大小: 栈的空间大小并不大,一般最多为2M,超过之后会报Overflow错误。堆的空间非常大,理论上可以接近3G。(针对32位程序来说,可以看到内存分布,1G用于内核空间,用户空间中栈、BSS、data又要占一部分,所以堆理论上可以接近3G,实际上在2G-3G之间)。
- 能否产生碎片: 栈的操作与数据结构中的栈用法是类似的。‘后进先出’的原则,以至于不可能有一个空的内存块从栈被弹出。因为在它弹出之前,在它上面的后进栈的数据已经被弹出。它是严格按照栈的规则来执行。但是堆是通过new/malloc随机申请的空间,频繁的调用它们,则会产生大量的内存碎片。这是不可避免地。
-
请你说说野指针⭐⭐⭐⭐⭐
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针不同于空指针,空指针是指一个指针的值为null,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针。
出现野指针的情况:
- 指针变量的值未被初始化: 声明一个指针的时候,没有显示的对其进行初始化,那么该指针所指向的地址空间是乱指一气的。如果指针声明在全局数据区,那么未初始化的指针缺省为空,如果指针声明在栈区,那么该指针会随意指向一个地址空间。
- 指针所指向的地址空间已经被free或delete:在堆上malloc或者new出来的地址空间,如果已经free或delete,那么此时堆上的内存已经被释放,但是指向该内存的指针如果没有人为的修改过,那么指针还会继续指向这段堆上已经被释放的内存,这时还通过该指针去访问堆上的内存,就会造成不可预知的结果,给程序带来隐患。
- 指针操作超越了作用域
-
如何避免野指针⭐⭐⭐⭐⭐
(1)初始化置NULL
(2)申请内存后判空:malloc申请内存后需要判空,而在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。
(3)使用时不要超出指针作用域。
(4)指针释放后置NULL
(5)使用智能指针。
-
请你说说内存泄露⭐⭐⭐⭐⭐
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
(1)new和malloc申请资源使用后,没有用delete和free释放;
(2)子类继承父类时,父类析构函数不是虚函数。
(3)比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
(4)shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。
有以下几种避免方法:
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:使用智能指针。
第四:一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
-
在函数中申请堆内存需要注意什么?⭐⭐⭐⭐⭐
(1)不要错误地返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡。
(2)不要返回了常量区的内存空间。因为常量字符串,存放在代码段的常量区,生命期内恒定不变,只读不可修改。
(3)通过传入一级指针不能解决,因为函数内部的指针将指向新的内存地址。
解决办法:
(1)使用二级指针
(2)通过指针函数解决,返回新申请的内存空间的地址。
-
请你说说内存碎片⭐⭐⭐⭐⭐
内存碎片通常分为内部碎片和外部碎片:
(1)内部碎片是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免;
(2)外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。再比如堆内存的频繁申请释放,也容易产生外部碎片。
解决方法:
(1)段页式管理
(2)内存池
-
请你说说malloc内存管理原理⭐⭐⭐⭐
当开辟的空间小于 128K 时,调用 brk()函数;
当开辟的空间大于 128K 时,调用mmap()。
malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块连接,每一个空闲块记录了一个未分配的、连续的内存地址。
-
什么是内存池⭐⭐⭐⭐
内存池也是一种对象池,我们在使用内存对象之前,先申请分配一定数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。当不需要此内存时,重新将此内存放入预分配的内存块中,以待下次利用。这样合理的分配回收内存使得内存分配效率得到提升。
-
说说new和malloc的区别,各自底层实现原理⭐⭐⭐⭐⭐
- new是操作符,而malloc是函数。
- new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
- malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
- new可以被重载;malloc不行
- new分配内存更直接和安全。
- new发生错误抛出异常,malloc返回null
-
说说使用指针需要注意什么?⭐⭐⭐⭐⭐
- 定义指针时,先初始化为NULL。
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。
- 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
- 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
- 动态内存的申请与释放必须配对,防止内存泄漏
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
-
初始化为0的全局变量在bss还是data⭐⭐⭐⭐⭐
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。