c++面试知识整理

C++基础部分
1. 基础知识
1.1 内存
1.1.0 内存四区
1.1.1 简述C、C++程序编译的内存分配情况
1.1.2 分配函数与释放函数
1.2 预编译
1.2.1 头文件< > < ><>和“ ““ ” ””的问题
1.2.2 c o n s t constconst 与 #d e f i n e definedefine 相比有什么优点
1.3 宏,内联函数
1.3.1 内联函数
1.3.2 内联函数与宏的差别
1.3.3 写一个 “标准”宏m i n minmin
1.3.4 t y p e d e f typedeftypedef 和d e f i n e definedefine 有什么区别
1.4 指针
1.4.1 指针常量和常量指针
1.4.2 指针函数和函数指针
1.4.3 指针数组数组指针
1.4.4 函数传参
1.4.5 一些定义
1.4.6 指针与引用的区别
1.4.7 t h i s thisthis指针
1.4.8 指针和句柄
1.4.9 如何避免“野指针”
1.4.10 空指针与迷途指针区别
1.5 c o n s t constconst
1.5.1 c o n s t constconst 使用
1.5.2 c o n s t constconst 作用
1.5.3 如何修改c o n s t constconst成员函数
1.5.4 将c o n s t constconst类型转化为非c o n s t constconst类型
1.6 s i z e o f sizeofsizeof
1.6.1 s i z e o f sizeofsizeof 和s t r l e n strlenstrlen 的区别
1.6.2 s i z e o f sizeofsizeof 的使用场合
1.7 强制类型转换运算符
1.8 什么是右值引用,跟左值又有什么区别?
1.9 变量的声明和定义有什么区别
1.10 说一说e x t e r n “ C ” extern“C”extern“C”
2. C++面向对象
2.1 面对对象的三大特性(基本特征)
2.2 封装
2.3 继承
2.4 多态
2.5 成员函数
2.5.1 构造函数与析构函数
2.5.2 初始化列表方式
2.5.3 构造函数调用方式
2.5.4 C++的空类默认产生哪些成员函数
2.5.5 继承中子类和父类的构造和析构顺序
2.5.6 深拷贝与浅拷贝
2.5.7 拷贝构造函数与赋值运算符
2.5.8 C 语言的关键字 s t a t i c staticstatic 和 C++ 的关键字 s t a t i c staticstatic 有什么区别
2.5.9 静态成员
2.5.10 常函数与常对象
2.6 引用
2.6.1 引用
2.6.2 常引用
2.6.3 引用与指针区别
2.7 虚函数
2.7.1 虚函数与纯虚函数
2.7.2 抽象类
2.7.3 虚析构与纯虚析构
2.7.4 多态类中的虚函数表是c o m p i l e − T i m e compile-Timecompile−Time建立的还是R u n − T i m e Run-TimeRun−Time建立的
2.7.5 析构函数可为v i r t u a l virtualvirtual型,构造函数不能,为什么?
2.7.6 能否把每个函数都声明为虚函数?
2.8 隐藏、重载与重写
2.9 c l a s s classclass 与s t r u c t structstruct 区别
2.10 友元
3. S T L STLSTL
3.1 v e c t o r vectorvector的底层原理
3.2 v e c t o r vectorvector中的r e s e r v e reservereserve和r e s i z e resizeresize的区别
3.3 v e c t o r vectorvector中的s i z e sizesize和c a p a c i t y capacitycapacity的区别
3.4 v e c t o r vectorvector中e r a s e eraseerase方法与a l g o r i t h m algorithmalgorithm中的r e m o v e removeremove方法区别
3.5 v e c t o r vectorvector迭代器失效的情况
3.6 正确释放v e c t o r vectorvector的内存( c l e a r ( ) , s w a p ( ) , (clear(), swap(),(clear(),swap(), s h r i n k shrinkshrink_ t o toto_f i t ( ) ) fit())fit())
3.7 l i s t listlist的底层原理
3.8 什么情况下用v e c t o r vectorvector,什么情况下用l i s t listlist,什么情况下用d e q u e dequedeque
3.9 p r i o r i t y q u e u e priority_queuepriority
q

