关于后端:千万不要错过的后端纯干货面试知识点整理-I-I

3次阅读

共计 7837 个字符,预计需要花费 20 分钟才能阅读完成。

千万不要错过的后端【纯干货】面试知识点整顿 I I

c++ 内存治理

上次分享整顿的面试知识点 I,明天咱们来持续分享面试知识点整顿 II

linux kernel 内核空间、内存治理、过程治理设施、驱动虚构文件系统(vfs) 内核空间是受爱护的,用户不能对内核空间读写,否则会呈现段谬误
环境变量(env) PATH
命令行参数 char *agrv[]
栈区⬇️ 函数的返回地址,返回值,参数,局部变量
共享库(映射区)⬇️ 调用动静库,或者 mmap 函数进行文件映射
堆区⬆️ 用 new/malloc 申请的内存,同时须要实用 delete/free 来开释采纳链式贮存构造
.bss 区 未初始化的全局变量和动态变量以及 初始化为 0 的 全局变量和动态变量编译时就曾经调配了空间
.data 区 已初始化的全局变量和动态变量编译时就曾经调配了空间
.text 1、只读存储区 — 常量,const 全局变量 2、文本区 — 程序代码,机器代码
0-4k 保护区
#include<stdio.h>
 
int a;    // 未初始化全局区 .bss
int b=1;    // 已初始化全局区  .data
static int c=2;    // 已初始化全局区  .data
const int d=3;    // 只读数据段,也叫文字常量区 ro.data, d 的值不能被批改
int main(void)
{
    int e=4;    // 栈区
    static int f=5;    // 已初始化全局区
    const int g=6;    // 栈区,不能通过变量名批改其值,但可通过其地址批改其值
    int *p=malloc(sizeof(int))    // 指针变量 p 在栈区,但其所指向的 4 字节空间在堆区
    char *str="abcd";    // 字符串“abcd”存在文字常量区,指针变量 str 在栈区,存的是“abcd”的起始地址
    return 0;
}

内存泄露及分类

内存透露,是因为忽略或谬误造成程序未能开释掉不再应用的内存。内存透露,并不是指内存内存再物理地址上的隐没,而是应用程序调配某段内存后,失去了对该段内存的管制,因此造成内存的节约。

  • 个别状况是 new/malloc 后,没有及时 delete/free 开释内存,判断为内存泄露
  • linux 中能够应用 valgrind 来检测内存透露

内存透露的分类:

  • 堆内存透露 — new/malloc 后 没有 delete/free 掉
  • 系统资源透露 — 零碎调配的资源,没有用指定的函数开释掉,导致系统资源的节约,重大影响零碎性能,如:socket,bitmap,handle
  • 没有将父类的析构函数定义为虚函数 — 父类指针指向子类对象的时候,开释内存的时候,若父类的析构函数不是 virtual 的话,子类的内存是不会失去开释的,因而会内存透露

c++ 中是如何解决内存透露的:

应用 valgrind,mtrace 来检测内存透露

防止内存透露:

1. 事先预防型。如智能指针等。2. 预先查错型。如透露检测工具。

智能指针

应用智能指针,智能指针会主动删除被调配的内存,他和一般指针相似,只是不须要手动开释指针,智能指针本人治理内存开释,不必放心内存透露问题

智能指针有:

  • auto_ptr
  • unique_ptr
  • shared_ptr
  • weak_ptr

其中 auto_ptr c++11 曾经被弃用了

unique_ptr

独占的智能指针,只能有一个对象领有所有权,独占指针的是本人治理内存的,指针存在于栈空间,开拓的内存在堆空间,这里的堆空间是和智能指针绑定的,智能指针随着函数完结被销毁之前,智能指针会先去把堆外面的内存销毁

其中波及

  • move 函数 — 能够应用 move 函数来转移所有权,转移所有权后,原来的指针就无权拜访
  • reset 函数 — 能够用 reset 函数来重置所有权,会把之前的对象所有权开释掉,从新创立一个所有权对象
  • make_unique — 疾速的创立一个 unique_ptr 智能指针 的对象 如 auto myptr = make_unique<person>();

如果心愿只有一个智能指针治理资源 就应用 unique_ptr

#include <iostream>
#include <string>
#include <memory>

using namespace std;

struct person
{~person()
    {cout<<"~person"<<endl;}
    string str;
};

unique_ptr<person> test()
{return unique_ptr<person> (new person);
}

