C++面经总结

设计模式

建议看我另外的博客,里面有全套设计模型的解析:https://www.cnblogs.com/cancantrbl/category/1973800.html

面向对象三大特性

  • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 多态:允许将子类类型的指针赋值给父类类型的指针,重载和覆盖
  • 继承:使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

野指针

定义:

  • 指针指向了一块没有访问权限的内存。(即指针没有初始化)
  • 指针指向了一个已经释放的内存。

避免:

  • 将指针初始化为NULL,用完后也可以将其赋值为NULL。这样做在代码出现段错误时,有利于找到错误并修改。
  • 使用malloc分配内存, 分配后要进行检查是否分配成功,最后要进行释放内存。

堆栈区别 (问过很多次了)

  • 管理方式不同:栈 (stack) 由操作系统自动分配释放;堆 (heap) 由程序员申请和释放,容易产生内存泄漏;
  • 空间大小不同:每个进程拥有的栈 (stack) 的大小要远远小于堆 (heap) 的大小;Linux默认为1M,Windows默认为2M。
  • 分配方式不同:栈 (stack) 有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈 (stack) 的动态分配和堆 (heap) 是不同的,他的动态分配是由操作系统进行释放,无需手动实现。堆都是动态分配的。
  • 分配效率不同:栈 (stack) 的效率比堆 (heap) 的好。栈 (stack) 由操作系统自动分配,会在硬件层级对栈提供支持:只是栈顶指针的移动,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆 (heap) 是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。
  • 存放内容不同:栈 (stack) 存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。堆 (heap),一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充。

参数传递

  1. 按值传递:函数接收到了传递过来的参数后,将其拷贝一份,其函数内部执行的代码操作的都是传递参数的拷贝。按值传参最大的特点就是不会影响到传递过来的参数的值,但因为拷贝了一份副本,会更浪费资源一些。
  2. 按引用传参:对传入参数进行一些修改的时候
  3. 按常量引用传参:读取参数的值,而并不需要去修改它。节省拷贝开支的优点,又拥有按值传参的不影响原值的优点
  4. 右值引用传参:存储的是临时的将要被摧毁的资源,移动一个对象的状态总会比赋值这个对象的状态要来的简单(开销小)

Union, Struct, Class的区别

  • Struct的默认访问级别是public,接口和函数都默认为公共接口;class的默认访问级别是private
  • 在union中,所有的共用体成员共用一个空间,且其长度为联合中最大的变量长度。由于union的成员共用一个内存空间,所以必须存取正确的成员才能正确的读取变量值,可以使用一个额外的variable或enum型态来记录最后一次使用空间的是哪个成员。union的成员不可以是静态的
  • Struct中的成员可以同时存在,但union不行
  • Union不可以被继承,Struct和Class都可以被继承

智能指针

原理:防止忘记释放指针造成内存泄漏,智能指针自动调用析构函数,析构函数会自动释放资源

auto_ptr: 

对auto_ptr进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源。基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。已被摒弃,为了避免潜在的内存崩溃问题。

