c++基础部分面经总结
1. 多态
1.1 什么是多态?
- 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性,简单的说就是:基类的引用指向子类的对象。
1.2 多态有什么好处?
- 不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可,提高了可复用性
- 派生类的功能可以被基类的方法或者引用变量所调用,也即向后兼容,挺高可扩展性和可维护性
1.3 c++中实现多态的方法
-
虚函数、抽象类、覆盖、模板
-
模板:
c++
支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数取得任意类型-
函数模板:
template<class T> void f(T &a) { return ; }
-
类模板:
template<class T> class f{ public: T a,b; T f(T &a,T &b){} };
-
模板的声明只能在全局、命名空间或类范围内进行,不能再局部范围函数内进行
-
-
1.4 重载、覆盖、隐藏都是什么?
- 重载:在同一个类中,具有相同名称的两个函数,并且形参不同,重载并不是多态
- 覆盖:派生类和基类之间,基类重写了派生类的方法,基类的方法与派生类的方法名称相同,形参相同,基类中必须有
virtual
- 隐藏:派生类覆盖了基类的方法,则从基类继承过来的方法就被隐藏
2. 指针
2.1 指针函数和函数指针
- 指针函数就是函数的返回值是指针
- 函数指针是指指向函数的指针
int add(int x,int y){
return x+y;
}
int *mul(int x,int y){
return x*y;
}
int (*fun)(int x,int y);
int main(){
// 指针函数的应用
int *a = mul(5+6);
// 函数指针的应用
fun = add;
printf("%d\n",(*fun)(1,2));
}
2.2 指针数组和数组指针
- 指针数组:保存指针的数组,即数组里面全是指针
int *p[]
- 数组指针:指向数组的指针
int (*p)[]
int main(){
int arr[4][4];
for (int i=0;i<4;i++){
for (int j=0;j<4;j++){
arr[i][j]=i*4+j;
}
}
int *a[4];
int (*b)[4];
for (int i=0;i<4;i++){
a[i]=arr[i];
}
//指针数组的应用
for (int i=0;i<4;i++){
for (int j=0;j<4;j++){
printf("%d ",*(a[i]+j));
}
printf("\n");
}
//数组指针的应用
for (int i=0;i<4;i++){
for (int j=0;j<4;j++){
printf("%d ",*(*(b+i)+j));
}
printf("\n");
}
}
2.3 智能指针
解决了内存泄漏(所谓的内存泄漏就是指已动态分配的堆内存由于某种原因未被释放,造成系统浪费,导致程序运行速度减慢甚至系统崩溃等严重后果)的问题,由于使用new
从动态内存堆上获取空间需要delete
释放,但是由于过早的return
可能会使得程序跳过delete
,所以引入智能指针。本质上所有的智能指针都是类,也就是用静态内存栈上的空间(因为栈空间会自动释放)来管理动态内存的空间,利用类的构造函数和析构函数来实现空间释放
野指针:就是指向一个已删除的对象或者未申请访问受限内存区域的指针
-
auto_ptr
(1)
auto_ptr
的拷贝构造将源指针的管理权给目标指针,导致源指针指针悬空(2)
auto_ptr
不能用来管理数组,析构中用的是delete -
unique_ptr
(1)
auto_ptr
进化版本,只允许对象的所有权归一个对象所有,也就是智能指针不能赋值给别的智能指针,当然可以临时赋值unique_ptr<int> q = unique_ptr<int>(new int(1));
-
share_ptr
(1) 计数智能指针,有多少share_ptr
指向当前地址空间
(2) 存在循环引用的缺陷
-
weak_ptr
(1) 为了解决
share_ptr
的问题而出现的,内部并没有*
2.4 指针和数组的区别
指针 | 数组 |
---|---|
保存数据的地址 | 保存数据 |
由地址间接访问 | 直接访问 |
动态申请 | 元素数目固定 |
存储在堆区 | 存储在栈区 |
2.5 空指针与野指针
所谓的空指针就是指向NULL
的指针,这样的指针实际指向地址0
所谓的野指针就是不知道指向了哪里
所以野指针是有很大的危害的,他可能指向了一个没有访问权限的地址,甚至是操作系统内核的地址,这样容易造成程序崩溃甚至系统崩溃。
2.6 void*
void*
可以是任意类型的指针
3. 虚函数
纯虚函数,不需要实现,在函数最前面加上virtual
,在后面加上=0
,虚函数是用虚函数表存放的
3.1 虚函数表
在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置,虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。
-
无继承
按照声明顺序存放
-
继承无覆盖
按照声明顺序存放,父类在子类前面
-
继承有覆盖
覆盖的函数占用父类原来的虚函数位置,其余位置跟无覆盖一样
-
多继承无覆盖
有多个虚函数表,按照声明顺序存放,子类的虚函数存储在第一个虚函数表的后面
-
多继承有覆盖
有多个虚函数表,按照声明顺序存放,子类覆盖的父类虚函数就覆盖相应的虚函数表,未覆盖的就接在第一个表后面
4. STL
主要由以下部分组成:
- 容器:
- 迭代器:
- 算法:
- 仿函数:
- 适配器:
- 空间配置器:
4.1 vector
- 底层实现是数组,采用动态空间,数据结构上就是线性连续的地址空间,用三个迭代器,其中
_Myfirst
和_Mylast
指向连续空间中已使用的范围,用_Myend
指向整个连续地址空间的末尾 vector
容量永远大于或等于其大小,一旦容量等于大小那么就是满载,如果在加入元素,那么需要整个vector
搬家
vector
和list
的区别
vector
空间连续,list
空间不连续
vector
可以常数级随即访问,list
O(n)
的访问速度
vector
插入删除比较慢,list
插入删除快
4.2 deque
- 双端队列采用分段连续空间存储,采用中央控制来维持整体连续的假象,所谓中控器也就是一段连续的地址空间,其中每个元素都是一个指针,用来指向另一段连续的地址空间,这个连续的地址空间叫做缓冲区,
deque
中元素就是存在缓冲区的
4.3 stack
4.4 queue
4.5 list
- 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
list
是一个双向链表
4.6 set/multiset
- 根据元素的值自动排序,不能通过迭代器改变
set
中元素的值 - 底层实现红黑树
set
和map
的区别
两者都是红黑树底层实现
set
只有key
没有value
,map
有key
和value
set
元素会自动排序,map
中是根据key
排序
set
中元素的key
值不能变,map
中key
值不能变,value
可变
set
和map
中元素都是key
不能重复,mulitset
和mulitmap
可以重复
4.7 map/multimap
- 根据键值自动排序,不能修改键值可以修改实值
- 底层实现红黑树
4.8 unordered_map
- 无序的
- 底层实现
hashtable
+buket
,hashtable
就是一个数组或者可以看做vector
,hash
冲突的解决方法就是在hash
值的元素位置下面挂buket
,当数据量小于8的时候就用链表链接,大于8就用红黑树
5. c++内存分配
5.1 分配图示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGiTfpSE-1616860213619)(C:\Users\23585\Desktop\TyporaWorkSpace\20161029171857434.png)]
堆、栈、自由存储区、全局/静态存储区、常量存储区、代码区
自由存储区:是
c++
中通过new
和delete
动态分配和释放对象的概念,通过new
来申请的内存区域可称为自由存储区
5.2 动态区
5.2.1 栈
按内存地址由高到低方向生长,速度快但自由性差,保存程序中的局部变量,这样的变量伴随着函数的终止和调用,相应的减少或增加。这样的变量在创建时期按照顺序加入,消亡的时候按照反的顺序移出。大小由编译时确定。
5.2.2 堆
自由申请的空间,大小由系统内存或者虚拟内存上线决定,速度较慢,但自由性大,可用空间大。
5.2.3 堆和栈的区别总结
-
申请方式不同
栈是系统自动分配,堆是程序员自己申请
-
申请后的响应
只要栈的空间足够,系统将为程序提供内存;采取连续内存分配方法分配内存给程序
-
申请大小限制
栈的空间小,是高地址向低地址扩展的数据结构,是一块连续的内存区域;堆的大小受限于计算机系统中的有效虚拟内存,是不连续的内存区域,由低地址向高地址扩展的数据结构,因此堆的空间比较灵活,也比较大
-
申请速度
栈的申请速度快;堆的就要慢,容易产生内存碎片,但是使用起来灵活
-
速度问题
栈是机器系统提供的数据结构,计算机在底层对栈提供支持,专门分配寄存器存放栈地址,栈操作有专门指令;堆由c++函数库提供的、机制很复杂,所以堆的效率比栈低很多
-
分配方式
堆是动态分配,栈有静态分配和动态分配,静态分配(如局部变量)由编译器完成,动态分配由
alloca
函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现
5.3 string的内存分配
当数据<=16
字节的时候存储在栈区,当数据>16
字节的时候存储在堆区
6. c++特点
-
继承并兼容
C
语言,具有C
语言优点 -
扩充
C
语言,内联函数,函数重载,更灵活、方便的内存管理,引用,模板类、函数模板关于内联函数
内联函数就是在程序内部将函数展开,所以当函数体积小的时候可以加快程序运行的速度,因为程序不需要再去调用函数而是顺序执行,但是当函数的体积很大展开就需要耗费大量的时间,这时内联函数无法保证运行速度会更快。
inline
必须放在函数定义体前面,不能放在声明前面,否则会无效- 类中的成员函数自动的成为内联函数
-
支持面向对象编程机制:封装、继承、多态
7. 类型转换
c语言的强制类型转换看似很强大什么都能转,但是转化不明确,不能进行错误检查,容易出错
7.1 static_cast
静态类型转换,多态向上(子类到基类的转换)转换,向下转换能成功但是不安全,结果未知,基本数据类型的转换、非const
转const
、void*
转指针等
7.2 dynamic_cast
动态类型转换,只能用于含有虚函数的类,用于类层次间的向上(也就是子类向父类)和向下转化,只能转指针和引用,向下转化的时候如果非法的对于指针就返回NULL
,对于引用就抛出异常,它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
7.3 const_cast
用于将const
变量转化为非const
变量
7.4 reinterpert_cast
几乎什么都可以转,比如将int
转化为指针,但是会出现问题,尽量少用
8. struct和class的区别
struct
默认成员访问权限是public
;而class
默认成员访问权限是private
struct
默认继承权限是public
;而class
默认继承权限是private
struct
不可以用于声明模板,而class
可以声明模板struct
在没有定义构造函数的情况下可以用{}
进行初始化;class
只有当所有数据成员及其函数为public
的情况下才可以用{}
初始化
9. resize和reserve的区别
resize
是重置容器内元素的个数reserve
是重置容器的容量大小
10. new和malloc
new
是c++
关键字,而malloc
是c
的一个函数,所以new
可以重载- 申请的内存所在位置:
new
分配在自由存储区,malloc
在堆上 - 返回值类型:
new
返回一个申请类型的指针,malloc
返回一个void*
类型的指针,需要强制类型转换 - 分配失败是的返回值:
new
会抛出一个异常,malloc
会返回一个空指针 new
不需要指定内存大小,malloc
需要指定内存大小new
不可以扩容,malloc
可以用realloc
扩容,他首先会查看是否能够在原位置扩充到足够大小,不可以的话在分配新的空间- 处理数组:
new
有new[]
,malloc
需要自己计算大小 - 互相调用:
operator new
可以调用malloc
,但是malloc
不可以调用new
- 内存不足时:
operator new
抛出异常,会先调用一个用户指定的错误处理函数;对于malloc
,用户无法决定,所以只能返回NULL
- 调用构造和析构函数:
new
会操作三步,调用operator new
函数,分配一块足够大的原始的未命名的空间,随后运行相应的构造函数,并传入初值,对象构造完毕后返回一个指向该对象的指针(delete
就是先调用析构函数,随后调用operator delete
);malloc
则不会调用
11. static
11.1 局部变量
给局部变量加上static
,表示变量不会因为函数执行结束而释放
11.2 全局变量
给全局变量加上static
,表示变量只文件内可见
11.3 函数
表示函数只文件内可见
11.4 类
修饰类的数据成员时,表示该类的所有对象这个数据成员只有一个实例,也即该实例归所有对象共有
修饰类的函数,该函数只能访问他自己的参数、类的静态成员和全局变量
12. 段错误
通常发生在访问非法内存地址的时候:如使用野指针,试图修改字符串常量的内容
13.c++源文件从文本到可执行文件
预处理阶段:对源码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
编译阶段:将经过预处理后的预编译文件转换称特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
14. c++类成员的访问权限
public
:这一类成员可以被成员函数、外部对象和派生类访问protected
:外部不可访问,只有派生类成员函数和本类成员函数可以访问private
:只有类内部成员函数可以访问
继承方式
公有继承
所有
protected
和public
可以被派生类继承而来,访问权限不变保护继承
所有
protected
和public
可以被派生类继承而来,访问权限变为protected
私有继承
所有
protected
和public
可以被派生类继承而来,访问权限变为private
15. 引用
引用就是对C++
对C
语言的重要扩充,引用就是一个变量的别名,对引用的操作与对变量直接操作完全一样。
15.1 引用和指针的区别
- 指针有自己的空间,而引用只是别名
- 指针有自己的大小,而引用是引用对象的大小
- 指针可以初始化大小为
NULL
,而引用声明以后必须初始化一个对象 - 可以有
const
指针,但没有const
引用 - 指针使用中可以指向其他对象,而引用只能是一个对象的引用不能改变
- 存在多级指针,不存在多级引用
- 指针和引用的自增操作不一样
- 如果返回动态内存分配的对象或者内存,必须使用指针,不能使用引用,否则造成内存泄漏
- 作为参数传递时,指针需要解引用才会对对象操作,引用直接就可以对对象操作
15.2 右值引用
右值引用是c++11引入的新特性,实现了语义转移和精确传递,主要目的:(1)消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;(2)能够更简洁明确地定义泛型函数
左值引用和右值引用的区别
- 左值可以寻址,右值不可以
- 左值可以被赋值,右值不可以,但可以给左值赋值
- 对于基础类型,左值可变,右值不可变
16. i++和++i
i++是先返回i的值再自增,++i是先自增再返回值
两者都不是原子操作,本质都是先从内存取值到寄存器,自增,然后写回内存
17. const
- 修饰普通变量,变量的值将不可变,对于局部变量是可以通过
(int*)(&a)
转成指针后修改值,但是访问变量a
时,依然是原值,如果想要获取真实值,就需要加上volatile
- 修饰指针顶层
const
int * const a
防止指针被篡改和底层const
const int* a
防止值被篡改 - 修饰函数参数,如果是值传递,那么使用
const
没有什么意义,只是该参数在函数内不可变;如果是指针传递,只进行浅拷贝,拷贝一份指针给函数,因此指针加上顶层const
防止指针被篡改和底层const
防止值被篡改;对于引用,是别名所以可以加上底层const
防止值被篡改 - 修饰函数返回值,对于返回值为普通类型,如果时传值则
const
没有意义,如果是指针我们可以把函数看做变量,修饰含义与修饰普通指针变量无异;对返回值为对象,不建议用const
修饰 - 修饰类成员:修饰成员变量,只能初始化,不可修改;修饰成员函数,
const
关键字写在函数最后面,防止修改类对象的内容;const
修饰对象/对象指针/对象引用,不可调用非const
成员函数
18.内存对齐
内存对齐原则:
确定对齐单位
- 若设置
#pragram pack(n)
,且n位于类成员变量中占内存最大最小的基本数据结构之间,则取n
为对齐单位 - 否则,类以基本数据类型组成,则取类成员变量中所占内存最大的基本数据结构,为对齐单位
- 若类中有自定义的数据类型,则以展开后的所占内存最大的基本数据类型为对齐单位
填充内存
所占内存小于对齐单位的成员变量,扩充内存到对齐单位,若连续的成员变量所占内存之和都不超过对齐单位,则这几个成员变量共享一个对齐单位
19.构造函数和析构函数与虚函数
构造函数不能是虚函数,从多态角度讲,虚函数主要实现多态,在运行时才可以明确调用对象,在调用构造函数的时候还不能确定对象的真实类型,并且构造函数的作用是提供初始化,在对象的生命周期内仅仅运行一次,不是对象的动态行为,没有必要称为虚函数。
析构函数在有类继承的时候必须是虚函数否则容易造成内存泄漏,如果父类指针指向子类对象的话,如果基类析构函数不是虚函数那么delete的时候只释放基类,不释放子类,因为析构函数不是虚函数的话,直接按指针类型调用该类型的析构函数代码,因为指针类型是基类,所以直接调用基类析构函数代码。
20. 拷贝构造函数
如果不是自己手写的拷贝构造函数会有一个默认的,当你用一个本类对象去初始化另一个本类对象的时候调用,一般在三种情况下使用1. 2. 3.
iota函数,iota(a,b,c)三个参数,前两个a,b是迭代器,c是起始值
21. 解决哈希冲突的办法
-
开放地址方法(再散列法)
所有的地址都对所有的数值开放,而不是链式地址法的封闭方式,一个数值固定在一个索引地址位置
(1) 线性探测
(2) 再平方测试
(3) 伪随机探测
-
链式地址法
-
建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据
-
再哈希法
一直哈希,直到没有冲突为止
22.各数据类型占的内存
char | 1字节 |
---|---|
short | 2字节 |
int | 16位系统2字节;32、64位系统4字节 |
long | 16、32位系统4字节;63位系统8字节 |
float | 4字节 |
double | 8字节 |
long long | 8字节 |
double | 16位系统8字节;32位系统12字节(有效位10位,为了对齐分配12字节);64位系统16字节(有效位10位,为了对齐分配16字节) |
指针 | 16位系统2字节;32位系统4字节;64位系统8字节 |
23. define和inline关键字
define:定义预编译时处理的宏,只是简单的字符串替换,无类型检查
inline:内联函数只是给编译器提出建议,是否进行宏替换,编译器有权拒绝;内敛还是在编译器最终生成的代码中是没有定义的这个函数是不存在的,没有普通函数调用时的额外开销,是一种特殊的函数具有普通函数的特性。
内联编译限制:
- 能不存在循环和过多的条件判断语句
- 函数体不能过于庞大
- 不能对内联函数进行取地址操作
- 内联函数声明必须在调用语句之前
24. cookie和session
-
cookie
由于http是无状态的,所以第一次登录后服务器,服务器给客户端分配一个唯一的cookie,当第二次再访问服务器的时候附带上cookie信息,服务器就知道当前的用户是哪个,cookie在分配的时候就指定一个值,这个值就是生命周期,过了生命周期就被清除,当这个值被置为0或者负数的时候表示浏览器被关闭就清除
-
session
用户打开浏览器,访问服务器多个web资源,然后关闭浏览器,这整个过程是个会话。当用户请求服务器的web时,如果没有session就创建一个,当用户再这个服务器的web上互相跳转的时候session不会丢失,服务器会给一个session分配一个session_id字段,创建后将这个字段加入到cookies中,随后每次传送cookie时就包含了session_id字段,从而能找到用户对应的session对象是哪个
cookie和session的区别
- cookie存储在浏览器或者本地,session存储在服务器
- cookie是string类型,session可以是任意的java对象
- session比cookie安全
- session占用服务器性能,如果过多的session,会增加服务器压力
- cookie有大小和数量限制,大小不超过4k,对于一个服务器一般最多20个cookie;session没有大小和数量的限制,取决于服务器的内存大小