C++常见面试题

1、c++编译的过程

这是一个超链接

编译预处理、编译、汇编、链接



2、指针跟引用的区别

1.指针可以为空,引用初始化必须绑定对象

2.指针可以改变指向的地址,引用绑定后不能改变绑定的对象

3.指针可以多级指向,引用不可以

4.用sizeof运算符的话,对于引用计算的是指向的对象的大小
对于指针是指针本身的大小



3、栈和堆的区别

1.栈是系统自动分配的,而堆是程序员自己申请的

2.栈在内存中是一块连续的地址,向低地址扩展。而堆在内存的空间是不连续的,向高地址扩展,栈内存空间小,堆内存空间较大

3.栈的申请效率高,堆的效率低,使用起来方便但是容易产生内存碎片

4.栈中主要存放局部变量、函数参数等,堆的内容程序员自己安排



4、new和malloc的区别

1.malloc是库函数,需要有头文件支持,new是操作符,需要编译器支持

2.new申请空间的时候,无需计算大小,编译器会根据类型计算,而malloc需要确定申请空间的大小

3.new申请的空间在自由存储区,而malloc申请的空间是在堆上

4.new分配成功后,返回是对象类型的指针,而malloc返回的是void指针,需要强制转换为对应类型的指针

5.new分配失败时,会抛出 bad_alloc 异常,而malloc分配失败时,返回NULL

6.new可以重载,malloc不行

7.new会调用对象的构造函数,malloc不会

new的底层分为两步,第一步先调用operator new申请空间,而operator new的内部实现其实就是malloc,
第二步调用对象的构造函数



5、define和const的区别

1.define是在编译预处理阶段起作用,const是在编译和程序运行阶段起作用的

2.define定义的宏常量是没有类型的,只会进行简单的替换,而const修饰的只读常量是有类型的

3.define定义的常量在内存中有若干份拷贝,而const修饰的常量只有一份拷贝,存放在静态区,节省空间

4.define可防止重复定义



6、const和static的用法

const

这是一个超链接

1.const修饰的变量,相较于宏定义,可以进行类型检查,节省内存,提高了效率

2.const修饰的函数参数,在函数中,参数值不可改变

3.const修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外,函数参数也除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量



static

1.static作用于局部变量的时候,改变了局部变量的生命周期,使得变量存在于定义之后直到程序运行结束才消亡

2.static修饰的全局变量,改变了全局变量的作用域,使得全局变量只能在定义它的文件中使用,在源文件中不具有全局可见性

3.static作用于类中的成员变量和成员函数的时候,这些成员变量和成员函数就属于了类,即不需要通过创建对象就能访问它们。

注意:静态成员函数只能访问静态成员变量或静态成员函数,不能将静态成员函数定义为虚函数



7、const和static在类中的注意事项

这是一个超链接


static 静态成员变量

1.静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和 private/public/protected 访问规则。

2.静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。

3.静态成员变量可以作为成员函数的参数可选参数,而普通成员变量不可以

4.静态数据成员的类型可以是所属类的类型,而普通数据成员不可以,普通数据成员只可能声明成类的指针或引用

static 静态成员函数

1.静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数

2.静态成员函数不能声明成虚函数(virtual)、const、volatile

const 成员变量

1.const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化

2.const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的,所以不能在类的声明中初始化 const 成员变量,因为类的对象还没有创建,编译器不知道他的值

const 成员函数

1.不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量

2.不能调用非常量成员函数,以防修改成员变量的值

const static

1.如果要想成员变量在整个类中都是恒定的常量,应该用类的枚举常量或者 static const.

2.在类中进行声明,在类外进行初始化(类似于类的静态常量)



8、C++内存管理(重要)

这是一个超链接

C++ 内存分区:

  • 栈:存放函数的局部变量,由编译器自动分配和释放
  • 堆:动态申请的内存空间,就是由 malloc 或者new分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收
  • 全局数据区(静态区):存放全局变量和静态变量
  • 常量存储区:存放的是常量,不允许修改
  • 代码区:存放程序的代码,即CPU执行的机器指令,并且是只读的

堆和自由存储区的区别

自由存储区到底有没有,暂不清楚,概念提出者没有明确,所以既可以有也可以没有,面试字节的时候,面试官告诉我其实是没有的,new和malloc动态申请的都在堆中

这是一个超链接

1.自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。

2.new所申请的内存区域在C++中称为自由存储区。藉由堆实现的自由存储,可以说new所申请的内存区域在堆上。

3.堆与自由存储区还是有区别的,它们并非等价。



9、C++类大小的计算