int main() {
  auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针

for(int i = 0; i < 5; ++i)
  cout << *films[i] << endl; // 此时程序会崩溃,因为films[2]已经是空指针了,下面输出访问空指针当然会崩溃了
 cout << "The winner is " << *pwin << endl; 

unique_ptr:

独享所有权,无法进行复制构造,无法进行复制赋值操作。两个unique_ptr指向同一个对象,像上面的auto_ptr赋值是不支持会报错的,但是可以进行移动构造和移动赋值操作

int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = ptest // 不允许 会报错
    ptest2 = unique_ptr<Test>(new Test("123"); // 允许,因为这里调用的是 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 ptest2 后就会被销毁。
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if(ptest == NULL)cout<<"ptest = NULL\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数
    ptest2->print();
    return 0;

share_ptr

使用计数机制来表明资源被几个指针共享, 当调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。可以安全地放到标准容器中。要学会手写share_ptr

/**********
* cite from: https://www.cnblogs.com/buerdepepeqi/p/12461343.html
**********/
#include <iostream>
#include <cstdlib>
using namespace std;

template <typename T>
class SmartPointer{
public:
    SmartPointer(T* ptr){
        ref = ptr;
        ref_count = (unsigned*)malloc(sizeof(unsigned));
        *ref_count = 1;
    }
    
    SmartPointer(SmartPointer<T> &sptr){
        ref = sptr.ref;
        ref_count = sptr.ref_count;
        ++*ref_count;
    }
    
    SmartPointer<T>& operator=(SmartPointer<T> &sptr){
        if (this != &sptr) {
            if (--*ref_count == 0){
                clear();
                cout<<"operator= clear"<<endl;
            }
            
            ref = sptr.ref;
            ref_count = sptr.ref_count;
            ++*ref_count;
        }
        return *this;
    }
    
    ~SmartPointer(){
        if (--*ref_count == 0){
            clear();
            cout<<"destructor clear"<<endl;
        }
    }
    
    T getValue() { return *ref; }
    
private:
    void clear(){
        delete ref;
        free(ref_count);
        ref = NULL; // 避免它成为迷途指针
        ref_count = NULL;
    }
   
protected:    
    T *ref;
    unsigned *ref_count;
};

weak_ptr:

用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。解决办法就是将两个类中的一个成员变量改为weak_ptr对象

两个不同进程的指针有可能指向同一个地址

共享内存允许两个或更多进程访问同一块内存,要注意的是多个进程之间对一个给定存储区访问的互斥

new/delete v.s. free/malloc 

  1. 属性:new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  2. 参数:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  3. 返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  4. 异常:new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。
  5. 自定义类型:malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。而new/delete允许重载
  6. 内存区域:new操作符从由c++默认使用堆实现的free store(凡是通过new操作符进行内存申请,该内存即为自由存储区)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

new 相对于 malloc的优点

  1. 不需要提前知道内存申请的大小,因为new 内置了sizeof、类型转换和类型安全检查功能
  2. new是类型安全的,而malloc不是
  3. new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上

this指针

定义:this作用域是在类内部,指向被调用函数所在的类实例的地址。当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数

this指针创建时间:this在成员函数的开始执行前构造,在成员的执行结束后清除

this指针存放在何处:this指针会因编译器不同而有不同的放置位置,可能是栈,也可能是寄存器,甚至全局变量。

this指针是如何传递类中的函数的:大多数编译器通过ecx寄存器传递this指针。

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置

虚函数

为什么要虚析构函数基类采用virtual虚析构函数是为了防止内存泄漏。如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

虚表的结构:首地址偏移量,虚函数所对应的序号(0开始递增)。

虚函数的默认参数问题:基类中虚函数的默认参数会在编译过程就被保存,再调用子类的函数后发生多态,编译器会使用基类的默认参数;基类有默认参数而子类没有,则调用的函数永远是基类中的函数,不能动态绑定的原因是与运行效率有关。

构造函数是否可以作为虚函数?不可以,因为构造函数是,需要确定对象的类型,而虚函数是在运行期间确定类型的,因此编译器无法知道是要构造基类对象,还是构造派生类的对象,因此构造函数不可以是虚函数。虚函数的执行依赖于虚函数表,而虚函数表是在构造函数中进行初始化工作的,即初始化vptr,指向正确的虚函数表。在构造期间,虚函数表还没有生成,因此不能将构造函数设为虚函数

虚继承

首先,虚继承和虚函数是两种完全不一样的东西。虚继承出现的意义是为了解决多种继承的问题。多个派生类保存相同基类的同名成员时,虽可以在不同的数据成员中分别存放不同的数据 ,但我们只需要相同的一份。 解决了多父类重复成员只保留一份的问题。

虚类结构:每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)

 

举例,比如现在有一个沙发床,它既有床的属性又有沙发的属性,它们都有家具里的长宽高的属性,但是我们却只需要知道它的一个状态的属性。

家具{
    属性:长,宽,高;
}

沙发床:public 沙发,public 床{
    属性:长,宽,高;
}

沙发:public 家具   ----> 沙发:virtual public 家具
{
    属性:长,宽,高;
}


床:public 家具   ----> 床:virtual public 家具
{
    属性:长,宽,高;
}

我们只需要他们的共同属性长宽高就行了,我们可以把共同属性提出来作为家具类,再由不同状态继承(虚继承),但最后都归总于沙发床。  

构造函数的类型

  1. 默认构造函数。默认构造函数的原型为 Student(); //没有参数
  2. 初始化构造函数 Student(int num,int age);//有参数
  3. 复制(拷贝)构造函数 Student(Student&);//形参是本类对象的引用
  4. 转换构造函数 Student(int r) ;//形参时其他类型变量,且只有一个形参

什么时候会调用析构函数

  • 对象生命周期结束,被销毁时;
  • delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
  • 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

stl的vector和list的区别,hash_table, map增删分别的时间复杂度

vector:动态数组,拥有一段连续的内存空间。每次执行push_back/pop_back操作,都是O(1),但在头部和中间进行插入和删除操作需要移动内存,会造成内存块的拷贝,其时间复杂度为O(n)

vector的扩容:当数组中内存空间不够时,会先重新申请一块内存空间,然后将旧数组拷贝到新数组再释放掉旧数组内存,reserve(size_t n) 改变的是capacity大小,resize(size_t n, const &T T())改变的是内存大小,如果n小于当前的内存,则会长度减少,但capacity是不会变的,如果n大于当前内存,就会插入n-size()个元素

vector的查找:数组中按照下标随机访问的时间复杂度是O(1)

vector内容的复制:

  1. 初始化构造时拷贝: vector<int> tem(list);
  2. assign: temlist.assign(list.begin(), list.end()); // copy the data, list unchanged
  3. swap: 可用于减少使用的内存。temlist.swap(list); // move list into temlist, list is empty
  4. insert: temlist.insert(temlist.end(), temlist2.begin(), temlist2.end()) // insert temlist2 at the end of temlist

list:双向链表,内存空间是不连续的,只能通过指针访问数据。首尾插入删除时间复杂度为O(1)。

map: 基于红黑树,元素排列有序,删除插入查找都是O(log n)(复杂度为红黑树的高度)。map仅需要为其存在的节点分配内存,内存效率接近于100%。查找性能更为稳定,类似于平衡二叉树。

unorder_map: 基于哈希表,根据关键码值而直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。在没有发生冲突的情况下,查找是O(1),插入删除O(1)。Hash需要事先应该分配足够的内存存储散列表,所以内存占用更多,而且会存在部分未被使用的位置,所以其内存效率不是100%的。查找性能不稳定是依赖于hash table,如果哈希函数映射的关键码出现的冲突过多,则最坏时间复杂度可以达到是O(n)。

unorder_map的删除:先在循环里找到要删除的元素的迭代器iter,再用erase(iter)删除,此时的迭代器已经失效了

unorder_map的扩容:超过桶的数量的时候回按一定的倍数扩容,gcc两倍, vs2019 8的倍数

unorder_map的查找:通过一个hash函数来计算元素的索引值index,来满足O(1)的搜索时间复杂度。

size_t bucket_index(const std::unordered_map<int, int>&  map, int key) { 
    size_t bucket_count   = map.bucket_count();		// 先算出map 的桶的个数
    const auto& hash_func = map.hash_function();	// 拿到hash函数
    size_t hash_code      = hash_func(key);			// 计算hashcode

    // 由于hashcode不一定处于[0, bucket_count]范围内,因此需要将hashcode映射到该范围
    return  hash_code % bucket_count; // 键key将插入的桶位置
} 

assert()的作用

计算assert里的表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。 

函数调用过程中的栈帧结构及其变化

可以看看我之前写的博客

被调用函数运行:

push %ebp // %epb寄存器保存的是调用者栈帧的栈底地址,将调用者栈帧的栈底地址压入栈,即保存旧的%ebp

mov %esp %ebp // 调用者栈帧的栈底%ebp 现在作为了新的栈帧的栈顶 %esp, 为了函数返回时,恢复父函数的栈帧结构

sub $0x16 %esp // 将%esp低地址移动16个字节。有了这么多的储存空间,才能支持函数里面的各种操作

C中函数的参数是从右到左:为了支持可变长参数形式。若顺序是从左到右,除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反

一个函数带有多个参数的时,C++语言没有规定函数调用时实参的求值顺序。这个是编译器自己规定的。

函数的返回:

movl %ebp %esp  // 使 %esp 和 %ebp 指向同一位置,即子栈帧的起始处, 旨在恢复原来栈顶的状态

popl  %ebp // 将栈中保存的父栈帧的 %ebp 的值赋值给 %ebp,恢复调用者栈帧的栈底

return 的值是通过%eax寄存器传递的

堆栈溢出一般是由什么原因导致的

  • 函数调用层次太深
  • 动态申请空间使用之后没有释放
  • 数组访问越界
  • 指针非法访问

C++与C#的区别

  • C++是一门面向对象的语言,而C#被认为是一门面向组件(component)的编程语言。面向对象编程聚焦于将多个类结合起来链接为一个可执行的二进制程序,而面向组件编程使用可交换的代码模块(可独立运行)并且你不需要知道它们内部是如何工作的就可以使用它们。
  • C++将代码编译成机器码,而C#将代码编译成CIL(Common Intermedier Language)后再编译成机器码
  • C++要求用户手动处理内存,C#有CLR支持自动垃圾收集机制,防止内存泄露
  • C#不使用指针(pointer),而C++可以在任何时候使用指针。
  • C#中所有对象都只能通过关键词“new”来创建
  • 数组变为了类,因此对于数组里的元素,.NET Framework提供了一系列的操作:查找、排序、倒置
  • 在异常处理上,C++允许抛出任何类型,而C#中规定抛出类型为一个派生于System.Exception的对象。

SLT中的sort()排序函数用的什么排序算法

时间复杂度:N*Log(N)

原理:sort不仅是快速排序还结合了插入排序和推排序。

适用容器:vector,deque; 关联性容器(map, set)有自动排序功能不需要;stack、queue和priority-queue不允许用户对元素排序

一个数据量较大的数组排序,开始采用快速排序,分段并归排序,分段之后每一段的数据量达到一个较小值后它就不继续往下递归(这是为了避免快排的递归调用带来过大的额外负荷),就选择插入排序,如果递归的层次太多,则会选择推排序

字节对齐

访问特定类型的变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定规则在空间上排列,而不是顺序地一个接一个地排放,这种所谓的规则就是字节对齐。这么长一段话的意思是说:字节对齐可以提升存取效率,也就是用空间换时间。比如#pragma pack(4)

类之间的关系有哪些

关联:单向,双向,多元

聚合:空心菱形,两个类之间有整体和局部的关系,并且就算没有了整体,局部也可以单独存在。就像卡车与引擎的关系,离开了卡车,引擎还是能单独存在。

组合:实心菱形,两个类之间有整体和局部的关系,部分脱离了整体便不复存在。就像大雁与翅膀的关系一样

依赖:虚线箭头,司机这个类,必须要依靠一个车对象才能发挥作用

继承:空心三角,有多个类出现相同部分的实例变量和方法用继承,人类与学生类或者老师类都是继承关系

实现:空心三角虚线,类与接口的关系

C++多态原理

多态定义:程序运行时,父类指针可以根据具体指向的子类对象,来执行不同的函数,表现为多态

原理:

  • 当类中存在虚函数时,编译器会在类中自动生成一个虚函数表
  • 虚函数表是一个存储类成员函数指针的数据结构,由编译器自动生成和维护
  • virtual 修饰的成员函数会被编译器放入虚函数表中
  • 存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针)

