C++知识备忘及面试指导

一、C++基础

1. C和C++有什么区别?

C语言

  • C语言是纯面向过程的语言,不支持面向对象,函数名字不会被改编(函数不能重载)
  • C语言的结构体只支持成员变量,不支持成员函数,成员没有访问控制(即没有public protected private)

C++

  • C++是C语言的超集,同时支持面向过程和面向对象(类)
  • 函数名字编译后会被改编(支持多态),还包括标准模板库STL(容器、算法、迭代器)
  • 结构体成员默认访问属性为public,支持成员函数,类的成员默认访问属性是private

2.a和&a有什么区别?

a代表变量本身(值),&a表达式的结果为a在内存(地址空间)中的地址(而非物理地址)

3.static关键字有什么作用?

static关键在在C语言和C++中的作用不同。

相同点

  • 修饰全局变量时,全局变量只可在本文件内使用,不可跨文件使用,否则会链接错误
  • 修饰全局函数时,该函数只能在文件内使用,不可跨文件使用,否会会链接错误
  • 修饰局部变量时,只初始化一次
  • 保存在静态存储区域

区别(主要是面向对象部分)

  • 修饰成员函数时,该函数不可直接访问对象的属性,需要通过对象或对象指针访问属性
  • 修饰成员变量时,该变量属于类而不属于对象,该成员变量不计入对象的大小(sizeof时)

4.#define和const有什么区别?

#define

  • define为宏定义,在预处理阶段进行宏展开(gcc -E xx.c -o xx.i),编译器只进行字符串替换,不做类型安全性检查

  • 定义的常量没有类型,不做安全检查,是简单的替换
  • 宏本身不分配内存
  • 不受命名空间影响

const

  • 本质上是【只读变量】,编译阶段使用
  • const全局变量存储在只读数据段,编译器将其保存在符号表中,第一次使用时分配内存,不可以通过引用或指针修改
  • const局部变量存储在栈上,编译阶段将其常量值在使用处进行替换,可以通过引用或指针修改
  • const修饰形参时,表明函数内部不会对传入的实参进行修改
  • const修饰对象类型的引用形参时(形如 const CString& str)可以接收左值和右值实参
  • const修饰函数返回值时,表明该函数返回值不能被修改
  • const修饰函数时(放在函数声明和定义的最后),表明该函数不会修改任何成员变量
  • const成员函数只能调用const成员函数
  • 非const成员函数和const成员函数均可调用const成员函数
  • 受命名空间影响

5.静态链接和动态链接有什么区别?

静态链接

  • gcc main.c -o main.out -static -lxxx
  • 静态库在win下位.lib,在linux下位.a
  • 静态库位若干个目标文件(obj文件,即gcc -c xx.c -o xx.o 的输出)的集合,在链接时将这些目标文件的代码与主程序进行合并,运行时不再依赖目标机器上存在该库文件,但是主程序体积会变大

动态链接

  • gcc main.c -o main.out -lxxx
  • 动态库在win下位.dll(对应的导入库为.lib),在linux下位*.so
  • 动态链接在linker进行链接时需要找到目标库中的符号(函数、全局变量、类)存在,否则会链接失败
  • 动态链接不会合并代码到主程序中,生成的主程序文件体积小,但是需要目标机器上存在依赖的库文件

6.变量的声明和定义有什么区别?

声明

  • 声明只有一种形式,那就是 extern int a;
  • 声明不会占据内存空间,变量还不存在,也无法取地址

定义

  • 除了声明以外的形式都是定义,比如 int a; extern int a = 0;
  • 定义的时候变量会占据内存空间,变量已经存在,可以取地址

7.简述#ifdef #else #endif #ifndef的作用

这些都是条件编译指令,会在预处理阶段将代码去除不需要编译的代码,只保留符合条件的代码。

  • ifdef 如果定义了某宏

  • else 与#ifdef配合使用

  • endif #if 或 #ifdef 或 #ifndef的结束指令

  • ifndef 如果没有定义某宏

8.写出int bool float 指针变量与“零值”比较的if语句

int

if (0 == a)
if (0 != a)

bool

if (b)
if (!b)

float

const float EPSION = 0.00001F;
if (fabs(f) <= EPSION>)

指针

if (!p)
if (nullptr == p)
if (NULL == p)

9.结构体可以直接赋值吗?

可以,但是C++中的结构体使用.符号或:冒号符号按照声明顺序!
C语言中使用点或冒号赋值时【可以不】按照声明顺序。

struct Test
{
    int a;
    int b;
};

Test t1 = {100, 200}; // c++11,a == 100, b == 200
Test t2 = {.a = 100, .b = 200}; // C++中 顺序必须与结构体中声明顺序一致
Test t3 = {a : 100, b : 200}; // gcc 

10.sizeof和strlen的区别

sizeof

  • 取变量或类型占据的内存空间,单位是字节
  • 对一个数组计算sizeof时值为数组个数乘以每个元素大小
  • 对一个字符串指针计算sizeof时值为指针大小(32位平台为4Bytes)
  • 对一个数组形参计算sizeof时值为指针大小,因为数组会退化为指针

strlen

  • strlen是计算以\0结尾的字符串长度的,从头开始遇到\0结束,其大小不包括末尾的\0
  • 可以对字符指针(char*)或字符数组计算,单位是个

11.C语言的关键字static和C++的关键字static的区别

12.volatile有什么作用

  • 对于一个volatile关键字修饰的变量,不让编译器对齐进行读取优化,即每次都从内存里重新读取该变量的值,而不是从cpu的寄存器中读取

13.一个参数可以既是const又是volatile的吗?

可以。

  • const局部变量可以在运行时被通过指针或引用修改器值,但是编译器在编译期使用该常量值对使用的地方进行替换,因此任何使用这个const局部变量的时候都会表现为原本的常量值。
  • const局部变量声明为volatile后,运行时的值表现为使用指针或引用修改后的值。
  • const全部变量则不允许在运行时修改。
#include <iostream>

const int g_val = 100;

int main()
{
    const int val = 100;
    int& ref = const_cast<int&>(val);
    ref = 200;
    // output: ref == 200, val == 100
    std::cout << "ref = " << ref << ", val = " << val << std::endl;

    volatile const int val2 = 100;
    int& ref2 = const_cast<int&>(val2);
    ref2 = 200;
    // output: ref2 == 200, val2 == 200
    std::cout << "ref2 = " << ref2 << ", val2 = " << val2 << std::endl;

    int& ref3 = const_cast<int&>(g_val);
    ref3 = 200;
    // error , can NOT modify g_val via ref3
    std::cout << "ref3 = " << ref3 << ", g_val = " << g_val << std::endl;

    return 0;
}

14.全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?

操作系统和编译期通过其在内存中的不同位置来区分。

全局变量

  • 全局变量存储在全局存储区域,在进入main函数之前初始化
  • 多个全局变量的初始化次序不一定
  • 全局变量的生命周期为进程的声明周期

局部变量

  • 存储在栈中,声明周期为代码块,比如大括号或者函数
  • 当代码执行到局部变量的定义处时才分配内存空间并初始化