这个需要先会结构体的字节对齐原则 不会要先学 下面是结构体的
这是一个超链接
然后下面这个超链接是类的大小计算
这是一个超链接

个人理解就是:
首先你一行能存放多少个字节,取决于基本数据类型的最大字节数。
比如int是4,double是8
然后按顺序存放,如果放不下,那么就放在下一行,总的字节数是基本数据类型的最大字节数的倍数。

struct node{
	char a;
	double b;
	int c; 
};

这个就是第一行放一个char类型,一行最多放8个字节,所以double占满第二行,然后int放在第三行,用了三行,所以占用内存为24.

对于类的话,和这个一样,不过如果有虚函数,那么第一行是先放虚函数表指针,8个字节。对于多继承,则是按照继承的先后顺序,然后才是放该类的成员属性。
特别的,内存最少为1.



10、重载和重写的区别

重载是在一个类中,函数名字一样,参数不同(参数个数、类型、顺序),不关注返回值类型。而重写是在不同类之间、子类与父类,函数名、参数、返回值都要相同。父类中被重写的函数需要用virtual修饰。



11、面向对象和面向过程的区别

1.面向过程:以过程为编程的中心思想,将解决一个问题的基本步骤列出来,按照执行顺序逐一完成每一个步骤。

2.面向对象:所有的事物都有自己的属性与行为。比如一个人的属性有身高、体重、外貌等,行为有吃饭、走路、打代码。我们将一个事物抽象为一个类,它的属性抽象为成员变量,行为抽象为成员函数,对象与对象之间通过成员函数交互。



12、面向对象的三大特性

1.封装,将具体的数据和实现过程封装为类,只能通过接口访问,降低耦合性

2.继承,子类继承父类的行为和属性,子类拥有父类中非私有的变量和函数。子类可以重写父类中的方法,增强了耦合性。被final修饰的类不能被继承,被final修饰的成员不能被重写或修改

3.多态,不同子类对同一个消息,作出不同的反应。基类的指针指向子类的对象,使得基类指针作出不同的应答



13、如何实现多态

C++的多态分为两类,一个是编译期多态,一个是运行期多态
编译期多态主要是通过模板类来实现的。
运行期的多态主要是通过虚函数结合动态绑定实现的



14、虚函数的实现原理

虚函数是通过虚函数表实现的。虚函数表保存了虚函数的指针,虚函数表指针保存在含有虚函数的类的实例对象的内存空间里,虚函数表的指针放在实例对象的最前面。

虚函数表在编译期生成,存放在全局数据区,即静态区.



15、虚继承

虚继承是用来解决多重继承命名冲突数据冗余的问题。比如类C继承了类B1、类B2,而类B1和类B2都是继承了A,那么在D中就有两份类A里的数据,虚继承就是,B1和B2对A都是虚继承,那么A就是虚基类。这样子在派生类C中就只有一份类A中的数据。

虚继承的目的就是让类声明,愿意共享基类,这个基类就是虚基类



16、析构函数为什么要写成虚函数

这是一个超链接

这是为了防止内存泄漏。如果不定义成虚函数,那么只会调用基类的析构函数,等于只是释放了基类的内存,而没有释放掉派生类的内存,定义为虚函数后,会先调用派生类的析构函数,然后调用基类的析构函数。



17、构造函数为什么不能写成虚函数

每一个拥有虚函数的类的对象都有一个指向虚函数表的指针。对象通过虚函数表里存储的虚函数指针来调用虚函数
而虚函数表指针是在调用构造函数的时候初始化的,等于在调用构造函数的时候,此时虚函数表指针还不存在,找不到对应虚函数指针,所以不能将构造函数定义为虚函数。



18、构造函数和析构函数为什么不能调用虚函数

这是一个超链接

可以用,但是没有用的必要。
对于构造函数,比如你基类调用了虚函数,而此时派生类的虚函数还没构造好,因为派生类肯定比基类晚构造,所以你调用的虚函数就是基类里面的,这样就没有实现多态。

同理,析构的话肯定是派生类先析构,那么此时你基类的析构函数调用虚函数,派生类都被析构完了,调用的也只是基类内的函数而已,也没有实现多态

说白了就是脱裤子放屁,你设为普通函数照样可以达到这个效果,那你还设为虚函数干嘛



19、移动语义与完美转发

c++11引入了移动语义和完美转发 两者都基于右值引用

这是一个超链接

移动语义

移动语义(move):无条件实现将参数转为右值
移动语义其实就是为了减少资源开销和提高效率而引入的
比如主函数去接收一个函数返回一个类的实例对象时候
在这个过程中函数内会调用一次构造函数,返回值那里会进行一次拷贝构造函数给临时对象,而主函数部分会通过拷贝构造函数获得临时对象的值。

