├── 基础语言 ├── utils │ ├── mtu.png │ ├── 语言.png │ ├── 多虚继承.png │ ├── 菱形继承.png │ ├── 计算机基础知识.png │ └── 项目基础知识.png ├── README.md ├── STL │ └── README.md ├── C++编译底层 │ └── README.md ├── C++智能指针_异常机制_强制转换_RTTI │ └── README.md ├── C++基础知识 │ └── README.md └── C++面向对象 │ └── README.md ├── 计算机基础知识 ├── 基本手写代码 │ ├── 堆排序.cpp │ ├── 归并排序.cpp │ ├── 插入排序.cpp │ ├── 二分查找法.cpp │ ├── README.md │ ├── 非递归二叉树遍历.cpp │ ├── 智能指针的设计与实现.cpp │ ├── 单例模式.cpp │ └── 快速排序(单排,双排).cpp ├── README.md ├── 海量数据处理 │ └── README.md ├── 数据结构 │ └── README.md ├── 计算机网络 │ └── README.md └── 操作系统 │ └── README.md ├── 项目基础知识 ├── README.md ├── Redis │ └── README.md ├── Socket编程 │ └── README.md ├── MySQL │ └── README.md └── Linux系统编程及基本命令 │ └── ReadMe.md └── README.md /基础语言/utils/mtu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/mtu.png -------------------------------------------------------------------------------- /基础语言/utils/语言.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/语言.png -------------------------------------------------------------------------------- /基础语言/utils/多虚继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/多虚继承.png -------------------------------------------------------------------------------- /基础语言/utils/菱形继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/菱形继承.png -------------------------------------------------------------------------------- /基础语言/utils/计算机基础知识.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/计算机基础知识.png -------------------------------------------------------------------------------- /基础语言/utils/项目基础知识.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/基础语言/utils/项目基础知识.png -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/堆排序.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/计算机基础知识/基本手写代码/堆排序.cpp -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/归并排序.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/计算机基础知识/基本手写代码/归并排序.cpp -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/插入排序.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/计算机基础知识/基本手写代码/插入排序.cpp -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/二分查找法.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twomonkeyclub/BackEnd/HEAD/计算机基础知识/基本手写代码/二分查找法.cpp -------------------------------------------------------------------------------- /基础语言/README.md: -------------------------------------------------------------------------------- 1 | 语言 2 | =============== 3 | 4 |
-------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/README.md: -------------------------------------------------------------------------------- 1 | # [基本手写代码](https://blog.csdn.net/qq_41572503/article/details/88542859) 2 | 3 | > * 二分查找 4 | > * 插入排序 5 | > * 堆排序 6 | > * 归并排序 7 | > * 快速排序(随机单排,双排) 8 | > * 单例模式 9 | > * 非递归二叉树遍历 10 | -------------------------------------------------------------------------------- /项目基础知识/README.md: -------------------------------------------------------------------------------- 1 | 项目基础知识 2 | =============== 3 | 4 |
-------------------------------------------------------------------------------- /计算机基础知识/README.md: -------------------------------------------------------------------------------- 1 | 计算机基础知识 2 | =============== 3 | 4 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 知识总结 2 | =============== 3 | 本总结主要针对C++后台开发相关技术栈,配套[Linux下C++轻量级Web服务器开发](https://github.com/qinguoyi/TinyWebServer)项目更佳. 4 | > * [基础语言](https://github.com/twomonkeyclub/BackEnd/tree/master/%E5%9F%BA%E7%A1%80%E8%AF%AD%E8%A8%80) 5 | > * [计算机基础知识](https://github.com/twomonkeyclub/BackEnd/tree/master/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86) 6 | > * [项目基础知识](https://github.com/twomonkeyclub/BackEnd/tree/master/%E9%A1%B9%E7%9B%AE%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86) 7 | 8 | 9 | 更多资料 10 | ------------ 11 | 请关注公众号 **“两猿社”**. 12 | > * **带你丰富互联网相关项目经验,轻松应对校招!!!** 13 | > * **项目模块详细讲解,在公众号内持续更新!!!** 14 | 15 |
16 | -------------------------------------------------------------------------------- /计算机基础知识/海量数据处理/README.md: -------------------------------------------------------------------------------- 1 | # 海量数据处理 2 | 3 | 4 | 海量数据,不能一次加载到内存中 5 | 6 | > * 海量数据topK(最大和最小k个数),第k大,第k小的数 7 | > * 海量数据判断一个整数是否存在其中 8 | > * 海量数据找出不重复的数字 9 | > * 找出A,B两个海量url文件中共同的url 10 | 11 | ## 海量数据topK 12 | 最大K使用最小堆,最小K使用最大堆,这里以最大K为例 13 | > * 海量数据hash分块 14 | > * 维护最小堆的K个数据的数据容器 15 | > * 堆中数据是topK大的数据,堆顶的数据是第K大数据 16 | 17 | 1. 先将海量数据hash再取模m,分成m个小文件,hash(num)%m,也可以直接取模 18 | 2. 在每个小文件中维护K个数据的最小堆,堆顶是当前堆中的最小值 19 | 3. 遍历每个小文件中剩余的数据,与堆顶的数据进行比较,更新最小堆中的数据 20 | 4. 生成m * K个数据,然后对这些数据再进行排序,或者再次通过维护最小堆 21 | 22 | **变形** 23 | 1. 第K大不只是topK,此时堆顶数据即是 24 | 2. 只求最大或最小 25 | 3. 海量数据不仅仅是整数,也可以是字符串 26 | 4. 海量数据按照出现的次数或者频率排序,topK 27 | 28 | > * 海量数据按照出现的次数或者频率排序,topK 29 | 1. 先将海量数据hash再取模m,分成m个小文件,hash(num)%m 30 | 2. 扫描每个小文件的数据,通过hash_map建立值和频率的键值对 31 | 3. 以出现的频率维护最小堆的K个数据的数据容器 32 | 4. 遍历每个小文件中剩余的数据,与堆顶的数据进行比较,更新最小堆中的数据 33 | 5. 生成m * K个数据,然后对这些数据再进行排序,或者再次通过维护最小堆 34 | 35 | ## 找出A,B两个海量url文件中共同的url 36 | 题目:两个文件各存50亿个url,每个url64个字节,内存限制4G,找出A,B共同的url 37 | > * 单个文件读取肯定超出内存大小,所以还是采取之前的分治思想,大化小,对A/B分别取模分成1000个文件存储,这样两个文件中相同的url都被分到相同的小文件中,若有一方的小文件还是太大,则可以扩大分块或者通过不同hash函数继续hash(若继续,两方应该一起),50亿url算下来每个文件300M。 38 | > * 对小文件求公共url的时候可以使用hash_set去重。A文件Set建立后另外一个文件的内容遍历跟Set中内容比对,如果相等则记录 39 | 40 | ## [bitmap](https://blog.csdn.net/qq_22080999/article/details/81975889) 41 | bitmap一般是total/32 + 1个数组,从a[0]开始,每组是32bit表示,对应位的0或1表示十进制的0-31是否存在,可以用于快速排序,快速去重,快速查询 42 | 43 | ## 海量数据判断一个整数是否存在其中 44 | 45 | > * 分治思想,首先分成小文件,然后建立HashTable进行统计 46 | > * 可以使用BitMap,每个数分配1Bit,0不存在,1存在建立完毕扫描数据把对应位置的比特位描成0/1,最后查找整数的位置是否为1(通过商判断在哪个数组中,余数判断哪一位) 47 | 48 | 49 | ## 海量数据找出不重复的数字/仅出现一次的数据 50 | > * 可以使用BitMap,每个数分配两Bit,00不存在,01出现一次,10出现多次,11没意义。需要内存2^32 * 8 * 2bit,建立完毕扫描数据把对应位置的比特位描成00/01/10/11,最后查找01 51 | -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/非递归二叉树遍历.cpp: -------------------------------------------------------------------------------- 1 | //二叉树的前序,中序和后序遍历是根据根结点的位置来判断 2 | //前序:根左右 3 | //中序:左根右 4 | //后序:左右根 5 | //叶子结点也需要判断左右节点,只不过左右节点都是空 6 | 7 | 8 | //非递归前序遍历,根左右 9 | //(1) 从根结点开始,向左遍历压栈并输出 10 | //(2) 一直找到二叉树最左边的结点,将最左侧的叶子结点压入栈 11 | //(3) 出栈,指向该结点的右孩子 12 | //(4) 将右孩子作为根节点重复(1)(2)(3) 13 | void Pretravel(BiNode* root) 14 | { 15 | if (!root) 16 | { 17 | return; 18 | } 19 | 20 | stack st; 21 | BiNode* p = root; 22 | 23 | while (p || !st.empty()) 24 | { 25 | while (p){ 26 | cout << p->data; 27 | st.push(p); 28 | p = p->lchild; 29 | } 30 | 31 | 32 | if (!st.empty()) 33 | { 34 | p = st.top(); 35 | st.pop(); 36 | p = p->rchild; 37 | } 38 | } 39 | } 40 | 41 | //非递归中序遍历,左根右 42 | //先输出最左侧叶子结点 43 | //(1) 从根结点开始,往左遍历压栈 44 | //(2) 找到最左侧的叶子结点,也将其压栈 45 | //(3) 出栈,输出结点值,并指向该结点的右孩子 46 | //(4) 将右孩子作为根结点继续(1)(2)(3) 47 | void Intravel(BiNode* root){ 48 | if (root == NULL) 49 | return; 50 | stackst; 51 | BiNode *p = root; 52 | 53 | while (!st.empty() || p){ 54 | while (p){ 55 | st.push(p); 56 | p = p->lchild; 57 | } 58 | 59 | if (!st.empty()){ 60 | p = st.top(); 61 | st.pop(); 62 | cout << p->data; 63 | p = p->rchild; 64 | } 65 | } 66 | } 67 | 68 | //非递归后序遍历,左右根 69 | // 维护一个pre结点 70 | //(1) 从根结点开始,往左遍历压栈 71 | //(2) 找到最左侧的叶子结点,也将其压栈 72 | //(3) 出栈,判断当前的结点是不是叶子结点或是不是根结点(上一次访问的是右孩子) 73 | //(4) 若是,输出结点值,更新pre指针 74 | //(5) 若不是,指向右孩子,重复(1)(2)(3)(4) 75 | void behtravel(BiNode* root){ 76 | if (NULL == root) 77 | return; 78 | stack st; 79 | BiNode * p = root; 80 | BiNode * pre = NULL; 81 | while (!st.empty || p){ 82 | while (p){ 83 | st.push(p); 84 | p = p->lchild; 85 | } 86 | if (!st.empty()){ 87 | p = st.top(); 88 | st.pop(); 89 | 90 | //右孩子为空(左叶子结点和右叶子结点) 或 刚刚访问的是该结点的右孩子(根结点) 91 | if (!p->rchild || pre == p->rchild){ 92 | cout << p->data; 93 | pre = p; 94 | } 95 | //右孩子不为空,则将刚刚出栈的结点重新压入,指向结点的右孩子 96 | else{ 97 | st.push(p); 98 | p = p->rchild; 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/智能指针的设计与实现.cpp: -------------------------------------------------------------------------------- 1 | //智能指针的设计与实现 2 | //智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。 3 | //每次创建类的新对象时,初始化指针并将引用计数置为1; 4 | //当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数; 5 | //对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数; 6 | //调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。 7 | //所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。 8 | 9 | #include 10 | #include 11 | 12 | template 13 | class SmartPointer { 14 | private: 15 | T* _ptr; 16 | //在赋值时,需要修改赋值后指针的引用计数 17 | size_t* _count; 18 | public: 19 | //初始化 20 | SmartPointer(T* ptr = NULL) : 21 | _ptr(ptr) { 22 | if (_ptr) { 23 | _count = new size_t(1); 24 | } 25 | else { 26 | _count = new size_t(0); 27 | } 28 | } 29 | 30 | //拷贝构造 31 | SmartPointer(const SmartPointer& ptr) { 32 | if (this != &ptr) { 33 | this->_ptr = ptr._ptr; 34 | this->_count = ptr._count; 35 | (*this->_count)++; 36 | } 37 | } 38 | 39 | //重载=运算符 40 | SmartPointer& operator=(const SmartPointer& ptr) { 41 | if (this->_ptr == ptr._ptr) { 42 | return *this; 43 | } 44 | 45 | if (this->_ptr) { 46 | (*this->_count)--; 47 | if (this->_count == 0) { 48 | delete this->_ptr; 49 | delete this->_count; 50 | } 51 | } 52 | 53 | this->_ptr = ptr._ptr; 54 | this->_count = ptr._count; 55 | (*this->_count)++; 56 | return *this; 57 | } 58 | 59 | //重载* 60 | T& operator*() { 61 | assert(this->_ptr == nullptr); 62 | return *(this->_ptr); 63 | 64 | } 65 | 66 | //重载-> 67 | T* operator->() { 68 | assert(this->_ptr == nullptr); 69 | return this->_ptr; 70 | } 71 | 72 | ~SmartPointer() { 73 | //因为引用计数是指针,当智能指针声明为空时,仍需释放 74 | if (*this->_count == 0) { 75 | delete this->_ptr; 76 | delete this->_count; 77 | std::cout << "释放" << std::endl; 78 | } 79 | else 80 | (*this->_count)--; 81 | if (*this->_count == 0) { 82 | delete this->_ptr; 83 | delete this->_count; 84 | std::cout << "释放" << std::endl; 85 | } 86 | } 87 | 88 | size_t use_count(){ 89 | return *this->_count; 90 | } 91 | }; 92 | 93 | int main() { 94 | { 95 | //只初始化了两次 96 | SmartPointer sp(new int(10)); 97 | SmartPointer sp2(sp); 98 | SmartPointer sp3(new int(20)); 99 | sp2 = sp3; 100 | std::cout << sp.use_count() << std::endl; 101 | std::cout << sp3.use_count() << std::endl; 102 | 103 | //SmartPointer sp(NULL); 104 | //std::cout << sp.use_count() << std::endl; 105 | } 106 | 107 | system("pause"); 108 | return 0; 109 | } -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/单例模式.cpp: -------------------------------------------------------------------------------- 1 | //单例思路:静态成员建议在类外进行初始化,但在类内也可以初始化,只是通过类名访问静态成员的属性时,访问不到 2 | //构造函数声明为private或protect防止被外部函数实例化 3 | //内部保存一个private static的类指针保存唯一的实例 4 | //实例的动作由一个public的类方法代劳 5 | 6 | //懒汉模式:在getinstance中实例化 7 | //饿汉模式:在单例类定义时实例化 8 | 9 | //********************** 10 | //懒汉模式最初实现 11 | //********************** 12 | //#include 13 | // 14 | //using namespace std; 15 | // 16 | //class single{ 17 | //private: 18 | // static single *p; 19 | // single(){} 20 | // ~single(){} 21 | // 22 | //public: 23 | // static single* getinstance(); 24 | //}; 25 | // 26 | //single* single::p = NULL; 27 | //single* single::getinstance(){ 28 | // if (NULL == p) 29 | // p = new single; 30 | // 31 | // return p; 32 | //} 33 | 34 | 35 | //********************** 36 | //懒汉模式线程安全经典实现 37 | //********************** 38 | //#include 39 | //#include 40 | //#include 41 | // 42 | //using namespace std; 43 | // 44 | // 45 | //class single{ 46 | //private: 47 | // static single *p; 48 | // static pthread_mutex_t lock; 49 | // single(){ 50 | // pthread_mutex_init(&lock, NULL); 51 | // } 52 | // ~single(){} 53 | // 54 | //public: 55 | // static single* getinstance(); 56 | // 57 | //}; 58 | //pthread_mutex_t single::lock; 59 | //single* single::p = NULL; 60 | //single* single::getinstance(){ 61 | // if (NULL == p){ 62 | // pthread_mutex_lock(&lock); 63 | // if (NULL == p) 64 | // p = new single; 65 | // } 66 | // pthread_mutex_unlock(&lock); 67 | // return p; 68 | //} 69 | 70 | //************************************************** 71 | //懒汉模式线程安全内部静态变量实现 72 | //将经典实现中的私有唯一实例删掉 73 | //改为在instance函数里定义一个静态的实例 74 | //也可以保证拥有唯一实例,在返回时只需要返回其指针就可以 75 | //************************************************** 76 | //#include 77 | //#include 78 | //#include 79 | // 80 | //using namespace std; 81 | // 82 | // 83 | //class single{ 84 | //private: 85 | // static pthread_mutex_t lock; 86 | // single(){ 87 | // pthread_mutex_init(&lock, NULL); 88 | // } 89 | // ~single(){} 90 | // 91 | //public: 92 | // static single* getinstance(); 93 | // 94 | //}; 95 | //pthread_mutex_t single::lock; 96 | //single* single::getinstance(){ 97 | // pthread_mutex_lock(&lock); 98 | // static single obj; 99 | // pthread_mutex_unlock(&lock); 100 | // return &obj; 101 | //} 102 | 103 | //************************************************************ 104 | //饿汉模式,在定义单例类最初就实例化,此后返回的就一个,感觉相当于全局变量 105 | //在饿汉模式下,在单例类定义的时候就已经定义了一个对象,对类进行了初始化。 106 | //后面不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。 107 | //所以是线程安全的,不需要在成员函数getinstance中加锁。 108 | //************************************************************ 109 | #include 110 | 111 | using namespace std; 112 | 113 | class single{ 114 | private: 115 | static single* p; 116 | single(){} 117 | ~single(){} 118 | 119 | public: 120 | static single* getinstance(); 121 | 122 | }; 123 | single* single::p = new single(); 124 | single* single::getinstance(){ 125 | return p; 126 | } 127 | 128 | int main(){ 129 | 130 | single *p1 = single::getinstance(); 131 | single *p2 = single::getinstance(); 132 | 133 | if (p1 == p2) 134 | cout << "same" << endl; 135 | 136 | system("pause"); 137 | return 0; 138 | } 139 | -------------------------------------------------------------------------------- /计算机基础知识/基本手写代码/快速排序(单排,双排).cpp: -------------------------------------------------------------------------------- 1 | //快速排序思想: 2 | //选出一个哨兵,通过一趟排序将待排序列分割成两部分 3 | //其中一部分小于哨兵值,另外一部分大于哨兵值 4 | //然后再对两部分分别进行上述操作,直到排序完成 5 | 6 | 7 | //解释时间复杂度为nlogn 8 | //分治算法,每次选出哨兵,将待排序列分成两部分,一直分下去,直到只有一个元素,平均分开,相当于n个结点的二叉树,共有logn层,递归深度为logn 9 | //每一层都需要对当前分组的元素排序,问题规模大概为n,第一层遍历n个元素,第二层遍历n-1个...直到最后一层为一个 10 | //nlogn 11 | 12 | //解释空间复杂度O(logn)~O(n) 13 | //主要是递归造成的栈空间的使用 14 | //最好情况递归树深度为logn,最坏情况为n 15 | 16 | //快排的最差情况什么时候发生? 17 | //1. 已排序 18 | //2. 数值全部相等(1的特殊情况) 19 | //在上面的情况下选择的标定元素一直为第一个,则时间复杂度变为O(n^2) 20 | 21 | //快速排序,平均情况O(nlogn),最好O(nlogn),最坏O(n^2)(选择标定元素有关),空间复杂度O(logn)~O(n),不稳定 22 | 23 | /************************************************************************** 24 | ** 要取得[a,b)的随机整数,使用(rand() % (b-a))+ a; 25 | ** 要取得[a,b]的随机整数,使用(rand() % (b-a+1))+ a; 26 | ** 要取得(a,b]的随机整数,使用(rand() % (b-a))+ a + 1; 27 | ** 通用公式:a + rand() % n;其中的a是起始值,n是整数的范围。 28 | ** 要取得a到b之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。 29 | ** 要取得0~1之间的浮点数,可以使用rand() / double(RAND_MAX)。 30 | *************************************************************************/ 31 | 32 | 33 | #include 34 | using namespace std; 35 | 36 | //双路快排,尽量写这种 37 | //begin,begin+1...i...j...end 38 | int partition2(int *data, int start, int end){ 39 | //产生start和end之间的随机数 40 | int index = (rand() % (end - start + 1)) + start; 41 | 42 | //将选中的数字交换到start位置 43 | swap(data[index], data[start]); 44 | 45 | int pivot = data[start]; 46 | 47 | //选择的pivot为start位置 48 | //data[start+1, i) <= pivot i-1 为小于v的最后一个元素,i为当前左边访问的元素 49 | //data(j, end] >= pivot j+1 为大于v的第一个元素,j为当前右边访问的元素 50 | int i = start + 1, j = end; 51 | while (true){ 52 | while (i <= end && data[i] <= pivot) 53 | i++; 54 | while (j >= start + 1 && data[j] >= pivot) 55 | j--; 56 | if (i > j) 57 | break; 58 | swap(data[i], data[j]); 59 | 60 | //下面这两行,swap交换之后,双方需要移动,否则会增加一次无用的比较 61 | i++; 62 | j--; 63 | } 64 | //最后j停止在<= v的最后一个位置, i停止在>=pivot的第一个位置,pivot与j进行交换 65 | swap(data[start], data[j]); 66 | return j; 67 | } 68 | 69 | void swapOffer(int &a, int &b){ 70 | int temp = a; 71 | a = b; 72 | b = temp; 73 | } 74 | 75 | //单路随机快排 76 | //begin,begin+1...small,small+1...end 77 | int partition(int *data, int start, int end){ 78 | //产生start和end之间的随机数 79 | int index = (rand() % (end - start + 1)) + start; 80 | 81 | //将选中的数字交换到start位置 82 | swap(data[index], data[start]); 83 | 84 | //选择的pivot为start位置 85 | int pivot = data[start]; 86 | 87 | //我们要达到这样的效果 88 | //data[start+1, small] < v small为小于pivot的最后一个元素 89 | //data[small+1, i - 1] >= v small+1为大于等于pivot的第一个元素 90 | 91 | //注意small的取值,small是为了标识小于 92 | int small = start; 93 | for (int i = start + 1; i <= end; ++i){ 94 | if (data[i] < pivot){ 95 | //若当前的元素小于pivot,需要将该元素放到data[start+1, small]中紧挨着small位置 96 | //将small+1和i进行交换,并将small的长度加长 97 | swap(data[small + 1], data[i]); 98 | ++small; 99 | } 100 | } 101 | //最后将start放在应该的位置,即small和small+1之间,因为左侧全是小于pivot的,因此将small和pivot交换 102 | swap(data[small], data[start]); 103 | return small; 104 | } 105 | 106 | void quickSortOffer(int *data,int start, int end){ 107 | if (start == end) 108 | return; 109 | if (start < end){ 110 | int index = partition2(data, start, end); 111 | quickSortOffer(data, start, index - 1); 112 | quickSortOffer(data, index + 1, end); 113 | } 114 | } 115 | 116 | int main(){ 117 | int arr[8] = { 2, 1, 3, 78, 78,53, 13, 20 }; 118 | quickSortOffer(arr, 0, 7); //0表示从数组0位置开始,到4位置排序 119 | int i; 120 | for (i = 0; i<8; ++i){ 121 | printf("%d ", arr[i]); 122 | } 123 | system("pause"); 124 | return 0; 125 | } 126 | -------------------------------------------------------------------------------- /基础语言/STL/README.md: -------------------------------------------------------------------------------- 1 | # STL 2 | > * [vector如何扩展内存和释放内存](#vector如何扩展内存和释放内存) 3 | > * [STL中各种容器对比](#各种容器对比) 4 | > * [STL中的swap函数](#STL中的swap函数) 5 | > * [STL中哈希表扩容](#STL中的哈希表扩容) 6 | > * [STL迭代器失效的情况和原因](#STL迭代器失效的情况和原因) 7 | > * [vector删除元素后如何避免当前迭代器会失效](#vector删除元素后如何避免当前迭代器会失效) 8 | > * [vector的iterator和const_iterator和const iterator](#vector的iterator和const_iterator和constiterator) 9 | 10 | ## [vector如何扩展内存和释放内存](https://www.cnblogs.com/biyeymyhjob/archive/2012/09/12/2674004.html) 11 | > * 内存增长 12 | > * [1.5还是2倍扩容](https://blog.csdn.net/dengheCSDN/article/details/78985684) 13 | > * 内存释放 14 | 15 | ## 各种容器对比 16 | 17 | |容器 |底层数据结构|时间复杂度 |有无序 |可不可重复 | 其他| 18 | | :------: | :------: | :------: |:------: |:------: |:------: | 19 | |array |数组 | 随机读改 O(1)| 无序 |可重复 |支持快速随机访问 | 20 | |vector |数组 | 随机读改、尾部插入、尾部删除 O(1)、头部插入、头部删除 O(n) |无序 |可重复 |支持快速随机访问 | 21 | |list |双向链表 |插入、删除 O(1)、随机读改 O(n) |无序 |可重复 |支持快速增删 | 22 | |deque |双端队列 |头尾插入、头尾删除 O(1) |无序 |可重复 |一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 | 23 | |stack |deque / list |顶部插入、顶部删除 O(1) |无序 |可重复 |deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 | 24 | |queue |deque / list |尾部插入、头部删除 O(1) |无序 |可重复 |deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 | 25 | |priority_queue |vector + max - heap |插入、删除 O(log2n) |有序 |可重复 |vector容器 + heap处理规则 | 26 | |set |红黑树 |插入、删除、查找 O(log2n) |有序 |不可重复 | 27 | |multiset |红黑树 |插入、删除、查找 O(log2n) |有序 |可重复 | | 28 | |map |红黑树 |插入、删除、查找 O(log2n) |有序 |不可重复| | 29 | |multimap |红黑树 |插入、删除、查找 O(log2n) |有序 |可重复 | | 30 | 31 | ## [STL中的swap函数](https://blog.csdn.net/ryfdizuo/article/details/6435847) 32 | * 除了数组,其他容器在交换后本质上是将内存地址进行了交换,而元素本身在内存中的位置是没有变化 33 | * swap在交换的时候并不是完全将2个容器的元素互换,而是交换了2个容器内的内存地址。 34 | 35 | ## STL中的哈希表扩容 36 | * 这里需要知道STL中的swap底层,其实扩容也是vector扩容 37 | * 创建一个新桶,该桶是原来桶两倍大最接近的质数(判断n是不是质数的方法:用n除2到sqrt(n)范围内的数) ; 38 | * 将原来桶里的数通过指针的转换,插入到新桶中(注意STL这里做的很精细,没有直接将数据从旧桶遍历拷贝数据插入到新桶,而是通过指针转换两个桶的地址) 39 | * 通过swap函数将新桶和旧桶交换,销毁新桶 40 | 41 | ## vector删除元素后如何避免当前迭代器会失效 42 | 删除时,将当前的迭代器存起来 43 | 44 | ## STL迭代器失效的情况和原因 45 | 迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。 46 | 47 | * 数组型数据结构 48 | * 该数据结构的元素是分配在连续的内存中 49 | * insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。 50 | * 解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 `iter = cont.erase(iter);` 51 | 52 | ```C++ 53 | //不要直接在循环条件中写++iter 54 | for (iter = cont.begin(); iter != cont.end();) 55 | { 56 | (*it)->doSomething(); 57 | if (shouldDelete(*iter)) 58 | iter = cont.erase(iter); //erase删除元素,返回下一个迭代器 59 | else 60 | ++iter; 61 | } 62 | ``` 63 | 64 | * 链表型数据结构 65 | * 对于list型的数据结构,使用了不连续分配的内存 66 | * 插入不会使得任何迭代器失效 67 | * 删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器. 68 | * 解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++). 69 | 70 | * 树形数据结构 71 | * 使用红黑树来存储数据 72 | * 插入不会使得任何迭代器失效 73 | * 删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器. 74 | * **erase迭代器只是被删元素的迭代器失效,但是返回值为void**,所以要采用erase(iter++)的方式删除迭代器。 75 | 76 | 77 | **注意** :经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite 78 | 79 | ## vector的iterator和const_iterator和const iterator 80 | 81 | * 三种的区别 82 | * iterator,可遍历,可改变所指元素 83 | * const_iterator,可遍历,不可改变所指元素 84 | * const iterator,不可遍历,可改变所指元素 85 | * const_iterator转iterator,iterator不能转const_iterator 86 | * const_iterator 主要是 **在容器被定义成常量、或者非常量容器但不想改变元素值的情况** 下使用的,而且容器被定义成常量之后,它返回的迭代器只能是const_iterator 87 | * 有些容器成员函数只接受iterator作为参数,而不是const_iterator。那么,如果你只有一const_iterator,而你要在它所指向的容器位置上插入新元素呢? 88 | * const_iterator转iterator 89 | * 强制转换的函数会报错,只能通过 `advance(a, distance(a, b));` 其中,distance用于取得两个迭代器之间的距离,advance用于将一个迭代器移动指定的距离 90 | * 如果a是iterator,b是const_iterator,distance会报错,可以显式的指明distance调用的模板参数类型,从而避免编译器自己得出它们的类型 91 | 92 | ```C++ 93 | typedef deque IntDeque; 94 | typedef IntDeque::iterator iter; 95 | typedef IntDeque::const_iterator ConstIter; 96 | IntDeque d; 97 | ConstIter ci; 98 | Iter i(d.begin()); 99 | advance(i,distance(i,ci)); 100 | ``` -------------------------------------------------------------------------------- /基础语言/C++编译底层/README.md: -------------------------------------------------------------------------------- 1 | # C/C++编译底层 2 | 3 | > * [C++内存管理](#C++内存管理) 4 | > * [LINUX进程区分段及存储数据](#LINUX进程区分段及存储数据) 5 | > * [GCC编译流程](#GCC编译流程) 6 | > * [动态库静态库区别及LINUX加载库](#动态库静态库区别及GCC加载库) 7 | > * [extern C的结果和CPP编译的区别](#extern-C的结果和CPP编译的区别) 8 | > * [重载的底层原理](#重载的底层原理) 9 | > * [编译性语言和解释性语言的本质区别和优缺点](#编译性语言和解释性语言的本质区别和优缺点) 10 | 11 | 12 | 13 | ## C++内存管理 14 | > * **栈** 存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长 15 | > * **堆** malloc/free开辟内存的空间,从低地址向高地址增长 16 | > * **自由存储区** new/delete开辟内存空间 17 | > * **数据区** 18 | 数据区包含全局/静态存储区和常量存储区,存储已初始化的全局变量和静态变量、未初始化的全局变量和静态变量及字符串常量 19 | > * **代码区** 存储程序的机器代码和程序指令 20 | 21 | ## LINUX进程区分段及存储数据 22 | Linux的每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。 23 | 逻辑地址分段如下,自下而上: 24 | > * 代码段。分为只读存储区和代码区,存放字符串是常量和程序机器代码和指令 25 | > * 数据段。存储已初始化的全局变量和静态变量。 26 | > * bss段。存储未初始化的全局变量和静态变量,及初始化为0的全局变量和静态变量 27 | > * 堆。 当进程未调用malloc时是没有堆段的,malloc/free开辟的内存空间,向上生长 28 | > * 映射区。存储动态链接库以及调用mmap函数进行的文件映射 29 | > * 栈。存储函数的返回地址、参数、局部变量、返回值,向下生长。 30 | 31 | ## GCC编译流程 32 | * 预处理阶段:hello.c -- "gcc -E预处理,头文件展开,宏替换" --> hello.i 33 | * 编译阶段:hello.i -- "gcc -s生成汇编文件" --> hello.s 34 | * 汇编阶段:hello.s -- "gcc -c生成二进制文件" --> hello.o 35 | * 链接阶段:hello.o -- "调用ld进行链接" --> a.out 36 | 37 | ## 动态库静态库区别及GCC加载库 38 | 静态库 39 | > * 编译时期链接 40 | > * 浪费空间和资源,如果多个程序链接了同一个库,则每一个生成的可执行文件就都会有一个库的副本,必然会浪费系统空间。 41 | > * 若静态库需修改,需重新编译所有链接该库的程序 42 | 43 | 动态库 44 | > * 运行时链接 45 | > * 运行时被链接,故程序的运行速度稍慢 46 | > * 动态库是在程序运行时被链接的,所以磁盘上只须保留一份副本,因此节约了磁盘空间。如果发现了bug或要升级也很简单,只要用新的库把原来的替换掉即可 47 | 48 | GCC编译加载静态库 49 | 50 | * 将所有的.c文件编译成.o目标文件 51 | * `gcc -c add.c` 生成add.o 52 | * `gcc -c max.c` 生成max.o 53 | * 对生成的.o目标文件打包生成静态库 54 | * `ar crv libfoo.a add.o max.o //libfoo.a是库的名字` 55 | * ar:做库的命令 56 | * c:创建库 57 | * r:将方法添加到库里 58 | * v:显示过程,可以不要 59 | 60 | * 使用静态库 61 | * `gcc -o main main.c -static -L. -lfoo //这里写的foo是去掉前后缀后库的名字` 62 | * -L:指定路径 .代表当前路径 63 | * -l:指定库名 64 | 65 | GCC编译加载动态库 66 | 67 | * 对生成的.o文件处理生成共享库,共享库的名字为libfoo.so 68 | * `gcc -shared -fPIC -o libfoo.so add.o max.o` 69 | * -shared 表示输出结果是共享库类型的 70 | * -fPIC 表示使用地址无关代码(Position Independent Code)技术来生产输出文件 71 | 72 | * 库的使用 73 | * `cp libfoo.so /usr/lib //将库拷贝到系统库路径下`(不推荐) 74 | * export更改LD_LIBRARY_PATH当前终端的环境变量 75 | * 修改/etc/ld.so.conf文件,加入库文件所在目录的路径,然后 76 | 运行ldconfig 目录名字,该命令会重建/etc/ld.so.cache文件即可 77 | 78 | * 上面三种选一个即可`gcc -o main main.c -lfoo` 79 | 80 | ## [extern-C的结果和CPP编译的区别](https://www.cnblogs.com/qingergege/p/7478864.html) 81 | * 一个C语言文件p.c 82 | ```C 83 | #include 84 | void print(int a,int b) 85 | { 86 | printf("这里调用的是C语言的函数:%d,%d\n",a,b); 87 | } 88 | ``` 89 | * 一个头文件p.h 90 | ```C++ 91 | #ifndef _P_H 92 | #define _P_H 93 | 94 | void print(int a,int b); 95 | 96 | #endif 97 | ``` 98 | * C++文件调用C函数 99 | ```C++ 100 | #include 101 | using namespace std; 102 | #include "p.h" 103 | int main() 104 | { 105 | cout<<"现在调用C语言函数\n"; 106 | print(3,4); 107 | return 0; 108 | } 109 | ``` 110 | * 编译后链接出错:main.cpp对print(int, int)未定义的引用。 111 | * 原因分析 112 | * p.c我们使用的是C语言的编译器gcc进行编译的,其中的函数print编译之后,在符号表中的名字为 _print 113 | * 我们链接的时候采用的是g++进行链接,也就是C++链接方式,程序在运行到调用print函数的代码时,会在符号表中寻找_print_int_int(是按照C++的链接方法来寻找的,所以是找_print_int_int而不是找_print)的名字,发现找不到,所以会t提示“未定义的引用” 114 | * 此时如果我们在对print的声明中加入 extern “C” ,这个时候,g++编译器就会按照C语言的链接方式进行寻找,也就是在符号表中寻找_print,这个时候是可以找到的,是不会报错的。 115 | 116 | * 总结 117 | * 编译后底层解析的符号不同,C语言是_print,C++是_print_int_int 118 | 119 | ## [重载的底层原理](https://blog.csdn.net/fantian_/article/details/80719144) 120 | 根据上面的编译分析,可以知道C语言没有重载,只有C++才有函数重载,因为函数重载通过参数列表的不同来实现。 121 | 122 | * C语言没有重载 123 | ```C++ 124 | "int __cdecl Add(int,int)" (?Add@@YAHHH@Z) 125 | "double __cdecl Add(double,double)" (?Add@@YANNN@Z) 126 | "long __cdecl Add(long,long)" (?Add@@YAJJJ@Z) 127 | ``` 128 | 在C语言中被解析为_Add,三个一样,所以不能进行区分,因此C语言不支持函数重载 129 | 130 | * C++重载 131 | 底层的重命名机制将Add函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。 _Add_int_int,_Add_long_long,_Add_double_double 132 | 133 | * C++中可以通过在函数声明前加 extern "C" 将一个函数按照 C 语言的风格来进行编译。 134 | 135 | ## [编译性语言和解释性语言的本质区别和优缺点](https://blog.csdn.net/u014647208/article/details/78329187) 136 | 137 | * 根本区别 138 | * 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。**两种方式只是翻译的时间不同**。 139 | * 解释性语言不用编译,在运行时翻译 140 | * 编译性语言是编译的时候直接编译成机器可以执行的语言,编译和运行是分开的,但是不能跨平台。比如exe文件,以后要运行的话就不用重新编译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行的时不要翻译,所以编译型语言的程序执行效率高 141 | 142 | * 编译性语言的优缺点 143 | * 优点 144 | * 运行速度快,代码效率高,编译后程序不可以修改,保密性好 145 | * 缺点 146 | * 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。 147 | * 解释性语言的优缺点 148 | * 优点 149 | * 可移植性好,只要有解释环境,可以在不同的操作系统上运行。 150 | * 缺点 151 | * 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程 152 | 153 | -------------------------------------------------------------------------------- /基础语言/C++智能指针_异常机制_强制转换_RTTI/README.md: -------------------------------------------------------------------------------- 1 | # C++补充 2 | 3 | > * [构造函数可以抛出异常吗,有什么问题?](#构造函数可以抛出异常吗有什么问题) 4 | > * [初始化列表的异常怎么捕获?](#初始化列表的异常怎么捕获) 5 | > * [析构函数可以抛出异常吗,有什么问题?](#析构函数可以抛出异常吗有什么问题) 6 | > * [析构函数如何处理异常](#析构函数如何处理异常) 7 | > * [智能指针](#智能指针) 8 | > * [内存泄漏](#内存泄漏) 9 | > * [野指针](#野指针) 10 | > * [强制转换](#强制转换) 11 | > * [RTTI](#RTTI) 12 | > * [RAII](#RAII) 13 | > * [CPP11新特性](#CPP11新特性) 14 | > * [仿函数](#仿函数) 15 | 16 | 17 | ## [构造函数可以抛出异常吗,有什么问题?](https://www.cnblogs.com/qinguoyi/p/10304882.html) 18 | 构造函数中应该避免抛出异常。 19 | > * 构造函数中抛出异常后,对象的析构函数将不会被执行 20 | > * 构造函数抛出异常时,本应该在析构函数中被delete的对象没有被delete,会导致内存泄露 21 | > * 当对象发生部分构造时,已经构造完毕的子对象(非动态分配)将会逆序地被析构。 22 | 23 | ## 初始化列表的异常怎么捕获? 24 | > * 初始化列表构造,当初始化列表出现异常时,程序还未进入函数体,因此函数体中的try-catch不能执行,catch也无法处理异常。可以通过函数try块解决该问题。 25 | > * 函数try块中的try出现在表示构造函数初始值列表的冒号以及表示构造函数体的花括号之前,与这个try关联的catch既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常。 26 | 27 | 28 | ## 析构函数可以抛出异常吗,有什么问题? 29 | 析构函数不应该抛出异常 30 | > * **其他正常,仅析构函数异常**。 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 31 | > * **其他异常,且析构函数异常**。 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。 32 | 33 | ## 析构函数如何处理异常? 34 | > * 若析构函数抛出异常,调用std::abort()来终止程序 35 | > * 在析构函数中catch捕获异常并作处理,吞下异常; 36 | > * 如果客户需要对某个操作函数运次期间抛出的异常做出反应,class应该提供普通函数执行该操作,而非在析构函数中。 37 | 38 | ## [智能指针](https://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html) 39 | 智能指针有shared_ptr,weak_ptr,unique_ptr,[参考](https://www.cnblogs.com/wxquare/p/4759020.html),使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。 40 | 41 | * shared_ptr核心要理解引用计数,什么时候销毁底层指针,还有赋值,拷贝构造时候的引用计数的变化,析构的时候要判断底层指针的引用计数为0了才能真正释放底层指针的内存 42 | * 不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如`std::shared_ptr p4 = new int(1);` 43 | * 可以`std::shared_ptrp4(new int(1));` 44 | * 拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象 45 | * 赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数 46 | 47 | * shared_ptr创建后是栈上的对象,当出作用域后,每个对象会自动调用析构函数,如上所述,new int(1)会生成一个指针,此时将其传参数给shared_ptr,由shared_ptr对其进行管理,shared_ptr虽然是对象,但其有指针的特性,通过重载运算符*和->实现指针的特性来访问被管理的指针。 48 | 49 | * shared_ptr是可以共享所有权的智能指针 50 | * shared_ptr的管理机制其实并不复杂,就是对所管理的对象(这里的对象本质是被管理的指针new int(1),并不是类和对象中的对象)进行了引用计数,当新增一个shared_ptr对该对象进行管理时,就将该对象的引用计数加一;减少一个shared_ptr对该对象进行管理时,就将该对象的引用计数减一,如果该对象的引用计数为0的时候,说明没有任何指针对其管理,才调用delete释放其所占的内存。 51 | * 对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类,可以通过make_shared函数或者通过构造函数传入普通指针 52 | * 不要把一个原生指针给多个shared_ptr,不要把this指针交给智能指针管理,这样会重复释放 53 | * shared_ptr之间的资源共享是通过shared_ptr智能指针拷贝、赋值实现的,因为这样可以引起计数器的更新;而如果直接通过原生指针来初始化,就会导致m_sp和p都根本不知道对方的存在,然而却两者都管理同一块地方 54 | 55 | ```C++ 56 | int* ptr = new int; 57 | shared_ptr p1(ptr); 58 | shared_ptr p2(ptr); //这样不会导致更新,两者不知对方存在 59 | shared_ptr p3(p1);//这样才会导致计数器更新 60 | ``` 61 | 62 | * shared_ptr循环引用导致内存泄漏,引出weak_ptr 63 | * 循环引用是两个强引用(shared_ptr)互相引用,使得两者的引用计数无法为0,进而无法释放,此时将循环引用的一方变为weak_ptr即可。 64 | 65 | ```C++ 66 | template 67 | class Node 68 | { 69 | public: 70 | Node(const T& value) 71 | :_pPre(NULL) 72 | , _pNext(NULL) 73 | , _value(value) 74 | { 75 | cout << "Node()" << endl; 76 | } 77 | ~Node() 78 | { 79 | cout << "~Node()" << endl; 80 | cout << "this:" << this << endl; 81 | } 82 | 83 | shared_ptr> _pPre; 84 | shared_ptr> _pNext; 85 | T _value; 86 | }; 87 | 88 | void Funtest() 89 | { 90 | shared_ptr> sp1(new Node(1)); 91 | shared_ptr> sp2(new Node(2)); 92 | 93 | cout << "sp1.use_count:" << sp1.use_count() << endl; 94 | cout << "sp2.use_count:" << sp2.use_count() << endl; 95 | 96 | sp1->_pNext = sp2; 97 | sp2->_pPre = sp1; 98 | 99 | cout << "sp1.use_count:" << sp1.use_count() << endl; 100 | cout << "sp2.use_count:" << sp2.use_count() << endl; 101 | } 102 | int main() 103 | { 104 | Funtest(); 105 | system("pause"); 106 | return 0; 107 | } 108 | ``` 109 | 上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。 110 | 111 | 112 | * **弱引用(weak_ptr)并不修改该对象的引用计数**,weak_ptr必须从一个share_ptr或另一个weak_ptr转换而来,这也说明,进行该对象的内存管理的是那个强引用的share_ptr,weak_ptr只是提供了对管理对象的一个访问手段这意味这弱引用它并不对对象的内存进行管理。在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存 113 | 114 | ```C++ 115 | weak_ptr> _pPre; 116 | weak_ptr> _pNext; 117 | ``` 118 | expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用指针,不能直接访问弱引用,需要将其先通过lock转换为强引用再访问 119 | 120 | * unique_ptr 121 | unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象 122 | 123 | ## 内存泄漏 124 | 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。 125 | 126 | ## 野指针 127 | 野指针指向一个已删除的对象或 申请访问受限内存区域的指针。 128 | 129 | * 原因 130 | * 指针变量未初始化 131 | * 指针释放未置空 132 | * 指针操作超出作用域。返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。 133 | 134 | ## 强制转换 135 | C++中强制转换为static_cast, dynamic_cast,const_cast, reinterpret_cast 136 | 137 | * static_cast 138 | * 完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换,不能用于普通指针的转换(void空指针除外) 139 | * dynamic_cast 140 | * 动态类型转换,用于实现RTTI。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常bad_cast 141 | * const_cast 142 | * 用于删除 const、volatile特性 143 | * reinterpret_cast 144 | * 几乎什么都可以转,不能丢掉 const、volatile特性 145 | 146 | ## RTTI 147 | 运行时类型检查,在C++层面主要体现在dynamic_cast和typeid 148 | > * dynamic_cast 149 | 动态类型转换 150 | > * typeid 151 | typeid 运算符允许在运行时确定对象的类型,获取对象的实际类型 152 | 153 | ## RAII 154 | RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”. 155 | 156 | * 在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。 157 | * RAII的核心思想是将资源或者状态与对象的生命周期绑定,通过C++的语言机制,实现资源和状态的安全管理,智能指针是RAII最好的例子 158 | 159 | ## CPP11新特性 160 | * nullptr常量 161 | * C++中NULL仅仅是`define NULL 0`的一个宏定义,因此,有时候会产生歧义 162 | * 比如f(char*)和f(int),参数传NULL的话到底该调用哪个? 163 | * 事实上,在VS下测试这样的函数重载会优先调用f(int),但是f(char *)也是正确的,因此C++引入nullptr来避免这个问题 164 | * nullptr是一个空指针,可以被转换成其他任意指针的类型 165 | * auto类型指示符 166 | * 让编译器替我们去分析表达式所属的类型,直接推导 167 | * 尤其是STL中map的迭代器这种很长的类型,适合用auto 168 | * decltype类型指示符 169 | * 从表达式的类型推断出要定义的变量的类型,跟表达式的类型也就是参数类型紧密相关 170 | * `delctype (f()) sum = x;` 并不实际调用函数f(),只是使用f()的返回值当做sum的类型 171 | * `delctype (i) sum = x;`和`delctype ((i)) sum = x;` 其中i为int类型,前面的为int类型,后面的为int&引用 172 | * 范围for语句 173 | * 多与auto配合使用 174 | 175 | ```C++ 176 | string str("somthing"); 177 | for(auto i:str) //对于str中的每个字符,i类型为char 178 | cout << c << endl; 179 | 180 | for(auto &i:str) //对于若要改变每个字符的值,需要加引用 181 | cout << c << endl; 182 | ``` 183 | 184 | * 定义双层vector 185 | * `vector>(m, vector(n, 0))` 创建m行n列的二维数组,全部初始化为0 186 | * [lambda表达式](https://blog.csdn.net/qq_43265890/article/details/83218413) 187 | * 用于实现匿名函数,匿名函数只有函数体,没有函数名 188 | 189 | ```C++ 190 | [capture list] (params list) mutable exception-> return type {function body}; //1 191 | [capture list] (params list) -> return type {function body}; //1 省略mutable,表示const不可修改 192 | [capture list] (params list) {function body}; //2 省略返回类型,按照函数体返回值决定返回类型 193 | [capture list] {function body}; //3 省略参数列表,无参函数 194 | ``` 195 | * 参数 196 | * capture list:捕获外部变量列表 197 | * params list:形参列表 198 | * mutable指示符:用来说用是否可以修改捕获的变量 199 | * exception:异常设定 200 | * return type:返回类型 201 | * function body:函数体 202 | 203 | ```C++ 204 | //示例 205 | sort(vec.begin(), vec.end(), [](int a, int b)->bool{return a < b}) 206 | ``` 207 | * 参数捕获方式 208 | * 值捕获(传参)、引用捕获(传引用)、隐式捕获(传=,函数体直接使用变量))。 209 | 210 | * 智能指针 211 | * shared_ptr 212 | * weak_ptr 213 | * unique_ptr 214 | * 右值引用 215 | * 左值引用,必须引用左值 `int a = 0; int &b = a;` 216 | * 右值引用可以引用结果 `int && i = 0` 217 | 218 | ## 仿函数 219 | * 定义 220 | * 仿函数(functor)又称之为函数对象(function object),其实就是重载了operator()操作符的struct或class,是一个能行使函数功能的类 221 | * 它使一个类的使用看上去像一个函数,这个类就有了类似函数的行为,就是一个仿函数类。 222 | -------------------------------------------------------------------------------- /基础语言/C++基础知识/README.md: -------------------------------------------------------------------------------- 1 | # C/C++基础知识 2 | 3 | > * [数组和指针的区别](#数组和指针的区别) 4 | > * [指针数组和数组指针](#指针数组和数组指针) 5 | > * [字符数组和字符串常量](#字符数组和字符串常量) 6 | > * [引用和指针的区别](#引用和指针的区别) 7 | > * [C++中class和struct的区别](#C中class和struct的区别) 8 | > * [new/delete和malloc/free区别](#new/delete和malloc/free区别) 9 | > * [new运算符的原理](#new运算符的原理) 10 | > * [malloc的内存分配机制](#malloc的内存分配机制) 11 | > * [栈和堆的区别](#栈和堆的区别) 12 | > * [面向对象和面向过程的区别](#面向对象和面向过程的区别) 13 | > * [const关键字](#const关键字) 14 | > * [static关键字](#static关键字) 15 | > * [extern关键字](#extern关键字) 16 | > * [volatile关键字](#volatile关键字) 17 | > * [explicit关键字](#explicit关键字) 18 | > * [类成员属性](#类成员属性) 19 | > * [大小端序的定义和代码判断](#大小端序的定义和代码判断) 20 | > * [代码判断32位和64位系统](#代码判断32位和64位系统) 21 | 22 | ## 数组和指针的区别 23 | > * **概念不同.** 指针相当于一个变量,它存放的是数据在内存中的地址;数组是用于储存多个相同类型数据的集合 24 | > * **赋值不同.** 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝 25 | > * **访问数据不同.** 指针是间接访问数据,获取指针,先解引用,再访问指针指向的地址中的内容;数组是直接访问 26 | > * **sizeof意义不同.** 27 | 数组所占存储空间的内存:sizeof(数组名) 28 | 数组的大小:sizeof(数组名)/sizeof(数据类型) 29 | 在32位平台下,sizeof(指针名)是4,在64位平台下,sizeof(指针名)是8 30 | > * **指针和数组名异同.** 指针和数组名都可以表示地址,但指针是变量,可以修改;数组名是常量,不可修改赋值 31 | > * **传参.** 32 | 数组传参时会退化成指针 33 | 1. 退化的意义:C语言只会以值拷贝的方式传递参数,参数传递时,如果只拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。 34 | 2. 因此,C语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。 35 | 36 | ## 指针数组和数组指针 37 | 指针数组本质是数组,数组指针本质是指针,谁优先级高,本质是谁 38 | > * 指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素。 39 | int\* arr[8]; 40 | 1. 优先级问题:[]的优先级比\*高 41 | 2. 说明arr是一个数组,而int\*是数组里面的内容 42 | 3. 这句话的意思就是:arr是一个含有8和int*的数组 43 | > * 数组指针:它实际上是一个指针,该指针指向一个数组。 44 | int (\*arr)[8]; 45 | 1. 由于[]的优先级比\*高,因此在写数组指针的时候必须将\*arr用括号括起来 46 | 2. arr先和\*结合,说明p是一个指针变量 47 | 3. 这句话的意思就是:指针arr指向一个大小为8个整型的数组。 48 | 49 | ## 字符数组和字符串常量 50 | char arr[]="hello"; //字符数组 51 | char \*arr2="hello"; //字符串常量 52 | > * char arr[]="hello",此处的赋值是将常量区的字符串"hello"拷贝到了堆栈区的数arr的空间了。数组arr是在堆栈区开辟了空间,此时是可以修改字符串的值,因为修改的是堆栈区的字符串的值。另外此时的数组名p是堆栈区中的"hello"的首地址。 53 | > * char \*arr2="hello",指针arr2是存储在堆栈区,但字符串是常量,存储在常量区,只是指针arr指向了存储在常量区的字符串首地址,此时不能改变常量区的字符串的值。 54 | 55 | const char arr[]="hello"; //这里hello本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区 56 | const char \*arr2="hello"; //字符串hello保存在常量区,const本来是修饰arr2指向的值不能通过arr2去修改,但是字符串hello在常量区,本来就不能改变,所以加不加const效果都一样 57 | 58 | 59 | ## 引用和指针的区别 60 | 61 | > * **指针有内存分配,而引用只是一个别名.** 引用声明时必须初始化,从而指向一个已经存在的对象 62 | > * **引用可以看做常量指针,指针是一个存储地址的变量.** 指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。 63 | > * **引用创建时必须初始化,且不为空,指针创建时可以为空.** 不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。 64 | > * **指针和引用的自增运算符意义不同.** 指针是对内存地址的自增,引用是对值的自增 65 | > * **sizeof的意义不同.** 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小 66 | > * **没有引用常量,有指针常量.** 没有int& const p,有int\* const p. 67 | 常量指针和常量引用是存在的,const int \*p和cosnt int &p,都表示指向的对象为常量常量引用形参的好处:(1)使用引用作为形参,不会产生新的变量,减少形参和实参传递的开销;(2)使用引用可能会导致实参随着形参的改变而改变。声明为const之后就会消除这种副作用。 68 | > * **参数传递.** 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象 69 | > * **多级指针,一级引用.** 指针可以有多级指针(\*\*p),而引用只有一级 70 | 71 | ## C++中class和struct的区别 72 | C中的strcut不能有函数,但C++中可以。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!! 73 | > * **默认的继承访问权限.** struct是public的,class是private的 74 | class B : public A就是为了指明是public继承,而不是用默认的private继承,若class B : A则是private继承 75 | > * **定义模板参数.** class这个关键字还用于定义模板参数,就像typename。但关键字struct不用于定义模板参数。 76 | 77 | ## new/delete和malloc/free区别 78 | 79 | malloc和calloc间的主要区别在于后者在返回指向内存的指针之前把它初始化为0。另一个区别是calloc的参数包括所需的元素的数量和每个元素的字节数 80 | > * **属性不同.** new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c 81 | > * **申请的内存所在位置.** new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存. 82 | > * **返回类型安全性.** new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。 83 | 而malloc内存分配成功则是返回void*类型,需要通过强制类型转换将空类型指针转换成我们需要的类型。 84 | > * **内存分配失败时的返回值.** new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。 85 | > * **是否需要指定内存大小.** 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的大小 86 | int *pi=new int;//在自由存储区中分配创建了一个整形对象,并返回一个指向该对象的地址来初始化指针pi。 87 | int *pi=new int();//对指针pi指向的地址的值进行了初始化为0 88 | int *pi=new int(1024);//初始化为1024。 89 | int *p=(int *)malloc(100);//指向整型的指针p指向一个大小为100字节的内存的地址 90 | int *p=(int *)malloc(25*sizeof(int));//指向整型的指针p指向一个25个int整型空间的地址 91 | > * **数组分配内存(与前一个特点类似).** new中分配内存对于变量和数组不同,malloc分配内存则相同 92 | int *pi=new int[]; //指针pi所指向的数组未初始化 93 | int *pi=new int[n]; //指针pi指向长度为n的数组,未初始化 94 | int *pi=new int[](); //指针pi所指向的地址初始化为0 95 | > * **是否调用构造函数/析构函数.** new会先调用operator_ new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator_ delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。 96 | > * **能否重载.** opeartor_new/operator_delete允许重载,malloc/free不允许重载 97 | > * **已分配内存的扩充.** malloc/free可以通过realloc函数扩充,new/free无法直观地处理 98 | > * **能否相互调用.** operator_new /operator _delete的实现可以基于malloc/free,而malloc的实现不可以去调用new。 99 | 100 | ## [new运算符的原理](https://blog.csdn.net/hihozoo/article/details/51441521) 101 | * 内存分配 102 | * 调用相应的 operator new(size_t) 函数,动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler()函数用于处理new失败问题。如果没有设置 new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常 103 | 104 | * 构造函数 105 | * 在分配到的动态内存块上 初始化相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。 106 | 107 | ## malloc的内存分配机制 108 | malloc内存分配机制是怎么样的,在哪里分配内存,最大可以申请多大的内存? 109 | > * 首先会扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存。如果这一内存块的尺寸正好与要求相当,就将它返回给调用者,如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中 110 | 111 | ## [栈和堆的区别](https://blog.csdn.net/caogenwangbaoqiang/article/details/79788368) 112 | * 管理方式不同 113 | * 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。 114 | * 空间大小不同 115 | * 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M 116 | * 能否产生碎片不同 117 | * 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出 118 | * 生长方向不同 119 | * 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长 120 | 121 | ## [面向对象和面向过程的区别](https://blog.csdn.net/jerry11112/article/details/79027834) 122 | 面向对象就是高度实物抽象化、面向过程就是自顶向下的编程 123 | > * 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用 124 | > * 面向对象是把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为 125 | > * 面向过程的优缺点 126 | 1. 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 127 | 2. 缺点:没有面向对象易维护、易复用、易扩展 128 | > * 面向对象的优缺点 129 | 1. 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 130 | 2. 缺点:性能比面向过程低 131 | 132 | ## const关键字(不可修改) 133 | > * 修饰变量,说明该变量不可以被改变; 134 | > * 修饰指针,分为指向常量的指针和指针常量;int *const p和const int *p 135 | > * 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改; 136 | > * 修饰成员函数,说明该成员函数内不能修改成员变量,本质是const this指针。 137 | 138 | ## static关键字(对外不可见) 139 | > * **修饰普通变量,** 修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,在整个程序运行期间一直存在,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它,自动初始化为0。 140 | 全局变量作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。 141 | 局部变量作用域:仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变; 142 | > * **修饰普通函数,** 其只能在定义它的源文件中使用,不能在其他源文件中被引用 143 | > * **修饰类成员变量和成员函数,** 它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。在 static 函数内不能访问非静态成员 144 | 145 | ## [extern关键字](https://www.cnblogs.com/qinguoyi/p/10100777.html) 146 | extern关键字主要修饰变量或函数,表示该函数可以跨文件访问,或者表明该变量在其他文件定义,在此处引用. 147 | > * **修饰变量或函数.**被 extern 限定的函数或变量是 extern 类型的 148 | > * **extern "C".** extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。 149 | 1. 在c头文件中通过#ifdef __cplusplus extern "C" { #endif来定义 150 | 2. 在对应的c文件中实现 151 | 3. 在cpp文件中通过“extern "C" 函数名”调用,或者包含c头文件 152 | **注意:** extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间,但若需要调用的函数太多,还是直接包含头文件吧. 153 | 154 | 155 | ## volatile关键字 156 | > * **不可优化性.** volatile 关键字是一种类型修饰符,用它声明的类型变量表示不可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。 157 | > * **易变性.** volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值) 158 | 159 | ## explicit关键字 160 | explicit关键字的作用就是防止对象间实现=赋值,防止类构造函数的隐式自动转换,类构造函数默认情况下即声明为implicit(隐式),另外explicit只用于单参数的构造函数,或者除了第一个参数以外的其他参数都有默认值. 161 | > * explicit 修饰构造函数时,可以防止隐式转换和复制初始化 162 | > * explicit 修饰转换函数时,可以防止隐式转换 163 | 164 |
165 | explicit用法 166 |
 
167 | class Person{
168 | public:
169 |     Person(){
170 |     }
171 |     //有参构造初始化数据
172 |     explicit Person(const char*str_){
173 |         str = (char *)malloc(sizeof(char)*100);
174 |         strcpy(str,str_);
175 |     }
176 |     ~Person() {
177 |         if (str != NULL){
178 |             free(str);
179 |             str = NULL;
180 |         }
181 |     }
182 |     char *str;
183 | };
184 | void test05(){
185 |     //Person p = "abc"; 隐式调用
186 |     Person p ("abc"); //显式调用
187 | }
188 |     
189 |
190 | 191 | 192 | ## 类成员属性 193 | 类的成员有三个权限:公有权限(public),私有权限(private),保护权限(protected)。 194 | > * **私有权限.** 私有成员在类内部可以访问,类外不可访问,一般推荐将成员变量设置为私有成员; 195 | > * **公有权限.** 类内类外都可以进行访问; 196 | > * **保护权限.** 类内和当前类的子类可以访问,类外不可访问。 197 | 198 | ## 大小端序的定义和代码判断 199 | ### 定义 200 | 一个16进制的地址,存放在内存中从低地址开始存储,如16进制的地址为0x1234,对于地址而言,从右往左是从低到高 201 | 202 | * 大端 203 | * 若16进制的高地址存放在内存的低地址,则为大端字节序,34存储在高位,12存储在低位 204 | * 小端 205 | * 若16进制的低地址存放在内存的低地址,则为小端字节序,12存储在高位,34存储在高位 206 | 207 | ### 代码判断 208 | 可以通过联合体来判断,联合体是同一块内存被联合体中的所有成员公用,如果后续成员对内存重新赋值,会覆盖内存中原有数据 209 | 210 | ```C++ 211 | union U{ 212 | int a; 213 | char b; 214 | }; 215 | 216 | int main(){ 217 | U u; 218 | u.a = 0x01020304; 219 | 220 | if (u.b == 0x04){ 221 | cout << u.b << endl; //输出char字符 222 | cout << "little" << endl; 223 | } 224 | 225 | else if (u.b == 0x01){ 226 | cout << u.b << endl; 227 | cout << "big" << endl; 228 | } 229 | system("pause"); 230 | return 0; 231 | } 232 | ``` 233 | 234 | 235 | ## 代码判断32位和64位系统 236 | 写一个指针,输出指针所占的字节大小 -------------------------------------------------------------------------------- /项目基础知识/Redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | 3 | > * [NoSQL](#NoSQL) 4 | > * [CAP和BASE](#CAP和BASE) 5 | > * [分布式和集群](#分布式和集群) 6 | > * [Redis及优缺点](#Redis及优缺点) 7 | > * [Redis线程模型](#Redis线程模型) 8 | > * [为什么单线程效率还这么高](#为什么单线程效率还这么高) 9 | > * [Redis数据类型和应用场景](#Redis数据类型和应用场景) 10 | > * [Redis持久化机制RDB和AOF](#Redis持久化机制RDB和AOF) 11 | > * [Redis事务](#Redis事务) 12 | > * [Redis过期策略和缓存淘汰策略](#Redis过期策略和缓存淘汰策略) 13 | > * [Redis主从复制+Redis哨兵](#Redis主从复制Redis哨兵) 14 | > * [Redis集群](#Redis集群) 15 | > * [分布式算法一致性哈希](#分布式算法一致性哈希) 16 | > * [Redis哈希槽](#Redis哈希槽) 17 | > * [Redis和MySQL缓存和数据库的双写一致性](#Redis和MySQL缓存和数据库的双写一致性) 18 | > * [缓存血崩和缓存穿透及解决方案](#缓存血崩和缓存穿透及解决方案) 19 | > * [Redis常见问题及解决方案](#Redis常见问题及解决方案) 20 | 21 | 22 | ## NoSQL 23 | Not only SQL,非关系型数据库 24 | 25 | * K-V键值对 26 | * Redis和memcached 27 | * 文档型数据库 28 | * mongoDB| 29 | 30 | ## CAP和BASE 31 | SQL数据库中是ACID,NoSQL中是CAP原理 32 | 33 | ### CAP原理 34 | * C 35 | * consistency 强一致性 36 | * A 37 | * Availability 可用性 38 | * P 39 | * Partition tolerance 分区容错性 40 | 41 | ### CAP的3进2 42 | 一个分布式系统**必须实现分区容错性**,CAP只能同时满足其中两个 43 | 44 | * CA 45 | * 传统的MySQL和Orcal数据库 46 | * AP 47 | * 大多数网站的架构选择 48 | * CP 49 | * Redis和MongoDB 50 | 51 | ### BASE 52 | 为了解决关系数据库强一致性导致可用性降低的问题提出的解决方案,通过让系统放松对某一时刻数据的一致性要求来换取系统整体的伸缩性和性能上的改观 53 | 54 | * BA 55 | * 基本可以,Basically Available 56 | * S 57 | * 软状态,Soft State 58 | * E 59 | * 最终一致,Eventually consistent 60 | 61 | ## 分布式和集群 62 | * 分布式 63 | * 不同的多台服务器上部署不同的服务器模块,他们之间通过RPC(远程过程调用)来通信和调用 64 | * 集群 65 | * 不同的多台服务器上部署着相同的服务器模块,通过分布式调度软件统一调度 66 | 67 | ## Redis及优缺点 68 | Redis是Remote Dictionary Server(远程字典服务器),是**高性能的K/V分布式内存数据库** 69 | 70 | ### 相对于memcached的不同 71 | * Redis支持数据持久化,可以将内存中的数据保存到磁盘中,重启时再次加载到内存中 72 | * Redis支持丰富的数据结构,list,set,hash,zset 73 | * Redis是单进程单线程,Memcached是单进程多线程 74 | 75 | ### 缺点 76 | * Redis主要消耗内存资源,数据库容量受到**物理内存的限制**,不能用作海量数据的高性能读写 77 | * 因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上 78 | 79 | ## Redis线程模型 80 | * 单线程模型处理客户端请求 81 | * 封装epoll函数实现读写事件的响应 82 | 83 | ## 为什么单线程效率还这么高 84 | * 纯内存操作 85 | * 核心是基于非阻塞的 IO 多路复用机制 86 | * 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题 87 | 88 | ## Redis数据类型和应用场景 89 | Redis五大数据类型,string,hash,list,set,zset 90 | 91 | * string(字符串) 92 | * **一个字符串类型的值能存储最大容量是512M** 93 | * set/get 94 | * 设置/获取 95 | * setex 96 | * setex k4 10 v4 97 | * 设置过期时间10s,然后给k4赋值为v4 98 | * setnx 99 | * setnx k1 v11 100 | * 若k1存在,则不覆盖,否则,建立k1,并赋值v11 101 | * mset/get 102 | * hash(哈希) 103 | * list(列表) 104 | * lpush/rpush 105 | * 从左边/右边压入 106 | * lrange 107 | * 指定范围,从左边获取 108 | * lpop/rpop 109 | * 从左边/右边弹出 110 | * set(集合) 111 | * zset(有序集合) 112 | * Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构 113 | 114 | ## [Redis持久化机制RDB和AOF](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-persistence.md) 115 | * 默认是RDB持久 116 | * redis 的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化,RDB是数据,AOF是写命令。 117 | * 同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。 118 | 119 | ### RDB 120 | RDB(Redis Database)持久化机制,是对redis中的**数据**执行**周期性**的持久化,生成dump.rdb文件。 121 | 122 | * snapshot快照 123 | * 在指定时间间隔将内存的数据集快照写入磁盘,恢复时将快照直接读入内存 124 | * 默认rdb参数,可以通过save 秒数 次数来修改rdb参数 125 | * 1分钟改1万次 126 | * 5分钟改10次 127 | * 15分钟改1次 128 | * 当执行save,bgsave,flushall,shutdown命令时,也会生成快照文件dump.rdb,其中flushall会生成空的dump文件。 129 | * save和bgsave 130 | * save立即保存,不会等待RDB参数,另外save只保存,其他不管,因此会阻塞 131 | * bgsave下Redis会在后台异步进行快照操作,同时相应客户端请求 132 | * 恢复 133 | * redis启动时,会自动加载dump.rdb文件,将快照加载到内存中 134 | 135 | * 优点 136 | * RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 redis 的数据,这种多个数据文件的方式,非常适合做冷备 137 | * 我们知道RDB每次生成的新dump文件都会覆盖旧文件,如果想保存每个时刻写入的快照文件,可以另外修改代码,按时间生成的dump文件,并传到备份机器。 138 | * RDB 对 redis 对外提供的读写服务,影响非常小,可以让 redis 保持高性能,因为 redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。 139 | * 大规模数据比AOF快,直接将数据恢复到内存中,想对于AOF回放要快 140 | * 缺点 141 | * 一定时间间隔做一次持久化,若redis意外宕机,则会丢失最后一次快照的修改 142 | * 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。 143 | 144 | ### AOF 145 | AOF(Append Only File) 机制对**每条写命令**作为日志,以 append-only(追加) 的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集,生成appendonly.aof文件 146 | 147 | * 追加方式 148 | * everysec 149 | * 异步持久化,默认是每秒进行记录,最多丢失一秒的数据 150 | * always 151 | * 同步持久化,每次修改都会同步,redis性能会大大降低 152 | * 恢复 153 | * 正常恢复 154 | * 启动appendonly yes 155 | * 将数据aof复制到对应目录 156 | * 重新启动redis 157 | * 异常恢复 158 | * redis-check-aof --fix appendonly.aof 159 | * dump也可以这样修复 160 | * rewrite 161 | * AOF会越来越大,文件会越来越大,redis默认每个文件64M,当超过上一个文件大小一倍时,启动重写 162 | * 在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。 163 | * 在创建新日志文件的时候,老的日志文件还是照常写入。 164 | * 每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建 165 | * 当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。 166 | 167 | * 优点 168 | * AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次fsync操作,最多丢失 1 秒钟的数据。 169 | * AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合**做灾难性的误删除的紧急恢复**。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。 170 | * 缺点 171 | * 类似 AOF 这种较为复杂的基于命令日志 / merge / 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。 172 | * 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大 173 | 174 | ## [Redis事务](https://juejin.im/post/5cb1c9adf265da03893284e9) 175 | ### Redis事务操作 176 | 177 | 178 | ## [Redis过期策略和缓存淘汰策略](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-expiration-policies-and-lru.md) 179 | ### 过期策略 180 | redis 过期策略是:定期删除+惰性删除 181 | 182 | * 定期删除 183 | * redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。 184 | * 惰性删除 185 | * 在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。 186 | 187 | ### 淘汰策略 188 | * noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。 189 | * allkeys-lru:当内存不足以容纳新写入数据时,**在键空间中**,移除最近最少使用的 key**(这个是最常用的)**。 190 | * allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。 191 | * volatile-lru:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间中**,移除最近最少使用的 key**(这个一般不太合适)**。 192 | * volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。 193 | * volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。 194 | 195 | ### MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据? 196 | * 首先缓存预热,将我们认为是热点的数据放在redis中,然后根据请求更新数据,redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 197 | 198 | ## [Redis主从复制+Redis哨兵](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md) 199 | Redis实现高并发和高可用,可以使用Redis主从架构+哨兵或Redis集群两种方式 200 | 201 | 202 | ## [Redis集群](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-cluster.md) 203 | 204 | ## 分布式算法一致性哈希 205 | ### 普通哈希 206 | * 原始的做法是对缓存项的键进行哈希,将hash后的结果对**缓存服务器的数量**进行取模操作,通过取模后的结果,决定缓存项将会缓存在哪一台服务器上。 207 | * 缺点 208 | * 当服务器数量发生变化的时候,所有缓存在一定时间内是失效的,重新哈希,跟服务器结点的数量有关 209 | * 当应用无法从缓存中获取数据时,则会向后端服务器请求数据,造成了缓存的雪崩,整个系统很有可能被压垮 210 | 211 | ### 一致性哈希 212 | * 环形hash空间的概念 213 | * 通常hash算法都是将value映射在一个32位的key值当中,那么把数据首尾相接就会形成一个圆形,取值范围为0 ~ 2^32-1,这个圆环就是环形hash空间。 214 | 215 | * 一致性哈希步骤 216 | * 首先求出服务器(节点)的哈希值,并将其配置到0~2^32-1的圆(continuum)上。 217 | * 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。 218 | * 然后从数据映射到的位置开始**顺时针查找**,将数据保存到找到的第一个服务器上。如果超过2^32-1仍然找不到服务器,就会保存到第一台服务器上。(0和2^32重合,超过后就顺序找到第一台了) 219 | 220 | * 当发生服务器结点变化 221 | * 一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。 222 | * 一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性 223 | 224 | ### 虚拟结点 225 | 一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。 226 | 227 | * rehash实现虚拟结点进行平衡 228 | * 对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点 229 | * 数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上,这样就解决了服务节点少时数据倾斜的问题。 230 | * 在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。 231 | 232 | ## Redis哈希槽 233 | ### 哈希槽 234 | **Redis集群没有使用一致性hash,而是引入了哈希槽的概念**,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。 235 | ### Redis集群最多结点数量 236 | 16384 237 | 238 | ### 新增结点,哈希槽如何重新分配 239 | 每个结点从自己的哈希槽中分出若干,分配给新结点,最终使所有节点负责的哈希槽数量基本一致 240 | 241 | ## [Redis和MySQL缓存和数据库的双写一致性](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md) 242 | 243 | 244 | ## [缓存血崩和缓存穿透及解决方案](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md) 245 | ### 缓存血崩 246 | 缓存宕机,此时所有请求落到数据库上,由于高并发的请求,导致数据库宕机,发生缓存血崩。 247 | 248 | * 解决方案 249 | * 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 250 | * 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 251 | * 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 252 | 253 | * 请求流程 254 | * 用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。 255 | * 限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。 256 | 257 | * 好处 258 | * 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 259 | * 只要数据库不死,就是说,对用户来说,部分请求都是可以被处理的。 260 | 261 | ### 缓存穿透 262 | 发来的请求或恶意攻击,此时的数据在缓存和数据库中都没有,因此全部在数据库中查询,导致缓存穿透;若每次都有大量穿透,直接查数据库,则数据库宕机。 263 | 264 | * 解决方案 265 | * 每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set key UNKNOWN。 266 | * 然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据 267 | 268 | ### 缓存击穿 269 | 缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库。 270 | 271 | * 解决方案 272 | * 将热点key设置为永不过期 273 | 274 | ## Redis常见问题及解决方案 275 | * Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 276 | * 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 277 | * 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 278 | * 尽量避免在压力很大的主库上增加从库 279 | * 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。 -------------------------------------------------------------------------------- /项目基础知识/Socket编程/README.md: -------------------------------------------------------------------------------- 1 | # 网络编程 2 | 3 | > * [网络编程的步骤](#网络编程的步骤) 4 | > * [常用API](#常用API) 5 | > * [TCP中的accept和connect和listen的关系](#TCP中的accept和connect和listen的关系) 6 | > * [UDP中的connect](#UDP中的connect) 7 | > * [广播和组播过程](#广播和组播过程) 8 | > * [服务端大量TIMEWAIT或CLOSEWAIT状态](#服务端大量TIMEWAIT或CLOSEWAIT状态) 9 | > * [复位报文段RST](#复位报文段RST) 10 | > * [优雅关闭和半关闭](#优雅关闭和半关闭) 11 | > * [解决TCP粘包](#解决TCP粘包) 12 | > * [select可以判断网络断开吗](#select可以判断网络断开吗) 13 | > * [send和read的阻塞和非阻塞情况](#send和read的阻塞和非阻塞情况) 14 | > * [网络字节序和主机序](#网络字节序和主机序) 15 | > * [IP地址分类及转换](#IP地址分类及转换) 16 | > * [select实现异步connect](#select实现异步connect) 17 | > * [为什么忽略SIGPIPE信号](#为什么忽略SIGPIPE信号) 18 | > * [如何设置非阻塞](#如何设置非阻塞) 19 | 20 | 21 | 22 | ## 网络编程步骤 23 | 24 | ### TCP 25 | 26 | * 服务端:socket -> bind -> listen -> accept -> recv/send -> close 27 | * 创建一个socket,用函数socket(),设置SOCK_STREAM 28 | * 设置服务器地址和侦听端口,初始化要绑定的网络地址结构 29 | * 绑定服务器端IP地址、端口等信息到socket上,用函数bind() 30 | * 设置允许的最大连接数,用函数listen() 31 | * 接收客户端上来的连接,用函数accept() 32 | * 收发数据,用函数send()和recv(),或者read()和write() 33 | * 关闭网络连接close(),需要关闭服务端sock和accept产生的客户端sock文件描述符 34 | 35 | * 客户端:socket -> connect -> send/recv -> close 36 | * 创建一个socket,用函数socket() 37 | * 设置要连接的对方的IP地址和端口等属性 38 | * 连接服务器,用函数connect() 39 | * 收发数据,用函数send()和recv(),或read()和write() 40 | * 关闭网络连接close() 41 | * 注意 42 | * INADDR_ANY表示本机任意地址,一般服务器端都可以这样写 43 | * accept中接收的是客户端的地址,返回对应当前客户端的一个clisock文件描述符,表示当前客户端的tcp连接 44 | * send和recv中接收的是新建立的客户端的sock地址 45 | 46 | ### UDP 47 | * 服务端:socket -> bind -> recvfrom/sendto -> close 48 | * 建立套接字文件描述符,使用函数socket(),设置SOCK_DGRAM 49 | * 设置服务器地址和侦听端口,初始化要绑定的网络地址结构 50 | * 绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定 51 | * 接收客户端的数据,使用recvfrom()函数接收客户端的网络数据 52 | * 向客户端发送数据,使用sendto()函数向服务器主机发送数据 53 | * 关闭套接字,使用close()函数释放资源 54 | * 客户端:socket -> sendto/recvfrom -> close 55 | * 建立套接字文件描述符,socket() 56 | * 设置服务器地址和端口,struct sockaddr 57 | * 向服务器发送数据,sendto() 58 | * 接收服务器的数据,recvfrom() 59 | * 关闭套接字,close() 60 | * 注意 61 | * sendto和recvfrom的第56个参数是sock地址 62 | * 服务器端的recvfrom和sendto都是cli地址 63 | * 客户端sendto是服务器端的地址,最后一个参数是指针,recvfrom是新建的from地址,最后一个参数是整型 64 | * UDP不用listen,accept,因为UDP无连接 65 | * UDP通过sendto函数完成套接字的地址分配工作 66 | * 第一阶段:向UDP套接字注册IP和端口号 67 | * 第二阶段:传输数据 68 | * 第三阶段:删除UDP套接字中注册的目标地址信息 69 | * 每次调用sendto函数都重复上述过程,每次都变更地址,因此可以重复利用同一UDP套接字向不同的目标传输数据 70 | 71 | ## 常用API 72 | sendto、recvfrom保存对端的地址 73 | 74 | * sendto 75 | * recvfrom 76 | 77 | ## [TCP中的accept和connect和listen的关系](https://blog.csdn.net/tennysonsky/article/details/45621341) 78 | 79 | ### listen 80 | * listen功能 81 | * listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求 82 | * 参数 backlog 的作用是设置内核中连接队列的长度 83 | * 根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换成LISTEN状态。 84 | 85 | * 是否阻塞 86 | * listen()函数不会阻塞,它将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。 87 | 88 | * backlog的作用 89 | * backlog是队列的长度,内核为任何一个给定的监听套接口维护两个队列: 90 | * 未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。 91 | * 已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态 92 | * 当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,该项就从未完成连接队列移到已完成连接队列的队尾,将建立好的链接自动存储到队列中,如此重复 93 | * backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5 94 | 95 | ### connect 96 | * connect功能 97 | * 对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。 98 | * connect之后是三次握手 99 | 100 | * 是否阻塞 101 | * 通常的情况,客户端的connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。 102 | 103 | 104 | ### accept 105 | * accept功能 106 | * accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接(**三次握手之后**) 107 | 108 | * 是否阻塞 109 | * 如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。 110 | * 如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT 111 | 112 | ## UDP中的connect 113 | UDP的connect和TCP的connect完全不同,UDP不会引起三次握手 114 | 115 | * 未连接的UDP传输数据 116 | * 第一阶段:向UDP套接字注册IP和端口号 117 | * 第二阶段:传输数据 118 | * 第三阶段:删除UDP套接字中注册的目标地址信息 119 | * 已连接的UDP传输数据 120 | * 第一阶段:向UDP套接字注册IP和端口号 121 | * 第二阶段:传输数据 122 | * 第三阶段:传输数据 123 | 124 | * 可以提高传输效率 125 | * 采用connect的UDP发送接受报文可以调用send,write和recv,read操作,也可以调用sendto,recvfrom,此时需要将第五和第六个参数置为NULL或0 126 | * 由已连接的UDP套接口引发的异步错误,返回给他们所在的进程。相反我们说过,未连接UDP套接口不接收任何异步错误给一个UDP套接口,connect后的udp套接口write可以检测发送数据成功与否,直接sendto无法检测 127 | 128 | * 多次调用connect拥有一个已连接UDP套接口的进程的作用 129 | * 指定新的IP地址和端口号 130 | * 断开套接口 131 | 132 | 133 | ## 广播和组播过程 134 | 135 | * 广播 136 | * 只适用于局域网 137 | * 只能向同一网络中的主机传输数据, 138 | * 组播 139 | * 适用于局域网和广域网(internet) 140 | 141 | ## 服务端大量TIMEWAIT或CLOSEWAIT状态 142 | 首先通过TCP的四次挥手过程分析确定两个状态的出现背景。TIMEWAIT是大量tcp短连接导致的,确保对方收到最后发出的ACK,一般为2MSL;CLOSEWAIT是tcp连接不关闭导致的,出现在close()函数之前。 143 | ### TIMEWAIT 144 | * 可以通过设置SOCKET选项SO_REUSEADDR来重用处于TIMEWAIT的sock地址,对应于内核中的tcp_tw_reuse,这个参数不是“消除” TIME_WAIT的,而是说当资源不够时,可以重用TIME_WAIT的连接 145 | * 修改ipv4.ip_local_port_range,增大可用端口范围,来承受更多TIME 146 | * 设置SOCK选项SO_LINGER选项,这样会直接消除TIMEWAIT 147 | 148 | ### CLOSEWAIT 149 | 客户端主动关闭,而服务端没有close关闭连接,则服务端产生大量CLOSEWAIT,一般都是业务代码有问题 150 | 151 | ## 复位报文段RST 152 | * 访问不存在的端口,或服务器端没有启动 153 | * 异常终止连接 154 | * TCP提供了异常终止连接的方法,给对方发送一个复位报文段 155 | * 此时对端read会返回-1,显示错误errno:Connection reset by peer 156 | * 这种错误可以通过shutdown来解决 157 | * 处理半打开连接 158 | * 当某端崩溃退出,此时对端并不知道,若往对端发送数据,会响应一个RST复位报文段 159 | 160 | ## [优雅关闭和半关闭](https://www.cnblogs.com/liyulong1982/p/3990740.htm) 161 | ### 概念 162 | * 一个文件描述符关联一个文件,这里是网络套接字。 163 | * close会关闭用户应用程序中的socket句柄,释放相关资源,从而触发关闭TCP连接 164 | * 关闭TCP连接,是关闭网络套接字,断开连接 165 | * close只是减少引用计数,只有当引用计数为0的时候,才发送fin,真正关闭连接 166 | * shutdown不同,只要以SHUT_WR/SHUT_RDWR方式调用即发送FIN包 167 | * shutdown后要调用close 168 | * 保持连接的某一端想关闭连接了,但它**需要确保要发送的数据全部发送完毕以后才断开连接**,此种情况下需要使用优雅关闭,一种是shutdown,一种是设置SO_LINGER的close 169 | * 半关闭,是关闭写端,但可以读对方的数据,这种只能通过shutdown实现 170 | 171 | ### close 172 | close函数会关闭文件描述符,不会立马关闭网络套接字,除非引用计数为0,则会触发调用关闭TCP连接。 173 | 174 | * 检查接收缓冲区是否有数据未读(不包括FIN包),如果有数据未读,协议栈会发送RST包,而不是FIN包。如果套接字设置了SO_LINGER选项,并且lingertime设置为0,这种情况下也会发送RST包来终止连接。其他情况下,会检查套接字的状态,只有在套接字的状态是TCP_ESTABLISHED、TCP_SYN_RECV和TCP_CLOSE_WAIT的状态下,才会发送FIN包 175 | * 若有多个进程调用同一个网络套接字,会将网络套接字的文件描述符+1,close调用只是将当前套接字的文件描述符-1,只会对当前的进程有效,只会关闭当前进程的文件描述符,其他进程同样可以访问该套接字 176 | * close函数的默认行为是,关闭一个socket,close将立即返回,TCP模块尝试把该socket对应的TCP缓冲区中的残留数据发送给对方,并不保证能到达对方 177 | * close行为可以通过SO_LINGER修改 178 | 179 | ```C++ 180 | struct linger{ 181 | int l_onoff; //开启或关闭该选项 182 | int l_linger; //滞留时间 183 | } 184 | ``` 185 | * l_onoff为0,该选项不起作用,采用默认close行为 186 | * l_onoff不为0 187 | * l_linger为0,close立即返回,TCP模块丢弃被关闭的socket对应的TCP缓冲区中的数据,给对方发送RST复位信号,这样可以异常终止连接,且完全消除了TIME_WAIT状态 188 | * l_linger不为0 189 | * 阻塞socket,被关闭的socket对应TCP缓冲区,若还有数据,close会阻塞,进程睡眠,直到收到对方的确认或等待l_linger时间,若超时仍未收到确认,则close返回-1设置errno为EWOULDBLOCK 190 | * 非阻塞socket,close立即返回,需要根据返回值和errno判断残留数据是够发送完毕 191 | 192 | ### shutdown 193 | shutdown没有采用引用计数的机制,会影响所有进程的网络套接字,可以只关闭套接字的读端或写端,也可全部关闭,用于实现半关闭,会直接发送FIN包 194 | 195 | * SHUT_RD,关闭sockfd上的读端,不能再对sockfd文件描述符进行读操作,且接收缓冲区中的所有数据都会丢弃 196 | * SHUT_WR,关闭写端,确保发送缓冲区中的数据会在真正关闭连接之前会发送出去,不能对其进行写操作,连接处于半关闭状态 197 | * SHUT_RDWR,同时关闭sockfd的读写 198 | 199 | ## 解决TCP粘包 200 | ### 什么是TCP粘包 201 | 由于TCP是流协议,因此TCP接收不能确保每次一个包,有可能接收一个包和下一个包的一部分。 202 | ### 如何解决 203 | 包头和包体,包头中有包体长度 204 | 205 | ## select可以直接判断网络断开吗 206 | 不可以。若网络断开,select检测描述符会发生读事件,这时调用read函数发现读到的数据长度为0. 207 | 208 | ## send和recv的阻塞和非阻塞情况 209 | send函数返回100,并不是将100个字节的数据发送到网络上或对端,而是发送到了协议栈的写缓冲区,至于什么时候发送,由协议栈决定。 210 | 211 | ### send 212 | * 阻塞 213 | * 一直等待,直到写缓冲区有空闲 214 | * 成功写返回发送数据长度 215 | * 失败返回-1 216 | * 非阻塞 217 | * 不等待,立即返回,成功返回数据长度 218 | * 返回-1,判断错误码 219 | * 若错误码为EAGAIN或EWOULDBLOCK则表示写缓冲区不空闲 220 | * 若错误码为ERROR,则表示失败 221 | 222 | ### recv 223 | * 阻塞 224 | * 一直等待,直到读缓冲区有数据 225 | * 成功写返回数据长度 226 | * 失败返回-1 227 | * 非阻塞 228 | * 不等待,立即返回,成功返回数据长度 229 | * 返回-1,判断错误码 230 | * 若错误码为EAGAIN或EWOULDBLOCK则表示读缓冲区没数据 231 | * 若错误码为ERROR,则表示失败 232 | * 返回0 233 | * 对端关闭连接 234 | 235 | ## 网络字节序和主机序 236 | 字节序分为大端字节序和小端字节序,大端字节序也称网络字节序,小端字节序也称为主机字节序。 237 | 238 | * 大端字节序 239 | * 一个整数的高位字节存储在低位地址,低位字节存储在高位地址 240 | * 小端字节序 241 | * 高位字节存储在高位地址,低位字节存储在低位地址 242 | * 转换API 243 | * htonl 主机序转网络序,长整型,用于转换IP地址 244 | * htons 主机序转网络序,短整型,用于转换端口号 245 | * ntohl 网络序转主机序 246 | * ntohs 网络序转主机序 247 | 248 | ## [IP地址分类及转换](https://blog.csdn.net/kzadmxz/article/details/73658168) 249 | ### IP地址分类 250 | 251 | ### IP转换 252 | 字符串表示的点分十进制转换成网络字节序的IP地址 253 | 254 | * pton,点分十进制转换成地址 255 | * ntop,地址转换成点分十进制 256 | 257 | ## [select实现异步connect](https://blog.csdn.net/nphyez/article/details/10268723) 258 | 通常阻塞的connect 函数会等待三次握手成功或失败后返回,0成功,-1失败。如果对方未响应,要隔6s,重发尝试,可能要等待75s的尝试并最终返回超时,才得知连接失败。即使是一次尝试成功,也会等待几毫秒到几秒的时间,如果此期间有其他事务要处理,则会白白浪费时间,而用非阻塞的connect 则可以做到并行,提高效率。 259 | 260 | ### 实现步骤 261 | * 创建socket,返回套接字描述符; 262 | * 调用fcntl 把套接字描述符设置成**非阻塞**; 263 | * 调用connect 开始建立连接; 264 | * 判断连接是否成功建立。 265 | 266 | ### 判断连接是否成功建立: 267 | * 如果为**非阻塞**模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。 268 | * 如果connect 返回0,表示连接成功(服务器和客户端在同一台机器上时就有可能发生这种情况) 269 | * 失败可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。 270 | * 调用select 来等待连接建立成功完成; 271 | * 如果select 返回0,则表示建立连接超时。我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去。 272 | * 如果select 返回大于0的值,并不是成功建立连接,而是表示套接字描述符可读或可写 273 | * 当连接建立成功时,套接字描述符变成可写(连接建立时,写缓冲区空闲,所以可写) 274 | * 当连接建立出错时,套接字描述符变成既可读又可写(由于有未决的错误,从而可读又可写) 275 | * 如果套接口描述符可写,则我们可以通过调用getsockopt来得到套接口上待处理的错误(SO_ERROR) 276 | * 如果连接建立成功,这个错误值将是0 277 | * 如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等)。 278 | 279 | ## 为什么忽略SIGPIPE信号 280 | * 假设server和client 已经建立了连接,server调用了close, 发送FIN 段给client(其实不一定会发送FIN段,后面再说),此时server不能再通过socket发送和接收数据,此时client调用read,如果接收到FIN 段会返回0 281 | * 但client此时还是可以write 给server的,write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错,而server收到数据后应答一个RST段,表示服务器已经不能接收数据,连接重置,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。 282 | * 如果client再次调用write发数据给server,由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序。 283 | 284 | * 有时候代码中需要连续多次调用write,可能还来不及调用read得知对方已关闭了连接就被SIGPIPE信号终止掉了,这就需要在初始化时调用sigaction处理SIGPIPE信号,对于这个信号的处理我们通常忽略即可 285 | 286 | * 往一个读端关闭的管道或者读端关闭的socket连接中写入数据,会引发SIGPIPE信号。当系统受到该信号会结束进程是,但我们不希望因为错误的写操作导致程序退出。 287 | 288 | * 通过sigaction函数设置信号,将handler设置为SIG_IGN将其忽略 289 | * 通过send函数的MSG_NOSIGNAL来禁止写操作触发SIGPIPE信号 290 | 291 | ## 如何设置文件描述符非阻塞 292 | * 通过fcntl设置 293 | ```c++ 294 | int flag = fcntl(fd, F_GETFL); 295 | flag |= O_NONBLOCK; 296 | fctncl(fd, F_SETFL, flag); 297 | ``` -------------------------------------------------------------------------------- /计算机基础知识/数据结构/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 查找算法 3 | > * [二叉树基础](#二叉树基础) 4 | > * [最大堆和最小堆](#最大堆和最小堆) 5 | > * [二分查找](#二分查找) 6 | > * [二叉排序树](#二叉排序树) 7 | > * [平衡二叉树](#平衡二叉树) 8 | > * [多路查找树2-3树](#多路查找树2-3树) 9 | > * [红黑树](#红黑树) 10 | > * [B/B+树](#BB树) 11 | > * [哈希表](#哈希表) 12 | > * [跳表](#跳表) 13 | > * [位图](#位图) 14 | 15 | # 数组和链表 16 | > * [数组和链表的区别](#数组和链表的区别) 17 | 18 | # 赫夫曼编码 19 | > * [赫夫曼树](#赫夫曼树) 20 | > * [赫夫曼编码](#赫夫曼编码) 21 | 22 | 23 | ## 二叉树基础 24 | * 二叉树定义 25 | * n个结点的有限集合,该集合为空集,或者一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成 26 | * 满二叉树 27 | * 一棵二叉树中所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上 28 | * 完全二叉树 29 | * 一棵有n个结点的二叉树按层序编号,编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同 30 | * 二叉树的性质 31 | * 非空二叉树第 i 层最多 2^(i-1) 个结点 (i >= 1) 32 | * 深度为 k 的二叉树最多 2^k - 1 个结点 (k >= 1) 33 | * 度为 0 的结点数为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1 34 | * 有 n 个结点的完全二叉树深度 k = ⌊ log2(n) ⌋ + 1 35 | * 对于含 n 个结点的完全二叉树中编号为 i (1 <= i <= n) 的结点 36 | * 若 i = 1,为根,否则双亲为 ⌊ i / 2 ⌋ 37 | * 若 2i > n,则 i 结点没有左孩子,否则孩子编号为 2i 38 | * 若 2i + 1 > n,则 i 结点没有右孩子,否则孩子编号为 2i + 1 39 | 40 | ## 最大堆和最小堆 41 | 堆是完全二叉树,根据结点和左右孩子的大小关系,分为最大堆和最小堆 42 | 43 | * 最大堆 44 | * 每个结点的值都大于或等于其左右孩子结点的值 45 | * 最小堆 46 | * 每个结点的值都小于或等于其左右孩子结点的值 47 | 48 | ## 二分查找 49 | 二分查找适用于有序数组 50 | 51 | * 查找时间复杂度O(logn) 52 | 53 | 54 | 55 | ## 二叉排序树 56 | 57 | * 查找时间复杂度O(logn),最坏情况变成右斜树O(n) 58 | 59 | 二叉排序树,又称二叉搜索树,若二叉排序树不为空,则具有下列性质 60 | > * 若左子树不为空,则左子树上所有结点的值小于根节点的值 61 | > * 若右子树不为空,则右子树上所有结点的值大于根节点的值 62 | 63 | ## 平衡二叉树 64 | 65 | * 查找时间复杂度O(logn),插入和删除时间复杂度O(logn) 66 | 67 | 平衡二叉树是**二叉排序树**,每一个结点左右子树高度之差的绝对值不超过1 68 | 69 | ## [多路查找树2 3树](https://blog.csdn.net/u014688145/article/details/67636509) 70 | 多路查找树,每一个结点的孩子数可以多于两个,每一个节点处可以存储多个元素 71 | ### 2-3树 72 | 每一个结点都具有两个孩子(2结点)或三个孩子(3结点) 73 | 一个2结点包含一个元素和两个孩子 74 | 一个3结点包含一小一大两个元素和三个孩子 75 | 76 | ## [红黑树](https://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html) 77 | 78 | * 查找时间复杂度O(logn),插入和删除时间复杂度O(logn) 79 | 80 | * 红黑树的五个性质(性质没法解释),红黑树是这样的树!!! 81 | > * 每个结点非红即黑 82 | > * 根结点为黑色 83 | > * 每个叶结点(叶结点即实际叶子结点的NULL指针或NULL结点)都是黑的; 84 | > * 若结点为红色,其子节点一定是黑色(没有连续的红结点) 85 | > * 对于每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点(叶子结点要补充两个NULL结点) 86 | > * 有了五条性质,才有一个结论:**通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡**。 87 | * 平衡树和红黑树的区别 88 | * **AVL树是高度平衡的**,频繁的插入和删除,会引起频繁的调整以重新平衡,导致效率下降 89 | * **红黑树不是高度平衡的**,算是一种折中,插入最多两次旋转,删除最多三次旋转,调整时新插入的都是红色。 90 | 91 | * 为什么红黑树的插入、删除和查找如此高效? 92 | * 插入、删除和查找操作与树的高度成正比,因此如果平衡二叉树不会频繁的调整以重新平衡,那它肯定是最快的,但它需要频繁调整以保证平衡 93 | * 红黑树算是一种折中,保证最长路径不超过最短路径的二倍,这种情况下插入最多两次旋转,删除最多三次旋转,因此比平衡二叉树高效。 94 | 95 | * 红黑树为什么要保证每条路径上黑色结点数目一致? 96 | * 为了保证红黑树保证最长路径不超过最短路径的二倍 97 | * 假设一个红黑树T,其到叶节点的最短路径肯定全部是黑色节点(共B个),最长路径肯定有相同个黑色节点(性质5:黑色节点的数量是相等),另外会多几个红色节点。性质4(红色节点必须有两个黑色儿子节点)能保证不会再现两个连续的红色节点。所以最长的路径长度应该是2B个节点,其中B个红色,B个黑色。 98 | 99 | ## [B/B+树](https://blog.csdn.net/du5006150054/article/details/82379210) 100 | B树是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例,结点最大的孩子数目称为B树的阶。 101 | 102 | ### B/B+树的区别 103 | 以(key,value)二元组来存储信息 104 | 105 | #### B树 106 | * B树中的每个结点中既包含key,也包含value值,每个结点占用一个盘块的磁盘空间,一个结点上有两个升序排序的关键字和三个指向子树根结点的指针,指针存储的是子结点所在磁盘块的地址。 107 | * **缺点**:每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。 108 | 109 | #### B+树 110 | * 所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度 111 | * B+树的头指针有两个,一个指向根节点,另一个指向关键字最小的元素,因此B+树有两种遍历的方式 112 | * 从根节点开始随机查询 113 | * 从最小关键词顺序查询 114 | * 优点 115 | * 由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 116 | * B+树的叶子结点都是相连的,因此对整棵树的便利只需要一次线性遍历叶子结点即可,方便随机查找和范围查找 117 | 118 | #### 区别 119 | > * 非叶子节点只存储键值信息,该值是其子树中的最大或最小值 120 | > * 所有叶子节点之间都有一个链指针。 121 | > * 数据记录都存放在叶子节点中,叶子结点按照关键字的大小自小而大顺序连接 122 | > * n棵子树的结点中包含n个关键字 123 | > * 非叶子结点中的元素会在子树根结点中再次列出 124 | 125 | ### [B树和红黑树应用场景](https://blog.csdn.net/zcf9916/article/details/84915506) 126 | 127 | * B树 128 | * B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度. 129 | * 红黑树 130 | * IO多路复用epoll的实现采用红黑树组织管理sockfd,以支持快速的增删改查. 131 | 132 | #### 实例 133 | * 假定一个节点可以容纳100个值,那么3层的B树可以容纳100万个数据,如果换成二叉查找树,则需要20层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在100万个数据中查找目标值,只需要读取两次硬盘。 134 | 135 | ## [哈希表](https://www.cnblogs.com/yangecnu/p/Introduce-Hashtable.html) 136 | 哈希表是一种以键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其存储位置,在该位置上存储着对应的数据 137 | 138 | ### 哈希表的实现 139 | * 主要包括构造哈希和处理哈希冲突 140 | * **使用哈希函数将被查找的键转换为数组的索引。**方法有**直接地址法、平方取中法、除留余数法**等。理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突。 141 | * **处理哈希碰撞冲突。**有很多处理哈希碰撞冲突的方法,如**拉链法(哈希桶)、线性探测法(开放定址法)、再哈希法、公共溢出区法**。 142 | 143 | ### 构造哈希 144 | * 直接地址法 145 | * 直接用key或key的线性函数值作为索引 146 | * 如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。 147 | * 平方取中法 148 | * 将key平方后取中间的几位数作为索引,可以是3位或4位 149 | * 除留余数法 150 | * 最常用的构造哈希函数的方法,直接对key取模,也可以平方后取模。 151 | * 获取**正整数哈希值**最常用的方法是使用除留余数法,即对于大小为素数M的数组,对于任意正整数k,计算k除以M的余数。**M一般取素数**。 152 | 153 | #### 字符串哈希值 154 | * 将字符串作为键的时候,可以将它作为一个大的整数,将组成字符串的每一个字符取ASCII值然后进行哈希,采用Horner计算字符串哈希值。 155 | * 如果对每个字符去哈希值可能会比较耗时,所以可以通过间隔取N个字符来获取哈西值来节省时间 156 | 157 | ```C++ 158 | int hash = 0; 159 | char *str = "abcdef"; 160 | for(int i = 0;i < strlen(str);i++){ 161 | hash = 31*hash + (int)str[i]; 162 | } 163 | cout<0-31 264 | a[1]--------->32-63 265 | a[2]--------->64-95 266 | a[3]--------->96-127 .......... 267 | 268 | 269 | **位映射步骤** 270 | 271 | * 求十进制0-N对应在数组a中的下标: 272 | * 十进制0-31,对应在a[0]中,先由十进制数n转换为与32的模(商)可转化为对应在数组a中的下标。比如n=24,那么 n/32=0,则24对应在数组a中的下标为0。又比如n=60,那么n/32=1,则60对应在数组a中的下标为1,同理可以计算0-N在数组a中的下标。 273 | 274 | * 求0-N对应每个下标0-31中的数: 275 | * 十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32的余数求得对应0-31中的数。 276 | 277 | * 利用移位0-31使得对应位为1. 278 | 279 | ### 应用 280 | * 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。 281 | * **“内存空间不足以容纳这2.5亿个整数”我们可以快速的联想到Bit-map** 282 | * 使用两个位图 283 | * 第一个Bitmap存储的是整数是否出现 284 | * 如果再次出现,则在第二个Bitmap中设置 285 | * 使用一个位图 286 | * 遍历一次这2.5亿个数字,如果对应的状态位为00,则将其变为01;如果对应的状态位为01,则将其变为11;如果为11,,对应的转态位保持不变。 287 | * 最后,我们将状态位为01的进行统计,就得到了不重复的数字个数,时间复杂度为O(n)。 288 | 289 | ### 扩展-布隆过滤器 290 | **基本思想** 291 | 292 | * 当一个元素被加入集合中时,通过k个散列函数将这个元素映射成一个位数组中的k个点,并将这k个点全部置为1. 293 | * Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注:如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。 294 | * 在判断y是否属于这个集合时,对y应用k次哈希函数,若所有hi(y)的位置都是1(1≤i≤k),就认为y是集合中的元素,否则就认为y不是集合中的元素。 295 | 296 | **缺点** 297 | 298 | 有一定的误判率,当判断某个元素在集合中,有可能是其他的元素的位;若集合中不存在该元素,则一定不存在 299 | 300 | ## 数组和链表的区别 301 | * 存储 302 | * 数组时连续的内存空间,链表不需要连续 303 | * 长度 304 | * 数组的长度需要预先确定,若超出数组则会溢出 305 | * 链表的长度是动态扩展的 306 | * 随机访问 307 | * 数组可以随机访问,时间复杂度为O(1) 308 | * 链表不支持随机访问,平均需要O(n) 309 | * 插入、删除 310 | * 数组其实位置的插入和删除, 时间复杂度为O(n) 311 | * 链表的时间复杂度为O(1) 312 | 313 | ### 什么是随机访问 314 | 比如first是第一个元素的地址,现在想访问第N个元素。 315 | 随机访问:直接first+N,便可以得到第N个元素的地址,因为这些相邻元素是按顺序连续存储的。比如普通数组就是可随机访问的。 316 | 317 | 318 | ## [赫夫曼树](https://www.cnblogs.com/kubixuesheng/p/4397798.html) 319 | ### 基本概念 320 | * 路径长度 321 | * 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度,为结点数-1 322 | * 树的路径长度是从树根到每一结点的路径长度之和 323 | * 带权路径长度 324 | * 若结点带权,则带权路径长度为**该结点到树根之间的路径长度**与**结点上权重**的乘积 325 | * 赫夫曼树 326 | * 带权路径长度WPL(weight path length)最小的二叉树为赫夫曼树 327 | 328 | ### 构建赫夫曼树(自下而上) 329 | * 根据给定的n个权值{w1,w2,…,wn}构成二叉树集合F={T1,T2,…,Tn},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树为空. 330 | * 在F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左右子树根结点的权值之和. 331 | * 在F中删除这两棵树,同时将新的二叉树加入F中. 332 | * 重复2、3,直到F只含有一棵树为止.(得到哈夫曼树) 333 | 334 | ### 构建示例 335 | 有4 个结点 a, b, c, d,权值分别为 7, 5, 2, 4,构造哈夫曼树。 336 | 337 | * 根据给定的n个权值{7,5,2,4}构成二叉树集合F={T1,T2,…,Tn},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树为空. 338 | * F中有四个二叉树{a(7),b(5),c(2),d(4)},每个二叉树左右子树为空,只有根节点 339 | * 在F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左右子树根结点的权值之和. 340 | * 先取最小的c和d,新结点为m,左右子树为c和d,m权值为6 341 | * 在F中删除这两棵树,同时将新的二叉树加入F中. 342 | * 在F中删除c和d,加入m,此时F中有三棵树{a(7),b(5),m(6)} 343 | * 重复2、3,直到F只含有一棵树为止 344 | 345 | ## 赫夫曼编码 346 | 用于解决电报数据传输,赫夫曼编码使得电报编码长度最短,且可以保证唯一性 347 | 348 | * 前缀编码 349 | * 设计长短不等的编码,必须是任一字符的编码都不是另一个字符编码的前缀,这种编码称为前缀编码 350 | * 赫夫曼编码 351 | * 需要变得字符集为{d1,d2,…,dn},各个字符在电文中出现的次数或频率为{w1,w2,…,wn},以d1,d2,…,dn为叶子结点,以w1,w2,…,wn为相应叶子结点的权值来构建一棵赫夫曼树 352 | * 规定赫夫曼树的左分支代表0,右分支代表1,从根结点到叶子结点所经过的路径分支所组成的0,1序列变为该结点对应的字符编码 353 | 354 | ### 译码 355 | * 从哈夫曼树根开始,对待译码电文逐位取码。 356 | * 若编码是“0”,则向左走;若编码是“1”,则向右走,一旦到达叶子结点,则译出一个字符; 357 | * 再重新从根出发,直到电文结束。 358 | 359 | -------------------------------------------------------------------------------- /项目基础知识/MySQL/README.md: -------------------------------------------------------------------------------- 1 | # MySQL 2 | 3 | ## 基本概念 4 | > * [主键、外键、唯一键、自增主键](#主键-外键-唯一键-自增主键) 5 | > * [数据库范式](#数据库范式) 6 | > * [内连接、左右外连接](#内连接左右外连接) 7 | > * [存储过程](#存储过程) 8 | > * [触发器](#触发器) 9 | > * [视图和游标](#视图和游标) 10 | > * [SQL手写和执行顺序](#SQL手写和执行顺序) 11 | > * [二进制文件binlog](#二进制文件binlog) 12 | > * [drop、truncate、delete区别](#drop-truncate-delete区别) 13 | > * [like %和-的区别](#like-%和-的区别) 14 | > * [count(\*)、count(1)、count(column)的区别](#count(\*)、count(1)、count(column)的区别) 15 | 16 | ## 索引及优化 17 | > * [索引](#索引) 18 | > * [聚集索引和非聚集索引](#聚集索引和非聚集索引) 19 | > * [优化](#优化) 20 | 21 | ## 事务 22 | > * [事务的四大特性ACID](#事务的四大特性ACID) 23 | > * [事务的并发](#事务的并发) 24 | > * [事务的隔离级别](#事务的隔离级别) 25 | 26 | ## 存储引擎 27 | > * [MyISAM和InnoDB区别](#MyISAM和InnoDB区别) 28 | > * [MySQL主从复制](#MySQL主从复制) 29 | 30 | ## 数据库锁 31 | > * [mysql中锁的级别](#mysql中锁的级别) 32 | > * [乐观锁和悲观锁](#乐观锁和悲观锁) 33 | > * [行锁怎么实现](#行锁怎么实现) 34 | > * [间隙锁](#间隙锁) 35 | 36 | ## 主键 外键 唯一键 自增主键 37 | * 主键 **PRIMARY KEY** 38 | * 数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(NULL)。 39 | * 外键 **FOREIGN KEY** 40 | * 在一个表中存在的另一个表的KEY(可以是另外一个表的主键或唯一键)称此表的外键。 41 | * 唯一键 **UNIQE** 42 | * 保证字段的唯一性,可以为空 43 | * 自增主键 **AUTO_INCREMENT** 44 | * 自增列每增加一行自动增量,每个表只能有一个自增列, 45 | * 一般将主键设置为自增列,又叫自增主键 46 | * 只能是数值型,delete删除自增列后,若再次插入,从断开处插入 47 | * truncate删除后,再次插入从1开始 48 | 49 | ## 数据库范式 50 | 范式之间的关系,第一范式包含第二范式,第二范式包含第三范式。这里只举反例,因为实际的数据在设计数据库表的时候,范式都是相对的。 51 | 52 | * **第一范式 1NF** 53 | * **确保每列保持原子性,所有字段值都是不可分解的原子值** 54 | * 比如,学生包括学号、姓名;地址包括省份,城市。若在使用中经常访问学号,城市等字段,则建表时,不能把学生、地址作为字段,若将其作为字段,则不满足第一范式。 55 | * 将学号、城市等作为字段,符合第一范式(相对来讲,因为城市还是可以可分的,只是平时访问的多,也不会访问城市以下的字段) 56 | * **第二范式 2NF** 57 | * **需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言);**或者说每一个非主属性都完全函数依赖与任何一个主键 58 | * 比如,学号,课程号,学院,成绩建一个数据库表。此时(学号,课程号)为主键,也称联合主键,此时只能当学号,课程号两者都确定的情况下,才能确定成绩。但学院只与学号有关,因此部分依赖与联合主键,这种情况不符合第二范式。 59 | * **第三范式 3NF** 60 | * **数据表中的每一列数据都和主键直接相关,而不能间接相关;**或每一个非主属性既不传递依赖主键,也不部分依赖主键。 61 | * 订单表【Order】(OrderID,OrderDate,CustomerID,CustomerName,CustomerAddr,CustomerCity)主键是(OrderID),CustomerName,CustomerAddr,CustomerCity 直接依赖的是 CustomerID(非主键列),而不是直接依赖于主键,它是通过传递才依赖于主键,所以不符合3NF。 62 | 63 | ## 内连接、左右外连接 64 | * 内连接(inner join): 只连接匹配的行 65 | * 左外连接(left join): 包含左边表的全部行(不管右边的表中是否存在与它们匹配的行),以及右边表中全部匹配的行 66 | * 右外连接(right join): 包含右边表的全部行(不管左边的表中是否存在与它们匹配的行),以及左边表中全部匹配的行 67 | 68 | ## 存储过程 69 | 存储过程是是事先经过编译,并存储在数据库中的一段SQL语句的集合。 70 | 通俗讲,存储过程是由一些SQL语句组成的代码块,这些SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查)。 71 | 72 | ### 优点 73 | * 存储过程只在创建时进行编译,以后每次执行存储过程都不需再重新编译,而一般 SQL语句每执行一次就编译一次,所以使用存储过程可提高数据库执行效率; 74 | * 当SQL语句有变动时,可以只修改数据库中的存储过程而不必修改代码; 75 | * 通过存储过程能够使**没有权限的用户在控制之下**间接地存取数据库,从而确保数据的安全。 76 | 77 | 78 | ## 触发器 79 | 触发器是一种特殊的存储过程,主要是通过事件来触发而被执行的。如,某表上的触发器上包含对另一个表的数据操作,售出一件商品,仓库库存-1 80 | 81 | ## 视图和游标 82 | * 视图 83 | * 一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是有一个表或者多个表的行或列的子集。对视图的修改会影响基本表。它使得我们获取数据更容易,相比多表查询。 84 | * 游标 85 | * 对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。 86 | 87 | ## SQL手写和执行顺序 88 | * 手写 89 | * select 90 | * from 91 | * join 92 | * on 93 | * where 94 | * group by 95 | * having 96 | * order by 97 | * limit 98 | 99 | * 执行顺序 100 | * from 101 | * on 102 | * join, where 103 | * group by 104 | * having 105 | * select 106 | * order by 107 | * limit 108 | 109 | 110 | ## 二进制文件binlog 111 | 用来记录对mysql数据更新或潜在发生更新的SQL语句,并以"事务"的形式保存在磁盘中 112 | ### 作用 113 | * 复制 114 | * 用于主从复制,读写分离,Master把它的二进制日志传递给slaves并回放(在slave上执行一遍)来达到master-slave数据一致的目的 115 | * 恢复 116 | * 让mysql将保存在binlog日志中指定段落区间的sql语句逐个重新执行一次 117 | 118 | ## drop truncate delete区别 119 | * drop 120 | * 直接删除表 121 | * truncate 122 | * 清空整个表数据,并不删除表 123 | * 若删除自增列,再次插入从1开始 124 | * delete 125 | * 可以通过where删除某一行的数据 126 | * 删除自增列,再次插入从断点开始 127 | 128 | ## like %和-的区别 129 | %不限制通配的个数,-仅仅是一个字符 130 | 131 | ## [count(\*)、count(1)、count(column)的区别](https://www.cnblogs.com/wenxiaofei/p/9853682.html) 132 | * count(\*)对行的数目进行计算,包含NULL 133 | * count(1)这个用法和count(\*)的结果是一样的,如果表没有主键,那么count(1)比count(\*)快 134 | * count(column)对特定的列的值具有的行数进行计算,不包含NULL值。 135 | * 任何时候count(\*)都是最优选择 136 | 137 | ## 索引 138 | ### 概念 139 | 索引是对数据进行排序并快速查找的数据结构,主要功能是排序和查找 140 | ### 数据结构 141 | B/B+树,hash索引,存储引擎MyISAM和InnoDB使用的B/B+树,MEMORY/Heap有hash和B/B+,默认是hash 142 | 143 | * B/B+树,hash索引的区别 144 | * hash适用于等值查找的情况,不能进行范围查找;hash索引在任何时候都不能避免表扫描 145 | * hash当有大量重复键值的情况,由于hash冲突,性能并不一定就会比B-Tree索引高 146 | 147 | * 为什么B+树比B树更适合文件系统索引 148 | * B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低 149 | * B+树的非叶子结点中只存放关键字的信息,没有存放关键字具体信息,因此结点更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多,相对来说IO读写次数也就降低了; 150 | * 由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引,所以,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当 151 | 152 | ### 索引分类 153 | 154 | * 单值索引 155 | * 一个索引值包含单个列 156 | * 复合索引 157 | * 索引包含多个列 158 | * 唯一索引 159 | * 索引列必须唯一 ,一般只是为了防止重复 160 | * 主键索引是唯一索引的特殊类型,主键自动建立索引,主键索引可以既防止重复,又提高访问速度 161 | * 非唯一索引 162 | * 普通索引,为了提高访问速度,索引中的值可重复 163 | 164 | ### 什么样的字段适合创建索引 165 | 166 | * 主键自动建立唯一索引 167 | * 经常作查询选择的字段 168 | * 经常作表连接的字段,比如外键 169 | * 经常出现在order by, group by, distinct 后面排序或分组的字段 170 | 171 | ### 哪些不适合建立索引 172 | * 数据记录太少的表 <300w 173 | * 经常增删改的表 174 | * 数据重复且分布平均的表字段,如性别和国籍 175 | 176 | ### 索引的缺点 177 | * 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度; 178 | * 空间方面:索引需要占物理空间。 179 | 180 | ### 最左前缀原则 181 | 182 | * 在创建联合索引的时候,需要做联合索引多个字段之间顺序你们是如何选择的 183 | * 把最频繁,识别度最高的放在最前面,因为最左前缀原则 184 | * 最左前缀原则:顾名思义,就是最左优先,上例中我们创建了lname_fname_age多列索引,相当于创建了(lname)单列索引,(lname,fname)组合索引以及(lname,fname,age)组合索引。 185 | 186 | 187 | ## 聚集索引和非聚集索引 188 | 聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致。聚集索引的逻辑顺序和物理顺序相同,非聚集索引则不同。 189 | 190 | ### B+树叶子结点可以存哪些数据 191 | * 聚集索引的叶节点就是数据节点,而非聚集索引的叶节点仍然是索引节点,只不过其包含一个指向对应数据块的指针 192 | * 叶子结点存储的是主键整行数据的是聚簇索引,叶子结点存储的仅仅是主键,然后通过指针指向具体信息的是非聚簇索引。 193 | 194 | 195 | ## 优化 196 | ### 索引优化 197 | * 全值匹配我最爱 198 | * 创建索引后,尽量在where筛选条件中使用索引列 199 | * 最左前缀要遵守 200 | * 建立索引,将最常用的放在最左侧 201 | * 带头大哥不能死 202 | * 查询从索引最左列开始 ,否则全部**失效** 203 | * 中间兄弟不能断 204 | * 不能跳过索引中的列,否则部分**失效** 205 | * 索引列上少计算 206 | * 不在索引列上做任何操作,比如计算或类型转换,有些VARCHAR不写引号,sql会默认转换,但索引**失效** 207 | * 范围之后全失效 208 | * 用<或>时,当前索引不失效,范围条件右边的列**失效** 209 | * LIKE百分写最右 210 | * LIKE百分号不要写在最左侧,否则索引**失效** 211 | * 覆盖索引不写\* 212 | * 当查询索引列时,select后面不写\*,而是写索引列 213 | * 不等空值还有or 214 | * 尽量不使用!=或<>符号,不使用is NULL或is not NULL,少用or,否则索引**失效** 215 | * VARCHAR引号不能丢 216 | * 字符串不用单引号会**失效**,相当于索引列做自动类型转换 217 | 218 | ### 查询优化 219 | * 慢查询步骤和过程 220 | * 开启慢查询 221 | * set slow_query_log = 1慢查询开启状态。 222 | * slow_query_log_file 慢查询日志存放的位置(这个目录需要MySQL的运行帐号的可写权限,一般设置为MySQL的数据存放目录)。 223 | * 设置超时阈值,比如5s或10s,超时的sql语句会放在慢日志中 224 | * long_query_time 查询超过多少秒才记录。 225 | * cat查看慢日志 226 | * 用explain分析慢sql语句 227 | * explain优化,实际上模拟优化器执行sql语句,查看mysql如何执行你的sql语句 228 | * table:显示这一行的数据是关于哪张表的 229 | * type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_ref、ref、range、index和ALL,一般来说到range不错,最好到ref 230 | * all:full table scan ;MySQL将遍历全表以找到匹配的行; 231 | * index: index scan; index 和 all的区别在于index类型只遍历索引; 232 | * range:索引范围扫描,对索引的扫描开始于某一点,返回匹配值的行,常见与between ,等查询; 233 | * ref:非唯一性索引扫描,返回匹配某个单独值的所有行,常见于使用非唯一索引即唯一索引的非唯一前缀进行查找; 234 | * eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常用于主键或者唯一索引扫描; 235 | * const,system:当MySQL对某查询某部分进行优化,并转为一个常量时,使用这些访问类型。如果将主键置于where列表中,MySQL就能将该查询转化为一个常量。 236 | * possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句 237 | * key: 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MySQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MySQL忽略索引 238 | * key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好 239 | * rows:MySQL认为必须检查的用来返回请求数据的行数 240 | * Extra:关于MySQL如何解析查询的额外信息 241 | * Using temporary和Using filesort,意思MySQL根本不能使用索引,结果是检索会很慢。 242 | * Using index表示不错的信息,使用了覆盖索引 243 | * 根据explain的结果,进行索引优化 244 | 245 | 246 | ## 事务的四大特性ACID 247 | ### 事务 248 | 事务是用户自定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。在关系数据库中,事务可以是一条SQL语句,也可以是一组SQL语句。 249 | ### 四大特性ACID 250 | 事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。 251 | 252 | * 原子性 253 | * 事务所包含的一系列数据库操作要么全部成功执行,要么全部回滚 254 | * 事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响 255 | * 一致性 256 | * 事务必须使数据库从一个一致性状态变换到另一个一致性状态 257 | * 一个事务执行之前和执行之后都必须处于一致性状态。 258 | * 隔离性 259 | * 并发执行的事务之间不能相互影响 260 | * 当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰 261 | * 持久性 262 | * 指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的 263 | * 即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 264 | 265 | ## 事务的并发 266 | 从理论上来说, 事务应该彼此完全隔离, 以避免并发事务所导致的问题,然而, 那样会对性能产生极大的影响, 因为事务必须按顺序运行, 在实际开发中, 为了提升性能, 事务会以较低的隔离级别运行,这样会带来一些问题。 267 | 268 | * 脏读 269 | * 一个事务读取了另一个事务未提交的数据; 270 | * 事务A读取事务B更新的数据,然后事务B回滚,此时事务A读到的是脏数据 271 | * 不可重复读 272 | * 不可重复读的**重点是修改**,同样条件下两次读取结果不同,也就是说,被读取的数据可以被其它事务修改; 273 | * 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果因此本事务先后两次读到的数据结果会不一致 274 | * 幻读 275 | * 幻读的**重点在于新增或者删除**,同样条件下两次读出来的记录数不一样。 276 | * 事务A统计表中的数据,此时事务B想表中添加或删除了数据,当事务A再次统计表中的数据时,发现两次的记录不一样。 277 | * 不可重复读和幻读的区别 278 | * 不可重复读侧重于修改,幻读侧重于新增或删除。 279 | * 解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。 280 | 281 | ## 事务的隔离级别 282 | * 读未提交。最低的隔离级别,会产生脏读,不可重复读,幻读问题 283 | * 读提交。会产生不可重复读,幻读问题 284 | * 可重复读。会产生幻读问题 285 | * 串行化。最高的隔离级别,在这个隔离级别下,不会产生任何异常。并发的事务,就像事务是在一个个按照顺序执行一样 286 | 287 | ### Mysql中默认的事务隔离级别 288 | 可重复读的事务本身是会发生幻读,但MySQL中默认事务隔离级别是“可重复读”,不会发生脏读,可重复读和幻读的情况,因为可以通过Next-Key进行控制,Next-Key锁是行锁和GAP(间隙锁)的合并 289 | 290 | * 事务隔离级别:未提交读时,写数据只会锁住相应的行。 291 | * 事务隔离级别为:可重复读时,写数据会锁住整张表。 292 | * 事务隔离级别为:串行化时,读写数据都会锁住整张表。 293 | 294 | 295 | ## MyISAM和InnoDB区别 296 | 在MySQL 5.5之前,MyISAM是mysql的默认数据库引擎,之后的版本是InnoDB。 297 | ### InnoDB 298 | * 支持事务 299 | * 有行级锁定和外键约束 300 | * 不支持FULLTEXT类型的索引 301 | * 没有保存表的行数 302 | * 叶子结点是聚集索引 303 | 304 | ### MyISAM 305 | * 不支持事务 306 | * 不支持行锁和外键,因此当INSERT或UPDATE数据时即写操作需要锁定整个表,效率便会低一些 307 | * 叶子结点是非聚集索引 308 | 309 | ### 区别 310 | * 事务 311 | * InnoDB支持,MyISAM不支持 312 | * 行数 313 | * InnoDB没有保存表的行数,MyISAM保存了表的行数,可以直接读取 314 | * 索引存储 315 | * InnoDB是聚集索引,MyISAM是非聚集索引 316 | * 外键 317 | * InnoDB支持,MyISAM不支持 318 | * 锁 319 | * InnoDB支持行锁,表锁。行锁可以提高多用户并发操作,但InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的 320 | * MyISAM支持表锁 321 | 322 | ## [MySQL主从复制](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mysql-read-write-separation.md) 323 | ### 原理 324 | * 主从复制需要三个线程来完成 325 | * 主库中有一个binary-log dump线程,当从库连接主库时,主库会创建该线程,用于给从库发送二进制文件信息,其中读取二进制文件信息的时候会加锁 326 | * 从库中有两个线程,一个是从节点I/O线程,一个是SQL线程。其中从节点I/O线程接收主节点线程发来的二进制文件信息,存入到中继日志文件(relay-log),SQL线程负责执行中继日志文件中的SQL操作 327 | * Master将数据改变记录到二进制日志(binary log)中 328 | * Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容 329 | * Master接收到来自Slave的IO进程的请求 330 | * 负责复制的IO进程会根据请求信息读取日志指定位置之后的日志信息,返回给Slave的IO进程。 331 | * 返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置 332 | * Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master从某个bin-log的哪个位置开始往后的日志内容 333 | * Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行,从而实现主从数据的一致 334 | 335 | ### 读写分离 336 | 基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。 337 | 338 | ### 三种复制方式 339 | * 同步 340 | * master的变化,必须等待slave-1,slave-2,...,slave-n完成后才能返回。 这样,显然不可取,也不是MySQL复制的默认设置。比如,在WEB前端页面上,用户增加了条记录,需要等待很长时间。 341 | * 异步 342 | * master只需要完成自己的数据库操作即可。至于slaves是否收到二进制日志,是否完成操作,不用关心,MySQL的默认设置,**这样可能会丢失数据** 343 | * 半同步 344 | * master一般至少有两个slave,主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。 345 | 346 | * 注意 347 | * 从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。 348 | * 由于从库从主库拷贝日志以及串行执行SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的 349 | * 经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 350 | * 解决思路 351 | * 从库中开启多线程并行操作 352 | 353 | ## mysql中的锁 354 | ### 读写锁 355 | * 读锁会阻塞写,但不会阻塞读 356 | * 写锁会阻塞读和写 357 | 358 | ### 行页表锁 359 | * 对表中的记录加锁,叫做记录锁,又称行锁,行锁只锁定一行,偏写 360 | * 表锁会锁定整个表,偏读 361 | * 页锁在行锁和表锁之间 362 | 363 | ### InnoDB加锁 364 | * 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及及数据集加排他锁 365 | * 对于普通SELECT语句,InnoDB不会加任何锁 366 | * 事务可以通过以下语句显示给记录集加共享锁或排锁。 367 | 368 | ```C++ 369 | //共享锁(S) 370 | SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE 371 | //排他锁(X) 372 | SELECT * FROM table_name WHERE ... FOR UPDATE 373 | ``` 374 | 375 | ## 乐观锁和悲观锁 376 | ### 悲观锁 377 | 先获取锁,再进行业务操作,悲观的认为所有的操作均会导致并发安全问题,因此要先确保获取锁成功再进行业务操作。`select ... for update` 实现悲观锁 378 | 379 | ### 乐观锁 380 | 先进行业务操作,再获取锁,一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,提交更新时,时间戳或版本号必须大于当前版本才提交更新 381 | 382 | ```C++ 383 | SELECT data AS old_data, version AS old_version FROM …; 384 | //根据获取的数据进行业务操作,得到new_data和new_version 385 | UPDATE SET data = new_data, version = new_version WHERE version = old_version 386 | if (updated row > 0) { 387 | // 乐观锁获取成功,操作完成 388 | } else { 389 | // 乐观锁获取失败,回滚并重试 390 | } 391 | ``` 392 | 393 | ### 使用场景 394 | 一般情况下,**读多写少更适合用乐观锁,读少写多更适合用悲观锁**。乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。 395 | 396 | 397 | ## 行锁怎么实现 398 | * 显示系统上行锁的争夺情况`show status like 'innodb_row_lock%';` 399 | * 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁 400 | 401 | ```C++ 402 | //显式锁定 403 | set auoto_commit = 0; 404 | begin; 405 | //加索引 406 | ALTER TABLE account ADD INDEX index_user_id (user_id); 407 | //检索索引 408 | SELECT * FROM account WHERE user_id = 1 FOR UPDATE 409 | commit; 410 | ``` 411 | 412 | ## 间隙锁 413 | ### 概念 414 | * 当我们使用范围条件而不是相等条件查询数据时,InnoDB会把符合条件的数据记录索引项都加锁,此时键值在条件范围内,但并不存在的数据记录称为间隙。 415 | * 比如检索1-3之间的数据,但表中并没有数据2,此时InnoDB会对这个间隙也加锁 416 | * 若范围检索不提交,另一个对间隙进行操作(比如添加2)会被阻塞,无法插入数据,导致性能变差 417 | * 在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待 418 | 419 | ### 作用 420 | * 间隙锁的主要作用是为了防止出现幻读 421 | 422 | ### 解除间隙锁 423 | * 修改系统参数innodb_locks_unsafe_for_binlog=on就可以关闭改间隙锁机制,该值默认为off 424 | * 修改事务隔离级别为read-committed也可以避免间隙锁 425 | 426 | -------------------------------------------------------------------------------- /项目基础知识/Linux系统编程及基本命令/ReadMe.md: -------------------------------------------------------------------------------- 1 | 2 | # linux系统编程及基本命令 3 | 4 | ## 系统编程 5 | > * [按下开机键Linux发生了什么](#按下开机键Linux发生了什么) 6 | > * [进程退出方式及区别](#进程退出方式及区别) 7 | > * [回收进程资源的方式和区别](#回收进程资源的方式和区别) 8 | > * [守护进程](#守护进程) 9 | > * [线程退出方式与线程回收](#线程退出方式与线程回收) 10 | > * [共享内存](#共享内存) 11 | > * [信号量](#进程中的信号量) 12 | > * [信号通知进程](#信号通知进程) 13 | > * [valgrind检查内存泄漏](#valgrind检查内存泄漏) 14 | > * [程序从main函数开始吗](#程序从main函数开始吗) 15 | 16 | ## 基本命令 17 | > * [Linux基本目录结构](#Linux基本目录结构) 18 | > * [文件操作命令](#文件操作命令) 19 | > * [磁盘及内存命令](#磁盘及内存命令) 20 | > * [进程命令](#进程命令) 21 | > * [网络命令](#网络命令) 22 | > * [字符处理命令](#字符处理命令) 23 | > * [调试命令](#调试命令) 24 | > * [文本处理工具](#文本处理工具) 25 | 26 | ## [按下开机键Linux发生了什么](https://blog.csdn.net/T146lLa128XX0x/article/details/93988210) 27 | BIOS -> MBR -> 引导加载程序 -> 内核 -> init process -> login 28 | 29 | ## [进程退出方式及区别](https://www.cnblogs.com/xiaojianliu/p/8473083.html) 30 | 不管哪种退出方式,系统最终都会执行内核的同一代码,这段代码用来关闭进程打开的文件描述符,释放它占用的内存和其他资源 31 | 32 | * 退出 33 | * 正常退出 34 | * main函数调用return 35 | * 调用exit()函数 36 | * 调用_exit()函数 37 | * 异常退出 38 | * 调用abort函数 39 | * 进程收到某个信号,该信号使程序终止 40 | 41 | * 已结束进程的状态 42 | * shell执行 echo $?,保存最近一次运行的进程的返回值 43 | * 程序中main函数运行结束,保存main函数的返回值 44 | * 程序调用exit函数结束运行,保存exit函数的参数 45 | * 程序异常退出,保存异常出错的错误号 46 | 47 | * 区别 48 | * exit和return的区别 49 | * exit是函数,有参数,exit执行完会把控制权交给系统,exit(0)表示正常终止,其他值表示有错误发生 50 | * return是函数执行完后的返回,return执行完后把控制权交给调用函数 51 | * exit和abort的区别 52 | * exit是正常终止进程 53 | * abort是异常终止进程 54 | * exit和_exit函数的区别 55 | * exit在头文件stdlib.h中声明,_exit是在头文件unistd.h中声明 56 | * exit是_exit之上的一个封装, **exit先刷新流数据** ,再调用_exit函数 57 | * _exit会关闭进程打开的文件描述符,清理内存,不会刷新流数据 58 | * linux的库函数,有一种“缓冲IO”的操作,对应每一个打开的文件,在内存中有一片缓冲区,每次读文件,会连续读出若干条记录,下次再读文件的时候,直接从内存的缓冲区中读;同样写文件也是先写入缓冲区,满足一定条件才将缓冲区的内容一次性写入文件。具体可以看printf和write的区别,及行缓冲和全缓冲 59 | * exit先刷新流数据,将文件缓冲区的内容写回文件,可以保证数据的完整性,_exit会将数据直接丢失 60 | 61 | ## 回收进程资源的方式和区别 62 | * init进程(进程号为1)会周期性的调用wait系统调用来清除各个僵尸进程 63 | * wait 64 | * `pid_t wait (int *status)` status表示子进程的退出状态,成功返回值为子进程进程号,失败为-1 65 | * 进程一旦调用wait函数,立即阻塞自己, 判断当前进程的某个子进程是否变成僵尸进程 66 | * 若存在则收集子进程的信息,将它彻底销毁然后返回 67 | * 若没有,则会一直阻塞,直到出现一个 68 | * waitpid 69 | * waitpid相当于wait函数的封装,多了两个由用户控制的参数pid和options,可以自定义回收的子进程进程号,并设置是否阻塞 70 | * `pid_t waitpid(pid_t pid, int * status, int options)` 71 | * pid 72 | * pid < -1,等待进程组ID为pid绝对值的任何子进程 73 | * pid = -1,等待任何子进程,相当于wait 74 | * pid = 0,等待进程组ID与目前进程相同的任何子进程 75 | * pid > 0,等待子进程ID为pid的进程 76 | * options 77 | * 0,与wait相同,也会阻塞 78 | * WNOHANG,不会阻塞,如果当前没有可回收的子进程,立即返回0 79 | 80 | ## 守护进程 81 | Daemon进程,守护进程,是脱离终端并在后台运行的进程,脱离终端是为了避免进程运行过程中的信息显示在任何终端上,另外进程越不会被任何终端产生的终端信息所打断。 82 | 83 | ### 终端,进程组,会话期,setsid函数 84 | * 终端 85 | * linux中,每一个系统与用户进行交流的界面称为终端。 86 | * 每一个从此终端开始运行的进程都依附于此终端,这个终端称为这些进程的控制终端,当控制终端被关闭时,相应的进程会自动关闭。??????????????????????????????????? 87 | * 守护进程可以突破这种限制,从运行开始执行,整个系统关闭才退出 88 | 89 | * 进程组 90 | * 进程组是一个或多个进程的集合 91 | * 进程组由进程组ID唯一标识,该进程组中的进程除了进程号之外,还有进程组ID的属性 92 | * 进程组中第一个进程默认为进程组长,其进程号 = 进程组ID 93 | 94 | * 会话期 95 | * 会话期是一个或多个进程组的集合 96 | * 进程组的组长不能创建会话,只有组员才能创建会话,因此都是子进程创建会话实现守护进程 97 | * 一个会话期开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期 98 | 99 | * setsid函数 100 | * 用于创建新会话,将调用setsid的进程担任会话期的会长 101 | * 让进程摆脱原会话的控制 102 | * 让进程摆脱原进程组的控制 103 | * 让进程摆脱原控制终端的控制 104 | 105 | * 为什么创建守护进程要调用setsid? 106 | * 一般子进程实现守护进程,子进程fork于父进程,子进程全盘拷贝了父进程的会话期、进程组、控制终端 107 | * 虽然父进程退出了,但子进程的各个属性都没变,不算真正意义的独立 108 | 109 | ### 创建守护进程 110 | * 创建子进程,父进程退出 111 | * 子进程中调用setsid创建新会话 112 | * 切换工作目录,一般切换为根目录/ 113 | * 从父进程继承来的工作目录可能是一个挂载的文件系统,若不修改工作目录,该文件系统不能卸载 114 | * 重设文件权限掩码 115 | * 由于从父进程继承来的文件权限掩码会屏蔽掉文件权限中的对应位,这给该子进程使用文件带来麻烦 116 | * 通过umask(0),表示用户、用户组和其他用户都有可读可写可执行权限 117 | * 关闭文件描述符 118 | * 子进程会从父进程继承一些打开的文件描述符,但这些可能永远不会被守护进程使用,但他们一样消耗系统资源,而且会导致所在文件系统无法结束 119 | * 守护进程已经脱离了所属的控制终端,因此终端的输入,输出和报错也失去了存在的价值 120 | * 可以遍历MAXFILE,然后close所有文件描述符 121 | * 如果想结束守护进程,直接kill -9 pid即可 122 | 123 | ## 线程退出方式与线程回收 124 | ### 线程退出方式 125 | 注意:不能使用exit,exit表示退出整个进程 126 | 127 | * pthread_exit 128 | * `int pthread_exit(void *retval);` 129 | * 在任何线程中使用,使该线程直接退出 130 | * 主线程退出而不影响其他线程,只能使用这种方式 131 | * return 132 | * 子线程中可以使用,主线程不能使用,主线程代表退出整个进程 133 | 134 | ### 线程回收 135 | * pthread_join 136 | * `int pthread_join(pthread_t thread, void **retval) ` 137 | * 用来等待一个线程的结束,并回收该线程的资源 138 | * 一般是主线程调用,用来等待子线程退出,是阻塞的 139 | 140 | ### 线程分离 141 | * pthread_detach 142 | * `int pthread_detach(pthread_t thread)` 143 | * 分离已经创建的线程,将主线程与子线程分离,子线程结束后,资源自动回收。 144 | * 状态分离后,该线程的结束状态不能被该进程中的其他线程得到,因此pthread_join不能调用,否则会出错 145 | 146 | ## [共享内存](https://blog.csdn.net/hj605635529/article/details/73163513) 147 | 共享内存有两种shm和mmap 148 | 149 | * IPC通信System V版本的共享内存shm 150 | * 存储映射I/O(mmap函数) 151 | 152 | ### shm 153 | * 原理 154 | * 多个进程的地址空间映射到同一个物理内存,不同进程可以将同一段共享的内存连接到自己的地址空间中,从而所有进程都可以访问共享内存中的地址 155 | * API 156 | * `int shmget(key_t key, size_t size, int shmflg);` 157 | * 在物理内存创建一个共享内存,返回共享内存的编号 158 | * key是一个非0整数,命名共享内存段,运行成功返回一个与key相关的共享内存标识符 159 | * size表示以字节为单位指定需要的共享内存的容量 160 | * shmflag是权限标志位,与open的mode参数一致,若key标识的共享内存不存在,通过0666|IPC_CREAT来创建,并设置权限 161 | 162 | * `void *shmat(int shmid, const void shmaddr,int shmflg);` 163 | * 连接成功后把共享内存区对象映射到调用进程的地址空间,函数返回各个进程挂接的虚拟的地址空间 164 | * shmid是挂接的进程号, 165 | * shmaddr置为NULL,让系统选择一个合适的地址空间进行挂接 166 | * shmflg表示什么方式进行挂接,一般都是取0. 167 | 168 | * `void *shmdt(const void* shmaddr);` 169 | * 将共享内存从当前进程中分离,断开用户级页表到共享内存的那根箭头。 170 | 171 | * `int shmctl(int shmid, int cmd, struct shmid_ds* buf);` 172 | * 释放物理内存中的那块共享内存 173 | * cmd取IPC_RMID表示删除这块共享内存 174 | 175 | ### mmap 176 | * 原理 177 | * mmap是映射磁盘上的一个文件,每个进程在自己的逻辑地址空间中开辟一块空间对磁盘上的文件进行映射 178 | * 内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存 179 | * mmap返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,建立内存映射并没有实际拷贝数据,这时,将产生一个缺页中断,会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中 180 | 181 | * 效率 182 | * read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,在这个过程中,实际上完成了两次数据拷贝 183 | * mmap()也是系统调用,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝 184 | 185 | * 映射文件 186 | * 普通文件 187 | * open系统调用打开一个文件,然后进行mmap操作,得到共享内存,这种方式适用于任何进程之间。 188 | * 匿名映射 189 | * 调用 mmap 时,在参数 flags 中指定 MAP_ANONYMOUS 标志位,并且将参数 fd 指定为 -1 ,用于父子进程之间 190 | 191 | ### 不同进程访问共享内存 192 | * shm 193 | * 不同进程通过shmget->shmat函数,将共享内存连接到自己的虚拟内存地址 194 | * mmap 195 | * 不同进程通过mmap函数创建映射区,将自己的内存虚拟地址映射到磁盘的文件上 196 | 197 | ### 程序异常退出,共享内存会释放吗? 198 | * 不会 199 | * Linux中通过API函数shmget创建的共享内存一般都是在程序中使用shmctl来释放的,但是有时为了调试程序,开发人员可能通过Ctrl + C等方式发送中断信号来结束程序,此时程序申请的共享内存就不能得到释放,当然如果程序没有改动的话,重新运行程序时仍然会使用上次申请的共享内存,但是如果我们修改了程序,由于共享内存的大小不一致等原因会导致程序申请共享内存错误。 200 | * 如何释放 201 | * 如果总是通过Crtl+C来结束的话,可以做一个信号处理器,当接收到这个信号的时候,先释放共享内存,然后退出程序。 202 | * 不管你以什么方式结束程序,如果共享内存还是得不到释放,那么可以通过linux命令ipcrm shm shmid来释放,在使用该命令之前可以通过ipcs -m命令来查看共享内存。 203 | 204 | ### 两者的区别 205 | * 作用 206 | * mmap系统调用并不完全是为了共享内存来设计的,它本身提供了不同于一般对普通文件的访问的方式,进程可以像读写内存一样对普通文件进行操作 207 | * IPC的共享内存shm是纯粹为了共享。 208 | * 映射位置 209 | * mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间对磁盘上的文件进行映射。 210 | * shm每个进程映射到同一块物理内存,shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大 211 | * 内容丢失 212 | * 进程挂了重启不丢失内容,二者都可以做到 213 | * 机器挂了重启,mmap把文件存在磁盘上,可以不丢失内容(文件内保存了OS同步过的映像),而 shmget 会丢失 214 | 215 | ## [信号量](https://blog.csdn.net/qq_38813056/article/details/85706006) 216 | [参考资料](https://blog.csdn.net/mijichui2153/article/details/84930553) 217 | 信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问,分为两种POSIX信号量和SystemV信号量 218 | 219 | ### POSIX信号量 220 | * 有名信号量,用于进程间同步 221 | * 无名信号量,用于线程间同步 222 | * posix信号量一般是单个计量信号,全程操作一个信号量 223 | 224 | ### SystemV信号量,用于进程间同步 225 | * 进程中使用共享内存实现进程间通信,但他并不是线程安全的,需要通过信号量进行同步 226 | * 一般说的systemV信号量是计量信号集,可以使用多个信号量进行同步 227 | 228 | * API 229 | * `int semget(key_t key, int nsems, int semflag);` 230 | 231 | ## [信号通知进程](http://www.360doc.com/content/16/0804/10/30953065_580685165.shtml) 232 | ### 信号的使用 233 | 用kill函数发送信号,在接收进程里,通过signal或者signalaction函数调用sighandler,来启动对应的函数处理信号消息。 234 | 235 | * 发送信号 236 | * raise,向本身发送信号 237 | * kill,向指定进程发送信号 238 | * pid > 0 :向进程号为pid的进程发送信号 239 | * pid = 0 :向当前进程所在的进程组发送信号 240 | * pid = -1 :向所有进程(除PID=1外)发送信号(权限范围内) 241 | * pid < -1 :向进程组号为-pid的所有进程发送信号 242 | * 自定义信号动作/注册捕捉函数 243 | * signal 244 | * `typedef void (*sighandler_t)(int);` 245 | * `sighandler_t signal(int signum, sighandler_t handler);` 246 | * signal里面需要设置捕捉的信号signum、自定义回调函数handler 247 | * sigaction 248 | * 同样需要设置捕捉信号和回调处理函数 249 | 250 | ### 信号处理机制 251 | 每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来 252 | 253 | * 信号的接收 254 | * 接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。 255 | 256 | * 信号的检测 257 | * 进程陷入内核态后,有两种场景会对信号进行检测: 258 | * 进程从内核态返回到用户态前进行信号检测 259 | * 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测 260 | * 当发现有新信号时,便会进入下一步,信号的处理。 261 | 262 | * 信号的处理 263 | * ( **内核** )信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。 264 | * ( **用户** )接下来进程返回到用户态中,执行相应的信号处理函数。 265 | * ( **内核** )信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。 266 | * ( **用户** )如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。 267 | 268 | 至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。 269 | 270 | ### 信号通知进程,为什么通过内核转发? 271 | * 之所以要通过内核来转发,这样做的目的应该也是为了对进程的管理和安全因素考虑。 272 | * 因为在这些信号当中,SIGSTOP和SIGKILL这两个信号是可以将接收此信号的进程停掉的,而这类信号,肯定是需要有权限才可以发出的,不能够随便哪个程序都可以随便停掉别的进程。 273 | 274 | ### 信号处理示例 275 | * A,B两个进程,A进程发送信号给B进程,信号并不是直接从进程A发送给进程B,而是要通过内核来进行转发。 276 | * A进程发送的信号消息,由内核对B进程相应的表项进行设置。 277 | * 内核接受到这个信号消息后,会先检查A进程是否有权限对B进程的信号表对应的项进行设置 278 | * 如果可以,就会对B进程的信号表进行设置 279 | * 如果不可以,就忽略 280 | * 信号处理有个特点,就是没有排队的机制,也就是说某个信号被设置之后,如果B进程还没有来及进行响应,那么如果后续第二个同样的信号消息过来,就会被阻塞掉,也就是丢弃。 281 | * 内核对B进程信号设置完成后,就会发送中断请求给B进程,这样B进程就进入到内核态 282 | * 进程B根据那个信号表,查找对应的此信号的处理函数,保护现场,跳回到用户态执行信号处理函数,处理完成后,再次返回到内核态,再次保护现场,然后再次返回用户态,从中断位置开始继续执行。 283 | * 保护现场是在用户态和内核态之间跳转的时候,对堆栈现场的压栈保存。 284 | 285 | ## valgrind检查内存泄漏 286 | * 检查内存泄露原理 287 | * 检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。 288 | * 截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存 289 | * 最常用的是memcheck,用于发现绝大多数的内存错误使用情况 290 | * 使用未初始化的内存 291 | * 使用已经释放的内存 292 | * 内存访问越界 293 | * memcheck原理 294 | * Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。 295 | * Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。 296 | * Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。 297 | * 检测原理: 298 | * 当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,**memcheck则报告读写错误**。 299 | * 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会**报告使用未初始化内存错误**。 300 | 301 | 302 | 303 | ## 程序从main函数开始吗 304 | * 程序在main函数开始之前,已经完成了全局变量的初始化,堆栈初始化和系统I/O 305 | * main之前完成全局变量的构造,在main之后完成全局变量的析构 306 | * 另外atexit函数还可以在main函数之后运行,它接受一个函数指针作为参数 307 | 308 | ## Linux基本目录结构 309 | * /bin,binaries存放二进制可执行文件 310 | * /usr,unix shared resources用于存放共享的系统资源 311 | * /sbin,super user binaries存放二进制可执行文件,只有root才能访问 312 | * /etc,etcetera存放系统的配置文件 313 | * /boot,存放启动linux和引导文件的目录 314 | * /lib,存放着系统最基本的动态连接共享库 315 | * /dev,存放linux的设备文件,比如显示器,键盘等 316 | * /mnt,用户可以在这个目录下挂在其他临时文件系统 317 | * /media,linux系统会自动识别一些设备,例如U盘、光驱等等,linux会把识别的设备挂载到这个目录下 318 | * [/proc](https://www.cnblogs.com/zydev/p/8728992.html),proc被称为虚拟文件系统,它是一个控制中心,可以通过更改其中某些文件改变内核运行状态,它也是内核提空给我们的查询中心,用户可以通过它查看系统硬件及当前运行的进程信息。 319 | * /proc/loadavg,前三列分别保存最近1分钟,5分钟,及15分钟的平均负载。 320 | * /proc/meminfo,当前内存使用信息 321 | * /proc/cpuinfo , CPU的详细信息 322 | * /proc/diskstats, 磁盘I/O统计信息列表 323 | * /proc/net/dev , 网络流入流出统计信息 324 | * /proc/filesystems, 支持的文件系统 325 | * /proc/cmdline , 启动时传递至内核的启动参数,通常由grub进行传递 326 | * /proc/mounts , 系统当前挂在的文件系统 327 | * /proc/uptime , 系统运行时间 328 | * /poc/version , 当前运行的内核版本号等信息 329 | * /opt,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。 330 | 331 | ## 文件操作命令 332 | * ls 333 | * `ls -lrt` 递归显示文件的详细信息并按照时间排序 334 | * tail 335 | * `tail -n 100` 显示文件尾,指定显示行数,默认10行 336 | * chmod 337 | * `chmod 777 filename` 修改文件的权限为用户,用户组,其他人有所有权限 338 | * rm 339 | * `rm -r` 递归删除子目录 340 | * `rm -f` 强制删除 341 | * vim的三种模式 342 | * 命令模式(一般模式,通过yy进行赋值) 343 | * 编辑模式,通过i或者a 344 | * 末行模式,冒号 345 | 346 | 347 | ## 磁盘及内存命令 348 | * 文件大小和占用空间大小是不一样的,因为要对齐 349 | 350 | * 显示每个文件和目录的磁盘使用空间 351 | * (disk used) du -h 352 | * 显示磁盘分区上可以使用的磁盘空间 353 | * (disk free) df -h 354 | * 显示内存使用情况 355 | * [free](https://www.cnblogs.com/ultranms/p/9254160.html) 356 | * Mem是物理内存的使用情况 357 | * Swap是交换空间的使用情况 358 | * total是物理内存和交换空间的总大小 359 | * used是物理内存和交换空间已经被使用的大小 360 | * free是物理内存和交换空间可用空间(从内核和系统的角度看,真正尚未被使用的物理内存数量) 361 | * shared 列显示被共享使用的物理内存大小。 362 | * buff/cache 列显示被 buffer 和 cache 使用的物理内存大小(其实是内存为缓存磁盘数据设置的缓冲区)。 363 | * available 列显示还可以被应用程序使用的物理内存大小,当应用程序需要内存时,如果没有足够的 free 内存可以用,内核就会从 buffer 和 cache 中回收内存来满足应用程序的请求,理想来说available = free + buffer + cache 364 | 365 | ## 进程命令 366 | * ps 367 | * 当前运行的进程的快照,指定ps命令的那个时刻的那些进程 368 | * `ps -aux` 查看所有的在内存中的进程信息 369 | * `ps -ajx` 查看进程组相关信息,可以追踪进程之间的血缘关系 370 | * `ps -ef` 线城市所有进程信息,并显示程序间的关系 371 | * `ps -u username` 显示指定用户username信息 372 | * top 373 | * 实时显示系统中各个进程的资源占用情况,按"q"退出top命令 374 | * `top -H -p pid`显示对应pid的所有线程资源使用情况 375 | * `load average` 表示系统最近1min,5min,15min的平均负载,越大表示负载越来越小 376 | * %MEM物理内存占用比 377 | * Cpu(s)和%cpu 378 | * Cpu(s)表示的是所有用户进程占用整个cpu的平均值 379 | * %CPU显示的是进程占用一个核的百分比,而不是整个cpu(8核)的百分比,有时候可能大于100,那是因为该进程启用了多线程占用了多个核心,所以有时候我们看该值得时候会超过100%,但不会超过总核数*100。 380 | * kill 381 | * 杀掉进程 382 | * `kill -9 pid`杀掉指定进程 383 | * lsof 384 | * 列出当前系统打开文件的工具 385 | * `lsof -i :8600` 查看8600端口的运行情况 386 | * `lsof -u username` 查看username打开的文件 387 | * `lsof -c string` 查看包含指定字符的进程所打开的文件 388 | 389 | ## 网络命令 390 | * netstat 391 | * 用于显示与IP、TCP、UDP、和ICMP协议相关的统计数据,用于检验本机各端口的网络连接情况 392 | * `-a` 列出所有端口 393 | * `-p` 显示出进程和PID 394 | * `-n` 将主机、端口和用户名用数字代替 395 | * `netstat -apn | grep port` 显示指定端口的状态信息和进程信息 396 | * tcpdump 397 | * `tcpdump host ip` 截获主机发出和收到的数据包 398 | * `tcpdump port 6666` 截获端口上通过的包 399 | * `tcpdump -i eth0` 截获某网卡上的包 400 | * ping 401 | * `ping ip` 用于测试另一台主机是否可达,测试网络是否连通以及时延 402 | * windows下ping是32比特,默认发送4次数据包结束 403 | * linux下ping是64比特,默认不停发送数据包,直到手动停止 404 | * host 405 | * `host 域名` 返回域名的IP地址 406 | * 用来查询DNS记录 407 | * ifconfig 408 | * 输出当前系统中所有处于活动状态的网络接口 409 | * `ifconfig eth0 ip/24` 手工指定网卡的IP地址和广播地址,其中广播地址可以根据掩码计算出来 410 | * `ifconfig eth0 up` 启动网卡eht0 411 | * `ifconfig eth0 down` 关闭网卡eht0 412 | * traceroute 413 | 414 | ## 字符处理命令 415 | * 管道| 416 | * grep 417 | 418 | ## 调试命令 419 | * gdb 420 | * `l` 列出函数代码及行数 421 | * `b 16` 在16行设置断点 422 | * `b func` 在函数func设置断点 423 | * `r` 运行程序 424 | * `n` 单条执行程序 425 | * `p i` 打印i变量的值 426 | * `bt` 查看函数堆栈 427 | * `finish` 退出函数 428 | * `q` 结束调试 429 | * strace 430 | * 监控用户空间进程和内核的交互,跟踪系统调用和信号传递 431 | * `strace -c ./test` 统计./test使用的系统调用 432 | * `strace -p pid` 跟踪现有进程 433 | * ipcs 434 | * 用于报告系统的消息队列,信号量和共享内存等使用情况 435 | * `ipcs -a`用于列出本用户所有相关的ipcs参数 436 | * `ipcs -q`用于列出进程中的消息队列 437 | * `ipcs -s`用于列出所有的信号量 438 | * `ipcs -m`用于列出所有的共享内存信息 439 | * `ipcs -l`用于列出系统限额,比如共享内存最大限制 440 | * `ipcs -u`用于列出当前的使用情况 441 | * ipcrm 442 | * 用于移除一个消息队列,或者共享内存段,或者一个信号集,同时会将与ipc对象相关联的数据也一起移除,只有超级管理员,或者ipc对象的创建者才能这样做 443 | * `ipcrm -M shmkey` 移除用shmkey创建的共享内存段 444 | * `ipcrm -m shmid` 移除用shmid标识的共享内存段 445 | * `ipcrm -Q msgkey` 移除用msqkey创建的消息队列 446 | * `ipcrm -q msqid` 移除用msqid标识的消息队列 447 | * `ipcrm -S semkey` 移除用semkey创建的信号 448 | * `ipcrm -s semid` 移除用semid标识的信号 449 | 450 | ## 文本处理工具 451 | * sed 452 | * awk -------------------------------------------------------------------------------- /基础语言/C++面向对象/README.md: -------------------------------------------------------------------------------- 1 | # C++面向对象知识 2 | 3 | > * [内存字节对齐](#内存字节对齐) 4 | > * [面向对象三大特性](#面向对象三大特性) 5 | > * [双冒号、using和namespace](#双冒号using和namespace) 6 | > * [内联函数和函数重载](#内联函数和函数重载) 7 | > * [虚函数可以是内联函数吗](#虚函数可以是内联函数吗) 8 | > * [构造函数/析构函数](#构造函数析构函数) 9 | > * [拷贝构造函数与深浅拷贝](#拷贝构造函数与深浅拷贝) 10 | > * [只在堆上/栈上创建对象](#只在堆上栈上创建对象) 11 | > * [this指针](#this指针) 12 | > * [常函数和常对象](#常函数和常对象) 13 | > * [delete this合法吗](#delete-this合法吗) 14 | > * [为什么空类大小不为0](#为什么空类大小不为0) 15 | > * [静态成员变量与静态成员函数](#静态成员变量与静态成员函数) 16 | > * [能否通过初始化列表初始化静态成员变量](#能否通过初始化列表初始化静态成员变量) 17 | > * [初始化列表的好处和使用条件](#初始化列表的好处和使用条件) 18 | > * [友元全局函数、友元类、友元成员函数](#友元全局函数友元类友元成员函数) 19 | > * [运算符重载及++重载实现](#运算符重载及重载实现) 20 | > * [继承方式、对象模型、同名处理](#继承方式对象模型同名处理) 21 | > * [多继承和菱形继承](#多继承和菱形继承) 22 | > * [静态函数可以是虚函数吗](#静态函数可以是虚函数吗) 23 | > * [类型兼容性原则-为什么会有多态](#类型兼容性原则-为什么会有多态) 24 | > * [重载、覆盖、重写](#重载覆盖重写) 25 | > * [多态实现的基础](#多态实现的基础) 26 | > * [静态多态和动态多态](#静态多态和动态多态) 27 | > * [虚函数指针和虚函数表](#虚函数指针和虚函数表) 28 | > * [函数指针与指针函数](#函数指针与指针函数) 29 | > * [怎么理解多态和虚函数](#怎么理解多态和虚函数) 30 | > * [构造函数能否实现多态/虚函数指针什么时候初始化](#构造函数能否实现多态虚函数指针什么时候初始化) 31 | > * [构造函数能否是虚函数](#构造函数能否是虚函数) 32 | > * [抽象类和纯虚函数](#抽象类和纯虚函数) 33 | > * [虚析构和纯虚析构](#虚析构和纯虚析构) 34 | > * [为什么析构函数必须是虚函数](#为什么析构函数必须是虚函数) 35 | > * [为什么C++默认的析构函数不是虚函数](#为什么C++默认的析构函数不是虚函数) 36 | > * [类模板和函数模板](#类模板和函数模板) 37 | 38 | 39 | ## [内存字节对齐](https://www.cnblogs.com/jijiji/p/4854581.html) 40 | > * `#pragma pack(n)` 表示的是设置n字节对齐,windows默认是8,linux是4 41 | 42 | ```C++ 43 | struct A{ 44 | char a; 45 | int b; 46 | short c; 47 | }; 48 | ``` 49 | > * char占一个字节,起始偏移为零,int占四个字节,min(8,4)=4;所以应该偏移量为4,所以应该在char后面加上三个字节,不存放任何东西,short占两个字节,min(8,2)=2;所以偏移量是2的倍数,而short偏移量是8,是2的倍数,所以无需添加任何字节,所以第一个规则对齐之后内存状态为0xxx|0000|00 50 | > * 此时一共占了10个字节,但是还有结构体本身的对齐,min(8,4)=4;所以总体应该是4的倍数,所以还需要添加两个字节在最后面,所以内存存储状态变为了 0xxx|0000|00xx,一共占据了12个字节 51 | 52 | * 内存对齐规则 53 | * 对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack()指定的数,这个数据成员的自身长度)的倍数 54 | * 在所有的数据成员完成各自对齐之后,结构或联合体本身也要进行对齐,对齐将按照 #pragam pack指定的数值和结构或者联合体最大数据成员长度中比较小的那个,也就是 min(#pragram pack() , 长度最长的数据成员) 55 | 56 | * 需要对齐的原因 57 | * 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常 58 | * 硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。访问未对齐的内存,处理器要访问两次(数据先读高位,再读低位),访问对齐的内存,处理器只要访问一次,为了提高处理器读取数据的效率,我们使用内存对齐 59 | 60 | ## 面向对象三大特性 61 | 通过类创建一个对象的过程叫实例化,实例化后使用对象可以调用类成员函数和成员变量,其中类成员函数称为行为,类成员变量称为属性。类和对象的关系:类是对象的抽象,对象是类的实例 62 | 63 | * 封装 64 | * 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 65 | * public,private,protected 66 | * 继承 67 | * 基类(父类)——> 派生类(子类) 68 | * 多态 69 | 70 | 71 | 72 | ## 双冒号、using和namespace 73 | * namespace主要用来解决命名冲突的问题 74 | * 必须在全局作用域下声明 75 | * 命名空间下可以放函数,变量、结构体和类 76 | * 命名空间可以嵌套命名空间 77 | * 命名空间是开放的,可以随时加入新成员(添加时只需要再次声明namespace,然后添加新成员即可 78 | 79 | * 双冒号::作用域运算符 80 | * 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 81 | * 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的 82 | * 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的 83 | 84 | * using分为using声明和using编译指令 85 | * `using std::cout; //声明` 86 | * `using namespace std; //编译指令` 87 | * 尽量使用声明而不是编译指令,不同命名空间中可能会有相同的变量名,编译指令执行两个命名空间后,会产生二义性 88 | 89 | ## 内联函数和函数重载 90 | * 内联函数 91 | * 相当于把内联函数里面的内容写在调用内联函数处; 92 | * 相当于不用执行进入函数的步骤,直接执行函数体; 93 | * 相当于宏,却比宏多了类型检查,真正具有函数特性; 94 | * 不能包含循环、递归、switch 等复杂操作; 95 | * 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数,内联函数对于编译器而言只是一个建议,编译器不一定会接受这种建议,即使没有声明内联函数,编译器可能也会内联一些小的简单的函数。 96 | 97 | * C++的函数名称可以重复,称为函数重载。 98 | * 其中必须在同一作用域下的函数名称相同,不能是一个在全局,一个局部,或者不同的代码块中 99 | * 可以根据函数参数的个数、类型(const也可以作为重载条件)、顺序不同进行函数重载,但**不能用函数返回值进行重载** 100 | * 当函数重载遇到函数默认参数时,要注意二义性。 101 | 102 | ## 虚函数可以是内联函数吗 103 | * 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。 104 | * 内联是在编译期内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。 105 | * inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。 106 | 107 | ## 构造函数/析构函数 108 | 构造函数和析构函数,分别对应变量的初始化和清理,变量没有初始化,使用后果未知;没有清理,则会内存管理出现安全问题。 109 | 当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数 110 | > * 构造函数:与类名相同,没有返回值,不写void,**可以发生重载**,可以有参数,编译器自动调用,只调用一次。 111 | > * 析构函数:~类名,没有返回值,不写void,**不可以发生重载**,不可以有参数,编译器自动调用,只调用一次。 112 | 113 | * 构造函数 114 | * 系统会默认给一个类提供三个函数:默认构造函数(无参,函数体为空)、默认拷贝构造和析构函数(无参,函数体为空),其中默认拷贝构造可以实现简单的值拷贝。 115 | * 提供了有参构造函数,就不提供默认构造函数;提供了拷贝构造函数,就不会提供其他构造函数,若自己定义可有参构造,也需要自定义无参构造函数 116 | 117 | * 析构函数 118 | * 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好自定义析构函数,在销毁类之前,释放掉申请的内存空间,避免内存泄漏 119 | 120 | ## 拷贝构造函数与深浅拷贝 121 | 拷贝构造函数的参数必须加const,因为防止修改,本来就是用现有的对象初始化新的对象。 122 | 123 | * 拷贝构造函数的使用时机 124 | * 使用已经创建好的对象初始化新对象 `A a; A b = a; A c(a); b = c;//b = c不是初始化,调用赋值运算符 ` 125 | * 以值传递的方式来给函数参数传值 126 | * 以值方式返回局部对象(不常用,一般不返回局部对象) 127 | 128 | 129 | * 深拷贝和浅拷贝 130 | 只有当对象的成员属性在堆区开辟空间内存时,才会涉及深浅拷贝,如果仅仅是在栈区开辟内存,则默认的拷贝构造函数和析构函数就可以满足要求。 131 | * **浅拷贝**:使用默认拷贝构造函数,拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标,因此涉及堆区开辟内存时,会将两个成员属性指向相同的内存空间,从而在释放时导致内存空间被多次释放,使得程序down掉。 132 | * **浅拷贝的问题**:当出现类的等号赋值时,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次free函数,指向的内存空间已经被释放掉,再次free会报错;另外,一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了所以,这时,必须采用深拷贝 133 | * **深拷贝**:自定义拷贝构造函数,在堆内存中另外申请空间来储存数据,从而解决指针悬挂的问题。**需要注意自定义析构函数中应该释放掉申请的内存** 134 | 135 | 我们在定义类或者结构体,这些结构的时候,最后都重写拷贝函数,避免浅拷贝这类不易发现但后果严重的错误产生 136 | 137 | ## 只在堆上/栈上创建对象 138 | > * **只能在堆上生成对象:将析构函数设置为私有。** 139 | 原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。 140 | > * **只能在栈上生成对象:将new 和 delete 重载为私有。** 141 | 原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。 142 | 将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。 143 | 144 | ## this指针 145 | * **为什么会有this指针** 146 | 在类实例化对象时,只有非静态成员变量属于对象本身,剩余的静态成员变量,静态函数,非静态函数都不属于对象本身,因此非静态成员函数只会实例一份,多个同类型对象会共用一块代码,由于类中每个实例后的对象都有独一无二的地址,因此不同的实例对象调用成员函数时,函数需要知道是谁在调用它,因此引入了this指针。 147 | * **this指针的作用** 148 | this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都会含有一个系统自动生成的隐含指针this。this指针指向被调用的成员函数所属的对象(谁调用成员函数,this指向谁),*this表示对象本身,**非静态成员函数中才有this,静态成员函数内部没有**。 149 | * this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。 150 | * 对非静态成员函数默认添加了this指针,类型为classname *const this 151 | * **this指针使用** 152 | * 当形参与成员变量名相同时,用this指针来区分 153 | * 为实现对象的链式引用,在类的非静态成员函数中返回对象本身,可以用return *this,this指向对象,/*this表示对象本身。 154 | 155 | ## 常函数和常对象 156 | ` void func() const //常函数,此处func为类成员函数` 157 | ` const Person p2; //常对象` 158 | 159 | * 常函数修饰的是this指针,不允许修改this指针指向的值,如果执意要修改常函数,可以在成员属性前加**mutable**。 160 | * 常对象不允许修改属性,不可以调用普通成员函数,可以调用常函数。 161 | 162 | ## delete this合法吗 163 | 164 | 合法,但有前提: 165 | 166 | * 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的 167 | * 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数 168 | * 必须保证成员函数的 delete this 后面没有调用 this 了 169 | * 必须保证 delete this 后没有人使用了 170 | 171 | ## 为什么空类大小不为0 172 | sizeof(空class) = 1,为了确保两个不同对象的地址不同。 173 | 174 | ## 静态成员变量与静态成员函数 175 | 若将成员变量声明为static,则为静态成员变量,与一般的成员变量不同,无论建立多少对象,都只有一个静态成员变量的拷贝,静态成员变量属于一个类,所有对象共享。静态变量在编译阶段就分配了空间,对象还没创建时就已经分配了空间,放到全局静态区。 176 | 177 | * 静态成员变量 178 | * 最好是类内声明,类外初始化(以免类名访问静态成员访问不到) 179 | * 无论公有,私有,静态成员都可以在类外定义,但私有成员仍有访问权限 180 | * 非静态成员类外不能初始化 181 | * 静态成员数据是共享的。 182 | * 静态成员函数 183 | * 静态成员函数可以直接访问静态成员变量,不能直接访问普通成员变量,但可以通过参数传递的方式访问 184 | * 普通成员函数可以访问普通成员变量,也可以访问静态成员变量 185 | * 静态成员函数没有this指针。非静态数据成员为对象单独维护,但静态成员函数为共享函数,无法区分是哪个对象,因此不能直接访问普通变量成员,也没有this指针。 186 | 187 | ## 初始化列表的好处和使用条件 188 | * 初始化列表的使用条件 189 | * const类型的数据 190 | * 引用类型的数据 191 | * 好处 192 | * 初始化是直接初始化成员 193 | * 赋值是初始化再赋值 194 | 195 | ## 能否通过初始化列表初始化静态成员变量 196 | 不能,静态成员变量最好类内声明,类外初始化.静态成员是单独存储的,并不是对象的组成部分。如果在类的内部进行定义,在建立多个对象时会多次声明和定义该变量的存储位置。在名字空间和作用域相同的情况下会导致重名的问题。 197 | 198 | ## [友元全局函数、友元类、友元成员函数](https://www.cnblogs.com/qinguoyi/p/10254263.html) 199 | 友元主要是为了访问类中的私有成员(包括属性和方法),会破坏C++的封装性,尽量不使用 200 | 201 | * 友元全局函数 202 | * 友元函数声明可以在类中的任何地方,一般放在类定义的开始或结尾 203 | * 一个函数可以是多个类的友元函数,只需要在各个类中分别声明 204 | * 友元函数在类内声明,类外定义,定义和使用时不需加作用域和类名,与普通函数无异。 205 | * 友元类 206 | * 友元不可继承 207 | * 友元是单向的,类A是类B的友元类,但类B不一定是类A的 208 | * 友元不具有传递性,类A是类B的友元类,类B是类C的友元类,但类A不一定是类C的友元类。 209 | 210 | * 友元成员函数 211 | * 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员 212 | 213 | ## 运算符重载及++重载实现 214 | ### 运算符重载基本属性 215 | 216 | * 运算符重载的目的是扩展C++中提供的运算符的适用范围,使之能作用于对象,或自定义的数据类型 217 | * 运算符重载的实质是函数重载,可以重载为普通成员函数,也可以重载为成员函数 218 | * 运算符重载也是多态的一种,和函数重载称为静态多态,表示函数地址早绑定,在编译阶段就确定好了地址 219 | 220 | ### 运算符重载总结 221 | 222 | * 重载运算符(),[] ,->, =的时候,运算符重载函数必须声明为类的成员函数 223 | * 重载运算符<<,>>的时候,运算符只能通过全局函数配合友元函数进行重载 224 | * 不要重载&&和||运算符,因为无法实现短路原则。 225 | 226 | ### i++和++i实现 227 | C++内置类型的后置++返回的是变量的拷贝,也就是不可修改的值;前置++返回的是变量的引用,因此可以作为修改的左值。即++(++a)或(++a)++都可以,但++(a++)不可以,(C++默认必须修改a的值,如果不修改则报错)。 228 | ```C++ 229 | //++i 230 | int& int::operator++() 231 | { 232 | *this +=1; 233 | return *this; 234 | } 235 | 236 | //i++,注意后置++有占位参数以区分跟前置++不同 237 | const int int::operator++(int) 238 | { 239 | int oldValue = *this; 240 | ++(*this); 241 | return oldValue; 242 | } 243 | ``` 244 | 245 | ## [继承方式、对象模型、同名处理](https://www.cnblogs.com/qinguoyi/p/10277350.html) 246 | 继承主要是为了减少代码的重复内容,解决代码复用问题。通过抽象出一个基类(父类),将重复代码写到基类中,在派生类(子类)中实现不同的方法。 247 | 248 | ### 继承方式 249 | 250 | * 公有继承:保持父类中的访问属性 251 | * 私有继承:将父类中的所有访问属性改为private 252 | * 保护继承:除父类中的私有属性,其他改为保护属性 253 | 254 | ### 继承的对象模型 255 | 256 | * 子类中会继承父类的私有成员,只是被编译器隐藏起来了,无法访问到,通过sizeof(子类class)可以检查出。 257 | * 子类创建对象时,先调用父类的构造函数,然后再调用自身的构造,析构顺序与构造顺序相反 258 | * 由于继承中父类和子类的构造、析构顺序原因,当父类中只提供了有参构造(默认构造等函数会被隐藏),而子类仅仅调用默认构造时,会因为子类创建对象时无法调用父类构造函数而报错,这里可以让子类利用初始化列表来显式调用父类有参构造函数来进行父类构造,然后进行子类构造。 259 | * 子类会继承父类的成员属性和成员函数,但子类不会继承父类构造函数和析构函数 260 | 261 | ### 继承中的同名处理 262 | * 父类和子类**成员属性**同名,用子类声明对象调用子类属性,若想调用父类成员,则加上父类的作用域 263 | * 父类和子类**成员函数**同名,子类函数不会覆盖父类的成员,只是隐藏起来,用子类声明对象调用子类成员函数,若想调用父类函数(包括重载),则加上父类的作用域 264 | * 若子类中没有与父类同名的成员函数,子类声明对象后,可以直接调用父类成员函数。 265 | 266 | ## [多继承和菱形继承](https://www.cnblogs.com/qinguoyi/p/10277350.html) 267 | ### 多继承 268 | 多继承会产生二义性的问题。如果继承的多个父类中有同名的成员属性和成员函数,在子类调用时,需要指定作用域从而确定父类。 269 | 270 | ### 菱形继承 271 | 两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类,这种继承称为菱形继承。比如羊和驼继承于动物类,同时羊驼继承于羊和驼。 272 | 273 | #### 菱形继承会产生问题 274 | * **浪费空间。**羊驼继承了两份动物类中的某些数据和函数,但只需要一份即可 275 | * **二义性。从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题。** 羊驼调用数据和函数时,会出现二义性,通过sheep类得到一个age,通过carmel类得到一个age,两个数据不会相互影响,相互修改,导致同一份数据不一致。 276 | 277 | 278 | #### 解决菱形继承的问题 279 | 使用虚继承,在**继承方式前加virtual**,这样的话羊驼可以直接访问m_Age,不用添加作用域,且这样操作的是共享的一份数据 280 | ```C++ 281 | class Animal{ 282 | public: 283 | int m_Age; 284 | }; 285 | class Sheep:virtual public Animal{ 286 | int m_sheep; 287 | }; 288 | class Camel :virtual public Animal{ 289 | int m_camel; 290 | }; 291 | 292 | class Son :public Sheep, public Camel{ 293 | int m_son 294 | }; 295 | void test01(){ 296 | Son son; 297 | son.m_Age = 10; 298 | cout << sizeof(Animal) << endl; //m_Age 299 | cout << sizeof(Sheep) << endl; //sheep-Vbptr,m_sheep,m_Age 300 | cout << sizeof(Camel) << endl; //camel-Vbptr,m_camel,m_Age 301 | cout << sizeof(Son) << endl; //sheep-Vbptr,m_sheep,camel-Vbptr,m_camel,m_son,m_Age 302 | } 303 | ``` 304 |
305 | 306 | > * **特别注意:**此时son没有自己的虚基类表和虚基类指针,只是继承了sheep和camel的虚基类指针和虚基类表,只是修改了两个虚基类表中的值,修改为当前类中,如何通过继承的虚基类指针查找虚基类数据 307 | > * Son继承Sheep父类,父类中有虚基类指针vbptr(virtual base pointer),对象结构类似结构体,首元素是虚基类指针,其余为自身数据(不包括静态成员和成员函数) 308 | > * Sheep的虚指针指向下面Sheep的虚基类表vbtale@Sheep(virtual base table),虚基类表是一个整型数组,数组第二个元素值为20,即Sheep的虚指针地址偏移20指向Animal的m_Age地址。Camel父类同理,因此,类中只有一个m_Age元素。 309 | > * Son中包含了两个指针和四个int类型,所以大小为24。 310 | 311 | ```C++ 312 | class Animal{ 313 | public: 314 | int m_Age; 315 | }; 316 | class Sheep:virtual public Animal{ 317 | int m_sheep; 318 | }; 319 | class Camel :virtual public Animal{ 320 | int m_camel; 321 | }; 322 | 323 | class Son :virtual public Sheep, virtual public Camel{ 324 | int m_son 325 | }; 326 | void test01(){ 327 | Son son; 328 | son.m_Age = 10; 329 | cout << sizeof(Animal) << endl; //m_Age 330 | cout << sizeof(Sheep) << endl; //sheep-Vbptr,m_sheep,m_Age 331 | cout << sizeof(Camel) << endl; //camel-Vbptr,m_camel,m_Age 332 | cout << sizeof(Son) << endl; //son-vbptr,m_son,m_Age,sheep-Vbptr,m_sheep,camel-Vbptr,m_camel, 333 | } 334 | ``` 335 | 336 |
337 | 338 | > * 注意跟上面的区别,一个是son类中的元素顺序,一个是son类有了自己的虚基类指针和虚基类表 339 | 340 | * 虚继承 341 | * 一般通过虚基类指针和虚基类表实现,将共同基类设置为虚基类 342 | * **每个虚继承的子类(虚基类本身没有)**都有一个虚基类指针(占用一个指针的存储空间)和虚基类表(不占用类对象的存储空间),**虚基类指针属于对象,虚基类表属于类** 343 | * 当虚继承的子类被当做父类继承时,虚基类指针也会被继承。 344 | * 虚表中只记录了虚基类数据在派生类对象中与派生类对象首地址(虚基类指针)之间的偏移量,以此来访问虚基类数据 345 | * 虚继承不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。 346 | * 虚基类表本质是一个**整型数组** 347 | 348 | ## 静态函数可以是虚函数吗 349 | 不可以,因为虚函数属于对象,不属于类,静态函数属于类 350 | 351 | ## 类型兼容性原则 为什么会有多态 352 | 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,如使用子类对象可以直接赋值给父类对象或子类对象可以直接初始化父类对象时,**对于同样的一条语句,不管传入子类还是父类对象,都是调用的父类函数,但我们想实现的是同样的一条语句,传入不同的对象,调用不同的函数**. 353 | ```C++ 354 | class Animal{ 355 | public: 356 | void speak(){ 357 | cout << "Animal speak" << endl; 358 | } 359 | }; 360 | 361 | class Sheep :public Animal{ 362 | public: 363 | void speak(){ //重定义,子类重新定义父类中有相同名称的非虚函数 364 | cout << "Sheep speak" << endl; 365 | } 366 | }; 367 | 368 | void doSpeak(Animal &animal){ 369 | animal.speak(); 370 | } 371 | 372 | //想通过父类引用指向子类对象 373 | void test01(){ 374 | Sheep sheep; 375 | doSpeak(sheep); //Animal speak; 376 | sheep.speak(); //sheep speak 377 | sheep.Animal::speak(); //Animal speak; //继承中的重定义可以通过作用域 378 | } 379 | ``` 380 | 381 | 但我们想**传入子类对象调用子类函数,传入父类对象调用父类函数**,即同样的调用语句有多种不同的表现形态,这样就出现了**多态** 382 | 383 | ## 重载、覆盖、重写 384 | 385 | * 重载(overload):是函数名相同,参数列表不同。重载只是在同一个类的内部存在,但是不能靠返回类型来判断 386 | * 覆盖(override):子类重新定义父类中有相同名称和参数的虚函数。两者的函数特征相同。 387 | * 被重写的函数不能是static的。必须是virtual的 388 | * 重写函数必须有相同的类型,名称和参数列表 389 | * 重写函数的访问权限可以不同。尽管virtual是private的,子类中重写改写为public,protected也是可以的。 390 | * 重写(overwrite):也叫做隐藏。子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。如果一个类,存在和父类相同的函数,那么,这个类将会隐藏其父类的方法,除非你在调用的时候,强制转换为父类类型或加上父类作用域 391 | 392 | ## 多态实现的基础 393 | > * 继承 394 | > * 虚函数覆盖 395 | > * 父类指针或引用指向子类对象访问虚函数 396 | 397 | ```C++ 398 | class Animal{ 399 | public: 400 | virtual void speak(){ //在父类中声明虚函数,可以实现多态,动态联编 401 | cout << "Animal speak" << endl; 402 | } 403 | int m_age = 0; 404 | }; 405 | 406 | class Sheep :public Animal{ 407 | public: 408 | void speak(){ //发生多态时,子类对父类中的成员函数进行重写,virtual可写可不写 409 | cout << "Sheep speak" << endl; 410 | } 411 | int m_age = 1; 412 | }; 413 | 414 | void doSpeak(Animal &animal){ 415 | animal.speak(); 416 | } 417 | 418 | void test01(){ 419 | //传入子类对象调用子类成员函数 420 | Sheep sheep; 421 | doSpeak(sheep); //sheep speak; 422 | 423 | //子类对象直接调用子类成员函数 424 | sheep.speak(); //sheep speak; 425 | 426 | //子类对象通过作用域调用父类成员函数 427 | sheep.Animal::speak(); //animal speak; 428 | 429 | //基类成员不能转换为子类成员,即不能向下转换 430 | //Animal *animal0 = new Animal(); 431 | //Sheep * sheep0 = animal0; 432 | //sheep0->speak(); 433 | 434 | //同样不能向下转换 435 | //Animal animal0; 436 | //Sheep sheep0 = animal0; 437 | 438 | //父类指针指向子类对象 439 | Sheep *sheep1 = new Sheep(); 440 | Animal *animal1 = sheep1; 441 | animal1->speak(); //sheep speak; 442 | 443 | //父类引用指向子类对象 444 | Sheep sheep2; 445 | Animal &animal2 = sheep2; 446 | animal2.speak(); //sheep speak; 447 | 448 | //子类对象直接赋值给父类对象,不符合多态条件,符合类型兼容性原则 449 | Sheep sheep0; 450 | Animal animal0 = sheep0; 451 | animal0.speak(); //animal speak; 452 | } 453 | ``` 454 | 455 | ## 静态多态和动态多态 456 | > * 静态多态(运算符重载、函数重载) 457 | > * 动态多态(继承、虚函数) 458 | 459 | 两者主要的区别:函数地址是早绑定(静态联编)还是晚绑定(动态联编)。即,在编译阶段确定好地址还是在运行时才确定地址。 460 | 461 | ## 虚函数指针和虚函数表 462 | > * 前提发生了多态,每个类中都有虚函数表,最开始的父类创建虚函数表,后面的子类继承父类的虚函数表,然后对虚函数重写 463 | > * 虚函数重写(覆盖)的实质就是重写父类虚函数表中的父类虚函数地址; 464 | > * 实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,**虚函数表(vftable)属于类**,或者说这个类的所有对象共享一个虚函数表;**虚函数指针(vfptr)属于单个对象**。 465 | > * 在程序调用时,先创建对象,编译器在对象的内存结构头部添加一个虚函数指针,进行动态绑定,虚函数指针指向对象所属类的虚函数表。 466 | > * 虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。 467 | > * 编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心调用的函数是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr--->虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。 468 | 469 | ## [函数指针与指针函数](https://www.cnblogs.com/qinguoyi/p/10198019.html) 470 | * 指针函数`int* f(int x, int y)`本质是函数,返回值为指针,函数指针`int (*f)(int x)`本质是指针,指向函数的指针 471 | 472 | * 通常我们可以将指针指向某类型的变量,称为类型指针(如,整型指针)。若将一个指针指向函数,则称为函数指针。 473 | 474 | * 函数名代表函数的入口地址,同样的,我们可以通过根据该地址进行函数调用,而非直接调用函数名。 475 | 476 | ```C++ 477 | void test001(){ 478 | printf("hello, world"); 479 | } 480 | 481 | int main(){ 482 | void(*myfunc)() = test001;//将函数写成函数指针 483 | myfunc(); //调用函数指针 hello world 484 | } 485 | ``` 486 | test001的函数名与myfunc函数指针都是一样的,即都是函数指针。test001函数名是一个函数指针常量,而myfunc是一个函数指针变量,这是它们的关系。 487 | 488 | * 函数指针多用于回调函数,回调函数最大的优势在于灵活操作,可以实现用户定制的函数,降低耦合性,实现多样性,如STL中 489 | 490 | 491 | 492 | ## 怎么理解多态和虚函数 493 | * 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。 494 | 495 | * 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数 496 | 497 | 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。 498 | 499 | ## 构造函数能否实现多态/虚函数指针什么时候初始化 500 | 两个问题本质是一样的,构造函数不能实现多态 501 | 502 | * 对象在创建时,由编译器对VPTR指针进行初始化,只有当对象的构造完全结束后VPTR的指向才最终确定。 503 | 504 | * 子类中虚函数指针的初始化过程 505 | 当定义一个子类对象的时候比较麻烦,因为构造子类对象的时候会首先调用父类的构造函数然后再调用子类的构造函数。当调用父类的构造函数的时候,此时会创建Vptr指针,该指针会指向父类的虚函数表;然后再调用子类的构造函数,子类继承父类的虚函数指针,此时Vptr又被赋值指向子类的虚函数表。 506 | 507 | ## 构造函数能否是虚函数 508 | 不能,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针 509 | 510 | ## 抽象类和纯虚函数 511 | 在程序设计中,如果仅仅为了设计一些虚函数接口,打算在子类中对其进行重写,那么不需要在父类中对虚函数的函数体提供无意义的代码,可以通过纯虚函数满足需求。 512 | 513 | * 纯虚函数的语法格式:`virtual 返回值类型 函数名 () = 0; `只需要将函数体完全替换为 =0即可,**纯虚函数必须在子类中进行实现**,在子类外实现是无效的。 514 | 515 | * 注意 516 | * 如果父类中出现了一个纯虚函数,则这个类变为了抽象类,抽象类不可实例对象 517 | * 如果父类为抽象类,子类继承父类后,必须实现父类所有的纯虚函数,否则子类也为抽象类,也无法实例对象**但纯虚析构函数例外,因为子类不会继承父类的析构函数** 518 | 519 | ## 虚析构和纯虚析构 520 | > * 仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数; 521 | > * 当发生多态时(父类指针或引用指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。 522 | 523 | 解决方法:将父类中的析构函数设置为虚函数,设置后会先调用子类析构函数,再调用父类析构函数 524 | 525 | * 纯虚析构 526 | * 纯虚析构需要类内声明,类外实现 527 | * 纯虚析构也是虚函数,该类也为抽象类 528 | * 子类不会继承父类的析构函数,当父类纯虚析构没有实现时,子类不是抽象类,可以创建创建对象。 529 | 530 | ## 为什么析构函数必须是虚函数 531 | 因为当发生多态时,父类指针在堆上创建子类对象,销毁时会内存泄漏 532 | 533 | ## 为什么C++默认的析构函数不是虚函数 534 | 因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存 535 | 536 | ## 类模板和函数模板 537 | 通过template或template实现,主要用于数据的类型参数化,简化代码,有类模板和函数模板,函数模板是用于生成函数的,类模板则是用于生成类的 538 | 539 | * 类模板和函数模板定义 540 | > * template声明下面是函数定义,则为函数模板,否则为类模板。 541 | > * 注意:每个函数模板前必须有且仅有一个template声明,不允许多个template声明后只有一个函数模板,也不允许一个template声明后有多个函数模板(类模板同理)。 542 | 543 | * 类模板与函数模板的区别 544 | * 类模板不支持自动类型推导 545 | * 数据类型可以有默认参数. 546 | -------------------------------------------------------------------------------- /计算机基础知识/计算机网络/README.md: -------------------------------------------------------------------------------- 1 | # 计算机网络 2 | > * [OSI七层协议及TCP/IP四层协议](#OSI七层协议及TCPIP四层协议) 3 | > * [通信交互方式](#通信交互方式) 4 | > * [MAC地址和IP地址](#MAC地址和IP地址) 5 | > * [ARP协议的作用](#ARP协议的作用) 6 | > * [ping发生了什么](#ping发生了什么) 7 | > * [traceroute发生了什么](#traceroute发生了什么) 8 | > * [TCP/UDP的区别和应用场景](#TCPUDP的区别和应用场景) 9 | > * [拥塞控制和流量控制的区别](#拥塞控制和流量控制的区别) 10 | > * [TCP滑动窗口实现流量控制](#TCP滑动窗口实现流量控制) 11 | > * [TCP超时重传](#TCP超时重传) 12 | > * [TCP拥塞机制](#TCP拥塞机制) 13 | > * [TCP三次握手及三次缘由](#TCP三次握手及三次缘由) 14 | > * [TCP四次挥手及四次缘由](#TCP四次挥手及四次缘由) 15 | > * [TIME-WAIT状态及2MSL时间](#TIME-WAIT状态及2MSL时间) 16 | > * [域名系统DNS](#域名系统DNS) 17 | > * [统一资源定位符URL](#统一资源定位符URL) 18 | > * [描述一下HTTP协议](#描述一下HTTP协议) 19 | > * [HTTP2.0](#HTTP2) 20 | > * [HTTP持久连接与管线化](#HTTP持久连接与管线化) 21 | > * [HTTP协议请求报文具体信息](#HTTP协议请求报文具体信息) 22 | > * [GET和POST的区别](#GET和POST的区别) 23 | > * [HTTP协议响应报文具体信息](#HTTP协议响应报文具体信息) 24 | > * [HTTP状态码](#HTTP状态码) 25 | > * [浏览器键入URL后的访问流程](#浏览器键入URL后的访问流程) 26 | > * [IP/TCP/UDP分片](#IPTCPUDP分片) 27 | > * [对称密钥和公钥密码体制](#对称密钥和公钥密码体制) 28 | > * [数字签名和数字证书](#数字签名和数字证书) 29 | > * [HTTP和HTTPS的区别](#HTTP和HTTPS的区别) 30 | > * [运输层安全协议及SSL工作过程](#运输层安全协议及SSL工作过程) 31 | > * [HTTPS必须在每次请求中都要先在SSL/TLS层进行握手传输密钥吗?](#HTTPS必须在每次请求中都要先在SSL/TLS层进行握手传输密钥吗) 32 | > * [cookie和session](#cookie和session) 33 | > * [浏览器关闭后,session就销毁了吗](#浏览器关闭后session就销毁了吗) 34 | 35 | ## OSI七层协议及TCP/IP四层协议 36 | * 七层协议:物、数、网、传、会、表、应 37 | * 物理层 RJ45 38 | * 数据链路层 PPP,IEEE 802.3/802.2 39 | * 网络层 IP,ARP 40 | * 传输层 TCP,UDP 41 | * 会话层 42 | * 表示层 TIFF,GIF,JPEG, 43 | * 应用层 DNS,HTTP,FTP 44 | 45 | * 四层协议:数据链路层(物理层,数据链路层),网络层(网络层),传输层(传输层),应用层(会话层,表示层,应用层) 46 | * 数据链路层: PPP**MAC不属于协议,只是一个地址** 47 | * 网络层:IP、ARP、ICMP 48 | * 传输层:TCP、UDP 49 | * 应用层:DNS、HTTP、FTP 50 | 51 | ## 通信交互方式 52 | * 单工通信 53 | * 只能有一个方向的通信没有反方向的交互 54 | * 半双工通信 55 | * 双方可以发送消息,但不能同时发送。可以交替进行一方发送、另一方接收 56 | * 全双工通信 57 | * 通信的双方可以同时发送和接收信息 58 | 59 | ## MAC地址和IP地址 60 | * MAC地址又叫硬件地址或物理地址,它不是地址位置,实际上是适配器地址,每一台计算机中固化在适配器的ROM中的地址,作用是用来定义网络设备的位置 61 | * IP地址是IP协议提供的一种统一的地址格式,为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异 62 | * 两者的区别 63 | * 物理地址是数据链路层和物理层使用的地址,放在MAC帧的首部 64 | * IP地址是网络层和以上各层使用的地址,放在IP数据报的首部 65 | 66 | ## ARP协议的作用 67 | 网络层使用的是IP地址,数据链路层使用的是硬件地址。 68 | ARP协议的用途是为了从网络层使用的IP地址,解析出数据链路层使用的硬件地址。 69 | 70 | * 在主机ARP高速缓存中存放一个从IP地址到硬件地址的映射表 71 | * 当需要解析时,先去arp缓存表(存着ip-mac对应关系)去查找目标ip的mac地址 72 | * 如果查到了,将目标ip的mac地址封装到链路层数据报 73 | * 如果缓存中没有找到,会发起一个广播:who is ip XXX tell ip XXX,所有收到的广播的机器看这个ip是不是自己的,如果是自己的,则以单播的形式将自己的mac地址回复给请求的机器 74 | 75 | ## [ping发生了什么](https://blog.csdn.net/fd8559350/article/details/52135571) 76 | ping主要是为了测试两台主机之间的连通性,通过应用层直接使用网络层ICMP,没有通过运输层TCP和UDP,是通过发送ICMP报文回显请求实现。 77 | 78 | > * A主机构建一个ICMP格式的数据包,通过ICMP协议把该数据包和B主机的IP地址一起交给IP协议; 79 | > * IP层构建一个数据包(A主机的IP地址+控制信息+B主机的IP地址),获得B主机的MAC地址,以便构建一个数据帧(IP协议会根据B主机的IP地址和自己的子网掩码判断是不是属于同一层网络,如果是属于同一层网络的话,就会获得B主机的MAC地址,如果以前两机有过通信,在A机的ARP缓存表应该有B机IP与其MAC的映射关系,如果没有,就发一个ARP请求广播,得到B机的MAC) 80 | > * 主机B接受到主机A的发过来的数据帧以后,先检查该帧中包含的B的IP地址,并和本地的物理地址进行比对,如果符合的话,就接受,否则,就抛弃。同样,需要将该数据帧交由自己的IP层协议,IP层检查以后,再交由ICMP协议,构建一个ICMP的应答包,发送给主机A。 81 | 82 | ## traceroute发生了什么 83 | traceroute用来跟踪一个分组从源点到终点的路径,及到达其中每一个路由器的往返时间 84 | 85 | * 通过发送UDP报文,设置目的端口为一个不可能的值 86 | * 将IP首部中的TTL分别设置从1到N,每次逐个增加 87 | * 每次设置TTL后,重新发送数据报,路由器接收到数据报后,将TTL减1,若当前的路由器接收到数据报,发现TTL为1时,会将TTL减1变为0,然后丢弃数据报,发送ICMP时间超过报文 88 | * 如果最后一个数据报刚刚达到主机,数据报的TTL是1,此时主机不把TTL减1 89 | * 因IP数据报中封装的是无法交付的UDP数据报,此时目的主机向源主机发送ICMP终点不可达差错报文,表示达到目的主机 90 | 91 | ## TCP/UDP的区别和应用场景 92 | 93 | ###区别 94 | TCP,全称:传输控制协议,面向连接的安全的流式传输协议 95 | UDP,全称:用户数据报协议,面向无连接的不安全的报式传输协议 96 | 97 | * 连接 98 | * TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接。 99 | * UDP无连接。 100 | 101 | * 服务对象 102 | * TCP是点对点的两点间服务,即一条TCP连接只能有两个端点 103 | * UDP支持一对一,一对多,多对一,多对多的交互通信。 104 | 105 | * 可靠性 106 | * TCP是可靠交付:无差错,不丢失,不重复,按序到达。 107 | * UDP是尽最大努力交付,不保证可靠交付。 108 | 109 | * 拥塞控制,流量控制 110 | * TCP有拥塞控制和流量控制保证数据传输的安全性。 111 | * UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。 112 | 113 | * 报文长度 114 | * TCP是动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的,流式传输 115 | * UDP面向报文,不合并,不拆分,保留上面(应用层)传下来报文的边界,直接传输报文。 116 | 117 | * 首部开销 118 | * TCP首部开销大,首部20个字节。 119 | * UDP首部开销小,8字节。(源端口,目的端口,UDP数据报长度,检验和,每个字段两个字节) 120 | 121 | ### 应用场景 122 | * 要求通信数据完整性,则应该选用TCP协议(如文件传输、重要状态的更新,登录数据传输等) 123 | * 要求通信实时性,使用 UDP 协议(如视频传输,通话,屏幕共享软件) 124 | 125 | ## 拥塞控制和流量控制的区别 126 | * 拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程。 127 | * 流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收 128 | 129 | ## [TCP滑动窗口实现流量控制](https://blog.csdn.net/dangzhangjing97/article/details/81008836) 130 | * 流量控制是让发送方的发送速率不要太快,要让接收方来得及接收,实现对发送方的流量控制. 131 | * 滑动窗口出现的原因:在确认应答策略中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能比较差,尤其是数据往返的时间长的时候 132 | * 滑动窗口以字节为单位,而不是报文 133 | 134 | ## TCP超时重传 135 | * 保证了数据的可靠传输,对于一些出错,丢包等问题TCP设计了超时与重传机制。 136 | * 基本原理:在发送一个数据之后,就开启一个定时器,并设置RTO,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。 137 | * 不同的网络情况不一样,不可能设置一样的RTO(超时重传时间),实际中RTO是根据网络中的RTT(报文段往返时间)来自适应调整的 138 | 139 | ## [TCP拥塞机制](https://blog.csdn.net/shuxnhs/article/details/80644531) 140 | * 拥塞的标志 141 | * 超时重传 142 | * 3次重复的ACK 143 | 144 | 慢启动,拥塞避免,快恢复,快重传 145 | 146 | ## ACK SYN FIN解释及是否消耗序列号 147 | * ACK 确认标志位,ACK可以携带数据,若不携带,则不消耗序列号 148 | * SYN 同步标志位,SYN不能携带数据,必须消耗一个序列号 149 | * FIN 终止标志位,FIN可以携带数据,必须消耗一个序列号 150 | 151 | ## TCP三次握手及三次缘由 152 | * **为什么TCP三次握手,不能两次或者四次吗?** 153 | * 三次握手是为了防止,客户端的请求报文在网络滞留,客户端超时重传了请求报文,服务端建立连接,传输数据,释放连接之后,服务器又收到了客户端滞留的请求报文,建立连接一直等待客户端发送数据。 154 | * 服务器对客户端的请求进行回应(第二次握手)后,就会理所当然的认为连接已建立,而如果客户端并没有收到服务器的回应呢?此时,客户端仍认为连接未建立,服务器会对已建立的连接保存必要的资源,如果大量的这种情况,服务器会崩溃。 155 | * 服务器端给客户端发送同步及确认报文时可以合并,四次会浪费时间 156 | 157 | 158 | ## TCP四次挥手及四次缘由 159 | 四次报文中服务器端发送给客户端的请求关闭连接报文FIN和ACK也是合并的,相对于三次来说,只是前面多了一次ACK的确认。 160 | 161 | * **为什么TCP四次挥手,不能三次吗?** 162 | * 当客户端确认发送完数据且知道服务器已经接收完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给服务器。 163 | * 服务器收到客户端发送的FIN,表示收到了,就会发送ACK回复。 164 | * 但这时候服务器可能还在发送数据,没有想要关闭数据口的意思,所以服务器的FIN与ACK不是同时发送的,而是等到服务器数据发送完了,才会发送FIN给客户端。 165 | * 客户端收到服务器发来的FIN,知道服务器的数据也发送完了,回复ACK, 客户端等待2MSL以后,没有收到服务器传来的任何消息,知道服务器已经收到自己的ACK了,客户端就关闭链接,服务器也关闭链接(服务器比客户端早关闭)。 166 | 167 | 168 | ## TIME-WAIT状态及2MSL时间 169 | * 四次挥手期间,客户端和服务器端都可主动释放连接,谁主动释放,谁将进入TIME_WAIT状态 170 | * MSL是最长报文寿命,一般为2分钟,2MSL即4分钟 171 | * 为什么TIME-WAIT状态必须等待2MSL时间? 172 | * **保证最后一次挥手报文能到B,能进行超时重传。**若B收不到A的ACK报文,则B会超时重传FIN+ACK,A会在2MSL时间内收到重传报文段,然后发送ACK,重新启动2MSL计时器 173 | * 2MSL后,本次连接的所有报文都会消失,不会影响下一次连接。 174 | 175 | ## 域名系统DNS 176 | 用于将域名转换为IP地址。 177 | DNS解析过程有两种,分别是递归查询和迭代查询。 178 | 179 | * 递归查询 180 | * 若主机询问的本地域名服务器不知道被查询域名的IP地址,本地域名服务器以DNS客户身份,向其他根域名服务器继续发出查询请求报文(代替该主机继续查询),而不是该主机自己进行下一步查询 181 | * 迭代查询 182 | * 当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出IP地址,要么告诉本地域名服务器,应该向哪一个域名服务器进行查询,然后本地域名服务器进行后续查询 183 | 184 | ## 统一资源定位符URL 185 | 统一资源定位符URL,用来表示从互联网上得到的资源位置。 186 | 187 | * 一般由四个部分组成 188 | * <协议>://<主机>:<端口>/<路径> 189 | * 主机一般为域名,需要通过DNS系统解析出IP 190 | 191 | * 使用HTTP的URL 192 | * http://<主机>:<端口>/<路径> 193 | 194 | 195 | ## [描述一下HTTP协议](https://www.cnblogs.com/ranyonsue/p/5984001.html) 196 | ### 概述 197 | * HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议。 198 | * HTTP属于应用层协议,基于TCP/IP通信协议来传递数据 199 | 200 | ### 特点 201 | * 灵活 202 | * HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。 203 | * 无连接 204 | * 无连接的含义是通信双方在交换HTTP报文之前不需要建立HTTP连接 205 | * 无状态 206 | * 无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时应答较快。 207 | * 支持B/S和C/S模式 208 | * 默认端口80 209 | * 基于TCP协议 210 | 211 | ### HTTP工作原理 212 | HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。 213 | 214 | * 客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。 215 | * 服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。 216 | 217 | **HTTP 请求/响应的步骤** 218 | 219 | > * **客户端连接到Web服务器** 220 | 一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.oakcms.cn。 221 | > * **发送HTTP请求** 222 | 通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。 223 | > * **服务器接受请求并返回HTTP响应** 224 | Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。 225 | > * **释放连接TCP连接** 226 | 若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求; 227 | > * **客户端浏览器解析HTML内容** 228 | 客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。 229 | 230 | ## [HTTP2](https://www.cnblogs.com/heluan/p/8620312.html) 231 | * 新的二进制格式(Binary Format) 232 | * HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。 233 | * 多路复用(MultiPlexing) 234 | * 即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。 235 | * pipelining在接收response返回时,必须依顺序接收,如果前一个请求遇到了阻塞,后面的请求即使已经处理完毕了,仍然需要等待阻塞的请求处理完毕。这种情况就如图中第三种,第一个请求阻塞后,后面的请求都需要等待,这也就是队头阻塞 236 | * header压缩 237 | * 对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小 238 | * 服务端推送 239 | * 我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了 240 | 241 | ## HTTP持久连接与管线化 242 | HTTP协议首先要和服务器建立TCP连接,这需要三次握手。 243 | 244 | * 请求一个万维网文档的时间 245 | * 当建立TCP连接的三次握手前两次完成后,即经过一个RTT时间,万维网客户就把HTTP请求报文,作为建立TCP连接的三次握手中的第三次的数据,发送给万维网服务器,服务器收到HTTP请求后,把请求的文档作为响应报文返回给客户。 246 | * 文档传输时间+2*RTT 247 | 248 | * HTTP1.0非持久连接的缺点 249 | * 每请求一个文档,需要两倍RTT的开销。服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接,然后重新建立连接发出请求 250 | 251 | * HTTP1.1持久连接 252 | * 万维网服务器在发送响应后仍然在一段时间内保持这段连接,可以使得同一用户继续在该连接上传送后续请求和响应报文 253 | 254 | * 持久连接的两种工作方式 255 | * 非管线化 256 | * 发送请求后需等待并收到回应,才能发送下一个请求 257 | * 管线化 258 | * 不用等待响应,直接发送下一个请求,但接收的时候必须按照顺序接收,如果有一个请求阻塞,则接收会全部阻塞 259 | 260 | 261 | ## HTTP协议请求报文具体信息 262 | HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成 263 | 264 | * **GET** 265 | ```C++ 266 | GET /562f25980001b1b106000338.jpg HTTP/1.1 267 | Host:img.mukewang.com 268 | User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) 269 | AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 270 | Accept:image/webp,image/*,*/*;q=0.8 271 | Referer:http://www.imooc.com/ 272 | Accept-Encoding:gzip, deflate, sdch 273 | Accept-Language:zh-CN,zh;q=0.8 274 | 空行 275 | 请求数据为空 276 | ``` 277 | 278 | * **POST** 279 | ```C++ 280 | POST / HTTP1.1 281 | Host:www.wrox.com 282 | User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022) 283 | Content-Type:application/x-www-form-urlencoded 284 | Content-Length:40 285 | Connection: Keep-Alive 286 | 空行 287 | name=Professional%20Ajax&publisher=Wiley 288 | ``` 289 | 290 | > * **请求行**,用来说明请求类型,要访问的资源以及所使用的HTTP版本. 291 | GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。 292 | > * **请求头部**,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息 293 | * HOST,给出请求资源所在服务器的域名. 294 | * User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等 295 | * Accept,说明用户代理可处理的媒体类型 296 | * Accept-Encoding,说明用户代理支持的内容编码 297 | * Accept-Language,说明用户代理能够处理的自然语言集 298 | * Content-Type,说明实现主体的媒体类型 299 | * Content-Length,说明实现主体的大小 300 | * Connection,连接管理,可以是Keep-Alive或close 301 | > * **空行**,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。 302 | > * **请求数据**也叫主体,可以添加任意的其他数据。 303 | 304 | ## GET和POST区别 305 | ```C++ 306 | GET /books/?sex=man&name=Professional HTTP/1.1 307 | Host: www.wrox.com 308 | User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) 309 | Gecko/20050225 Firefox/1.0.1 310 | Connection: Keep-Alive 311 | 空行 312 | 请求数据为空 313 | ``` 314 | * 区别 315 | * **get参数通过url传递,post放在request body中**,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变 316 | * **POST的安全性要比GET的安全性高**,一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码. 317 | * **get请求在url中传递的参数是有长度限制的,而post没有** 318 | * **GET产生一个TCP数据包,POST产生两个TCP数据包**。 319 | * 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); 320 | * 对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据) 321 | 322 | 323 | ## HTTP协议响应报文具体信息 324 | HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。 325 | ```C++ 326 | HTTP/1.1 200 OK 327 | Date: Fri, 22 May 2009 06:07:21 GMT 328 | Content-Type: text/html; charset=UTF-8 329 | 330 | 331 | 332 | 333 | 334 | 335 | ``` 336 | > * 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。 337 | 第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK 338 | > * 消息报头,用来说明客户端要使用的一些附加信息 339 | 第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8 340 | > * 空行,消息报头后面的空行是必须的 341 | > * 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文 342 | 343 | 344 | ## HTTP状态码 345 | * 1xx:指示信息--表示请求已接收,继续处理。 346 | * 2xx:成功--表示请求正常处理完毕。 347 | * 200 OK:客户端请求被正常处理 348 | * 206 Partial content:客户端进行了范围请求 349 | * 3xx:重定向--要完成请求必须进行更进一步的操作。 350 | * 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一 351 | * 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得 352 | * 4xx:客户端错误--请求有语法错误,服务器无法处理请求。 353 | * 400 Bad Request:请求报文存在语法错误 354 | * 403 Forbidden:请求被服务器拒绝 355 | * 404 Not Found:请求不存在,服务器上找不到请求的资源 356 | * 5xx:服务器端错误--服务器处理请求出错。 357 | * 500 Internal Server Error:服务器在执行请求时出现错误 358 | * 503 Service Unavaliable:服务器正在停机维护 359 | 360 | 361 | ## 浏览器键入URL后的访问流程 362 | * 浏览器使用**DNS协议**向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址,其中DNS服务器是基于UDP的,因此会用到**UDP协议**; 363 | * 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接,会使用到**TCP协议**; 364 | * 然后浏览器就要与服务器建立一个http连接,因此要用到**http协议**,http生成一个get请求报文,如果采用https还会使用**https协议**对http数据进行加密,涉及到**SSL协议**,将报文发送到TCP层 365 | * TCP层如果有需要先将HTTP数据包分片,分片依据MTU和MSS( mtu是网络传输最大报文包,mss是网络传输数据最大值)。 366 | * TCP的数据包然后会发送给IP层,用到**IP协议**。IP层通过路由选路,一跳一跳发送到目的地址。 367 | * 当然在一个网段内的寻址是通过以太网协议实现,**以太网协议**需要知道目的IP地址的物理地址,则需要**ARP协议**。 368 | * 服务器端接收到请求,然后发送返回响应请求 369 | * 释放 TCP连接(若connection为close,则释放TCP连接,若为keep-alive则不会释放); 370 | * 浏览器将该解析html文本并显示内容  371 | 372 | ## IP/TCP/UDP分片 373 | 数据发送时,将数据从应用层->传输层->网络层->数据链路层,其中传输层是TCP和UDP,网络层是IP协议。 374 | 375 | * MTU以太网帧的长度为1500字节,所能接收的传输层数据段最大为 1480 个字节(以太网帧中的数据包括 IP 协议的报头信息,IP 协议的报头信息为 20 字节) 376 | * 在计算 MSS (网络传输数据最大值)的时候,用 MTU 减去网络层报头长度以及传输层报头长度即可。 377 | * UDP 378 | * 一旦 UDP 携带的数据超过了 1472 (MTU - IP报头 - UDP报头 = 1500 - 20 - 8),那么在 IP 层就会对该数据分片,一旦分片就意味着增加了 UDP 传输丢包的可能性。 由于 UDP 协议传输本身就不负责可靠性,再加上分片,那么丢包的可能性就大大增加 379 | 380 |
381 | 382 | 383 | ## 对称密钥和公钥密码体制 384 | * 对称密钥密码体制 385 | * 加密密码和解密密码是相同的密钥 386 | * 公钥密码体制,非对称加密 387 | * 加密密码和解密密码不同 388 | * 加密和解密算法都是公开的,加密密钥也是公开的,解密密钥是保密的 389 | * 公钥和私钥是配对关系,公钥加密就用私钥解密,反之亦然 390 | 391 | ## [数字签名和数字证书](https://juejin.im/post/5b8f3190e51d4538a67abfad) 392 | 使用公钥密码加密的一般流程:通过A的公钥对报文加密,发送给B,然后B拿A的私钥进行解密,得到报文. 393 | **注意:并不是每次传输报文的时候都要加数字签名,数字签名一般用于数字证书的验证,这样的话浏览器内置的CA拥有服务端的公钥和私钥。** 394 | 395 | * 数字签名 396 | * 普通数字签名(**能核实发送者,但无法保证报文完整性**) 397 | * A通过A的私钥对报文进行加密,将其附在报文的后面,发送给B,然后B拿A的公钥对附加信息进行解密的过程,为数字签名 398 | * 上述过程中仅仅实现了数字签名,但并没有对实际报文进行加密。实际操作时,可以通过A-->A私钥(数字签名)-->B公钥(报文加密)-->B私钥(报文解密)-->A公钥(验证数字签名) 399 | 400 | * 密码散列函数 401 | * 使用密码散列函数对报文进行与运算得到hash值,简称摘要 402 | * 密码散列函数有MD5和安全散列算法SHA 403 | 404 | * 报文摘要数字签名(**核实发送者,保证报文完整性**) 405 | 对报文本身加密可能是个耗时过程,比如这封Email足够大,那么私钥加密整个文件以及拿到文件后的解密无疑是巨大的开销 406 | * A先对这封Email执行哈希运算得到hash值简称“摘要”,取名h1 407 | * 然后用自己私钥对摘要加密,生成的东西叫“数字签名” 408 | * 把数字签名加在Email正文后面,一起发送给B 409 | * 防止邮件被窃听你可以用继续B公钥加密 410 | * B收到邮件后使用B私钥对报文解密,用A的公钥对数字签名解密,成功则代表Email确实来自A,失败说明有人冒充 411 | * B对邮件正文执行哈希运算得到hash值,取名h2 412 | * B会对比数字签名的hash值h1和自己运算得到的h2,一致则说明邮件未被篡改。 413 | 414 | * 数字签名的作用 415 | * 确认核实发送者 416 | * 保证报文的完整性 417 | * 一般用于验证数字证书 418 | 419 | 420 | * 数字证书 421 | 明文和数字签名共同组成了数字证书,这样一份数字证书就可以颁发给网站了,由认证中心(CA)或者认证中心的下级认证中心颁发。通俗来说,A确认收到的公钥真的是B的公钥,而不是别人伪造的 422 | * 制作数字签名 423 | * CA拥有非对称加密的私钥和公钥。 424 | * CA对证书明文信息进行hash。 425 | * 对hash后的值用私钥加密,得到数字签名 426 | * 明文和数字签名共同组成了数字证书 427 | * 数字证书验证过程 428 | * 拿到证书,得到明文T,数字签名S。 429 | * 用CA机构的公钥对S解密,得到S’。(由于是浏览器信任的机构,浏览器保有它的公钥,操作系统、浏览器本身会预装一些它们信任的根证书,如果其中有该CA机构的根证书,那就可以拿到它对应的可信公钥) 430 | * 用证书里说明的hash算法对明文T进行hash得到T’。 431 | * 比较S’是否等于T’,等于则表明证书可信。 432 | 433 | ## HTTP和HTTPS的区别 434 | > * HTTP协议是以明文的方式在网络中传输数据,而HTTPS协议传输的数据则是经过TLS加密后的,HTTPS具有更高的安全性 435 | > * HTTPS可以保证报文完整性,另外可以核实发送者身份 436 | > * HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书 437 | > * HTTPS在TCP三次握手阶段之后,还需要进行SSL的handshake,协商加密使用的对称加密密钥 438 | > * HTTP协议端口是80,HTTPS协议端口是443 439 | 440 | ## 运输层安全协议及SSL工作过程 441 | * SSL 安全套接字层协议 442 | * TLS 运输层安全协议,在SSL的基础上设计 443 | * SSL工作过程 444 | * 协商加密算法 445 | * 浏览器A向服务器B发送SSL版本,及自身支持的加密组件(包括加密算法及密钥长度等) 446 | * B从中选择自身支持的加密组件和SSL版本,发送给A 447 | * 服务器鉴别 448 | * B向A发送包含公开密钥的数字证书 449 | * A对数字证书进行鉴别,获取B的公钥 450 | * 会话密钥计算 451 | * A随机产生秘密数,将秘密数通过B的公钥发送给B,之后A通过协商的加密算法产生会话密钥 452 | * B接收到秘密数后,通过B的私钥将其解密得到秘密数,然后根据协商加密算法产生会话密钥 453 | * 安全数据传输 454 | * 双方会互相发送一次数据,用会话密钥加密和解密他们之间传达的数据并验证其完整性 455 | * 通信 456 | * 上述验证通过后,才继续进行http通信 457 | 458 | ## HTTPS必须在每次请求中都要先在SSL/TLS层进行握手传输密钥吗? 459 | 显然每次请求都经历一次密钥传输过程非常耗时,那怎么达到只传输一次呢?用session就行。 460 | 461 | * 服务器会为每个浏览器(或客户端软件)维护一个session ID,在TSL握手阶段传给浏览器,浏览器生成好密钥传给服务器后,服务器会把该密钥存到相应的session ID下 462 | * 之后浏览器每次请求都会携带session ID,服务器会根据session ID找到相应的密钥并进行解密加密操作,这样就不必要每次重新制作、传输密钥了! 463 | 464 | ## cookie和session 465 | HTTP协议作为无状态协议,对于HTTP协议而言,无状态指每次request请求之前是相互独立的,当前请求并不会记录它的上一次请求信息,如何将上下文请求进行关联呢?**客户端(不同的浏览器)记录用户的状态通过cookie,服务器端(不同的网站)记录用户的状态通过session。** 466 | 467 | ### cookie 468 | #### **工作流程** 469 | * 客户端请求服务器端,服务器端产生cookie响应头,随响应报文发送给客户端,客户端将cookie文本保存起来 470 | * 下次客户端再次请求服务端时,会产生cookie请求头,将之前服务器发送的cookie信息,再发送给服务器,服务器就可以根据cookie信息跟踪客户端的状态。 471 | 472 | #### **基础知识** 473 | Cookie总是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie和硬盘Cookie,它是服务器端存放在本地机器中的数据,随每一个请求发送给服务器,由于Cookie在客户端,所以可以编辑伪造,不是十分安全。 474 | 475 | * 非持久cookie 476 | * 内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。 477 | * 持久cookie 478 | * 硬盘Cookie保存在硬盘里,有一个过期时间(客户端cookie设置的时间),除非用户手工清理或到了过期时间,硬盘Cookie不会被删除,其存在时间是长期的。 479 | 480 | 481 | ### session 482 | #### **工作流程** 483 | * 当用户第一次访问站点时,服务器端为用户创建一个sessionID,这就是针对这个用户的唯一标识,每一个访问的用户都会得到一个自己独有的session ID,这个session ID会存放在响应头里的cookie中,之后发送给客户端。这样客户端就会拥有一个该站点给他的session ID。 484 | * 当用户第二次访问该站点时,浏览器会带着本地存放的cookie(里面存有上次得到的session ID)随着请求一起发送到服务器,服务端接到请求后会检测是否有session ID,如果有就会找到响应的session文件,把其中的信息读取出来;如果没有就跟第一次一样再创建个新的。 485 | 486 | #### **基础知识** 487 | session是存放在服务器里的,所以session 里的东西不断增加会增加服务器的负担,我们会把一些重要的东西放在session里,不太重要的放在客户端cookie里 488 | 489 | * session失效 490 | * 服务器(非正常)关闭时 491 | * session过期/失效(默认30分钟) 492 | * 问题:时间的起算点 从何时开始计算30分钟?从不操作服务器端的资源开始计时(例如:当你访问淘宝页面时,点开页面不动,第29分钟再动一下页面,就得重新计时30分钟;当过了30分钟,就失效了。) 493 | * 手动销毁session 494 | 495 | * sessionID的传递方式 496 | * 通过cookie传递 497 | * 当cookie禁用后,可以通过url传递 498 | * 不同场景下的session 499 | * 当在同一个浏览器中同时打开多个标签,发送同一个请求或不同的请求,仍是同一个session; 500 | * 当不在同一个窗口中打开相同的浏览器时(打开多个相同的浏览器),发送请求,仍是同一个session; 501 | * 当使用不同的浏览器时,发送请求,即使发送相同的请求,是不同的session; 502 | * 当把当前某个浏览器的窗口全关闭,再打开,发起相同的请求时,是不同的session。 503 | 504 | ### 区别 505 | * cookie数据存放在客户的浏览器上,session数据放在服务器上。 506 | * cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。 507 | * session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。 508 | * 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。 509 | * 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。 510 | 511 | ## [浏览器关闭后,session就销毁了吗?](https://blog.csdn.net/qq1012421396/article/details/70842148) 512 | 浏览器关闭和服务器session销毁没有任何关系,会话Cookie(非持久cookie)在关闭浏览器后就会消失,但是原来服务器的Session还在,只有等到了销毁的时间会自动销毁。 513 | -------------------------------------------------------------------------------- /计算机基础知识/操作系统/README.md: -------------------------------------------------------------------------------- 1 | # 操作系统 2 | 3 | > * [程序的局部性原理](#程序的局部性原理) 4 | > * [跨平台技术实现原理](#跨平台技术实现原理) 5 | > * [库函数和系统调用的区别](#库函数和系统调用的区别) 6 | > * [并行和并发](#并行和并发) 7 | > * [计算密集任务和IO密集任务](#计算密集任务和IO密集任务) 8 | > * [单核CPU/多核CPU/多CPU](#单核CPU多核CPU多CPU) 9 | > * [什么时候使用多线程和多进程](#什么时候使用多线程和多进程) 10 | > * [进程与PCB](#进程与PCB) 11 | > * [fork、v_fork、clone](#forkvforkclone) 12 | > * [进程间通信](#进程间通信) 13 | > * [共享内存底层原理](#共享内存底层原理) 14 | > * [进程异常退出共享内存会销毁吗](#进程异常退出共享内存会销毁吗) 15 | > * [进程调度算法](#进程调度算法) 16 | > * [进程调度时机](#进程调度时机) 17 | > * [僵尸进程和孤儿进程](#僵尸进程和孤儿进程) 18 | > * [进程状态转移](#进程状态转移) 19 | > * [进程间共享和私有资源](#进程间共享和私有资源) 20 | > * [线程](#线程) 21 | > * [协程](#协程) 22 | > * [线程间同步及系统调用](#线程间同步及系统调用) 23 | > * [线程间共享和私有资源](#线程间共享和私有资源) 24 | > * [进程与线程的区别](#进程与线程的区别) 25 | > * [用户态和内核态](#用户态和内核态) 26 | > * [什么是虚拟内存](#什么是虚拟内存) 27 | > * [缺页中断](#缺页中断) 28 | > * [虚拟内存置换算法](#虚拟内存置换算法) 29 | > * [虚拟内存页表寻址](#虚拟内存页表寻址) 30 | > * [说一下LINUX系统中的锁](#说一下LINUX系统中的锁) 31 | > * [自旋锁发生死锁](#自旋锁发生死锁) 32 | > * [死锁产生的条件](#死锁产生的条件) 33 | > * [如何避免死锁](#如何避免死锁) 34 | > * [死锁检测和死锁恢复](#死锁检测和死锁恢复) 35 | > * [信号处理机制](#信号处理机制) 36 | > * [哪两个信号不能忽略](#哪两个信号不能忽略) 37 | > * [原子操作和锁机制](#原子操作和锁机制) 38 | 39 | ## 程序的局部性原理 40 | ### 基本概念 41 | 程序倾向于引用临近于其他最近引用过的数据项的数据项,或最近引用过的数据项本身,这种倾向性被称为局部性原理。 42 | 43 | * 时间局部性 44 | * 良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用 45 | * 空间局部性 46 | * 良好空间局部性的程序中,一个内存位置被引用,程序很可能在不远的将来引用其附近的一个内存位置 47 | 48 | ### 从硬件和操作系统层面看如何利用局部性 49 | * 硬件层 50 | * 局部性原理允许硬件引入高速缓存存储器这种小而快速的存储器来存储最近被引用的指令和数据,从而提高对主存的访问速度 51 | * 操作系统 52 | * 允许系统使用主存作为虚拟地址空间作为最近被引用块的高速缓存 53 | 54 | ### [从存储结构看如何利用局部性](https://www.jianshu.com/p/5c9b28c95c64) 55 | 存储器层次结构的中心思想是,对于每个 k,位于 k 层的更快更小的存储设备作为位于 k + 1 层的更大更慢的存储设备的缓存。 56 | 57 | * 时间局部性 58 | * 同一数据对象可能被多次使用。一旦一个数据对象在第一次不命中时被复制到缓存中,我们就会期望后面对目标有一系列的访问命中。因为缓存比低一层的存储设备更快,对后面的命中的服务会比最开始的不命中的快很多。 59 | * 空间局部性 60 | * 块通常包含多个数据对象。我们会期望后面对该块中其他对象的访问能补偿不命中后复制该块的花费。 61 | 62 | 63 | ## [库函数和系统调用的区别](https://www.cnblogs.com/liwei0526vip/p/8998751.html) 64 | ### 概念 65 | * 库函数调用是语言或应用程序的一部分,而系统调用是操作系统的一部分,跨平台技术的原理就是通过库函数实现的,库函数可以理解为是对系统调用的一层封装,但库函数不是必须包含系统调用。 66 | * 库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。 67 | 68 | ### 区别 69 | > * 所有 C 函数库是相同的,而各个操作系统的系统调用是不同的。 70 | > * 函数库调用是调用函数库中的一个程序,而系统调用是调用系统内核的服务。 71 | > * 函数库调用是与用户程序相联系,而系统调用是操作系统的一个进入点 72 | > * 函数库调用是在用户地址空间执行,而系统调用是在内核地址空间执行 73 | > * 函数库调用的运行时间属于「用户」时间,而系统调用的运行时间属于「系统」时间 74 | > * 函数库调用属于过程调用,开销较小,而系统调用需要切换到内核上下文环境然后切换回来,开销较大 75 | > * 在C函数库libc中大约 300 个程序,在 UNIX 中大约有 90 个系统调用 76 | > * 函数库典型的 C 函数:system, fprintf, malloc,而典型的系统调用:chdir, fork, write, brk 77 | 78 | ### 为什么不直接用函数调用 79 | * 因为读写文件通常是大量的数据(相对于底层驱动的系统调用所实现的数据操作单位),这时,使用库函数可以大大减少系统调用的次数。这是因为**缓冲区技术**,在用户空间和内核空间对文件操作都使用了缓冲区。当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓存区。同理,内核缓冲区满或写结束时,才将内核缓冲区内容写到文件对应的硬件媒介。 80 | * 为了保证可移植性 81 | 82 | ### [库函数的缓冲区](https://blog.csdn.net/it_liuwei/article/details/45022671) 83 | * 对于库函数,如果标准输出连到终端设备(直接输出到屏幕),则它是行缓冲的(遇到回车换行符或者是缓冲区满了才输出);否则(输出到文件)是全缓冲的(缓冲区填满或者是程序运行结束了才输出)。 84 | * 程序运行结束时,会刷新所有的缓冲区。 85 | 86 | 由于上面的缓冲机制,也给我们编写程序时带来了一些奇怪的问题。解决办法有如下两种: 87 | 88 | * 任何时候我们都可以使用fflush(stdout)来刷新标准输出缓冲区。 89 | * 使用不带缓冲的系统调用write替代printf输出。 90 | 91 | ### 系统调用底层原理 92 | * 每个系统调用函数都有一个系统调用号 93 | * 首先找到系统调用对应的中断号(Linux下是int 0x80),然后在中断向量表中找到对应的中断处理函数,再根据系统调用号,在中断处理函数找到对应系统调用函数进行执行。 94 | 95 | ## [跨平台技术实现原理](https://segmentfault.com/q/1010000005178192) 96 | 现有跨平台技术就是通过库函数调用实现的,不使用系统函数调用。 97 | 98 | * Qt如何识别不同系统 99 | * Qt各个操作系统都有特定的宏,然后代码里面根据不同的宏调用不同平台的API 100 | 101 | 102 | ## 并行和并发 103 | * 并发 104 | * 在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。 105 | 106 | * 并行 107 | * 在同一时刻,有多条指令在多个处理器上同时执行。 108 | 109 | ## 计算密集任务和IO密集任务 110 | * 计算密集型任务 111 | * 特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。 112 | * 虽然可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。 113 | 114 | * IO密集型任务 115 | * 涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。 116 | * 对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。 117 | 118 | ## [单核CPU/多核CPU/多CPU](https://www.cnblogs.com/csfeng/p/8670704.html) 119 | 120 | > * 都是一个CPU,不同的是每个CPU上的核心数。 121 | > * 多核CPU是多个CPU的替代方案,同时也减少了功耗。 122 | > * 一个核心只能同时执行一个线程。 123 | 124 | * 单核CPU 125 | * 一个CPU中只有一个核心处理器 126 | * 多核CPU 127 | * 一个CPU有多个核心处理器,处理器之间通过**CPU内部总线**进行通讯 128 | * 多CPU 129 | * 简单的多个CPU工作在同一个系统上,多个CPU之间通过**主板上的总线**进行通讯 130 | 131 | ### 深入理解进程和线程 132 | * 进程的调度和资源分配是操作系统负责 133 | 134 | * 线程的调度和资源分配是CPU负责 135 | 136 | * 进程是操作系统资源分配(包括cpu、内存、磁盘IO等)的基本单位,一个CPU同时刻只能执行一个进程 137 | * **单核CPU实现多进程,并发。** 通过操作系统的进程调度算法,单核CPU进行进程调度的时候,需要读取上下文+执行程序+保存上下文,即进程切换。 138 | * **多CPU实现多进程,并行。** 不同的进程运行在不同的CPU上。 139 | 140 | * 线程是CPU调度和资源分配的基本单位,一个CPU核心同时刻只能执行一个线程 141 | * **单核CPU实现多线程,并发。** 不同线程为了使用CPU核心,则会进行线程切换,但是由于共享了程序执行环境,这个线程切换比进程切换开销少了很多。 142 | * **多核CPU实现多线程,并行。** CPU可以将不同线程分配到不同的CPU核心处理器中。 143 | 144 | > * 单CPU中进程只能是并发,多CPU计算机中进程可以并行。 145 | > * 单CPU单核中线程只能并发,单CPU多核中线程可以并行。 146 | > * 并行有上限,进程与CPU个数,线程与CPU核心个数有关,并不是所有线程和所有进程都能同时运行 147 | 148 | ## 什么时候使用多进程和多线程 149 | * 多核CPU——计算密集型任务。此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置. 150 | * 单核CPU——计算密集型任务。此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作。 151 | * 单核CPU——IO密集型任务,使用多线程还是为了人机交互方便. 152 | * 多核CPU——IO密集型任务,这就更不用说了,跟单核时候原因一样。 153 | 154 | ## [进程与PCB](https://blog.csdn.net/lvyibin890/article/details/82193900) 155 | ### 进程 156 | * 进程是操作系统的资源分配单位,实现操作系统的并发,对于一个进程,它在被执行前其实是一个可执行程序。这个程序是被放在磁盘上的,当它要被执行的时候,它先被加载到内存当中,然后再放入到寄存器中,最后再让cpu执行该程序,这个时候一个静态的程序就变成了进程 157 | * 进程创建时会分配4G的内存,其中0-3G是用户空间,3-4G是内核空间,PCB存在于内核空间 158 | * 进程的用户空间是不同的,内核空间也是不同的。比如每个进程的不同系统调用,是陷入自己独立的内核空间里面,所以每个进程内核的堆栈肯定是不一样的 159 | 160 | 161 | ### PCB 162 | * 每个进程的PCB都是存在所有进程共享的内核空间中,操作系统管理进程,也就是在内核空间中管理的,在内核空间中通过链表管理所有进程的PCB,如果有一个进程要被创建,实际上多分配了这么一个4G的虚拟内存,并在共享的内核空间中的双向链表中加入了自己的PCB。 163 | * PCB(Process Control Block)进程控制块,描述进程的基本信息和运行状态,**进程的创建和销毁都是对PCB进行操作**,PCB的具体内容如下 164 | * 标识相关:pid,ppid等等 165 | * 文件相关:进程需要记录打开的文件信息,于是需要文件描述符表 166 | * 内存相关:内存指针,指向进程的虚拟地址空间(用户空间)信息 167 | * 优先级相关:进程相对于其他进程的调度优先级 168 | * 上下文信息相关:CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。 169 | * 状态相关:进程当前的状态,说明该进程处于什么状态 170 | * 信号相关:进程的信号处理函数,以及记录当前进程是否还有待处理的信号 171 | * I/O相关:记录进程与各种I/O设备之间的交互 172 | * 每个进程的内核空间中都有PCB,但真正的PCB是存储在物理内存上的,当进程创建和销毁时,会由操作系统操作PCB,每个进程只是虚拟地址空间,并不会存储实际数据,数据存储在物理内存中,只有一份。 173 | 174 | 175 | ## 进程间同步 176 | 信号量 177 | 178 | ## [forkvforkclone](https://blog.csdn.net/gogokongyin/article/details/51178257) 179 | fork、v_fork、clone底层都是do_fork,追踪发现底层使用的是sys_clone 180 | 181 | * fork 182 | * 父进程fork之后创建子进程,子进程复制父进程的所有资源,子进程的代码段、数据段、堆栈都是指向父进程的物理空间,但此时仅仅是子进程的虚拟地址空间和父进程指向的物理地址空间建立了映射关系,并没有真正复制 183 | * 由于fork()后会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux中引入了“写时复制技术-Copy-On-Write” 184 | * 若两个进程一直只是读数据,则子进程一直不会复制,直到任一进程进行写操作 185 | * 父进程和子进程执行顺序没有规定,可以乱序执行 186 | * 读时共享,写时复制 187 | * vfork 188 | * vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。 189 | * 规定必须子进程先执行 190 | * 严格意义上讲,vfork产生的不叫进程,因为他没有独立的地址空间,和父进程共享同一个 191 | 192 | ## 进程间通信 193 | 进程间通信主要包括管道、系统IPC(包括消息队列、信号、共享内存等)、本地套接字socket。 194 | 195 | * 管道(缓冲区有限) 196 | * 无名管道PIPE 197 | * 一种半双工的通信方式,只能在具有亲缘关系的进程间使用(父子进程或兄弟进程) 198 | * 有名管道FIFO 199 | * 一种半双工的通信方式,可以在非亲缘关系的进程间使用 200 | * 消息队列 201 | * 消息队列是消息的链接表,存放在内核中并由消息队列标识符标识 202 | * 消息队列克服了信号传递信息少,管道缓冲区大小受限的缺点 203 | * 一个消息队列由一个标识符(即队列ID)来标记 204 | * 信号 205 | * 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 206 | * [共享内存](https://blog.csdn.net/hj605635529/article/details/73163513) 207 | * 它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。多个进程可以同时操作,所以需要进行同步 ,一般与信号量配合使用。 208 | * shm 209 | * mmap 210 | * 套接字 211 | * 本地套接字用于本机不同进程间通信,另外普通套接字可以用于不同主机间的进程间通信 212 | 213 | ## [共享内存底层原理](https://blog.csdn.net/joejames/article/details/37958017) 214 | 共享内存有两个,一个mmap,一个systemV的shmget 215 | 216 | * 虚拟内存在硬盘上 217 | 218 | ### 不同进程如何访问共享内存? 219 | 220 | ## [进程异常退出共享内存会销毁吗](https://blog.csdn.net/brucexu1978/article/details/7728717) 221 | 222 | ## 进程调度算法 223 | ### 批处理系统 224 | 批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 225 | 226 | * 先来先服务 first-come first-serverd(FCFS) 227 | > * 非抢占式的调度算法,按照请求的顺序进行调度。 228 | > * 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 229 | 230 | * 短作业优先 shortest job first(SJF) 231 | > * 非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 232 | > * 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 233 | 234 | * 最短剩余时间优先 shortest remaining time next(SRTN) 235 | > * 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 236 | > * 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 237 | 238 | ### 交互式系统 239 | 交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。 240 | 241 | * 时间片轮转 242 | > * 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。 243 | > * 当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 244 | 245 | * 优先级调度 246 | > * 为每个进程分配一个优先级,按优先级进行调度。 247 | > * 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 248 | 249 | * 多级反馈队列 250 | > * 一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。 251 | > * 多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 252 | > * 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 253 | > * 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。 254 | 255 | ### 实时系统 256 | * 实时系统要求一个请求在一个确定时间内得到响应。 257 | * 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 258 | 259 | ## 进程调度时机 260 | > * 进程状态转换的时刻:进程终止、进程睡眠; 261 | > * 当前进程的时间片用完时(current->counter=0); 262 | > * 设备驱动程序 263 | > * 进程从中断、异常及系统调用返回到用户态时; 264 | 265 | ## 僵尸进程和孤儿进程 266 | * 当父进程先结束,子进程此时就会变成孤儿进程,孤儿进程会自动向上被init进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素。 267 | * 如果子进程先结束,父进程并未调用wait或者waitpid获取进程状态信息,回收进程资源,那么子进程描述符就会一直保存在系统中,这种进程称为僵尸进程。 268 | * 僵尸进程是每个子进程退出时必然经历的过程 269 | * 僵尸进程的危害 270 | * 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 271 | * 如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。 272 | * 如何消除僵尸进程 273 | * kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管 274 | * 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。 275 | 276 | ## 进程状态转移 277 | * 就绪状态(ready):等待被调度 278 | * 运行状态(running) 279 | * 阻塞状态(waiting):等待资源 280 | > * 就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 281 | > * 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 282 | 283 | 284 | ## 进程间共享和私有资源 285 | * 私有:地址空间、堆、全局变量、栈、寄存器(0-3G的用户空间) 286 | * 共享:3-4G的内核空间 287 | 288 | ## 线程 289 | 线程是CPU调度的基本单位 290 | 291 | ## [协程](https://blog.csdn.net/pinganting/article/details/53750142) 292 | [协程学习笔记](https://blog.csdn.net/somezz/article/details/81265198) 293 | ### 协程概述 294 | * 协程是轻量级线程,拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。 295 | * 协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。 296 | 297 | ### 协程和线程的区别 298 | * 协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。 299 | * 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 300 | 301 | ### 应用场景 302 | 303 | * I/O 密集型任务。 304 | 305 | > * 这一点与多线程有些类似,但协程调用是在一个线程内进行的,是单线程,切换的开销小,因此效率上略高于多线程。 306 | > * 当程序在执行 I/O 时操作时,CPU 是空闲的,此时可以充分利用 CPU 的时间片来处理其他任务。在单线程中,一个函数调用,一般是从函数的第一行代码开始执行,结束于 return 语句、异常或者函数执行(也可以认为是隐式地返回了 None )。 307 | > * 有了协程,我们在函数的执行过程中,如果遇到了耗时的 I/O 操作,函数可以临时让出控制权,让 CPU 执行其他函数,等 I/O 操作执行完毕以后再收回控制权。 308 | 309 | ## 线程间同步及系统调用 310 | ### 信号量 311 | 信号量是一种特殊的变量,可用于线程同步。它只取自然数值,并且只支持两种操作: 312 | > * P(SV):如果信号量SV大于0,将它减一;如果SV值为0,则挂起该线程。 313 | > * V(SV):如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。 314 | 315 | * 系统调用 316 | * sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。 317 | * sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。 318 | 319 | 320 | ### 互斥量 321 | 322 | 互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区 时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。 323 | 324 | * 系统调用 325 | * pthread_mutex_init:初始化互斥锁 326 | * pthread_mutex_destroy:销毁互斥锁 327 | * pthread_mutex_lock:以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。 328 | * pthread_mutex_unlock:以一个原子操作的方式给一个互斥锁解锁。 329 | 330 | 331 | ### 条件变量 332 | 333 | 条件变量,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用 signal/broadcast。此时操作共享变量时需要加锁。 334 | 335 | * 系统调用 336 | * pthread_cond_init:初始化条件变量 337 | * pthread_cond_destroy:销毁条件变量 338 | * pthread_cond_signal:唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。 339 | * pthread_cond_wait:等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。 340 | 341 | ## [线程间共享和私有资源](https://www.cnblogs.com/Lxk0825/p/9559070.html) 342 | * 私有:**线程栈,寄存器,程序寄存器**,线程ID,错误返回码,信号屏蔽字,调度优先级 343 | * 共享:文件描述符表,堆,地址空间,全局变量,静态变量,进程代码段,进程的当前目录和进程用户ID与进程组ID 344 | 345 | ## 进程与线程的区别 346 | > * 进程是cpu资源分配的最小单位,线程是cpu调度的最小单位。 347 | > * 进程有独立的系统资源或地址空间,而同一进程内的线程共享进程的大部分系统资源,包括堆、代码段、数据段,每个线程只拥有一些在运行中必不可少的私有属性,比如线程Id,栈、寄存器、程序计数器PC(或者说IP)。 348 | > * 一个进程崩溃,不会对其他进程产生影响;而一个线程崩溃,会让同一进程内的其他线程也宕掉。 349 | > * 进程在创建、销毁时开销比较大,而线程比较小。进程创建的时候需要分配虚拟地址空间等系统资源,而销毁的的时候需要释放系统资源;线程只需要创建栈,栈指针,程序计数器,通用目的寄存器和条件码等,不需要创建独立的虚拟地址空间。 350 | > * 进程切换开销比较打,线程比较小。进程切换需要分两步:切换页目录、刷新TLB以使用新的地址空间;切换内核栈和硬件上下文(寄存器);而同一进程的线程间逻辑地址空间是一样的,不需要切换页目录、刷新TLB。 351 | > * 进程间通信比较复杂,而同一进程的线程由于共享代码段和数据段,所以通信比较容易。 352 | 353 | ### [TLB](https://www.cnblogs.com/linhaostudy/p/7771437.html) 354 | TLB( Translation Look- aside buffer)专门用于缓存内存中的页表项,一般在MMU单元内部,页表一般存储在屋里内存中。当处理器要访问一个虚拟地址时,首先会在TLB中查询。如果TLB表项中没有相应的表项,称为TLB Miss,那么就需要访问页表来计算出相应的物理地址。如果TLB表项中有相应的表项,那么直接从TLB表项中获取物理地址,称为TLB命中。 355 | 356 | ### 程序计数器PC和指令指针寄存器IP(http://blog.sina.com.cn/s/blog_5ede281a0100sn4w.html) 357 | 358 | * 程序计数器PC 359 | * 用指令事先编好的程序连续存放在内存程序区中,靠地址+1的方法连续取指执行”。在八位机8080CPU中是采用先取指后执行的串行操作的原理,而其中执行地址+1指令寻址的部件就是程序计数器PC。那么在程序的执行过程中,PC始终是指向下一条要执行的指令。 360 | * 结论:PC中的地址就是需要转移、循环、调用子程序和中断子程序等操作时的断点。 361 | * 指令指针寄存器IP 362 | * 在向上兼容的十六位机8086CPU中首先分为两个功能部件,即总线接口部件BIU和执行部件EU,BIU负责取指令,EU负责译码执行。并且当BIU执行指令排队栈中的六个字节装满后,(8088CPU是4个字节),EU开始从指令排队栈的出栈口,取指令进行译码执行,同时BIU并行操作向入栈口补充一条取指令命令。 363 | * 指令指针IP则是指向下个条要取指的指令,而不是EU要执行的指令。而断点则应该是要执行的指令内存地址,而不是IP内的下一条要取指的指令地址。 364 | * PC是模型机中的概念,IP是实际使用的,调试时我们发现,IP实现的就是PC的功能。 365 | 366 | ### 为什么有了进程还需要线程? 367 | * 优点 368 | * 进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量. 369 | * 缺点 370 | * 进程在同一时间只能干一件事 371 | * 进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。 372 | 373 | 因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性 374 | 375 | ## 用户态和内核态 376 | ### 概念 377 | > * 用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。 378 | > * 用户态拥有最低的特权级,内核态拥有较高的特权级。 379 | > * 运行在用户态的程序不能直接访问操作系统内核数据结构和程序 380 | > * 操作系统的数据都是存放于系统空间的,用户进程的数据是存放于用户空间的。 381 | * 分开来存放,就让系统的数据和用户的数据互不干扰,保证系统的稳定性。 382 | * 分开存放,管理上很方便,而更重要的是,将用户的数据和系统的数据隔离开,就可以对两部分的数据的访问进行控制。这样就可以确保用户程序不能随便操作系统的数据,这样防止用户程序误操作或者是恶意破坏系统。 383 | 384 | ### [用户态和内核态可以通过指针传递数据吗?](http://blog.chinaunix.net/uid-26611973-id-3190018.html) 385 | * 用户态不能访问内核态的指针 386 | * 为了实现内存的保护,防止越界访问而造成受保护内存的被非法修改,甚至造成系统的崩溃,这种直接传递数据指针来传递数据的方式是被禁止的。 387 | * 内核态可以访问用户态的指针(有前提) 388 | * 必须保证用户态虚拟空间的指针(虚拟空间的地址),已经分配物理地址,否则指针传入内核态中将不会引发缺页异常而报错 389 | * [内核中访问用户进程的地址的时候用copy_from_user,而不是用memcpy直接拷贝(或者说使用用户态指针)](https://blog.csdn.net/u014089131/article/details/56272892) 390 | * copy_from_user主要是这个函数提供了两个功能 391 | * 对用户进程传过来的地址范围进行合法性检查; 392 | * 当用户传来的地址没有分配物理地址时,定义了缺页处理后的异常发生地址,保证程序顺利执行; 393 | * 对于用户进程访问虚拟地址,如果还未分配物理地址,就会触发内核缺页异常,接着内核会负责分配物理地址,并修改映射页表。这个过程对于用户进程是完全透明的。但是在内核空间发生缺页时,必须显式处理,否则会导致内核出现错误 394 | * 直接使用memcpy时为什么没有出现异常 395 | * 只有用户传来的地址空间没有分配对应的物理地址时才会进行修复,如果用户进程之前已经使用过这段空间,代表已经分配了物理地址,自然不会发生缺页异常。 396 | 397 | ### 两种状态转换 398 | * 系统调用 399 | * 用户进程主动要求切换到内核态的一种方式,用户进程通过系统调用申请操作系统提供的服务程序完成工作 400 | * 异常 401 | * 当CPU在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此异常的内核相关程序中,也就到了内核态,比如缺页异常。 402 | * 外围设备中断 403 | * 当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令,转而去执行中断信号的处理程序 404 | * 比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等 405 | 406 | ## 存储器层次结构 407 | ### 层次结构 408 | 本地磁盘 -> 主存(DRAM) -> L3高速缓存(SRAM) -> L2高速缓存(SRAM) -> L1高速缓存(SRAM) -> L0寄存器 409 | 410 | ### 缓存思想 411 | > * 位于K层的更快更小的存储设备作为位于K+1层更大更慢的存储设备的缓存 412 | > * K+1层的存储器被划分成连续的数据对象组块,称为块,数据总是以块大小为传送单元在K和K+1层之间来回复制 413 | 414 | ### 缓存命中 415 | > * 当程序需要K+1层的某个数据对象d时,首先在当前存储在K层的块中查找d,若d刚好缓存在k层中,则称为缓存命中 416 | > * 若缓存不命中,则需要将K+1层中包含对象d的块缓存到K层中,若K层中满了,则需要替换现存的一个块 417 | 418 | ## 什么是虚拟内存 419 | 为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存,防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。 420 | 421 | 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但**不需要映射到连续的物理内存**,也**不需要所有页都必须在物理内存中**。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 422 | 423 | 虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。 424 | 425 | ### [虚拟内存的好处](https://www.jianshu.com/p/baf3a13c47db) 426 | 427 | * 可以更加高效的使用物理内存 428 | * 虚拟地址空间一开始并没有真正的对应物理地址,而是在真正使用的时候才去对应。 429 | * 通过虚拟内存置换算法在访问后边的地址空间的时候就可以将前边当前没有在访问的物理页释放掉,或者交换到硬盘中。这样这个物理页又可以去对应新的虚拟地址。从而使物理内存可以充分的利用。 430 | * 内存管理 431 | * 为每个进程提供了一致的地址空间,简化内存管理 432 | * 内存保护 433 | * 在使用虚拟地址的时候,暴露给程序员永远都是虚拟地址,而具体的物理地址在哪里,这个只有系统才了解。这样就提高了系统的封装性。 434 | * 保护了每个进程的地址空间不被其他进程破坏 435 | 436 | 437 | ## 虚拟内存页表寻址 438 | ### 分页 439 | 虚拟内存分割成虚拟页,物理内存被分割成物理页,用来作为磁盘和主存的传输单元。 440 | 虚拟页分为三个不相交的子集 441 | > * 未分配的,不占磁盘空间 442 | > * 缓存的,当前已缓存在物理内存中的已分配页,在页表中标志位为1 443 | > * 未缓存的,未缓存在物理内存中的已分配页,在页表中标志位为0 444 | 445 | ### 页表 446 | 内存管理单元(MMU,属于硬件)管理着地址空间和物理内存的转换,操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,存储着程序地址空间到物理内存空间的映射表。 447 | 448 | 页表存放在物理内存中,物理页存放在物理内存中,虚拟页存放在磁盘上 449 | 450 | ### 页表寻址 451 | > * 一个虚拟地址分为两部分,一部分存储页面号,一部分存储偏移量 452 | > * 页表分为序号、页基地址、标志位 453 | > * 访问虚拟地址,先通过页表查询页面号,查看标志位确认虚拟地址是否在物理内存中有缓存,然后由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移虚拟地址中的偏移量就得到最后的物理地址 454 | > * 一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。 455 | 456 | ## 缺页中断 457 | 在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时(缓存不命中),会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。 458 | 459 | 缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤: 460 | > * 保护CPU现场 461 | > * 分析中断原因 462 | > * 转入缺页中断处理程序进行处理 463 | > * 恢复CPU现场,继续执行 464 | 465 | 但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别: 466 | > * 在指令执行期间产生和处理缺页中断信号 467 | > * 一条指令在执行期间,可能产生多次缺页中断 468 | > * 缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。 469 | 470 | ## 虚拟内存置换算法 471 | 当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。 472 | 473 | 当前操作系统最常采用的缺页置换算法如下: 474 | > * 先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。 475 | 476 | > * 最近最少使用(LRU)算法: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。 477 | 478 | 当前最常采用的就是LRU算法。 479 | 480 | ## 说一下LINUX系统中的锁 481 | 互斥锁,读写锁,自旋锁 482 | > * 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。**当获取锁操作失败时,线程会进入睡眠**,等待锁释放时被唤醒 483 | 484 | > * 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它**获取写锁失败的线程都会进入睡眠状态**,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。 485 | 486 | > * 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是**当获取锁操作失败时,不会进入睡眠,而是会在原地自旋**,循环检测锁的保持者是否释放,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。 487 | 488 | ## 自旋锁发生死锁 489 | 490 | ## 死锁产生的条件 491 | 多个并发进程因争夺系统资源而产生相互等待的现象。 492 | 493 | * 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源; 494 | 495 | * 请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源 496 | 497 | * 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放 498 | 499 | * 环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链 ,环路中每个进程都在等待下一个进程所占有的资源 500 | 501 | ## 如何避免死锁 502 | * **破坏请求和等待条件。** 所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源 503 | * **破坏不可抢占条件。** 当进程新的资源未得到满足时,释放已占有的资源 504 | * **破坏环路等待条件。** 系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反 505 | 506 | ## 死锁检测和死锁恢复 507 | * 死锁检测 508 | * 每种类型一个资源的死锁检测 509 | * 每种类型多个资源的死锁检测 510 | * 死锁恢复 511 | * **抢占恢复。** 从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态 512 | * **回滚恢复。** 周期性地检查进程的状态(包括请求的资源),将其写入一个文件,当发生死锁,回滚到之前的某个时间点 513 | * **杀死进程恢复。** 终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。 514 | 515 | ## [信号处理机制](http://www.360doc.com/content/16/0804/10/30953065_580685165.shtml) 516 | 517 | 518 | ## [哪两个信号不能忽略](https://www.cnblogs.com/Lynn-Zhang/p/5677118.html) 519 | SIGKILL和SIGSTOP,这两种信号不能被忽略 520 | 521 | * 它们向超级用户提供一种使进程终止或停止的可靠方法。 522 | * 如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是示定义的。 523 | 524 | ## [原子操作和锁机制](https://blog.csdn.net/CringKong/article/details/79966161) 525 | [原子操作实现同步](https://blog.csdn.net/c472769019/article/details/82663148) 526 | --------------------------------------------------------------------------------