int main()
{
    //unique_ptr is ownership 
    unique_ptr<person> p = test();
    p->str = "hello world";
    
    unique_ptr<person> p2 = move(p);  // 能够应用 move 函数来转移所有权,转移所有权后,原来的指针就无权拜访
    if(!p)
    {cout<<"p == null" <<endl;}

    if(p2)
    {
        cout<<"p2 have ownership"<<endl;
        cout<<p2->str<<endl;
    }

    p2.reset(new person);// 能够用 reset 函数来重置所有权,会把之前的对象所有权开释掉,从新创立一个所有权对象
    if(p2->str.empty())
    {cout<<"str is null"<<endl;}
 
     return 0;
 }

shared_ptr

共享的智能指针,shared_ptr应用援用计数(use_count 办法),每个 shared_ptr 的拷贝都指向同一块内存,在最初一个 shared_ptr 被析构的时候,内存才会被开释

  • shared_ptr 是援用计数的形式,应用 use_count 查看计数
  • make_shared 快捷创立 shared_ptr

应用函数返回本人的 shared_ptr 时,须要继承 enable_shared_from_this 类,应用 shared_from_this 函数进行返回

注意事项:

  • 不要将 this 指针作为返回值
  • 要防止循环援用
  • 不要再函数实参种创立 shared_ptr,在调用函数之前先定义以及初始化它
  • 不要用一个原始指针初始化多个 shared_ptr

心愿多个指针治理同一个资源就应用 shared_ptr

#include <iostream>
#include <string>
#include <memory>

using namespace std;

struct person
    :enable_shared_from_this<person>{
    string str;
    void show()
    {cout<<str<<endl;}
    ~person()
    {cout<<"~person"<<endl;}
    shared_ptr<person> getshared()
    {return shared_from_this();
    }
};

int main()
{
    #if 0
    shared_ptr<person> ptr(new person);
    cout<< ptr.use_count()<<endl;
    
    shared_ptr<person> ptr2 = ptr;
    cout<< ptr.use_count()<<endl;

    shared_ptr<person> a = make_shared<person>();
    cout<< a.use_count()<<endl;
    a = ptr2;
    cout<< ptr.use_count()<<endl;


    shared_ptr<person> mm = a->getshared();
    
    #endif

    shared_ptr<person> ptr;
    {shared_ptr<person> ptr2(new person);
        ptr2->str = "hello";
        
        ptr = ptr2->getshared();    
        cout<< ptr.use_count()<<endl;}
    ptr->show();

    return 0;
}

weak_ptr

弱援用的智能指针

是用来监督 shared_ptr 的,不会应用计数器加 1,也不会应用计数器减 1,次要是为了监督 shared_ptr 的生命周期,更像是 shared_ptr 的一个助手。weak_ptr 还能够用来返回 this 指针和解决循环援用的问题。

shared_ptr 会有循环援用的问题,解决形式为 把类中的 shared_ptr 换成 weak_ptr 即可

struct ListNode
{
    std::shared_ptr<ListNode> _next;//std::weak_ptr<ListNode> _next; 就能够解决
    std::shared_ptr<ListNode>  _prev;//std::weak_ptr<ListNode> _pre; 就能够解决
    
    ~ListNode()
    {cout << "~ListNode()" << endl;
    }
};
void test_shared_ptr_cycleRef()
{std::shared_ptr<ListNode> cur(new ListNode);
    std::shared_ptr<ListNode> next(new ListNode);
 
    cur->_next = next;
    next->_prev = cur;
 
}
int main()
{test_shared_ptr_cycleRef();
    system("pause");
    return 0;
}

例如上述代码案例

void shared_ptr_cycleRef(){
    std::shared_ptr<LISTNODE> cur LISTNODE;
    std::shared_ptr<LISTNODE> next LISTNODE;

     cur->_next = next;
     next->_pre = cur;
}

Cur 和 next 存在循环援用,他们的援用计数都变为 2

出了作用域之后,cur 和 next 被销毁,援用计数减 1

因而要开释 cur,就须要开释 next 的 _pre,要开释 next,就须要开释 cur 的 _next

内存透露检测工具

valgrind 内存检测工具

valgrind 的官网网址是:http://valgrind.org

valgrind 被设计成非侵入式的,它间接工作于可执行文件上,因而在查看前不须要从新编译、连贯和批改你的程序。要查看一个程序很简略

命令如下:valgrind --tool=tool_name program_name

  • 做内存查看:valgrind --tool=memcheck ls -l
  • 查看内存透露:valgrind --tool=memcheck --leak-check=yes ls -l

valgrind 有如下几个工具

memcheck

memcheck 探测程序中内存治理存在的问题。