ueue的底层原理
3.10 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimapmap、set、multiset、multimap的底层原理
3.11 为何m a p mapmap和s e t setset的插入删除效率比其他序列容器高
3.12 为何m a p mapmap和s e t setset每次I n s e r t InsertInsert之后,以前保存的i t e r a t o r iteratoriterator不会失效?
3.13 当数据元素增多时(从10000 1000010000到20000 2000020000),map的set的查找速度会怎样变化?
3.14 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimapmap、set、multiset、multimap的特点
3.15 为何m a p mapmap和s e t setset的插入删除效率比其他序列容器高,而且每次i n s e r t insertinsert之后,以前保存的i t e r a t o r iteratoriterator不会失效?
3.16 为何m a p mapmap和s e t setset不能像v e c t o r vectorvector一样有个r e s e r v e reservereserve函数来预分配数据?
3.17 s e t setset的底层实现实现为什么不用哈希表而使用红黑树?
3.18h a s h hashhash _m a p mapmap与m a p mapmap的区别?什么时候用h a s h hashhash _m a p mapmap,什么时候用m a p mapmap?
3.19 迭代器失效的问题
3.20 S T L STLSTL线程不安全的情况
4. 补充
4.1 N U L L 与 n u l l p t r NULL与nullptrNULL与nullptr
4.1.1 C语言中的N U L L NULLNULL
4.1.2 C++中的N U L L NULLNULL
4.1.3 C++中的n u l l p t r nullptrnullptr
4.2 智能指针
1. 基础知识
1.1 内存
1.1.0 内存四区
意义在于:赋予其不同的生命周期,给编程带来更大的灵活性

运行前
代码区:存放函数体的二进制代码,由操作系统管理
共享的
只读的:防止程序意外修改其指令
全局区:存放全局变量和静态变量以及常量,结束后由系统释放
全局区还包括常量区(字符串常量,const修饰的全局常量)
运行后
栈区:由编译器自动分配和释放,存放函数体的参数值、局部变量等
不能返回局部变量的地址,当离开作用域后,开辟在栈区的局部变量会被编译器自动回收
堆区:由程序员分配和释放,若不释放,程序结束后由操作系统释放
分全局堆和局部堆
全局堆就是所有没有分配的空间,局部堆就是用户分配的空间
堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆

1.1.1 简述C、C++程序编译的内存分配情况
从静态存储区域分配:
内存在程序 编译 时 就已 经 分配 好,这块内 存在 程序 的整 个运行 期间 都存在。速度快不容易出错,因为有系统会善后。例如全局变量, static 变量, 常量字符串等。
在栈上分配:
在执行函数时, 函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。 栈内存分配运算内置于处理器的指令集中, 效率很高, 但是 分配的内存容量有限 。大小为2M。
从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏 ,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块 。

1.1.2 分配函数与释放函数
C:malloc、calloc、realloc / free
C++:new / delete
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等

1.1.2.1 malloc / free

malloc函数向内存申请一块连续可用的空间
开辟成功则返回一个指向该空间的void* 型指针,所以需要对其进行强制类型转换,转换成我们想要的类型
开辟失败则返回 NULL,所以一定要对malloc的返回值进行检查
free 用来释放动态开辟的内存,而不是释放指针

int* ptr = NULL;
ptr = (int*)malloc(1000*sizeof(int));//开辟一千个int大小的内存,并强制类型转换
if(NULL == ptr){
exit(1);
}
free(ptr);
ptr = NULL;

1
2
3
4
5
6
7
8
释放只能一次,如果释放两次及两次以上会出现错误
释放空指针例外,释放空指针其实也等于什么都没做,所以释放空指针释放多少次都没有问题

1.1.2.2 new / delete
new分配内存步骤

调用operator new 函数
调用相应的构造函数构造对象,并传入初值
对象构造完成后,返回一个指向该对象的指针
delete释放内存步骤

调用对象的析构函数
调用operator delete 函数释放内存空间

//开辟变量
int* a = new int(10);
delete a;

//开辟数组
int* arr = new int[10];
delete[] arr;

1
2
3
4
5
6
7
8
1.1.2.3 new/delete 与 malloc/free 区别
开辟位置
严格来说,malloc动态开辟的内存在堆区,new开辟的叫做自用存储区
若不重载new操作符,c++编译器一般默认使用堆来实现自用存储,此时等价于堆区
特别:new可以不为对象分配内存
重载
new、delete是操作符,可以重载,只能在C++ 中使用。 malloc、free 是函数,可以覆盖,C、C++ 中都可以使用。
是否调用构造与析构函数
new 可以调用对象的构造函数,对应的delete 调用相应的析构函数。malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
是否需要指定内存大小
malloc 需要显式指出开辟内存的大小,new 无需指定,编译器会自动计算
返回值类型
new返回的是某种数据类型指针,malloc返回的是void 指针,new比malloc更安全
new内存分配失败时,会抛出bac_alloc异常,不会返回NULL;malloc开辟内存失败会返回NULL指针,所以需要判断
1.1.2.4 calloc 、realloc
calloc(number,size):为number个大小为size的元素开辟一块空间,并把每个字节初始化为0
realloc(内存地址,大小):用于调整申请的空间大小

1.1.2.5 在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?
不能,malloc/free主要为了兼容C,new和 delete 完全可以取代malloc/free的。malloc/free 的操作对象都是必须明确大小的。而且不能用在动态类上。new 和 delete会自动进行类型检查和大小 ,malloc/free不能执行构造函数与析构函数 ,所 以动态对象它是不行的。当然从理论上说使用malloc 申请的内存是可以通过delete释放的 。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常

1.2 预编译
1.2.1 头文件< > < ><>和“ ““ ” ””的问题
#include < >:只搜索系统目录,不会搜索本地目录
#include " “:首先搜索本地目录,若找不到才会搜索系统目录
#include<>相较于#include” " 快一些

1.2.2 c o n s t constconst 与 #d e f i n e definedefine 相比有什么优点
const 常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行安全检查。对后者只进行字符替换,没有安全类型检查,并且在字符替换可能会产生意想不到的错误
有些集成化的调试工具可用对const进行调试,但是不能对宏常量进行调试

1.3 宏,内联函数
1.3.1 内联函数
定义:在函数定义体前加入关键字inline,使函数成为内联函数
增加空间消耗换取效率提高,这点与宏一样
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用

void fun(int x,int y);
inline void fun(int x,int y)//必须放在定义体前面,不能放在声明前面
{

}
1
2
3
4
5
适用情况
一个函数不断被重复调用
函数只有简单几行,且函数内不包括for、while、switch语句

1.3.2 内联函数与宏的差别
内联函数要做类型检查,而宏不需要
宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入到调用处,而减少了普通函数调用时的资源消耗

1.3.3 写一个 “标准”宏m i n minmin
#define min(a,b)((a)<=(b)?(a):(b))
1
特别注意括号

1.3.4 t y p e d e f typedeftypedef 和d e f i n e definedefine 有什么区别
用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用都是正确的。
对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
注意:typedef 定义是语句, 因为句尾要加上分号。 而define不是语句,千万不能在句尾加分号

1.4 指针
1.4.1 指针常量和常量指针
常量:const int p = a;
常量指针:const int* p = &a; 、 int const *p = &a;
指针常量:int * const p = &a;
常量指针常量:const int * const p = &a;

const象征内容,* 象征地址

指针常量定义必须初始化

谁在前就先读,谁就不许变

常量指针:const修饰的是指针,指针指向可以改变,但是指针指向的值不能改变

指针常量:修饰的是常量,指针指向不可改变,但是在指针指向的值可以改变

常量指针常量:指针指向和指针指向的值均不可改变

int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.4.2 指针函数和函数指针
指针函数:类型说明符 * 函数名(参数);
是一个函数,返回一个指针,实际上就是返回一个地址给调用函数
在调用指针函数时,需要一个同类型的指针来接收其函数的返回值。
也可以将其返回值设为void * 类型,调用时强制转换返回值为自己想要的类型
函数指针:类型说明符 (* 函数名)(参数) ;
int (*FunPointerName)(int a,int b);
是一个指针,指向函数的指针,包含了函数的地址,可以用它来调用函数,本质是一个指针变量,该指针指向这个函数
把函数地址赋值给函数指针
FunPoniterName = &FunctionName;
FunPoniterName = FunctionName;
调用函数指针
x = (*FunPointerName)(参数);
x = FunPointerName(参数);
#include <iostream>
using namespace std;
int add(int a,int b){
return a + b;
}
int main(){
int (*fun)(int a,int b);
fun = add;
cout << fun(10,20) << endl;
}

1
2
3
4
5
6
7
8
9
10
11
1.4.3 指针数组数组指针
指针数组:int *a[10];
是一个数组,a[ ]里面存的是地址
数组指针:int (*a)[10];
是一个指针,指向整个数组

1.4.4 函数传参
函数传参的三种方式:值传递,地址传递,引用传递

//值传递:就是函数调用时实参将数值传入给形参
//值传递时,如果形参发生,并不会影响实参
void swap01(int a,int b){
int temp = a;
a = b;
b = temp;
}
//地址传递:利用指针作函数参数,可以修改实参的值
void swap02(int* a,int* b){
int temp = *a;
*a = *b;
*b = temp;
}
//引用传递
//通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
void swap03(int& a;int& b){
int temp = a;
a = b;
b = temp;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.4.5 一些定义
定义 说明
int a 一个整型数
int *a 一个指向整型的指针
int **a 一个指向指针的指针,它指向的指针是一个整数类型
int a[10] 一个有10个整型的数组
int *a[10] 指针数组:一个有10个指针的数组,指针指向整型
int (*a)[10] 数组指针:一个指向有10个整型数数组的指针
int (*a)(int) 函数指针:一个指向函数的指针,该函数有一个整型参数,并返回一个整型
int (*a[10])(int) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型
1.4.6 指针与引用的区别
指针有自己的一块空间,而引用只是一个别名,所以不能创建引用的引用,引用必须初始化,而指针可用为空;
使用sizeof看一个指针的大小是4,而引用的大小则是被引用对象的大小;
指针和引用使用++运算符的意义不一样;引用自增自减时,是引用所代表的空间的值发生变化,而指针自增自减时是指针指向的位置发生变化
作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
可以有const指针,但是没有const引用;
指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
指针可以有多级指针(**p),而引用止于一级;
如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
引用本身不是一种数据类型,因此引用本身并不占存储单元,系统也不给引用分配存储单元,不能建立数组的引用

1.4.7 t h i s thisthis指针
this指针本质上是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数
this 只能在成员函数中使用,全局函数和静态函数(属于类,不属于对象)都不能使用this
this 在成员函数的开始前构造,在成员的结束后清除。调用类成员函数时,编译器将类的指针作为参数传递进去
用途:
当形参与成员变量同名时,可以用this指针来区分
在类的非静态成员函数中返回对象本身,可用return *this;
this 指针并不占用对象空间,所以成员函数的参数,不管是不是隐含的,都不会占用对象空间,只会占用参数传递时的栈空间,或者直接占用一个寄存器
this 会因编译器不同而有不同的存放位置,可能是堆、栈、也可能是寄存器
this 指针只有在成员函数中才有定义,不能通过对象使用this指针,无法知道一个对象的this指针位置(只有在成员函数里才有this指针的位置,可通过&this获取)

1.4.8 指针和句柄
句柄和指针其实是两个截然不同的概念,window系统用句柄标记系统资源,隐藏系统的信息,它一个一个32bit的整数
而指针则标记某个物理内存地址,两者概念不同

1.4.9 如何避免“野指针”
指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

1.4.10 空指针与迷途指针区别
当delete一个指针的时候,实际上仅仅是让编译器释放内存,但指针本身依然存在,此时他就是一个迷途指针
可令ptr = 0; 使迷途指针变为空指针

1.5 c o n s t constconst
任何不会修改数据成员的函数都应该声明为const 类型
在参数中使用const应该使用引用或指针,而不是一般的对象实例
除了重载操作符外一般不要将返回值类型定为对某个对象的const引用

1.5.1 c o n s t constconst 使用
const使用:定义常量、修饰函数参数、修饰函数返回值
const 修饰类的成员变量,表示成员变量,不能被修改
如果const构成函数重载,const对象只能调用const函数,非const对象优先调用非const函数
const 函数只能调用const函数,非const函数可以调用const函数
类体外定义的const成员函数,在定义和声明处都需要const修饰符

1.5.2 c o n s t constconst 作用
1.5.3 如何修改c o n s t constconst成员函数
用mutable修饰成员变量名后,就可以修改类成员变量

1.5.4 将c o n s t constconst类型转化为非c o n s t constconst类型
采用const_cast 进行转换。
用法:const_cast <type_id> (expression)

1.6 s i z e o f sizeofsizeof
数据对齐原则:是指数据所在的内存地址必须是该数据长度的整数倍。

1.6.1 s i z e o f sizeofsizeof 和s t r l e n strlenstrlen 的区别
sizeof是一个操作符,strlen是库函数。
sizeof的参数可以是数据的类型,也可以是变量、函数;而strlen只能用char*做参数且且以结尾为‘\0’的字符串。
编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
数组做sizeof的参数不退化,传递给strlen就退化为指针了
sizeof不能返回被动态分配的数组或外部的数组的尺寸
sizeof不能作用于函数类型,不完全类型或位字段,不完全类型是指具有未知存储大小数据的类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等

1.6.2 s i z e o f sizeofsizeof 的使用场合
其中一个主要用途就是与存储分配和I/O系统那样的例程通信
可以查看某种类型的对象在内存中所占的单元字节
在动态分配一个对象时,可以让系统知道要分配多少内存
便于一些类型的扩充。在window中有很多结构类型就有一个专用的字段来存放该类型的字节大小
如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小

1.7 强制类型转换运算符
static_cast

用于非多态类型的转换
不执行运行时类型检查(转换安全性不如 dynamic_cast)
通常用于转换数值数据类型(如 float -> int)
可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知
dynamic_cast

用于多态类型的转换,只能用于含有虚函数的类
执行行运行时类型检查
只适用于指针或引用
对不明确的指针的转换将失败(返回 nullptr),但不引发异常
可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast

用于将const变量转为非const
用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 ) reinterpret_cast
用于位的简单重新解释
滥用 reinterpret_cast 运算符可能很容易带来风险。除非所需转换本身是低级别的,否则应- 使用其他强制转换运算符之一。
允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
也允许将任何整数类型转换为任何指针类型以及反向转换。
reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
bad_cast

由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常

try {
Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}

1
2
3
4
5
6
7
为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

1.8 什么是右值引用,跟左值又有什么区别?
左值和右值的概 念 :

左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象; 区别:
左值能寻址,右值不能;
左值能赋值,右值不能;
左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)

1.9 变量的声明和定义有什么区别
变量的定义为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。 加入extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义

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

2. C++面向对象
对象是类的实例,类不分配存储空间,对象才分配存储空间,空类对象大小为1字节

2.1 面对对象的三大特性(基本特征)
2.2 封装
意义
将属性和行为作为一个整体表现事物
对属性和行为加以权限控制(公共、保护、私有)
增加代码的内聚性,进而提高可复用性和可维护性
封装手法
通过文件:对头文件的包含,把相关定义,声明等封装到某个头文件中
通过语法:C++的namespace、Java的package、Python的module等

2.3 继承
分类
继承(泛化)
可视继承
实现继承
组合(聚合)
接口继承
纯虚数

2.4 多态
静态多态(编译阶段,地址早绑定)
函数重载:包括普通函数的重载和成员函数的重载
函数模板的使用:通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。
动态多态(运行阶段,地址晚绑定)在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
派生类
虚函数

2.5 成员函数
2.5.1 构造函数与析构函数
构造函数(constructor):主要作用在于创建对象时为对象的成员属性赋值,由编译器自动调用,可以有参数,可以重载
析构函数(destructor):主要在于对象销毁前系统自动调用。执行一些清理工作,无参数,且不能重载

2.5.2 初始化列表方式
构造函数( ) : 属性1(值1),属性1(值1),...{ }

1
2
常量,引用和父类的构造函数必须在构造函数的初始化列表里初始化,或者将其设置为static

2.5.3 构造函数调用方式
括号法:Person p1(10); //无参构造不加括号;Person p1;
显式法:Person p2 = Person(10);Person p2 = Person(p1);
隐式转换法:Person p3 = 10;
Person(10)单独写为匿名对象,当前行结束后立刻析构

2.5.4 C++的空类默认产生哪些成员函数
缺省构造函数
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const 。

2.5.5 继承中子类和父类的构造和析构顺序
构造:先调用父类的构造函数,然后再调用子类构造函数
析构:先调用子类析构函数,然后再调用父类的析构函数

2.5.6 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作

2.5.7 拷贝构造函数与赋值运算符
拷贝构造函数和赋值运算符重载有以下两个不同之处:

拷贝构造函数生成新的类对象,而赋值运算符不能。
由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象 是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉。
注意:当有类中有指针类型的成员变量时 ,一 定要重写拷贝构造函数和赋值运算符,不要使用默认的 。

拷贝构造函数调用时机:

使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部对象

2.5.8 C 语言的关键字 s t a t i c staticstatic 和 C++ 的关键字 s t a t i c staticstatic 有什么区别
在 C 中static 用来修饰局部静态变量和外部静态变量、函数而C++中除了上述功能外, 还用来定义类的成员变量和函数。 即静态成员和静态成员函数 。
注意:编程时static 的记忆性和全局性的特点可以让在不同时期调用的函数进行通信,传递信息 ,而C++ 的静态成员则可以在多个对象实例间进行通信,传递信息

2.5.9 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员变量:所有对象共享同一份数据;在编译阶段分配内存,类内声明,类外初始化
静态成员函数:所有对象共享同一个函数;静态成员函数只能访问静态成员变量
非静态成员变量占用对象空间,静态成员变量、函数不占用对象空间

2.5.10 常函数与常对象
常函数
成员函数后加const 后,称这个函数为常函数
常函数内不可以修改成员属性
成员属性声明前加关键字mutable 后,在常函数中依然可用修改
常对象
声明对象前加const 称该对象为常对象
常对象只能调用常函数

2.6 引用
2.6.1 引用
引用是某个目标变量的别名:类型标识符 &引用名 = 目标变量;int &a = b;

2.6.2 常引用
const 类型标识符 &引用名 = 目标变量;const int &a = b;
可以提高程序的效率,保护传递给函数的数据不在函数中改变

2.6.3 引用与指针区别
指针有自己的一块空间,而引用只是一个别名,所以不能创建引用的引用,引用必须初始化,而指针可用为空;
使用sizeof看一个指针的大小是4,而引用的大小则是被引用对象的大小;
指针和引用使用++运算符的意义不一样;引用自增自减时,是引用所代表的空间的值发生变化,而指针自增自减时是指针指向的位置发生变化
作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
可以有const指针,但是没有const引用;
指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
指针可以有多级指针(**p),而引用止于一级;
如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
引用本身不是一种数据类型,因此引用本身并不占存储单元,系统也不给引用分配存储单元,不能建立数组的引用

2.7 虚函数
2.7.1 虚函数与纯虚函数
虚函数:
virtual 返回值类型 函数名(参数列表){ }
在基类中冠以virtual的成员函数,它提供了一个接口界面。允许在派生类中对基类的虚函数重新定义
使用虚函数有一定的空间开销,当类中有虚函数时,编译器会为该类构造一个虚函数表
虚函数表是一个指针数组用来存放每个虚函数的入口地址
纯虚函数
virtual 返回值类型 函数名(参数列表)= 0;
在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义
作为接口存在,纯虚函数不具备函数的功能,一般不能直接被调用。
从基类继承来的纯虚函数,在派生类仍然是虚函数。

2.7.2 抽象类
如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)
抽象类中不仅包括纯虚函数,也可包括虚函数。
抽象类必须用作派生其他类的基类。且不能直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。
特点
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类

