- 接着面试问题的总结,若投软件开发方向的岗位,会问关于C++相关的内容,这些题目都是从牛客网面经中挑选出来的面试问题,自己一道一道的搜集答案,拿来分享给大家,若有写的不对的地方,欢迎指正!
C++
- 1.面向对象的三大特性:继承、封装、多态
- 2.野指针、内存泄漏、指针悬挂、内存溢出如何理解?
- 3.如何避免内存泄漏?
- 4.C++中关于智能指针
- 5.关于引用和指针
- 6.C++程序编译过程(C++源文件从文本到可执行文件经历的过程)
- 7.include头文件的顺序以及双引号”“和尖括号<>的区别?
- 8.不同类型的变量与“零“作比较
- 9.C和C++的区别
- 10.深与浅拷贝
- 11.C++11的特性用过哪些?
- 12.C++中关于Lambda表达式(匿名函数)
- 13.C、C++如何交错使用?
- 14.一些常用的STL头文件
- 15.C++中四种cast转换
- 16.C++中static的作用
- 17.C++中const的作用
- 18.C++中extern的作用
- 19.C++中private protect public
- 20.C++中struct和class的区别
1.面向对象的三大特性:继承、封装、多态
封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等等集合在一个个的单元之中(也称为类),封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些特性。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过基类的指针,来调用实现派生类中的方法。
2.野指针、内存泄漏、指针悬挂、内存溢出如何理解?
野指针:指的是指针指向的位置是不可知的,比如指针变量的值未被初始化,或者它指向的空间已经被释放了。
内存泄漏:内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉已经不再使用的内存的情况。
①内存泄漏并不是指的是物理层面上的消失,而是在编写程序时分配某段内存之后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
②常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。
③比如说在new之和没有及时进行delete,或者delete [ ]p误写成delete p。(new一个指针后用delete,new一个数组后用delete [ ]p)
指针悬挂:指针指向一个已经释放的内存空间。
内存溢出:程序申请内存时,没有足够的内存空间供其使用。
3.如何避免内存泄漏?
①如果使用了内存分配的函数,要记得使用其相应的函数释放掉内存,比如在new关键字分配内存之和之后,及时写上配套的delete关键字来释放内存,可以始终在new和delete之间编写代码。
②使用string而不是char*,因为string类在内部处理所有内存管理,而且它速度快且优化得很好。
③使用RAII(resource acquisition is initialization的缩写,意为“资源获取即初始化”),就是将需要动态内存的东西都隐藏在一个RAII对象之中,当它超出范围时就释放内存;比如RAII在构造函数中分配内存并在析构函数中释放内存,这样当变量离开当前范围时,内存就可以被释放。
④不要手动管理内存,可以尝试在适用的情况下使用智能指针。
4.C++中关于智能指针
什么是智能指针?
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory>
头文件中。
C++11 中的智能指针及其用法
①共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
②独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
一个unique_ptr怎么赋值给另一个unique_ptr 对象?
借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。
//举个例子:A 作为一个类
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
③弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由 shared_ptr 带来的循环引用问题。
智能指针的实现原理:计数原理
。
使用智能指针会出现什么问题?怎么解决?
智能指针可能出现的问题:循环引用
举个例子,假设在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。
解决方法:使用weak_ptr,因为引起循环引用的原因是该被调用的析构函数没有被调用,从而出现了内存泄漏。
5.关于引用和指针
引用的定义
定义:引用即别名,就是某个变量的别名,对引用别名的操作与对变量本身完全相同。(就相当于两个变量名指向了同一个内存地址,一个被修改,另一个也跟着修改。)
引用的语法规则
语法规则:类型 & 引用名 = 变量名;
eg.int& b = a;//b引用a,b就是a的别名
(PS:引用在定义时必须初始化,初始化以后绑定的目标不能再改变。引用的类型与绑定的目标变量类型要相同。
指针的定义
指针就是利用地址的值直接指向存在电脑存储器中另一个地方的值,即通过地址可以找到所需的变量单元,因此,将地址形象化的称为“指针“,意思是通过它能找到以它为地址的内存单元。
引用与指针有什么区别?
①指针可以不做初始化,其指向的目标可以修改(指针常量除外);而引用必须初始化,一旦初始化其绑定目标的不能再修改。
②不存在指向空值的引用,但是存在指向空值的指针。
③指针有自己的一块空间,而引用只是一个别名。
④指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用。
⑤指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变。
⑥使用++运算符的意义不一样,指针++是指向下一个内存地址,引用++是该值++。
⑦使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小。
⑧可以定义指针的指针(二级指针),但是不能定义引用的指针。
⑨可以定义指针的引用(指针变量的别名),但是不能定义引用的引用。
⑩可以定义指针数组,不能定义引用数组,但可以定义数组引用(数组的别名)。
6.C++程序编译过程(C++源文件从文本到可执行文件经历的过程)
编译过程分为四个过程:预处理阶段、编译阶段、汇编阶段、链接阶段。
预处理阶段:处理以#开头的指令,生成预编译文件。
编译阶段:将源码的.cpp文件翻译成.s汇编代码。
汇编阶段:将汇编代码.s翻译成机器指令.o文件。
链接阶段:对于汇编阶段生成的.o文件,并不会立刻执行,因为可能出现在源码的.cpp文件中引用了另一个.cpp文件中的东西,则链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序.exe文件。
关于链接阶段的知识(动态封装、静态封装)
链接分成两种:静态链接和动态链接
,就是平常我们说的静态封装和动态封装。
静态封装的意思就是在打包成可执行程序.exe文件时,将我们写的程序中所包含的外部库都拷贝一份,将它们封装在一起。
动态封装的意思就是在打包成可执行程序.exe文件时,那些程序中所包含的外部库需要都放到一个文件夹中,比如我使用到了opencv4.4版本,则相应的配置文件.dll等都要放在同一个文件夹中。
两者的区别:
静态封装运行的速度快,但遇到程序升级时,比如我更新了opencv版本等等,就需要重新进行编译封装,比较费事,且浪费空间;
动态封装的话,节省内存,更新方便,但是一个个的配置相关的外接库比较繁琐,而且每次执行的时候都需要去链接,性能上相比于静态封装也会有一定的损失。
7.include头文件的顺序以及双引号”“和尖括号<>的区别?
Include头文件的顺序
对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则编译器会报变量类型未声明错误。
这个其实从原理上来说,是因为#include本来就是一个预编译指令,在预编译阶段,编译器会将include引入的文件直接进行原封不动的替换,也就是说在得到汇编文件之前,cpp文件中就已经没有#include、#define等语句了,有的是指定的文件内容。
双引号和尖括号的区别
区别:编译器预处理阶段查找头文件的路径不一样。
双引号查找头文件路径的顺序为:
当前头文件目录——编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)——系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。
尖括号查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)——系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径 。
8.不同类型的变量与“零“作比较
①bool型变量:
if(!var)
②int型变量:
if(var == 0)
③float型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
因为计算机内表示小数时(float和double)都有误差。
④指针变量:
if(var == NULL)
9.C和C++的区别
①C++是面向对象的语言,而C是面向过程的结构化编程语言。
②C++相比C,增加多许多类型安全的功能,比如强制类型转换。
③C++支持范式编程,比如模板类、函数模板等
④C++支持函数重载,C不支持。
10.深与浅拷贝
概括的说:浅拷贝是让两个指针指向同一个位置,而深拷贝是让另一个指针自己再开辟空间。
浅拷贝:就是对于一些基本的对象或者数据类型,浅拷贝就是直接复制内存;但如果当类的成员包含指针的时候,使用浅拷贝就会使得两个指针指向同一块内存空间,会造成指针悬挂的问题。
深拷贝:深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。
11.C++11的特性用过哪些?
①nullptr:用来替代之前的NULL,传统的C++会把NULL、0视为同一种东西,C++11引入了nullptr,专门用来区分空指针、0。
②auto自动推导变量的类型。
vector<int> temp;
for (auto x : temp)
std::cout << x << std::endl;
③智能指针:基于RAII原则,引入shared_ptr、unique _ptr等等。
④Lambda 表达式:这么做可以定义匿名函数,且形成“闭包”,限制了别人的访问,更私有安全。
12.C++中关于Lambda表达式(匿名函数)
定义:Lambda表达式,又称匿名函数,假如在编程时,需要有一个函数只会被复用一次,其他地方再也不会调用时,lambda表达式就很实用。
Lambda表达式的基本语法如下:
[ 捕获列表 ] ( 参数列表 ) -> 返回类型 {
函数体 }
举例:
正常的函数是这样:
int Foo(int a, int b)
{
return a+b;
}
则在主函数中,匿名函数就可以这样写
int main()
{
auto c = [ ] ( int a, int b ) -> int
{
return a+b;
}
}
其中,捕获列表可以有值捕获、引用捕获、隐式捕获等。具体内容参考如下博客:
https://blog.csdn.net/weixin_39640298/article/details/84996642
13.C、C++如何交错使用?
原理:C++不能直接调用C语言的函数,因为C++支持函数重载,因此在编译生成函数符号信息的时候,不能仅仅通过函数名,因为重载函数的函数名都是一样的,只是其中的参数不同,我也曾在网上看到过别人讲过这个知识点,关于C++的函数重载,表面是一样的函数名不同的参数,但在实际编译的时候,底层是对重载的函数名进行了换名的操作的,就相当于是不同的函数名了。所以C++不能直接调用C语言的函数。
操作如下:
修改test.h文件,用extern "C"将testCfun接口包裹起来,告诉编译器,这里是C接口,要按C代码的方式处理。
#include <stdio.h>
extern "C"
{
void testCfun();
}
虽然上面的C接口可以被C++正常调用了,但是如果这个C接口要被C代码调用呢?修改test.h文件如下
#include <stdio.h>
#ifdef _cplusplus
extern "C"
{
#endif
void testCfun();
#ifdef _cplusplus
}
#endif
_cplusplus是cpp中的自定义宏,定义了这个宏的话表示这是一段cpp的代码。
14.一些常用的STL头文件
白板编程的时候需要。
哈希表:#include <unordered_map>
eg.unordered_map<int, int> hash;
大根堆:#include <queue>
eg.priority_queue<int> maxheap;
小根堆:#include <queue>
eg.priority_queue<int,vector<int>,greater<int>> minheap;
标准模板库:算法:#include <algorithm>
#include <limits.h>
eg.INT_MIN,INT_MAX
15.C++中四种cast转换
① 静态类型转换:static_cast
基本语法:目标变量 = static_cast<目标类型>(源类型变量);
适用场景:主要用于将void*转换为其它类型的指针。
void* pv = pi;
pi = static_cast<int*>(pv); //合理
②动态类型转换:dynamic_cast
基本语法: 目标变量 = dynamic_cast<目标类型>(源类型变量);
适用场景:只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。
③(去)常类型转转:const_cast
基本语法: 目标变量 =const_cast<目标类型>(源类型变量);
适用场景: 主要用于去除指针或引用的const属性。
④重解释类型转换:reinterpret_cast
基本语法: 目标变量 = reinterpret_cast<目标类型>(源类型变量);
适用场景:1.任意类型指针或引用之间的显式转换 2.在指针和整型数之间的显式转换。感觉几乎是什么都能转,但尽量少用,可能会出问题。
16.C++中static的作用
我列举几个常见的应用吧,
①如果定义在函数内部的static变量是存储在全局静态区的,它作用域为该函数体,并且只初始化一次,它的生命周期为程序开始到程序结束。
②如果定义在类的static变量,它是属于整个类所有的,可以实现多个对象之间的数据共享。该static变量在内存中只存储一份,供所有对象共用,如果一个对象改变它,那么其他对象也会接收到这个改变。
③如果定义在类的static函数的话,不需要定义对象即可使用,并且这个函数不接受this指针,只能访问类的static成员。
④在函数名前面加上static变成静态函数,好处:<1> 静态函数不能被其他文件所用。<2> 其他文件中可以定义相同名字的函数,不会发生冲突。<3> 静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
17.C++中const的作用
①const定义的变量表示该变量不可被改变,在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了。
②对于指针来说,常量指针表示该指针指向的内容不可改变,指针常量表示该指针指向的地址不可改变。
③在一个函数声明中,const可以修饰形参,表明在函数内部不能改变其值。
④对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量。
指针常量和常量指针的问题:
int * const p //指针常量
指针常量是
①本身的指针是个常量;
②指向的地址可以不能够改变,但是指向的地址的内容的值可以改变。
const int *p = &a; //常量指针
常量指针是
①指向的对象不能通过这个指针来修改,就是值不能变。
②指针可以指向别处,因为指针本身为变量,可以指向任意地址。
18.C++中extern的作用
①在C++中调用C函数时,就需要在C++程序中用extern “C”声明要引用的函数。如:
extern "C" void fun(int a, int b);
告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,因为C++支持函数的重载。
(在函数重载中,C++编译器是通过对函数进行换名,将参数表的类型整合到新的函数名中,解决函数重载和名字冲突的矛盾)
②在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern是声明不是定义,即不分配存储空间。
19.C++中private protect public
访问权限
①public:类内外部都可以访问。
②private:只有类内部可以访问,子类和其对象都不可以。
③protect:子类可以访问,类内部可以访问。其他不可以
继承权限
①假设private作为基类(父类):
则其中的public成员,protected成员,private成员的访问属性在派生类(子类)中分别变成:private, private, private。
②假设public作为基类(父类):
则其中的成员在派生类(子类)中保持不变。
③假设protect作为基类(父类):
则只需要将派生类(子类)中的public变成protect。
20.C++中struct和class的区别
在C++中,可以用struct和class定义类,都可以继承。
区别在于:struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。