15.简述strcpy sprintf与memcpy的区别

  • strcpy是字符串拷贝,操作对象是字符串,单位是字符个数
  • sprintf是按照指定的格式对字符串进行格式化
  • memcpy是内存拷贝,操作对象是一段内存,单位是字节数

16.对于一个频繁使用的短小函数,应该使用什么来实现?有什么优缺点?

应该使用inline内联函数,编译器将inline内联函数内的代码替换到函数调用处。

优点

  • 省去函数调用的开销,执行效率高
  • 相比于宏定义,内敛函数在代码展开时编译期会进行语法检查或数据类型转换,更加安全

缺点

  • 代码体积更大
  • 如果内联函数本身的执行时间开销远超过调用开销,那么效率提升不明显,因为瓶颈不在调用开销上
  • 如果修改了内联函数代码,则所有用到的源代码文件都需要重新编译

17.什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?

shared_ptr

只能用裸指针或shared_ptr对象 或 std::make_shared 初始化

// (1)构造函数 用裸指针构造
std::shared_ptr<T> p1(new T(...)); 
// (2)拷贝构造
std::shared_ptr<T> p2(new T(...));
std::shared_ptr<T> p3 = p2; // case1
std::shared_ptr<T> p4(p3);  // case2
// (3)转移构造
std::shared_ptr<T> p5(std::move(p4)); // after operation p4 useless
// (4)operator =
std::shared_ptr<T> p6;;
p6 = p5;
// (5)转移operator = 
std::shared_ptr<T> p7;
p7 = std::move(p6); // after operation p6 useless
// (6)std::make_shared 构造
// 这种情况初始化时,T对象和引用计数内存为一块内存,比用构造函数构造减少一次内存申请
std::shared_ptr<T> p8 = std::make_shared<T>(...)
// (7)std::make_shared原型
template <typename T, typename ... Arg>
std::shared_ptr<T> make_shared(Arg&&... args)
{
    return std::shared_ptr<T>(new T(std::forward<Arg>(args)...));
}
// (8)对于new[10]的形式需要指定一个删除器
std::shared_ptr<int[]> sp(new int[10], [](int* p){delete[] p;})

unique_ptr

unique_ptr采用独享语义,在任何给定时刻,只能有一个指针管理内存。当指针超出作用域时,内存将自动释放,而且该类型的指针不可copy,只可以move。

// 构造函数 使用裸指针构造
std::unique_ptr<T> p1(new T(...))

// 无拷贝构造、 无operator=

// 转移构造
std::unique_ptr<T> p2(std::move(p1)); // after operation, p1 useless

// 转移operator=
std::unique_ptr<T> p3; // default constructor
p3 = std::move(p2);    // after operation, p2 useless

// 使用std::make_unique 构造
std::unique_ptr<T> p4 = std::make_unique<T>(...)

// std::make_unique原型
template <typename T, typename ... Arg>
std::unique_ptr<T> make_unique(Arg&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)...));
}

// 对于new[10]的形式,默认用delete[] 释放内存,无需指定

std::weak_ptr

std::weak_ptr 是一种智能指针,通常不单独使用,只能和 shared_ptr 类型指针搭配使用,可以视为 shared_ptr 指针的一种辅助工具。借助 weak_ptr 类型指针可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、通过expired()判断shared_ptr 指针指向的堆内存是否已经被释放等等,还可以解决shared_ptr 循环引用的问题。
————————————————
版权声明:本文为CSDN博主「物随心转」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_31608641/article/details/107702175/

// 用shared_ptr初始化
std::shared_ptr<T> p1 = std::make_shared<T>(...)
std::weak_ptr<T> wp1 = p1; // copy constructor
std::weak_ptr<T> wp2(p1);  // copy constructor

// move构造函数
std::weak_ptr<T> wp3(std::move(p1));
std::weak_ptr<T> wp4(std::move(wp3));

// operator =
std::weak_ptr<T> wp5;
wp5 = wp4;

// move operator =
std::weak_ptr<T> wp6;
wp6 = std::move(wp5);

// 检查共享的对象(shared_ptr)是否销毁
wp6.expired()

// 从weak_ptr获取shared_ptr
// 如果返回非空对象则会增加shared_ptr的引用计数
if (auto ptr = wp6.lock())
{
    ptr.doSomething();
}

std::auto_ptr

c++11起废弃,原因在于其支持拷贝构造和operator=,但是在这个两个操作之后资源的所有权会转移,而很多场景下是需要保留原指针对象的。

18.shard_ptr是如何实现的?

19.右值引用有什么作用?完美转发有什么作用?

为了实现move语义和完美转发。

20.悬挂指针与野指针有什么区别?

悬挂指针

野指针

char* p = new  char[100];
delete[] p;
// 此时p指向内存已经被释放,但p指针本身的值仍然存在,再次使用p的时候可能会引发访问违例

21.动态多态有什么作用?有哪些必要条件?

  • 多态即在编译器使用静态类型为基类对象引用或指针来访问在基类中定义的虚函数,而在运行期次对象引用或指针类型的动态对象为子类对象,此时实际执行的是子类中重写的虚函数
  • 多态是面向对象的三大特性之一(封装、继承、多态)
  • 必要条件:1、继承体系 2、基类定义虚函数 3、子类重写虚函数 4、静态类型使用基类对象的引用或指针来访问

22.请解析((void())()0)()的含义

23.C语言的指针和引用和C++的有什么区别?

指针

  • 定义时可以不需要初始化
  • 运行时可以改变指针的指向
  • 一般在使用时需要判空,避免空指针操作引发内存访问违例异常
  • 指针变量也是一个变量,编译器为其开辟存储空间
  • 可以有多级指针
  • 自加1运算的意义是指针向后移动一个sizeof所指向元素类型的位置

引用

  • 定义时必须初始化
  • 不可改变引用对象
  • 不需要对引用对象进行判空,除非是指针的引用(无意义)
  • 引用类型不占用存储空间
  • 只有一级引用
  • 自加1运算的意义是其绑定对象的值加1

24.typedef和define有什么区别

typedef

  • 可以定义类型别名,函数指针,数组类型,数字指针,与类型本身没有任何区别
  • 受命名空间影响
  • 在编译期处理

define

  • 简单的字符串替换
  • 不受命名空间影响
  • 在预处理阶段处理 gcc -E *.c -o main.i

25.指针常量与常量指针的区别

指针常量

不可以通过p改变被指向的内容,但是可以改变p的指向,即【底层const】

// 两者一致
char const *p;
const char *p;

常量指针

不可以改变p的指向,但是可以通过p改变被指向的内容,即【顶层const】

char* const p;

26.简述队列和栈的异同

队列

  • 先进先出FIFO
  • 队头队尾

  • 先进后出FILO
  • 只有一个出入口

27.设置地址为0x6789的整型变量的值为0xaa66

int* p = 0x6789;
*p = 0xaa66;

28.C语言的结构体和C++的有什么区别