2.7.3 虚析构与纯虚析构
多态使用时,若子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,此时将父类中的析构函数改为虚析构或者纯虚析构即可
总结
解决通过父类指针释放子类对象
子类中没有堆区数据,可不写为虚析构或纯虚析构
拥有纯虚析构的类也称为抽象类

2.7.4 多态类中的虚函数表是c o m p i l e − T i m e compile-Timecompile−Time建立的还是R u n − T i m e Run-TimeRun−Time建立的
虚拟函数表是在编译时期就建立了,各个虚函数这时候已经被组织成一个虚拟函数的入口地址的数组。而对象的隐藏成员----虚函数表指针是在运行期间,也就是构造函数被调用时进行初始化的,这是实现多态的关键

2.7.5 析构函数可为v i r t u a l virtualvirtual型,构造函数不能,为什么?
虚函数采用一种虚调用的方法,虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数,但是如果要创建一个对象,势必要知道对象的准确类型,因此构造函数不能为虚

2.7.6 能否把每个函数都声明为虚函数?
不能,虚函数是有代价的:由于每个虚函数的对象都必须维护一个v表,因此在使用虚函数的时候都会产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么根本没必要使用虚函数

2.8 隐藏、重载与重写
重写/覆盖(override):函数返回值类型,函数名,参数列表完全一致称为重写
重载(overload):同一作用域下函数名相同,参数类型、个数、顺序不同
函数的返回值不可以做重载的条件
(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一 定不同。
virtual 的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被 virtual
修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同 。

与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。 当参数不相同时,无论基类中的参数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。
注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同 ,达到的目的也是完全不同的,覆盖是动态态绑定的多态 ,而重载是静态绑定的多态 。

2.9 c l a s s classclass 与s t r u c t structstruct 区别
默认继承权限不同,class默认继承权限是private,struct默认继承权限是public
class 还可用于定义模板参数 ,像typename,但是关键字struct不能用于定义模板参数.

2.10 友元
友元是定义在类外部的普通函数
需要在类体内进行说明,需加上关键字friend
友元不是成员函数,但是它可以访问类中的私有成员
作用在于提高程序运行效率,但是破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员
友元可以是一个函数(全局函数、成员函数),被称为友元函数;可用是一个类,被称为友元类

3. S T L STLSTL
3.1 v e c t o r vectorvector的底层原理
vector底层是一个动态数组 ,包含三个迭代器, start和finish之间是已经被使用的空间范围 end _ of _ storage是整块连续空间包括备用空间的尾部 。
当空间不够装下数据( vec.push_back( val) )时,会自动申请另一片更大的空 间(1.5倍或者2倍) ,然后把原来的数据拷贝到新的内存空间,接着释放原来的 那片空间[vector内存增长机制] 。
当释放或者删除( vec.clear())里面的数据时,其存储空间不释放,仅仅是清 空了里面的数据。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector 的所有迭代器会都失效了 。

3.2 v e c t o r vectorvector中的r e s e r v e reservereserve和r e s i z e resizeresize的区别
reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。
resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。

3.3 v e c t o r vectorvector中的s i z e sizesize和c a p a c i t y capacitycapacity的区别
size表示当前vector中有多少个元素(finish - start);
capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

3.4 v e c t o r vectorvector中e r a s e eraseerase方法与a l g o r i t h m algorithmalgorithm中的r e m o v e removeremove方法区别
vector中erase方法真正删除了元素,迭代器不能访问了
remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

3.5 v e c t o r vectorvector迭代器失效的情况
当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。

3.6 正确释放v e c t o r vectorvector的内存( c l e a r ( ) , s w a p ( ) , (clear(), swap(),(clear(),swap(), s h r i n k shrinkshrink_ t o toto_f i t ( ) ) fit())fit())
vec.clear():清空内容,但是不释放内存。
vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

3.7 l i s t listlist的底层原理
list的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。
list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

3.8 什么情况下用v e c t o r vectorvector,什么情况下用l i s t listlist,什么情况下用d e q u e dequedeque
vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
需要从首尾两端进行插入或删除操作的时候需要选择deque。

3.9 p r i o r i t y q u e u e priority_queuepriority
q

ueue的底层原理
priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。

3.10 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimapmap、set、multiset、multimap的底层原理
map 、set 、multiset 、multimap的底层实现都是红黑树 ,epoll模型的底层数据结构也是红黑树 ,linux 系统中CFS 进程调度算法,也用到红黑树 。
红黑树的特性:
每个结点或是红色或是黑色;
根结点是黑色;
每个叶结点是黑的;
如果一个结点是红的,则它的两个儿子均是黑色;
每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

3.11 为何m a p mapmap和s e t setset的插入删除效率比其他序列容器高
因为不需要内存拷贝和内存移动

3.12 为何m a p mapmap和s e t setset每次I n s e r t InsertInsert之后,以前保存的i t e r a t o r iteratoriterator不会失效?
因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

3.13 当数据元素增多时(从10000 1000010000到20000 2000020000),map的set的查找速度会怎样变化?
RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

3.14 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimapmap、set、multiset、multimap的特点
set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。
map和set的增删改查速度为都是logn,是比较高效的。

3.15 为何m a p mapmap和s e t setset的插入删除效率比其他序列容器高,而且每次i n s e r t insertinsert之后,以前保存的i t e r a t o r iteratoriterator不会失效?
存储的是结点,不需要内存拷贝和内存移动。
插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

3.16 为何m a p mapmap和s e t setset不能像v e c t o r vectorvector一样有个r e s e r v e reservereserve函数来预分配数据?
在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。

3.17 s e t setset的底层实现实现为什么不用哈希表而使用红黑树?
set中元素是经过排序的,红黑树也是有序的,哈希是无序的

3.18h a s h hashhash _m a p mapmap与m a p mapmap的区别?什么时候用h a s h hashhash _m a p mapmap,什么时候用m a p mapmap?
构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。
存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。
总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
如果考虑效率,特别当元素达到一定数量级时,用hash_map。
考虑内存,或者元素数量较少时,用map。

3.19 迭代器失效的问题
插入操作:

对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
对于deque,如果插入点位于除front和back的其它位置iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
对于list和forward_list,所有的iterator,pointer和refercnce有效。 删除操作:
对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;
对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;
对于list和forward_list,所有的iterator,pointer和refercnce有效。
对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

3.20 S T L STLSTL线程不安全的情况
在对同一个容器进行多线程的读写、写操作时;
在每次调用容器的成员函数期间都要锁定该容器;
在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
在每个在容器上调用的算法执行期间锁定该容器。

4. 补充
4.1 N U L L 与 n u l l p t r NULL与nullptrNULL与nullptr
4.1.1 C语言中的N U L L NULLNULL
C语言中的NULL通常被定义为:#define NULL ((void *)0)
NULL实际上是一个空指针,C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。

4.1.2 C++中的N U L L NULLNULL
C++是强类型语言,void*是不能隐式转换成其他类型的指针的,所以实际上编译器提供的头文件做了相应的处理:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

1
2
3
4
5
6
实际上,用NULL代替0表示空指针在函数重载时会出现问题;

void func(void* i);//func(nullptr)
void func(int i);//func(NULL)
1
2
不知道调用哪个函数

4.1.3 C++中的n u l l p t r nullptrnullptr
nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
nullptr的l另一种实现方式如下:

#include <iostream>
using namespace std;

void func(void* i)
{
cout << "func1" << endl;
}

void func(int i)
{
cout << "func2" << endl;
}

void main(int argc,char* argv[])
{
func(NULL);
func(nullptr);
getchar();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4.2 智能指针
4.2 智能指针
C+ + 里面的四个智能指针:

auto_ptr(C++11已弃用)
shared_ptr,
weak_ptr,
unique_ptr
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束
忘记释放,造成内存泄漏 。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间不 需要手动释放内存空间 。
————————————————
原文链接:https://blog.csdn.net/weixin_44478077/article/details/117868713

 

posted @ 2023-06-23 17:15  imxiangzi  阅读(314)  评论(0编辑  收藏  举报