C++面经总结


设计模式

参考:https://refactoringguru.cn/design-patterns/factory-method

创建型

工厂方法模式:在父类中提供一个创建对象的方法 允许子类决定实例化对象的类型

  • 创建者 (Dialog):声明返回产品对象的工厂方法,可以作为抽象类,强制要求子类以不同的方式实现该方法
  • 具体创建者 (WindowsDialog, WebDialog):重写基础工厂方法,使返回不同的类型的产品
  • 产品 (Button):声明接口
  • 具体产品 (WindowsButton, HTMLButtons):对产品接口的不同实现

 

工厂模式例子:跨平台 UI (用户界面 组件 并同时避免客户代码与具体 UI 类之间的耦合,根据平台,初始化不同的具体创建者

不同的产品创建采用不同的工厂创建,这样创建不同的产品过程就由不同的工厂分工解决:WindowDialog专心负责生产WindowButton,WebDialog专心负责生产WebButton,WindowDialog和WebDialog之间没有关系,只为Button这个产品做接口,产品种类单一。

适用场景:

  • 适用于产品种类结构单一的场合,为一类产品提供创建的接口。
  • 如果需要向应用中添加一种新产品 你只需要开发新的创建者子类 然后重写其工厂方法即可。便于后期产品种类的扩展。
  • 需要用户能扩展你软件库或框架的内部组件
  • 需要复用现有对象来节省系统资源 而不是每次都重新创建对象 可使用工厂方法

抽象工厂模式:创建一系列相关的对象 而无需指定其具体类。抽象工厂接口声明一系列构建方法 客户端代码可调用它们生成不同风格的方法

  • 抽象产品 (Button, Checkbox):构成系列产品的一组不同但相关的产品声明接口
  • 具体产品 (WinButton, WinCheckbox, MacButton, MacCheckbox):是抽象产品的多种不同实现
  • 抽象工厂 (GUIFactory):声明一组创建各种抽象产品的地方
  • 具体工厂 (WinFactory, MacFactory):每个工厂都对应特定产品变体,且仅创建此种产品变体

抽象模式例子:跨平台的 UI 元素 同时确保所创建的元素与指定的操作系统匹配,调用factory

让低端工厂生产不同种类的低端产品,高端工厂生产不同种类的高端产品。让WinFactory生产不同种类的Window UI, 让MacFactory生产不同种类的Mac UI。产品有Button, Checckbox等等多个产品。

适用场景:

  • 需要与多个不同系列的相关产品交互。抽象工厂提供了一个接口 可用于创建每个系列产品的对象 只要代码通过该接口创建对象 那么就不会生成与应用程序已生成的产品类型不一致的产品。适用于产品种类结构多的场合.
  • 每个类仅负责一件事 如果一个类与多种类型产品交互 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中

生成器模式:分步骤创建复杂对象,该模式允许使用相同的创建代码生成不同类型和形式的对象

  • 生成器 (Builder):声明所有类型生成器中通用的产品构造步骤
  • 具体生成器 (CarBuilder, CarManualBuilder):提供构造过程的不同实现
  • 产品 (Car, CarManual):由不同生成器构造的产品,无需属于同一类层次结构
  • 主管 (Directior):定义构造步骤的顺序,可以创建和复用特定的产品配置

生成器模式例子:复用相同的对象构造代码来生成不同类型的产品:汽车及其相应的使用手册,客户端必须将生成器对象传递给主管对象

应用场景

  • 避免 “重叠构造函数 (telescopic constructor
  • 如果需要创建的各种形式的产品 它们的制造过程相似且仅有细节上的差异 此时可使用生成器模式
  • 生成器模式能分步骤构造产品, 这样可以延迟执行某些步骤而不会影响最终产品

原型模式:能够复制已有对象 而又无需使代码依赖它们所属的类

 

  • 原型 (Shape):接口将对克隆方法进行声明。
  • 具体原型 (Rectangle, Circle):实现克隆方法,将原始对象的数据复制到克隆体中之外。

 

原型模式例子:生成完全相同的几何对象副本 同时无需代码与对象所属类耦合

应用场景

  • 需要复制一些对象 同时又希望代码独立于这些对象所属的具体类
  • 子类的区别仅在于其对象的初始化方式 那么你可以使用该模式来减少子类的数量 

单例模式:保证一个类只有一个实例 并提供一个访问该实例的全局节点

应用场景:数据库连接类,将缓存首次生成的对象 并为所有后续调用返回该对象。常用于管理资源

结构型

适配器:能够转换对象接口 能使接口不兼容的对象能够相互合作

  • 适配器(SquarePegAdapter):可以同时与客户端和服务端交互的类,现客户端接口的同时封装了服务对象
  • 服务 (SquareReg):其中有与客户端和其接口不兼容的类,无法直接调用
  • 客户端接口 (RoundPeg):描述了其他类与客户端代码合作必须遵循的协议

客户端代码只需通过接口与适配器交互即可 无需与具体的适配器类耦合。因此 你可以向程序中添加新类型的适配器而无需修改已有代码

 

 

适配器例子:适配器假扮成一个Round­Peg

由于RoundHole不能直接调用SquareReg,用SquarePegAdapter封装SquarePeg对象,返回RoundPeg类型,使RoundHole可以用。

应用场景:

  • 希望使用某个类 但是其接口与其他代码不兼容时 可以使用适配器类
  • 需要复用这样一些类 他们处于同一个继承体系 并且他们又有了额外的一些共同的方法 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性

桥接模式:可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构 从而能在开发时分别使用

  • 抽象部分 (Remote):提供高层控制逻辑 依赖于完成底层实际工作的实现对象
  • 精确抽象部分 (AdvancedRemote):提供控制逻辑的变体
  • 实现部分 (Device):为所有具体实现声明通用接口 
  • 具体实现 (Radio, TV):包括特定于平台的代码

 

桥接例子:拆分程序中同时管理设备及其遥控器的庞杂代码, 可以开发独立于设备类的遥控器类 只需新建一个遥控器子类即可

设备Device类作为实现部分,而遥控器Remote类则作为抽象部分。遥控器基类声明了一个指向Device对象的引用成员变量。 所有遥控器通过通用设备接口与设备进行交互, 使得同一个遥控器可以支持不同类型的Device。

应用场景:

  • 想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类 可以使用桥接模式

行为模式 

面向对象三大特性

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

野指针

定义:

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

避免:

  • 将指针初始化为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. 右值引用传参:存储的是临时的将要被摧毁的资源,移动一个对象的状态总会比赋值这个对象的状态要来的简单(开销小)

智能指针

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

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。

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

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

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是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
  6. 内存区域:new操作符从由c++默认使用堆实现的自由存储区(free store)上为对象动态分配内存空间,而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开始递增)。

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

构造函数的类型

  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:动态数组,拥有一段连续的内存空间。在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。数组中按照下标随机访问的时间复杂度是O(1)。是用数组实现的,每次执行push_back操作,即先free掉原来的存储,后重新malloc。

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

hash_table: 哈希表,在没有发生冲突的情况下,查找是O(1),插入删除O(1)。

map: 底层红黑树,删除插入查找都是O(log n)。红黑树的内存比哈希表的内存占用少,map仅需要为其存在的节点分配内存,而Hash事先应该分配足够的内存存储散列表,即使有些槽可能弃用.

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

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#将代码编译成CLR (common language runtime)
  • C++要求用户手动处理内存,C#有自动垃圾收集机制,防止内存泄露
  • C#不使用指针(pointer),而C++可以在任何时候使用指针。
  • C#中所有对象都只能通过关键词“new”来创建
  • 数组变为了类,因此对于数组里的元素,.NET Framework提供了一系列的操作:查找、排序、倒置
  • 在异常处理上,C++允许抛出任何类型,而C#中规定抛出类型为一个派生于System.Exception的对象。

字节对齐

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

类之间的关系有哪些

关联:单向,双向,多元

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

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

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

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

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

C++多态原理

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

原理:

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

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

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

深拷贝和浅拷贝

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

4种面向对象的设计原则

  • 单一职责原则 (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。

c++遇到的一个最深刻的bug

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倍扩容

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)

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请求、申请缓冲区失败等)暂时无法继续执行的状态。

参考资料

  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

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM