网络知识 娱乐 C++面试题总结,一篇就够了

C++面试题总结,一篇就够了

C++面试题汇总

  • 1. C基础
    • 1.1 内存模型
      • 1.1.0 内存四区
      • 1.1.1 简述C、C++程序编译的内存分配情况
      • 1.1.2 分配函数与释放函数
        • 1.1.2.1 malloc / free
        • 1.1.2.2 new / delete
        • 1.1.2.3 new/delete 与 malloc/free 区别
        • 1.1.2.5 calloc 、realloc
        • 1.1.2.6 在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?
    • 1.2 预编译
      • 1.2.1 头文件问题
      • 1.2.2 const 与 #define相比有什么优点
    • 1.3 宏,内联函数
      • 1.3.1 内联函数
      • 1.3.2 内联函数与宏的差别
      • 1.3.3 写一个 “标准”宏MIN
      • 1.3.4 typedef 和define 有什么区别
    • 1.4 指针
      • 1.4.1 指针常量和常量指针
      • 1.4.2 指针函数和函数指针
      • 1.4.3 指针数组数组指针
      • 1.4.4 函数传参
      • 1.4.5 一些定义
      • 1.4.6 指针与引用的区别
      • 1.4.7 this指针
      • 1.4.8 指针和句柄
      • 1.4.9 如何避免“野指针”
      • 1.4.10 空指针与迷途指针区别
    • 1.5 const
      • 1.5.1 const 使用
      • 1.5.2 const 作用
      • 1.5.3 如何修改const成员函数
      • 1.5.4 将Const类型转化为非Const类型
    • 1.6 sizeof
      • 1.6.1 sizeof 和strlen 的区别
      • 1.6.2 sizeof 的使用场合
    • 1.7 强制类型转换运算符
    • 1.8 什么是右值引用,跟左值又有什么区别?
      • 1.9 变量的声明和定义有什么区别
      • 1.10 说一说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.6 拷贝构造函数与赋值运算符
      • 2.5.7 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
      • 2.5.8 静态成员
      • 2.5.9 常函数与常对象
    • 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 多态类中的虚函数表是compile-Time建立的还是Run-Time建立的
      • 2.7.5 析构函数可为virtual型,构造函数不能,为什么?
      • 2.7.6 能否把每个函数都声明为虚函数?
    • 2.8 隐藏、重载与重写
    • 2.9 class 与struct 区别
    • 2.10 友元
  • 3. STL
    • 3.1 vector的底层原理
    • 3.2 vector中的reserve和resize的区别
    • 3.3 vector中的size和capacity的区别
    • 3.4 vector中erase方法与algorithn中的remove方法区别
    • 3.5 vector迭代器失效的情况
    • 3.6 正确释放vector的内存(clear(), swap(), shrink_to_fit())
    • 3.7 list的底层原理
    • 3.8 什么情况下用vector,什么情况下用list,什么情况下用deque
    • 3.9 priority_queue的底层原理
    • 3.10 map 、set、multiset、multimap的底层原理
    • 3.11 为何map和set的插入删除效率比其他序列容器高
    • 3.12 为何map和set每次Insert之后,以前保存的iterator不会失效?
    • 3.13 当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?
    • 3.14 map 、set、multiset、multimap的特点
    • 3.15 为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?
    • 3.16 为何map和set不能像vector一样有个reserve函数来预分配数据?
    • 3.17 set的底层实现实现为什么不用哈希表而使用红黑树?
    • 3.18 hash_map与map的区别?什么时候用hash_map,什么时候用map?
    • 3.19 迭代器失效的问题
    • 3.20 STL线程不安全的情况
  • 4. C++ 11
    • 4.1 NULL与nullptr
      • 4.1.1 C语言中的NULL
      • 4.1.2 C++中的NULL
      • 4.1.3 C++中的nullptr
    • 4.2 智能指针

C++知识点总结: 快速跳转

1. C基础

1.1 内存模型

1.1.0 内存四区

意义在于:赋予其不同的生命周期,给编程带来更大的灵活性

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

1.1.1 简述C、C++程序编译的内存分配情况

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

一个C、C++ 程序****编译时内存分为5大存储区:堆区、栈区、全局区、文字常量区、程序代码区

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.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.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.5 calloc 、realloc