C语言结构体

  • 不含函数成员,没有访问属性控制
  • 定义结构体对象时需要使用struct关键字
  • 使用点号或冒号对结构体字段赋值时无需按照字段声明顺序

C++中结构体

  • 可以包含函数成员,有访问属性,默认为public
  • 定义结构体对象时可以不用struct关键字
  • 使用点号或冒号对结构体字段赋值时需要按照字段声明顺序

29.简述指针常量和常量指针的区别

同25题。

30.如何避免野指针

  • 内存释放后将内存指针置空
  • 使用智能指针

31.句柄和指针的区别和联系是什么

句柄

  • windows内核对象句柄,句柄表的索引值,对应一个内核对象,每个进程都有一个句柄表,初始为空
  • 用户对象句柄,操作系统用来唯一确定一个资源的ID,全局唯一

指针

  • 一个对象或变量在内存中的地址,进程内唯一

32.说一说extern "C"

  • C++语言支持重载,即同一个函数名,可以有不同的参数类型、参数个数和参数顺序。
    编译器可以通过以上“不同”来对函数名字进行改编(name mangling)以进行区分。
  • extern "C" 即告诉编译器不要对函数名字进行这种改编,生成的函数名不包含参数信息,所以也就不支持重载。

33.对C++中的四个智能指针 shared_ptr unique_ptr weak_ptr auto_ptr的理解

34.C++的顶层const和底层const

int * const p = &i;   
// 其中p是顶层const,不允许改变p的指向(即不允许改变p的值)

const int *p = &i; 
// 其中p是底层const,不允许改变p指向的内存单元的值,即 *p = value 是错误的!!!

// 特别的:对const对象取地址的指针是一种底层const指针;
  • 推导auto类型时,等号右边的变量的顶层const属性通常被忽略, 底层const属性保留;
  • 推导auto引用类型时,等号右边的顶层const属性需要保留,底层const属性也需要保留;

35.拷贝初始化和直接初始化,初始化和赋值的区别

拷贝初始化

调用对象的拷贝构造函数,无返回值

直接初始化

调用对象的普通构造函数,无返回值

赋值构造函数

调用对象的重载operator=函数,返回对象本

初始化

  • 对象从无到有的过程

赋值

  • 对象先存在(普通构造或拷贝构造或operator=)
  • 然后再被赋值(赋值构造函数,如果没有operator=则进行浅拷贝)

36.C++的异常机制

37.为什么函数参数一般是const CObj& obj常引用类型?

  • const可以保证不会在函数内部修改传入的参数
  • 引用类型可以减少一次参数拷贝(对象类型会减少一次调用拷贝构造函数)
  • 而const引用可以接收右值

38.简述std::move语义和完美转发std::forward的作用

std::move

// remove_reference结构体模板
// 用来去除引用,只萃取原始类型
template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };
 
// 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };
 
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };


// move函数模板
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

// move函数的参数T&& t是一个万能引用(universal reference),可以接收左值和右值
// 传入的实参类型T可能为原始类型为Tp,则 T == Tp、引用类型T == Tp& 或 右值引用类型T == Tp&&
// 在传递参数的时候,根据引用折叠原则,实参t的类型结果就是Tp& or Tp&&
// remove_reference 去掉Tp& or Tp&& 的& or && 获取Tp类型
// move的作用就是一个强制类型转换,将传入的左值或右值都强制转换为右值Tp&&类型
//// move的结果是右值,而非右值引用!!!

CObj obj;
CObj&& ref = std::move(obj);
ref是左值(具名的右值引用是左值),std::move(obj)表达式的结果是右值。
  

std::forward

// std::forward函数模板 转发左值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

// std::forward函数模板 转发右值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
  static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
        " substituting _Tp is an lvalue reference type");
  return static_cast<_Tp&&>(__t);
}

// 在参数传递时给forward时,_Tp可能为左值原始类型T、左值引用类型T&、纯右值、右值引用类型T&&
// remove_reference去掉引用,获取原始类型T,即 T == type, 此时 __t 的类型为 T& or T&&
// 在函数内部使用static_cast进行强转为_Tp&&时,_Tp和&&发生引用折叠,

引用折叠规则
// (1) 实参为左值T t类型,       T    + T&& ==> T&
// (2) 实参为左值引用T& t类型,  T&    + T&& ==> T&
// (3) 实参右值T value类型,    value + T&& ==> T&&
// (4) 实参为右值引用T&& t类型, T&&   + T&& ==> T&&

因此,对应到std::forward函数中
// (1)当_Tp == T t     时,type == T, _Tp&&折叠为T& (折叠规则1)
// (2)当_Tp == T& t    时,type == T,_Tp&&折叠为T&   (折叠规则2)
// (3)当_Tp == T value 时,type == remove_reference(value) == T, _Tp&&折叠为T&& (折叠规则3)
// (4)当_Tp == T&& t   时,type == T,_Tp&&折叠为T&&   (折叠规则4)
  

引用折叠

  • 实参左值T + T&&参数 ==> 左值引用T&
  • 实参左值引用T& + T&&参数 ==> 左值引用T&
  • 实参右值T(0) + T&&参数 ==> 右值引用T&&
  • 实参右值引用T&& + T&&参数 ==> 右值引用T&&

二、C++面向对象

1.面向对象的三大特征是是哪些?

封装、继承、多态。

2.C++中类的访问权限

public protected private

3.多态的实现有哪几种?

动态多态

使用基类的引用或指针调用基类中的虚函数,从而运行时可以根据动态类型的虚函数表找到真正的函数。

静态多态

  • 函数重载
  • 函数模板,不同类有相同的函数,通过模板实例化不同对象实参
  • 第三种
template<class T>
class CUI
{
public:
	void OnPaint()
	{
		static_cast<T&>(*this).OnPaintImpl();
	}

	void OnPaintImpl()
	{
		std::cout<< "Paint" <<std::endl;
	}
};

class CNormalUI : public CUI<CNormalUI>
{
public:
	void OnPaintImpl()
	{
		std::cout<< "Normal Paint" <<std::endl;
	}
};

class CColorUI : public CUI<CColorUI>
{
public:
	void OnPaintImpl()
	{
		std::cout<< "Colorfullly Paint" <<std::endl;
	}
};

int main(int argc, char **argv)
{
	CUI<CNormalUI> ui1;
	ui1.OnPaint();

	CUI<CColorUI> ui2;
	ui2.OnPaint();
	
	return 0;
}

4.动态绑定是如何实现的?

参考:
https://blog.csdn.net/hiyajo_salieri/article/details/69362366
https://zhuanlan.zhihu.com/p/616522622?utm_id=0

每个带有虚函数的类都有一个vtpr虚函数表指针成员,这个对象的在内存中的放在首位,其存储的是值是虚函数表中第一个函数的地址。

单继承 子类不重写基类的虚函数

vtptr               ---------> table[0](vBasefunc1)
m_base_member1                 table[1](vBasefunc2)
m_base_member2                 ...     (vBasefuncN)
m_derived_member1              table[N+1](vDerivied::vfunc) // 派生类自己的虚函数写在最后
m_derived_member2

单继承 子类重写了基类虚函数

