C++服务端面试准备(1)C++相关

c/c++

浏览数:77

2020-6-15

声明:本文内容纯属博主自己查找和归纳的个人所需的知识点,仅作参考,如有错误,博主强烈希望您指出。如果您是某个知识点的原创博主,如有需要,可联系本人加上链接。本文内容会根据博主所需进行更新,希望大家多多关照。

面向对象

       把数据及对数据的操作方法放在一起,作为一个相互依存的整体

       OOP——面向对象的编程,OOD——面向对象的设计,OOA——面向对象的分析

       三大特征:

       1.封装
       将对象的属性和方法封装到一个独立单元中,并且隐藏对象的属性和方法,仅对外提供公共访问方式,将变化隔离,便于使用。

       2.继承
       继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法。继承是多态的前提。

       3.多态
       多态是面向对象的最后一个主要特征,它本身主要分为两个方面:

  • 方法的多态性:重载与覆写

       重载:同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同。
       重写:子类中的方法与父类中的某个方法的名称和参数完全相同,子类对象调用这个方法时,将调用子类中方法

  • 对象的多态:父子类对象的转换。

       向上转型:子类对象变为父类对象,格式:父类 父类对象 = 子类实例,自动;
       向下转型:父类对象变为子类对象,格式:子类 子类对象 = (子类)父类实例,强制。

     多态存在的三个必要条件:继承、重写、父类引用指向子类对象

c++类里面编译器默认生成的函数

1.构造函数
2.拷贝构造函数
3.赋值构造函数
4.析构函数

voliate关键字的作用

       使用voliate声明变量值的时候,系统总是重新从它所在的内存读取数据;用voliate声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错.
       使用场景:多任务环境下各任务间共享的标志应该加voliate;存储器映射的硬件寄存器通常也要加voliate,因为每次对它的对写都可能有不同意义;

const的用法

       const修饰的内容不可改变,C++有5种用法:

       1.定义常量

       2.指针常量和常量指针:

  • 指针常量——指针类型的常量int *const p

       本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化;

  • 常量指针——指向“常量”的指针const int *pint const *p

       常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的

       3.const修饰函数传入参数

       4.修饰函数返回值

       5.const修饰成员函数(c++特性)
       const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
       const对象的成员是不能修改的,但是可以用另外的指针指向成员去修改;
       const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

const和宏的区别

  1. const是有数据类型的常量,而宏常量没有,编译器可以对前者进行静态类型安全检查,对后者仅是字符替换,没有类型安全检查
  2. 有些编译器可以对const常量进行调试, 不能对宏调试。
  3. 宏能作为卫哨来防止文件的重复包含,但const不可以

static的作用

  1. 隐藏。static修饰的变量只在当前文件生效,故使用static在不同的文件中定义同名函数和同名变量,不必担心命名冲突。
  2. 保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
  3. static的第三个作用是默认初始化为0

new和malloc的区别

       new和delete是c++的运算符,malloc和free是C语言的标准库函数

特征 new/delete malloc/free
分配内存的位置 自由存储区
内存分配失败返回值 完整类型指针 void*
内存分配失败返回值 默认抛出异常 返回NULL
分配内存的大小 由编译器根据类型计算得出 必须显式指定字节数
处理数组 有处理数组的new版本new[] 需要用户计算数组的大小后进行内存分配
已分配内存的扩充 无法直观地处理 使用realloc简单完成
是否相互调用 可以,看具体的operator new/delete实现 不可调用new
分配内存时内存不足 客户能够指定处理函数或重新制定分配器 无法通过用户代码进行处理
函数重载 允许 不允许
构造函数与析构函数 调用 不调用

C++存储机制

  1. 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量。
  2. 堆区(heap): 存放malloc申请的内存,使用free释放
  3. 全局区(静态区):存放全局变量和静态变量,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
  4. 常量区 :常量字符串就是放在这里的。 程序结束后由系统释放。
  5. 自由存储区:存放new申请的内容,使用delete

为什么会出现段错误

  1. 访问不存在的内存地址
  2. 访问系统保护的内存地址
  3. 访问只读的内存地址
  4. 栈溢出

引用和指针的区别

  1. 引用是变量的别名,一定有本体,指针是是存放地址的一个变量
  2. 引用声明时就必须初始化,指针可以暂时不初始化
  3. 指针的使用需要注意指向的内存,防止出现野指针的情况,引用则不需要

你所了解的c++常见的标准库

       <iostream>、<fstream>、各种容器、<algorithm>、<ctime>、<memory>(智能指针)、<cstdio> 、<functional>(lambda表达式)、<complex> 、<cmath>、<cstring>(free,malloc需要包含<stdlib.h>)、<iterator>等等

三种智能指针

       注意,智能指针只是指针本身自动释放,解决野指针的问题,但其对象一样要手动释放

       1.unique_ptr:
       一个 unique_ptr “拥有” 它所指向的对象。某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。不能拷贝和赋值,但可以用std::move、release、reset转移,或者用函数返回局部的unique_ptr

       2.shared_ptr
       最终的实现是两个指针成员:一个指向数据成员,一个指向计数器成员,计数器维护的是一个指针,指向的实际内存在堆上,可以用make_shared<>()创建和初始化shared_ptr