calloc(number,size):为number个大小为size的元素开辟一块空间,并把每个字节初始化为0
realloc(内存地址,大小):用于调整申请的空间大小

1.1.2.6 在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 const 与 #define相比有什么优点

  • const 常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行安全检查。对后者只进行字符替换,没有安全类型检查,并且在字符替换可能会产生意想不到的错误
  • 有些集成化的调试工具可用对const进行调试,但是不能对宏常量进行调试

1.3 宏,内联函数

1.3.1 内联函数

  • 定义:在函数定义体前加入关键字inline,使函数成为内联函数
  • 增加空间消耗换取效率提高,这点与宏一样
  • 内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用
void fun(int x,int y);
inline void fun(int x,int y){//必须放在定义体前面,不能放在声明前面
	...
}
  • 适用情况
    • 一个函数不断被重复调用
    • 函数只有简单几行,且函数内不包括for、while、switch语句

1.3.2 内联函数与宏的差别

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

1.3.3 写一个 “标准”宏MIN

#define min( a, b)( (a )<=(b) ?( a) :(b ))

1.3.4 typedef 和define 有什么区别

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

1.4 指针

(常量指针,数组指针,函数指针,this指针,指针传值,指向指针的指针,指针与引用的区别)

1.4.1 指针常量和常量指针

指针常量和常量指针

1.4.2 指针函数和函数指针

指针函数和函数指针

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.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 this指针

  • this指针本质上是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数
  • this 只能在成员函数中使用,全局函数和静态函数(属于类,不属于对象)都不能使用this
  • this 在成员函数的开始前构造,在成员的结束后清除。调用类成员函数时,编译器将类的指针作为参数传递进去
  • 用途:
    • 当形参与成员变量同名时,可以用this指针来区分
    • 在类的非静态成员函数中返回对象本身,可用return *this;
class A{
	public:
	int func(int p){} //相当于 int func(A* const this,int p)
};
A a;
a.func(10);//相当于 A::func(&a,10);
  • 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 const

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

1.5.1 const 使用

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

1.5.2 const 作用

作用说明
可以定义const常量
便于进行类型检查const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误
可以保护被修饰的东西防止意外的修改,增强程序的健壮性。
可以很方便地进行参数的调整和修改同宏定义一样,可以做到不变则已,一变都变
为函数重载提供了一个参考void f(int i) {…} //一个函数 void f(int i) const {…} //上一个函数的重载
可以节省空间,避免不必要的内存分配const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝
提高了效率编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高

参考链接:https://blog.csdn.net/moli152_/article/details/45100717

1.5.3 如何修改const成员函数

用mutable修饰成员变量名后,就可以修改类成员变量

1.5.4 将Const类型转化为非Const类型

采用const_cast 进行转换。
用法:const_cast (expression)

1.6 sizeof

数据对齐原则:是指数据所在的内存地址必须是该数据长度的整数倍。

1.6.1 sizeof 和strlen 的区别

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

1.6.2 sizeof 的使用场合

  • 其中一个主要用途就是与存储分配和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();
}

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

1.8 什么是右值引用,跟左值又有什么区别?

左值和右值的概 念 :

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

1.9 变量的声明和定义有什么区别

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

1.10 说一说extern“C”

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

2. C++面向对象

对象是类的实例,类不分配存储空间,对象才分配存储空间,空类对象大小为1字节

2.1 面对对象的三大特性(基本特征)

  • 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection (private , protected ,public )。
  • 继承性:广义的继承有三种实现形式:
    • 实现继承(使用基类的属性和方法而无需额外编码的能力)
    • 可视继承(子窗体使用父窗体的外观和实现代码)
    • 接口继承(仅使用属性和方法,实现滞后到子类实现)。
  • 多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

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)...{ }

常量必须在构造函数的初始化列表里初始化,或者将其设置为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.6 拷贝构造函数与赋值运算符

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

  • 拷贝构造函数生成新的类对象,而赋值运算符不能。
  • 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象 是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉。

注意:当有类中有指针类型的成员变量时 ,一 定要重写拷贝构造函数和赋值运算符,不要使用默认的 。

拷贝构造函数调用时机:

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