vtptr               ---------> table[0](vDerivedfunc1) // 派生类重写了Base::vfunc1
m_base_member1                 table[1](vBasefunc2)
m_base_member2                 ...     (vBasefuncN)
m_derived_member1              table[N+1](vDerivied::vfunc) // 派生类自己的虚函数写在最后
m_derived_member2 

多重继承 子类不重写基类虚函数

vtptr1               ---------> table[0](vBase1func1)
m_base1_member1                 table[1](vBase1func2)
m_base1_member2                  ...     (vBase1funcN)
                                table[N+1](vDerivied::vfunc) // 派生类自己的虚函数写在第一个基类的虚函数表最后
vtptr2               ---------> table[0](vBase2func1)
m_base2_member1                 table[1](vBase2func2)
m_base2_member2                 ...     (vBase2funcN)
m_derived_member1
m_derived_member2

多重继承 子类重写基类虚函数

vtptr1               ---------> table[0](vBase1func1)
m_base1_member1                 table[1](vDerivedfunc2)  // 派生类重写了Base1::vfunc2
m_base1_member2                  ...     (vBase1funcN)
                                table[N+1](vDerivied::vfunc) // 派生类自己的虚函数写在第一个基类的虚函数表最后
vtptr2               ---------> table[0](vDerivedfunc1)  // 派生类重写了Base2::vfunc1
m_base2_member1                 table[1](vBase2func2)
m_base2_member2                 ...     (vBase2funcN)

m_derived_member1
m_derived_member2

菱形继承

参考 https://blog.csdn.net/hiyajo_salieri/article/details/69362366

class Base
{
public:
    virtual void fun1()
    {
        cout << "Base::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::fun2()" << endl;
    }
private:
    int b;
};
class Base1 :virtual public Base  虚继承
{
public:
    virtual void fun1()          //重写Base的func1()
    {
        cout << "Base1::fun1()" << endl;
    }
    virtual void fun3()
    {
        cout << "Base1::fun3()" << endl;
    }
private:
    int b1;
};
class Base2 :virtual public Base  //虚继承
{
public:
    virtual void fun1()       //重写Base的func1()
    {
        cout << "Base2::fun1()" << endl;
    }
    virtual void fun4()
    {
        cout << "Base2::fun4()" << endl;
    }
private:
    int b2;
};
class Derive :public Base1, public Base2
{
public:
    virtual void fun1()          //重写Base1的func1()
    {
        cout << "Derive::fun1()" << endl;
    }
    virtual void fun5()
    {
        cout << "Derive::fun5()" << endl;
    }
private:
    int d;
}; 

5.动态多态有什么作用?有哪些必要条件?

6.纯虚函数有什么作用,如何实现?

纯虚函数声明一个类为抽象类,不可以实例化。

class CBase
{
public:
    virtual void func() = 0;
};

7.虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?

虚函数表是针对类的,?????

8.为什么基类的构造函数不能定义为虚函数?

因为一个对象还没构造完成之前是无法构造虚函数表的。

9.为什么基类的构造函数不能定义为虚函数?

因为一个对象还没构造完成之前是无法构造虚函数表的。

10.构造函数和析构函数能抛出异常吗?

可以抛出异常,但是不建议这样做。因为异常发生时很可能是内存申请失败或其他严重错误,不如直接让程序崩溃。

11.如何让一个类不能实例化?

定义一个删除了构造函数的基类,让其他类继承自它,它的派生类就无法实例化。
另外,如何禁止一个类不能复制?
答:定义一个删除了拷贝构造和move拷贝构造的基类,让其他类继承自它,它的派生类就无法复制。

12.多继承存在什么问题?如何消除多继承的二义性?

ref:https://blog.csdn.net/m0_63783532/article/details/124784321

二义性情况1:多继承的问题在于基类之间、或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性。

class A{
public:
    int m;
};
class B{
public:
    int m;
};
class C : public A, public B{
};
int main()
{
    C c;
    // c.m = 1; // error, 不能知道m是A::m还是B::m
    // 解决方法1:使用类限定名
    c.A::m = 1;
    c.B::m = 2; 
    return 0;
}

// 解决方法2
class C : public A, public B
{
public:
    int m;
};

int main()
{
    C c;
    c.m = 1; // ok
    return 0;
}
---

二义性情况2:菱形继承

#include <iostream>

class A{
public:
    int m;
};

class B : public A{};
class C : public A{};

class D : public B, public C{
public:
    int m;   
};

int main()
{
    D d;
    std::cout << "sizeof d = " << sizeof d << std::endl; // output:8

    //d.m = 1; // error,二义性
    //d.A::m = 1; // error, gcc:错误:‘A’是‘D’的有歧义的基类 

    // 解决方法1:添加类限定名
    d.B::m = 1;
    d.C::m = 2; 
    std::cout << "B::m = " << d.B::m << ", C::m = " << std::endl;

    return 0;
}

// 解决方法2: 在类D中添加同名成员覆盖掉基类B和C个字从基类A中集成的同名成员
class D : public B, public C
{
public:
    int m;
};

int main()
{
    D d;
    std::cout << "sizeof d = " << sizeof d << std::endl; // output: 12 

    // 基类的成员仍然存在
    d.B::m = 1;
    d.C::m = 2;
    d.m = 3;     // ok
    std::cout << "B::m = " << d.B::m << ", C::m = " << d.C::m <<  ", D::m" << d.m << std::endl;

    return 0;
}
// 解决方法3:使用虚继承,虚基类
#include <iostream>

class A{
public:
    int m;
};

class B : virtual public A{};
class C : virtual public A{};

class D : public B, public C{ 
};

int main()
{
    D d;
    std::cout << "sizeof d = " << sizeof d << std::endl; // gcc 4.8 output: 24, but why?

    d.m = 1;  
    std::cout << "B::m = " << d.B::m << ", C::m = " << d.C::m << ", D::m = " << d.m << std::endl;   
    d.A::m = 2;  
    std::cout << "B::m = " << d.B::m << ", C::m = " << d.C::m << ", D::m = " << d.m << std::endl;  
    d.B::m = 1;
    std::cout << "B::m = " << d.B::m << ", C::m = " << d.C::m << ", D::m = " << d.m << std::endl;  
    d.C::m = 2;
    std::cout << "B::m = " << d.B::m << ", C::m = " << d.C::m << ", D::m = " << d.m << std::endl;  
    return 0;
}

13.如果类A是一个空类,那么sizeof(A)是多少?

sizeof(空类) == 1

14.覆盖和重载之间有什么区别?

覆盖

函数名、函数参数个数、类型、顺序都相同(不包含返回值类型),此时是覆盖。

重载

C++通过对函数名进行改编达到重载的目的。
函数名相同,但是函数参数不同或(且)类型不同或(且)顺序不同。

15.拷贝构造函数和赋值运算符重载之间有什么区别?

拷贝构造

对象从无到有。

赋值运算符重载

对象先有再修改其值。

16.对虚函数和多态的理解

每个带有虚函数的类对象都会有一个虚函数指针,这个指针指向虚函数表的首项,运行时通过查找虚函数表来执行相应的代码。