它查看所有对内存的读 / 写操作,并截取所有的 malloc/new/free/delete 调用。因而 memcheck 工具 可能探测到以下问题:

Memcheck 工具次要查看上面的程序谬误:

  • 应用未初始化的内存 (Use of uninitialised memory)
  • 应用曾经开释了的内存 (Reading/writing memory after it has been free’d)
  • 应用超过 malloc 调配的内存空间(Reading/writing off the end of malloc’d blocks)
  • 对堆栈的非法拜访 (Reading/writing inappropriate areas on the stack)
  • 申请的空间曾经开释开释,即内存透露 (Memory leaks – where pointers to malloc’d blocks are lost forever)
  • malloc/free/new/delete 申请和开释内存的匹配(Mismatched use of malloc/new/new [] vs free/delete/delete [])
  • src 和 dst 的重叠(Overlapping src and dst pointers in memcpy() and related functions)

cachegrind

cachegrind 是一个 cache 分析器。

它模仿执行 CPU 中的 L1, D1 和 L2 cache,

因而它能很准确的指出代码中的 cache 未命中。

它能够打印出 cache 未命中的次数,内存援用和产生 cache 未命中的每一行 代码,每一个函数,每一个模块和整个程序的摘要。

若要求更粗疏的信息,它能够打印出每一行机器码的未命中次数。

在 x86 和 amd64 上,cachegrind 通过 CPUID 主动探测机器的 cache 配置,所以在少数状况下它不再须要更多的配置信息了。

helgrind

helgrind 查找多线程程序中的竞争数据。

helgrind 查找内存地址,那些被多于一条线程拜访的内存地址,然而没有应用统一的锁就会被查出。这示意这些地址在多线程间拜访的时候没有进行同步,很可能会引起很难查找的时序问题。

产生段谬误的起因

  • 应用野指针
  • 试图对字符串常量进行批改

new 和 malloc 的区别:

在申请内存时

  • new 是一个操作符,能够被重载,malloc 是一个库函数
  • new 在申请内存的时候,会依照对象的数据结构分配内存,malloc 调配指定的内存大小
  • new 申请内存时,会调用构造函数,malloc 不会
  • new 申请内存时,返回对象的指针,malloc 申请内存的时候,返回(void *) 因而须要强转
  • 申请数组的时候,new[],会一次性调配所有内存,调用多个构造函数,因而须要 delete[]来销毁内存,调用屡次析构函数,而 malloc 只能 sizeof(int)*n
  • new 申请内存失败,会抛 bac_malloc 异样,malloc 申请失败则返回 NULL
  • malloc 当调配的内存不够的时候,会应用 realloc 再次分配内存,new 没有这样的机制。
  • new 调配的内存须要用 delete 开释,delete 会调用析构函数,malloc 调配的内存须要 free 函数开释

realloc 的原理:

realloc 是在 C 语言中呈现的,c++ 曾经摒弃 realloc 函数,realloc 函数调配一块新内存的时候,会把原内存中的内存 copy 到新内存中,通过 memmove 的形式

共享内存相干的 api

  • shmget 新建共享内存
  • shmat 连贯共享内存到以后地址空间
  • shmdt 拆散共享内存
  • shmctl 管制共享内存

c++ STL 内存优化

c++11 新个性:

关键字和语法

  • auto 关键字

编译器能够依据初始化来推导数据类型,不能用于函数传参和以及数组类型推导

  • nullptr 关键字

一种非凡类型的字面量,能够被转成任意的其余类型

  • 初始化列表

初始化类的列表

  • 右值援用

能够实现挪动语义和完满转发,打消两个对象交互时不必要的拷贝,节俭存储资源,提高效率

新增容器

  • 新增 STL array,tuple、unordered_map,unordered_set

智能指针,内存治理

  • 智能指针

新增 shared_ptr、weak_ptr 用于内存治理

多线程

  • atomic 原子操作

用于多线程互斥

其余

  • lamda 表达式

能够通过捕捉列表拜访上下文的数据

  • std::function std::bin d 封装可执行对象

避免头文件反复援用:

#ifndef

作用:雷同的两个文件不会被反复蕴含。

长处:

  • 受 C /C++ 语言规范的反对,不受编译器的限度。
  • 不仅仅局限于防止同一个文件被反复蕴含,也能防止内容完全相同的两个文件(或代码片段)被反复蕴含。

毛病:

  • 如果不同头文件中的宏名恰好雷同,可能就会导致你看到头文件明明存在,编译器却说找不到申明的状况。
  • 因为编译器每次都须要关上头文件能力断定是否有反复定义,因而在编译大型项目时,#ifndef 会使得编译工夫绝对较长。