2.5.7 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别

  • 在 C 中static 用来修饰局部静态变量和外部静态变量、函数而C++中除了上述功能外, 还用来定义类的成员变量和函数。 即静态成员和静态成员函数 。
  • 注意:编程时static 的记忆性和全局性的特点可以让在不同时期调用的函数进行通信,传递信息 ,而C++ 的静态成员则可以在多个对象实例间进行通信,传递信息

2.5.8 静态成员

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

2.5.9 常函数与常对象

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

2.6 引用

2.6.1 引用

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

2.6.2 常引用

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

2.6.3 引用与指针区别

见 1.4.6

2.7 虚函数

2.7.1 虚函数与纯虚函数

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

2.7.2 抽象类

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

2.7.3 虚析构与纯虚析构

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

2.7.4 多态类中的虚函数表是compile-Time建立的还是Run-Time建立的

虚拟函数表是在编译时期就建立了,各个虚函数这时候已经被组织成一个虚拟函数的入口地址的数组。而对象的隐藏成员----虚函数表指针是在运行期间,也就是构造函数被调用时进行初始化的,这是实现多态的关键

2.7.5 析构函数可为virtual型,构造函数不能,为什么?

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

2.7.6 能否把每个函数都声明为虚函数?

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

2.8 隐藏、重载与重写

在这里插入图片描述

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

(2)隐藏和重写、重载有以下几点不同 。

  • 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
  • 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。 当参数不相同时,无论基类中的参数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。

注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同 ,达到的目的也是完全不同的,覆盖是动态态绑定的多态 ,而重载是静态绑定的多态 。
图片参考链接:https://blog.csdn.net/qq_37934101/article/details/81365449

2.9 class 与struct 区别

  • 默认继承权限不同,class默认继承权限是private,struct默认继承权限是public
  • class 还可用于定义模板参数 ,像typename,但是关键字struct不能用于定义模板参数.

2.10 友元

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

3. STL

STL知识点总结:快速跳转

3.1 vector的底层原理

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

3.2 vector中的reserve和resize的区别

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

3.3 vector中的size和capacity的区别

  • size表示当前vector中有多少个元素(finish - start);
  • capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

3.4 vector中erase方法与algorithn中的remove方法区别

  • vector中erase方法真正删除了元素,迭代器不能访问了
  • remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

3.5 vector迭代器失效的情况

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

3.6 正确释放vector的内存(clear(), swap(), shrink_to_fit())

  • vec.clear():清空内容,但是不释放内存。
  • vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
  • vec.shrink_to_fit():请求容器降低其capacity和size匹配。
  • vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

3.7 list的底层原理

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

3.8 什么情况下用vector,什么情况下用list,什么情况下用deque

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

3.9 priority_queue的底层原理

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

3.10 map 、set、multiset、multimap的底层原理

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

3.11 为何map和set的插入删除效率比其他序列容器高

因为不需要内存拷贝和内存移动

3.12 为何map和set每次Insert之后,以前保存的iterator不会失效?

因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

3.13 当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?

RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

3.14 map 、set、multiset、multimap的特点

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

3.15 为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?

  • 存储的是结点,不需要内存拷贝和内存移动。
  • 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

3.16 为何map和set不能像vector一样有个reserve函数来预分配数据?

  • 在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map声明的时候从参数中传入的Alloc。

3.17 set的底层实现实现为什么不用哈希表而使用红黑树?

  • set中元素是经过排序的,红黑树也是有序的,哈希是无序的
  • 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(lgn)

3.18 hash_map与map的区别?什么时候用hash_map,什么时候用map?

  • 构造函数: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 STL线程不安全的情况

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

4. C++ 11

4.1 NULL与nullptr

4.1.1 C语言中的NULL

  • C语言中的NULL通常被定义为:#define NULL ((void *)0)
  • NULL实际上是一个空指针,C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。

4.1.2 C++中的NULL

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

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

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

void func(void* i);//func(nullptr)
void func(int i);//func(NULL)

4.1.3 C++中的nullptr

  • nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
  • nullptr的l另一种实现方式如下:
#include 
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();
}

4.2 智能指针

C+ + 里面的四个智能指针:

  • auto_ptr(C++11已弃用)
  • shared_ptr,
  • weak_ptr,
  • unique_ptr

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束
忘记释放,造成内存泄漏 。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间不 需要手动释放内存空间 。