C/C++ 基础知识

1 语言基础

C 与 C++ 的区别
1)C++ 兼容 C,又有许多新特性,如引用、智能指针、auto 变量等
2)C++ 面向对象,C 面向过程
3)C 语言有不安全的语言特性,如强制转换的不确定性、内存泄漏等,C++ 引入很多特性来完善安全性
4)C++ 引入 STL 标准模板库,提高了代码的复用性

struct 与 class 的区别
1)struct 默认的访问权限是 public,class 默认的访问权限是 private
2)struct 默认是公有继承,class 默认是私有继承
3)struct 描述的是一个数据结构集合,class 是对一个对象数据的封装
4)模板泛型可以应用于 class ,不能应用于 struct
5)struct 的构造函数即使被重载,默认构造函数依然被保留;class 重载了构造函数,默认/缺省构造函数被覆盖。

C 的 struct 与 C++ 的 struct 区别
C++ 的 struct是对 C 的 struct 的一个扩充,C 的 struct 不能继承,C++ 的 struct 可以继承

include 使用 "" 和 <> 的区别
区别:
1)<> 的头文件是系统文件,"" 的文件是自定义文件
2)编译器预处理阶段查找头文件的路径不一样
2-1)使用 <> 的头文件查找路径:不查找当前文件目录,编译器设置的头文件路径->系统变量
2-2)使用 "" 的头文件查找路径:优先查找当前文件目录,再按照 <> 方式查找,即当前文件目录->编译器设置的头文件路径->系统变量
备注:编译器设置的头文件路径(/xxx/include/)

导入 C 函数的关键字是什么?C++ 编译时和 C有什么不同?
关键字:使用 extern C。加上 extern C ,会指示这部分代码按 C 语言的进行编译,而不是 C++;
编译区别:C++ 支持函数重载,因此编译器编译函数的时候会将函数的 参数类型 也加到编译后的代码中。而 C 不支持函数重载,因此编译后的代码无需添加参数类型。

简述C++从代码到可执行二进制文件的过程
【编译过程:词法分析、语法分析、语义分析】
预编译——编译——汇编——链接——装载——执行

简述静态链接和动态链接的区别
【静态:库连接到可执行文件——装载速度快】
【动态:执行时,链接库——装载速度慢】
1)静态链接:在链接的时候就已经把要调用的库链接到生成的可执行文件中,只要编译成可执行文件,即使删除静态库也不会影响程序的执行。静态库在 Windows 下的后缀是 .lib,在 Linux 下的后缀是 .a。【优点】安全,【缺点】消耗内存空间大
2)动态链接:动态链接是在执行的时候,需要到了某个库,才去链接这个库。动态库在 Windows 下的后缀是 .dll,在 Linux 下的后缀是 .so

静态库与动态库的链接方式:

1.静态库制作方式:

1)通过 ar 命令:ar rcs libhello.a hello.o
2)CMakeList 的 ADD_LIBRARY() 函数:add_library(emu STATIC ${libemu_SRCS}) //将 SRC 指定的源文件生成静态库文件

2.静态库链接方式:

-l选项后面跟静态库名称 gcc main.c -lhello -o staticLibrary

3.动态库制作方式:

1)通过 -shared 选项指定生成共享库,-fpic指定生成位置无关代码(Position Independent Code),这对于共享库而言是必需的,因为它可以在内存中加载并映射到不同进程的地址空间中:gcc -shared -fpic hello.c -o libhello.so
2)CMakeList 的 ADD_LIBRARY() 函数:ADD_LIBRARY(eolutil-main SHARED ${SRC}) //将 SRC 指定的源文件生成共享库文件

static 关键字作用 / 静态变量作用
1)改变生命周期

  • 对于非类静态变量,需要区分全局变量和局部变量。
    1-1)对于静态全局变量,它的生命周期开始于程序初始化,结束于程序结束;
    1-2)对于静态局部变量,它的生命周期开始于函数首次调用,结束于程序结束;
    1-3)对于类静态变量,它的生命周期开始于类对象的首次初始化,结束于类销毁阶段

2)改变作用域

  • 静态全局变量/全局函数只能在声明它的文件可见,而不能通过 extern 作用域整个工程;

3)改变存储方式

  • 局部变量存储在栈,静态局部变量存储在 .data 段或 BSS 段

类的静态函数为什么不能调用非静态变量?

当调用对象的非静态函数时,编译器会把该对象的地址赋值给成员函数的 this 指针,而静态函数不属于任何对象,因此静态函数无 this 指针,因此无法访问非静态变量。

函数指针概念、定义方式、使用场景
函数指针就是指向函数的指针变量,指向该函数的入口地址(代码编译后,每个函数都有一个入口地址)。

int function(int a);
int (*f)(int a);  // 右边的 (int a) 声明它指向的函数参数
f = &function;  // 由于指针 f 要指向地址,所以用取地址符

应用场景:回调指针。

类对象指针初始时指向空,再调用函数指针可以吗?(考察类函数地址的概念)
A:可以。相同类的的任何函数入口地址都相同,因此在编译阶段,编译器无需考虑对象的地址。但是,若空对象调用的函数里有用到 this 指针调用其他成员变量或成员函数,运行时会出错。

class DogtTest
{
public:
   void bark();
};

void DogtTest::bark()
{
   cout << "WWW!" << end;
}

int main( )
{
   DogtTest *pd = nullptr;
   pd->bark();
   return 0;
}
// 输出
WWW!

野指针概念、产生原因及避免措施
野指针是指指向的位置是不可知的指针。
产生原因:指针指向的对象释放后,并未将指针置空;
避免措施:1)初始化置空;2)申请内存后判空;3)指针释放后置空;4)使用智能指针

智能指针(auto_ptr、share_ptr)
1)智能指针概念:智能指针是封装了指针、及构造函数、析构函数的类。
2)使用智能指针原因:管理指针,防止内存泄漏;当超出该对象的作用域时,类会自动调用析构函数,释放资源;
3)auto_ptr 与 share_ptr 的区别:
3-1)auto_ptr 采用所有权模式,若 p2 = p1,则 p1 所有权会被剥夺,此时 p1 变为野指针;
3-2)share_ptr 采用共享模式:使用引用计数,当最后一个指向它的指针销毁时,才释放资源。

内联函数的概念及其作用
在普通函数之前加上关键字 inline,就变成了内联函数。内联函数一般都是比较短如 3-5 行的函数,且不能含有复杂的结构控制语句。
作用:内联函数避免了函数调用时出入栈的开销,它在编译时展开,以免于运行时调用。它是以代码膨胀为代价的;
【注1:宏定义在预编译时展开,内联函数在编译时展开,函数在运行时展开】
【注2:与宏定义相比,宏定义只是在预编译阶段简单做文本替换,容易出现二义性,与函数相比,内联函数在运行阶段展开,避免了出入栈的开销】

C++ 3 种值传递方式 —— 值传递、引用传递、指针传递
1)值传递:形参在参数体内的改变,不会影响实参;
2)引用传递:传递的是引用对象,形参在参数体内的改变,会影响实参;
3)指针传递:传递的是对象的地址,形参在参数体内的改变,会影响实参;