释放方法:
       如果该shared_ptr是唯一指向其对象的shared_ptr,则直接可以<shared_ptr> = nullptr或者<shared_ptr>.reset()释放对象

       3.weak_ptr
       weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
作用:
       可判断shared_ptr的对象是否存在(使用 lock方法),从而避免访问一个不存在的对象的情况。
       解决shared_ptr循环引用问

虚函数原理

       虚函数是用来实现动态绑定的。

       C++中虚函数使用虚函数表和虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址,假如子类重写了父类的虚函数,则在虚函数表中会把对应的虚函数地址替换为子类的函数的地址;虚函数表指针存在于每个对象中,它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。

       虚函数表是每个(有虚函数的)类对应一个。虚函数表指针是每个对象对应一个。

       如果一个函数不是虚函数,那么对它的调用在编译阶段就会确定。而虚函数要运行时才能确定。

为什么需要虚析构函数

       在存在类继承并且析构函数中需要析构某些资源时,析构函数需要是虚函数。否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄露。

如何解决类之间的相互依赖问题

  1. 写两个头文件A.h和B.h分别用于声明类A和B;
  2. 写两个.cpp文件分别用于定义类A和B,定义B的.cpp文件里包含A.h;
  3. 在A的头文件中导入B的头文件,在main文件包含A.h即可;
  4. 在B的头文件中不导入A的头文件,前置声明类A,并且,在B中使用A的时候要用指针的形式。

单例设计模式

       保证一个类仅可以有一个实例化对象,并且提供一个可以访问它的全局接口。

实现单例模式必须注意一下几点:

  • 单例类只能由一个实例化对象。
  • 单例类必须自己提供一个实例化对象。
  • 单例类必须提供一个可以访问唯一实例化对象的接口

2种方法:

  • 双重判断+上锁
  • 使用std::call_once函数

       std::call_once()函数:第二个参数为函数名,保证函数只调用一次,比互斥量消耗小

       注意:可以创建内部类进行释放对象内存,不能用析构函数,因为会出现一直调用析构函数的死循环

双重判断+上锁:

std::mutex resource_mutex;

class MyCAS
{
private:
    MyCAS(){}//构造函数私有化,不能直接创建对象
    static MyCAS *m_instance;
    
public:
    static MyCAS *GetInstance()
    {
        if(m_instance == NULL)//双重判断+上锁,防止多线程多次new对象
        {
            std::unique_lock<std::mutex> mymutex(resource_mutex);
            if(m_instance == NULL)
            {
                m_instance = new MyCAS();
                static CGarhuishou cl;
            }
        }        
        return m_instance;
    }
    
    //此类用于自动删除内存,不能用析构函数,因为在delete了外部指针,会调用析构函数delete m_instance,
//而m_instance指向对象为MyCAS,然后又会再次调用析构函数,变成死循环
    class CGarhuishou
    {
    public:
        ~CGarhuishou()
        {
            if(MyCAS::m_instance)
            {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
};

MyCAS *MyCAS::m_instance = NULL;
MyCAS *p_a = MyCAS::GetInstance();//创建线程前先初始化单例对象

使用std::call_once函数:

std::once_flag g_flag;
std::mutex resource_mutex;

class MyCAS
{
private:
    MyCAS(){}
    static MyCAS *m_instance;
    
public:
    static void CreateInstance()
    {
        m_instance = new MyCAS();
        static CGarhuishou cl;
    }
    static MyCAS *GetInstance()
    {
        //第一个线程进来call_once执行函数后,第二个线程才能进来判断是否执行
        std::call_once(g_flag, CreateInstance);//g_flag看成执行了函数的标记
        return m_instance;
    }
    
    class CGarhuishou
    {
    public:
        ~CGarhuishou()
        {
            if(MyCAS::m_instance)
            {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
};

工厂设计模式

  • 简单工厂模式:由一个工厂判断类型然后创建不同的对象
  • 工厂方法模式:多个工厂创建相应的对象
  • 抽象工厂模式:多个工厂创建相应的对象组合

i++和++i的区别

  • 注意不要只看问题表面,可以拓展
  1. i++是返回原来的值再+1,++i是+1再返回值
  2. i++不能作为左值,++i可以作为左值(左值:可以用&获取内存地址),因为重写前缀++的函数内容是+1操作,返回值为引用形式,而后缀++的函数内容是把原来的值赋给一个临时的变量,然后原来的值+1,返回临时变量
  3. 如果i是迭代器或者其他自定义的类,根据第二点的描述,++i的效率明显高于i++

引用和指针的区别

  1. 引用是变量的别名,一定有本体,指针是是存放地址的一个变量
  2. 引用声明时就必须初始化,指针可以暂时不初始化
  3. 指针的使用需要时刻注意指向的内存,防止出现野指针的情况,引用则不需要

作者:DX3906