17.请你来说一下C++中struct和class的区别

  • 成员默认访问属性不同:struct是public,class是private

18.说说强制类型转换运算符

static_cast

  • 基本数据类型转换
  • 隐式类型转换的显式化
  • 继承结构里的上转型,即派生类对象指针或引用转为基类指针或引用
  • void*转任意类型指针

dynamic_cast

  • 继承结构中下转型,即基类指针或引用转为派生类指针或引用(需要基类中有任意一个虚函数)

const_cast

  • 将底层const指针(const char* p)转为非const指针

    const int a = 0;  // 对a取地址是底层const,即const int* 
    int* p = const_cast<int*>(&a);  
    
  • 将非const引用绑定到去除const属性后的const变量

      const int a = 0;
      int& ref = const_cast<int&>(a);
    
  • 类的const成员函数里的this指针是底层const指针,可以使用该转换去除

      class A 
      {
      public:
          void func() const { //const成员函数
              // do something
              // Now [this] is [const A*]
              // remove const from this pointer            
              (const_cast<A*>(this))->m_val = 100; // ok
          }
    
          int m_val;
      };
    
      int main()
      {
          A a;
          a.func();
          std::cout << a.m_val << std::endl; //output 100
          return 0;
      }
    
    
  • 将非volatile引用绑定到volatile变量上

    int main()
    {
        volatile int b = 100;
        // int& c = b; // error
        volatile int& d = b; // ok
        int& d = const_cast<int&>(b); // ok        
        return 0;
    }

interpret_cast

  • 指针和整数之间的转换
  • 任意类型指针互转
  • 任意类型引用互转

19.简述类成员函数的重写、重载和隐藏的区别

  • 隐藏是继承结构中,派生类中有一个与基类同名的非virtual函数
  • 重载是有多个相同函数名和/或不同函数参数、不同参数顺序、不同参数类型的函数
  • 重写是在派生类中重新实现基类的virtual函数

20.类型转换分为哪几种? 各自有什么样的特点?

见 18题。

21.RTII是什么?其原理是什么?

资源获取即初始化。
依赖于对象创建时调用构造函数进行初始化,对象在离开生命周期时会自动调用析构函数。

22.简述default delete的用法

  • default 是告诉编译器生成默认的无参构造函数/析构函数/拷贝构造函数/operator=/move拷贝构造/move operator=
  • delete 是告诉编译器不要生成这个默认的构造函数/析构函数/拷贝构造/operaotr=/move拷贝构造/move operator

三、C++ STL

1.什么是C++ STL

标准模板库,包括一系列模板、算法、迭代器。

容器

  • 顺序容器:array vector deque deque list stack forward_list(since c++11 单向链表)
  • 关联容器:map set multiset multimap
  • 无序容器:unurdered_set unordered_multiset unordered_map unordered_multimap

算法

find find_if sort ...

迭代器

STL中的萃取技术:
迭代器类型萃取器

struct input_interator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag{};
struct bidirectioin_iterator_tag{};
struct random_access_iterator_tag{};

template <class Category, classs T, class Distance = std::ptrdiff_t, class Pointer = T*, calss Reference = T&>
struct iterator{
    typedef Category  iterator_category;
    typedef T         value_type;
    typedef Distance  difference_type;
    typedef Pointer   pointer;
    typedef Reference reference; 
};

// 迭代器萃取器
template <class Iterator>
struct iterator_traits{
    typedef Iterator::iterator_category iterator_category;
    typedef Iterator::value_type        value_type;
    typedef Iterator::difference_type   difference_type;
    typedef Iterator::pointer           pointer;
    typedef Iterator::reference         reference;
};

// 迭代器萃取器特化
template <class T>
struct iterator_traits<T*>{
    typedef random_access_iterator_tag iterator_category;
    typedef T                          value_type;
    typedef ptrdiff_t                  difference_type;
    typedef T*                         pointer;
    typedef T&                         reference;
};

// 迭代器萃取器特化
template <class T>
struct iterator_traits<const T*>{
    typedef random_access_iterator_tag iterator_category;
    typedef T                          value_type;
    typedef ptrdiff_t                  difference_type;
    typedef const T*                   pointer;
    typedef const T&                   reference;
};


// 萃取迭代器的category
template <calss Iterator>
inline typename iterator_traits<Iterator>::category
category(const Iterator&){
    typedef iterator_traits<Iterator>::iterator_category category;
    return category();
}


// 萃取迭代器的value_type,返回的是类型指针
template <class Iterator>
inline typename iterator_traits<Iterator>::value_type*
value_type(const Iterator&){
    return static_cast<typename iterator_traits<Iterator>::value_type*>(0);
}

// 萃取迭代器的difference_type,返回的是类型指针
template <calss Iterator>
inline typename iterator_traits<Iterator>::difference_type*
distance_type(const Iterator&){
    return static_cast<typename iterator::traits<Iterator>::difference_type>(0);
}

// 迭代器__distance函数实现1
template <class InputIterator>
inline iterator_traits<InputIterator>::difference_type
__distance(InputIterator first, InputIterator last, input_iterator_tag)
{
    iterator_traits<InputIterator>::difference_type n = 0;
    while (first != last)
    {
        ++first;
    }
    return n;
}

// 迭代器__distance函数实现2
template <class RandomIterator>
inline iterator_traits<RandomIterator>::difference_type
__distance(RandomIterator first, RandomIterator last, random_iterator_tag)
{
    return last - first;
}

// 迭代器的distance函数实现
template <class InputIterator>
inline iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last)
{
    return __distance(first, last, category(first)); 
}

// 迭代器的__advance函数实现1
template <class InputIterator, class Distance>
inline void __advance(InputIterator& i, Distance n, input_iterator_tag)
{
    while (n--) ++i;
}

// 迭代器的__advance函数实现2
template <class RandomIterator, class Distance>
inline void __advance(RandomIterator& i, Distance n, randome_iterator_tag)
{
    i += n;
}

// 迭代器的__advance函数实现2
template <class BiDirectoinIterator, class Distance>
inline void __advance(BiDirectionIterator& i, Distance n, bidirection_iterator_tag)
{
    if (n >= 0)
    {
        while (n--) ++i;
    }
    else 
    {
        while (n++) --i;
    }
}

// 迭代器的advance实现
template <class InputIterator, class Distance>
inline void advance(InputIterator& i, Distance n)
{
    __advance(i, n, category(i));    
}

// __type_traits的实现
struct __true_type{};
struct __false_type{};

// 默认成员都是__false_type的,安全起见尽量保守,其他简单类型通过枚举特化实现
template <class type>
struct __type_traits{
    typedef __true_type this_dummy_member_must_be_first;

    typedef __false_type has_trival_default_constructor;
    typedef __flase_type has_trival_copy_contructor;
    typedef __false_type has_trival_assignment_operator;
    typedef __false_type has_trival_destructor;
    typedef __false_type is_POD_type;
};