class DogtTest
{
public:
   void bark();
};

void DogtTest::bark()
{
   cout << "WWW!" << end;
}

void func2(DogtTest* dt)
{
   dt->bark();
}

void func(DogtTest &dt)
{
   dt.bark();
}

int main( )
{
   DogtTest pd;
   // 引用传递
   func(pd);
   // 指针传递
   func2(&pd);
   return 0;
}

谈谈 C++ 11 5 种新特性

  • auto 自动推导
    编译器根据初始化的表达式自动确定变量的类型。

  • 智能指针
    智能指针是对指针、构造函数、析构函数封装的类,它的作用是避免内存泄漏,它会在指针生命周期结束时释放内存。常见的有 unique_ptrshared_ptrauto_ptr已被弃用,它有一个重大的设计缺陷,及所有权转移,会导致 auto_ptr 变成野指针。

  • 类模板
    类模板关键句为 template <typename T>

    // 类模板
    template <typename T>
    class Compare
    {
      private:
        T x;
    }
    

    补充:模板类是类模板的一个实例:

    template <int T>
    class Compare
    {
      int T;
    }
    
  • 移动语义 std::move()
    优点:std::move() 唯一的功能是将一个左值引用强制转化为右值引用而避免拷贝构造。它将对象的所有权从一个对象转移到另一个对象,没有内存搬迁或内存拷贝。被移动的对象,其原先拥有的资源都会被置空(安全状态)。

    int a = 1;
    vector<int> vec;
    vec.push_back(move(a));
    
  • Lamda 表达式

std::copy_if(nums.begin(), nums.end(), std::back_inserter(even_nums), [](int num) {
  return num % 2 == 0;
})

1、4 种类型转换详解(const_cast、dynamic_cast、static_cast、reinterpret_cast)

1.1 const_cast

const_cast 主要将 const 类型的指针转成非 const 类型的指针。const_cast 存在风险,因为将指向 const 变量的 const 指针转成非 const 指针,可以修改 const 变量的值,这是未定义行为。

const int a = 10;  
const int* p_const = &a;  
int* p = const_cast<int*>(p_const);  // 移除const属性  
*p = 20;  // 未定义行为,因为a是const,尝试修改它可能导致程序崩溃

1.2 dynamic_cast

dynamic_cast基类指针或引用转换为派生类指针或引用,即在类的继承体系中安全向下转换。

dynamic_cast动态转换,主要用于在运行时确定能否安全地将基类指针或引用转换为派生类指针或引用,即确定能否安全地向下类型转换。若转换不安全,则返回空指针nullptr(对于指针),或抛出 std::bad_cast异常(对于引用)。

class Base {  
public:  
    virtual ~Base() {}  
};  
  
class Derived : public Base {  
public:  
    void print() { std::cout << "Derived instance" << std::endl; }  
};  
  
int main() {  
    Base* basePtr = new Derived();  
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);  // 动态转换  
    if (derivedPtr != nullptr) {  
        derivedPtr->print();  // 输出 "Derived instance"  
    }  
    delete basePtr;  
    return 0;  
}

1.3 static_cast

参考链接:https://blog.csdn.net/wangshubo1989/article/details/49105627 # 浅析c++中的类型转换--static_cast

static_cast静态转换,在编译时确定转换的结果。用于 1)基础数据类型之间的转换(如intfloat);2)向下类型转换:将一个指向基类的指针转成指向子类的指针,这是一种不安全的转换,但编译器不会报错,需要程序员保证转换的安全性(使用 static_cast向下转换是一种不安全的转换); 3)向上类型转换:将派生类的指针或引用转换为基类指针或引用,这种转换并不会有报错的风险。

float f = static_cast<float>(3.14);  // 将double转换为float  
  
Base* basePtr2 = new Derived();  
Derived* derivedPtr2 = static_cast<Derived*>(basePtr2);  // 向上转型(不安全)

1、详解 std::move

参考:https://blog.csdn.net/qq_41687938/article/details/119797468

1.1 demo 示例

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n"; //str输出为"Hello"
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n"; //str输出为空
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
                                         //v[0]、v[1]都为"Hello"
}

1.2 左值、左值引用、右值、右值引用

1.2.1 左值和右值的概念
  • 左值可以放在赋值号左边被赋值的值,左值必须要在内存中有实体
  • 右值是在赋值号右边取出值赋给其他变量的值,右值可以在内存,也可以在寄存器
  • 左值一定是右值,右值不一定是左值
  • 一个对象被用作右值时,使用的是它的内容值,被当作左值时,使用的是它的地址。
1.2.2 引用

引用的本质是靠指针来实现的,引用相当于变量的别名。
引用的基本规则: 引用必须初始化,且不能对引用重定义

1.2.3 左值引用和右值引用
  • 左值引用语法:type &引用名 = 左值表达式

  • 右值引用:type &&引用名 = 右值表达式

    int a = 100;
    int&& b = 100; // 右值引用
    int& c = b;  // 正确,b 为左值
    int& d = 100; // 错误
    
1.2.4 std::move 简介

std::move 唯一的功能是将一个左值引用强制转化为右值引用而避免拷贝构造。它将对象的所有权从一个对象转移到另一个对象,没有内存搬迁或内存拷贝。被移动的对象,其原先拥有的资源都会被置空(安全状态)。 ** 从实现上讲,std::move 基本等同于一个类型转换 static_cast<T&&>(lvalue)

int a = 1;
vector<int> vec;
vec.push_back(move(a));

2 C++ 内存

堆和栈的区别
1)内存分配与释放:栈的内存分配与释放由编译器负责。当定义一个变量时,栈会自动分配内存;当变量不再使用时,栈会自动释放内存。堆由程序员申请分配与释放内存,否则可能会造成内存泄漏。对象在创建时在堆上被分配一块内存,不再引用时,垃圾回收器将其内存释放回堆;
2)数据结构不同:栈是一种线性数据结构,具有先进后出的特性。而堆是一种树状的数据结构。
3)扩展方向不同:栈向下扩展、堆向上扩展
4)能否产生碎片不同:栈不会产生内存碎片,堆会产生内存碎片

简述 C++ 的内存管理

注意:C++ 内存管理与 Linux 进程内存布局不是相同问题
1)Linux 进程内存布局(程序有哪些内存布局):分 5 个段,text代码段、data数据段、bss 段、堆、栈

1)BSS 段存储的是未初始化的或初始化为0的全局变量、静态变量。在程序执行之前,BSS 段会自动清 0 ,所以未初始化的全局变量在程序执行之前变成 0
2)代码段:是只读的,存放程序执行代码,及只读的常量

===================================== 暂时忽略 =====================================
2)C++ 内存管理:分 5 个区,常量存储区(存储全局常亮)、全局/静态存储区、自由存储区(free store)、堆区、栈区。

对自由存储区理解的链接:https://www.cnblogs.com/zhxiaomiao/archive/2009/10/13/1582188.html
对自由存储区理解的链接:https://www.cnblogs.com/qg-whz/p/5060894.html