静态多态(编译器多态):函数重载和模板。函数重载,就是具有相同的函数名但是有不同参数列表,模板是c++泛型编程的一大利器

动态多态(运行期多态):类继承,虚函数

深拷贝和浅拷贝

浅拷贝:只复制指针内容,不复制指针所指对象,结果为两个指针指向同一块内存;浅拷贝发生时,通常表明存在着一个“相识关系”。share_ptr
深拷贝:重新为指针分配内存,并将原来指针所指对象的内容拷贝过来,最后结果为两个指针指向两块不同的内存;当深拷贝发生时,通常表明存在着一个“聚合关系”,copy_ptr

5种面向对象的设计原则

  • 单一职责原则 (The Single Responsiblity Principle,简称SRP):一个类,最好只做一件事,只有一个引起它的变化.
  • 开放-封闭原则 (The Open-Close Principle,简称OCP):对于扩展是开放的,对于更改是封闭的
  • Liskov 替换原则(The Liskov Substitution Principle,简称LSP):子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法
  • 依赖倒置原则(The Dependency Inversion Pricinple,简称DIP):依赖于抽象
  • 接口隔离原则 (The Interface Segregation Principle,简称ISP):使用多个小的专门的接口,而不要使用一个大的总接口

windows下一个程序的最大内存

  • 在windows 32位操作系统中,每一个进程能使用到的最大空间(包含操作系统使用的内核模式地址空间)为4GB(2的32次方) , 在通常情况下操作系统会分配2GB内存给程序使用,另外2GB内存为操作系统保留
  • 32位的Linux默认占用4GB中的1GB,程序只能使用剩下的3GB
  • 64位的Windows默认占用256TB中的248TB,程序只能使用剩下的8TB。
  • 64位的Linux默认占用256TB中的128TB,程序只能使用剩下的128TB。