pragma once

作用:物理上的同一个文件不会被反复蕴含。

长处:

  • 防止 #ifndef 中因为宏名雷同导致的问题。
  • 因为编译器不须要关上头文件就能断定是否有反复定义,因而在编译大型项目时,比 #ifndef 更快。

毛病:

  • \#pragma once 只针对同一文件无效,对雷同的两个文件(或代码片段)应用有效
  • \#pragma once 不受一些较老版本的编译器反对,一些反对了的编译器又打算去掉它,所以它的兼容性可能不够好。

继承与组合

  • 继承是面向对象三大基本特征之一 (继承,封装,多态),继承就是子类继承父类的特色和行为,使得子类对象(实例)具备父类的实例域和办法,或子类从父类继承办法,使得子类具备父类雷同的行为,继承强调的是is-a 关系,是 ‘白盒式’ 的代码复用
  • 组合是通过对现有对象进行拼装即组合产生新的具备更简单的性能,组合体现的是整体和局部,强调的是 has- a 的关系,是 ‘黑盒式’ 的代码复用

继承与组合应用场景

  • 逻辑上 B 是 A 的“一种”a kind of

继承(如 男人 继承 人类)

  • 逻辑上 A 是 B 的“一部分”a part of

组合(如 组合 眼 耳 口 鼻 -> 头)

继承与组合区别

  • 在继承中,父类的外部细节对子类可见,其代码属于 白盒式 的复用,调的是 is-a 的关系,关系在编译期就确定
  • 组合中,对象之间的外部细节不可见,其代码属于 黑盒式 复用。强调的是 has-a 的关系,关系个别在运行时确定

继承与组合优缺点

继承

长处:

  • 反对扩大,通过继承父类实现,但会使系统结构较简单
  • 易于批改被复用的代码

毛病:

  • 代码白盒复用,父类的实现细节裸露给子类,毁坏了封装性
  • 当父类的实现代码批改时,可能使得子类也不得不批改,减少保护难度。
  • 子类不足独立性,依赖于父类,耦合度较高
  • 不反对动静拓展,在编译期就决定了父类

组合

长处:

  • 代码黑盒复用,被包含的对象外部实现细节对外不可见,封装性好。
  • 整体类与部分类之间松耦合,互相独立。
  • 反对扩大
  • 每个类只专一于一项工作
  • 反对动静扩大,可在运行时依据具体对象抉择不同类型的组合对象(扩展性比继承好)

毛病:

  • 创立整体类对象时,须要创立所有部分类对象。导致系统对象很多。

函数指针的益处和作用:

益处:简化构造和程序通用性的问题,也是实现面向对象编程的一种路径

作用:

  • 实现面向对象编程中的多态性
  • 回调函数

inline 函数与宏定义

inline 函数是 C ++ 引入的机制,目标是解决应用宏定义的一些毛病。

为什么要引入内联函数(内联函数的作用)

用它代替宏定义,打消宏定义的毛病。

宏定义应用预处理器实现,做一些简略的字符替换因而不能进行参数有效性的检测。

  • inline 相比宏定义有哪些优越处
  • inline 函数代码是被放到符号表中,应用时像宏一样开展,没有调用的开销效率很高;
  • inline 函数是真正的函数,所以要进行一系列的数据类型查看;
  • inline 函数作为类的成员函数,能够应用类的爱护成员及公有成员;

inline 函数应用的场合

  • 应用宏定义的中央都能够应用 inline 函数;
  • 作为类成员接口函数来读写类的公有成员或者爱护成员;

为什么不能把所有的函数写成 inline 函数

  • 函数体内的代码比拟长,将导致内存耗费代价;
  • 函数体内有循环,函数执行工夫要比函数调用开销大;
  • 另外类的结构与析构函数不要写成内联函数。

内联函数与宏定义区别

  • 内联函数在编译时开展,宏在预编译时开展;
  • 内联函数间接嵌入到指标代码中,宏是简略的做文本替换;
  • 内联函数有类型检测、语法判断等性能,而宏没有;
  • inline 函数是函数,宏不是;
  • 宏定义时要留神书写(参数要括起来)否则容易呈现歧义,内联函数不会产生歧义;

总结

  • 分享了内存治理,内存泄露,智能指针
  • 内存泄露检测工具
  • 代码中产生段谬误的起因
  • 内存优化
  • 其余小知识点

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

好了,本次就到这里,下一次 GO 的并发编程分享

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

我是 小魔童哪吒,欢送点赞关注珍藏,下次见~

正文完
 0