[面经整理](C++篇)

文章目录


基础知识

什么是面向对象编程

在这里插入图片描述
c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的,会有很大影响。

cpp中,将数据data和处理数据的函数都包含在一起(class)创建出一个对象,即为面向对象。 面向对象编程针对的是多个类之间的关系的设计,针对单一class的设计是基于对象编程。
数据和函数(类的方法)都是局部的,不是全局的。
在这里插入图片描述

构造函数和析构函数

构造函数

构造函数:用于创建对象特殊成员函数,当创建对象的时候,系统自动的调用构造函数

作用:

1.类成员变量的初始化静态变量除外,它是在全局数据区进行初始化的

2.为对象分配内存空间

3.请求其他资源

原型:

类名::类名(参数表);

特性:

1.用户没有定义构造函数时,系统会默认生成一个构造函数,默认创建的是一个空函数,如果类中存在其他构造函数,或者我们已经显式的定义了构造函数,那系统就不会自动创建~

2.构造函数名字,不论是系统默认生成,还是人为自定义,需要和类名保持一致~

3.构造函数可以有任意类型的参数,但是没有返回值~

4.构造函数可以进行函数重载~

析构函数

析构函数一般是在对象的生命周期结束的时候被调用

作用:

对对象一些资源进行回收,像内存释放,还有清除对象

三种条件下,会调用析构函数:

1.实例化对象的生命周期结束的时候~

2.delete指向对象的指针时~

3.对象A是对象B的成员,B的析构函数被调用时,对象A的析构函数也被调用~

原型:

类名::~类名();

特性:

1.析构函数没有参数,也没有返回类型~

2.因为无参数,无返回值,所以不可以重载~

3.没有用户定义析构函数的时候,系统会自动生成析构函数~

什么是类的继承?

  1. 继承,表示is-a,所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属 性和方法,被称为子类或者派生类,被继承的类称为父类或者基类
  2. 父类的数据会被完整继承下来。子类拥有自己的以及父类的数据。子类可以调用父类的数据和函数,即继承了函数(实际上是继承了函数的调用权)。(数据的继承可以从内存的角度来理解,函数的继承不是从内存的角度,而是从调用权的角度)
  3. 继承和虚函数搭配是关键,在任何成员函数之前加上virtual关键字,即为虚函数。虚函数,希望子类重新定义它,且已有默认定义。纯虚函数,希望子类重新定义它,且目前没有默认定义,一定要去定义。
    在这里插入图片描述

在这里插入图片描述
构造时,先调用父类的构造函数,然后再调用自己的。
析构时,先析构自己,然后析构父类的。
编译器会自动完成。父类的析构函数必须是虚函数(待解),否则。。。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

为什么需要虚继承

多继承 共享基类
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

虚基类表
在这里插入图片描述

在这里插入图片描述

override 和 overload

1、override是重写(覆盖)了⼀个⽅法
以实现不同的功能,⼀般是⽤于⼦类在继承⽗类时,重写⽗类⽅法。
规则:

  1. 重写⽅法的**参数列表,返回值,**所抛出的异常与被重写⽅法⼀致
  2. 被重写的⽅法不能为private
  3. 静态⽅法不能被重写为⾮静态的⽅法
  4. 重写⽅法的访问修饰符⼀定要⼤于被重写⽅法的访问修饰符(public>protected>default>private)

2、overload是重载,这些⽅法的名称相同⽽参数形式不同
⼀个⽅法有不同的版本,存在于⼀个类中。
规则:
5. 不能通过访问权限、返回类型、抛出的异常进⾏重载
6. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不⼀样)
7. ⽅法的异常类型和数⽬不会对重载造成影响

方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,而重写实现的是运行时的多态性重载发生在一个类中同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问。

介绍多态

多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式

多态存在的三个必要条件
一、要有继承;
二、要有重写;
三、父类引用或者指针指向子类对象。

多态分为静态多态和动态多态

  1. 静态多态 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载运算符重载泛型编程等。
  2. 动态多态 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。

在这里插入图片描述在这里插入图片描述

简述一下面向对象的三大特征

面向对象的三大特征是:封装、继承、多态

  1. 封装数据操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互

封装本质上是一种管理:比如景区,如果人人都能随意进来,那么很容易造成问题,比如损坏公物。所以我们需要建一堵围墙将景区围起来,但是我们的目的不是不让别人进去,所以开放了售票通道,可以买票在合理的监管机制下进去游玩。 C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。 - private 修饰的成员只能在本类中访问 - protected 表示受保护的权限,修饰的成员只能在本类或者子类中访问 - public 修饰的成员是公共的,哪儿都可用访问。 封装的好处:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。

  1. 继承 C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个 B 类继承于 A 类,或称从类 A 派生类 B。这样的话,类 A 成为基类(父类), 类 B 成为派生类(子类)。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。 继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。

  2. 多态 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。

虚函数

在这里插入图片描述
在这里插入图片描述
C++虚函数表,虚表指针,内存分布

在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述

C++类的虚函数表和虚函数的关系
关系:虚函数表指针->虚函数表->虚函数

C++类的虚函数表指针在内存中的位置
虚函数表指针是虚函数表所在位置的地址。虚函数表存放在全局数据区.

虚函数表是class specific的,也就是针对一个类来说的,这里有点像一个类里面的static成员变量,即它是属于一个类所有对象的,不是属于某一个对象特有的,是一个类所有对象共有的。
在这里插入图片描述

抽象类与接⼝的实现

在这里插入图片描述在这里插入图片描述

gcc编译过程

GCC编译C/C++程序过程
GCC编译C/C++程序过程

GCC 编译器并未提供给用户可用鼠标点击的界面窗口,要想调用 GCC 编译器编译 C 或者 C++ 程序,只能通过执行相应的 gcc 或者 g++ 指令。实际上,C 或者 C++ 程序从源代码生成可执行程序的过程需经历 4 个过程,分别是预处理、编译、汇编和链接。

同样,使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历这 4 个过程。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。可以一步生成可执行程序。

1、预处理

通过为 gcc 指令添加 -E 选项,即可控制 GCC 编译器仅对源代码做预处理操作。预处理操作,进行头文件展开,宏定义展开、删除注释等工作主要是处理那些源文件和头文件中以 # 开头的命令(比如 #include、#define、#ifdef 等),并删除程序中所有的注释 // 和 /* … */

2、编译

编译是整个程序构建的核心部分,也是最复杂的部分之一。所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。通过给 gcc 指令添加 -S(注意是大写)选项,即可令 GCC 编译器仅将指定文件加工至编译阶段,并生成对应的汇编代码文件
3、汇编

汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。汇编的作用是生成目标文件

4、链接

得到生成目标文件之后,接下来就可以直接使用 gcc 指令继续执行链接操作。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

为什么C/C++语言使用指针?

  1. 一方面,每一种编程语言都使用指针。不止C/C++使用指针。

每一种编程语言都使用指针。C++将指针暴露给了用户(程序员),而Java和C#等语言则将指针隐藏起来了。

辅助解释:(1)指针本身就是内存地址的抽象化内存地址本身是计算机不可避免的,这也是有的人强调“指针天然存在”、“指针就是内存地址”的原因,这揭示了指针的本质当然严格从语言规范来说,这一描述有问题。
(2)因此不是c语言引入了指针,而是常见的高级语言都隐藏了指针以降低编程难度。同时牺牲了部分高性能运算能力。

  1. C++中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等
  2. 能较方便的使用字符串:把字符串第一个元素地址赋给一个指针,然后在末位加一个终止标识符’\0’。这样可以方便保持各种长短不一的字符串,而不需要开辟一个固定大小的字符串数组
  3. 能动态分配内存,实现内存的自由管理;动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
    补充
  4. 指针直接与数据的储存地址有关,比如:值传递不如地址传递高效

引用和指针有什么区别?

对象 可变 底层 自增

一版

  1. 指针存放某个对象的地址,其本身就是变量命了名的对象),本身就有地址,需要分配内存空间,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变
  2. 引⽤就是变量的别名必须初始化,并且不能够改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变
  3. 引用的底层是通过指针实现的;
    理论上引用不占内存空间而指针占用内存空间,但实际上
    编译器是使用指针常量来实现引用的
    *,只是把这一细节对上层屏蔽了。引用(本质是*const)。
  4. 不存在指向空值的引⽤,但是存在指向空值的指针
  5. 指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量 值加 1)

