C++一些知识回顾


多态

简单概括:一个接口,多种方法。

多态性:相同对象收到不同消息或不同对象收到相同消息时产生不同的动作。

种类:

  • 编译时多态(静态多态),通过重载函数实现。

    主要体现在函数模板。

    #include <iostream>
    
    template <typename T>
    T add(T a,T b) {
        T c = a + b;
        return c;
    }
    
    int main() {
        int i1 = 1;
        int i2 = 2;
        int result = 0;
        result = add(i1,i2);
        std::cout << "result: "<<result<<std::endl;
    }
    

    函数重载不是多态。

    重载:一个类中,函数名相同,参数列表不同。编译器会根据参数列表,生成不同名称的预处理函数。没有体现多态。

    重写:覆盖父类中相同名称相同参数的虚函数。

  • 运行时多态(动态多态),通过虚函数实现。

    程序运行的时候,动态绑定所需要的函数,动态找到函数的调用入口,从而确定具体调用哪个函数。

    #include <iostream>
    using std::cout;
    using std::endl;
    
    class parent
    {
    public:
        parent() {}
        virtual void eat()
        {
            cout << "Parent eat." << endl;
        }
    
        void drink()
        {
            cout << "Parent drink." << endl;
        }
        
        
    };
    
    class child: public parent {
    public:
        child() {}
        void eat()
        {
            cout << "Child eat." << endl;
        }
    
        void drink()
        {
            cout << "Child drink." << endl;
        }
    };
    
    int main() 
    {
        parent *pa = new child();
        pa->eat(); // Child eat.
        pa->drink(); // Parent drink.
        return 0;
    }
    

    两个函数调用有区别就是因为drink不是虚函数,所以父类指针要调用父类里面的函数。而eat是虚函数,就会找实际子类对象的函数。

    父类的指针只能访问子类中重写了父类中的那些虚函数,而不能访问子类新增的特有的函数。

    如果类里定义了virtual void ear() = 0;,那这个类就是抽象类,不能定义对象。

智能指针

帮忙管理动态分配的内存的,避免内存泄漏。

内存泄漏

分配了一块内存,但是不再持有引用了,但是没有收回,这块泄漏的内存再整个程序生命周期都不可再使用。

一般是分配了堆内存没有释放。更容易犯的错误就是在一个函数里分配了内存,别的函数调用,但是没有释放。

valrind定位内存泄漏的原因。

内存泄漏例子

#include <iostream>
using std::cout;
using std::endl;

class Test
{
public:
    Test() { cout << "hi" << endl; }
    ~Test() { cout << "bye" << endl; }

private:
    int debug = 20;
};

int main()
{
    Test *test = new Test();
    // hi
    return 0;
}

valgrind检测

$ valgrind --tool=memcheck --leak-check=full ./a.out 
==17915== Memcheck, a memory error detector
==17915== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==17915== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==17915== Command: ./a.out
==17915== 
hi
==17915== 
==17915== HEAP SUMMARY:
==17915==     in use at exit: 4 bytes in 1 blocks
==17915==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated

这里不完全准确,分配和释放次数,因为C++分配内存时,为了提高效率,使用了内存池,程序终止时内存才会被回收。

==17915== 
==17915== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17915==    at 0x4C2A593: operator new(unsigned long) (vg_replace_malloc.c:344)
==17915==    by 0x400971: main (test.cpp:17)

这里指出了泄漏所在的代码位置

==17915== 
==17915== LEAK SUMMARY:
==17915==    definitely lost: 4 bytes in 1 blocks
==17915==    indirectly lost: 0 bytes in 0 blocks
==17915==      possibly lost: 0 bytes in 0 blocks
==17915==    still reachable: 0 bytes in 0 blocks
==17915==         suppressed: 0 bytes in 0 blocks
==17915== 
==17915== For lists of detected and suppressed errors, rerun with: -s
==17915== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

总结:有一个泄漏

auto_ptr

已被抛弃,用unique_ptr代替

#include <iostream>
#include <memory>
using std::cout;
using std::endl;

class Test
{
public:
    Test() { cout << "hi" << endl; }
    ~Test() { cout << "bye" << endl; }
    int getDebug() { return debug; }

private:
    int debug = 20;
};

int main()
{
    std::auto_ptr<Test> test(new Test());
    cout << test->getDebug() << endl;
    // hi
    // 20
    // bye
    return 0;
}

抛弃原因

  • 复制或者赋值会改变资源所有权

    int main()
    {
        std::auto_ptr<Test> p1(new Test(1));
        std::auto_ptr<Test> p2(new Test(2));
    
        cout << p1->getDebug() << endl;
        cout << p2->getDebug() << endl;
    
        p1 = p2;
    
        cout << p1->getDebug() << endl; // 2
        cout << p2->getDebug() << endl; // segmentfault
    
        return 0;
    }
    

    p1=p2语句:释放p1托管的指针,接收p2托管的指针,p2所托管的指针指向null。

  • 不支持数组对象的内存管理。

unique_ptr

基本是auto_ptr的替代品。

特性:

  • 两个指针不能指向同一个资源。
  • 无法进行左值复制构造、复制赋值,但允许临时右值构造和赋值。
  • 会自动释放。
  • 容器中保存指针是安全的。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;

class Test
{
public:
    Test(int size):sz(size) {
        data = new int[sz];
        cout << "Test" << endl;
    }

    // copy constructor
    Test(const Test &t):sz(t.sz) {
        data = new int[sz];
        for (int i = 0; i < sz; i++) {
            data[i] = t.data[i];
        }
        cout << "Test copy" << endl;
    }

    // move constructor
    Test(Test&& t):sz(t.sz) {
        data = t.data;
        sz = t.sz;

        t.sz = 0;
        t.data = nullptr;
        cout << "Test move constructor" << endl;
    }


    ~Test() {
        delete [] data;
        cout << "~Test" << endl;
    }
    int *data;
    int sz;
};


int main()
{
    std::unique_ptr<string> p1(new string("hi"));
    std::unique_ptr<string> p2(new string("hi2"));

    // 不允许
    // p1 = p2;
    // unique_ptr<string> p3(p2);

    unique_ptr<string> p3(std::move(p1));
    p1 = std::move(p2);
    cout << "p1 = p2 赋值后:" << endl;
    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;
    cout << "p3:" << p3.get() << endl;
    // p1 = p2 赋值后:
    // p1:0x23b3060
    // p2:0
    // p3:0x23b3010
    return 0;
}

shared_ptr

指针共享,引用计数。

weak_ptr

配合shared_ptr引入的,不会引起计数的改变。不能访问数据。

如shared_ptr存在循环引用,就没法释放资源,引起内存泄漏。

如果weak_ptr指向的对象存在,lock函数返回一个shared_ptr,否则是空的shared_ptr.

解决循环引用问题

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

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa;  // 在B中引用A
};

int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    return 0;
    // 函数结束,思考一下:spa和spb会释放资源么?
}

左值右值

左值可以通过&操作符取地址(左值必然有名字),其它都是右值。

右值引用:&&。左右值一个主要区别:左值可以被修改,右值不能。

C++标准库例如:vector::push_back之类,会对参数的对象和数据都进行复制,会造成对象内存的额外创建。

#include <iostream>
#include <memory>
#include <vector>
using std::cout;
using std::endl;

class Test
{
public:
    Test(int size):sz(size) {
        data = new int[sz];
        cout << "Test" << endl;
    }

    Test(const Test &t):sz(t.sz) {
        data = new int[sz];
        for (int i = 0; i < sz; i++) {
            data[i] = t.data[i];
        }
        cout << "Test copy" << endl;
    }


    ~Test() {
        delete [] data;
        cout << "~Test" << endl;
    }
    int *data;
    int sz;
};


int main()
{
    std::vector<Test> v;
    Test a = Test(10);
    v.push_back(a);
    // Test
    // Test copy
    // ~Test
    // ~Test
    return 0;
}

move:

#include <iostream>
#include <memory>
#include <vector>
using std::cout;
using std::endl;

class Test
{
public:
    Test(int size):sz(size) {
        data = new int[sz];
        cout << "Test" << endl;
    }

