1、简述下C++语言的特点
-
三大特性(封装、继承、多态)
-
面向对象
-
引入模板可复用性高
2、C++中struct与class的区别
-
使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
-
class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
-
class 可以使用模板,而 struct 不能
3、include头文件中""与<>的区别
""的查询目录为:当前头文件目录->编译器设置的头文件目录->系统变量
<>的查询目录为:编译器设置的头文件目录->系统变量
4、C语言结构体与C++结构体的区别
5、导入c函数的关键字是什么,c语言编译与c++编译有何不同
extern,告诉编译器这部分按c语言编译
C语言编译时不支持函数重载,编译一般仅包含函数名,而C++支持函数重载,编译时一般包含函数名与参数类型
6、简述C++从代码到可执行二进制文件的过程
-
预编译:处理#define、#if、#endf等,过滤所有注释
-
编译:生成汇编语言,需要经过词法分析、语法分析等
-
汇编:将汇编语言转换为可执行的指令
-
链接:将不同目标文件链接为程序
链接分为动态链接与静态链接: •动态链接:在链接时未将调用函数代码链接进去,而是在执行时寻找链接函数,库文件被删除,程序就不可以执行 •静态链接:在链接时将代码链接进可执行文件,即使库文件被删除,程序依旧可以执行
7、数组与指针的区别
数组:连续的内存空间
指针:一种变量,指向地址空间
-
二者均可通过增减偏移量来访问数组中的元素。
-
同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
-
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
-
当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。
8、指针函数与函数指针的区别,应用场景
指针函数:返回值为指针的函数
函数指针:指向函数的指针。可以当做参数传递给其他函数(回调)
int *func(int a); //指针函数的声明
int (*func)(int a); //函数指针的声明
9、静态变量什么时候初始化
全局或静态对象当且仅当首次使用时初始化
10、nullptr可以调用成员函数吗,为什么?
可以,函数地址在编译对象时便绑定,与指针空不空无关
11、什么是野指针与悬空指针
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。
解决方法:
- 好的编程习惯
- 智能指针
12、内联函数与宏函数的区别
-
内联函数有类型检查,且在编译器处理
-
宏函数无类型检查,在预编译时处理
inline函数一般用于比较小的,频繁调用的函数,这样可以减少函数调用带来的开销
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少。
另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,
自动地取消不符合要求的内联。
13、i++与++i的区别
i++先赋值再加,只能充当右值,速度慢
++i先加再赋值,能够充当左值,速度快
++i;
00007FF7D69F17E8 mov eax,dword ptr [i]
00007FF7D69F17EB inc eax
00007FF7D69F17ED mov dword ptr [i],eax
i++;
00007FF7D69F17F0 mov eax,dword ptr [i]
00007FF7D69F17F3 inc eax
00007FF7D69F17F5 mov dword ptr [i],eax
都不是原子操作
进程有一个全局变量i,还有两个线程。i++ 在两个线程里边分别执行100次,能得到的最大值和最小值分别是多少?
++i 和 i++ 是否为原子操作?_c++ ++i自增操作是原子操作吗_LikeMarch的博客-CSDN博客
14、new与malloc的区别及底层原理
-
new为操作符,调用时先分配内存,再调用构造函数,释放时调用析构函数,安全且会返回异常
-
malloc为函数,没有构造函数与析构函数,可能不安全,发生错误返回null
-
new的底层:创建对象并返回一个对象
-
malloc的底层:开辟小于128K时调用brk(),开辟大于128K时调用mmap(),采用内存池管理方式,减少内存碎片,隐式链表将空闲块连在一起
15、const与define的区别
const有类型,在编译时处理,占用空间
define无类型,在预编译时处理,不占用额外空间
16、const int* a, int const* a, const int a, int *const a,const int * const a的区别
有左边先修饰左边,没有左边则修饰右边。
例如 const int* a,没有左边,则修饰右边的int,也就是指针所指向的值不变
而int const* a,有左边,则修饰左边int,也是指针所指向的值不变
17、内联函数与函数的区别,内联函数的作用
内联函数:inline关键字,避免函数调用开销,不需要寻址,要求函数体代码简单
函数: 普通函数,有函数调用开销,需要寻址,不要求函数体简单
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
18、c++传值方式及区别
传参方式:值传递、引用传递、指针传递
-
值传递:形参变化,不影响实参值;发生拷贝,拷贝值
-
引用传递:形参变化,会影响实参值;不发生拷贝,只是绑定对象
-
指针传递:在指针指向没有发生改变的前提下,形参变化,会影响实参值;发生拷贝,拷贝指针
19、堆与栈的区别
堆由程序员分配,置于二级缓存,慢,地址由低到高
栈由系统分配,置于以及缓存,块,地址由高到低
20、什么是内存泄露及解决方法
申请使用内存结束后,并未释放
例:new/malloc后没有delete/free释放
解决:
- 良好的编程习惯
- 智能指针
21、内存模型,每个段存储的数据都是什么?程序启动过程
-
代码段:二进制执行代码。只读。
-
数据段:已初始化的全局变量、全局静态变量
-
BSS 段:未初始化的或者初始化为0全局变量、局部静态变量。
-
运行时多出两个区域:堆区和栈区。
堆区:new/malloc放在堆区
栈区:存储局部变量、函数参数值
-
共享区,位于堆和栈之间。
程序启动的过程:
-
操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
-
加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
-
加载器针对该程序的每一个动态链接库调用LoadLibrary (1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。 (2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。 (3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3 (4)调用该动态链接库的初始化函数
-
初始化应用程序的全局变量,对于全局对象自动调用构造函数。
-
进入应用程序入口点函数开始执行。
22、初始化为0的全局变量在bss段还是data段
BSS段:未初始化的或者初始化为0的全局变量、静态变量
23、请简述atomoic内存顺序(了解)
-
memory_order_relaxed:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。
-
memory_order_consume:只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。
-
memory_order_acquire:当前线程的读写操作都不能重排到此操作之前。
-
memory_order_release:当前线程的读写操作都不能重排到此操作之后。
-
memory_order_acq_rel:memory_order_acq_rel在此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。
-
memory_order_seq_cst:memory_order_seq_cst比std::memory_order_acq_rel更为严格。memory_order_seq_cst不仅是一个"获取释放"内存顺序,它还会对所有拥有此标签的内存操作建立一个单独全序。
除非你为特定的操作指定一个顺序选项,否则内存顺序选项对于所有原子类型默认都是memory_order_seq_cst。
24、什么是内存对齐,哪些数据结构用了,为什么要内存对齐
struct / class / union
内存对齐:从结构体存储的首地址开始,元素按结构体中size最大的成员对齐
加快CPU访问数据效率,如果不对齐,访问数据可能需要多次访问并拼接
25、简述面向对象三大特征
封装、继承、多态
26、C++重写与重载、重定义的区别与实现
重写:函数名与参数均相同,由virtual+继承实现
重载:函数名相同,参数列表不同,由倾轧技术实现(编译时带上函数参数名)
重定义:派⽣类新定义父类中相同名字的非virtual 函数,参数列表和返回类型都可以不同,
27、构造函数种类与作用
默认构造 | 默认存在,类中默认的构造函数(无参),当存在初始化构造时则不存在默认构造 |
---|---|
初始化构造 | 定义类,可被重载 |
拷贝构造 | 默认存在,调用方式B(A),默认为浅拷贝,需要自己定义深拷贝 |
赋值构造 | 默认存在,重载了“=”,默认为浅拷贝,需要自己定义为深拷贝 |
移动构造 | 首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。new\delete用到 |
28、一个类默认生成那些函数
默认构造、拷贝构造、赋值构造、析构函数
29、C++类对象的初始化与析构的顺序(存在多重继承)
构造:父类构造->成员类对象构造->自身构造
析构:自身析构->成员类对象析构->父类析构
30、向上转型与向下转型
向上:子类转父类,安全
向下:父类转子类,不安全
31、如何实现深拷贝
-
浅拷贝:源对象和目标对象共用一份实体,地址其实还是相同的,拷贝构造时仅拷贝指针
-
深拷贝,拷贝时先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去
32、简述C++多态
多态分为静态多态与动态多态
重写:函数名与参数均相同,由virtual+继承实现
重载:函数名相同,参数列表不同,由倾轧技术实现(编译时带上函数参数名)
33、为什么可以虚析构、但不能虚构造
虚析构:释放子类对象,基类指针指向子类对象时,通过基类指针释放子类对象,防止内存泄漏
不能虚构造:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。
虚指针指向的就是虚函数表,本质是一个数组,存着所有的虚函数指针。
如果父类的虚函数没有被子类改写, 那么子类的虚函数表中的元素就是父类的对应的虚函数指针;相反,如果子类改写了父类的虚函数,那么对应的虚函数表中的元素就是自己的虚函数指针,决议这个指向的过程发生在运行时,就是所谓的动态绑定!
虚函数中的元素的顺序就是按照虚函数定义的方式存储。
存储位置:虚函数表存储在只读数据段(.rodata
)、虚函数存储在代码段(.text
)、虚表指针的存储的位置与对象存储的位置相同,可能在栈、也可能在堆或数据段等。
34、说说模板类在什么时候实现
在编译时由编译器决定实例化类型
35、类继承时,子类与子类对象对父类访问权限
继承方式只影响外界通过子类对父类成员的访问权限
public继承,父类成员的访问权限全部保留至子类:
protected继承,父类public成员的访问权限在子类中降至protected;
private继承,父类public,protected成员的访问权限在子类中均降至private
36、C++是否可以定义引用数据成员
可以,但
-
必须提供构造函数初始化引用变量
-
构造函数形参也为引用
-
不能在构造函数内初始化,需要在初始化列表初始化
37、什么是常函数
成员函数后加const,表示不可以对类成员进行修改
38、什么是虚继承,解决什么问题,实现原理
虚继承的基类中含有虚基表指针,指向虚基表
39、简述虚函数与纯虚函数,及实现原理、区别
虚函数采用vtable,类中含有纯虚函数时,其vtable不完全,有空位。导致指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。
40、拷贝构造函数值传递方式是什么,为什么?
引用。否则会在传递时发生拷贝,从而陷入递归拷贝
41、什么是仿函数
重载了()的类
42、C++中哪些不可以被声明为虚函数
普通函数、静态成员函数、内联函数、构造函数、友元函数
43、虚函数表中内容是什么时候写入的?
编译时写入,对象构建时将vptr写入对象空间
44、STL的组成部分与作用
-
容器(Container) :一种数据结构, 如list, vector, 和deques,以模板类的方式提供。
-
算法(Algorithm):操作容器中的数据的模板函数。
例如,STL用sort()来对一 个vector中的数据进行排序函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。 -
迭代器(Iterator):提供了访问容器中对象的方法。
事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象; -
仿函数(Function object):函数对象, 就是重载了操作符的struct
-
适配器(Adaptor):一种接口类,专门用来修改现有类的接口,提供一种新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
-
空间配制器(Allocator):为STL提供空间配置的系统。其中主要工作包括两部分:
(1)对象的创建与销毁;
(2)内存的获取与释放。
45、STL中常见容器与实现原理
顺序容器(无排序关系) | vector | 插入O(n) 查看O(1) 删除O(n) | |
---|---|---|---|
deque(双向队列) | 双向队列 | 插入O(n) 查看O(1) 删除O(n) | |
list | 双向链表 | 插入O(1) 查看O(N) 删除O(1) | |
关联型容器 | set/multiset | 红黑树 | 插入O(logN) 查看O(logN) 删除O(logN) |
map/multimap | 红黑树 | 插入O(logN) 查看O(logN) 删除O(logN) | |
容器适配器 | stack | ||
queue(队列) | |||
priority_queue(优先级队列) | |||
其他 | hashtable | 函数映射 |
46、C++ STL空间配置器实现
STL中,将对象的构造分成空间配置和对象构造两部分。
内存配置操作: 通过alloc::allocate()实现 内存释放操作: 通过alloc::deallocate()实现
对象构造操作: 通过::construct()实现 对象释放操作: 通过::destroy()实现
关于内存空间的配置与释放,STL采用了两级配置器:
一级配置器主要考虑大块内存空间,利用malloc和free实现;
二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
47、为什么需要迭代器,迭代器什么时候会失效
迭代器是类,使得不暴露内部结构而循环遍历
vector\deque删除一个元素,后面的均会失效,而关联容器不会
48、STL中resize与reserve的区别
resize既改变capacity大小,也改变size大小
reserve只改变capacity大小
49、说说map与unordered_map的区别,底层实现
map的底层实现是红黑树,unordered_map的底层为哈希表,且无序
50、map的实现原理
map是关联型容器,底层为红黑树,所有元素均为pair,同时拥有键值
51、push_back与emplace_back的区别
push_back()会先构造临时对象再拷贝到其末尾,而emplace_back()则没有拷贝,直接在尾部构造
52、有指针为什么还需要迭代器
迭代器把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
53、STL 容器动态链接可能产生的问题
容器是一种动态分配内存空间的一个变量集合类型变量。容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
54、C++新特性
语法改进:
-
统一初始化方式:用{}初始化
-
成员变量默认初始化
-
auto关键字:编译器自动判断类型
-
decltype:求表达式i类型
-
智能指针:防止野指针与内存泄露
-
空指针nullptr
-
基于范围的for循环
标准库扩充
-
哈希表:效率更高
-
正则表达式
55、简述Lambda匿名函数
一种具有函数性质的,可以像表达式一样,做为参数传递给其它函数体的一种结构体
Lambda 表达式就是一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个Lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,Lambda可以定义在函数内部,其语法格式如下:
[capture list](parameter list) mutable(可选) 异常属性->return type{function body}
capture list(捕获列表)
是一个Lambda所在函数中定义的局部变量的列表,通常为空,表示Lambda不使用它所在函数中的任何局部变量。也可以使用 “=” ,表示函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(编译器自动为我们按值传递了所有局部变量)也可以使用 “this” , [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。也可以使用 “&”,表示 以引用方式捕获外部作用域中所有变量
-
parameter list (参数列表) 相当于函数入参
-
mutable 参数选项
-
ret 返回值类型
-
body 函数体
其中Lambda表达式必须的部分只有capture list和function body。
-
在Lambda忽略参数列表时表示指定一个空参数列表,
-
忽略返回类型时,Lambda可根据函数体中的代码推断出返回类型。
// 表示:我们定义一个 可调用的对象 f ,它不接受任何参数,返回值是42
auto f=[]{return 42;}
main.cpp
#include<iostream>
#include<vector>
#include <algorithm>
auto f = [] {return 42; };
int a = 43;
auto lamdba = [=]()->void {
std::cout << "in lamdba: " << a << std::endl;
};
int main() {
// 直接调用 Lamdba几种方式(1)
std::cout << f() << std::endl;
std::cout << [] {return 42; }() << std::endl;
// 直接调用 Lamdba几种方式(2)
lamdba();
// 直接调用 Lamdba几种方式(3)有参
// [](int a)-> void {
// cout << a;
// }(1);
// 外部间接调用: Lambda函数变量 Vector
[](int val) {
cout << val;
}
//上面Lamdba表达式 是由 for_each() 调用的
for_each(v.begin(),v.end(),[](int val)
{
cout << val;
});
}
添加 mutable 选项修改变量值
#include<iostream>
auto f = [] {return 42; };
int a = 43;
auto lamdba = [=]()->void {
std::cout << "in lamdba: " << a << std::endl;
};
auto lamdba2 = [=]() mutable ->void {
a = 45;
std::cout << "in mutable lamdba: " << a << std::endl;
};
int main() {
lamdba();
// 添加 mutable 修改变量值
lamdba2();
}
// 打印结果
in lamdba: 43
in mutable lamdba: 45
56、智能指针与指针的区别
智能指针是对指针的一层封装,管理对象生命周期,安全释放。
57、C++中智能指针有哪些?分别解决的问题与区别
- auto_ptr:采用所有权模式。存在潜在的内存内存崩溃问题
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。
- unique_ptr:实现独占式拥有,保证同一时间只有一个智能指针可以指向该对象,较auto_ptr更安全
实现原理:将拷贝构造函数和赋值拷贝构造函数申明为private或delete。
不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,
通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr
- shared_ptr:实现共享拥有概念,多个智能指针可以同时指向同一个对象,该对象会在“最后一个引用被销毁”时释放。实现原理:计数器。是为了解决auto_ptr在对象所有权上的局限性
实现原理:有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。
- weak_ptr:是一种不控制对象生命周期的智能指针,他指向一个shared_ptr管理的对象。weak_ptr是为了解决shared_ptr相互引用的死锁问题。协助shared_ptr智能指针工作。
shared_ptr存在一个问题,当两个shared_ptr指针相互引用时,那么这两个指针的引用计数不会下降为0,资源得不到释放。当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导 致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以
注意:
1)weak_ptr 能不能知道对象计数为 0,为什么?
不能。
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。
进行该对象管理的是那个引用的shared_ptr。weak_ptr只是提供了对管理 对象的一个访问手段。
weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针,配合shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。
2)weak_ptr 如何解决 shared_ptr 的循环引用问题?
weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
线程安全性
多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的,即使这些shared_ptr拥有的是同样的对象。但是如果多线程访问(有写操作)同一个shared_ptr,则需要同步,否则就会有race condition 发生。也可以使用 shared_ptr overloads of atomic functions来防止race condition的发生。
多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。
58、类的底层原理!是什么?
首先看一个类:
class A
{
public:
void get();
}
A a;
a.get()表示什么:get(&a),由于在类的成员函数的第一个參数都是一个指向该类数据结构的指针(静态成员函数除外),所以成员函数get()的存在形式为void get(A* this);这也能说明为什么我们在成员函数的定义中总是能够用this来指代调用对象。
要使用一个C++类。必要的条件是在编译期能得到这个类的头文件,并在链接期能够找到相应的符号的链接地址(比方成员函数、静态数据成员等)
一个C++类实际上是声明或定义了例如以下几类内容:
1.声明了一个数据结构。类中的非静态数据成员、代码中看不到但假设有虚函数就会生成的虚表入口地址指针等。
2.声明并定义了一堆函数,它们第一个參数都是一个指向这个数据结构的指针。这些实际上就是类中那些非静态成员函数(包含虚函数),它们尽管在类声明中是写在类的一对大括号内部。但实际上没有不论什么东西被加到前面第1条中所说的内部数据结构中。
实际上。这种声明仅仅是为这些函数添加了两个属性:函数名标识符的作用域被限制在类中;函数第一个參数是this。被省略不写了。
3.声明并定义了还有一堆函数。它们看上去就是一些普通函数,与这个类差点儿没有关系。这些实际上就是类中那些静态函数。它们也是一样,不会在第1条中所说的内部数据结构中添加什么东西,仅仅是函数名标识符的作用域被限制在类中。
4.声明并定义了一堆全局变量。
这些实际上就是类中那些静态数据成员。
5.声明并定义了一个全局变量。此全局变量是一个函数指针数组,用来保存此类中全部的虚函数的入口地址。当然,这个全局变量生成的前提是这个类有虚函数。
59、如何理解抽象类?
-
抽象类:
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。
-
抽象类有如下几个特点:
1)抽象类只能用作其他类的基类,不能建立抽象类对象。
2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
60、说说什么是虚基类,可否被实例化?
在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类
可以被实例化。
61、简述一下拷贝赋值和移动赋值?
-
拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。
-
移动赋值是通过移动构造函数来赋值,二者的主要区别在于
1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;
2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
62、解释下 C++ 中类模板和模板类的区别
-
类模板:定义类的模板
-
模板类:类模板的实例化
63、decltype与auto的区别
auto 和 decltype 关键字都可以自动推导出变量的类型
auto varname = value; decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
auto:根据"="右边的初始值 value 推导出变量的类型,要求变量必须初始化。
decltype:根据 exp 表达式推导出变量的类型,跟"="右边的 value 没有关系,不要求初始化。
64、右值引用和move语义
1)左值、右值
左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。
int a = 5;
- a可以通过 & 取地址,位于等号左边,所以a是左值。
- 5位于等号右边,5没法通过 & 取地址,所以5是个右值。
2)左值引用、右值引用
左值引用:能指向左值,不能指向右值
右值引用:能指向右值,不能指向左值
int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败
//-----------------------------------------------------//
int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值
3) 右值引用有办法指向左值吗:
std::move:唯一的功能是把左值强制转化为右值
int &&ref_a = 5;
ref_a = 6;
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
4) 左值引用、右值引用本身是左值还是右值?
被声明出来的左、右值引用都是左值
5)作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
6)右值引用和std::move的应用场景
右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。
vector::push_back使用std::move提高性能:
// 例2:std::vector和std::string的实际例子
int main() {
std::string str1 = "aacasxs";
std::vector<std::string> vec;
vec.push_back(str1); // 传统方法,copy
vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val);
void emplace_back (Args&&... args);
65、 正则表达式
正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串。常用符号的意义如下:
符号 | 意义 |
---|---|
^ | 匹配行的开头 |
$ | 匹配行的结尾 |
. | 匹配任意单个字符 |
[…] | 匹配[]中的任意一个字符 |
(…) | 设定分组 |
\ | 转义字符 |
\d | 匹配数字[0-9] |
\D | \d 取反 |
\w | 匹配字母[a-z],数字,下划线 |
\W | \w 取反 |
\s | 匹配空格 |
\S | \s 取反 |
+ | 前面的元素重复1次或多次 |
* | 前面的元素重复任意次 |
? | 前面的元素重复0次或1次 |
{n} | 前面的元素重复n次 |
{n,} | 前面的元素重复至少n次 |
{n,m} | 前面的元素重复至少n次,至多m次 |
| | 逻辑或 |
66、简述一下 C++11 中四种类型转换
-
const_cast:将const变量转为非const
-
static_cast:最常用,可以用于各种隐式转换,比如非const转const,static_cast可以用于类向上转换,但向下转换能成功但是不安全。
-
dynamic_cast:只能用于含有虚函数的类转换,用于类向上和向下转换
向上转换:指子类向基类转换。
向下转换:指基类向子类转换。
这两种转换,子类包含父类,当父类转换成子类时可能出现非法内存访问的问题。
dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。dynamic_cast可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等转换失败。运用RTTI技术,RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。在c++层面主要体现在dynamic_cast和typeid,vs中虚函数表的-1位置存放了指向type_info的指针,对于存在虚函数的类型,dynamic_cast和typeid都会去查询type_info。
4. reinterpret_cast:可以做任何类型的转换,不过不对转换结果保证,容易出问题。
注意:为什么不用C的强制转换:C的强制转换表面上看起来功能强大什么都能转,但是转换不够明确,不能进行错误检查,容易出错。
67、C++ 中 const 和 static 关键字(定义,⽤途)
static 作⽤:控制变量的存储⽅式和可⻅性。
作⽤⼀:修饰局部变量:
存储位置 | 生命周期 | 作用域 | |
有static局部变量 | 静态区 | 整个程序 | 语句块 |
无static局部变量 | 栈区 | 语句块 | 语句块 |
作⽤⼆:修饰全局变量、函数:
可见性 | |
有static修饰全局变量、函数 | 本文本可见 |
无static修饰全局变量、函数 | 同工程可见,访问添加extern即可 |
作⽤四:修饰类函数/类变量:
如果 C++ 中对类中的某个函数⽤ static 修饰,则表示该函数属于⼀个类⽽不是属于此类的任何 特定对象,无this指针,只能访问类的static变量;
如果对类中的某个变量进⾏ static 修饰,则表示该变量为所有的对象所有,存储空间中只存在⼀个副本,可以通过类和对象去调⽤。 (补充:静态⾮常量数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。)
作⽤五:类成员/类函数声明 static:
- static 类对象必须要在类外进⾏初始化:static 修饰的变量先于对象存在,所以 static 修饰的变量要在类外初 始化;
- static 成员函数不能被 virtual 修饰:static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意 义;
- 静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个 vptr 指针,⽽ vptr 是通过 this 指 针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function
const 关键字:含义及实现机制
const 修饰基本类型数据类型:修饰符 const 可以⽤在类型说明符前,也可以⽤在类型说明符后, 其结果是⼀样的。在使⽤这些常量的时候,只要不改变这些常量的值即可。
const 修饰指针变量和引⽤变量:如果 const 位于⼩星星的左侧,则 const 就是⽤来修饰指针所指向的变量,即指 针指向为常量;如果 const 位于⼩星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
const 应⽤到函数中:作为参数的 const 修饰符:调⽤函数的时候,⽤相应的变量初始化 const 常量,则在函数体 中,按照 const 所修饰的部分进⾏常量化,保护了原对象的属性。
[注意]:参数 const 通常⽤于参数为指针或引⽤ 的情况; 作为函数返回值的 const 修饰符:声明了返回值后,const 按照"修饰原则"进⾏修饰,起到相应的保护作 ⽤。
const 在类中的⽤法:const 成员变量,只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔是可以改变的。因为 类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员, 因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构 造函数的初始化列表中进⾏。
const 成员函数:const 成员函数的主要⽬的是防⽌成员函数修改对象的内容。要注 意,const 关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员函数不含 有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。 const 修饰类对象,定义常量对象:常量对象只能调⽤常量函数,别的成员函数都不能调⽤。
补充:const 成员函数中如果实在想修改某个变量,可以使⽤ mutable 进⾏修饰。成员变量中如果想建⽴在整个类 中都恒定的常量,应该⽤类中的枚举常量来实现或者 static const。
C ++ 中的 const类成员函数(⽤法和意义) 常量对象可以调⽤类中的 const 成员函数,但不能调⽤⾮ const 成员函数; (原因:对象调⽤成员函数时,在形 参列表的最前⾯加⼀个形参 this,但这是隐式的。this 指针是默认指向调⽤函数的当前对象的,所以,很⾃然, this 是⼀个常量指针 test * const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即⼩括号) 后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test * const,即不可以通过 this 指针来改变指向对象的值
68、常量存放在内存的哪个位置?
对于局部常量,存放在栈区;
对于全局常量,编译期⼀般不分配内存,放在符号表中以提⾼访问效率;
字⾯值常量,⽐如字符串,放在常量区
69、new / delete ,malloc / free 区别
new:操作符,有异常机制
malloc:库函数,无异常机制
有malloc,为什么还需要new:为对于非内部数据类型而言,光用 malloc/free 无法满足动 态对象的要求。对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前要⾃动执⾏析构函数。也无法对malloc进行更改。
70、volatile 和 extern 关键字
volatile :
易变性:不读寄存器,从内存中读数据
不可优化性:不对变量进行优化
顺序性:不对顺序进行优化
extern :
在C++中使用C语言代码,编译器规则不同(C++中函数倾轧,C中没有)
引用其他文件的变量,加速编译
71、计算下⾯⼏个类的⼤⼩
class A{}; sizeof(A) = 1; //空类在实例化时得到⼀个独⼀⽆⼆的地址,所以为 1.
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit) //当 C++ 类中有虚函数的时候,会有⼀
个指向虚函数表的指针(vptr)
class A{static int a; }; sizeof(A) = 1;//静态成员变量在静态区开辟空间,不计入
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4
-------------------------*1*-------------静态成员
class base
{
public:
base()=default;
~base()=default;
private:
static int a;
int b;
char c;
};
sizeof(base) = 8;//类内函数对大小不影响,存在内存对齐
-------------------------*2*-------------虚函数
class Base {
public:
int a;
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
sizeof(base) = 16;//含有虚函数指针,大小为8,所有内存对齐,大小为16
-------------------------*3*-------------虚继承
class A {
int a;
};
class B:virtual public A{
virtual void myfunB(){}
};
class C:virtual public A{
virtual void myfunC(){}
};
class D:public B,public C{
virtual void myfunD(){}
};
sizeof(A) = 16;//虚继承时存在虚基类指针
sizeof(B) = sizeof(C) = 24; //指向虚基类指针+int + 虚函数表指针
sizeof(D) = 32;//指向B的虚基类指针+指向C的虚基类指针 + int + 虚函数表指针
- 空类的大小为1,因为空类可以实例化,实例化必然在内存中占有一个位置,因此,编译器为其优化为一个字节大小。
对于基类B,存在虚函数,所以在堆开辟空间,存储虚函数指针与long变量。虚函数指针指向虚函数表,虚函数表存储所有虚函数。
对于D类,同时继承B、C类,所以同时复制B、C虚函数表,并分别构建虚函数指针指向虚函数表,且在D类中对B类的虚函数进行重写,所以对B::bar()继承过来的虚函数表进行覆盖,并将自己类中虚函数表与从B中继承的虚函数表相合并。
72、编译器处理虚函数表应该如何处理
对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:
- 拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表
- 当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派⽣类的主基类
- 查看派⽣类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中。
73、虚继承内存布局(*重要*)
- 虚继承的派生类,如果定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。(非虚继承时,派生类新的虚函数直接扩展在基类虚函数表的下面。)
- 虚继承的派生类有单独的虚函数表,基类也有单独的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。
- 虚继承的派生类对象中,含有四字节的虚基表指针。
在C++对象模型中,虚继承而来的派生类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。我们通过一张图来更好地理解。
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" ; }
virtual void Bf() { cout << "B::Bf()" ; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" ; }
virtual void f1() { cout << "B1::f1()" ; }
virtual void Bf1() { cout << "B1::Bf1()" ; }
};
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : virtual public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i = 10000) :id(i) {}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)。
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
- 共同虚基类B的内容放到了派生类对象D内存布局的最后。
其中B1::vbptr中28指虚基类偏移位置,即(8 - 1)*4 = 28,B1::vbptr中-4指到B1::vpt偏移(0-1)*4 = -4
其中B2::vbptr中16指虚基类偏移位置,即(8 - 4)*4 = 16,B1::vbptr中-4指到B1::vpt偏移(3-4)*4 = -4
1、数据成员如何访问(直接取址)
跟实际对象模型相关联,根据对象起始地址+偏移量取得。
2、函数成员如何访问(间接取址)
跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。
3、多态如何实现?
多态(Polymorphisn)在C++中是通过虚函数实现的。如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。
4、为什么析构函数设为虚函数是必要的
析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。
5、如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了。
不存在虚继承时,子类虚函数与父类虚函数合并为同一个虚函数表,直接追加到表内
存在虚继承时,子类虚函数与父类虚函数分两个表存储,并通过两个虚表指针指向虚表,且父类虚表指针放在内存最后,由vbptr通过偏移量指出,从而实现查询父类虚函数的目的。
74、动态绑定与静态绑定
动态绑定:发生在运行时期,virtual函数是动态绑定的
静态绑定:发生在编译器,非虚函数是静态绑定的
75、调用拷贝构造函数的情况
- 对象以值传递的方式传入函数体
- 对象以值传递的方式返回
- 对象通过另一个对象进行初始化
76、二叉树
二叉树:任何节点最多只允许有两个⼦节点
二叉搜索树:任何节点的值⼀定⼤于其左子树的每⼀个节点的键值,并⼩于其右⼦树中的每⼀个节点的键值
平衡⼆叉树:
AVL-tree :⾼度平衡的平衡⼆叉树(严格的平衡⼆叉树)AVL-tree 是要求任何节点的左右⼦树⾼度相差最多为 1 的平衡⼆叉树。
RB-tree:红黑树。
77、