这个过程进行了两次拷贝,如果类比较复杂,那么是比较消耗时间的,所以引入了移动语义,移动语义通过移动构造函数,将资源所有权移动给了临时对象,临时对象移动给了主函数部分。本质其实就是swap


完美转发

完美转发(forward):不改变参数的左右值类型



20、智能指针

这是一个超链接

通过new出来的指针,我们需要delete去释放资源,而如果还没到delete之前就因为某些原因,而导致函数return 或者抛出异常,那么就会存在内存泄漏。而智能指针本质就是将指针封装成一个类,这样子类的管理是在栈区上的,也就不需要手动释放了,只要对象离开了作用域,一定会调用析构函数。

智能指针基于RAII(Resource Acquisition Is Initialization),称为"资源获取就是初始化",是一种利用对象生命周期控制程序资源的技术



21、NULL和nullptr的区别

在c语言中,NULL其实是个宏定义为(void*(0))的东西。

int *p=NULL;

对于这里其实发生了隐式转换,将void *类型转为了int *类型。
但是对于C++,因为存在重载的功能,所以存在二义性。
NULL在C++里其实是数字0,因为在C++中void * 类型是不允许隐式转换成其他类型的
所以C++引入了nullptr用来表示空指针。


22、为什么拷贝构造函数的参数要用左值引用,不用值传递

  • 如果用值传递会陷入无限的死循环,因为如果是值传递,那么从实参拷贝到给形参,这里又拷贝了。。
  • 不考虑死循环,在效率上用引用也高于值传递,用值传递还需要拷贝一份

23、在构造函数体和初始化列表赋值的区别

其实构造函数干两件事,第一件事就是初始化,第二件事就是执行构造函数体。这里注意在构造函数体内的赋值只能叫做赋初值,并不是初始化。只有在列表初始化上才叫初始化。
而基本的数据类型没有初始化的话是由默认值的。
对于以下三种成员数据必须通过初始化列表才可以。

  • 用const修饰的变量,因为必须定义时候初始化,不允许先定义后赋值
  • 引用修饰的变量,因为必须定义时候初始化,不允许先定义后赋值
  • 自定义的数据类型



24、深拷贝与浅拷贝

浅拷贝就是两个对象同时指向了同一块内存,这样会导致析构的时候,进行两次析构,即内存释放两次,会导致程序崩溃。
深拷贝就是手写拷贝构造函数,为即将被赋值的对象新开辟一个内存。
对于类中默认的拷贝构造函数都是浅拷贝



25、内存泄漏与内存溢出

内存泄漏:一般指的就是堆内存的泄露,因为堆的内容是程序员自己安排的。内存泄漏即new出来的东西没有delete,造成了内存被占用。

内存溢出:指所剩余的可申请内存空间小于要申请的空间,比如栈满时候进栈,就产生了内存溢出。


26、为什么重载不可以根据返回值类型

int f(int a,int b);
double f(int a,int b);
int main(){
	f(1,2)
	return 0;
}

如上述代码,加入可以根据返回值类型进行重载,那么当我们主函数不需要接受这个返回值的时候,此时会产生二义性,编译器并不知道要调用哪一个重载。


27、动态链接和静态链接

静态链接:即在程序运行之前,将库函数全部载入到可执行的目标文件之中,这样子程序运行就脱离了库函数,优点是可移植性强,缺点也很明显,将不需要的库函数导入进去,消耗了太多内存

动态链接:即在程序运行之前,到需要调用的库函数的时候,去看其他执行的文件有没有,如果存在的话,共享这个库函数,直接调用,等于库函数只有一份内存拷贝,没有的话再进行动态链接,这样会耗费一些时间,并且可移植性差,但是与静态链接相比占用内存少,并且如果修改了库函数的话,静态链接需要重新编译,而动态链接则不需要,因为调用的是库函数的接口。


28、c++四种强制转换

这是一个超链接

static_cast:
1.用于基本数据类型的转换
2.不能用于基本数据类型之间的指针转换
3.用于有类继承关系对象之间的转换和类指针的转换

const_cast:
去除变量的const属性

dynamic_cast:
1.用来将基类的指针和引用安全的转化为派生类的指针或者引用。
2.dynamic_cast在转换之前会进行检查,如果转换失败指针返回,应用抛出bad_cast异常

reinterpret_cast:
1.用于指针类型间的强制转换
2.用于整数和指针类型间的强制转换


29、vector

vector底层实现是数组,是连续存储结构。每次push_back后,会把当前的size与vector中的capacity进行比较,如果当时size等于capacity的话,那么会先新开辟一片二倍的capacity的空间,然后把原来的元素迁移到新的空间,释放掉原来的空间。

