C++ 知识小结
- C 语言 typedef 与 #define 比较
https://www.runoob.com/note/24230
- 野指针和悬空指针区别?
野指针:指的是没有被初始化过的指针。 解决方法:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针最初指向的内存已经被释放了的一种指针。解决方法:指针free或delete之后没有及时置空 => 释放操作后立即置空
- C++ 11 新特性
- 详细参考: https://www.cnblogs.com/chengjundu/p/10893702.html
- auto 为编译器特性 不影响运行效率 推断变量类型 注意与void* 区别
- decltype 获取变量类型
- nullptr 空指针 可以解决NULL 二义性问题 ,其实NULL 的本质为 #define NULL 0 即为0
见如下代码:
void fun(int a) {
count<<"fun(int)"<<endl;
}
void fun(int* a){
count<<"fun(int*)"<<endl;
}
int main() {
//C++ 98 标准中 会报错,会存在二义性 会认为 上述两个方法都符合
fun(NULL);
}
- C++ 14 auto 也可以作为函数参数了
- C++ 17 作用域的调整,如可以将变量定义只声明在if else 语句作用域中
- 注意右值引用与左值引用的区别
- https://zhuanlan.zhihu.com/p/335994370
- https://blog.csdn.net/yi_chengyu/article/details/122098389
- 右值引用可以进行读写操作,而常引用只能进行读操作
- 右值引用支持移动语义的实现,可以减少拷贝,提升程序的执行效率
- move和forward 区别
- move 参考:https://www.zhihu.com/question/43513150
- 需要了解其底层原理
- 右值引用为啥能避免拷贝:主要是使用move 移动语义,实现对象的所有权归属
- move 避免传参过程的拷贝https://zhuanlan.zhihu.com/p/55856487
- forward 避免重复
- 浅谈std::move和std::forward原理 https://blog.csdn.net/qq_33850438/article/details/107193177
- 当引用作为函数返回值时:参考https://blog.csdn.net/xujianjun229/article/details/120026260
- 用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本
- 不能返回局部变量
- 引用作为形参时:也不会产生副本
- 详细参考:C++ 左值、左值引用、右值、右值引用、move语义及完美转发
- C++ static
https://blog.csdn.net/qq_42564908/article/details/106466409
- sizeof、strlen区别
- sizeof() 是一个运算符,编译时就能确定对象的大小,而 strlen() 是一个函数,需要在运行时才能确定。
- sizeof() 计算的是变量或类型所占用的内存字节数,而 strlen() 计算的是字符串中字符的个数。
- sizeof() 可以用于任何类型的数据,而 strlen() 只能用于以空字符 '\0' 结尾的字符串。
- sizeof() 计算字符串的长度,包含末尾的 '\0',strlen() 计算字符串的长度,不包含字符串末尾的 '\0'。
- 智能指针的实现,需要注意什么:C++智能指针 share_ptr, weak_ptr, unique_ptr原理
https://blog.csdn.net/sun007700/article/details/107640708
https://supwills.com/post/understanding-cpp-smart-pointer/
6.1 unique_ptr
- 底层实现原理 https://zhuanlan.zhihu.com/p/378406523
- 自定义实现:https://xmuli.tech/posts/9ec225d6/
- 其实现基本原理与share_ptr 一样,但是需要:
- 禁止拷贝构造:unique_ptr(const unique_ptr &) = delete;
- 禁止拷贝赋值:unique_ptr &operator=(const unique_ptr &) = delete;
- 实现移动主义 :参考上面的move 实现
- 智能指针不要指向栈空间对象,会造成多次析构。
6.2 share_ptr
- 实现底层原理:https://www.saoniuhuo.com/article/detail-31621.html
- 基本实现思路:
- 一个Shared_ptr 对一个对象都是一个强引用
- 每一个对象都有与之对应的强引用计数,记录着当前对象被多少个shared_ptr 强引用着
- 可以使用use_count 函数获得引用计数的值
- 当一个新的shared_ptr 指向对象时,对象的引用计数就会+1
- 当一个shared_ptr 销毁时(比如作用域结束),对象强引用计数就会-1
- 当一个对象的强引用计数为0时(没有任何shared_ptr指向对象时),对象就会自销毁
- share_ptr 线程不安全原因:https://blog.51cto.com/waleon/5678197
- 为什么多线程读写 shared_ptr 要加锁?
- shared_ptr 使用的坑 http://senlinzhan.github.io/2015/04/24/%E6%B7%B1%E5%85%A5shared-ptr/
- shared_ptr 创建控制块的时机:
- https://blog.csdn.net/qq_38410730/article/details/105788724
- 如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题 https://developer.aliyun.com/article/1383819
- https://blog.csdn.net/qq_43727529/article/details/127187739
- 管理动态数组需要指定删除器
- shared_ptr 的默认删除器不支持释放数组对象,需要指定删除器。
- 智能指针 自定义实现 https://cloud.tencent.com/developer/article/1688444
- 注意智能指针的循环引用
- std::enable_shared_from_this 参考:C++11 新特性 enable_shared_from_this解析
6.3 unique_ptr 与shared_ptr区别
- 所有权:shared_ptr通过引用计数共享所有权,而unique_ptr独占所有权。
- 复制与移动:shared_ptr允许复制和移动,而unique_ptr禁止复制,只能移动。
- unique_ptr 和 shared_ptr 可以互相转换吗?
- https://juejin.cn/post/7096514898800148488
6.4 weak_ptr 底层实现原理与源码:https://blog.csdn.net/ithiker/article/details/51532484
https://juejin.cn/post/7112987636007960606
weak_ptr 手写实现:
https://juejin.cn/post/7235574844435939365
- C++ include 中都包含什么东西?如果把函数声明与函数定义都写在头文件中会有什么问题?include 引入头文件其实都会将引入头文件信息引入到当前文件,如果在头文件写函数声明当多次引入时可能会造成重复定义
https://www.cnblogs.com/haloboy/p/7305705.html
- 红黑树是什么样的数据结构?红黑树和B数的区别?
- 结构体struct和联合体union的区别
- 定义:结构体struct:把不同类型的数据组合成一个整体,自定义类型。
- 共同体union: 使几个不同类型的变量共同占用一段内存。
- 地址: struct和union都有内存对齐,结构体的内存布局依赖于CPU、操作系统、编译器及编译时的对齐选项。
- 具体参考:https://blog.csdn.net/shihuboke/article/details/79303539
- 内存对齐:(Class /Struct)、Union区别
- http://www.cppblog.com/cc/archive/2006/08/01/10765.html
- https://blog.csdn.net/loverooney/article/details/38262709
- 为什么要内存对齐https://blog.csdn.net/m0_46216098/article/details/128962902
- 平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐。
- 性能原因:内存对齐能够提高内存访问效率。CPU 在读取内存时,一次读取固定长度的数据(如4字节、8字节等)。如果将多个变量连续分配在内存中而不做对齐处理,会造成 CPU 读取数据出现“半包”的情况,此时,需要再进行一次内存访问才能获取该变量数据。
- 示例
- 在 32 位编译器(4字节对齐)中,以下结构的内存是如何分配的?
- struct { char a; int b; }st;
- 内存对齐的情况下,结构体 st 总共占 8 字节,其中 a 与 b 之间空余的 3 字节用于补齐缺少的字节(如果是 8 字节对齐,则需要补齐 8 字节)。
- 而在非内存对齐的情况下,仅占据 5 字节。
- 其在内存中的分布如下图所示:
- 可以发现,内存对齐会浪费一部分空间。事实也是如此,通常情况下,相对于空间,运行速度的提升更为重要。
- 在内存对齐的情况下,CPU 读取 int 类型的 b,仅需一次内存寻址即可完成。而对于非内存对齐,第一次寻址只能获得 b 的前 3 个字节数据,要获取完整的数据还需要再进行一次内存寻址,以获得第 4 个字节的数据。
- 这也是内存对齐能够提高效率的原因。
- 空结构体”(不含数据成员)的大小不为0,而是1
- C和C++的特点与区别(注意本质区别)
https://blog.csdn.net/zgcjaxj/article/details/105020055
- C++中基类的析构函数为什么要用virtual虚析构函数(重要知识点)
https://blog.csdn.net/wangkui1331/article/details/80485041
- 多态相关
- 实现原理(留意 C++ 类的内存布局是什么样的, 注意子类与基类的内存布局)
https://www.cnblogs.com/jerry19880126/p/3616999.html
- C++:构造函数和析构函数能否为虚函数?
- https://www.cnblogs.com/zhizhan/p/4676064.html
- 构造函数不能为虚函数:https://www.cnblogs.com/sylar5/p/11525113.html
- 存储空间角度:虚函数对应一个vtable,vtable存储于对象的内存空间
- 若构造函数是虚的,则需要通过 vtable来调用,若对象还未实例化,即内存空间还没有,无法找到vtable
- 使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。
- 构造函数本身就是要初始化实例,那使用虚函数就没有实际意义
- 从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数**
- 析构函数可以是虚函数,且常常如此
- 这个就好理解了,因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。
- 虚函数表是存在堆上还是栈上?
在C++中,虚函数表在编译期就已经确定,所以既不在堆上,也不在栈上,而是存在于编译后的可执行文件里面。堆和栈只有到了运行器才开始分配的。不过在每一个有虚函数的类的对象的存储空间中都有一个虚函数表的指针,这个指针是在运行时确定的,用于实现多态
https://www.wenjiangs.com/group/topic-13344.html
- 虚函数存在在哪?
https://www.zhihu.com/question/400069506
- 虚函数表是在编译时生成的,通常存放在程序的全局数据区,具体是只读数据段(.rodata)。
- 虚函数指针(vptr)是在运行时动态设置的,存放在每个对象的内存中,通常是对象内存布局中的第一个字段。
- 如何在 C++ 中实现动态绑定?
- 在 C++ 中,通过使用虚函数可以实现动态绑定。当使用基类的指针或引用调用虚函数时,会根据实际指向或引用的对象类型来决定调用哪个具体的实现。
- 使用虚函数实现动态绑定的步骤
- (1)在基类中声明虚函数:
- 在基类中,使用virtual关键字声明一个成员函数为虚函数。例如:
- class Base { public: virtual void print() { std::cout << "Base class print function." << std::endl; } };
- (2)在派生类中重写虚函数:
- 在派生类中,使用相同的函数签名重写基类的虚函数。例如:
- class Derived : public Base { public: void print() override { std::cout << "Derived class print function." << std::endl; } };
- (3)通过基类指针或引用调用虚函数:
- 创建基类指针或引用,使其指向派生类对象。当通过基类指针或引用调用虚函数时,会根据实际指向的对象类型在运行时确定调用哪个版本的函数,实现动态绑定。例如:
- int main() { Base* basePtr = new Derived(); basePtr->print(); // 调用 Derived 类中的 print 函数 delete basePtr; return 0; }
- 动态绑定的原理
- 虚函数表(VTable):
- 当一个类包含虚函数时,编译器会为该类创建一个虚函数表(VTable)。虚函数表是一个存储函数指针的数组,每个指针指向类中的一个虚函数。
- 对于每个包含虚函数的对象,编译器会在对象的内存布局中添加一个隐藏的指针,称为虚函数表指针(VTable Pointer),该指针指向对象所属类的虚函数表。
- 运行时确定函数调用:
- 当通过基类指针或引用调用虚函数时,实际上是通过虚函数表指针找到虚函数表,然后根据函数在表中的位置调用相应的函数。
- 由于虚函数表是在运行时根据对象的实际类型确定的,所以可以实现动态绑定,即根据对象的实际类型来调用正确的函数版本。
- 注意事项
- 虚函数的开销:使用虚函数会带来一定的运行时开销,因为需要通过虚函数表指针进行间接调用。在性能敏感的代码中,需要考虑虚函数的开销。
- 纯虚函数和抽象类:
- 如果一个类中至少有一个纯虚函数,那么这个类就是抽象类,不能被实例化。抽象类通常用于定义接口,派生类必须实现所有的纯虚函数才能被实例化。
- 虚函数的继承:
- 派生类继承基类的虚函数表。如果派生类重写了基类的虚函数,那么在派生类的虚函数表中,相应的函数指针会指向派生类的函数实现。
- 构造函数的顺序和析构函数的顺序,原因?
- 构造函数顺序:先基类,再派生类。 成员变量构造(按照它们在类中声明的顺序,不是初始化列表顺序),继承时,子类的内存结构为:继承过来的基类成员变量,放在内存前面
- 析构函数顺序:先派生类,再基类
- https://blog.csdn.net/qq_43287763/article/details/145823602
- C++ 内存结构及管理(注意与java 内存模型的对比)
https://blog.csdn.net/skrskr66/article/details/92769994
- C++ 内存模型:
- 栈区:由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似。
- 堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表
- 全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放
- 文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放
- 程序代码区:存放函数体的二进制代码。但是代码段中也分为代码段和数据段。
- 关于文字常量区文字常量区,在大多数解释中,都仅仅说明常量字符串存放这里。但是如果深究字眼,那么其他常量比如整型是否存放这里呢?我查阅了一些资料,是这么解释的:常量之所以称为“文字常量”,其中“文字”是指我们只能以它的值的形式指代它,“常量”是指它的值是不可变的。同时注意一点:文字常量是不可寻址的(即我们的程序中不可能出现获取所谓常量20的存储地址&20这样的表达式),虽然常量也是存储在内存的某个地方,但是我们没有办法访问常量的地址的。
- JAVA内存模型:
- 堆 栈与C++ 基本一致
- 方法区:也称为静态区 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 运行时常量池
存放字面量及符号引用
- c++ dynamic_cast 实现原理(同时需要理解reinterpret_cast 与static cast转换)
https://segmentfault.com/a/1190000039318539
比较4种转换类型:
https://zhuanlan.zhihu.com/p/54228612
转换其实就是将内存内容赋值
类型转换陷阱例子:
class Base {
public:
virtual void print() const {
printf("base \n");
}
};
class DerivedA : public Base {
public:
void print() const override {
printf("DerivedA \n");
}
};
class DerivedB : public Base {
public:
void print() const override {
printf("DerivedB \n");
}
};
int main() {
Base* ptr = new DerivedA();
ptr->print();
DerivedB *pb = static_cast<DerivedB*>(ptr);
pb->print();
delete ptr;
return 0;
}
打印:
分析:为什么打印的都是“DerivedA” ,第26行代码不是 "DerivedB"?
因为static_cast 转换 不会进行类型检查,让不合法的类型增加的发生了,这种不安全的类型转换最终导致程序的多态性遭到破坏。在下述代码时,ptr 已经指向了DerivedA对象,
在执行到 DerivedB *pb = static_cast<DerivedB*>(ptr); 并没有将pb 指向 DerivedB。所以输出的依然是“DerivedA”。
- static_cast :这是最基本的类型转换符。它主要用于基本数据类型之间、基类与派生类之间以及空指针转换为任何类型的空指针。static_cast无法在不兼容的类型之间进行转换,如将一个整数转换为指针类型。static_cast 在编译时进行类型检查,而不在运行时进行类型检查.因此无法用static_cast 在基类和派生类之间转换,子类-》基类 是安全的,但是基类-》子类是不安全的,可能在某些情况下导致未定义行为。
- dynamic_cast :动态转换主要用于基类和派生类之间的指针或引用类型进行转换。会在运行时进行检查,以确保转换安全。如果类型转换不合法,则返回nullptr。程序可能通过返回值判断转换是否成功。使用dynamic_cast 基类必需包含虚函数,以便支持运行时的类型检查。上述例子为了得到"DerivedB" 结果,就可以使用dynamic_cast 转换。
- const_cast :这个类型转换运算符用于改变表达式的const属性。可以将const类型转换为非const类型,反之亦可。
- reinterpret_cast:可以将一个指针或引用强制转换为另一个不同类型的指针或引用。reinterpret_cast的转换过程仅仅是重新解释原始数据的位模式。例如,在将一个指向int 类型的指针转换为指向char 类型的指针时,reinterpret_cast 操作符会将int 类型的位模式按照char类型的内存而已重新解释,得到一个指向char类型的指针,但是这个指针指向的内存的区域并没有改变。reinterpret_cast 不会执行任何类型检查,因为可能破坏类型系统。通常用于底层编程。
- 工具篇
项目名称 | 链接 |
C/C++内存泄漏及检测 | https://blog.csdn.net/w09103419/article/details/113990140 |
android NDK 内存泄露leakTrace 原理 | https://blog.csdn.net/xiaoting451292510/article/details/105850409 |
- C++ 函数重载是因为采用了name mangling技术,即在底层将函数再重新命名。
在 C++ 中,函数重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表不同(参数的类型、个数或顺序不同)。实现原理是编译器根据函数调用时提供的实参来确定要调用的具体函数版本。
- 函数重载的概念
函数名相同:多个函数具有相同的函数名称。这使得代码更加简洁和易于理解,因为可以使用相同的函数名来执行类似的操作,而不必为每个不同的参数组合创建不同的函数名。
参数列表不同:函数重载的关键是参数列表不同。参数列表可以在参数的类型、数量或顺序上有所不同。例如,可以有一个函数接受两个整数参数,另一个函数接受一个整数和一个浮点数参数,还有一个函数接受三个整数参数。
- 函数重载的实现原理
名字修饰(Name Mangling):C++ 编译器在编译过程中会对函数进行名字修饰,以区分不同的重载函数。名字修饰是将函数名和参数类型信息组合在一起,生成一个唯一的内部名称。这样,即使函数名相同,编译器也可以通过内部名称来区分不同的重载函数。
函数调用解析:在函数调用时,编译器会根据函数的参数类型和数量来确定要调用的具体函数。编译器会遍历所有具有相同函数名的重载函数,并尝试找到一个与调用参数类型和数量完全匹配的函数。如果找到匹配的函数,编译器就会生成相应的代码来调用该函数。如果没有找到匹配的函数,编译器会尝试进行类型转换,以找到一个最接近的匹配函数。如果仍然无法找到匹配的函数,编译器会发出错误信息。
- STL中常用的容器,map和hashmap底层实现以及应用场景
https://blog.csdn.net/lyq_csdn/article/details/80630872
- c++中map与unordered_map的区别
https://blog.csdn.net/batuwuhanpei/article/details/50727227
- char* 和 string有什么区别?实际中哪一个用的比较多?为什么?从char*聊到网络传输中粘包问题
- https://www.cnblogs.com/fonddream/p/9806810.html
- string 转换为char* 时有什么需要注意的点?
- https://blog.csdn.net/wcc27857285/article/details/84852269
- 你的项目中如何做的yuv到rgb的变换?为什么不直接用yuv?
yuv是经过压缩的视频信号,要还原成可以处理的数字信号,得必须转换
- 正确区分重载、重写和隐藏。
- 注意三个概念的适用范围:处在同一个类中的函数才会出现重载。处在父类和子类中的函数才会出现重写和隐藏。
- 重载(overload):同一类中,函数名相同,但参数列表不同。
- 重写/覆盖(override):父子类中,函数名相同,参数列表相同,且有virtual修饰。
- 隐藏(overwrite):父子类中,函数名相同,参数列表相同,但没有virtual修饰;函数名相同,参数列表不同,无论有无virtual修饰都是隐藏
- 基类中:
(1) virtual void show(); //是虚函数
(2) void show(int); //不是虚函数
子类中:(3) void show(); //是虚函数
(4) void show(int); //不是虚函数1,2构成重载,3,4构成重载,1,3构成重写,2,4构成隐藏。另外2,3也会构成隐藏,子类对象无法访问基类的void show(int)成员方法,但是由于子类中4的存在导致了子类对象也可以直接调用void show(int)函数,不过此时调用的函数不在是基类中定义的void show(int)函数2,而是子类中的与3重载的4号函数。
- switch/case 与if /else 对比
https://www.cnblogs.com/anlia/p/11685639.html
- STL Vector 底层实现原理
http://c.biancheng.net/view/6901.html
- C++ 是如何检查指针越界的?
https://blog.csdn.net/TalesOV/article/details/107320124
- C++中的RVO优化和NRVO优化
https://www.nowcoder.com/discuss/601545
https://blog.csdn.net/songchuwang1868/article/details/83622039
- 为了减少拷贝构造次数以及析构次数而采用的一种编译器优化技术
- C++ 模版实现原理
https://www.cnblogs.com/wuhongbin/p/14048321.html
[https://blog.csdn.net/qq_39108291/article/details/102019805
- 模板定义与实现为什么不能分开写?
- https://blog.csdn.net/weixin_46382844/article/details/139996572
- C++中每一个对象所占用的空间大小,是在编译的时候就确定的(C++是编译型语言),在模板类没有被实例化前,编译器是无法知道模板类中使用模板类型的对象的所占用的空间的大小。只有模板被实例化,编译器才知道模板套用的是什么类型,应该分配多少空间。这也就是模板类为什么只是称之为模板,而不是泛型的缘故。
- malloc 与 new 区别:
- 基本区别 https://blog.csdn.net/weixin_39411321/article/details/89311059
- new运算符的原理?
- https://juejin.cn/post/7023663734367191076
- new/new[]和delete/delete[]的区别原理
- https://blog.csdn.net/zyazky/article/details/52627200
- https://blog.csdn.net/weixin_39411321/article/details/89310651
- malloc和free不会调用构造和析构函数,注意(new和free、malloc和delete可以配对使用)
- 成员函数调用delete this会发生什么?之后再进行读写会怎么样?
- 其他人总结的:
- https://zhuanlan.zhihu.com/p/354382975
- https://www.bilibili.com/read/cv8601848
- https://bbs.csdn.net/topics/80461007?list=lz
- 常用的知识点 https://www.iamshuaidi.com/2337.html
- malloc 是如何分配内存的?
- 方式一:通过 brk() 系统调用从堆分配内存
- 方式二:通过 mmap() 系统调用在文件映射区域分配内存;
- 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
- 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
- malloc 分配的是物理内存吗?
- malloc() 分配的是虚拟内存
- malloc(1) 会分配多大的内存?
- free 释放内存,会归还给操作系统吗?
- 如果通过brk() 方式分配的,就先进入缓存池
- 如果通过mmap申请的,会返回给系统
- 为什么不全部使用 mmap 来分配内存?
因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间。
所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。
另外,因为 mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。 也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。 为了改进这两个问题,malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。 等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
- 既然 brk 那么牛逼,为什么不全部使用 brk 来分配?
- 参见连接
- free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?
- malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节吗? 这个多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小
- https://xiaolincoding.com/os/3_memory/malloc.html#linux-%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83%E9%95%BF%E4%BB%80%E4%B9%88%E6%A0%B7
- Linux 用户进程与内核进程空间一般多大,注意区分32位与64位系统
https://zhuanlan.zhihu.com/p/524014167
- #include 重复引入的解决方法
- ifndef #define # endif 可以处理重复包含的问题,可以针对代码块
- pragram once 也可以屏蔽重复include的问题,不过是针对 整个文件
- 内联函数 inline function
- 执行效率变高,原因:不再开辟函数 栈空间,直接使用函数实现进行了
- 内联函数 声明规则:
- 函数体积不大
- 频繁调用的函数 (因为可以节约函数栈空开辟的时间)
- 宏替换也有提升效率的作用
- 析构函数中不需要手动释放对象的基本类型成员变量,但是需要手动释放对象类型的成员变量,即堆空间对象
- 对象内部申请的堆空间由对象内部回收
在构造器再次调用构造器,如果初次的构造器是在栈空间调用的话,那么此处调用的构造函数有可能在栈空间创建,即是一个临时对象,临时对象的生命周期很短如下面代码:
class Person{
int m_age;
public:
Person(){
Person(18); //此处如果 Person() 是在 栈上创建的 那么 创建的Person(18) 对象为临时对象,创建的m_age 并不会赋值给this.m_age
}
Person(int age) {
m_age = age;
}
}
- 虚继承
内存分布:
首先存入虚表指针,虚表指针指向虚表。虚表中存入两个对象:
对象一:虚表指针与本类起始的偏移量,因为虚表指针通常是放在本类对象内存空间的起始处所以它的值通常为0
对象二:虚基类第一个成员变量与本类起始的偏移量,在本类中,虚继承来的父类成员变量
是放在本类成员变量之后的,示例:
- student 内存分布如下
- Freshmen 内存分布
- 拷贝构造函数
Class Car{
public :
int m_price = 0;
Car(int price = 0) : m_price(price) {
}
//拷贝构造函数
Car(const Car& car) {
}
}
Car car1;
Car car4(car1);
- 如果一个对象没有实现拷贝构造函数,在进行拷贝构造的时候它不会调用拷贝构造,只是将已经存在的对象成员变量一一对应赋值给新对象的成员变量,如果上述例子
- 会将car1成员变量m_price 拷贝赋值给 car4的成员变量m_price新对象的成员变量
- 如果实现了拷贝构造函数 ,则向调用拷贝构造函数 创建对象
- 拷贝构造知识点2
堆空间指向栈空间需要注意,因为两者的生命周期不一档,有可能栈空间在堆空间之前被系统回收了,导致堆空间中的指针指向一块被回收的内存变成了野指针,常见于对象中的字符串赋值。示例如下:
class Car {
int m_price;
char* m_name;
public:
Car(int price,char*name) {
//防止在栈空间构造本对象,而付进来的name 如果来自于栈空间
//有可能导致m_name 变成野指针
m_name = new char[strlen(name) + 1]{};
strcpy(m_name,name);
}
~Car() {
if(m_name) {
delete[] m_name;
m_name = nullptr;
}
}
}
- 什么时候需要编写拷贝构造与拷贝赋值函数
参考连接:
https://blog.csdn.net/huashuolin001/article/details/120006195
重要知识点:
- 当对象中包含有指针与引用成员对象时,如果没有主动编写拷贝构造时,在外部使用拷贝构造时是走系统默认提供的拷贝构造,而默认的拷贝构造函数是仅对成员变量的一次浅拷贝,从而在析构的时候会出现指针或者引用成员变量析构两次。
- 在使用默认拷贝构造的时候成员变量也有可能出现内存泄露
- 来源于 高质量编程13-7
- 浅拷贝--指针类型只拷贝地址值,示例:
11、对象类型参数和返回值
使用对象类型作为函数参数或者返回值可能产生不必要的中间对象
12. 匿名对象(临时对象)
没有变时名,没有被指针指向的对象,用完后马上调用析构
13.隐式构造-可以使用explicit 声明构造函数 禁止使用隐式构造
C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数,如:
隐式构造 :
https://blog.csdn.net/ox0080/article/details/84141043
- auto 与void* 区别
- Void* https://www.cnblogs.com/tsh292278/p/11289739.html
- auto 能否作为函数参数?能否作为返回值? 都可以了
- https://zhuanlan.zhihu.com/p/343507680
- lambda 实现原理
https://blog.csdn.net/Chiang2018/article/details/122221182
- lambda表达式是通过什么原理捕获外部的值的?来自大疆
传值的话内部维护了一个副本,传引用不会复制。
https://zhuanlan.zhihu.com/p/150554945
https://blog.csdn.net/weixin_43450564/article/details/107754988
https://blog.csdn.net/qq_27390023/article/details/137466483
- string 与char* 区别
https://zhuanlan.zhihu.com/p/367500639
- 纯虚函数 与虚函数
- https://www.cnblogs.com/tsh292278/p/10931493.html
- 多态类型:
- 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:
- 编译时多态性,运行时多态性。
- 编译时多态性:通过重载函数实现
- 运行时多态性:通过虚函数实现。
- 数组指针与指针数组:
https://www.bilibili.com/read/cv3079900
- 编译的过程:
https://blog.csdn.net/qq_56999918/article/details/120632320
- C++编程技巧: Pimpl https://zhuanlan.zhihu.com/p/458947637
- C++ 委派:
https://blog.csdn.net/jiang_xinxing/article/details/78475665
- C++/C 函数传值时有副本机制,普通值即在函数内部会对形参作一次,如果传递的是指针会拷贝指针(因为指针存放的对象的地址,所以拷贝的是一个地址)此时有两个指针指向同一个对象
- 在检测C++ 内存泄露时,可以有两种方法
- 第一种 针对代码量少,完全自己管控的malloc/relloc/calloc, /free/delete ,可使用标记法。申请的内存的时候使用一个标记,包含是谁申请的,发生在哪行代码。内存释放的时候将这个标记清除,如果发现依然有存在标记则视为内存泄露了
- 第二种:针对第三方库,无法管控别人的内存申请与释放,可以使用dlsym库函数hook住 malloc/calloc/relloc与free/delete/delete[] 同时添加标记。释放的在时候如果依然有标记存在,则视为泄露了。
- STL 容器相关
- c++的容器是线程安全的吗,怎么实现vector的读和写的线程安全,锁加在哪个地方?
但是并发读的时候,由于潜在的内存重新申请和对象复制问题,会导致读方(消费者)的迭代器失效
https://developer.aliyun.com/article/940147
https://blog.csdn.net/weixin_45714013/article/details/128111522
锁加在哪个地方?
- 写操作:在写操作(如push_back、insert等)前加锁,确保没有其他线程可以同时读写该vector。使用std::mutex或std::unique_lock。
- 读操作:在读操作前加共享锁,确保多个读操作可以同时进行而不影响写操作。使用std::shared_mutex或std::shared_lock。
- 读写锁的底层思想是什么?
https://blog.csdn.net/xie__jin__cheng/article/details/138665604
- JNI 相关:JNI 内存管理及优化
- 内核态与用户态的区别
https://blog.csdn.net/qq_41709234/article/details/124320482
- 虚拟内存相关知识
https://xiaolincoding.com/os/3_memory/vmem.html#%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98
附:
- C++ 知识点小结,面试经验
- https://www.aliyun.com/sswb/507557.html
- https://zhuanlan.zhihu.com/p/717177001
- 音视频面试经验总结:https://www.bilibili.com/opus/703169657821986817
英特尔 编译器面试算法题:
宏展开
1.待处理字符串由大写字母(A-Z)和小写之母(a-z)组成,例如:
ABc
其中大写字母(A-Z)是预定义的宏,其本身可能是另一个待处理字符串,比如上例中的A,B以及相关的宏的定义如下
A=Dd
D=e
B=b
现要求完成如下函数,把待处理字符串中的宏全部展开。
std:string expand(std:string& input, std:unordered_map<char, std:string>& macro)
返回字符串仅由小写之母(a-z)组成。例子中的ABc展开变成edbc。
- 给定n阶台阶 ,一次只能走1/2 打印所有种走法
- 智力题:一根绳子烧完需要60分,请问至少需要多少根绳子来衡量70分钟?
- 有5个赛道,1个赛道只能跑1匹马,请问至少跑几次才能决出跑的最快的3匹马?
- 小明离家有50米,每走一米吃一个苹果,起点有100个苹果,每次最多背50个苹果,请问最多可以拿回家多少苹果?25个
- 根据题意得出:先背50根到25米处,这时,吃了25根,还有25根,放下.
- 回头再背剩下的50根,走到25米处时,又吃了25根,还有25根.
- 再拿起地上的25根,一共50根,继续往家走25米,还有25根.
常见智力题:
https://www.cnblogs.com/skyseraph/archive/2012/03/22/2410993.html