自由存储区:与堆十分相似。由 new 等分配的内存块存在于自由存储区,用 delete 来结束生命。自由存储区是使用堆结构来实现自由存储的。
全局/静态存储区:C语言中全局变量分为初始化和未初始化。在 C++ 里,二者共同占用一块内存区。
常量区:只读
常量的存储方式:局部常量存储于栈、全局常量放在符号表以提高访问速率,字符串常量放在常量区
====================================================================================

一、C++内存管理

参考链接:https://blog.csdn.net/caogenwangbaoqiang/article/details/79788368

1. C++ 内存管理详解

1.1 内存分配方式

C++ 内存分配:分 5 个区,常量存储区(存储全局常亮)、全局/静态存储区、自由存储区(free store)、堆区、栈区。

  • 堆:由 new 分配的内存块。为了防止内存泄露,一个 new 要对应一个 delete,若堆块未被程序员释放,程序结束后,操作系统会自动回收;
  • 自由存储区:由 malloc 分配的内存块,与堆十分相似,由 free 释放;
  • 全局/静态存储区:C++ 没有区分初始化和未初始化的全局/静态变量;
  • 常量存储区:存储常量。不允许修改。
1.1.1 补充:malloc 与 new 的区别
  • 语法:malloc 是 C 语言函数,用于申请一块内存空间;new 是 C++ 关键字,用于动态分配内存并调用对象的构造函数,用于对象的初始化。
  • 返回类型:malloc 返回的是空类型指针(void*),需要显示类型转换为所需类型;new 返回的就是所需类型的指针;

1.2 堆和栈区别

  • 管理方式:栈由操作系统管理,对象初始化时创建内存,跳出作用域时,操作系统自动释放;堆由程序员手动申请和释放;
  • 生长方向:堆自下而上生长,即向着内存地址增加的方向生长;栈自上而下生长,即向着内存地址减少的方向生长;
  • 碎片问题:栈是先进后出结构,不会出现碎片问题;堆频繁地申请和释放造成内存空间的不连续,会出现大量的内存碎片;
  • 分配效率:出栈入栈操作,会通过寄存器的配合,且有专门的指令执行,因此栈的分配效率较高;堆的申请和释放需按照一定的算法(如伙伴算法),分配效率较低。

例子:这里例子里既用到堆区,又用到栈区。指针 p 存放在栈区,它指向的是堆区的一个数组空间。在 delete 时,应该使用 delete[],告诉操作系统,删除一个数组。

int *p = new int[10];
delete[] p;

1.2 五大常见的内存错误及其对策:

错误1:内存未分配成功,却使用了它,导致使用“野指针”错误

  • 解决对策:定义指针时置空,申请内存时判空;
    错误2:内存分配成功,但尚未初始化
  • 解决对策:申请内存成功后,应立刻赋初值。
    错误3:内存初始化成功,但使用越界
  • 解决对策:避免指针或下标越界
    错误4:内存正常使用,但使用完毕后未释放内存,造成内存泄漏
  • 解决对策:1)mallocfreenewdelete 一一配对;2)使用智能指针;3)多态编程下,使用虚析函数
    错误5:内存正常释放,但释放后仍继续使用,导致使用“野指针”错误
  • 解决对策:内存释放后置空,防止使用野指针

1.3 内存泄漏的定义及解决办法

内存泄漏是申请了一块内存,使用完毕后没有释放掉。
1)new 和 malloc 申请资源使用后,没有 delete 和 free 释放掉
2)子类继承父类,父类析构函数不是虚函数。当基类指针指向子类对象,如果基类的析构函数不是虚函数,那么子类的析构函数不会被调用,子类的资源没有被释放,造成内存泄漏。

class Base {
public:
    Base() {}
    ~Base() { std::cout << "Base Destructor" << std::endl;}  // 基类析构函数未声明为虚函数
};

class Derived : public Base {
public:
    Derived() {}
    ~Derived() { std::cout << "Derived Destructor" << std::endl;}
};

Base* ptr = new Derived(); // 使用基类指针指向派生类对象
delete ptr; // 删除基类指针

解决办法:
1)良好编程习惯,new 和 delete,malloc 和 free 配套使用;
2)使用智能指针;
3)多态编程下,将基类析构函数定义为虚函数;
4)使用内存泄漏检测工具进行检测。

1.4 指针与数组对比

1.4.1 修改内容

通过指针修改常量字符串会导致异常,但编译器无法发现该错误。

char a[] = “hello”;  // 在栈上分配 6 个字符的空间存储字符串
a[0] = ‘X’; // 可以修改栈空间的内容
cout << a << endl;
char *p = “world”; // "world" 存储于常量区
p[0] = ‘X’; // 指针 p 试图修改常量区内存,编译器不能发现该错误
cout << p << endl;

1.5 内容复制与比较

数组的复制使用标准库函数 strcpy,数组的比较使用标准库函数 strcmp

1.6 C++ 中的健壮指针和资源管理

1.6.1 第一条规则(RAII)

在构造函数中分配资源,在析构函数中释放资源。

1.6.2 智能指针

auto_ptrshared_ptunique_ptr

简述下 atomic 原子类型概念及内存顺序

https://zhuanlan.zhihu.com/p/107092432

在多线程里,为了防止数据竞争问题,需要用到互斥锁。而管理互斥锁是比较麻烦的,因此引入 atomic 原子类型,以掩盖互斥锁。

1、内存对齐/结构体对齐

参考链接:https://blog.csdn.net/weixin_48896613/article/details/127371045 # 【C/C++】内存对齐(超详细,看这一篇就够了)

1.1 内存对齐作用

内存对齐作用:【CPU的快速访问】结构体是一种复合数据类型,为了使 CPU 能够对变量进行快速地访问,对结构体变量进行对齐。如果变量在自然对齐位置,只需要一次就可以访问数据。
内存对齐注意 2 点:1)“放”哪里;2)“补”多少

对齐的地址一般是 n(n=2、4、6、8)的倍数

  • 1)1 个字节的变量:如 char 类型变量,放在任意地址的位置;
  • 2)2 个字节的变量:如 short 类型变量,放在2 的整数倍地址的位置;
  • 3)4 个字节的变量:如 float、int 类型变量,放在4 的整数倍地址的位置;
  • 4)8 个字节的变量:如 long long、double 类型变量,放在8 的整数倍地址的位置;

1.2 内存对齐例子

1.2.1 研究结构体最后一个成员内存对齐问题 1

此结构体共占 24 + 8 + 4 + 4 + 1 + 7 = 48字节。

struct stu1 {
  char a[18];  // 18 字节(16 + 2)-> 8 + 8 + 2 + 2 + 4 = 24 字节
  double b;  // 8 字节  
  char c;    // 1 字节 -> 1 + 3 = 4 字节
  int d;     // 4 字节
  short e;   // 1 字节
             // 尾部补齐 7 个字节
}

1.2.2 研究结构体最后一个成员内存对齐问题 2

此结构体共占 20 + 12 + 2 + 2 + 4 + 1 + 3 = 44字节。

struct stu1 {
  char a[18];  // 18 字节(16 + 2)-> 4*4 + 2 + 2 = 20 字节
  int b[3];  // 4 * 3 = 12 字节 
  short c;    // 2 字节
  char d;     // 2 字节 -> 1 + 1 = 2 字节
  int e;      // 4 字节
  short f;   // 1 字节
             // 尾部补齐 3 个字节
}

