From 70e94f5cc93477a51c1e11ff4e6210b3626bf356 Mon Sep 17 00:00:00 2001 From: jaywcjlove <398188662@qq.com> Date: Tue, 7 May 2024 14:55:44 +0800 Subject: [PATCH] doc: update docs/cpp.md (#589) --- docs/cpp.md | 1226 ++++----------------------------------------------- 1 file changed, 96 insertions(+), 1130 deletions(-) diff --git a/docs/cpp.md b/docs/cpp.md index 32ef458..cea498d 100644 --- a/docs/cpp.md +++ b/docs/cpp.md @@ -718,7 +718,7 @@ std::this_thread::yield(); ``` ### 锁 - + > `#include ` @@ -875,7 +875,7 @@ cond.wait(lock, predicate); 唤醒所有调用 `wait` 的线程。 ### 获取线程的运行结果 - + > `#include ` @@ -962,14 +962,106 @@ else if (status == #### 多个返回值 +如果要多次获取结果,可以使用`std::shared_future`,其会返回结果的一个**拷贝**。 + ```cpp std::shared_future result; ``` -如果要多次获取结果,可以使用`std::shared_future`,其会返回结果的一个**拷贝**。 - 对于不可拷贝对象,可以在`std::shared_future`中存储对象的指针,而非指针本身。 +### 创建线程 + +```cpp +void threadFunction() { + // 线程函数体 + std::cout << "From thread" << std::endl; +} + +int main() { + // 创建线程并开始执行线程函数 + std::thread t(threadFunction); + + // 等待线程执行完毕 + t.join(); + + return 0; +} +``` + +### 传递参数给线程函数 + +```cpp +void threadFunction(int value) { + // 线程函数体 + std::cout << "Received value: " << value << std::endl; +} + +int main() { + int data = 42; + std::thread t(threadFunction, data); + t.join(); + return 0; +} +``` + +### 使用Lambda表达式创建线程 + +```cpp +int main() { + int data = 42; + std::thread t([data]() { + // Lambda 表达式作为线程函数 + std::cout << "Received value: " << data << std::endl; + }); + t.join(); + return 0; +} +``` + +### **处理线程间的同步:** + +```cpp +#include + +std::mutex mtx; + +void threadFunction() { + std::lock_guard lock(mtx); + std::cout << "Thread safe output." << std::endl; +} + +int main() { + std::thread t1(threadFunction); + std::thread t2(threadFunction); + t1.join(); + t2.join(); + return 0; +} +``` + +### **使用`std::async`启动异步任务:** + +```cpp +#include + +int taskFunction() { + // 异步任务 + return 42; +} + +int main() { + // 启动异步任务 + std::future fut = std::async(std::launch::async, taskFunction); + + // 获取异步任务的结果 + int result = fut.get(); + + std::cout << "Result: " << result << std::endl; + return 0; +} +``` + C++ 预处理器 ------------ @@ -1215,1129 +1307,3 @@ char * a = STR(object); #=> char * a = "object"; - [C++ Infographics & Cheat Sheets](https://hackingcpp.com/cpp/cheat_sheets.html) _(hackingcpp.com)_ - [C++ reference](https://zh.cppreference.com/w/) _(cppreference.com)_ - [C++ Language Tutorials](http://www.cplusplus.com/doc/tutorial/) _(cplusplus.com)_ - -# 数据结构与开发技巧 -## map和set -```cpp -#include -#include // 注意map的key会自动排序, 所以在遇到排序问题时参考 -#include -#include -#include -using namespace std; -// map中 所有元素都是pair -// pair中 第一个元素为key(键值) 用于索引 第二个元素value(实值) -// 所有元素都会根据键值自动排序 -// 本质: -// map /mulmap底层都是二叉树 -// 优点: -// 可根据key值快速找到value值 - -// map不允许容器中出现相同的值 -// mulmap中允许出现重复的值2 -// map大小和交换: -// .size() //返回容器中元素的数目 -// .empty() //判断容器是否为空 -// .swap(st) //交换两个容器 -// 插入和删除: -// insert(elem) //容器中插入元素 inseert(pair ( , )); -// clear() //清除所有元素 -// erase(pos) //删除pos迭代器所指的元素 返回下一个迭 代器位置 -// erase(key) 删除键值为key的元素 - -void map_test(){ - // https://blog.csdn.net/tcx1992/article/details/80928790 - // https://blog.csdn.net/sevenjoin/article/details/81937695 - typedef map myMap; // 这其实就是将map里面的数据格式给固定下来而已, map = myMap - myMap test; - //插入 - test.insert(pair(3, "a")); - test.insert(pair(4, "b")); - test.insert(pair(5, "c")); - test.insert(pair(8, "d")); - test.insert(pair(50, "e")); - - //遍历(二叉搜索树的中序遍历,按照key值递增顺序) - cout << "遍历" << endl; - - // for(auto i : test){ // 将temp里面的每个值, 放到i中, 这个i是新建的 - // for(auto &i : test){ // 将temp里面的每个值, 软连接到i, 修改i就是在修改temp中的值 - for(const auto &i : test){ // 将temp里面的每个值, 软连接到i, 禁用修改, 防止在遍历过程中出现改值 - cout << i.second << endl; - cout << endl; - auto iter = test.rbegin();//最大的N个数 - for (int i = 0; i < 3; i++) - cout << iter++->second << endl; - //查找 - cout << "查找" << endl; - // 使用find,返回的是被查找元素的位置,没有则返回map.end()。 - auto it = test.find(50); //查找key=50的数据是, find(key)返回的是pair格式, 也就是(50, e), 所以it->second= - if (it != test.end()) - cout << it->second << endl; - // 使用count,返回的是被查找元素的个数。如果有,返回1;否则,返回0 - cout << test.count(3) << endl; - //删除 - cout << "删除" << endl; - if (test.erase(3)) - cout << "delete success" << endl; - for (auto &i : test) - cout << i.second << endl; -} - -void map_test2(){ - map myMap; // 创建 - myMap.insert(pair(3, "a")); // 插入 - myMap.insert(pair(5, "b")); - myMap.insert(pair(50, "d")); - for (auto &i : myMap) cout <::reverse_iterator iter = myMap.rbegin(); - if (iter != myMap.rend()) cout<<"最后一个值是"<first << "-" << iter->second <first << "-" << myMap.end()->second <second << endl; - - // 查找find - auto it = myMap.find(50); //查找key=50的数据是, find(key)返回的是pair格式, 也就是(50, e), 所以it->second= - if (it != myMap.end()) - cout <first << "-"<second << endl; - - // 判断存在, - cout << "3有" << myMap.count(3) << endl; -} - -int main() -{ - // map_test2(); - unordered_map map1{{1, "hel"}, {2, "ahskg"}, {3, "world"}}; - cout< -#include -#include -#include -#include // 为了使用stringsteam -using namespace std; -// 题目描述: 给定字符串S,T, 求S中包含T所有字符的最短连续子字符串的长度, 时间复杂度不能超过O(n) -// 输入样例: -// Input: S = "ADOBECODEBANC", T = "ABC" -// Output: "BANC" - -string String_test(){ - string str="hello world"; - - //直接打印字符串 - cout<<"str="< - str3 << n1; - string str4 = str3.str(); - cout<<"将int类型转化为string类型: "<> str5; - cout< -#include -#include -#include -using namespace std; - -// 指针和引用 -void test1(int*a, int b, int &c){ - cout<<"函数参数输入是: *a="<<*a<<" b="< -#include -#include -#include -#include // C++引入了ostringstream、istringstream、stringstream这三个类 -/* -istringstream类用于执行C++风格的串流的输入操作。 string str="i am a boy"; 该类可以搞出来一个队列分解这四个单词 -ostringstream类用于执行C风格的串流的输出操作。 -strstream类同时可以支持C风格的串流的输入输出操作。 -*/ - -using namespace std; - -int main(int argc, char const *argv[]) -{ - // 这里打印出来的东西主要是看s的类型, - // 如果s是int, 就打印出12,3,4,5,67 - // 如果s是char, 就打印出所有字符 - // 如果s是string, 就直接将整行打印出来 - string a = "12+3-4+5*67"; - istringstream s1(a); - int i; - // 12 3 -4 5 - while(s1 >> i){ - cout<> c){ - cout<> s){ - cout< counts; - // hello//world,//my//name//is//zjq// - while(s4 >> str1){ - counts[str1]++; - }cout< // getline - -string str; -getline(cin, str); //可以将带空格的字符放入到str中 -cout<name` 和 ` (*p).name`注意: 降低程序运行效率, shared_ptr析构函数不能太复杂, 特别慢, 当他析构的时候, 整个线程会阻塞, - -weak_ptr 打破循环引用, 只做观察指针, 看一下对象对象存不存在 - -auto_ptr已经不用了 - -[智能指针是线程安全的么?](http://www.cppblog.com/Solstice/archive/2013/01/28/197597.html) 显然智能指针控制写不是,因为智能指针操作不是原子性, 当赋值语句执行时, 其实智能指针拷贝对象同时还得对对象的计数进行+1操作, 这两步就会被其他线程钻空子了 - - - - - -C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11 支持,并且第一个已经被 11 弃用。 - -1. 作用是管理一个指针; 申请空间在函数结束时忘记释放, 造成内存泄漏, 而使用智能指针, 一旦智能指针超出类的作用域, 类会自动调用析构函数, 释放资源, 所以智能指针的作用原理在函数结束后, 自动释放内存空间; -2. auto_ptr p1 (new string ("I reigned lonely as a cloud.”)); - auto_ptr p2; - p2 = p1; //auto_ptr 不会报错. 此时p2掠夺了p1所有权, 使用p1的时候, 内存崩溃 -3. unique_ptr p3 (new string ("auto")); - unique_ptr p4; - p4 = p3;// 报错, 非法, 避免内存崩溃 -4. shared_ptr共享拥有, 多个智能指针可以指向同一个对象, 该对象和其相关资源会在最后一个引用被销毁后释放 -5. weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象, 作为管理指针; 为了解决循环引用导致的内存泄漏, 构造函数不会改变引用计数, 不会对对象内存进行管理, 像是一个普通指针, 但会检测所管理的对象是否被释放, 从而避免内存泄漏; - ** - -*** - -## #define和const区别 - -| const | -| ---------------------------- | -| 有类型有名字, 放到静态存储 | -| 编译时确定, 只有一个拷贝 | -| 可以用指针去指向该变量的地址 | -| 不能定义函数 | - -## 重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别 - -1. overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是==函数重载==,返回值类型可以不同 - 特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无 -2. override,派生类覆盖基类的虚函数,实现接口的重用,==返回值类型必须相同== - 特征:==不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字==(必须是虚函数) -3. overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同 - 特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字 - - - -## 多态 - -### [动态多态和静态多态](https://www.cnblogs.com/lizhenghn/p/3667681.html) - -多态的实现分为==静态多态和动态多态== - -1. 静态多态: 主要是 ==重载== ,在编译的时候就已经确定; -2. 静态多态设计思想: 对于相关的对象类型,直接实现它们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明,这里的接口称之为隐式接口。客户端把操作这些对象的函数定义为模板,当需要操作什么类型的对象时,直接对模板指定该类型实参即可(或通过实参演绎获得)。 -3. 动态多态: 用虚函数机制实现的,在运行期间动态绑定 -4. 动态多态设计思想: 对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。客户端的代码(操作函数)通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。 - -如动物音乐大赛, 乌鸦和狗和猫报名, 但是这三个对象都指向动物类(这是一个基类), 使用动物指针对乌鸦, 狗, 猫进行方法调用, 就是多态 - -### 动态多台和静态多态的比较 - -| | 静态多态 | 动态多态 | -| ------ | ------------------------------------------------------------ | ---------------------------------------------- | -| 优点 | 编译期完成, 效率高, 编译器可优化 | 运行期动态绑定, | -| | 强适配性和松耦合性, 通过偏特化,全特化处理特殊类型 | 实现与接口分离, 可以复用 | -| | 静态多态通过模板编程为C++带来了泛型设计概念, 如STL库 | 处理同一继承体系下异质对象集合的强大威力 | -| 缺点 | 用模板实现静态多态, 模板不足, 调试困难,编译耗时, 代码膨胀, 编译器支持的兼容性, | 运行期绑定, 运行开销大 | -| | 不能处理异质对象集合 | 编译器无法对虚函数进行优化 | -| | | 笨重的类继承体系, 对接口的修改影响整个类的层次 | -| 不同点 | 本质不同, 静态多态,编译阶段, 模板实现, 动态多态,运行阶段, 继承虚函数实现 | | -| | 动态多态接口是显式, 静态是隐式, | | - - -https://www.cnblogs.com/zkfopen/p/11061414.html - -### 虚函数表 - -```cpp -class B { - virtual int f1 (void); // 0 - virtual void f2 (int); // 1 - virtual int f3 (int); // 2 -}; - -// 虚函数表 -vptr -> [B::f1, B::f2, B::f3] - 0 1 2 -``` - -首先对于包含虚函数的类, 编译器会为每个包含虚函数的类生成一张虚函数表,即存放每个虚函数地址的函数指针的数组,简称虚表(vtbl),每个虚函数对应一个虚函数表中的下标。 - -除了为包含虚函数的类生成虚函数表以外,编译器还会为该类增加一个隐式成员变量,通常在该类实例化对象的起始位置,用于存放虚函数表的首地址, -该变量被称为虚函数表指针,简称虚指针(vptr)。例如: - -```cpp -B* pb = new B; / -pb->f3 (12); -// 被编译为 -pb->vptr[2] (pb, 12); // B::f3 参数pb是this指针, 他首先找到虚函数表, 调用对应的f3函数 - -// 注意:虚表是一个类一张,而不是一个对象一张,同一个类的多个对象,通过各自的虚指针,共享同一张虚表。 -vptr-> | vptr1 | vptr2 | vptr3 | -``` - -## 多态的工作原理(底层实现机制) - -```cpp -// 继承自B的子类 -class D : public B { - int f1 (void); - int f3 (int); - virtual void f4 (void); -}; - -// 虚函数表 -// 子类覆盖了基类的f1和f3,继承了基类的f2,增加了自己的f4,编译器同样会为子类生成一张专属于它的虚表。 -// 如下所示, 当基类指向子类时, vptr->vptr(子类)->D::f3, 这是因为他根据动态绑定原则, 先不直接加载基类自身函数, 编译器在运行时, 根据基类指向的子类的vptr函数进行加载指令, 这就实现了多态 -vptr(子类)-> D::f1, B::f2, D::f3, D::f4 - 0 1 2 3 -``` - -```cpp -// 指向子类虚表的虚指针就存放在子类对象的基类子对象中。例如: -B* pb = new D; // 父类指向子类, 调用子类的方法 -pb->f3 (12); -// 被编译为 -pb->vptr(子类)[2] (pb, 12); // D::f3 pb是基类指针, 他首先找基类的虚函数表vptr, -``` - - -```cpp -// 示例 -class A{ -public: - A():m_ch('A'){} - virtual void foo() { - cout << m_ch << "::foo()" << endl ; - } - virtual void bar(){ - cout << m_ch << "::bar()" << endl ; - } -private: - char m_ch ; -} ; -class B:public A{ -public: - B():m_ch('B'){} - void foo(){ - cout << "B::foo()" <(i); //相当于创建一个static_cast类型的匿名对象赋值给d2 -float *p4 = static_cast(&i); // err -int* p2 = reinterpret_cast(i); // 任意类型转换 -int *p = const_cast(&i); -``` - -*** - -## [const指针](https://blog.csdn.net/xixihaha331/article/details/51280263) - -1. 常量指针和指针常量 - -```cpp -int a = 20; -int b=40; -//------------------------------------------------ -const int *p; // 常量指针值不变, 对象可变, 这就是为何 for(const auto &a : arr){} -p=&a; -// (*p)++; // 这里会报错, 因为不能修改指向的值 -p=&b; // 这里不会报错, 因为可以指向别的对象 -printf("a=%d, b=%d, *p=%d\n", a, b, *p); //a=20, b=40, *p=40 - -//------------------------------------------------ -int* const p1 = &a; // 指针常量, 对象不可变, 值可变 -// p1=&b; // 会报错, 因为指针是常量, 对象不能变 -(*p1)++; // 这里不会报错, 因为可以改值, 但是不可以改对象 -printf("a=%d, b=%d, *p1=%d\n", a, b, *p1); //a=21, b=40, *p1=40 - -//------------------------------------------------ -const int* const p2 = &b; // 常量指针常量值, -// p2=&a; // 会报错, 因为指针是常量, 对象不能变 -// (*p2)++; // 会报错, 因为值是常量, 值不能变 -printf("a=%d, b=%d, *p2=%d\n", a, b, *p2); //a=21, b=40, *p2=40 -``` - -2. 常量参数 `void func(char *dest_str, const char *src_str)` -3. 修饰函数返回值 `const char *get_string(void)` 注意只能是指针传递, 如果是值传递就没用了 -4. 修饰成员函数 `int get_count(void) const;` 不可以修改对象的成员变量 - - - -## [new/delete 与 malloc/free 的区别是什么](https://blog.csdn.net/linux_ever/article/details/50533149) - -| | new/delete | malloc/free | -| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 类型 | C++的关键字 | C语言库函数 | -| 返回类型安全性 | 只需要对象名即可创建对象, 返回的是对象类型指针, 类型严格与对象匹配, 无需进行类型转换 | 开辟空间大小严格, 返回的是(void*), 需要通过强制类型转换成需要的类型 | -| | ==new调用构造函数, delete调用析构函数, 能保证对象的初始化和回收内存== | 不会调用构造析构函数, 无法满足动态对象要求 | -| | ==由于new对象返回的指针, 在底层空间还存储了这个对象开辟空间的大小, 因此在析构的时候能够根据这个存储进行回收内存== | | -| 内存分配失败 | ==抛出bac_alloc异常try { int *a = new int(); } catch (bad_alloc) { }== | 返回NULL | -| 是都需要指定内存 | ==new无需指定, 编译器会根据类型自行计算== | 需要显式指出所需内存 | -| 实际创建步骤 | 1, 调用operator new函数, 分配一块足够大的内存, 方便存储特定类型对象, 2, 编译器运行相应的构造函数, 构造对象, 并传入初始值, 3, 对象构造完成, 返回一个指向该对象的指针 | | -| delete释放对象步骤 | 1, 调用对象析构函数, 2, 编译器调用operator delete函数释放内存空间 | | - -new/delete 是 C++的关键字,而 malloc/free 是 C 语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数 - -*** - -## 构造函数和析构函数 - -| 构造函数 | 析构函数 | -| ------------------------------------------------------------ | ------------------------------------------------------------ | -| 无类型, 没有返回值, 名字和类名相同, 可重载 | 无类型, 无返回值, 名字和类名相同, 不带参数, 不可重载, 析构函数只有一个, 前面加个`~` | -| 作用: 完成对象的初始化 | 对象被删除前有系统自动执行清理工作 | -| 当对象d被创建时, 会自动调用构造函数, 当未定义构造函数时,编译器会自动假设存在两个默认构造函数cdata::cdata(){} | | -| Cdate::Cdate(const Cdate& a) | | -| 对象的析构函数在被销毁前调用, 对象何时销毁与其作用域相关 | | -| 全局对象在程序运行结束时销毁 | | -| 自动对象在离开作用域时销毁 | | -| 动态对象使用delete时销毁 | | - - - - -## allocator 内存分配和释放? - -1. STL分配器封装与STL容器在内存管理上的底层细节; -2. new(调用operate new配置内存,调用对象构造函数构造对象内容)delete(调用析构函数, 释放内存); -3. allocator将两个阶段操作区分开来,内存配置有alloc::allocate()负责, 释放alloc::deallocate(); 对象构造由construct负责,内存析构由destroy负责; -4. 为了提升内存管理效率, 减少申请小内存内存碎片问题, STL采用两级配置器, 当分配大小空间超过128B, 使用一级空间配置器(malloc, realloc, free进行内存管理和内存空间分配和释放),大于128B, 二级(内存池技术,通过空闲链表来管理内存) - -*** - -## malloc 的原理 - -malloc函数用于动态分配内存; 为了减少内存碎片和系统调用开销, malloc采用内存池的方式, 首先申请大块内存作为堆, 再将堆分成多个内存块, 以块作为内存管理的基础单位; 当用户申请内存时, 直接从堆区分配一块合适的空闲块; malloc采用隐式链表结构将堆区分成连续,大小不一的块, 包含已分配和未分配块; 同时malloc采用显示链表结构管理所有空闲块, 双向链表, 每个空闲块记录一个连续的, 未分配的地址; -当进行内存分配时,Malloc 会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc 采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。 -Malloc 在申请内存时,一般会通过 brk 或者 mmap 系统调用进行申请。其中当申请内存小于128K 时,会使用系统函数 brk 在堆区中分配;而当申请内存大于 128K 时,会使用系统函数 mmap在映射区分配。 - - -## STL迭代器删除元素: - -1. 对于序列容器vector,deque来讲,使用erase, 后面元素前移一位,erase返回下一个有效的迭代器; -2. 对于map,set,使用erase,当前元素迭代器失效,但是因为结构为红黑树,所以删除元素不会影响下一元素迭代器,在调用erase之前,记录下一个元素的迭代器即可, -3. 对于list,使用不连续分配内存, erase返回下一个有效迭代器 - -## vector和list 的区别 - -| Vector | List | -| :----------------------------------------------------------: | :------------------------------------------------------: | -| 连续存储的容器,动态数组,在堆上分配空间, 两倍容量增长, 顺序内存 | 动态双向链表, 堆上空间, 每删除一个元素会释放一个空间 | -| 访问:O(1)(随机访问);插入:后插快, 中间需要内存拷贝, 内存申请和释放; 删除: 后删快, 中间需要内存拷贝 | 访问: 随机访问差, 只能开头和结尾; 插入和删除快, 常数开销 | -| 适用场景:经常随机访问,且不经常对非尾节点进行插入删除 | 适用于经常插入和删除 | -| 下面是区别 | | -| 数组 | 双向链表 | -| 支持随机访问 | 不支持随机访问 | -| 顺序内存 | 离散内存 | -| 中间节点插入删除会导致拷贝 | 不会 | -| 一次性分配好内存, 二倍扩容 | list每次在新节点插入会进行内存申请 | -| 随机访问性能好,插入性能差 | 相反 | - -## STL迭代器的作用, 为何不用指针而用迭代器? - -1. 迭代器提供一种方法顺序访问一个聚合对象各个元素, 而又不暴露该对象的内部表示; 或者说运用这种方法, 是的我们可以在不知道对象内部结构情况下, 按照一定顺序规则直接反问聚合对象的各个元素 -2. 与指针的区别: 迭代器不是指针, 而是类模板, 表现像指针,模拟指针功能,重载指针操作符如->, *, ++等, 相当于一种智能指针, 根据不同类型的数据结构实现不同的操作 -3. 迭代器类的访问方式就是把不同集合类的访问逻辑抽象出来, 是的不用暴露集合内部的结构而达到循环遍历的效果; - -## C++中类成员的访问权限 - -C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符 -类内部, 不区分, 无限制 -子类, 能访问父类的private以外的属性和方法 -其他类, 只能访问public - -## struct和class的区别 - -在 C++中,可以用 struct 和 class 定义类,都可以继承。区别在于:structural 的默认继承权限和默认访问权限是 public,而 class 的默认继承权限和默认访问权限是 private。另外,class 还可以定义模板类形参,比如 template - -## C++源文从文本到可执行文件经历过程 - -1. 预处理: 源代码文件包含的头文件, 预编译语句, 分析替换, 生成预编译文件 -2. 编译阶段: 特定编码 -3. 汇编阶段: 转化为机器码, 重定位目标文件 -4. 链接阶段: 多个目标文件及所需要的库链接成为最终可执行文件 - -## include "" 和include <>的区别 - -1. 编译器预处理阶段查找头文件的路径不一样 -2. 双引号查找路径: 当前头文件目录, 编译器设置的头文件路径, 系统变量路径path指定的路径 -3. <>查找路径: 编译器设置的头文件, 系统变量 - - -## fork,wait,exec 函数 - -父进程产生子进程使用 fork 拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec 函数可以加载一个 elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork 从父进程返回子进程的 pid,从子进程返回 0.调用了 wait 的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回 0,错误返回-1。exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1 - -## STL 里 resize 和 reserve 的区别 - -1. resize(): 改变当前容器内含有元素的数量 - vectorv; - v.resize(20); - v.push_back(2); // 此时的2是21位置 -2. reserve(len): 改变当前容器最大容量, 不会生成元素; 如果reserve大于capacity, 重新分配个len的对象空间, 原始对象复制过来 - - - -## BSS端等六段: C++的内存管理? - -![代码存储结构](D:/0_2024/work/README.assets/代码存储结构.jpg) -在C++中, 虚拟内存分为代码段,数据段, BSS段, 堆区, 文件映射区, 栈区六个部分 - -1. 代码段: 包括只读存储区(字符串常量)和文本区(程序的机器代码), 只读 -2. 数据段: 存储程序中已初始化的全局变量和静态变量; 属于静态内存分配 -3. BSS段: 存储未初始化或初始化为0的全局变量和静态变量(局部+全局); 属于静态分配, 程序结束后静态变量资源由系统自动释放。 -4. 堆区: 调用 new/malloc 函数时在堆区动态分配内存,同时需要调用 delete/free 来手动释放申请的内存。频繁的malloc free造成内存空间不连续, 产生碎片, 因此堆比栈效率低 -5. 映射区:存储动态链接库以及调用 mmap 函数进行的文件映射 -6. 栈区: 存储函数的返回地址,返回值, 参数, 局部变量; 编译器自动释放, - -## 内存泄漏 - -1. 堆内存泄漏, 如果malloc, new, realloc从堆分配的内存, 由于程序错误造成内存未释放, 产生的 -2. 系统资源泄漏: 程序使用系统资源: bitmap, handle, socket忘记释放, 将导致系统效能和稳定差 -3. 没有将基类析构函数定义为虚函数, 基类指针指向子类对象后, 释放基类时, 子类资源不会被正确释放 - - -## 判断内存泄漏: - -1. 内存泄漏原因: 通常调用malloc/new等内存申请操作, 缺少对应的free/delete -2. 判断内存是否泄漏, 可以使用Linux环境下的内存泄漏检测工具, 也可以在写代码时添加内存申请和释放统计功能, 统计申请和释放是否一致, 以此判断内存泄漏 - varglind,mtrace 检测 - - -## 如何采用单线程的方式处理高并发? - -I/O 复用 异步回调 - -## 大端小端? - -大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。 - -## 设计一个server, 实现多个客户端请求 - -1. 多线程, -2. 线程池 , -3. IO复用 - -## [C++的线程锁你知道几种?](https://www.cnblogs.com/steamedbun/p/9376458.html) - -### 互斥锁mutex - -用于控制多线程对他们共享资源互斥访问的一个信号量, 也就是说为了避免多个线程同一个时刻操作一个共同资源;例如线程池中的多个空闲线程核一个任务队列, 任何一个线程都要使用互斥锁互斥访问任务队列, 避免多个线程同时访问任务队列发生错乱, 如果其他线程想要获取互斥锁, 只能阻塞等待 - -### 条件锁 - -条件锁就是所谓的条件变量, 某一个线程因为某个条件未满足时, 可以使用条件变量是程序处于阻塞状态, 一旦条件满足以信号量的方式唤醒一个因为该条件而被阻塞的线程 - -### 自旋锁 - -假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。 - -首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。 - -而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。 - -### 读写锁 - -说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。 - -1 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样可以有多个线程并行操作。这个时候如果再用写锁加锁就会发生阻塞。写锁请求阻塞后,后面继续有读锁来请求时,这些后来的读锁都将会被阻塞。这样避免读锁长期占有资源,防止写锁饥饿。 - -2 如果一个线程用写锁锁住了临界区,那么其他线程无论是读锁还是写锁都会发生阻塞。 - -## 什么类不能被继承 - -1. 将自身的构造函数与析构函数放在private作用域 -2. 友元类 friend -3. class FinalClass final { }; - -## 结构体和类大小 - -空的为1, 内存对齐, int double, char 则4,8,1+3, 后面的char需要对齐 - -## 类的什么方法不能是虚函数 - -普通函数, 友元函数, 构造函数, 内联成员, 静态态成员函数 - - - -## hash扩容 - -HashMap初始容量大小16,扩容因子为0.75,扩容倍数为2; - -底层是数组加链表, 随着数据的增加, hash冲突会增加, 因此设置扩容因子, 当数据数量到达hash容量的扩容因子倍, 就会以二倍扩容, 16*2=32, 然后重新计算每个元素在数组中的位置. - -## C++派生类的构造函数和析构函数执行顺序及其构造形式 - -### 派生类的构造函数和析构函数的执行顺序 - - 先执行基类的构造函数,随后执行派生类的构造函数,当撤销派生类对象时,先执行派生类的析构函数,再执行基类的析构函数。 - -### 派生类构造函数和析构函数的构造原则 - - 1)派生类不能继承基类中的构造函数和析构函数。 - 当基类含有带参数的构造函数时,派生类必须定义构造函数,以提供把参数传递给基类构造函数的途径。 - 2)当派生类中还有对象成员时,其构造函数的一般形式为: - -### 注意 - -1. 当基类构造函数不带参数时,派生类不一定需要定义构造函数,然而当基类的析构函数哪怕只有一个参数,也要为派生类定义构造函数,甚至所定义的派生类析构函数的函数体可能为空,仅仅起到传递参数的作用 -2. 当基类使用缺省构造函数时或不带参数的构造函数时,则在派生类中定义构造函数时,可以省略:基类构造函数名(参数表),此时若派生类不需要构造 函数,则可以不定义构造函数。 -3. 如果派生类的基类也是一个派生类,则每个派生类只需负责其直接基类的 构造,依次上溯。 -4. 如果析构函数是不带参数的,在派生类中是否要定义析构函数与它所属的 基类无关,故基类的析构函数不会因为派生类没有析构函数而得不到执行,他们各自是独立的 - -## 虚函数调用的工作原理 基于虚函数多态的机制 - -### 虚函数和纯虚函数 - -虚函数: C++中用于实现多态的机制, 核心理念是通过基类访问派生类定义的函数, 是C++中多态的一个重要体现; 利用基类指针访问派生类中的虚函数, 这种情况采用的是动态绑定技术; - -纯虚函数: 基类声明的虚函数, 基类无定义, 要求任何派生类都需要定义自己的实现方法, 在基类中实现纯虚函数的方法是在函数原型后面加 `=0` 纯虚函数不能实例化对象; - -### 抽象类 - -特殊类, 为了抽象和设计的目的建立的, 处于继承层次结构的较上层; - -定义: 带有纯虚函数的类为抽象类 - -作用: 将有关操作作为结果接口组织在一个继承层次结构中, 由他来为派生类提供一个公共根, 派生类将具体实现在其积累中作为接口的操作. 所以派生类实际上刻画了一组子类的操作接口的通用语义, 这些语义传给子类, 子类可以具体实现这些语义, 在将这些语义传给自己的子类 - -注意: 抽象类只能作为基类, 纯虚函数的实现由派生类给出; 如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。 - - - -### 虚函数表 - -```cpp -class B { - virtual int f1 (void); // 0 - virtual void f2 (int); // 1 - virtual int f3 (int); // 2 -}; - -// 虚函数表 -vptr -> [B::f1, B::f2, B::f3] - 0 1 2 -``` - -首先对于包含虚函数的类, 编译器会为每个包含虚函数的类生成一张虚函数表,即存放每个虚函数地址的函数指针的数组,简称虚表(vtbl),每个虚函数对应一个虚函数表中的下标。 - -除了为包含虚函数的类生成虚函数表以外,编译器还会为该类增加一个隐式成员变量,通常在该类实例化对象的起始位置,用于存放虚函数表的首地址, -该变量被称为虚函数表指针,简称虚指针(vptr)。例如: - -```cpp -B* pb = new B; -pb->f3 (12); -// 被编译为 -pb->vptr[2] (pb, 12); // B::f3 参数pb是this指针 - -// 注意:虚表是一个类一张,而不是一个对象一张,同一个类的多个对象,通过各自的虚指针,共享同一张虚表。 -vptr-> | vptr1 | vptr2 | vptr3 | -``` - -### 多态的工作原理(底层实现机制) - -```cpp -// 继承自B的子类 -class D : public B { - int f1 (void); - int f3 (int); - virtual void f4 (void); -}; - -// 虚函数表 -// 子类覆盖了基类的f1和f3,继承了基类的f2,增加了自己的f4,编译器同样会为子类生成一张专属于它的虚表。 -vptr(子类)-> D::f1, B::f2, D::f3, D::f4 - 0 1 2 3 -``` - -```cpp -// 指向子类虚表的虚指针就存放在子类对象的基类子对象中。例如: -B* pb = new D; // 父类指向子类, 调用子类的方法 -pb->f3 (12); -// 被编译为 -pb->vptr(子类)[2] (pb, 12); // D::f3 -``` - - -```cpp -// 示例 -class A{ -public: - A():m_ch('A'){} - virtual void foo() { - cout << m_ch << "::foo()" << endl ; - } - virtual void bar(){ - cout << m_ch << "::bar()" << endl ; - } -private: - char m_ch ; -} ; -class B:public A{ -public: - B():m_ch('B'){} - void foo(){ - cout << "B::foo()" <