// 其他内置类型通过枚举特化实现
template<>
struct __type_traits<char>{
    typedef __true_type has_trival_default_constructor;
    typedef __true_type has_trival_copy_contructor;
    typedef __true_type has_trival_assignment_operator;
    typedef __true_type has_trival_destructor;
    typedef __true_type is_POD_type;
};
// 其他内置类型略

2.什么时候需要用hash_map,什么时候要用map?

  • map底层由红黑树实现,红黑树是一棵近似平衡二叉树,平均插入、删除、查找复杂度为O(logN),
  • hash_map底层由哈希表实现,查找时间复杂度为O(1),插入时间复杂度平均O(1),最差O(N),删除时间复杂度平均为O(1),需要先创建哈希表,再存放元素。
  • 对于需要频繁插入的场景使用map,对于不需要频繁插入,需要频繁查找的场景使用hash_map。

3.STL中的hashtable的底层实现?

4.vector底层原理

  • 底层是动态数组
  • size = finish - start,求个数的时间复杂度为O(1)
  • capacity = end_of_storage,求容量的capacity的时间复杂度为O(1),
  • capacity >= size forever
  • resize(newsize, const T& x) 如果newsize < size,则erase(begin + newsize, end()),
    否则按照当前size*2(或者当前capaciy * 1.5)作为newsize新创建一个vector,然后将旧vector中的元素拷贝构造到新vector,再将剩余空间初始化为x;
  • resize(newsize) 相当于调用 resize(newsize, T()),所以需要T有无参的构造函数。resize=分配内存+创建对象
  • 当插入时没有了剩余空间时,需要扩充vector的大小,方法是新申请一个大小为newsize = oldsize * 2的vector(或者newsize=capacity * 1.5),然后将原来vector中的元素复制到新vector开始的位置(调用拷贝构造),将待插入元素x放到后面。然后析构掉原来的vector中的元素并释放内存。
    需要特别指出:resize修改的是size,reserve修改的是capacity。
  • erase(first, last)
    只析构对象,不回收内存。
    将last后面的元素复制到first到i的位置,然后析构掉i后面的元素,只析构不回收内存。
  • reserve
    只分配内存,不创建对象。
  • shrink_to_fit
    如果capacity > size,则析构掉对于的对象,并且释放掉相应内存

5.list底层原理

双向链表

#include <iostream>
#include <list>
class A
{
public:

    A(int a = 0) : val(a)
    {
        std::cout << "A constructor=" << val  << std::endl;
    }

    ~A()
    {
        std::cout << "A Destructor=" << val << std::endl;
    }

    A(const A& a)
    {
        val = a.val;
        std::cout << "A copy constructor a.val=" << a.val << std::endl;
    }

    A& operator=(const A& a)
    {
        val = a.val;
        std::cout << "A operator= a.val=" << a.val << std::endl;
        return *this;
    }
    int val;
};


int main()
{
    std::list<A> lst;
    A a(100);
    lst.push_back(a);
    lst.push_back(a);
    lst.push_back(a); // 分配空间,调用拷贝构造
    std::cout << "lst.size = " << lst.size() << std::endl;
    lst.erase(++lst.begin()); // 析构对象,释放内存
    std::cout << "lst.size = " << lst.size() << std::endl;

    lst.unique();    // 去除连续的相同的元素(析构对象、释放内存),只保留一个
    lst.reverse();   // 链表逆置
    lst.pop_front(); // 移除头结点
    lst.pop_back();  // 移除尾结点 
    lst.sort();      // 快排,不能用stl算法sort,只能用成员函数sort
    lst.splice(iter, std::list<A>& lst2); // 将lst2连接到iter之前的位置

    return 0;
}

6.deque底层原理

由中控器、缓冲区、迭代器三部分组成,可以在头尾进行插入删除操作。

中控器map

  • 一段连续内存,每个元素为一个节点node,node是一个指针,指向每个缓冲区的首元素。

缓冲区

  • 有若干个缓冲区,每个缓冲区大小相同,缓冲区保存真正的数据

迭代器

  • deque有两个迭代器first(iterator)和finish(iterator),每个迭代器有4个指针成员:first cur last node
  • 每个迭代器的node指针成员指向map中的node,假如迭代器的node指向map[i],此时这个迭代器的first指针成员指向map[i]所指向的缓冲区的首元素,last指针成员指向缓冲区的尾元素,cur指针成员指向缓冲区的当前访问元素。

7.vector如何释放空间

  • erase
    只析构对象,不释放内存。
  • clear
    等于调用 erase(begin(), end())
  • shrink_to_fit
    如果capacity > size,则析构掉对于的对象,并且释放掉相应内存
  • remove
    将待删除元素移动到最后
  • 释放内存
    // 方法1
    vector<T>().swap(vec); 
    
    // 方法2
    vec.clear();
    vec.shrink_to_fit();
    
    

8.stack 栈

先进先出的数据结构,可以由某种容器包装而成,栈没有迭代器,可以使用deque或list作为底层容器。

#include <stack>

std::stack<int> st;
st.push(1);
st.push(2);

int n = st.top(); // 获取栈顶元素
int size = st.size();
if (st.empty()){ 
    // do something
}

st.pop(); // 栈顶元素出栈

9.queue队列

先进先出,单向队列,队头出队,队尾入队,没有迭代器,默认以deque作为底层容器,也可以以list为底层容器。

#include <queue>
std::queue<int> q;
q.push(1);
q.push(2);
bool b = q.empty(); // 判断队列是否为空
int size = q.size(); // 队列元素个数
int n = q.back(); // 取队尾元素
int n = q.front(); // 取队头元素
q.pop(); // 队头元素出队

10.heap堆

heap不是STL的容器组件,而是扮演priority_queue的助手。优先级队列的底层容器可以是数组、list,可以是完全二叉树。
priority_queue没有迭代器,默认以vector作为底层容器。
插入元素时排序,只有队列最顶端元素(权重最高者)才有机会被取用。

#include <iostream>
#include <queue>
#include <vector>

int main()
{
    std::priority_queue<int> q; // 默认大顶堆
    q.push(1);
    q.push(3);
    q.push(5);
    q.push(7);
    q.push(9);s
    q.push(2);
    q.push(4);
    q.push(6);
    q.push(8);
    
    int size = q.size();  // 元素个数
    bool b = q.empty();   // 判空
    std::cout << "size = " << size << std::endl;

    while (!q.empty())
    {
        // output: 9 8 7 6 5 4 3 2 1
        std::cout << "q element = " << q.top() << std::endl; // 打印队头元素(最大的)
        q.pop();  // 队头元素出队
    }   

    std::priority_queue<int /* T */, std::vector<int> /* 底层容器 */, std::greater<int> > q2;
    q2.push(1);
    q2.push(3);
    q2.push(5);
    q2.push(7);
    q2.push(9);
    q2.push(2);
    q2.push(4);
    q2.push(6);
    q2.push(8);
    while (!q2.empty())
    {
        // output: 1 2 3 4 5 6 7 8 9
        std::cout << "q2 element = " << q2.top() << std::endl; // 打印队头元素(最小的)
        q2.pop();  // 队头元素出队
    }   

    return 0;
}