static的作用

内存分配时间:static成员变量的内存空间既不是在声明类时分配,也不是在创建对象时分配,而是在初始化时分配

生命周期:编译时在静态数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。

访问权限:静态成员变量一共就一份,无论这个类的对象被定义了多少个,静态成员变量只分配一次内存,由该类的所有对象共享访问

  1. 隐藏:变量和函数,如果加了static修饰,就会其它源文件隐藏。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。Static可以作为函数和变量的前缀,对于函数来讲,static的作用仅仅限于隐藏。
  2. 保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也就是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围。
  3. 默认初始化为0:其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

内存五大区

  1. :用来存储一些局部变量以及函数的参数,局部变量等,在函数完成执行,系统自行释放栈区内存,不需要用户管理。栈区的大小由编译器决定,效率比较高,但空间有限。VS中默认的栈区大小为1M
  2. :由程序员手动申请空间,在程序运行期间均有效。堆区的变量需要手动释放。使用malloc或者new进行堆的申请,堆的总大小为机器的虚拟内存的大小
  3. 全局/静态存储区:存储程序的静态变量以及全局变量(在程序编译阶段已经分配好内存空间并初始化),整个程序的生命周期都存在的。
  4. 常量存储区:存放常量字符串的存储区,只能读不能写,const修饰的局部变量存储在常量区。
  5. 代码区:存放源程序二进制代码。

指针和引用

  • 指针是一个变量,这个变量存储的是一个地址,指向内存的一个存储单元;引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
  • 指针可以有多级,但是引用只能是一级
  • 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化
  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了
  • sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小
  • 指针和引用的自增(++)运算意义不一样

死锁

定义:指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)

必要条件:

  1. 互斥条件:线程/进程对于所分配到的资源具有排它性,即一个资源只能被一个线程/进程占用,直到被该线程/进程释放
  2. 请求与保持条件:一个线程/进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程/进程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程/进程必定会形成一个环路造成永久阻塞

避免死锁:破坏产生死锁的四个条件中的其中一个就可以

  1. 请求与保持条件:一次性申请所有的资源
  2. 不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
  3. 循环等待条件:靠按序申请资源来预防

线程和进程的区别 

  1. 进程是资源分配最小单位,线程是程序执行的最小单位;
  2. 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
  3. CPU切换一个线程比切换进程花费小;
  4. 创建一个线程比进程开销小;
  5. 线程占用的资源要⽐进程少很多。
  6. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;
  7. 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个进程死掉,整个线程就死掉了(因为共享地址空间);
  8. 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换

赛马问题

参考:https://zhuanlan.zhihu.com/p/103572219

TCP v.s. UDP

  1. 基于连接与无连接;
  2. 对系统资源的要求(TCP较多,UDP少);
  3. UDP程序结构较简单;
  4. 流模式与数据报模式 ;
  5. TCP保证数据正确性,UDP可能丢包;
  6. TCP保证数据顺序,UDP不保证。

如何保证UDP的可靠性

在应用层模仿传输层TCP的可靠性传输: 发送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。

  1. 添加seq/ack机制,确保数据发送到对端
  2. 添加发送和接收缓冲区,主要是用户超时重传。
  3. 添加超时重传机制

拥塞避免算法的具体过程

慢启动:慢启动为发送方的TCP增加了一个窗口: 拥塞窗口 (cwnd),初始化之后慢慢增加这个cwnd的值来提升速度。同时也引入了ssthresh门限值,如果cwnd达到这个值会让cwnd的增长变得平滑

  1. 连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据
  2. 每当收到一个ACK,cwnd++; 呈线性上升
  3. 每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
  4. 当cwnd >= ssthresh时,就会进入“拥塞避免算法”

拥堵避免:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多;只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。

快速重传:要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

快速恢复:当发送方连续收到三个重复确认时,由于发送方现在认为网络很可能发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

vector的扩容机制

  • vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素
  • 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器都会失效
  • 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1
  • 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容

vector的内存回收机制

erase(): 只是减少了size(),清除了数据,并不会减少capacity 

clear(): 清空vector中的元素但是所占的内存依旧存在,无法保证回收;一维vector.clear(),清空一维的元素,但是仍旧保留着row的capacity;二维vector.clear(),清空各行的colum,并且回收列内存capacity,但是保留row的capacity

swap(): 支持内存释放。去除vector多余的容量 x.swap(x); 也可以清空容器:vector<T> ().swap(x) (此格式会先生成一个空的 vector 容器,再借助 swap() 方法将空容器交换给 x,从而达到清空 x 的目的)

https和http的区别

http: 一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

https: 以安全为目标的HTTP通道,即HTTP下加入安全基础SSL层。可以建立一个信息安全通道,来保证数据传输的安全和确认网站的真实性

  • https协议需要到ca申请证书
  • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

如何防止一个类被拷贝

1. delete禁止使用

class noncopyable  
{  
protected:  
    //constexpr noncopyable() = default;  
   // ~noncopyable() = default;  
    noncopyable( const noncopyable& ) = delete;  
    noncopyable& operator=( const noncopyable& ) = delete;  
};

2. 把拷贝构造函数和赋值函数定义为私有函数

const修饰成员函数有什么作用

大根堆建堆的时间复杂度:O(n)

构建二叉堆是自下而上的构建,每一层的最大纵深总是小于等于树的深度的,因此,该问题是叠加问题,而非递归问题。

  1. 有n个元素的平衡二叉树,树高为㏒n,我们设这个高度为h。
  2. 最下层非叶节点的元素,只需做一次线性运算便可以确定大根,而这一层具有2^(h-1)个元素,我们假定O(1)=1,那么这一层元素所需时间为2^(h-1) × 1。
  3. 由于是bottom-top建立堆,因此在调整上层元素的时候,并不需要同下层所有元素做比较,只需要同其中之一分支作比较,而作比较次数则是树的高度减去当前节点的高度。因此,第x层元素的计算量为2^(x) × (h-x)。
  4. 又以上通项公式可得知,构造树高为h的二叉堆的精确时间复杂度为:S = 2^(h-1) × 1 + 2^(h-2) × 2 + …… +1 × (h-1) ①
  5. 2S = 2^h × 1 + 2^(h-1) × 2+ …… +2 × (h-1) ②
  6. S =2^h × 1 - h +1 ③  (② - ①)

  7. 将h = ㏒n 带入③,得出如下结论:S = n - ㏒n +1 = O(n)

2个链表如何判断是否相交

  • 暴力解法:若第一个链表遍历结束后,还未找到相同的节点,即不存在交点
  • 入栈解法:将链表压栈,通过top判断栈顶的节点是否相等即可判断两个单链表是否相交。
  • 遍历链表:同时遍历两个链表到尾部,同时记录两个链表的长度。若两个链表最后的一个节点相同,则两个链表相交。设较长的链表长度为len1,短的链表长度为len2,则先让较长的链表向后移动(len1-len2)个长度。然后开始从当前位置同时遍历两个链表,当遍历到的链表的节点相同时,则这个节点就是第一个相交的节点。

重载和重写的区别

重载:就是函数或者方法又相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间相互称之为重载函数或者方法。
重写:又称为方法覆盖,子类可以继承父类的方法,而不需要重新编写相同的方法。但是有时候子类并不想原封不动的继承父类的方法而是做了一个修改,需要重写。

sizeof 和 strlen 的区别

  1. sizeof是一个操作符,而strlen是库函数。
  2. sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为'\0'的字符串作参数。
  3. 编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来。
  4. sizeof计算数据类型占内存的大小,strlen计算字符串实际长度。

对中断函数的了解

定义:中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一事件的程序。等这些特殊事情处理完之后再回去执行之前的程序。

类型:

  • 计算机硬件异常或故障引起的中断,称为内部异常中断
  • 由程序中执行了引起中断的指令而造成的中断,称为软中断(这也是和我们将要说明的系统调用相关的中断);
  • 由外部设备请求引起的中断,称为外部中断

优先级:机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断

内存池、进程池、线程池。(c++程序员必须掌握)

首先介绍一个概念“池化技术 ”。池化技术就是:提前保存大量的资源,以备不时之需以及重复使用。池化技术应用广泛,如内存池,线程池,连接池等等。内存池相关的内容,建议看看Apache、Nginx等开源web服务器的内存池实现。由于在实际应用当做,分配内存、创建进程、线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作。因此,当程序中需要频繁的进行内存申请释放,进程、线程创建销毁等操作时,通常会使用内存池、进程池、线程池技术来提升程序的性能。

线程池:线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作,当工作完成后,线程又处于睡眠状态,而不是将线程销毁。

进程池与线程池同理。

内存池:内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

一个程序从开始运行到结束的完整过程(四个过程)

  • 预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
  • 编译:将预处理后的文件转换成汇编语言,生成.s文件
  • 汇编:汇编变为目标代码(机器代码)生成.o的文件
  • 链接:连接目标代码,生成可执行程序

进程的常见状态?以及各种状态之间的转换条件?

  • 就绪:进程已处于准备好运行的状态,即进程已分配到除CPU外的所有必要资源后,只要再获得CPU,便可立即执行。
  • 执行:进程已经获得CPU,程序正在执行状态。
  • 阻塞:正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态。

C++左值和右值

  • 左值 (lvalue, locator value):表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
4 = var; //wrong lvalue
var + 1 = 3; //wrong lvalue
int foo() {return 2;} foo() = 2 // wrong, because the return value of foo() is temporay 
int& foo() {return 2;} foo() = 2 // correct, because 引用一个左值是可以赋值给它,map的operator[]的重载就是返回引用以至于可以直接赋值

它们都是表达式的临时结果,而没有可识别的内存位置(也就是说,只存在于计算过程中的每个临时寄存器中)。因此,赋值给它们是没有任何语义上的意义的——我们赋值到了一个不存在的位置。

  • 右值 (rvalue) :不表示内存中某个可识别位置的对象的表达式。
  • 左值与右值之间的转化
    • 右转左:* 需要右值参数并返回一个左值参数
int arr[] = {1, 2}
int*p = &arr;
*(p+1) = 10; // p + 1 is rvalue
    • 左转右:& 需要左值参数并返回一个右值参数
int val = 10;
int *bad_address = &(val +1) // wrong because val + 1 is rvalue
int *add = &val; // correct beccause val is lvalue
&val = 4; // wrong because &val is rvalue

强枚举类型

C++11中的新语法,解决了传统C++中枚举常量的缺陷:

  • 若是同一作用域下有两个不同的枚举类型,但含有相同的枚举常量也是不可行的
  • 传统枚举值总是被隐式转换为整形,用户无法自定义类型

强枚举:枚举值也不会被隐式转换为整数,无法和整数数值比较

强类型枚举值具有传统枚举的功能-命名枚举值,同时又具有类的特点—具有类域的成员和无法进行默认的类型转换。所以也称之为枚举类——enmu class

lambda表达式优缺点

C++11新特性

nullptr,auto, 左值右值,智能指针,final, override, 强枚举

const关键词什么时候用

用来定义一个只读的变量或者对象:

  1. 便于类型检查,如函数的函数 fun(const int a) a的值不允许变,这样便于保护实参。
  2. 功能类似与宏定义,方便参数的修改和调整。如 const int max = 100;
  3. 节省空间,如果再定义a = max,b=max。。。就不用在为max分配空间了,而用宏定义的话就一直进行宏替换并为变量分配空间
  4. 为函数重载提供参考

1. 修饰成员变量:表示成员常量,不能被修改,同时它只能在初始化列表中赋值

2. 修饰成员函数:该成员函数不能修改类中任何非const成员函数,就是为了限制对于const对象的使用。

3. 修饰函数参数:

4. 修饰指针对象和引用对象

5. 修饰函数返回值:多用于操作符的重载。返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作。

指针常量和常量指针

1. 常量指针:表示const修饰的为所申明的类型。

  const char * p -> const修饰的是char,that is: p所指向的内存地址所对应的值,是const,因此不可修改。但指针所指向的内存地址是可以修改的.

  p = 10; // right, because p is pointer

  * (p +1) = 'a'; // wrong