vector中size是指当前的已有的对象元素个数,而capacity则是容器预留的容量,即最多可放的个数。

resize,调整vector中元素的个数,那么会使size增加,如果size会超过capacity的话,capacity也会增加

reserve,会调整vector中容器预留的容量,也就是capacity的值,但是他不会改变size的值,也就是他只是增加了预留空间的大小,没有新增元素对象。

在底层中,其实vector用了三个指针,first,last,end,
first表示起始位置的字节,last表示当前元素的最后一个位置的末尾字节,而end则是整个vector所占内存空间的末尾字节。

size就是last-first,而capacity即是end-first

c++11引入了emplace_back()
与push_back()的区别在于:emplace_back()是在vector的内存中原地构造,而push_back()是在外部构造后,通过移动或者拷贝到vector中。
所以emplace_back()的效率会更优。



30、list

list底层实现是双向链表,是非连续存储结构,每个元素维护一对前后向指针,所以支持正序和逆序遍历。
支持高效的插入和删除,但是随机访问的效率低。由于每个元素需要维护额外的两个指针,空间开销较大。
相较于vector而言,vector是当capacity==size时候,申请一个二倍的空间,而list是每次插入都会新开辟一个元素单位的空间存放。



31、deque

deque是双端队列,类似vector,也是连续存储结构,不同于vector,它提供了两级数组结构,第一级完全类似vector,代表容器,第二级维护了一个容器的首地址。不仅拥有vector的所有功能,还支持高效的首部/尾部的添加与删除。可以理解为合并了vector与list的功能。

vector、list、deque三者使用场景:

  • 如果要求随机存取,不在乎首部/尾部插入和删除的效率,用vector
  • 如果要大量的增删,不要求随机存取,用list
  • 要求随机存取,并且会在首部和尾部进行大量插入和删除的操作,用deque



32、map和unordered_map区别

map:底层实现是红黑树,红黑树是一种弱平衡的二叉搜索树,是关于黑节点的平衡。对于红黑树而言,它的中序遍历是从小到大有序的。增删改查的复杂度均为logn,插入一个结点最多旋转两次,删除一个结点最多旋转三次,旋转次数远小于二叉搜索树。但是红黑树比较占用空间,它每个节点不仅保留了左右儿子指针,还保留了父节点指针和结点的颜色。

unordered_map:底层实现是哈希表。相对于map而已,他的数据插入并不是有序的,对于查询而言,平均复杂度为O(1),最差会被卡成O(n)

map相较于unordered_map各种操作的复杂度都是稳定的logn,而unordered_map查找的平均复杂度优于map,但是不够稳定,map在对于要求数据有序的时候是非常适用的。



33、“extern C” 作用

超链接

为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。



34、inline

内联函数把函数编译好的二进制指令直接复制到函数的调用位置。

优点:能够提高程序的运行速度,因为没有这样就少去了函数调用的时间
缺点:会导致可执行文件冗余,因为是牺牲空间来换取时间

内联函数分为显示内联和隐式内联:
显示:在函数前加inline
隐式:在结构、类中直接定义的成员函数,则该函数也被自动优化成内联函数



35、内联函数和宏函数的区别

1、宏函数不是真正的函数,只是代码的一个替换,因此不会有参数压栈、出栈以及返回值,也不会进行参数的类型检查,因此所有类型都可以使用,但这样会带来很大的安全隐患。

而内联函数是真正的函数,函数调用时会进行传参,会进行压栈、出栈,可以有返回值,并进行严格检查参数类型,但这样就不能通用所有类型,如果想被多种类型调用需要重载

2、内联函数是在编译时展开,而宏函数在预编译时展开,在编译的时候,内联函数直接被嵌入到目标代码中去,而宏函数只是一个简单的文本替换过程

3、宏在定义时要小心处理宏参数,一般用括号括起来,否则会出现二义性,而内联函数不会出现二义性

4、inline有点类似于宏定义,但是它和宏定义不同的是,宏定义只是简单的文本替换,是在预编译阶段进行的,而inline的引入正是为了取消这种复杂的宏定义的。



36、内存对齐

内存对齐的好处:

  • 在硬件上来说,cpu读取内存不是一个字节一个字节读的,而是一块一块的读取,所以内存对齐有利于减少读取时间,本质上就是空间换时间
  • 从平台上来说,在有些平台上只能在特定位置读取一定的字节块,内存对齐就统一这样的规范,而不会使得在不同平台上的读取结果不同

可以通过 # pragma pack(1) 取消内存对齐

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我不会c语言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值