    // copy constructor
    Test(const Test &t):sz(t.sz) {
        data = new int[sz];
        for (int i = 0; i < sz; i++) {
            data[i] = t.data[i];
        }
        cout << "Test copy" << endl;
    }

    // move constructor
    Test(Test&& t):sz(t.sz) {
        data = t.data;
        sz = t.sz;

        t.sz = 0;
        t.data = nullptr;
        cout << "Test move constructor" << endl;
    }


    ~Test() {
        delete [] data;
        cout << "~Test" << endl;
    }
    int *data;
    int sz;
};


int main()
{
    std::vector<Test> v;
    Test a = Test(10);
    v.push_back(std::move(a));
    // Test
    // Test move constructor
    // ~Test
    // ~Test
    return 0;
}

C++/C的内存分配,栈和堆的区别,为什么栈要快

  1. 栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区(heap) 就是那些由 new 分配的内存块,一般一个 new 就要对应一个 delete。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链。堆可以动态地扩展和收缩。
  3. 自由存储区 就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。
  4. 全局区(静态区)(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域data段, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域bss段。程序结束后有系统释放
  5. 常量存储区 存放的是常量,不允许修改。常量字符串就是放在这里的。常量字符串不能修改, 否则程序会在运行期崩溃。

docker

只是用过。

虚函数、底层机制

虚函数表和虚函数指针。

image-20220714150037922

static

  • 修饰全局变量,只能本文件访问。
  • 静态局部变量只执行初始化一次,并且程序运行结束后才释放。

野指针

指向垃圾内存的指针,如free了,但是没有置为nullptr。

怎么找路由器地址

网关ip

new、malloc、delete,释放后调用的后果

new的时候两件事:分配内存、构造函数构造对象

delete:析构函数、释放内存。

如果用new 创建对象数组,那么只能使用对象的无参数构造函数。

new能自动计算分配空间,malloc需要计算字节数。

new是类型安全的,malloc不是(要转换类型),编译阶段找不到错误。

编译的过程

C/C++代码转化为二进制程序:

  • 预处理:处理宏定义,#include,删除注释等。
  • 编译:词法分析(分词)、语法分析(将单词组合成语法短语)、语义分析(类型审查、比如代码瞎写不让过)、优化生成汇编代码。
  • 汇编:对汇编代码进行处理。
  • 链接:静态链接、动态链接。

extern C

在C++代码中调用其他C语言代码,如C++支持函数重载,在编译的时候会将函数的参数类型也加到编译后的代码里,而C不支持函数重载,不会带上函数的参数类型。在C++里面调用C dll库的时候,加上extern C来正确使用库。

一个项目多线程版本和多进程版本之间的区别

进程是资源分配的最小单位,线程时CPU调度的最小单位。

  • 数据共享
  • 内存、CPU
  • 创建、销毁和切换
  • 可靠性:进程之间不会相互影响,线程一个挂掉整个进程挂掉。

设计模式

工厂模式

创建对象不会对客户端暴露逻辑,而是使用一个共同的接口来指向新创建的对象。

单例模式

单例

装饰模式

包装,为现有类添加功能,又不破坏原有的结构。

代理模式

可以在创建对象之后附加一些操作。

查询DNS的过程、DNS在哪一层、使用什么协议实现、为什么不使用TCP

image-20220714155649640

DNS在应用层:提供域名到IP地址之间的解析服务。

DNS协议使用UDP协议,只有主DNS服务器和辅助DNS服务器数据同步时使用TCP协议。

UDP不需经过三次握手,查询域名一般返回内容不超过512字节。

tcp、ip首部字段

tcp:源端口、目标端口、序列号(发送的第一个字节的编号)、确认号(期望收到的下一个字节的编号)、TCP首部长度、校验和、窗口大小、ACK、SYN、FIN。

udp:源端口、目的端口、长度、校验和。

ip:源ip地址、目的ip地址、TTL(ip数据包可以经过的路由器数)、版本、长度。

accept发生在TCP那个阶段

accept返回是在三次握手之后,但是在三次握手之前调用会阻塞,也可以三次握手之后调用。

执行函数,得到EAGAIN的情况

比如异步发送,发送给缓冲区之后立即返回,但是可能缓冲区满了,EAGAIN。

如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

epoll原理

网卡把数据写入到内存后,网卡向CPU发出一个中断信号,操作系统便得知有新数据到来。

image-20220714162847075

数据到来之后:

image-20220714163354451

select每次收到数据都需要重新添加等待队列然后阻塞,而且用户进程不知道哪个socket就绪了,只能遍历。

epoll使用epoll_ctl维护等待队列,epoll_wait阻塞进程。

image-20220714164201188

进程的状态改变:

image-20220714164352722

sock在epoll中以红黑树方式存放。

image-20220714164619952

共享内存、进程通信、匿名管道

以前记录过了:链接

模板能在cpp文件写实现吗?为什么?

回答:不能分开写,但是硬要分开写,也可以,会有问题,但是可以解决。基本上是不能的意思了。

模板类在.h中定义,在.cpp中实现

在C程序中#include <stdio.h>的作用仅仅是在预编译的时候得到printf的声明,知道要去外面找这个符号。但是这个符号在哪,.h文件是不知道的,需要在编译的时候让链接器去所有目标文件里找这个符号,然后把它链接进来。

初始化列表的理解,为什么不在构造函数里面初始化成员变量?

foo(string s, int i):name(s), id(i){} ; // 初始化列表

构造函数之前,会进行参数初始化,

如:

struct Test1
{
    Test1() // 无参构造函数
    { 
        cout << "Construct Test1" << endl ;
    }

    Test1(const Test1& t1) // 拷贝构造函数
    {
        cout << "Copy constructor for Test1" << endl ;
        this->a = t1.a ;
    }

    Test1& operator = (const Test1& t1) // 赋值运算符
    {
        cout << "assignment for Test1" << endl ;
        this->a = t1.a ;
        return *this;
    }

    int a ;
};

struct Test2
{
    Test1 test1 ;
    Test2(Test1 &t1)
    {
        test1 = t1 ;
    }
};

会调用两次构造函数,一次赋值函数。

使用初始化列表是基于性能考虑。对于类类型,会少一次构造。一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。

常量、引用类型(不能重新赋值)、没有默认构造函数的类,必须放入初始化列表。

(mmap)为什么不用read?(零拷贝的好处)

普通读写文件方式:

image-20220714175509370

mmap:直接在用户空间读写页缓存。把内核空间和用户空间的虚拟地址映射到同一个物理地址。

image-20220714175634588

零拷贝:CPU不需要将数据从一个内存区域复制到另一个内存区域,从而减少上下文切换及CPU的拷贝时间。

渐进式rehash

量可能比较大,短时间拷贝不完。

步骤:

  • 分配空间,新旧两个哈希表同时存在
  • 在字典中维护一个索引计数器变量。
  • rehash期间,执行添加、删除、查找、更新操作时,除了完成指定的操作,还会把索引计数器对应的键值rehash到新表,完成后,索引+1。
  • 在某个时间点,会全部rehash完,索引设置为-1,表示已经完成。

epoll边缘触发,EAGAIN

读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN 写: 只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

zookeeper原理

zookeeper 约等于:文件系统+通知机制。

它维护了一个类似文件系统的数据结构:

image-20220714182549052

每一个子目录项称为znode,可以增删改查。

四种类型znode:

  • 持久化目录节点,客户端和zookeeper断开连接后,节点依旧存在。
  • 持久化顺序编号目录节点,比上面多了一个给节点名称进行顺序编号。
  • 临时目录节点。
  • 临时顺序编号目录节点。

在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序。

客户端注册监听它关心的目录节点,目录节点变化后,zookeeper会通知客户端。

zookeeper用途

命名服务

通过path相互发现。

配置服务

远程动态配置。

集群管理

创建临时目录节点,然后监听父目录节点的子节点变化消息,一旦有机器挂掉,临时目录节点被删除,所有机器收到通知,加入也是如此。

选举master,所有机器创建临时顺序编号目录节点,每次选取最小的机器作为master。

分布式锁

zk会新建一个临时节点,争夺这个锁的都挂在下面,最上面的就是抢到了锁的,删除了就是释放,然后接着轮到第一个抢到锁。

无锁队列怎么实现

CAS操作,队尾,队首。