内存对齐作用、使用场景、对齐原则
内存对齐作用:
使用场景:strcut 、class、union
对齐原则:

3 面向对象1

面向对象与面向过程的区别
面向对象是一种编程思想,把一切东西都看成对象。把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示。
区别:
面向过程:根据业务逻辑从上到下写代码;
面向对象:将数据与函数绑定在一起,进行封装,增加代码的复用度,实现更快速的开发

面向对象的三大特征:
封装、继承、多态。
多态包括静态多态、动态多态。
静态多态:通过函数重载实现,编译器在编译期间完成,编译器根据实参类型推断调用哪个函数。
动态多态:通过虚函数实现。用基类指针指向子类对象,并调用虚函数
【理解多态:】多态是一种行为的封装,就是不同对象对同一行为会有不同的状态,它以封装和继承为基础。

1、虚表与虚表指针

1.1 虚表

虚表是 C++ 实现多态的重要机制。若某类定义了虚函数,编译器会为该类生成一个虚表,虚表记录的是该类每个虚函数的真正调用地址。
派生类在初始化的时候继承基类的虚表,若重写虚函数,修改虚表中对应的虚函数地址。

1.2 虚表指针

虚表指针是指向虚表的指针,每个包含虚函数的类的非静态对象都隐式地含有虚表指针。作用:虚表指针使得类对象在运行时能动态地找到正确的调用函数地址。

重写与重载的区别
重写:派生类中存在重新定义的函数,其函数名、参数列表、返回值类型,必须与基类相同。重写的基类中被重写的函数必须有 virtual 修饰。
重载:相同函数名,但参数列表不同(参数的类型、个数、顺序不同)、或返回值不同。
C++ 的重载:命名倾轧,这是在编译阶段完成的。
C++ 的重写;用基类指针指向子类对象,并调用虚函数,那么调用的将是子类对象的函数
备注:
1)虚表和类对应,虚表指针与对象对应。派生类在初始化的时候继承基类的虚表;
2)纯虚函数是虚函数加上 = 0;
3)抽象类是指包括至少一个纯虚函数的类。抽象类不能创建单例,其子类必须实现纯虚函数。

谈谈 4 种构造函数及其作用
若类对象是局部变量,则调用构造函数实例化对象,是在栈空间分配内存。new 是在堆上分配空间,然后再调用构造函数。

1)默认构造函数:给成员变量赋值
2)初始化构造函数:在默认构造函数基础上,加上初始化列表。成员变量初始化
3)拷贝构造函数:实现的是浅拷贝,即拷贝值或者地址【拷贝构造函数的形参是左值引用】

// 拷贝构造函数定义
class MyString {
public:
  MyString(const MyString& copySource) {
      if (copySource.buffer != nullptr) {
          buffer = new char[strlen(copySource.buffer) + 1];
          strcpy(buffer, copySource.buffer);
      } else {
          buffer = new char[1];
          *buffer = '\0';
      }
  }
}

// 拷贝构造函数使用
int main()
{
  MyString string1("Hello");
  MyString string2 = string1;
}

4)移动构造函数:(深拷贝)将其他类型的变量,隐式转换为本类对象。下例将 int 类型转换为 Student 类型。移动的语义是指将临时对象拥有的内存资源移为已用。临时对象的产生只是用于传递数据。移动构造函数使用右值引用传递参数。【移动构造函数涉及move() 函数,它的功能是将左值强制转为右值】【移动构造函数的形参是右值引用】

class MyString {
public:
  // 移动构造函数定义
  MyString(MyString&& copySource) {
      buffer = copySource.copySource;
      copySource.copySource = nullptr;
  }
}

// 移动构造函数使用
int main()
{
  MyString string1("Hello");
  MyString string2 = std::move(string1);
}

5)赋值构造函数:拷贝构造函数是在对象创建之处调用,以初始化一块内存。赋值构造函数是将一个已存在的对象复制到另一个已存在的对象中。它通过赋值运算符 (operator=) 来实现。拷贝构造函数只调用一次,赋值构造函数可调用多次。

// 赋值构造函数定义
class MyString {
public:
  MyString& operator=(const MyString& other) {
    buffer = other.other;
    return *this;
  }
}

补充:深拷贝与浅拷贝的区别
二者的区别只在于对引用对象的拷贝方式。
1)浅拷贝: 拷贝值或地址。对引用对象,拷贝的是该对象在栈中的内存地址,所以如果其中一个对象改变了这个地址的内容,就会影响到另一个对象。【致命性】:(出现野指针)一旦多个对象中的指针指向同一块堆空间,这些对象析构时就会对该空间释放多次。

2)深拷贝:将引用对象从内存中完整的拷贝一份出来,修改新对象不会影响到原对象。

空类,编译器会默认生成哪些函数?
1)默认构造函数;
2)拷贝构造函数;
3)赋值构造函数;
4)析构函数
注意:有了有参的构造函数,编译器就不提供默认的构造函数。

谈谈 C++ 类对象的初始化顺序
父类构造函数 -> 成员类对象构造函数 -> 自身构造函数
其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序
析构顺序和构造顺序相反。

简述向上类型转型和向下类型转型
【把握1)转换的安全性;2)转换的绑定时期:一个是动态绑定,一个是静态绑定】
1)子类转为父类:向上类型转型,使用 dynamic_cast<> ,安全的转换
2)父类转为子类:向下类型转型,可以使用 static_cast<> 强制类型转换,这种转换不安全,可能会导致数据的丢失。因为父类的内存可能不包含子类成员的内存

为什么不能虚构造?

为什么要虚析构?
若用基类指针指向子类对象,若基类析构函数没有使用 virtual,则释放基类指针时,只会调用基类的析构函数,而不会调用子类的析构函数,造成内存泄漏。
原因:如果无虚函数的动态绑定功能,析构的时候只能根据指针绑定的对象来调用对应的析构函数。而使用了虚析构,虚函数表对应的入口地址指向子类的析构函数,当子类进行析构时,先调用子类析构函数,再调用基类析构函数。
【C++ 默认的析构函数不是虚函数原因是:虚函数需要额外的虚函数表 和 虚表指针】

【用基类指针指向子类对象,如果不使用虚析构函数,当释放基类指针时,调用的是基类的析构函数,只会释放父类的资源。如果用的是虚析构,那么调用的是子类的析构函数,由于子类继承父类,所以调用完子类析构函数后,会继续调用父类的析构函数,因此不会造成内存泄漏。】

虚函数的动态绑定原理——虚函数表与虚函数表指针
每个类会为所有虚函数维护一个虚函数表,并且每个类的实例对象的首地址存储的就是指向该表首地址的指针。虚函数表记录的是虚函数的入口地址。派生类先继承基类的虚函数表,如果有重写虚函数,则覆盖对应的入口地址;如果有新添基类,则在表里新增一个对应关系。