二版
6. 本质上,指针是一个实体,需要分配内存空间。引用只是变量的别名
7. 引用的底层是通过指针实现的;
理论上引用不占内存空间而指针占用内存空间,但实际上编译器是使用指针常量来实现引用的,只是把这一细节对上层屏蔽了。引用(本质是*const)。
8. 引用在定义的时候必须进行初始化,并且不能够改变指针在定义的时候不一定要 初始化,并且指向的空间可变。(注:引用不能指向空值)
解释:指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变
在这里插入图片描述
9. 指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量 值加 1)
10. sizeof 引用得到的是所指向的变量(对象)的大小,而 sizeof 指针得到的是指针 本身的大小。(引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针(地址)本身的大小

值传递与引用传递

一版

  1. 用户自定义的类型
    如果输入参数为引用,加上const 之后不但起到了保护作用,也提高了程序效率。
    例如,

    void func(A a); //这里的A类型为用户定义的类型

这个函数在调用的时候会生成一个临时对象,而且会调用拷贝构造函数,函数结束还要析构。如果改成引用void func(A &a); 只是相当于实参的一个别名,不会产生临时变量。所以,如果是自定义类型,建议用引用作为函数形参。
但是,即使用引用,如果只是作为输入用,最好加上const 修饰,这样就不会改变实参的值,如果刻意修改,编译器会报错。所以,函数最后改成void func(const A& a);
当然,不是所有的数据类型都需要用应用,例如,

void func(int x);
  1. 内部类型
    对于内部类型的参数,不存在构造、析构的过程,产生临时变量、值的复制过程很快,无需用引用

二版

  1. 关于值传递
    值传递:是指在调用函数时,将实际参数复制一份传递到函数中这样在函数中如果对参数进行修改,就不会影响到实际参数

如下图所示,当传递参数之前会将参数进行复制,函数中修改了参数,不会影响实际参数
在这里插入图片描述
值传递是对于是对基本数据而言,例如下面例子,number没有改变。
在这里插入图片描述

  1. 关于引用传递

引用传递:是指在调用函数时,将实际参数的地址传递到函数中那么在函数中对参数进行修改,将会影响到实际参数

引用数据类型分为两个部分,引用变量和对象,这两个部分放在不同的地方,引用变量在栈中,而对象是放在堆内存中的,引用变量指向对象。
如下图所示,当传递参数之前会将参数进行复制,但是复制的是引用变量,复制后的引用变量还是指向内存中的同一对象,所以引用传递中,函数修改了参数会影响实际参数
在这里插入图片描述

智能指针

在这里插入图片描述

风险:内存泄漏,使用已经销毁的内存

在这里插入图片描述

解决方案:智能指针

unique_ptr
1、unique_ptr”唯⼀”拥有其所指对象
同⼀时刻只能有⼀个unique_ptr指向给定对象离开作⽤域时,若其指向对象,则将其所指对象销毁(默认
delete)。

2、定义unique_ptr时
需要将其绑定到⼀个new返回的指针上。
3、unique_ptr不⽀持普通的拷⻉和赋值(因为拥有指向的对象)
但是可以拷⻉和赋值⼀个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从⼀个(⾮const)
unique_ptr转移到另⼀个unique。

在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
weak_ptr
1、weak_ptr是为了配合shared_ptr⽽引⼊的⼀种智能指针
它的最⼤作⽤在于协助shared_ptr⼯作,像旁观者那样观测资源的使⽤情况,但weak_ptr没有共享资源,它的构造
不会引起指针引⽤计数的增加。

2、和shared_ptr指向相同内存
shared_ptr析构之后内存释放,在使⽤之前使⽤函数lock()检查weak_ptr是否为空指针。
在这里插入图片描述在这里插入图片描述

const

const在C/C++中重要性很大,如果单纯理解为“常量”那就错了,这一篇总结一下C++中const的详细的用法。

一、const 修饰变量
它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的安全性和可靠性。
在这里插入图片描述
在这里插入图片描述
二、const 修饰函数

  1. 修饰函数形参

当输入参数为用户自定义类型和抽象数据类型时,将“值传递”改为“const &传递”,可以提高效率。

例如:

void fun(A a);  //fun函数 **产生A类型的临时变量用于复制参数a,临时变量的构造、复制、析构过程都将产生时间损耗。**

void fun(A const &a); // **“引用传递”不需要产生临时对象,节省了时间,但在应用过程有可能改变a,故加const。**
  1. 修饰函数返回值

给“指针传递”的函数返回值加const,则返回值不能被修改,且该返回值只能被赋值给加const修饰的同类型指针。

const int *Getvalue(void){};

int *iv=Getvalue();//error

const int *iv=Getvalue();//correct

如果返回值为引用,同上面形参,都可以提高效率。

  1. const 修饰成员函数
    const 修饰的成员函数为了保护成员变量,要求const 函数不能修改成员变量,否则编译会报错。
    声明的时候const放在函数最后,例如,
class MyClass {
public:
    void func(int x) const;
};

const和#define

在这里插入图片描述

static

面向对象
在成员变量或函数前加static关键字,则变为静态函数/变量
在这里插入图片描述

  1. 静态数据成员

为什么要引入static?

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在 栈上分配空间 ,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

作⽤实现多个对象之间的数据共享 ,并且使⽤静态成员不会破坏类的封装性;默认初始化为0
静态局部变量在全局数据区分配内存;局部变量在栈区分配内存

//Example 5
#include <iostream.h>
class Myclass
{
public:
   Myclass(int a,int b,int c);
   void GetSum();
private:
   int a,b,c;
   static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员

Myclass::Myclass(int a,int b,int c)
{
   this->a=a;
   this->b=b;
   this->c=c;
   Sum+=a+b+c;
}

void Myclass::GetSum()
{
   cout<<"Sum="<<Sum<<endl;
}

void main()
{
   Myclass M(1,2,3);
   M.GetSum();
   Myclass N(4,5,6);
   N.GetSum();
   M.GetSum();

}
  1. 静态成员函数

静态函数 没有this pointer参数,因此不能直接处理普通的对象,只能处理静态数据

//Example 6
#include <iostream.h>
class Myclass
{
public:
   Myclass(int a,int b,int c);
   static void GetSum();/声明静态成员函数
private:
   int a,b,c;
   static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员

Myclass::Myclass(int a,int b,int c)
{
   this->a=a;
   this->b=b;
   this->c=c;
   Sum+=a+b+c; //非静态成员函数可以访问静态数据成员
}

void Myclass::GetSum() //静态成员函数的实现
{
  // cout<<a<<endl; //错误代码,a是非静态数据成员
   cout<<"Sum="<<Sum<<endl;
}

void main()
{
   Myclass M(1,2,3);
   M.GetSum();
   Myclass N(4,5,6);
   N.GetSum();
   Myclass::GetSum();
}

在这里插入图片描述

  1. 什么时候用static?

    需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

extern

声明外部变量【在函数或者⽂件外部定义的全局变量】
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

从汇编层去解释一下引用

在这里插入图片描述
在这里插入图片描述
所以,引用类型的变量会占用内存空间,占用的内存空间的大小和指针类型的大小是相同的。

STL

STL实现原理及其实现

在这里插入图片描述
容器(container):常用数据结构,大致分为两类,顺序容器,如vector,list,deque,关联容器,如set,map。在实现上,是类模板(class template)
容器适配器(adapter):将一种容器修饰为功能不同的另一种容器,如以容器vector为基础,在其上实现stack,stack的行为也是一种容器。这就是一种配接器。除此之外,还有迭代器适配器和仿函数适配器。

STL提供了六⼤组件,彼此之间可以组合套⽤,这六⼤组件分别是:容器、算法、迭代器、仿函数、适配器(配接
器)、空间配置器。

STL六⼤组件的交互关系:

  1. 容器通过空间配置器取得数据存储空间
  2. 算法通过迭代器存储容器中的内容 ,迭代器是一种泛化的指针
  3. 仿函数可以协助算法完成不同的策略的变化
  4. 适配器可以修饰仿函数

容器

各种数据结构,如vector、list、deque、set、map等,⽤来存放数据,从实现⻆度来看,STL容器是⼀种class template。
算法
各种常⽤的算法,如sort、find、copy、for_each。从实现的⻆度来看,STL算法是⼀种function tempalte.
迭代器
扮演了容器与算法之间的胶合剂,共有五种类型,从实现⻆度来看, 迭代器是⼀种将operator , operator-> , operator++,operator–等指针相关操作予以重载的class template.。*

所有STL容器都附带有⾃⼰专属的迭代器,只有容器的设计者才知道如何遍历⾃⼰的元素。

原⽣指针(native pointer)也是⼀种迭代器。

仿函数

⾏为类似函数,可作为算法的某种策略。从实现⻆度来看,仿函数是⼀种重载了operator()的class 或者class template
适配器
⼀种⽤来修饰容器或者仿函数或迭代器接⼝的东⻄。

STL提供的queue 和 stack,虽然看似容器,但其实只能算是⼀种容器配接器,因为它们的底部完全借助deque,
所有操作都由底层的deque供应。

空间配置器
为容器提供空间配置和释放,对象构造和析构的服务,也是一个class template负责空间的配置与管理。从实现⻆度看,配置器是⼀个实现了动态空间配置、空间管理、空间释放的class tempalte.
⼀般的分配器的std:alloctor都含有两个函数allocate与deallocte,这两个函数分别调⽤operator new()与
delete(),这两个函数的底层⼜分别是malloc()and free()
;但是每次malloc会带来格外开销(因为每次malloc⼀个元
素都要带有附加信息)

在这里插入图片描述

C++模板是什么,底层怎么实现的?

模板是泛型编程的基础泛型编程即以一种独立于任何特定类型的方式编写代码。

  1. 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具 体类型产生不同的函数编译器会对函数模板进行两次编译在声明的地方对模板 代码本身进行编译,在调用的地方对参数替换后的代码进行编译
  2. 这是因为函数模板被实例化后才能成为真正的函数,在使用函数模板的源文件中 包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例 化该模板,最终导致链接错误。

说一说 STL 中有哪些常见的容器

容器之间的实现关系以及分类:

在这里插入图片描述

在这里插入图片描述

容器种类:
1.顺序容器Sequence Containers

Array(固定元素个数)C++11
Vector(尾部个数可以扩充)
Deque(头尾个数可以扩充)
List(双向链表)
Forward-List(单向链表)C++11

2.关联容器Associative Containers:元素有key和value,适合做快速的查找(用key来找value)
底层实现是红黑树,可以自动左右平衡,是一种高度平衡的二叉树

Set/Multiset(key=value)
Map/Multimap(key对应value;multi map 允许重复元素,map不允许有重复)

不定序容器Unordered Containers(属于关联容器)
HashTable Separate chaining(不定序容器使用hashtable):同放一个内存,内存放这几个数据的链表

STL 中容器分为顺序容器关联式容器、容器适配器三种类型,三种类型容器特性分别如下:

  1. 顺序容器 容器并非排序的元素的插入位置同元素的值无关,包含 vector、deque、list。 - vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。 - deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。 - list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。

  2. 关联式容器 元素是排序的插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。 - set/multiset set中不允许相同元素,multiset 中允许存在相同元素。 - map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。

  3. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。 - stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。 - queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。 - priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。

vector容器实现与扩充

1. 底层实现
Vector在堆中分配了⼀段连续的内存空间来存放元素

1、三个迭代器
(1)first : 指向的是vector中对象的起始字节位置
(2)last : 指向当前最后⼀个元素的末尾字节
(3)end : 指向整个vector容器所占⽤内存空间的末尾字节
在这里插入图片描述
2. 扩容过程

如果集合已满,在新增数据的时候,就要分配⼀块更⼤的内存,将原来的数据复制过来,释放之前的内存,在插⼊新增的元素
所以对vector的任何操作,⼀旦引起空间重新配置,指向原vector的所有迭代器就都失效了

size() 和 capacity()
(1)堆中分配内存,元素连续存放,内存空间只会增⻓不会减少
vector有两个函数,⼀个是capacity(),在不分配新内存下最多可以保存的元素个数,另⼀个size(),返回当前已经
存储数据的个数

(2)对于vector来说,capacity是永远⼤于等于size的
capacity和size相等时,vector就会扩容,capacity变⼤(翻倍)
在这里插入图片描述在这里插入图片描述

list-链表

每个元素都是放在⼀块内存中,他的内存空间可以是不连续的通过指针来进⾏数据的访问

在哪⾥添加删除元素性能都很⾼,不需要移动内存,所以常⽤来做随机插⼊和删除操作容器
在这里插入图片描述
list是⼀个环状的双向链表,同时它也满⾜STL对于“前闭后开”的原则,即在链表尾端可以加上空⽩节点

list的迭代器的设计:

迭代器是泛化的指针所以⾥⾯重载了->,–,++,*,+=, -=,等运算符,同时迭代器是算法与容器之间的桥梁,算法需要了解容器的⽅⽅⾯⾯,于是就诞⽣了5种关联类型,(这5种类型是必备的,可能还需要其他类型)我们知道算法传⼊的是迭代器或者指针,算法根据传⼊的迭代器或指针推断出算法所想要了解的容器⾥的5种关联类型的相关信息。由于光传⼊指针,算法推断不出来其想要的信息,所以我们需要⼀个中间商(萃取器)也就是我们所说的iterator traits类,对于⼀般的迭代器,它直接提供迭代器⾥的关联类型值,⽽对于指针和常量指针,它采⽤的类模板偏特化,从⽽提供其所需要的关联类型的值。
在这里插入图片描述在这里插入图片描述

deque-双端数组

⽀持快速随机访问, 容器deque内部是分段连续的,对使用者表现为连续的,由于deque需要处理内部跳转,因此速度上没有vector快
在这里插入图片描述
1、deque概述:
deque是⼀个双端开⼝连续线性空间,其内部为分段连续的空间组成,随时可以增加⼀段新的空间并链接

注意:
由于deque的迭代器⽐vector要复杂,这影响了各个运算层⾯,所以除⾮必要尽量使⽤vector;为了提⾼效率,在
对deque进⾏排序操作的时候,我们可以先把deque复制到vector中再进⾏排序最后在复制回deque

在这里插入图片描述

deque控制中心:
deque::map的类型为二重指针T****,称为控制中心,控制中心是一个vector,其中每个元素指向一个buffer**,这里的名为map的vector扩充的时候是copy到中段,使得左右都有空间

坏处:其迭代器变得很复杂

deque迭代器:

迭代器deque::iterator的核心字段是4个指针:cur指向当前元素first和last分别指向当前buffer的开始和末尾node指向控制中心.

deque迭代器包含4个指针,即16个字节,一个deque包含start(deque 迭代器,16),finish(deque 迭代器,16),map(4),mapsize(4)一共40字节

template<class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator {
    // 定义5个关联类型
    typedef random_access_iterator_tag	iterator_category; 	// 关联类型1
    typedef T 							value_type;       	// 关联类型2
    typedef ptrdiff_t 					difference_type;	// 关联类型3
    typedef Ptr 						pointer;			// 关联类型4
    typedef Ref 						reference;			// 关联类型5

    typedef size_t size_type;
    typedef T **map_pointer;
    typedef __deque_iterator self;

    // 迭代器核心字段:4个指针
    T *cur;     		// **指向当前元素**
    T *first;   		// 指向当前buffer的开始
    T *last;    		// 指向当前buffer的末尾
    map_pointer node;   // **指向控制中心**
    // ...
};

heap and priority_queue

heap(堆):
heap是STL中的堆数据结构,堆其实是一种完全二叉树,即非叶子节点的左右孩子必不为空的二叉树。堆分为最大堆和最小堆,最大堆的特点就是堆顶的元素必定是所有元素中的最大值,而最小堆的特点就是堆顶元素的值必定是所有元素中的最小值,比如下面的结构就是一个最大堆。

在这里插入图片描述
STL底层使用vector来储存堆中的元素. 比如图中的最大堆会被储存为[23, 20, 13, 3, 10, 4],这是由于堆是完全二叉树,因此第i个元素的左孩子节点必定在第2i+1处,右孩子必定在第2i+2处,如果左右孩子的下标超过了vector中元素个数,则说明对应的孩子节点为空。

set和multiset

1.容器set和multiset以rb_tree为底层容器,因此其中元素是自动排序的,排序的依据是key. set和multiset元素的value和key一致.

2.set和multiset提供迭代器iterator用以顺序遍历容器,无法使用iterator改变元素值value,因为set和multiset使用的是内部rb_tree的const_iterator.

优点:

查找某⼀个数的时间为O(logn);遍历时采⽤iterator,效果不错。

缺点:
每次插⼊值的时候,都需要调整红⿊树,效率有⼀定影响。

map与unordered_map

底层实现:

map底层是基于红⿊树实现的,因此map内部元素排列是有序的
⽽unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱⽆序的

在这里插入图片描述

hashtable

空间足够时,编号有足够的空间独立放置;空间不足时,可能有几个编号对应到一个位置(冲突);哈希表做法:如果几个编号碰撞,那就吧他们串在一起

如果链表太长就要打散(所以又名散列表)

hashtable最开始只有53个桶, 当元素个数大于桶的个数时,桶的数目扩大为最接近当前桶数两倍的质数,实际上,桶数目的增长顺序被写死在代码里。
在这里插入图片描述

STL 容器用过哪些,查找的时间复杂度是多少,为什么?

  1. vector
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
  2. 双向队列deque

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  1. 双向链表list

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  1. set
    在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述

hash_map, hash_set, hash_multimap, and hash_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为:

插入:O(1),最坏情况O(N)。

查看:O(1),最坏情况O(N)。

删除:O(1),最坏情况O(N)。

记住,如果你采用合适的哈希函数,你可能永远不会看到最坏情况。但是记住这一点是有必要的。

在不碰撞的情况下,hash_map是所有数据结构中查找最快的,它是常数级的。

注意我的前提:“在不碰撞的情况下”,其实换句话说,就是要有足够好的hash函数,它要能使key到value的映射足够均匀,否则,在最坏的情况下,它的计算量就退化到O(N)级,变成和链表一样。
如果说 hash_map 是所有容器中最慢的,也只能说:“最拙劣的hash函数”会使hash_map成为查找最慢的容器。但这样说意义不大,因为,最凑巧的排列能使冒泡排序成为最快的排序算法。

vector 与 list 的区别与应用?怎么找某 vector 或者 list 的倒数第二 个元素

一版

vector和list的区别

  1. vector 底层实现是数组;list是双向链表
  2. vector拥有一段连续的内存空间,并且起始地址不变。因此能高效的 进行随机访问,时间复杂度为 O(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为 O(n)
  3. list 数据结构 list 是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所 以 list 的随机访问非常没有效率,时间复杂度为 O(n);但由于链表的特点,能高效 地进行插入和删除
  4. vector⼀次性分配好内存,不够时才进⾏翻倍扩容;list每次插⼊新节点都会进⾏内存申请

二版

  1. vector 数据结构 vector 和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的 进行随机存取,时间复杂度为 o(1);但因为内存空间是连续的,所以在进行插入和 删除操作时,会造成内存块的拷贝,时间复杂度为 o(n)。另外,当数组中内存空间 不够时,会重新申请一块内存空间(大概两倍大小)并进行内存拷贝。连续存储结构:vector 是可以 实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操 作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组array最大的区 别就是 vector 不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态 增长,而数组需要程序员手动写入扩容函数进形扩容。

  2. list 数据结构 list 是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所 以 list 的随机存取非常没有效率,时间复杂度为 o(n);但由于链表的特点,能高效 地进行插入和删除。非连续存储结构:list 是一个双链表结构,支持对链表的双向 遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向 下一个元素的节点(next)。因此 list 可以高效率的对数据元素任意位置进行访问 和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。

  3. 区别: vector 的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易 操作。list 的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除 操作等都比较方便,改变指针的指向即可。vec tor 中的迭代器在使用后就失效了,而 list 的迭代器在使用之后还可以继续使用。

  4. int mySize = vec.size();vec.at(mySize -2); list 不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问 list 里的元素只能遍历,不过你要是只需要访问 list 的最后 N 个元素的话,可以用反向 迭代器来遍历

map、set 是怎么实现的,红黑树是怎么能够同时实现这两种容 器? 为什么使用红黑树?

  1. 他们的底层都是以红黑树的结构实现,因此插入删除等操作都在 O(logn)时间内完 成,因此可以完成高效的插入删除;
  2. **容器set和multiset以rb_tree为底层容器,**因此其中元素是自动排序的,排序的依据是key.set和multiset元素的value和key一致.容器map和multimap以rb_tree为底层容器,因此其中元素是自动排序的,排序的依据是key.
  3. 因为 map 和 set 要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比 较低。

Vector和Deque

在这里插入图片描述

STL 中 unordered_map(hash_map)和 map 的区别,hash_map 如 何解决冲突以及扩容

一版
map与unordered_map
map中元素是⼀些key-value对,关键字起索引作⽤,值表示和索引相关的数据。
底层实现:
map底层是基于红⿊树实现的,因此map内部元素排列是有序的。
⽽unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱⽆序的。
map:
优点:
有序性,这是map结构最⼤的优点,其元素的有序性在很多应⽤中都会简化很多的操作。
map的查找、删除、增加等⼀系列操作时间复杂度稳定,都为O(logn )。
缺点:
查找、删除、增加等操作平均时间复杂度较慢,与n相关
unordered_map:
优点:
查找、删除、添加的速度快,时间复杂度为常数级O(1)。
缺点:
因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占⽤率⾼。
unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(1),取决于哈希函数。极端情况下可能为
O(n)。

二版

  1. unordered_map 和 map 类似,都是存储的 key-value(键值对) 的值,可以通过 key 快速索引 到 value。不同的是 unordered_map的底层实现是 hash_table,unordered_map 不会根据 key 的大小进行排序;而map的底层实现是红黑树,根据key 的大小自动排序的
  2. 所以使用时 map 的 key 需要定义 operator<。而 unordered_map 需要定义 hash_value 函数并且重载 operator==。但是很多系统内置的数据类型都自带这些
  3. 那么如果是自定义类型,那么就需要自己重载 operator<或者 hash_value()了。
  4. 解决冲突:
    在这里插入图片描述
    空间足够时,编号有足够的空间独立放置;空间不足时,可能有几个编号对应到一个位置(冲突)哈希表做法:如果几个编号碰撞,那就吧他们串在一起。当元素个数大于桶的个数时,桶的数目扩大为最接近当前桶数两倍的质数,实际上,桶数目的增长顺序被写死在代码里。

注:如果对象object是整数则可以将其自身当做编号,不然需要hashFunc计算其编号hashcode,然后利用hashcode除以bucket的个数得到最终位置

STL 中 hash_map 扩容发生什么?

  1. hash table 表格内的元素称为桶(bucket),而由桶所链接的元素称为节点(node), 其中存入桶元素的容器为 stl 本身很重要的一种序列式容器——vector 容器。之所以 选择vector为存放桶元素的基础容器,主要是因为vector容器本身具有动态扩容能力, 无需人工干预。
  2. 重新计算各个节点的hashcode并用list链接

怎样判断两个浮点数是否相等?

  1. 对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比 较反而是不相等!计算机表示浮点数(float或double类型)都有一个精度限制,对于超出了精度限制的浮点数,计算机会把它们的精度之外的小数部分截断。因此,本来不相等的两个浮点数在计算机中可能就变成相等的了。

  2. 对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要 取绝对值!浮点数与 0 的比较也应该注意。与浮点数的表示方式有关。
    abs( fa - fb) < 0.000001

在不使用额外空间的情况下,交换两个数?

算术:
在这里插入图片描述
异或:
在这里插入图片描述

迭代器++it,it++哪个好,为什么

在这里插入图片描述

虚函数与纯虚函数的区别在于

  1. 纯虚函数只有定义没有实现,虚函数既有定义又有实现;
  2. 含有纯虚函数的类不能定义对象,含有虚函数的类能定义对象;

C++中类成员的访问权限和继承权限问题。

  1. 三种访问权限
  1. public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被 访问,在类外也是可以被访问的,是类对外提供的可访问接口
  2. private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问, 在类体外是隐藏状态
  3. protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是 隐藏状态但是对于该类的派生类来说,相当于公有成员,在派生类中可以被 访问
  1. 三种继承方式
  1. 若继承方式是 public,基类成员在派生类中的访问权限保持不变,也就是说, 基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
  2. 若继承方式是 private,基类所有成员在派生类中的访问权限都会变为私有(p rivate)权限
  3. 若继承方式是 protected,基类的共有成员和保护成员在派生类中的访问权限 都会变为保护(protected)权限私有成员在派生类中的访问权限仍然是私有(p rivate)权限

内存管理

内存有哪几种类型

在这里插入图片描述

C++堆和栈的区别?

在这里插入图片描述
**注意:**区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方式类似于链表
它与数据结构中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

new和malloc的区别

  1. malloc与free是C++/C语⾔的标准库函数,new/delete是C++的运算符
    在这里插入图片描述

  2. new返回的是某种数据类型指针;malloc 返回的是 void 指针

  3. 使用new操作符申请内存分配时无须指定内存块的大小编译器会根据类型信息自行计算;使用malloc则需要显式地指出所需内存的尺寸。

  4. new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数;malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数。在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数delete会调用对象的析构函数,然后调用free回收内存。

  5. new操作符从⾃由存储区上为对象动态分配内存空间,⽽malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存

  6. new、delete 是操作符,可以重载;malloc、free 是函数,malloc/free并不允许重载,可以重写(覆盖)。

C/C++什么是内存泄露,内存泄露如何避免?

一版

1、什么是内存泄露?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存
在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪
费。
可以使⽤Valgrind, mtrace进⾏内存泄漏检查。
2、内存泄漏的分类
(1)堆内存泄漏 (Heap leak)
对内存指的是程序运⾏中根据需要分配通过malloc,realloc new从堆中分配的⼀块内存,再是完成后必须通过
⽤对应的 free或者 delete 删掉
。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被
使⽤,就会产⽣ Heap Leak.

(2)系统资源泄露(Resource Leak)

主要指程序使⽤系统分配的资源⽐如 Bitmap,handle ,SOCKET 等没有使⽤相应的函数释放掉,导致系统资源的浪
费,严᯿可导致系统效能降低,系统运⾏不稳定。

(3)没有将基类的析构函数定义为虚函数

基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没
有正确是释放,因此造成内存泄露。
3、什么操作会导致内存泄露?
指针指向改变,未释放动态分配内存。
4、如何防⽌内存泄露?

  1. 类中有指针,必须写拷贝构造函数和拷贝赋值,不然会内存泄漏或者程序崩溃。
  2. 使⽤智能指针
  3. 将析构函数设置为虚函数,当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类
    析构函数
    ,导致派⽣类资源⽆法释放,造成内存泄漏。

5、智能指针有了解哪些?
智能指针是为了解决动态分配内存导致内存泄露和多次释放同⼀内存所提出的,C11标准中放在< memory>头⽂
件。包括:共享指针,独占指针,弱指针
6、构造函数,析构函数要设为虚函数吗,为什么?
(1)析构函数
析构函数需要。当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类
析构函数
,导致派⽣类资源⽆法释放,造成内存泄漏。
(2)构造函数
构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝不知道对象的确
切类型
。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构
造函数不应该被定义为虚函数。

二版

  1. 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。最终的结果就是导致OOM。
  2. 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
    ————————————————

类中有指针,必须写拷贝构造和拷贝赋值,不然会内存泄漏。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
浅拷贝的问题:p2的析构函数调用以后已经释放堆区0x0011,p1的析构函数再次调用会造成内重复释放;另外一个问题就是上例中的内存泄漏问题。
在这里插入图片描述

在这里插入图片描述
1.我们需要的是深拷贝。
2.拷贝另一个string指针指向的字符串内容。
在这里插入图片描述在这里插入图片描述
1.拷贝赋值,如果两边目前都有东西(比如左边拷贝到右边),需要先把右边清空,然后右边创建同样大小的内存,然后把左边内容拷贝到右边来。
2.然后先把数据清空,然后新建指定大小的数组。然后把字符串内容复制过来。
3.如上图所示,在重载“=”赋值运算符时需要检查自我赋值,原因如下:
在这里插入图片描述
如果没有自我赋值检测,那么自身对象的m_data将被释放,m_data指向的内容将不存在,所以该拷贝会出问题。

深拷贝与浅拷贝

  1. 浅拷贝和深拷贝最根本的区别在于浅拷贝只是获取一个对象“引用”,而深拷贝会另外申请空间来储存数据,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是引用。

  2. 当数据成员中含有指针时,必须用深拷贝。如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅仅拷贝的是这个指针变量的值,也就是在目标对象中该指针类型数据和源对象中的该成员指向的是同一块堆空间。这样会带来一个问题,就是在析构函数中释放该堆区数据,会被释放多次。默认的拷贝构造函数和默认的赋值运算符重载函数都是浅拷贝。

浅拷贝和深拷贝一般在拷贝构造函数赋值运算符重载函数中涉及到。

类中有指针,必须写拷贝构造和拷贝赋值,不然会内存泄漏或者程序崩溃。
在这里插入图片描述1. 右下角的“world\0”还在,但是没有指针指着他了,变成孤儿了,发生内存泄漏
3. 默认的是浅拷贝。总结:如果属性有在堆区开辟的(比如指针),一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
在这里插入图片描述
浅拷贝的问题:p2的析构函数调用以后已经释放堆区0x0011,p1的析构函数再次调用会造成内重复释放;另外一个问题就是上例中的内存泄漏问题。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  1. 深拷贝 深拷贝在拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样指针成员就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了拷贝的目的,还不会出现问题,两个对象先后去调用析构函数,分别释放自己指针成员所指向的内存。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

1.我们需要的是深拷贝。
2.拷贝另一个string指针指向的字符串内容。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.拷贝赋值,如果两边目前都有东西(比如左边拷贝到右边),需要先把右边清空,然后右边创建同样大小的内存,然后把左边内容拷贝到右边来。
2.然后先把数据清空,然后新建指定大小的数组。然后把字符串内容复制过来。
3.如上图所示,在重载“=”赋值运算符时需要检查自我赋值,原因如下:

在这里插入图片描述
如果没有自我赋值检测,那么自身对象的m_data将被释放,m_data指向的内容将不存在,所以该拷贝会出问题。

C++11新特性

左值和右值

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

参考

右值引⽤

在这里插入图片描述在这里插入图片描述

c++为什么用右值引用?
右值引用是C++11中最重要的新特性之一,它解决了C++中大量的历史遗留问题,使C++标准库的实现在多种场景下消除了不必要的额外开销(如std::vector, std::string),也使得另外一些标准库(如std::unique_ptr, std::function)成为可能。即使你并不直接使用右值引用,也可以通过标准库,间接从这一新特性中受益。为了更好的理解标准库结合右值引用带来的优化,我们有必要了解一下右值引用的重大意义。
右值引用的意义通常解释为两大作用:移动语义和完美转发。本文主要讨论移动语义。

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。
举个栗子。
问题一:如何将大象放入冰箱?
答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。
问题二:如何将大象从一台冰箱转移到另一台冰箱?

普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。
2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。
等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?

“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。

为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的

右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。 C++中右值可以被赋值给左值或者绑定到引用。**类的右值是一个临时对象,**如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。

线程与进程

python多线程编程 入门教程
CPU核的个数与进程数:
拥有2个运算设备的CPU称作双核CPU,拥有4个运算器的CPU称作4核CPU。也就是说,一个CPU中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。相反,若进程数超过核数,进程将分时使用CPU资源。但因为CPU运转速度极快,我们会感到所有进程同时运行。当然,核数越多,这总感觉越明显。

进程的介绍:
进程(Process) 是资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位,通俗理解: 一个正在运行的程序就是一个进程。例如:正在运行的qq,微信等,他们都是一个进程。
注意:一个正在运行的程序才叫进程,而没有运行的程序,只能叫程序,不能叫进程。
同时,一个程序可以有一个或者多个进程。

线程的介绍:
进程分配资源最小单位一旦创建一个进程就会分配一 定的资源,就像跟两个人聊QQ就需要打开两个QQ软件一样是比较浪费资源的。
线程程序执行最小单位,实际上进程只负责分配资源,而利用这些资源执行程序的是线程,也就说进程是线程的容器。一个进程中最少有一一个线程来负责执行程序,同时线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。这就像通过一个QQ软件(一个进程)打开两个窗口(两个线程)跟两个人聊天一样,实现多任务的同时也节省了资源。
在这里插入图片描述

linux

说一说常用的 Linux 命令

linux最常用的18个命令
常用的 Linux 命令有:
cd:切换当前目录
ls:查看当前文件与目录
grep:通常与管道命令一起使用,用于对一些命令的输出进行筛选加工
cp:复制文件或文件夹
mv:移动文件或文件夹
rm:删除文件或文件夹
ps:查看进程情况
kill:向进程发送信号
tar:对文件进行打包
cat:查看文件内容
top:查看操作系统的信息,如进程、CPU占用率、内存信息等(实时)
free:查看内存使用情况
pwd:显示当前工作目录

  • 5
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姬霓钛美

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值