2. 指针常量:表示const修饰的指针。

    char * const p -> 指针所指向的内存地址是不可以修改但p所指向的内存地址所对应的值

    p = 10; // wrong

    p[1] = 'a'; // right because p[1] is the value of the memory address

四个cast,static_cast, dynamic_cast, const_cast,reinterpret_cast区别

static_cast: 多用于非多态类型的转换

  1. 基本数据类型之间的转换,如把int转换成char,把int转换成enum 
  2. 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的,但下行是不安全的

dynamic_cast: 用于安全的将基类转化为继承类,而且可以知道是否成功,如果强制转换的是指针类型,失败会返回NULL指针,如果强制转化的是引用类型,失败会抛出异常

const_cast:  可以去掉变量const属性或者volatile属性的转换符常量指针/引用被转化成非常量指针/引用,并且仍然指向原来的对象;

reinterpret_cast: 要求编译器将两种无关联的类型作转换

NULL 和 nullptr 区别

C++中,NULL其实就是0,但是也可以用作空指针。这样的话,NULL存在二义性,它既是整数,也是一个指针,函数无法根据参数的数据类型判断应该调用哪一个实现。

#include<iostream>
using namespace std;
void test(void *p)
{
    cout<<"p is pointer "<<p<<endl;
 }
void test(int num)
{
    cout<<"num is int "<<num<<endl; 
}
int main(void)
{
    test(NULL);
    return 0; 
}

编译错误
$ g++ -o test test.cpp
    main.cpp: In function ‘int main()’:
    main.cpp:14:14: error: call of overloaded ‘test(NULL)’ is ambiguous
         test(NULL);

C++11以后又引入了nullptr,把空指针这一层意思给剥离出来,用以解决NULL在隐式转换和作为函数传入参数时的二义性问题。

如何禁止拷贝对象

1. 将拷贝构造函数和copy assignment 声明为 private

class Uncopyable {
public:
  Uncopyable() {}
  ~Uncopyable() {}
private:
  Uncopyable(const Uncopyable&);
  Uncopyable& operator=(const Uncopyable&);
};
class B : private Uncopyable {
};

2. 可以在函数的参数列表后加上=delete来防止拷贝

class Base{
	public:
		Base(){}
	private:
		Base(Base&)=delete;
		Base& operator=(Base&)=delete;
	friend void friendfunc();	
};

强类型和弱类型是什么? 

强类型:在编译的时候就确定类型的数据,在执行时类型不能更改。

  • 强类型更安全,因为它事先已经确定好了,而且效率高。一般用于编译型编程语言,如 c++,java,c#

弱类型:将一块内存看做多种类型,在执行的时候才会确定类型。

  • 弱类型更灵活,但是效率低,而且出错概率高。多用于解释型编程语言,如 javascript,vb 等

举例:C#中 int i = 1; i = 'a' // 这是非法的

lua中,local a = 1 a = 'a' //这是有效的

 i++在线程中是安全的吗

i++实际上有三个指令:

  1. 从内存中把i的值取出来放到CPU的寄存器中
  2. CPU寄存器的值+1
  3. 把CPU寄存器的值写回内存

由于线程共享栈区,不共享堆区和全局区,所以当且仅当 i 位于栈上是安全的。但如果是全局变量的话,同一进程中的不同线程都有可能访问到。对于读值,+1,写值这三步操作,在这三步任何之间都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。

解决:对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作;

注意:volatile不能解决这个线程安全问题。因为volatile只能保证可见性,不能保证原子性

Reference

  1. https://www.cnblogs.com/tenosdoit/p/3456704.html
  2. https://refactoringguru.cn/design-patterns/bridge
  3. https://www.jianshu.com/p/9d60e5f9cd7e
  4. https://www.cnblogs.com/dolphin0520/archive/2011/04/03/2004869.html
  5. https://www.jianshu.com/p/6c73a4585eba
  6. https://www.jianshu.com/p/e1ce19da9b82
  7. https://blog.csdn.net/weixin_36299192/article/details/88599007
  8. https://blog.csdn.net/LeoSha/article/details/46116959
  9. https://zhuanlan.zhihu.com/p/352434745
  10. https://blog.csdn.net/zxliyao/article/details/112230549
  11. https://blog.csdn.net/sinat_38972110/article/details/82121501

 

posted @ 2020-10-13 22:48  cancantrbl  阅读(2797)  评论(0编辑  收藏  举报