虚函数表里存放的内容是什么时候写进去的?
1)虚表在编译阶段生成,对象内存空间开辟后,写入对象的虚表指针,然后调用构造函数,即虚表在构造函数之前写入。
2)二次写入机制,通过该机制让每个对象的虚表指针都能准确地指向自己类的虚表,为实现动态多态提供支持。

谈谈类的内存
类的内存包括:1)虚函数表指针;2)从基类继承的成员变量;3)自身的成员变量

构造时,不会对类内引用成员变量开辟空间,因为它是其他对象的别名。使用时有要求:
必须提供初始化列表构造函数,形参必须为引用类型。

谈谈常函数
1)常函数是在类成员函数后面加上 const,常函数只能读取数据成员,不能改变数据成员;
2)常量对象可以调用 const 成员函数;

谈谈虚继承与菱形继承问题
虚继承解决的是菱形继承的问题。所谓菱形继承就是,子类从不同途径继承来同一基类,在子类存在两份多份拷贝,1)既浪费内存空间,2)又会导致二义性问题。
使用虚继承,从不同途径继承来同一基类是共享的。
原理:使用虚继承的子类及其派生类,都含有一个虚继承表,指向虚继承的基类。

class A {
public:
    int dataA;
};

class B : public virtual A {
public:
    int dataB;
};

class C : public virtual A {
public:
    int dataC;
};

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

简述虚函数和纯虚函数,以及实现原理
1)虚函数:扣住动态多态的原理,及 vtable来阐述;
2)纯虚函数:实现方式是 virtual int func() = 0。扣住纯虚函数与抽象类,及创建对象的关系。【定义派生类的目的是,使派生类仅仅只是继承函数的接口】
【抽象函数不可实例化的底层原因:抽象函数具有纯虚函数,纯虚函数在虚函数表对应的入口地址是缺省值】

如何理解抽象类?
有纯虚函数的类称为抽象类。
1)抽象类只能用作其他类的基类,不能建立抽象类对象。原因:纯虚函数在虚函数表的入口地址为缺省值。
2)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

谈谈 C++ 的虚函数与纯虚函数的区别

成员函数可以被 static 与 virtual 同时定义吗?
虚函数和纯虚函数的定义中不能有 static 标识符,因为被 static 修饰的函数在编译时要求前期绑定,而虚函数是动态绑定,被二者修饰的函数生命周期也不一样。

拷贝构造函数的参数是什么传递方式?
拷贝构造函数的参数是const Object& obj 引用传递方式。如果使用传值方式,传值的方式回调用该类的拷贝构造函数,陷入死循环。
【备注:传指针也是传值,除了传引用外,其他方式都是传值方式。因此 const Object* obj 也是不行的】

C++ 哪些函数不能声明为虚函数,理由又是什么?
1)不能被继承的函数不能声明为虚函数:非成员函数;友元函数;
2)非运行时绑定的函数不能声明为虚函数:静态成员函数;内联函数(二者均为编译时绑定);
3)不依赖对象地址的函数不能声明为虚函数:构造函数。

常见的不能声明为虚函数的有:非成员函数、友元函数、静态成员函数、内联成员函数、构造函数
1)非成员函数
声明为虚函数的目的是为了实现多态,而多态是基于继承的。非成员函数不能继承,声明为虚函数无意义。
2)友元函数
声明为虚函数的目的是为了实现多态,而多态是基于继承的。友元函数不能继承,声明为虚函数无意义。
3)静态成员函数
静态成员函数在编译时绑定,虚函数在动态运行时绑定。二者矛盾。
4)内联成员函数
内联成员函数在编译时展开,虚函数在动态运行时绑定,二者矛盾。
并且内联函数的目的是减少函数调用的代价,使用虚函数,就要有虚表,代价是提高的,二者的目的相互矛盾。
5)构造函数
虚函数依靠虚表。在对象构造时,无虚表,无法将构造函数设置为虚函数

C++ 类模板和模板类的区别
1)类模板首先是模板,它的参数前面都要加class。
template<class T1, class T2>
class someClass {

};

template<class T1, class T2>
class someClass {

};

2)模板类首先是类,它是类模板的具象化,类定义中参数被实际类型所代替。

STL 的 6 大组成部分
容器、迭代器、适配器、空间配置器、算法、仿函数。
1)容器:各种存储数据的数据结构,容器包括顺序容器(vector、deque、list)、关联式容器(set、map)、容器适配器(栈、队列)等。
2)迭代器:提供了访问容器中对象的方法。迭代器封装了指针,重载了指针的一些操作符,使得用户在不知道容器具体内部的情况下,也能顺序访问容器。
3)适配器(Adaptor)
适配器封装了一些基本的容器,使之具备新的函数功能。STL 提供的队列(queue)、栈(stack)、优先队列(priority_queue),其实是一种容器适配器,它们的底层完全借助双端队列(deque);
4)空间配置器(Allocator)
为 STL 提供空间配置,主要工作包括:1)对象的创建与销毁;2)内存的获取与释放
5)算法:用来操作容器数据的模板函数
6)仿函数:仿函数即函数对象,是一个行使函数功能的类,它只有一个重载 operator() 运算符的类

class Func {
public:
  void operator() (const string& str) const {
    cout << str << endl;
  }
};

Func funcObj;
funcObj("hello world!!");
>> hello world!!

5)


谈谈 STL 中 map、哈希表、双端队列(deque)、链表(list)的实现原理
1)map:底层是红黑树(红黑树是非严格平衡的二叉搜索树,AVL 是严格平衡二叉搜索树)。红黑树有自动排序的功能,因此内部元素是有有序的。
2)哈希表(unordered_map):将记录的存储位置与记录的关键字关联起来,从而实现快速查找。
3)双端队列(deque):在两端增删元素具有较好的性能。

关于双端队列底层解释链接:http://www.aiuxian.com/article/p-pwypdoru-pr.html
https://www.iamshuaidi.com/2303.html

双端队列底层是一个假象的连续空间,实际上是分段存储的。它为每个结点都申请了一份空间。即:数据结构包括一个个固定数组、一个存放数组首地址的索引地址。即:数据结构包括一个固定数组、一个存放数组首地址的索引地址。

4)list:双向链表。

谈谈 STL 的空间配置器(allocator)
空间配置器用来实现容器的内存空间分配。
1)C++ 可以通过 2 种方式进行实例化对象。一种是调用构造函数实例化类对象Test test,一种是通过 new 来实例化 Test *pTest = new Test()
前者直接调用构造函数实例化对象,若该对象是局部变量,则在栈空间分配相应的存储空间;后者通过 new 实例化对象,执行了 2 个步骤:首先在堆空间分配内存,然后调用类的构造函数构造对象的内容。使用 delete 时,也经历了 2 个步骤:首先调用类的析构函数释放类对象,然后调用 delete 释放堆空间。
2)STL 空间配置器实现
空间配置器利用 new 和 delete 函数并对其进行封装进行实现。STL 中将空间配置和对象构造分开来:内存配置操作 alloc::allocate();内存释放操作 alloc::deallocate(),对象构造::construct(),对象释放::destroy()

关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池。