11.slist 单向链表

单向链表,只有next指针,STL的习惯,在某位置插入元素是在指定位置之前而非之后,所以要在一个位置之前插入元素需要从头遍历,效率低。
因此slist不提供push_back,只提供push_front在头部插入。

12.set 集合

底层使用红黑树数据结构,只有key没有value,key就是value,不可以更改key值。
其与map的区别是value_type是key,而map的value_type是std::pair<key, T>,插入的时候都是调用RB.insert()函数。
set调用的是 RB.insert(value_type(key))
【特别】红黑树的迭代器++iter、--iter、iter++、iter-- 都是按照二叉树的【中序遍历】顺序。

#include <iostream>
#include <set>

int main()
{
    std::set<int> s;
    s.insert(1);
    s.insert(3);
    s.insert(5);
    s.insert(7);
    s.insert(9);
    s.insert(2);
   // s.insert(4);
    s.insert(6);
    s.insert(8);
    s.insert(1); // 不允许重复,仍然只有一个key=1的元素
    
    
    auto iter = s.find(5); // 查找, STL的find算法效率更高 std::find(s.begin(), s.end(), 5)
    if (iter != s.end())
    {
        std::cout << "find it, value is " << *iter << std::endl; // 通过迭代器访问其值
       // s.erase(iter); // 删除元素
    }   

    bool b = s.empty();     // 判空
    int cnt = s.count(1);  // 计算key为3的
    int size = s.size();   // 元素个数
    std::cout << "cnt is " << cnt << ", size = " << size << std::endl;
    
    // 查找第一个>=4的元素
    iter = s.lower_bound(4);
    if (iter != s.end())
    {
        std::cout << "find it , value is " << *iter << std::endl;
    }

    // 查找第一个>4的元素
    iter = s.upper_bound(4);
    if (iter != s.end())
    {
        std::cout << "find it, value is " << *iter << std::endl;
        std::cout << "iter value : " << *(++iter) << ", " << *(--iter) << ", " << *(iter++) << ", " << *(iter--) << std::endl;
     }    

    return 0;
}

13.map

底层是红黑树,插入删除时会自动按照key排序。key值不可修改,value可修改,key不可重复。
其与set的区别是value_type是std::pair<key, T>,而set的value_type是key,插入的时候都是调用RB.insert()函数。
map调用的是 RB.insert(value_type(key, T))
【特别】红黑树的迭代器++iter、--iter、iter++、iter-- 都是按照二叉树的【中序遍历】顺序。

#include <iostream>
#include <map>
#include <string>

int main()
{
    std::map<std::string, int> mp;
    mp["zhangsan"] = 1;
    mp["lisi"] = 2;
    mp["wangwu"] = 3;
    mp["guodegang"] = 4; // 插入方式1

    auto iter = mp.find("lisi");
    if (iter != mp.end())
    {
        // 通过迭代器访问key value
        std::cout << "find it, key=" << iter->first << ", value=" << iter->second << std::endl;
    }
    std::cout << "now mp size is " << mp.size() << std::endl;

    iter = mp.find("wangwu");
    if (iter != mp.end())
    {
        mp.erase(iter);
    }
    std::cout << "after erase 1 item, now mp size is " << mp.size() << std::endl;

    mp.insert(std::pair<std::string, int>("eason", 5)); // 插入方式2
    mp.insert(std::make_pair("jacky", 6));              // 插入方式3
    mp.insert(std::map<std::string, int>::value_type("andy", 7)); // 插入方式4
    std::cout << "after insert 3 items, now mp size is " << mp.size() << std::endl;

    return 0;
}

13.multiset

与set一样都是以红黑树为底层实现,不同的是insert时调用的是RB.insert_equal(value_type(key));

15.multimap

与map一样都是以红黑树为底层实现,不同的是insert的调用的是RB.insert_equal(value_type(key, T))

16.如何在共享内存上使用STL标准库

17.map插入方式有哪几种

mp[key] = value;
mp.insert(std::pair<keytype, valuetype>(key, value));
mp.insert(std::make_pair(key, value));
mp.insert(std::map<keytype, valuetype>::value_type(key, value));

18.unordered_map unordered_set底层原理

19.迭代器的底层机制和实效问题

迭代器是是根据模板类型T定义的类型别名,对于iterator来说是T, 对const_iterator来说是 const T

20.为什么vector的插入操作可能会导致迭代器实效

21.vector的reserve()和resize()方法之间有什么区别

  • reserve只申请空间,不需要调用类型的默认构造函数
  • resize的大小超过当前大小时,会调用类型的默认构造函数T()进行初始化构造对象。

22.容器内部删除一个元素

23.vector越界访问下标,map越界访问下标,vector删除元素时会不会释放空间?

vector越界可能会崩溃,map的operator[key] 是调用RB.insert实现的,insert函数返回一个pair对象,pair.first是一个插入成功或失败的迭代器,pair.second是一个表示插入成功或失败的bool值,如果key不存在则不会失败,会返回一个临时pair对象(目前second值不详)。
vector::erase函数会析构对象而不会释放内存。

24.map中[]和find的区别?

// 返回引用
T& operator[](const key_type& key)
{
    // insert函数返回pair<iterator, bool>
    // insert底层调用的是红黑树的insert实现, 调用t.insert_unique
    return ((insert(value_type(key, T()))).first)->second;
}

// 返回迭代器
iterator find(const key_type& key)
{   
    // t 是底层红黑树
    return t.find(key);
}

25.STL内存优化

26.频繁对vector调用push_back()对性能的影响和原因

频繁push_back时,vector会因为capactiy不足而不断从堆上申请更多的连续内存,而申请堆内存是需要加锁的,另外,申请了新的vector后,需要调用元素的拷贝构造函数从旧的vector复制对象到新的vector,然后析构掉旧vector里的对象并释放其内存。
不断进行申请内存、调用拷贝构造函数、调用析构函数、释放内存会比较耗时。

27.push_back和emplace_back的区别

push_back和emplace_back都是在std::vector末尾插入元素,当操作内置类型时两者无差别。
emplace_back 是c++11起加入的,参数原型为Arg&&...。

区别主要在操作自定义类型时:

push_back(const T& t)
push_back(T&&) // c++11
emplace_back(Arg&&...) // c++11
  • emplace_back可以接收不定个数参数,可以传入对象的构造函数所需的参数,从而可以原地构造,只调用一次对象的构造函数
  • emplace_back传入左值对象时和push_back一致,都需要调用一次拷贝构造
  • push_back(T&&)接收右值参数时需要调用一次构造函数和一次拷贝构造
  • push_back不可以接收变长参数,但可以接收类的构造函数是单个参数的参数从而构造对象(一次构造函数和一次拷贝构造),前提是构造函数非explicit的
  • 这几个函数在传入内置类型时无差别
  • 主要差别是emplace_back可以原地构造,减少一次调用拷贝构造

28.hashtable 哈希表

