进程虚拟地址空间
前提:32位CPU 32位linux内核
2^32(4G)的虚拟地址空间,分别包括用户空间(3G)和内核空间(1G),每一个进程用户空间是私
有的,内核空间是共享的
用户空间:0x08048000开始 .text .rodata .data .bss heap 共享库区域(*.so) stack 命令行参数 环
境变量PATH
内核空间:ZONE_DMA(16M) Direct Memory Access ZONE_NORMAL(896M) .text .rodata. heap
stack
ZONE_HIGHMEM(高端内存 用户空间采用的地址映射方式是二级页表映射,而内核空间的地址映射方
式采用的线性映射,那么1G以上的物理内存如何进行映射,就靠这块高端内存区域了)
堆内存heap分配,从低地址 到 高地址;栈内存stack的分配,从高地址到低地址。data段的内存程序
启动时候分配,程序运行结束内存释放;heap内存是在调用new或者malloc的时候分配,调用delete或
者free的时候释放;调用函数分配新的栈空间,函数出右括号占内存释放。
32位linux创建进程fork(资源划分的单位)的上限:0-32767 进程间的通信(匿名管道、命令管道、
消息队列、共享内存、信号量);创建线程pthread_create,一个进程创建线程的上限数量是多少(线
程栈的大小是8M),3G / 8M = 384 ,如果如何提高进程里面创建的线程的数量???用limit设置系统
创建线程默认的线程栈的大小
一个进程里面创建的线程,每一个线程都有自己的栈空间,但是各个线程共享当前进程的data和heap
堆空间。多线程对比多进程的应用,其好处是线程间通信方式(sem)简单,方便共享变量(放在
heap或者data),而且线程占用的资源少,所以线程的上下文切换所耗费的系统性能也少。
虚拟内存
虚拟内存是操作系统管理内存的一种方案,该方案至少提供了以下几点功能:
1. 给系统运行的每一个进程,都分配4G的虚拟地址空间
2. 保证了所有进程虚拟地址空间的用户空间是隔离的,不能够互相访问
3. 放物理内存紧张的时候,再需要分配物理内存,系统会根据LRU(最近最久未使用)算法,把相应
的物理页面(dirty脏页面-数据被修改过的页面)写入磁盘的swap交换分区当中(产生磁盘
I/O),后面如果再需要使用这一块页面的数据,又会从磁盘的swap交换分区当中把数据再读回物
理页面当中
函数详细的调用过程main -> sum(int a, int b) 函数的运行,系统是需要分配一块栈空间的,系统通过CPU的ebp和esp指针
来标识一个函数栈的栈底和栈顶。
1.从右向左的顺序把sum函数的实参一次入栈
2.把call指令的下一行指定的地址入栈
3.push ebp : 把main函数的栈底地址入栈
4.mov ebp, esp: 让ebp指针指向sum函数的栈底
5.sub esp,4Ch :给sum函数分配栈帧
6.rep stos(for):把[ebp,esp]栈初始化成0xCCCCCCCC(windows的VS编译器会做函数栈的初始
化) gcc/g++不会做栈的初始化
7.执行sum函数的代码...直到sum函数的右括号
8.mov esp, ebp : 把sum函数的栈帧归还给系统
9.pop ebp : 出栈(把main函数的栈底地址出栈了),把出栈的元素赋给ebp,让ebp指向main函数
的栈底地址
10.ret : 出栈(把main函数中刚才调用sum的call指令的下一行指令地址出栈了),赋给CPU的PC寄
存器
11.add esp, 8 : 把sum函数的形参内存交还给系统
C&C++源码编译链接运行的详细过程
编译过程:所有源文件都是分开编译
1.预处理阶段:删除注释,处理所有以#号开头的指令,但除了#progma(link, "动态库")这个指令是告
诉编译器当前程序需要链接哪些库,所以该指令需要持续到链接阶段
2.语义,词法,语法分析,代码优化(gcc/g++ -O3),最终生成汇编指令
3.把汇编代码生成本地(windows/linux)机器码,并形成组成.o文件的各个段(text data 符号表...)
4.编译最终生成二进制可重定位目标文件
链接过程:所有的.o文件.a文件是一起链接的
1.把所有.o文件和.a静态库文件的所有段进行合并,开始做符号解析(符号表中对符号的引用,一定要
在其它的.o文件或者.a文件的符号表中找到符号的定义),符号解析完成,给所有的符号分配虚拟地
址。
2.符号的重定向(经过符号解析,所有的符号都有虚拟地址了,再去text段把所有访问符号时,符号的正
确地址更新上去)
3.产生可以运行在当前平台下的可执行文件 .exe elf格式的可执行文件
运行过程:
1.程序开始运行,把elf格式的可执行文件的text,data,bss映射到当前进程的虚拟地址空间上
2.从elf文件的hear头信息中,读取Entry Address(main函数第一行指令地址)写入CPU的PC寄存器当
中3.CPU开始读取PC寄存器的内容(就是main函数第一行指令地址),CPU发出的地址是虚拟地址,需要
经过MMU(Memory Management Unit)做页目录页表映射(虚拟地址 -> 地址映射 -> 物理地址),
此时的地址映射肯定会产生缺页异常,进入缺页异常处理程序(do_page_fault),这个方法就会分配相应
的物理页面,把磁盘上可执行文件的text段加载到刚分配的这块物理内存页面上,然后把物理页面的信
息写入到相应的页表项当中,然后重启地址映射。
C和C++的区别
1.带默认值参数的函数
2.inline函数
inline函数省了普通函数的调用开销,函数调用效率更高了
inline函数在编译阶段,在函数的调用点,把函数代码进行展开
inline函数只在release版本下起作用,debug版本下不起作用
inline修饰函数,只是一个建议,最终还有由编译器决定是否处理成内联函数
inline函数因为在编译阶段会被展开,所以inline函数是不产生符号的
inline函数和用宏#defifine定义一个函数,区别有什么?
inline函数可以调试的,宏是无法调试的
inline函数是一个独立的函数模块,而宏完全是字符串替换,不太安全
3.const
C语言的const定义的是常变量,除了不能作为左值,其它的情况和普通的变量是一样的
C++语言的const定义的是常量,编译过程中,所有使用常量名字的地方,都会被常量的初始值进行
替换;但是 C++的const常量有时候也会变成常变量(const int a = b),此时就和C语言里面的常变量是一
样的
4.const和指针的转换几大错误公式
const int * -> int *
const int ** -> int**
int ** -> const int **
int * const * -> int**
5.函数重载
C语言不支持函数重载,是因为C语言编译器生成函数符号,只根据函数名生成,所以在C语言当中,
不能定义名字相同的函数。
C++语言支持函数重载,是因为C++编译器生成函数符号,是根据函数名+参数列表生成,那么满足:
函数名相同,参数列表不同的一组函数,就构成函数重载;一组函数要重载,首先必须处在同一个作
用域当中。
C++语言怎么调用C语言写的函数呢? 调用不了,sum_int_int, sum,必须把函数的声明放在extern
"C"里面,sum sum
C语言怎么调用C++语言写的函数呢?调用不了,sum, sum_int_int,需要把C++代码全部扩在
externc "C"里面,sum sum#ifdef __cplusplus // C项目开发的函数接口,都会在函数头文件声明中,添加这样的宏控制
extern "C"
{
#endif
int sum(int , int ) ; // 函数声明
#ifdef __cplusplus
}
#endif
6.函数模板
可以做泛型编程,写一套代码,用任意的类型实例化,实例化的过程是发生在编译阶段的
7.new和delete
new和malloc有什么区别?
malloc是C的库函数,new是一个运算符重载函数operator new
malloc按字节开辟内存,内存开辟失败返回nullptr;new按照类型大小开辟内存,内存开辟失败抛出
bad_alloc类型的异常
malloc只能开辟内存,无法做初始化;new可以开辟内存+内存初始化
new底层开辟内存,调用的就是malloc
delete和free的区别?
free是C的库函数,delete是一个运算符重载函数operator delete
free只能释放内存;delete先调用析构函数,再释放内存
free不区分单个元素内存和数组内存的释放;delete是需要区分的,delete p; delete []p; 主要是因为
delete需要直到调用多少次析构函数
delete底层释放内存调用的就是free
8.namespace名字空间作用域
C语言只有函数局部作用域和全局作用域
C++语言有函数局部作用域,namespace MyName{}全局的名字空间作用域,类作用域
#include < iostream >
using namespace std; // using指示符
using std::cout; // using声明,只能声明一个符号
cout<<"hello world!"<<std::endl;
C++类和对象
1.类的成员变量
普通的成员变量 构造函数的函数体、构造函数的初始化列表const常成员变量 构造函数的初始化列表
static静态成员变量 只能在类外定义并初始化
2.成员方法
普通的成员方法:编译会产生 类类型 *this,调用必须以来一个对象
const常成员方法:编译会产生 const 类类型 *this,在方法中不能修改成员变量的值,调用也依赖对象
static成员方法:编译不会产生this指针,用类作用域调用
this指针有什么作用?
一个类产生的多个对象,它们有自己的一份成员变量,但是共享一套成员方法,在成员方法中,就是通
过this指针来区分,当前访问的到底是哪个对象的成员(包括访问其它的成员变量或者成员方法)
对象发生浅拷贝是不是一定会产生问题?不一定,主要看对象有没有占用除对象内存之外的其它资源。
如果浅拷贝有问题,造成多次析构时,对同一个资源释放多次,那么有如下解决办法:
1.把拷贝构造函数和operator=函数声明到private里面
2.利用C++11的语法,把拷贝构造函数和operator=函数直接delete
3.自定义拷贝构造函数和operator=赋值运算符的重载函数,也应当提供相应的带右值引用参数的拷贝
构造函数和operator=赋值运算符的重载函数
对象生命周期,把课堂代码拿出来看一下进行复习。
1.函数调用传递参数的时候,如果传递的时对象,要按对象的引用进行传递
2.函数返回对象的时候,应该优先返回一个临时对象
3.接收返回类型是对象的函数调用时,优先按照初始化的方式接收
对象的拷贝构造函数的参数能不能按值传递呢?String(String str) String(const String &str) ,无法实
现的,编译器直接编译报错,因为调用拷贝构造函数,需要先生成形参对象,但是形参对象要调用拷贝
构造生成,所以拷贝构造函数是按照引用传递的!
模板
函数模板和类模板
模板的意义:可以进行泛型编程,只用注重算法的实现,不用关心数据的类型,只用写一套代码,就可
以用任意的类型进行实例化(编译阶段, 静态的多态:模板和函数重载)
函数模板和类模板 -> 调用点进行实例化 (根据用户传入的<>的类型进行实例化,但是函数模板不一定
需要指定实例化的类型参数,因为函数模板可以进行模板的实参推演;类模板的模板参数可以给默认
值,这一点函数模板不支持) -> 编译器会根据实例化的类型,产生一份专门处理该类型的模板函数或
者模板类出来进行编译 -> 模板的实例化是发生在编译阶段的。
当编译器针对某种类型实例化的模板函数或者模板类,如果处理该类型的代码逻辑有问题,可以给相应
的函数模板或者类模板提供该类型的模板特例化实现。运算符的重载
意义:让对象的运算表现得和内置类型一样。因为我们写得模板代码,将来用什么类型实例化是不知道
的,有可能是编译器的内置类型,也有可能是自定义的类类型,只要我们给类对象提供相应运算符的重
载函数,那么当用对象类型实例化模板以后,编译器就能够正常编译对象的各种运算了。
template < typename T >
T sum(T a, T b){return a + b;}
string字符串类型
#include < string >
string str1 = "hello";
string str2 = str1 + "bbbbbb";
str1 > str2 str1 < str2 str1 == str2
char& operator[ ] ( int index ) char ch = str1[2] str1[2] = 'y'
str1.size()
const char* p = str2.c_str();
近容器(没有归纳标准容器里面) 支持迭代器 foreach(C++11)
for(char ch : str2){} =>
auto it = str2.begin();
for(; it != str2.end(); ++it){ *it; }
int index = str3.fifind_fifirst_of(','); // 找指定字符在string里面出现的位置的下标
fifind_last_fifirst_of
string str3 = str1.subString(1); // ello
string str4 = str1.subString(1, 4); // ello
智能指针
防止资源泄漏,智能指针可以保证资源一定会进行释放。利用栈上的智能指针对象出作用域自动析构的
特点,在析构函数里面添加释放资源的代码。
C++标准库 auto_ptr
Boost库(C++11以后,把常用的智能指针都纳入C++标准库了)
不带引用计数的智能指针
不带引用计数:一个资源只能用一个智能指针指向auto_ptr< int > ptr(new int); auto_ptr< int > ptr2(ptr1); ptr2指向资源,ptr1直接被置空
不建议使用auto_ptr,而且不能用auto_ptr来实例化容器 vector< auto_ptr< int >> vec;
scoped_ptr< int > ptr(new int); 直接把拷贝构造函数和operator=函数直接delete(C++11)
unique_ptr< int > ptr(new int);
把带左值引用的拷贝构造函数和operator=delete掉,但是提供了带右值引用参数的拷贝构造和
operator=函数,意味着可以用临时的(或者局部的)unique_ptr构造或者给另一个unique_ptr对象赋
值。
unique_ptr< int > func()
{
unique_ptr< int > ptr(new int);
return ptr;
}
int main()
{
unique_ptr< int > ptr = func();
return 0;
}
unique_ptr< int > func(unique_ptr< int > p)
{
unique_ptr< int > ptr(std::move(p);
return ptr;
}
int main()
{
unique_ptr< int > p(new int);
unique_ptr< int > ptr = func(std::move(p)); // 强转成&&
return 0;
}
s't'd::move:移动语义函数,可以把一个左值类型专程右值类型
std::forward:类型完美转发(参考博客)
带引用计数的智能指针带引用计数:一个资源可以用多个智能指针指向,为什么它不会重复释放资源呢?因为它会对资源的引
用计数进行线程安全你的++和--,智能指针对于资源的引用计数的线程安全保证,是通过 互斥锁? 基
于CAS的AtomicInt 原子整型类型定义的整形变量实现的线程安全。
shared_ptr:强智能指针,会改变资源的引用计数
weak_ptr:弱智能指针,不会改变资源的引用计数
weak_ptr -> shared_ptr -> 资源
代码上能不能直接都使用shared_ptr强智能指针呢?会造成强智能指针的交叉引用问题,结果就是智能
指针对象无法析构,那么资源也就无法释放,资源泄漏!!! 那应该怎么使用???
定义资源的时候,用强智能指针;其它地方引用资源的时候,用弱智能指针!
通过弱智能指针能不能直接访问资源呢???不能 weak_ptr operator* operator->
shared_ptr< T > sp = wp.lock(); // 把弱智能指针提升成强智能指针,实际上是在资源的引用计数是否
为0
if(sp != nullptr) // operator T*() {return ptr;}
{
// 资源还在,lock提升成功!
}
new和delete
new的四种操作:
new int ; 抛异常
new nothrow int; 返回nullptr
const char *p = new const int; 开辟常量
new (addr) int; 定位new
new:operator new重载函数开辟内存(malloc)、调用相应的构造函数
delete:调用对象的析构函数、operator delete(free)释放内存
什么时候new和delete不能混用?什么时候可以?
int *p = new int; delete p; delete []p;
int *p = new int[100]; delete p; delete []p;
如果A类型没有提供析构函数
A *p = new A; delete p; delete []p;
A *p = new A[100]; delete p; delete []p;
不能混用的前提:自定义类型,而且提供析构函数!!!A *p = new A; delete p; delete []p; // p-4 free(p-4)
A *p = new A[100]; delete p; delete []p; // p-4 free(p-4)
一个A 4 404 4个字节(记录对象个数) + (p)400
继承与多态
组织类和类之间的关系:组合(a part of...)、继承(a kind of...)
继承的好处:
1.代码的复用
2.基类里面可以给所有派生类提供统一的纯虚函数接口,派生类可以根据自己的情况进行不同的纯虚函
数的重写,实现不同的功能,方便使用多态
10个派生类 设计接口函数 (全部使用基类类型)
重载、隐藏、覆盖
重载:
1.必须处在同一个作用域
2.函数名相同,参数列表不同的一组函数,构成函数重载
隐藏(派生类不仅仅继承了基类的成员,还继承了基类的作用域)隐藏的是作用域:
1.基类和派生类的同名成员
2.通过派生类对象调用同名成员,调用的永远都是派生类自己的同名成员(派生类的同名成员把基类的
同名成员给隐藏掉了),如果派生类对象想调用基类的同名成员,要在成员名字前面添加基类的作用
域。
覆盖:
1.基类和派生类作用域当中
2.基类和派生类的函数名字相同,函数返回值相同,函数参数列表也相同,而且基类的函数是虚函数,
此时构成同名覆盖(在虚函数表中覆盖)关系
虚函数
在成员方法名字前面添加virtual关键字,成员方法就成为一个虚函数,带虚函数的类是怎么编译的?
在编译过程中,会给这个类型产生一张虚函数表vftable,vftable中主要存放[虚函数的地址,RTTI*],
当运行时,vftable加载的内存的.rodata区域,这个类型实例化的对象,都会增大4个字节,在对象头部
放一个vfptr,存储的是vftable虚函数表的地址,一个类型定义的n个对象,指向的是该类型唯一的虚函
数表。
一个类类型里面增加虚函数的个数,对象的大小不变,虚函数表的内存会增大!
派生类在编译的时候,由于从基类继承来了虚函数,所有派生类类型也要产生对应的vftable,里面放了
基类继承来的虚函数的地址,但是如果派生类提供了同名覆盖方法的实现,此时就会用派生类同名覆盖
方法的地址,把从基类继承来的虚函数方法的地址在vftable中给覆盖掉。构造函数能实现成虚函数吗?不可以,因为调用构造函数的时候,对象不存在,无法进行虚函数的动态
绑定
析构函数能实现成虚函数吗?可以,因为析构函数调用的时候,对象是存在的
static成员方法能实现成虚函数吗?不可以,因为static方法不依赖对象,根本无法动态绑定
inline方法能是现成虚函数吗?可以的,virtual inline是可以共同出现的,如果inline函数添加virtual
了,此时函数就不内联了,和普通函数一样,debug/release。
静态绑定和动态绑定 绑定=》函数调用
静态绑定:指的是函数调用,在编译阶段就是确定的了,体现在汇编指令上,就是call + 函数的地址,
如全局函数调用,普通成员函数调用,static成员函数调用,用对象本身调用虚函数都是静态绑定。
动态绑定:指的是函数调用,在编译阶段无法确定,只能编译成通过指针或者引用,访问指向对象的头
4个字节vfptr,再通过vfptr访问vftable,在vftable里面取相应的函数的地址,装入寄存器eax,生成汇
编指令 call eax,因为eax寄存器放的是哪个函数的地址,只有在运行的时候才能取到,因此这样的函
数调用(函数绑定)是动态绑定,也叫运行时期的函数绑定。动态绑定有一个前提:用指针或者引用调
用虚函数!
解释多态
1.静态(编译)的多态:模板和函数重载
2.动态(运行)的多态:用基类的指针(同引用)指向不同的派生类对象,然后调用同名覆盖方法,基
类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法(底层依赖函数的动态绑定机
制),这就叫多态。我们在设计模块代码、对象的函数接口的时候,可以统一的用基类类型指针(或引
用)来进行设计,可以接收任意的派生类对象,还可以通过动态绑定,调用相应派生类的方法,能够做
到软件设计需要遵守的“开-闭”原则,对扩展开放,对修改关闭。
基类的析构函数什么时候需要定义成虚析构函数
Dervice d;
Base *p = &d;
Base *p = new Derive();
delete p; // Base Base::~Base()
基类指针指向堆上创建的派生类对象的时候,当delete 指针的时候,由于指针是基类类型,而且基类的
析构函数是个普通函数,造成派生类对象的析构函数无法调用,造成资源泄漏。此时需要把基类的析构
函数定义成虚函数,那么派生类的析构函数也就自动成为虚函数了,这时候delete基类指针,看到基类
类型的是虚析构函数,析构函数的调用就成动态绑定,因为基类指针指向的是派生类对象,所以最终访
问的是派生类的虚函数表,最终调用到派生类的析构函数,再自动调用基类的析构函数。
抽象类是什么?为什么要定义抽象类?一般哪些类被定义成抽象类?
抽象类是不能被实例化的类,因为拥有纯虚函数, virtual void func() = 0;一般把不需要实例化的类,定义成抽象类,基类的作用,1是让派生类做代码复用,2是给所有派生类保
留统一的纯虚函数接口,等待派生类重写,所以基类的定义并不是为了抽象某个实体类型而得来的,所
以基类一般不需要实例化,因为不代表任何实体,所以基类的接口方法也不知道怎么实现(直接定义成
纯虚函数),所以一般基类定义成抽象类。
多重继承、虚基类
C++是支持多重继承的,好处:复用更多的代码,坏处:使派生类拥有间接基类多份成员变量(菱形继
承),如何解决这个问题?
所有从间接基类直接继承的地方,都采用虚继承的方式,基类被虚继承以后,在派生类中,虚基类数据
只存储一份,存储在派生类内存的最后面,在原来放基类成员数据的地方补充一个vbptr,vbptr指向一
张vbtable,vbtable主要存放了当前位置和虚基类数据存储的位置的偏移量。这样就能保证,在派生类
中,只有一份间接基类(虚基类)的数据。
A
B C => 这一层的两个类的成员同名
D =》 无法直接访问的,访问的时候,需要在成员名字添加基类(B/C)的作用域
C++的四种类型转换
const_cast: 去掉常量属性的
static_cast:只能做编译器认为类型安全的转换
reinterpret_cast:类似C语言的类型强转,没有安全保证
dynamic_cast:支持RTTI运行时类型信息的类型转换
什么时候识别静态类型?什么时候识别RTTI类型?
Base *p = new Derive();
typeid( * p ).name() typeid(p).name() : Base*
主要看Base里面有没有虚函数!!!
Base如果没有虚函数,那么上面识别的静态类型,Base
Base如果有虚函数,那么上面识别的就是RTTI类型,p -> new Dervice(vfptr) -> vftable -> RTTI* ->
"Derive"
C语言是否支持多态?C语言如果让你实现多态,怎么实现?(函数指针)
C++的异常处理
异常处理涉及的关键字:
try:把可能抛出异常的代码括起来 catch:必须紧跟在try后面,一个try可以有多个catch,主要用来捕获相应类型的异常,进行处理,处
理完成后,代码逻辑继续运行
throw:跟变量/对象,表示抛出了一个相应类型的异常
try
{
}catch(int err){}
catch(string err){}
catch(double err){}
什么是异常的栈展开过程?
main->func1->func2..............funcn-1 -> funcn 函数的调用链
当funcn抛出一个异常,首先在funcn上找是否有处理该异常的catch块,如果有,处理完该异常,代码
继续向下执行,如果无法处理异常,沿着函数调用链向上抛,在每一个函数调用栈上都做相同的事情,
如果抛到main函数,也无法处理该异常,那么直接把异常抛给系统,系统发现当前进程有一个异常未
被处理,会调用abort系统函数,强制终止当前进行的执行。
如何防止抛出异常后造成的资源泄露?
try
{
int *p = new int; // unique_ptr< int > p(new int);
// throw异常了
delete p;
}catch(int err){}
catch(string err){}
catch(double err){}
采用智能指针保证出try块资源一定会得到释放。
构造函数能不能抛异常?析构函数能不能抛异常?
构造函数不能抛出异常,因为如果抛出,说明对象构造失败了,那么对象出作用域,是不会调用析构函
数的,造成资源泄露,如果全部采用智能指针管理资源,即就是对象的析构无法调用,也不影响资源的
释放。
析构函数如果抛出异常,造成当前对象的成员对象都无法调用析构函数了,造成资源泄漏。析构函数如
果真的抛出异常,只能在析构函数本身把异常处理掉!!!此时因为异常已经被处理了,所以当前对象
析构完,成员对象可以依次正常进行析构。
C++ STL六大组件:
标准容器(空间配置器allocator): 顺序容器、容器适配器、关联容器
迭代器
泛型算法
函数对象(仿函数)
顺序容器:
vector:向量容器,内存可以2被扩容的数组 0-1-2-4-8-16。。。 reserve(100) 内存空间预留函数 /
resize(100)
push_back/pop_back insert/erase back(获取末尾元素的值)
iterator
operator[]
swap交换两个容器的元素
deque:双端队列容器,内存是一个动态开辟的二维数组,扩容主要扩容的是第1位(2倍扩容)
push_front/pop_front push_back/pop_back insert/erase back(获取末尾元素的值)
iterator
swap交换两个容器的元素
list:双向链表容器,内存是一个循环的双向链表
push_front/pop_front push_back/pop_back insert/erase back(获取末尾元素的值)
iterator
swap交换两个容器的元素
容器适配器 没有自己底层的数据结构,依赖顺序容器实现,不支持迭代器iterator
stack(deque):push pop top size empty
queue(deque) :push pop front back size empty
priority_queue(vector):默认构建一个大根堆 push pop top size empty
哈希统计+大/小根堆 找海量数据的top k元素或者第top k个元素
有序和无序关联容器
红黑树 O(log_2n)
set/multiset 集合,只存储key
map/multimap 映射表,存储[key,value]
链式哈希表 O(1)unordered_set/unordered_multiset
unordered_map/unordered_multimap
集合:insert(20), erase(20), fifind(成员方法),iterator
映射表:insert({key, value}) insert(make_pair(key,value)) erase(key) auto it = fifind(key) it->fifirst
it->second , iterator
近容器:char[] 、string、bitset位容器
容器的空间配置器allocator,主要负责管理容器底层的内存开辟,释放,对象构造和析构
allocate : 负责开辟内存
deallocate : 负责释放内存
construct : 定位new构造对象
destroy : 析构对象
语言类:
熟练使用C和C++,对C的内存管理和指针的应用比较熟悉,熟悉面向对象编程,熟悉继承多态、C++
STL组件(。。。。)、智能指针,以及常用的设计模式
操作系统类:
协议类:
数据结构类:
MySQL数据库
关系型数据库表的基本设计:
表用来描述实体的属性的,关系型数据库中存储的都是二维表,行称作记录,列称作属性/字段
实体和实体之间(对应表与表之间的)的关系有:
1.一对一的关系
table1 父表
id name age sex
1 张三 20 ‘男’
2 李四 22 ‘女’
table2 子表
uid(外键约束) idcard email
2 10002 123@163.com当查询id=2的人的所有信息时,name,age,sex;idcard,email
select name,age,sex from table1 where id=2
select idcard,email from table2 where id=2
select a.name,a.age,a.sex,b.idcard,b.email from table1 a inner join table2 b on a.id=b.uid
where a.id=2
select * from table1 where age=20 order by name asc/desc
2.一对多的关系
用户(user)
id name age sex
订单(orderlist) 子表的外键关联的是父表的主键
uid(外键) orderid date address account
写一个SQL语句,把id为2的用户的基本信息+所有的订单信息输出一下
select * from user a inner join orderlist b on a.id=b.uid where a.id=2
user
id name pwd
friendlist
uid friendid
1 2
1 3
1 4
3.多对多的关系 =》 一定要产生一张的新的中间表
商品(product) id name price
1 手机 1000.0
2 电脑 5000.0
订单(orderlist)orderid date address account totalprice id number
productprice
100 2019.8.8 曲江校区 2 6000.0 1 2 2000.0
100 2019.8.8 曲江校区 2 6000.0 2 1 5000.0
上面订单表直接存储商品的详细信息,造成订单数据冗余,所以一定要生成中间表,如下:
productorderlist
orderlist id number productprice
1000 2 3 30.0
1000 4 1 50.0关系型数据库表的范式设计
指导开发者,如何设计一个比较完美的二维表,综合了效率和数据冗余问题。
范式一:每一列保持原子特性
id name pwd sex addressid
1
id name pwd sex
id country provice city street
1 中国 陕西省 西安市 曲江街道
uid = 3 人的address信息
select b.country,b.provice,b.city.b.street from user a inner join address b on a.addressid = b.id
where a.id=3
范式二:属性完全依赖于主键(主要针对联合主键)
范式二主要是针对联合主键说的,意思是非主属性,一定要依赖联合主键的每一个字段,而不能只依赖
一部分主键字段,否则设计出来的表存在严重的数据冗余。
学号, 姓名, 年龄, 课程名称, 成绩, 学分
100 张 20 C++开发 80.0 10
101 往 21 C++开发 90.0 10
学号, 姓名, 年龄,考试总成绩 =》 user
课程id,课程名称, 学分 =》 course
userid,courseid,score =》 studentscore
select a.name,a.age,count(b.score) from user a join studentscore b on a.id = b.userid
范式三:属性不依赖于其它非主属性
表中所有的非主属性,都必须依赖于主键,而不能依赖于其它的非主属性,否则又要造成数据冗余。
学号(primary key), 姓名, 年龄, 学院id
学院id 学院地址, 学院地点, 学院电话
遵守范式设计,设计出来的表,其数据冗余程度是比较低的,在表进行CURD的时候,效率也是比较高
的,一般设计能达到范式三就可以了。并不是说范式越高越好,范式越高,拆分的表也就越来越多,意
味着每一次的CURD都需要经常做多表的联合操作,比直接单表操作效率肯定要低一些,所以表的设
计,要综合数据冗余成都和CURD的效率,所以说一般达到范式三就可以了。
范式四:消除表中的多值依赖id name age sex
1 张 22 男
2 刘 23 女
skillid skillname
1 C
2 C++
3 Java
userid skillid
1 3
1 2
1 1
BC范式:每个表中只有一个候选键
候选键的意思就是表中每一行记录该字段的值都不一样
userid username email/cardid
实际的项目开发中,表的设计有的时候刻意的会设计一些字段存储冗余数据,就是为减少多表联合操作
的次数,提高效率。
存储引擎MyISAM和InnoDB
MySQL区别于其它的关系型数据库(SQL Server、Oracle、SQLite(运行在当前进程内部的))最大
的一个特点,就是其插件式的存储引擎。
MySQL:RDBMS =》 create database Chat; =》 create table user()
创建的二维表需要进行存储,存储在磁盘上,都要存储user.frm(desc user/show create table
user),user表的数据,user表的索引。不同的存储引擎,指的是上面三个内容的存储方式不同。
MyISAM特点:
1.把数据和索引是分开存放的,user.frm,user.MYD,user.MYI三个文件
2.所以MyISAM支持的索引结构是:非聚集索引
3.MyISAM是不支持事务、外键
4.MyISAM支持的事务并发操作锁的粒度:表锁,所以并发效率不高,但是它比较安全,不会出现高并
发带来的死锁问题
InnoDB特点:
1.把数据和索引放在一起的,user.frm,user.ibd两个文件,在ibd文件中,存储了数据和索引
2.所以InnoDB支持的索引结构是:聚集的索引3.InnoDB支持事务处理、外键设置
4.InnoDB支持的事务并发操作锁的粒度:行锁,所以并发效率高,但是有可能产生死锁问题,需要在
事务处理的过程中,仔细的考虑事务处理具体过程。
Memory:基于内存存储的存储引擎(frm-磁盘,数据和索引-内存)
MySQL的日志系统
二进制日志(binlog):记录了当前数据库操作过程中所有的CURD语句。
查询日志:记录了所有select语句的操作。
错误日志:主要mysql server启动、关闭、运行过程中出现的一些严重的error。mysql server:3306
redis:6379
慢查询日志:slow_query_time 表示慢查询的时间设置,所有查询时间超过slow_query_time 的select
语句,都会被记录在慢查询日志当中,然后可以通过explain + SQL语句,来查看SQL语句的执行计划,
根据打印的结果,查看索引是否使用正确,有没有额外的fifile sort等等,然后进行SQL语句优化。
SQL和索引优化和实现原理
你项目中有没有做过SQL或索引优化?具体怎么做的?
开启慢查询日志,设置一个项目中能接受、合适的慢查询时间,那么运行项目一段时间,查看慢查询日
志里面所有查询超值指定时间的select语句,然后用explain查看以下SQL语句的执行计划,再做具体的
分析,是做了整表搜索(那就要考虑是否要创建索引),或者索引没用到(考虑SQL语句的书写是否正
确),再进行问题修改。
在业务执行过程中,查看CPU或者磁盘I/O如果过高,考虑表的索引创建太多了(需要优化索引结
构),另外考虑表的数据过大了(考虑分表操作)。
1.对于单个的SQL语句,给作为条件过滤的列创建索引,如果当前SQL语句,除了where子句,还有排
序order by或者分组group by,考虑创建联合索引。
2.创建的索引是不是一定会用到,MySQL Server会进行语句优化,如果使用索引查询的结果过多,就
直接做整表查询。
3.select * from student where score >=90.0 or score <60.0 如果score创建过索引,是否能用到?也
可能会用到,因为MySQL Server会把上面的or子句,优化成
select * from student where score>=90.0 union select * from student where score<60.0 合并查询
4.select * from student where id in (1,2,3,4,5,6,7,8)
select * from student where id in (select uid from studentscore where score >= 60.0)
带in的子查询 , 可以使用索引
select * from student where id not in (select uid from studentscore where score >= 60.0)
not in用不到索引,一般把上面的语句优化成in子查询,或者优化成外连接查询
5.select * from student where age>20 and score>80.0 and sex='女';注意:依次查询一张表,只能使用一个索引,MySQL Server首先会通过age , score, sex的索引进行过
滤数据,哪个过滤出来的数据少,就用哪个索引
6.select * from user where score between 60.0 and 80.0 可以用索引
7.select * from user where address like '陕西省*'; // * %
select * from user where address like '*东大街';
like+通配符,如果通配符在最前面,无法使用索引;如果通配符在后面,可以使用索引。
select * from usermessage where message like ' 旅游:* 旅游 * ';
8.select * from user where cellphone = 18256781234; 如果SQL语句涉及了类型转换,那么索引就用
不上了。
9.多表的查询
id name pwd sex addressid
1
id name pwd sex
id country provice city street
1 中国 陕西省 西安市 曲江街道
uid = 3 人的所有信息
select a.*,b.country,b.provice,b.city.b.street from user a inner join address b on a.addressid = b.id
where a.id=3 // a 20(过滤以后成5行) b 10
多表连接查询的索引是怎么使用的?
在多表连接查询的时候,先区分大表和小表(这里的大小指的就是表的记录的个数),小表是整表搜索
的,上面假设a是10行,b是20行,那么a就是小表了,把a进行整表搜索,找到所有的addressid ,然
后在大表b里面进行查询,找符合a.addressid = b.id条件的b表的记录。
小表决定查询次数,大表决定查询时间,小表已经要整表搜索了,所以没必要创建所以,大表相应的条
件过滤字段一定要创建索引!!!!! 通过explain具体分析以下SQL的执行计划!!!
当你给一张表的某一列创建索引,会生成一颗B+树(平衡树),会用添加索引的列作为排序的字段,生
成一颗B+树,提高搜索的效率。
聚集索引:MySQL采用的存储引擎是InnoDB,数据和索引是放在一个文件当中,可以理解为数据就直
接放在索引树上
非聚集索引:MySQL采用的存储引擎是MyISAM,数据和索引是分开放在不同的文件当中,可以理解为
索引单独存储在索引的B+树上,而数据是要单独访问的
主键索引:因为设置primary key本身就会给主键创建索引,由主键构成的索引树,就称作主键索引
select * from user where id=3
InnoDB:通过搜索主键索引树,找到id=3的索引了,主键所在的该行记录的值都被拿到了
MyISAM:通过搜索主键索引树,找到id=3的索引了,此时拿不到数据,只能得到数据所在的地址(磁
盘的位置MYD),再去读取相应的数据记录。
辅助索引:给除主键之外的其它的列创建索引。辅助索引树上,存储的是辅助索引+主键值id name(name_index) age sex
select * from user where name="zhang san"
InnoDB:通过搜索辅助索引树,找到name="zhang san"的索引了,就得到这一行记录的主键值id,然
后再去主键索引树上,搜索对应id的用户的所有信息
select id from user where name="zhang san" select id,age from user where name="zhang san"
MyISAM:通过搜索辅助索引树,找到id=3的索引了,此时拿不到数据,只能得到数据所在的地址(磁
盘的位置MYD),再去读取相应的数据记录。
InnoDB存储引擎:主键索引和辅助索引的存储方式是有区别的
MyISAM存储引擎:主键索引和辅助索引的存储方式是一样的
索引的底层实现原理
MySQL的Memory存储引擎,采用哈希索引(哈希索引最大的缺点,就是对于SQL语句的区间范围查
找,只能做整表搜索)
MyISAM和InnoDB存储引擎,采用的都是B+树索引。
MySQL最终为什么要采用B+树存储索引结构呢,那么看看B-树和B+树在存储结构上有什么不同?
1.B-树的每一个节点,存了关键字和对应的数据地址,而B+树的非叶子节点只存关键字,不存数据地
址。因此B+树的每一个非叶子节点存储的关键字是远远多于B-树的,B+树的叶子节点存放关键字和数
据,因此,从树的高度上来说,B+树的高度要小于B-树,使用的磁盘I/O次数少,因此查询会更快一
些。
2. B-树由于每个节点都存储关键字和数据,因此离根节点近的数据,查询的就快,离根节点远的数据,
查询的就慢;B+树所有的数据都存在叶子节点上,因此在B+树上搜索关键字,找到对应数据的时间是
比较平均的,没有快慢之分。
3. 在B-树上如果做区间查找,遍历的节点是非常多的;B+树所有叶子节点被连接成了有序链表结构,因
此做整表遍历和区间查找是非常容易的。
MySQL为什么不采用红黑树/AVL树做底层索引存储结构呢?而选用B+树!
AVL树:2阶的平衡树 1个数据域+2个地址域
B+树:m阶平衡树 m-1个数据域+m个地址域(m:300-500之间的 16K 4K 一般一次磁盘I/O读取一
个磁盘block块,就刚好放在了B树的一个节点上)
10000000 AVL 24 最差的情况下,从AVL上读取一个索引,需要花费24次磁盘I/O
B+树 m:400 3层(400^3) 从B+树上读取一个索引,最多花费3次I/O
事务处理
事务:表示一组SQL语句,一个事务中的所有的SQL语句执行成功,最终事务的结果才能提交,如果一
部分成功,一部分失败,事务最终要回滚(binlog重做日志)到事务开始之前的状态。
A:800B:400
事务的ACID特性
1.原子性:一个事务中的所有的SQL语句执行成功,最终事务的结果才能提交,如果一部分成功,一部
分失败,事务最终要回滚(binlog重做日志)到事务开始之前的状态。
2.一致性:事务处理完成后,数据库的数据状态从状态A转成状态B,但是数据的总量是不变的。事务并
发操作带来的数据不一致性,主要体现在三个方面:脏读,不可重复度,幻读。
脏读:事务B读取了事务A还未提交的数据。(一定要防止的)
不可重复读:一个事务的执行过程中,两次相同条件的查询,但是查询的结果不一样,因为两次查询的
中间,有其它事务对相同条件的数据进行了更新。(oracle)
幻读:同一个事务的两次查询,第二次查询出现了之前没有的数据,说明中间有其它事务增加了满足相
同条件的记录。
3.隔离性:为了让事务的并发执行更安全,MySQL给事务提供了以下隔离级别的定义:
事务隔离级别 脏读 不可重复度 幻读
未提交读 x x x
已提交读 ok x x oracle的事务隔离级别默认工作在“已提交读”级
别上
可重复度 ok ok x MySQL的事务隔离级别默认工作在“可重复度”级
别上
串行化 ok ok ok
事务的隔离级别越高,效率越低,但是也更安全;隔离级别越低,效率越高,但是数据越不安全
4.持久性:事务更新数据成功以后,不管MySQL数据库发生任何异常,都要保证数据是能够恢复的。
隔离级别的原理 - S锁,X锁,间隙锁
S锁 - 共享锁(读)
X锁 - 排它锁(写)
串行化完全利用的就是S锁和X锁。
已提交读和可重复度都是通过MVCC(多版本并发控制,给所有的记录都打了版本标签)
间隙锁是给表的最后一行记录的后面,称作“间隙”的地方加锁,防止串行化事务隔离级别下幻读的发
生。
高级数据结构
BST树:二叉搜索树
AVL树:平衡二叉树,在BST树的基础上,引入了平衡(节点的左右子树高度差不超过1)
保持节点平衡,总共引入四种旋转操作:
1.左孩子的左子树太高 =》 右旋转操作2.左孩子的右子树太高 =》 左-右旋转操作(左平衡)
3.右孩子的右子树太高 =》 左旋转操作
4.右孩子的左子树太高 =》 右-左旋转操作(右平衡)
红黑树(不是平衡树,任意节点的左右子树高度差,长的不超过短的2倍):
五个性质 1.颜色 2.nullptr是黑色 3.roog是黑色 4.不能右连续红色节点 5.root-leaf每一条路径的黑
色节点的数量是一样的
红黑树的插入操作(先看叔叔节点的颜色):
1.叔叔节点是红色,把父节点和叔叔节点都涂成黑色,把祖先节点涂成红色,从祖先节点开始继续向上
调整
2.叔叔节点是黑色,祖先,父亲和新插入节点,在同一侧,交换父亲和祖先节点的颜色,一次旋转就完
了
3.叔叔节点是黑色,祖先,父亲和新插入节点,不在同一侧,先以父节点为起始节点进行一次旋转,再
以祖先节点为根节点执行第2步
红黑树的删除操作(先看兄弟节点的颜色):删除的节点在左边(右边同理,方向相反)
1.如果删除黑色节点,如果补上来一个红色节点,直接把这个红色节点涂黑就可以了
2.如果兄弟是黑色节点,兄弟的两个孩子也是黑色节点,兄弟那里没有办法借黑色节点。此时把兄弟设
置成红色,指针指向父节点,继续调整(父节点红色,把父节点直接涂黑。)
3.如果兄弟是黑色节点,兄弟的的孩子是左黑右红(左红右红),直接把兄弟节点和父节点的颜色进行
交换,再以父节点为根节点进行左旋转操作
4.如果兄弟是黑色节点,兄弟的的孩子是左红右黑,需要把兄弟节点和它左孩子的颜色进行交换,然后
以兄弟节点为根节点,进行右旋转操作,然后再执行上面第3步
5.如果兄弟节点是红色,兄弟节点此时无法借调黑色节点,但是红色的兄弟,其孩子一定都是黑色节
点,所以先以当前删除节点的父节点为跟,进行左旋转操作,就使红色兄弟的黑色孩子,成为当前节点
的一个黑色兄弟了,再执行上面的相应操作。
红黑树 =》 AVL 插入删除的旋转次数是可控的, 插入最多旋转2次,删除最多旋转3次。
分治算法:把大规模的数据,进行规模缩减,每一个子规模是不重复的,直到规模缩减到解是已知的,
然后再进行回溯,最终得到规模n问题的解。
动态规划:处理方式和上面的分支算法思想一样,但是能用动态规划解决的问题,有两个特点
1.子规模划分的问题,规模是有重复(某些子规模的问题被重复求解多次, dp数组)
2.具有最优子结构的性质
回溯算法:
1.子集树 在一个数组中,找三个数字,让他们的和为0,找出所有的可能的解打印出来,不能出现重
复的解
1 0 -1 1 -1 0
2.排列树 最终还是输出所有的元素,但是题目对于元素的排列顺序有一定的要求

本文详细探讨了C++中的内存管理,包括进程虚拟地址空间的划分,用户空间与内核空间的隔离,以及堆、栈、数据段的内存分配。文章还介绍了函数调用过程中的栈帧、虚函数表、动态绑定和异常处理。此外,讲解了C++的模板、智能指针、STL容器和多态等核心概念。同时,涵盖了Linux内核中的页表映射、缺页异常处理和动态内存分配。最后,简要分析了C++中的异常处理和STL组件,以及数据库中的索引和事务处理。
2527

被折叠的 条评论
为什么被折叠?