STL 容器查找的时间复杂度是什么,为什么?
1 vector
插入:O(N);查找:O(1);删除:O(N)
2 deque
头尾插入/删除:O(1);中间插入/删除:O(N); 查找:O(1)
3 list
头尾插入/删除:O(1);中间插入:O(N);查找:O(N);
4 map、set、multimap、multiset
四种容器采用红黑树实现,它是一种特殊的平衡二叉树。
插入、删除、查找:O(N)
5 unordered_map、unordered_set、unordered_multimap、unordered_multiset
四种容器采用哈希表实现
插入、删除、查找:最好 O(1), 最坏情况 O(N)


谈谈 map 和unordered_map 的区别?底层实现
1)map 底层是红黑树,红黑树是非严格平衡二叉搜索树,即它的元素是顺序存储的,中序遍历即可得到升序元素。map 所有元素以键值对存在,但以键进行排序的。键不能修改,但值可以修改。因为修改键会改变二叉搜索树(红黑树)的结构。
优点:顺序存储;缺点:插入访问删除等操作时间复杂度高
2)unordered_map的底层是哈希表,其存储无序。
优点:插入访问的时间复杂度最好可达O(1);缺点:数据无序存储


四种智能指针分别解决的问题:
1)auto_ptr (C++98的方案,C++11已经弃用)
采用所有权模式。同一时间只有一个对象只能被一个指针所指。

auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题。

2)unique_ptr(替换auto_ptr)
unique_ptr 是 auto_ptr 的改进,完全避免潜在的内存崩溃问题。

unique_ptr<string> p1(new string("I reigned loney as a cloud."));
unique_ptr<string> p2;
p2=p1; // 编译出错

3)shared_ptr
共享式所有权。同一时间只有一个对象能被多个指针所指。该对象和其相关资源会在“最后一个引用被销毁”时候释放。它的底层是通过计数来实现的。缺点:会出现环形引用,从而产生死锁。
4)weak_ptr
引入 weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题。weak_ptr 的构造和析构不会引起引用计数的增加或减少,它只是提供了对管理 对象的一个访问手段,进行该对象管理的是那个引用的shared_ptr。



简述 C++11 中 auto 的具体用法
auto 仅仅是一个占位符,编译器在编译阶段自动推导,用真正的类型替代它。
例如:

auto n = 10;  // 10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
auto f = 12.8;  // 12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
auto p = &n;  // &n 的结果是一个 int* 类型的指针,所以推导出变量 f 的类型是 int*
auto url = "www.123.com"; 由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。

什么是 RAII 机制?作用是什么?
RAII = Resources Acquisition Is Initialization,资源获得即初始化,它的作用就是避免内存泄漏。
它的本质是封装一个类,在申请内存块的时候,调用构造函数实例化一个对象,当对象超出作用域的时候会自动调用析构函数,而无需程序员手动释放,避免内存泄漏。



作用:弹性数组主要用于存储不定长数据,用法灵活,减少不必要的内存占用。

typedef struct {
     int32_t id;
     int32_t grade;
     int8_t  name[];   // 弹性数组
 }struct_info_struct;


指针和引用的区别
1)本质:指针的本质是地址,引用的本质是别名。使用指针指向的对象的时候,需要解引用。
2)内存分配:系统在栈里为指针分配内存空间,但不为引用分配内存空间。因为引用变量只能初始化,不能赋值。(所谓初始化就是创建对象的时候,根据特定字段的值进行创建。赋值是先创建个默认对象,再赋值。)
3)在不希望传入对象被修改的时候,常常用 const 引用传参。

1 指针与引用的区别
此问题被问过两三次,没有引起重视,只是回答到皮毛。
1)指针存储的是地址,引用是变量的别名;
2)指针可以为空,引用定义时必须初始化;
3)指针在初始化之后可以改变指向,引用在初始化之后不可再改变;
4)指针可以有多级,引用只有一级;
5)sizeof(指针) 得到的是指针的大小,sizeof(引用)得到的是变量的大小

C++ explicit 的作用
在C++中,explicit关键字用来修饰类的构造函数,它用来防止构造函数的隐式转换。被修饰的构造函数的类,必须以显示的方式进行类型转换。

谈谈重写(覆盖)与隐藏的区别

https://www.cnblogs.com/txwsh1/archive/2008/06/28/1231751.html

对于虚函数 virtual ,子类有与父类的相同函数时,就是重写;
对于非虚函数,子类有与父类的相同函数时,就是隐藏;

空类有哪些函数
· 缺省构造函数;
· 缺省拷贝函数;
· 缺省赋值函数;
· 析构函数

构造函数可以抛出异常吗?析构函数可以抛出异常吗?
· 构造函数可以抛出异常,但不建议抛出异常。若构造函数抛出异常,则不会调用析构函数,需要程序自身释放资源;
· 析构函数不可以抛出异常,也不应该抛出异常。一般而言,在异常处理中,会调用析构函数去释放因为异常而失效的对象。若析构函数又抛出异常,则上一个异常未处理完整,又来一个异常,程序会崩溃。

vector的clear()会释放内存?那如果想释放怎么释放内存呢?

https://cloud.tencent.com/developer/article/1383922

clear() 本身不释放对象,它只调用 vector 所保存的对象的析构函数,释放对象由析构函数解决。
(第二问不懂)

vector怎么将一个vector的数据转移到另一个vector?
使用 swap(),将两个 vector 的内容进行交换。

vector<int> vec1 = {1,2,3};
vector<int> vec2;
vec1.swap(vec2);

for (auto &d : vec1)
    cout << d << " ";
cout << "\n";
for (auto &d : vec2)
    cout << d << " ";
cout << "\n";

空类占用多少个字节?含虚函数的空类占用多少个字节?
空类占用 1 个字节,仅仅在内存中表示有这个对象;
含虚函数的空类在 32 位机器上含 4 个字节,存虚函数表指针。

构造函数可以私有吗?析构函数可以私有吗?

https://www.bilibili.com/read/cv11095933/

  • 构造函数可以私有,例如 单例模式,不允许外部创建对象,以确保一个类只能含有一个对象。
class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance) {
            instance = new Singleton();
        }
        return instance;
    }
private:
    Singleton() {}  // 构造函数私有化
    static Singleton* instance;
};
Singleton* Singleton::instance = nullptr;
  • 析构函数可以私有,但不建议私有。析构函数公有,才能在程序结束或者离开对象作用域时,系统自动调用对象的析构函数,以释放对象的资源。若析构函数为私有,则需要由该类的友元函数对对象进行释放。
    备注:私有析构函数的目的:希望仅由用户控制类对象的销毁与释放。

const 可以与 static 同时修饰类成员函数吗?
const 不可以与 static 同时存在。const 的常量属性是通过编译器隐式传入的常量指针 const this* 保证的。而 static 静态函数 this

编译器在编译 const 函数的时候,为了确保函数体内不会修改到类成员变量的值,会隐式传递一个 const this* 指针,而编译器不会为 static 静态函数传入 this 指针。

