只求念头通达
定义和声明的区别
定义分配空间,声明不分配空间
union,struct,class 的异同点
union默认public,可以包含构造析构,不能包含虚函数和静态变量。
struct默认public,可以包含构造析构,可以包含虚函数和静态变量。
class默认private,可以包含构造析构,可以包含虚函数和静态变量,可以定义模板。
深拷贝和浅拷贝
浅拷贝:增加一个指针指向原先的内存,容易重复释放资源。
深拷贝:增加一个指针指向新开辟的内存,可以避免重复释放资源。
new和malloc的区别
new:不用指定大小,返回对象类型指针,运行重载。
malloc:需要指定大小,返回void*,不能重载。
对非基本类型的对象,new/delete可以做构造析构函数,malloc/free不行。
被free回收的内存是立即返回给操作系统吗?
不是,会被ptmalloc使用双链表保存起来,下次申请可以返回这部分内存,避免频繁的系统调用。同时ptmalloc会对小块内存进行合并,避免碎片化内存。
new申请二维数组
int **dp=new int*[n];//动态申请二维数组nxm
for(int i=0;i<n;++i){
dp[i]=new int[m];
}
c++11新特性
nullptr:void*,代替NULL的0,解决二义性问题。
类型推导:auto,decltype可以查询表达式的类型。表示迭代器的时候很有用。
Lambda表达式:匿名函数
// 充当函数auto multiply = [ ](int a, int b) { return a * b; };
// 按值捕获(copy) auto add = [x, y]( ) { return x + y; };
// 按值捕获所有auto add = [=]( ) { return x + y; };
// 按引用捕获 auto increment = [&x]( ) { x += 1; };
// 按引用捕获所有auto increment = [&]( ) { x += 1; y += 1; };
智能指针unique_ptr和shared_ptr,防止内存泄漏,析构自动delete。
基于范围的for循环:for(auto &i : arr);
thread类和mutex类
delete于deletd[]
delete:释放new 分配的单个对象的内存。调用对象的析构函数,然后释放内存。
delete[]:释放 new[] 分配的数组的内存。调用每个元素的析构函数,释放数组的内存。
子类析构时要调用父类的析构函数吗?
对象创建时,先调用父类构造函数,再调用子类构造函数。
对象销毁时,先调用子类析构函数,再调用父类析构函数。
为什么父类析构函数要定义为虚函数virtual
派生类无法继承基类的析构函数,使用父类指针删除派生类对象时,如果不使用virtual,子类析构将不会被调用,造成资源泄漏。
纯虚函数
只是声明,没有实现的虚函数:virtual void func()=0;是抽象类,不能实例化。
当这个类本身产生一个实例没有意义的情况下,把这个类的函数实现为纯虚函数。
比如动物可以派生出老虎兔子,但是实例化一个动物对象 就没有意义。
静态绑定和动态绑定的介绍
静态绑定声明的类型,在编译的时候确定。
动态绑定具体的属性或函数在运行期确定,通常通过虚函数实现动态绑定。
什么是动态绑定?动态绑定是怎么实现的?
父类指针指向或引用子类对象时,调用该虚函数,最终会调用子类重写的虚函数。
父类指针通过子类地址找到虚表指针,通过虚表找到虚函数地址。
父类指针不能访问子类独有的函数。
虚表相关:
每个带有虚函数的类,都有一个虚函数指针vptr,在对象初始化后指向虚函数表。
虚函数表、虚函数指针是编译器确定的,但虚函数具体的实现是运行期确定的。
多继承会有多个虚表指针和虚表,独立函数地址默认放在第一个虚表。
类的所有对象共享这个类的虚表,虚表指针属于实列。
虚函数表由编译器在编译时生成,保存在.rdata 只读数据段。
虚表指针在创造实例的时候生成,处于类对象的内存块的前4/8个字节中。
模板的用法与适用场景实现原理
用template 关键字进行声明,
它与某种特定类型无关,因此代码可重复使用。
它是平台无关的,具有很好的移植性。
它在编译时检查数据类型而不是运行时检查数据类型,保证了类型的安全。
第一次编译检查语法,第二次编译替换参数生成具体的代码。
与继承相比模板默认内联,且没有虚表指针,调用开销小。
模板不能推导返回值类型,可以结合auto和decltype。
多态,虚函数,纯虚函数
多态:是对于不同对象接收相同消息时产生不同的动作。
虚函数:在父类中冠以关键字 virtual 的成员函数,子类可以重写虚函数。
纯虚函数:父类保留函数名,子类实现,父类不能实例化对象。
实现多态的方式
函数重载:允许有不同参数的函数有相同的名字。
虚函数:子类重新定义父类的虚函数
模板函数:某函数进行的操作一样,只是操作的数据类型不同,可以用模板
运算符重载:重载运算符实现自定义数据的处理。
继承机制中对象之间是如何转换的?
引用转换,父类对象引用子类对象,实现子类函数调用。
指针转换,父类指针指向子类对象,实现子类函数调用。
如何实现只能动态分配类的对象?只能静态分配呢?
只能动态分配:将构造析构函数放在protected里面,自己在定义函数new构造。
只能静态分配:将new和delete设置为private。
什么是引用
引用是某个变量的别名,一旦声明,不能修改,对引用操作与对变量直接操作相同。
常引用:既要保证效率,又要保护传递的数据时,使用常引用。const int &ra=a;
将“引用”作为函数参数有哪些特点
传递引用给函数与传递指针的效果是一样的。
内存中并没有产生实参的副本,它是直接对实参操作。
传递数组和链表用指针,传递大型对象用引用。
将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
好处:在内存中不产生被返回值的副本
注意: 不能返回局部变量的引用,
不能返回new分配的内存,无法使用delete
重载和重写的区别
重载(overload)指函数名相同,参数列表不同。返回值可以不同,但返回值不可以作为区分不同重载函数的标志。
重写(overwrite)指函数名相同,参数列表相同,方法体不相同。一般用于子类重写父类方法,父类方法会被隐藏。
C++是不是类型安全的?不是。两个不同类型的指针之间可以强制转换。
main函数执行之前,主要就是初始化系统相关资源:
设置栈指针,
初始化静态变量和全局变量,即.data段的内容。
将未初始化部分的全局变量赋初值:即.bss段的内容。
全局对象初始化,在main之前调用构造函数。
将main函数的参数argc,argv等传递给main函数,然后才真正运行 main函数。
全局对象的析构函数会在main函数之后执行
描述内存分配方式以及它们的区别
从静态存储区域分配。程序编译时分配,整个运行期间都存在。全局变量,static 变量。
在栈上创建。在执行函数时,函数内局部变量在栈上创建,执行结束自动被释放。
从堆上分配,动态内存分配。用malloc 或new 申请的内存,程序员自己负责释放。
简述数组与指针的区别?
使用指针指向字符串,字符串在只读内存区,无法修改。
sizeof可以计算数组大小,计算指针只能是指针大小。
当数组作为函数的参数传递时,数组自动退化为同类型的指针。
数组有自己的内存空间,指针指向常量区。
const 有什么用途?
常量指针:const int *m1,不能修改指向内容的值。
指针常量:int* const m1,不能指向其他的内存。
修饰传入函数的参数:不能改变参数的值。
修饰函数返回参数,与上面类似。
修饰成员变量:不能修改,只能在初始化列表中赋值。
修饰成员函数:const成员函数不能调用非const成员函数。
const,#define,typedf
const定义的常量有类型,具有类型安全,可以进行类型检查。
#define定义的宏没有类型,不进行类型检查,仅是简单的文本替换。
typedef用于为类型定义别名,不进行文本替换,使代码更简洁。
全局变量和局部变量有什么区别?
全局变量随作用域在整程序,主程序创建和创建,随主程序销毁而销毁;
局部变量作用域在函数内部,甚至局部循环体等内部存在,退出就不存在;
全局变量分配在全局数据段,程序运行时被加载。局部变量则分配在堆栈里面 。
C++中的四种类型转换有哪些?
- static_cast:可以实现 C++中内置基本数据类型之间的相互转换
- const_cast: 只能在同种类间转换,转换成const 类型,或者把 const 属性去掉
- dynamic_cast:用于动态类型转换。基类指针与派生类指针间的转换。
- reinterpret_cast:重新解释,此标识符的意思即为数据的二进制形式重新解释。
Inline 函数是什么意思?
内联函数避免了函数调用的开销,包括压栈、跳转和返回等操作,增强性能。
代码直接插入调用点,增大代码体积,增加编译时间。只内联小而平凡的函数。
C++异常怎么实现的
在执行程序发生异常,抛出错误信息,逐级上传,直到最高一级还无法处理,系统会自动调用系统函数 terminate,由它调用 abort终止程序。
把需要检查的语句放在 try 模块中,检查语句发生错误;throw 抛出异常,发出错误信息;由 catch 来捕获异常信息,并加以处理。
智能指针 auto_ptr?(shared_ptr, unique_ptr,weak_ptr)
动态内存管理经常会出现两种问题: ① 忘记释放内存,会造成内存泄漏 ② 在指针引用内存的情况下就释放了它,产生引用非法内存的指针。
shared_ptr:允许多个指针指向同一个对象。当这个对象所有的智能指针被销毁时就会自动进行回收,采用引用计数的机制进行维护。
unique_ptr:“独占”所指向的对象。不支持复制和赋值,直接赋值会编译出错。 实在想赋值的话,需要使用:std::move。
weak_ptr:是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。
make_shared与(new)的shared_ptr区别?
make_shared是一个非成员函数,只分配一次内存, (new)的shared_ptr至少两次分配内存。额外的开销可能会导致内存溢出。
什么时候使用只能指针?
程序不知道自己需要使用多少对象
.程序不知道所需对象的准确类型
.程序需要在多个对象间共享数据。
智能指针如果实现
智能指针是一个类,构造函数中传入一个指针,析构函数释放指针。
创建对象,计数初始化为1;对象进行拷贝时,计数加一,进行赋值时左-1右+1;
调用析构-1,为0的时候delete。
野指针,weakptr
当目标对象销毁时,weak_ptr 自然指向 null,不存在野指针的问题。
野指针:初始化后,指向的内存被释放,指针没有置为空。
空悬指针,指针没有初始化,直接赋值。Int *p; *p = 10;
实现 memcpy 需要注意哪些
memcpy(void* dest, const void* src, size_t num)
即从源source 中拷贝 num 个字节到目标 destin 中。
源和目标重叠,需要从src末端开始复制。
string的底层实现
对char进行封装,包含了char数组,容量,长度等属性。
可以动态扩展,每次扩展申请一个2倍的空间,再将源字符拷贝过去。
string与char*,char[ ]转换
const char* p = str.data();const char *p = str.c_str();
当声明了string类型变量s后,用printf("%s",s);是 会出错的。
string转char[],循环赋值,最后加上‘\0’
string与数字的转换
str = to_string(3.1415);
stoi(int),stol(long), stof(float), stod(double)
Volatile 关键字作用?
volatile的意思是“脆弱的”,表明它修饰的变量的值十分容易被改变,所以 编译器就不会对这个变量进行优化,进而提供稳定的访问。
静态成员函数
它们是与类而不是与实例相关联的。
静态成员函数只能访问类的静态成员
静态成员函数可以通过类名直接调用,
静态成员变量在类的所有实例中是共享的
static 关键字的用处
局部变量 系统不会对其初始化 作用域为代码块 储存在栈区
静态局部 必须初始化,系统自动初始化 代码块,一直存在 全变量区
全局变量 自动初始化,其他文件不能重定义 整个程序 全局变量区
静态全局 其他文件可以定义同名,不影响 只在文件内
类的隐形转换
使用单参数构造函数进行隐式转换,MyClass obj = 10;将 int 转换为 MyClass 类型。
从类类型到其他类型的隐式转换,operator int() const从 MyClass 到 int 类型。
explicit MyClass(int v) 和 explicit operator int() const 禁止了隐式转换。
危害:
可能不经意间发生,难以发现错误,难以维护。
可能导致构造函数和类型转换平凡调用,影响性能。
memset可以初始化类吗?
可以,但是如果类含有虚函数,虚表指针会被初始化为null,从而找不到虚函数表。
结构体类型中包含指针时,memset会把已经分配内存的p_x指针置为0,内存泄漏。
extern "C" { }
就是为了能够正确实现 C++代码调用其他 C 语言代码,实现 C++与 C 及 其它语言的混合编程。
*malloc 是怎么在堆里分配内存的
序由低到高的地址搜索“堆链”中的堆块,n,(heap1→heap2→heap3….heapn)
若 Heap1 == n,从“堆链”中删除Heap1,重新组织“堆链”
若 Heap1 > n,分裂部分Heap1,重新组织“堆链”
若 Heap1 >> n,malloc(n)多次申请的内存块是连续,由于header,用户使用时不连续。
auto 与decltype
类型不为引用,auto不保留const 属性; const auto n = x; auto f = n; auto 被推导为 int。
auto不能作为函数参数,非静态成员变量,数组类型,和模板实例化类型。
decltype返回expression 的类型时,附带expression原本的const与引用属性的;
expression是表达式时,编译器只分析表达式的类型,不计算它的值;(引用必须初始化)
expression是函数,则返回函数返回值类型;是带括号的变量,则返回值为引用类型;
float* func(){}; decltype(func) b4; int i= 0; decltype((i)) d2;
auto不会附带等号右侧类型的 ‘ & ’、‘ * ’ 以及const, 而decltype会。
auto在遇到数组时,编译器会替换为数组首元素的指针,但是decltype不会;
段错误
段错误就是访问了不可访问的内存,这个内存区要么不存在,要么是受到系统保护的,还有可能是缺少文件或者文件损坏。
1、访问不存在的内存地址。 int *ptr = NULL; *ptr = 0;
2、访问系统保护的内存地址 。int *ptr = (int *)0; *ptr = 100;
3、访问只读的内存地址。 char *ptr = "hello"; *ptr = 'H';
4、打印空指针。 int *ptr = NULL;printf ("%d\n", *ptr);
5、堆栈溢出 ,内存越界(数组越界,变量类型不一致等)
delete this
在成员函数中调用delete this,会导致指针错误,而在析构函数中 调用delete this,导致死循环,造成堆栈溢出。
C++和 和 java 内存管理的区别
new 一个对象
C++:需要手动 delete 掉,时刻记住能 delete 的最早时间(避免使用空指针),灵活性好。自动释放可以用 shared_ptr 和 weak_ptr 配合使用
Java:有一个垃圾回收器,自动将不需要的对象内存给释放掉,消除了内存泄露的问题,健壮性更好。
C++和python的区别
python是脚本语言,是解释执行的,而C++是编译语言,需要编译后在特定平台运行。python可以很方便的跨平台,但是效率没有 C++高。
python使用缩进来区分不同的代码块,C++使用花括号来区分。
C++中需要事先定义变量的类型,而python不需要。
python的库函数比C++的多,调用起来很方便。
C 和 和 C++ 的区别
C是面向过程的语言,C++是面向对象的语言,C++有“封装,继承和多态”的 特性。
C和C++内存管理的方法不一样,C使用malloc/free,C++除此之外还用 new/delete。
C++中还有函数重载和引用等概念,C中没有。
为什么C++没有实现垃圾回收?
额外的空间和时间开销。需要保存指针的引用计数。需要单独开辟一个线程free操作。
c++内存模型、(堆、栈、全局变量,静态变量,常 量,代码段)
1.栈(stack):由编译器在需要的时候分配,不需要的时候自动清除,通常是局部变量、函数参数等。分配的内存容量有限,地址向下增长 。
2.堆(heap):由new分配的内存块,由自己管理释放。需要程序员自己申请,并指明大小。C中malloc,C++中用new。p1 = (char *)malloc(10); int *p2 = new int(2);
但是注意p1、p2本身是在栈中的,分配方式倒是类似于链表。地址向上增长。
3.自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区。内存在程序编译的时候就已经分配 好,这块内存在程序的整个运行期间都存在。它主要存放静态数据(局部 static变量,全局static变量)、全局变量。
5.常量存储区:这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改
6.程序代码区:存放函数体的二进制代码。
7.bss段: 定义而没有赋初值的全局变量和静态变量,放在这个区域,通常只是记录变量名和大小,相当于一个占位符。
为什么堆空间因为会有频繁的分配释放操 作,会产生内存碎片?(内存池)
此动态分配将不可避免会产生内存碎片的问题。
内存池在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下) 的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。避免了内存碎片,使得内存分配效率得到提升。
在需要平凡开辟和释放内存的时候可以使用内存池。
堆快一点还是栈快一点?
栈快一点。操作系统会在底层对栈提供支持,分配专门的寄存器存放栈的地址。
堆的操作由C/C++函数库提供,分配堆内存时需要一定的算法寻找合适大小的内存。
获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存。
内存泄露出现的原因
1. 在类的构造函数和析构函数中没有匹配的调用 new 和 delete 函数
2. 没有正确地清除嵌套的对象指针
3. 在释放对象数组时在 delete 中没有使用方括号
4. 释放指向对象的指针数组,通过循环,将每个对象释放了,然后再把指针释放了。
5. 缺少拷贝构造函数
6. 两次释放相同的内存,浅拷贝。
7. 没有将基类的析构函数定义为虚函数
内存泄漏的排查
将 malloc 和 free 映射到调试版本,把申请内存和释放内存的信息写到一个文件中,然后去查找。 可以用现有的工具去做:valgrind 查找内存泄漏问题,可以使用valgrind、malloc_stats和malloc_info 监控查看内存情况。
避免内存泄漏的机制
1、不要手动管理内存,可以尝试在适用的情况下使用智能指针。
2、使用 string 而不是 char*。string内部处理所有内存管理,速度快且优化得很好。
3、除非要用旧的 lib 接口,否则不要使用原始指针。
4、尽可能少地在程序级别上进行 new 和 delete 调用。
5、最好在类的构造函数中分配内存并在析构函数中释放内存
6、使用了内存分配的函数,要记得使用其想用的函数释放掉内存。
模板特化的概念是什么?为什么要特化?
➢ 类的特化:
① 全特化:就是模板中模板参数全被指定为确定的类型。 全特化也就是定 义了一个全新的类型,全特化的类中的函数可以与模板类不一样。
② 偏特化:模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。
死锁产生原因
第1个线程访问资源AB,第22个访问BA,A被1加锁,B被2加锁,形成死锁。
1. 互斥条件:进程所需资源具有排他性,其他进程请求该资源,只能等待。
2. 不剥夺条件:进程获得的资源未释放前,不能被其他进程强行夺走,只能自己释放。
3. 请求和保持条件:在进程请求其他新资源时,由该进程继续占有。
4. 循环等待条件:每个进程获得的资源同时被循环等待链中下一个进程所请求。
解决死锁方法:
保持上锁资源顺序的一致性,可以避免死锁。
预防死锁:通过设置限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件。避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态。
检测死锁:可设置检测机构及时检测死锁并采取适当措施。
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
构造函数和析构函数中是否可以调用虚函数?
可以但没必要。进入构造函数和析构函数的时候,对象也会发生变化,会调用自己的虚函数,也就是说父类构造函数中只能调用自己的虚函数,因此写成普通的就行了。
构造函数可否是虚函数,为什么?析构函 数呢,可否是纯虚的呢?
构造函数不行,在对象创建时,虚函数依赖于虚表,而虚表又是在对象构造时建立的。
在构造对象时,必须明确对象的类型,以便分配正确的内存和初始化数据成员。
析构函数可以,特别是多态,父类指针指向的子类对象才能调用父类析构释放资源。
如果父类不想实现什么方法,可以定义纯虚析构,使其成为抽象类,防止创建父类对象。
类的公有继承是is,表示一样。私有继承是has,部分一样。
成员初始化列表的概念,为什么用成员初始化列表会快一些
一个冒号,使用多个括号和逗号初始化成员。
直接调用传入参数的拷贝构造函数,使用=需要调用构造再调用拷贝构造。
必须使用成员初始化列表的情况:
① 常量成员,const 修饰的成员变量,因为常量只能初始化不能赋值,
② 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,
③ 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数。
④ 如果类存在继承关系,派生类想调用基类的构造函数
线程池,内存池,对象池,都是为了避免频繁的创建和销毁。
左值引用和右值引用
可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;
完美转发与转移语义:std::forword(u) std::move
转移语义(Move Semantics) 通过右值引用实现对象资源的高效转移,避免不必要的复制操作。常见的实现包括移动构造函数和移动赋值运算符。
完美转发(Perfect Forwarding) 允许模板函数将参数完美地转发给另一个函数,保持参数的类型和值类别不变,通常通过std::forward函数实现。
空类的大小
空类的大小是1, 在C++中空类占一个字节,是为了让对象的实例能够相互区别,实例化对象后,每个对象在内存中都有独一无二的地址。作为基类则被优化为0;
类的成员函数为什么不占用类的大小
成员函数的大小不在类的对象里面,同一个类的多个对象共享函数代码。
访问成员函数和普通函数一样会发生跳转产生入栈出栈的开销。
这也就是为什么我们提倡把一些简短的,调用频率高的函数声明为inline形式。
介绍C++所有的构造函数
默认构造函数是当类没有实现自己的构造函数时,编译器默认提供的一 个构造函数。 重载构造函数也称为一般构造函数,一个类可以有多个重载构造函数。
拷贝构造函数是在发生对象复制的时候调用的。
新建一个空类,里面有什么函数
默认构造函数、拷贝构造、析构函数、赋值运算符、取址运算符、 const 取址运算符。
私有虚函数还可以实现多态吗?
多态性与将实现多态的函数的访问限定符没有任何关系,private 函数仍然 可以实现多态,它的指针仍然位于vtbl中,只不过该函数的多态一般只能在基类的内部由其他非虚函数调用该函数。(可以,但是想调用只能提供其他函数接口)
模板类的继承包括四种:
类模板实例化后就是模板类,具体的不能继承抽象的,抽象的能继承所有具体的
多参数传入
int sum(int count, ...) va_list arg_ptr; va_start(arg_ptr, count);
可变参数initializer_list:用法vector类似,比vector轻量,而且元素是常量。
调试程序的方法
通过设置断点进行调试
打印log进行调试
打印中间结果进行调试
Linux里面发生core dump(核心转储)错误
程序异常退出或者终止,生成的一个core文件,记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息,以此定位异常来源。
coredump产生的条件
- shell资源控制限制,使用 ulimit -c 命令查看shell执行程序时的资源 ,如果为0,则不会产生coredump。可以用ulimit -c unlimited设置为不限大小。
- 读写越界,包括:数组访问越界,指针指向错误的内存,字符串读写越界
- 使用了线程不安全的函数,读写未加锁保护
- 错误使用指针转换,堆栈溢出
友元函数和友元类
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。B是A的友元,A不一定是B的友元,
(3) 友元关系不具有传递性。B是A友元,C是B友元,C不一定是A的友元
内存分配的原理说一 下/malloc函数底层是怎么实现的?
两个系统调用,扩和找
brk是将进程数据段(.data)的最高地址指针向高处移动,扩大进程在运行时的堆大小
mmap从进程的虚拟地址空间中寻找一块空闲的虚拟内存,获得一块可以操作的堆内存。
C++ 三/五原则
单一职责原则SRP(Single Responsibility Principle):是指一个类的功能要单一,不能包罗万象。什么都让他干,效率就不会高。
开放封闭原则OCP(Open一Close Principle):一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。扩展开发,更改封闭,添加新功能不能修改原先的功能。
里式替换原则LSP(the Liskov Substitution Principle LSP):子类应当可以替换父类并出现在父类能够出现的任何地方。
依赖倒置原则DIP(the Dependency Inversion Principle DIP): 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。假设A是上层,B是下层,但A需要使用到B的功能,不应该直接在A里面创建B的类来使用B的方法;而应当由A定义一抽象接口,并由B来实现这个抽象接口,A只使用这个抽象接口。以后需要添加一个下层C类,只需要继承和重写这个抽象类。
接口分离原则ISP(the Interface Segregation Principle ISP):模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来。
模版是静态编译,为什么可以动态绑定?
动态绑定主要与多态性有关,通常是通过虚函数实现的。尽管模板是静态的,但在某些情况下,模板可以与动态绑定结合使用,从而实现灵活且高效的代码。
模板特化与偏特化
模板特化分为完全特化和部分特化。完全特化指的是为某个特定类型提供特化的实现。偏特化则是为一组类型模式提供特化的实现,也就是一部分确定,一部分写T。
智能指针shared_ptr优缺点
优点
- 自动内存管理:
- std::shared_ptr 自动管理对象的生命周期,确保对象在最后一个 shared_ptr 被销毁时自动删除,防止内存泄漏。
- 共享所有权:
- 允许多个 std::shared_ptr 实例共享同一个对象的所有权。对象会在最后一个 shared_ptr 被销毁时删除,这对处理复杂的数据结构和对象关系非常有用。
- 类型安全:
- std::shared_ptr 提供了类型安全的指针操作,避免了裸指针的类型错误问题。
- 线程安全:
- std::shared_ptr 的引用计数机制在内部是线程安全的。不同线程中的 std::shared_ptr 实例可以安全地增加或减少引用计数。
- 灵活性:
- 可以与 std::weak_ptr 结合使用,避免循环引用问题。std::weak_ptr 提供了一种观察 std::shared_ptr 所管理对象的方法,而不会增加引用计数。
缺点
- 性能开销:
- std::shared_ptr 引入了额外的性能开销,因为它需要维护引用计数并处理线程安全的引用计数操作。这可能导致比裸指针或其他智能指针(如 std::unique_ptr)更高的性能开销。
- 循环引用问题:
- 尽管 std::shared_ptr 可以共享所有权,但如果对象之间存在循环引用(即两个或多个 std::shared_ptr 互相引用),引用计数可能不会降到零,导致内存泄漏。使用 std::weak_ptr 可以缓解这个问题。
- 复杂性:
- 使用 std::shared_ptr 可能增加程序的复杂性,特别是在涉及大量的 std::shared_ptr 和复杂的对象关系时。正确地使用 std::shared_ptr 需要对其内部机制有一定的理解。
- 无法处理裸指针的所有权:
- std::shared_ptr 不适合用于管理裸指针的所有权。如果你使用裸指针创建 std::shared_ptr,需要确保裸指针的生命周期与 std::shared_ptr 的生命周期一致,否则可能导致未定义行为。
- 无法使用自定义删除器的所有权:
- 如果使用自定义删除器时,需要小心,因为 std::shared_ptr 只在引用计数为零时调用删除器。如果删除器与对象的生命周期不匹配,可能会导致错误。
共享指针(智能指针)线程安全问题
同一个shared_ptr被多线程读,线程安全;
同一个shared_ptr被多线程写,不是线程安全;
共享引用计数的不同的shared_ptr被多线程写,是线程安全。