解决冲突的方法:线性探测、二次探测、开放地址;
负载系数:元素个数 / 表格大小, 哈希表的平均插入、删除、查找时间为O(1)

  • 线性探测
    哈希函数计算过后,如果当前位置冲突则依次往后找,找到尾后从头开始找,直到找到空闲位置。
  • 二次探测
    hash(element) = x
    依次尝试 x + 1, x + 4, x + 9, x + 16, ... , x + i * i
  • 开放地址法
    负载系数大于1
    每个位置指向一个链表,相同哈希值的元素依次插入到链表中。

29.hash_set

底层以哈希表实现,使用方法与set一致,但是不会自动排序。

30.hash_set

底层以哈希表实现,使用方法与map一致,但是不会自动排序。

31.hash_multiset

底层以哈希表实现,不会自动排序。

32.hash_multimap

底层以哈希表实现,不会自动排序。

四、C++内存管理

1.new / delete 和 malloc / free之间有什么关系?

new / delete

  • C++内存分配运算符,分为全局::operator new / ::operator delete 和 类的成员A::operator new和A::operator delete
  • 执行 new CObj()时优先调用本类重载的A::operator new,如果有重载则调用,如果没有则调用全局::operator new,再调用类的构造函数
  • new / delete, new [] / delete [] 一一对应

malloc / free

  • C库函数,用来进行内存分配和内存释放
  • ::operator new和::operator delete可能基于malloc / free实现
#include <iostream>
#include <stdexcept>
#include <new>

class A
{
public:
    A() 
    {
        std::cout << "A::constructor" << std::endl; 
    }
    ~A()  
    {
        std::cout << "A::Destructor" << std::endl; 
    }

    void* operator new(std::size_t size) 
    {
        std::cout << "operator new = " << size << std::endl; 
        if (0 == size) ++size;
        if (void* p = malloc(size))
        {
            return p;
        }
        throw std::runtime_error("operator new error"); 
    }
    
    void* operator new(std::size_t size, const std::nothrow_t&) 
    {
        std::cout << "operator new nothrow_t = " << size << std::endl; 
        if (0 == size) ++size;
        return malloc(size);
    }

    void operator delete(void* p) 
    {
        std::cout << "function operator delete" << std::endl;
        throw std::runtime_error("operator delete error");
        free(p);
    }

 
    void operator delete(void* p, const std::nothrow_t&) noexcept
    {
        std::cout << "function operator delete nothrow_t" << std::endl;
        free(p);
    }
};

int main()
{
    try
    {
        A* p = new A();
        delete p;
        p = nullptr;
    }
    catch(const std::exception& e)
    {
        std::cerr << "what is " << e.what() << '\n';
    }  

    return 0;
}

2.delete和delete[]有什么区别

  • delete用来释放new申请的内存
  • delete[]用来释放new[]申请的内存

3.内存块太小导致malloc和new返回空指针,该怎么处理

  • malloc返回空指针说明分配失败,应该结合业务逻辑或者直接让其崩溃
  • new在一般情况下不会返回空指针,当内存分配失败时会抛出std::bad_alloc异常

4.内存泄漏的场景有哪些

  • 申请了内存没有释放,new没有对应的delete,malloc没有对应的free
  • new[]申请内存,但是用delete释放

5.内存的分配方式有几种

6.堆和栈有什么区别

  • 内存块大,分配速度慢,需要加锁,每个进程都有一个默认的堆

  • 空间小,分配速度快,每个现场都有一个栈

7.静态内存分配和动态内存分配有什么区别

  • 静态分配内存在栈上分配
  • 动态内存分配在堆上分配

8.如何构造一个类,使得只能在堆上或只能在栈上分配内存

重写operator new和operator new[]
重写 placement new ???

9.浅拷贝和深拷贝有什么区别

  • 浅拷贝是二进制拷贝,拷贝后的指针成员与原对象指向同一个资源
  • 深拷贝略

10.字节对齐的原则是什么

11.结构体内存对齐问题

12.C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free释放?

五、网络编程

1.什么是IO多路复用

2.epoll中dt和lt的区别和实现原理

3.tcp链接建立的时候3次握手,断开链接的4次握手是具体过程

4.connect方法会阻塞,请问有什么方法可以避免其长时间阻塞?

5.网络中,如何客户端突然掉线或者重启,服务端怎么样才能立刻知道?

6.在子网210.27.48.21/30中有多少个可用的地址,分别是什么?

7.TTL是什么?有什么用处,通常哪些工具会用到它?

8.路由表是做什么用的?在linux环境中怎么来配置一条默认路由?

9.在网络中两台主机A和B,并通过路由器和其他交换设备连接起来,已经确认物理连接正确无误,怎么来测试这两台机器是否联通?如果不通,怎么来判断故障点?怎么排除故障?

10.网络编程中设计并发服务器,使用多进程和多线程,请问有什么区别?

11.网络编程的一般步骤

12.TCP的重发机制是怎么来实现的?

13.TCP为什么不是两次连接,而是三次握手?

14.socket编程,如果client断电了,服务器如何快速知道?

15.fork()一子进程后,父进程的全局变量能不能使用

16.4G的long型整数中找到一个最大的,如何做?

17.tcp三次握手的过程,accept发生在三次握手的哪个阶段?

18.tcp流,udp数据包,之间有什么区别,为什么TCP要叫做数据流?

19.socket在什么情况下可读?

20.TCP通讯中,select到读事件,但是读到的数据是0,为什么,如何解决?

21.说说IO多路复用的优缺点?

22.说说select机制的缺点?

23.说一下epoll的好处

24.epoll需要再用户态和内核态考别数据吗?

25.epoll的实现知道吗?在内核当中是什么样的数据结构存储的,每个操作的时间复杂度是多少?

六、MySQL

1.MySQL架构

2.数据库三范式

3.char和varchar的区别

4.varchar(10)和varchar(20)的区别

5.索引

6.索引底层用的什么数据结构

7.B树

8.B+树

9.聚簇索引

10.哈希索引

11.覆盖索引

12.索引的分类

13.最左前缀原则

14.怎么知道创建的索引有没有被使用到?或者说怎么才可以知道这条语句执行的很慢的原因

15.什么情况下索引会创建失败,即查询不走索引

16.查询性能的优化方法

七 、Redis

八、计算机网络

九、操作系统

1.并发和并行

2.同步、异步、阻塞、非阻塞的概念

3.进程和线程的概念

4.为什么有了进程,还要有线程

5.进程的状态转换

6.进程间通信方式

7.进程的调度算法有哪些

8.什么是死锁

9.产生死锁的原因

10.死锁产生的条件

11.解决死锁的方法

12.怎么预防死锁

13.怎么解决死锁

14.什么是缓冲区溢出,有什么危害

15.分页和分段的区别

16.物理地址、逻辑地址、虚拟内存的概念

17.页面置换算法

18.外中断和异常有什么区别

19.一个程序从开始到结束的完整过程

20.用户态、内核态

21.进程终止的方式

22.守护进程、僵尸进程、孤儿进程

23.常见内存分配错误

24.原子操作

25.抖动是什么

posted on 2024-07-13 18:09  崔好好  阅读(10)  评论(0编辑  收藏  举报

导航