一个微信群有10个人,你有10块钱,使用 [0,1) 随机器发红包,尽可能地均匀,你有什么思路?
· 二倍均值法:val = rand(0, m / n * 2),m是剩余金额,n是剩余人数,该算法可以保证每个红包的平均随机区间相等,以此保证每个人抢红包的概率相等。例如剩100,10个人,然后区间(0,20],平均10元,下一个为 剩90,9个人,还是(0, 20],金额稳定性较大,但是缺乏用户惊喜
· 线段切割法:m钱n人,在(0,m)区间上随机生成n-1个点,将其分成n段,每一段的长度作为一个红包金额(随机生成n-1个不同点,排序,计算各个线段长度)
相比之下,实现难度较大,复杂度会更高(也可以先分段,然后对长段继续分段),但是用户惊喜度可能更高。

寻路问题,查一下 A*算法。

延迟删除思想:vector 删除某个元素,但不影响长度。

数据库连接池 并发场景下 有没有什么处理,例如消费者队列转存?
深入查一下数据库连接池的问题。

你知道 C++11 的 lambda 表达式吗?

https://www.cnblogs.com/DswCnblog/p/5629165.html

lambda 表达式就是一种匿名函数,它对外部变量的捕获方式有 值捕获( [a] )、引用捕获( [&a] )、隐式捕获 ( [=] )。

访问堆和访问栈哪个速度更快?
https://blog.51cto.com/u_15262460/3835557
结论:访问栈的速度快。
原因:函数栈与代码段一同被载入到内存,所以在栈上分配内存,cache 和 内存映射已经建立,效率会很高。除此之外,栈的变量还会存放在寄存器,提高访问速度。而堆的访问会涉及 malloc 过程,涉及缺页异常,页面置换调度等过程。

RAII 机制与异常安全
RAII 机制是异常安全的。C++ 保证,如果有异常被抛出,局部对象会被析构。

C++ 构造函数可以抛出异常吗?析构函数可以抛出异常吗?
1)构造函数可以抛出异常,由于抛出异常后,析构函数不能调用,因此在抛出异常前,应该将自身申请到的内存资源进行释放,防止内存泄漏。
2)析构函数不可以抛出异常,也不应该抛出异常。首先当程序抛出异常时,会调用对象的析构函数释放资源。如果析构函数又抛出异常,就会陷入无休止的循环调用。所以析构函数不可以也不应该抛出异常。

线程局部存储的原理是什么?
每个线程会维护一个ThreaadLocalMap,key 就是 threadLocal 变量的弱引用,value 就是其值。

vector 是线程安全的吗?或者说它在一个线程的迭代器会在另外个线程中失效吗?

https://www.zhihu.com/question/29987589

STL 容器不是线程安全的。如果写方线程 A 写入数据,由于潜在的内存重新申请和对象复制问题,会导致读方线程 B 读入数据。
解决办法:加锁。但是并发能力差。

各种排序算法的时间复杂度与稳定性分析

https://blog.csdn.net/qq_44665944/article/details/100937670

1 冒泡排序

冒泡排序是通过两个相邻元素的比较和交换来实现排序的。每次遍历都将最大元素“冒泡”到数组的尾部。
稳定性:稳定。只是相邻发生交换,算法稳定;
时间复杂度:O(n^2)。每次遍历时间复杂度为 O(n),遍历 n 次;


int main() {
//    vector<int> arr = { 3,5,6,2,1,4,8,9,7 };
    vector<int> arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int n= arr.size();
    int flag;
    int k = 1;
    for (int i=n-2; i>=0; --i) {
        flag = true;
        for (int j=0; j<=i; ++j) {
            if (arr[j] > arr[j+1]) {
                flag = false;
                swap(arr[j], arr[j + 1]);
            }
        }
        // 若在这趟比较里发现无交换,说明已经排好序
        if (flag)
            break;
        ++k;
    }

    for (auto &data : arr)
        cout << data << " " ;
    cout << endl;
    cout << k <<endl;

    return 0;
}

2 选择排序

每次从待排序选择最小,放到已排序的末尾,不稳定。
不稳定原因:对于待定位置的元素,它会被交换到待排序序列中,造成不稳定

int main() {
    vector<int> arr = { 3,5,6,2,1,4,8,9,7 };
//    vector<int> arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int n= arr.size();
    int minIndex;

    for (int i=0; i<n-1; ++i) {
        minIndex = i;
        for (int j=i; j<n; ++j) {
            if (arr[j] < arr[minIndex])
                minIndex = j;
        }
        swap(arr[i], arr[minIndex]);
    }

    for (auto &data : arr)
        cout << data << " " ;
    cout << endl;
    return 0;
}

3 堆排序

1)只建一次最大堆;
2)每次调整堆时,需要维护堆的长度;
3)调整堆时,使用选择排序的思想,若有左右孩子节点,从左右孩子节点与自身节点中选取最大——初始时 curIdx = maxIdx,然后 if (lft < heapLen && vec[lft] > vec[maxIdx]) // 容易出错,选择排序思想

#include <iostream>
#include <vector>
using namespace std;

// 对每个非叶子节点建立大顶堆的逻辑:若有左/右孩子,且左右孩子与自身三者的最大元素
// 由于交换后,可能会破坏孩子的【最大堆】性质,因此还要对孩子进行最大堆化处理
void maxHeapify(vector<int> &vec, int curIdx, int heapLen) {
    int lft = 2*curIdx+1, rgt= 2*curIdx+2, maxIdx = curIdx;
    if (lft < heapLen && vec[lft] > vec[maxIdx])  // 容易出错,选择排序思想
        maxIdx = lft;
    if (rgt < heapLen && vec[rgt] > vec[maxIdx])
        maxIdx = rgt;
    if (maxIdx != curIdx) {
        swap(vec[curIdx], vec[maxIdx]);
        maxHeapify(vec, maxIdx, heapLen);
    }
}

// 建大顶堆的逻辑:从最后个非叶子节点向前开始,对每个非叶子节点建立大顶堆
void buildMaxHeap(vector<int> &vec, int heapLen) {
    int n = vec.size();
    for (int i=n/2-1; i>=0; --i) {
        maxHeapify(vec, i, heapLen);
    }
}


int main() {
    vector<int> arr = { 3,5,6,2,1,4,8,9,7 };
    int n = arr.size(), heapLen = n;
    buildMaxHeap(arr, heapLen);
    for (int i=0; i<n-1; ++i) {
        swap(arr[0], arr[heapLen-1]);
        --heapLen;
        maxHeapify(arr, 0, heapLen);
    }
    for (auto &data : arr)
        cout << data << " ";
    cout << endl;
    return 0;
}

归并排序

归并算法是一种分治算法,它的基本思想是将待排序的数组分割成小块,直到每个小块只包含一个元素,然后再将这些小块两两合并,最终得到一个有序的数组。

Leetcode 912. 排序数组

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        // ans.resize(n, 0);
        mergeArr(nums, 0, n-1);
        return nums;
    }

    void mergeArr(vector<int>& nums, int lft, int rgt) {
        if (lft >= rgt)
            return;

        int mid = (lft + rgt) >> 1;
        mergeArr(nums, lft, mid);
        mergeArr(nums, mid+1, rgt);
        vector<int> ans;

        int ptr = lft, qtr = mid + 1;
        while (ptr<=mid && qtr<=rgt) {
            if (nums[ptr] < nums[qtr]) 
                ans.push_back(nums[ptr++]);
            else 
                ans.push_back(nums[qtr++]);
        }
        while (ptr <= mid) 
            ans.push_back(nums[ptr++]);
        
        while (qtr <= rgt)
            ans.push_back(nums[qtr++]);

        for (int i=0; i<= (rgt-lft); ++i) 
            nums[lft+i] = ans[i];
        
    }
};

