本文已参与「新人创作礼」活动,一起开启掘金创作之路。
请问构造函数中的能不能调用虚方法?
最好不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题;但是从效果上看,往往不能达到目的。
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
同样,进入基类析构函数时,对象也是基类类型。
所以,虚函数始终是仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。 我也终于到了能出去实习的年级,备战暑期实习和秋招,每天五道面试题,应该也不算多,贵在坚持和学透,希望能搞个大厂去上班
1.C++和C的特点与区别?
(1)C语言的特点
1.作为一种面向过程的结构化语言,易于调试和维护;
2.表现能力和处理能力极强,可以直接访问内存的物理地址;
3.C语言实现了对硬件的编程操作,也适用于应用软件的开发
4.C语言还具有效率高,可移植性强等特点。
(2)C++的特点
1.在C语言的基础上进行扩充和完善,使C++兼容了C语言的面向过程特点,又成为了一种面向对象的程序设计语言;
2.C++有三大特性(1)封装。(2)继承。(3)多态;
3.C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;
4.C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;
5.C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。
(3)两者的区别
1.C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。
2.C++是面对对象的编程语言;C语言是面对过程的编程语言。
3.C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
4.C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。
5.C++的STL库相对于C语言的函数库更灵活、更通用。
2.C++的多态是如何实现的?
(1)多态的概念:
多态分为静态多态和动态多态:(相同的函数名,不同的功能)
==静态多态==就是函数重载和运算符重载,编译期间确定函数地址,复用函数名(早绑定)
==动态多态==是派生类和虚函数实现运行时多态,在代码执行期间确定函数地址(晚绑定)
代码如下:我们调用此函数的本意是想让小猫来说话,但是运行出来的结果是==动物在说话==,这就是静态多态,而我们如果将基类函数定义成虚函数的化,就能实现动态多态,代码运行出来的结果是==小猫在说话==
#include <iostream>
using namespace std;
//多态
//动物类
class Animal
{
public:
void speak() { //早绑定
//virtual void speak() { //晚绑定
cout << "动物会说话" << endl;
}
};
//派生类:猫
class Cat : public Animal
{
public:
//重写 函数返回值类型 函数名 参数列表 完全相同
void speak() {
cout << "小猫会说话" << endl;
}
};
//执行说话的函数
//地址早绑定
//如果想执行让猫说话,那么需要晚绑定
//动态多态满足条件:
//1.有继承关系
//2.子类重写父类的虚函数
//动态多态的使用:
//父类的指针或引用 指向子类对象
void doSpeak(Animal &animal) {//Animal &animal = cat
animal.speak();
}
void test01() {
Cat cat;
doSpeak(cat);
}
int main() {
test01();
return 0;
}
如何实现多态:在函数前面加一个virtual后,使得该函数变成虚函数,函数的结构发生变化,新增一个虚函数指针指向虚函数表,而表内记录虚函数的地址,当子类继承父类时,子类的结构变得和父类一样,在子类对父类的虚函数进行重写后,则子类虚函数表内之前的虚函数地址会被替换成重写后的子类虚函数地址,也就实现了多态。
3.C++的虚函数是如何实现的?
(1)虚函数概念:
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};
(2)如何实现虚函数:
都会有一个虚函数表vptr,当我们在代码中通过这个实例来调用虚拟函数时,都是通过虚表指针(vfptr)先找到虚函数表(vftable), 接着在虚函数表中再找出指向的某个真正的虚拟函数地址。虚拟函数表中的内容就是类中按顺序声明的虚拟函数组织起来的。在派生的时候,子类都会 继承父类的虚表指针(vfptr),若子类改写了父类中的虚拟函数,则子类的vfptr成员也会作修改,此时,子类的vfptr成员指向的是子类所改写父类的虚拟函数地址。
4.C++如何实现内存管理?
在C++中,虚拟内存分为代码段、数据(data)段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
data段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
5.C++11中有哪些新特性?
1.语法的改进
(1)统一的初始化方法
(2)成员变量默认初始化
(3)auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)
(4)decltype 求表达式的类型
(5)智能指针 shared_ptr
(6)空指针 nullptr(原来NULL)
(7)基于范围的for循环
(8)右值引用和move语义 让程序员有意识减少进行深拷贝操作
2.标准库扩充(往STL里新加进一些模板类,比较好用)
(9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高
(10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串
(11)Lambda表达式
6.可变参数模板的作用
C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。 例如:
Template<class ... T>
void func(T ... args)
{
cout<<”num is”<<sizeof ...(args)<<endl;
}
func(); // args不含任何参数
func(1); // args包含一个int类型的实参
func(1,2.0) // args包含一个int一个double类型的实参
其中T叫做模板参数包,args叫做函数参数包
省略号作用如下:
1)声明一个包含0到任意个模板参数的参数包
2)在模板定义得右边,可以将参数包展成一个个独立的参数
C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。
7.malloc的原理以及brk系统调用和mmap系统调用的作用分别是什么?
Malloc函数用于动态分配内存。 为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。 当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
8.智能指针有哪几种?
除去一个C++11废弃掉的auto_ptr(被unique_ptr所替代),有三个智能指针是常用的,分别是unique_ptr、shared_ptr、weak_ptr。
(1)unique_ptr
实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
(2)shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数: use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
(3)weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。==进行该对象的内存管理的是那个强引用的 shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段==。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
为什么要使用智能指针?
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
9.如何解决智能指针循环依赖的问题
首先我们要知道,循环引用问题是shared_ptr智能指针引起的。shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。
(1)引起循环依赖的原因
当std::shared_ptr实例内部的引用计数为0时,就释放所指向的实例空间,而当两个类有循环依赖问题存在时,就会导致引用永远不为0,因此就不会被释放,从而导致内存泄漏。
(2)如何解决
将A类和B类中的shared_ptr成员变量类型替换为weak_ptr,就能解决内存泄漏问题。
#include <iostream>
#include <memory>
using namespace std;
class B;//前置声明
class A {
public:
void setB(shared_ptr<B> &pb) {
this->sharedPb = pb;
}
A() {
cout << "A::A()" << endl;
}
~A() {
cout << "A::~A()" << endl;
}
private:
// shared_ptr<B> sharedPb;//共享指针
weak_ptr<B> sharedPb;
};
class B {
public:
void setA(shared_ptr<A> &pa) {
this->sharedPa = pa;
}
B() {
cout << "B::B()" << endl;
}
~B() {
cout << "B::~B()" << endl;
}
private:
// shared_ptr<A> sharedPa;
weak_ptr<A> sharedPa;
};
int main() {
{//创建块作用域,用于测试
auto sharedPa = make_shared<A>();
auto sharedPb = make_shared<B>();
sharedPa->setB(sharedPb);
sharedPb->setA(sharedPa);
}
//此时sharedPa和sharedPb已经无法访问了,同时实例对象被释放,因此解决了内存泄漏问题
return 0;
}
10.STL中Vector,List和Map的底层原理,以及如何实现?
vector
是表示可以改变大小的数组的序列容器 底层数据结构为数组,支持快速随机访问
list
是序列容器,允许在序列中的任何地方进行常数时间插入和擦除操作,并在两个方向上进行迭代 底层数据结构为双向链表,支持快速增删,不支持随机访问 无序、可重复
map
是关联容器,按照特定顺序存储由 key value (键值) 和 mapped value (映射值) 组合形成的元素。 底层数据结构为红黑树 有序,不可重复 查找的时间复杂度为O(logn)
如何实现它们的底层原理
(1)vector
1.使用动态数组的方式实现的,里面有一个指针指向一片连续的内存空间。 2.如果动态内存的数组空间不够,就要动态地重新分配内存,一般是当前大小的两倍,然后把原数组的内容拷贝过去,接着释放原来的空间。 3.所以在一般情况下,其访问速度同一般数组,只有在重新分配内存发生时,其性能才会下降。 4.vector的size()表示数组中元素个数有多少,capacity()表示数组有多大容量。
(2)List
1.用双向链表来实现的,是一种物理存储单元上非连续、非顺序的存储结构。 2.以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
(3)map
map以红黑树为底层机制。红黑树是一种二叉平衡搜索树,自动排序效果不错。