阐述 malloc 和 new 的区别

malloc 和 new 都是用于在堆(heap)上分配内存的方法,但它们之间存在一些重要的区别,这些区别主要源于它们所属的编程语言和标准库的不同。以下是它们之间的一些主要区别:

所属语言和库:
malloc 是 C 语言标准库函数,定义在 <stdlib.h> 头文件中。
new 是 C++ 的一个操作符,它是 C++ 运算符重载的一部分,用于分配对象。
内存分配和初始化:
malloc 只分配内存,不会初始化内存区域。它返回一个 void* 类型的指针,通常需要显式转换为其他类型的指针。
new 不仅分配内存,还会调用对象的构造函数来初始化内存区域。它返回的是一个指向所分配类型对象的指针,无需类型转换。
处理构造函数和析构函数:
malloc 不了解 C++ 的构造函数和析构函数,因此它不会调用它们。
new 会调用对象的构造函数(如果定义了),并在对象不再需要时(通过 delete 操作符)调用析构函数。
内存释放:
对于 malloc 分配的内存,必须使用 free 函数来释放。
对于 new 分配的对象,必须使用 delete 操作符(或对于数组,使用 delete[])来释放。
异常安全性:
如果 malloc 分配内存失败(例如,由于堆空间不足),它会返回 NULL。这要求程序员检查 malloc 的返回值是否为 NULL,以避免解引用空指针。
new 在分配内存失败时会抛出一个 std::bad_alloc 异常,而不是返回 NULL。这允许使用异常处理机制来处理内存分配失败的情况。
类型安全:
malloc 返回 void*,需要显式转换为目标类型。这可能导致类型不匹配的错误。
new 返回的是指向所请求类型的指针,无需转换,提高了类型安全性。
灵活性:
malloc 允许分配任意大小的内存块(以字节为单位)。
new 主要用于分配对象,但也可以与定位 new(placement new)一起使用,以在已分配的内存块上构造对象。
内存对齐:
malloc 和 new 通常都会考虑内存对齐的问题,但 new 更有可能确保对象按照其类型的对齐要求来分配内存。
调试和诊断:
由于 new 和 delete 是 C++ 的一部分,它们可以与 C++ 的调试和诊断工具(如 Valgrind、AddressSanitizer 等)更好地集成。
使用 malloc 和 free 时,可能需要依赖特定的调试库或工具来检测内存泄漏和其他问题。

指针和数组的区别

  • 本质:数组是一块连续空间,指针是指向连续空间的地址变量;
  • 内存分配:数组在定义时指定固定大小,在编译阶段编译器为其分配内存空间;指针不分配内存,它只存储一个地址;
  • 数据访问:数组使用下标访问元素,指针通过解引用操作符(*)或箭头操作符(->)访问指向的对象;
int arr[4] = [1,2,3,4];
int *ptr = arr; // 数组名指向首元素地址
for (int i=0; i<4; i++)
  cout << *(ptr+i) << endl;

  • 数组名与指针:数组名本质是一个常量指针,指向首元素地址,但数组名不能被赋值或修改,指针可以改变指向。

指针函数与函数指针的区别
指针函数是一个返回值为指针类型的函数,函数指针是指向函数的指针。

1、谈谈友元函数

友元函数不是类的成员函数,却可以访问类的私有 private 和 受保护 protected 成员。
由于友元函数不是类的成员函数,因此它没有 this 指针,故友元函数不能访问非静态数据成员。

使用目的:当两个类存在藕合关系,需要共享私有数据时,可以使用友元函数来简化代码。如果不使用友元函数,就得将另一个类传递进来,并使用 get 函数获取私有成员,比较复杂。
常用友元函数的场景:运算符重载。运算符函数往往需要访问类的私有成员。由于运算符函数不是类的成员函数。
缺点:破坏类的封装性。

1.1 简单使用友元函数示例

class Data {
public:
  Data(int xx = 0) : x(xx) {}
  int GetX() { return x; }    
  friend int Add(Data &a, Data &b);
private:
  int x;
};
// Add 作为友元函数可以访问私有成员
int Add(Data &a,Data &b){
  return a.x + b.x;
}

1.2 运算符重载声明为友元函数的示例

class Point {
public:
  Point(int x=0, int y=0) : x_(x),y_(y) {}
  int getX() const { return x_;}

  // 重载加法运算符,需要声明为友元函数  
  friend Point operator+(const Point& lhs, const Point& rhs); 
private:
  int x_;
  int y_;
}
// 实现友元函数,用于重载 + 运算符
Point operator+(const Point& lhs, const Point& rhs) {
  return Point(lhs.x_ + rhs.x_, lhs.y_ + rhs.y_); 
}

举一反三:为什么赋值运算符不需要声明为友元函数?
因为赋值运算符(=) 在 C++ 里属于类的成员函数 ,已经具备访问私有成员的权限。

谈谈命名空间在 C++ 中的作用和优势
1)避免命名冲突:当时用多个库时,可能会出现多个库用同样的名称,使用命名空间可以将它们区分开来;
2)避免全局污染:使用命名空间可以减少全局命名的使用,有助于避免不必要的全局变量和函数污染

namespace MyNamespace {  
    void printMessage() {  
        std::cout << "Hello from MyNamespace!" << std::endl;  
    }  
}  
int main() {
  MyNamespace::printMessage();  
  return 0;
}

谈谈 C++ final 关键字

1)若修饰类,则说明该类不能被继承

class Base final {  
    // ...  
};  
  
// 下面的代码会编译错误,因为Base是final的  
class Derived : public Base {  
    // ...  
};

2)若修饰虚函数,则说明该虚函数无法被重写

class Base {  
public:  
    virtual void foo() final {  
        // ...  
    }  
    // ...  
};  
  
class Derived : public Base {  
public:  
    // 下面的代码会编译错误,因为foo()在Base中被标记为final  
    void foo() override {  
        // ...  
    }  
};

C/C++ 变量的作用域和生命周期

    1. 局部变量:作用域是函数体内,在函数调用时,变量被创建及初始化,调用结束,变量内存被释放;
  • 2)静态局部变量:作用域是函数体内,编译时变量被分配内存,并保存在静态变量区,程序结束时变量内存释放;
  • 3)全局变量:生命周期是编译阶段直到程序结束。作用域是全局范围,除了声明全局变量的 .c 文件以外,其他 .c 文件只需使用 extern 声明全局变量即可使用全局变量;
  • 4)静态局部变量:生命周期是编译阶段直到程序结束。作用域是当前文件。

谈谈 C++ 的 Lambda 表达式

参考链接:https://learn.microsoft.com/zh-cn/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170

posted @ 2022-07-13 16:21  MasterBean  阅读(49)  评论(0)    收藏  举报