C++的四种智能指针
为什么要使用智能指针?
智能指针的作用是管理一个指针,避免程序员申请的空间在函数结束时忘记释放,造成内存泄露这种情况的发生使用智能指针可以很大程度的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源,所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间
四种智能指针:
auto_ptr(C11已抛弃):采用所有权模式,存在潜在的内存崩溃问题
| |
unique_ptr(替换auto_ptr):实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象它相对auto_ptr而言禁止了拷贝操作,采用std:move来进行控制权的转移,对于避免资源泄露特别有用
| |
所有权模式,但比auto_ptr更加安全
**shared_ptr(共享型,强引用)**实现共享式拥有概念,多个智能指针可以指向相同的对象,该对象和其相关资源会在最后一个引用被销毁时释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享
可以通过成员函数use_count()来查看资源的所有者个数,除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放
share_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用技术的机制上提供了可以共享所有权的智能指针
shared_ptr 的引用计数存储在一个 动态分配的控制块(Control Block) 中,控制块包含以下核心信息
当 强引用计数 和 弱引用计数 同时归零时,控制块才会被释放。
**weak_ptr(弱引用)**weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr。
weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,他的构造和析构不会引起引用计数的增加或减少
weak_ptr是用来解决shared_ptr互相引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得
当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以。
智能指针的缺点
智能指针(如 C++ 的 shared_ptr、unique_ptr)虽然能简化内存管理,但存在以下问题:
| 缺点 | 说明 | 示例/解决方案 |
|---|---|---|
| 性能开销 | 引用计数(shared_ptr )需额外操作,影响高频场景性能。 | 避免在关键路径过度使用 shared_ptr ,优先用 unique_ptr 。 |
| 循环引用 | shared_ptr 相互引用导致内存泄漏。 | 使用 weak_ptr 打破循环(如 GUI 父子节点双向引用)。 |
| 所有权模糊 | 过度共享 shared_ptr 可能导致逻辑混乱。 | 明确所有权归属,优先用 unique_ptr 独占资源。 |
| 与原生指针混用风险 | 混用时可能引发悬垂指针或重复释放。 | 避免将智能指针的裸指针暴露给外部。 |
| 线程安全代价 | shared_ptr 的引用计数原子操作有性能损耗。 | 若无跨线程共享需求,可禁用原子操作(如 boost::shared_ptr 的局部用法)。 |
智能指针是否是线程安全的
shared_ptr线程安全的部分:引用计数的增减:通过原子操作保证,多个线程同时拷贝或析构 shared_ptr 时,引用计数正确。
线程不安全的部分:对同一 shared_ptr 实例的写操作:如多个线程同时修改同一个 shared_ptr(例如 reset() 或赋值),需加锁。对管理对象的访问:多个线程直接读写 shared_ptr 指向的对象,需同步(如互斥锁)。
unique_ptr完全线程不安全:移动操作(std::move):需保证所有权转移的原子性(通常需加锁)。默认禁止跨线程操作:若需跨线程传递,需明确移动所有权,并确保目标线程独占访问。
C++中的内存分配情况
栈:由编译器分配管理和回收,存放局部变量和函数参数,空间较小堆:由程序员管理,需要手动new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄露和空闲碎片的情况
- 生长方向不同:堆生长方向是向上的,也就是向着内存地址增加的方向,栈刚好相反,向着内存减小的方向生长。
- 分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
- 分配效率不同:栈的效率比堆高很多。栈是机器系统提供的数据结构,计算机在底层提供栈的支持,分配专门的寄存器来存放栈的地址,压栈出栈都有相应的指令,因此比较快。堆是由库函数提供的,机制很复杂,库函数会按照一定的算法进行搜索内存,因此比较慢
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量常量存储区:储存常量,一般不允许修改代码区:存放程序的二进制代码
静态全局变量与全局变量的区别
- 静态全局变量和全局变量都属于 常量区
- 静态全局变量只在本文件中有效,别的文件想调用该变量是调不了的,而全局变量可以在别的文件中调用。
- 如果别的文件中定义了一个该全局变量相同的变量名,是会出错的。
静态局部变量和局部变量的区别
- 静态局部变量是属于常量区的,而函数内部的局部变量属于栈区
- 静态局部变量在该函数调用结束时,不会销毁,而是随整个程序结束而结束,但是别的函数调用不了该变量,局部变量随该函数的结束而结束。
- 如果定义这两个变量的时候没有初始值时,静态局部变量会初始化为0,而局部变量就是一个随机值。
- 静态局部变量在编译期间只赋值一次,以后每次函数调用时,不再赋值,调用上次的函数调用结束时的值。局部变量在调用期间,每调用一次,赋一次值。
C++中的指针参数传递和引用参数传递
指针参数传递本质上是值传递:他所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的参数值,从而形成了实参的一个副本。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调参数中的实参变量。因此,被调函数对形参的任何操作都会影响主调函数的实参变量
引⽤传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变量,但是对于任何引用参数的处理都会通过一个间接寻址的方式操作到主调函数的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量,如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针或者指针引用
C++ 中 const 和 static 关键字(定义,⽤途)
static:控制变量的存储方式和可见性。
- 修饰局部变量:一般情况下,对于局部变量在程序中是存放在栈区的,并且局部的生命周期在包含语句块执行结束时便结束了。但是如果用static关键词修饰的话,该变量便会存放在静态数据区,其生命周期会一直延续到整个程序执行结束,但是要注意的时,虽然用static对全局变量进行修饰之后,其生命周期以及存储空间发生了变换,但其作用域并没有改变,作用域还是限制在其语句块
- 修饰全局变量:对于一个全局变量,它既可以在本文件中被访问到,也可以在同一个工程中其他源文件被访问。用static对全局变量进行修饰改变了其作用域范围,由原来的整个工程可见变成了本文件可见
- 修饰函数:和全局变量类似,也是改变了函数的作用域
- 修饰类:如果C++中对类中的某个函数用static修饰,则表示该函数属于一个类而不是属于此类的任何特定对象,如果对类中的某个变量进行static修饰,则表示该变量在存储空间中只存在一个副本,可以通过类和对象去调用
- 作用五:类成员/类函数声明 static
- 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
- 在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
- 在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
- 在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
- 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的 static 成员变量
- static 类对象必须要在类外进行初始化,static 修饰的变量先于对象存在,所以 static 修饰的变量要在类外初始化;
- 由于 static 修饰的类成员属于类,不属于对象,因此 static类成员函数是没有 this 指针,this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问非 static 的类成员,只能访问 static修饰的类成员;
- static 成员函数不能被 vrtual 修饰,static成员不属于任何对象或实例,所以加上 vrtual 没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 是通过 this 指针调用的,所以不能为 virtual;虚函数的调用关系,this->vptr->ctable->virtual function。
const含义及实现机制
- const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值即可。
- const 修饰指针变量和引用变量:如果 const 位于小星星的左侧、则 const 就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于小星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
- const 应用到函数中:作为参数的 const 修饰符:调用函数的时候,用相应的变量初始化,const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性。[注意:参数 const 通常用于参数为指针或引用的情况: 作为函数返回值的 const 修饰符:声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
- const 在类中的用法:const 成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。const 成员函数:const 成员函数的主要目的是防止成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
- const 修饰类对象,定义常量对象:常量对象只能调用常量函数,别的成员函数都不能调用,
补充:const 成员函数中如果实在想修改某个变量,可以使用 mutable 进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现或者 static const。

C++ 中重载重写的区别
重载(overload)
是指同一可访问区内被声明的几个具有不同参数列表的同名函数,依赖于C++函数名字的修饰会将参数加在后面,可以是参数类型,个数,顺序的不同,根据参数列表决定调用哪个函数,重载不关心函数的返回类型
重写(override)
派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是static的,一定要是虚函数,且其他一定要完全相同。要注意一,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管virtual中是private的,派生类中重写可以改为public
指针和引用的区别
指针和引用都是一种内存地址的概念,区别是指针是一个实体,引用只是一个别名指针指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有const和非const区别,甚至可以为空,sizeof得到的是指针类型的大小而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将“引用变量名-引用对象的地址”添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有const和非const的区别sizeof引用得到代表对象的大小。而sizeof指针得到的是指针本身的大小。另外在参数传递中指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的是指是传地址,传递的变量的地址
野指针和空指针的区别
野指针(wild pointer):就是没有被初始化过的指针。用 gcc -wa11 编译,会出现 used uninitialized 警告空指针:是指针最初指向的内存已经被释放了的一种指针。无论是野指针还是空指针,都是指向无效内存区域(这里的无效指的是"不安全不可控")的指针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。如何避免使用野指针?在平时的编码中,养成在定义指针后且在使用之前完成初始化的习惯或者使用智能指针。
函数传递参数的⼏种⽅式
值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯
new / delete ,malloc / free 区别
都可以用来在堆上分配和回收空间,new/delete是操作符(可以被重载)。malloc/free是库函数malloc使用时需要显式的计算内存大小,使用new则由编译器自动计算
delete 不需要指明内存大小的原因内存管理器记录元数据:new 操作时会额外分配一个头部(header)来存储分配的内存大小等信息delete 的工作流程:通过指针找到头部信息根据头部信息获取内存块大小释放整个内存块
执行new实际上执行两个过程:1.分配未初始化的内存空间;2.使用对象的构造函数对空间进行初始化,返回空间的首地址。如果在第一部分配空间中出现问题,则抛出std::bad_alloc异常,或被某个设定的异常处理函数捕获处理;如果在第二部构造对象是出现异常,则自动调用delete释放内存
执⾏ delete 实际上也有两个过程:1. 使⽤析构函数对对象进⾏析构;2.回收内存空间(free)。以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,⽽ malloc 得到的是未初始化的空间。
所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete ⼀个类型,free ⼀个字节⻓度的空间。
为什么有了 malloc/free 还需要 new/delete?因为对于⾮内部数据类型⽽⾔,光⽤ malloc/free ⽆法满⾜动态对象的要求。对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前要⾃动执⾏析构函数。由于 malloc/free 是库函数⽽不是运算符,不在编译器控制权限之内,不能够把执⾏的构造函数和析构函数的任务强加于malloc/free,所以有了 new/delete 操作符。
面向对象的三大特性,并举例说明
C++面向对象的三大特征是:封装,继承,多态
封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分以外的改变或错误的使用了对象的私有部分
继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:他可以使用现有的类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展。通过继承创建的新类成为子类或派生类,被继承的类称为基类,父类或超类。继承的过程,就是从一般到特殊的过程,要实现继承,可以通过继承和组合来实现
实现方式:实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力接口继承:接口继承是指仅使用属性和方法的名称,但是子类必须提供实现的能力
多态就是像不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译期间确定,需要在运行时才确定,这就属于晚绑定
多态的实现
多态的三要素:继承,方法重写,父类引用指向子类对象多态是指同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。简单来说,就是用一个名字来表示多种不同的行为或功能。继承主要是一种代码复用和层次结构构建的机制,它强调的是类之间的关系,即一个类可以基于另一个类来创建,形成一种 “是一种(is-a)” 的关系,如狗是一种动物。多态则更侧重于行为的多样性,关注的是同一个操作在不同对象上的不同表现形式。
**多态在实际开发中的应用场景:**在图形绘制软件中,可能会有多种不同类型的图形,如圆形、矩形、三角形等。可以定义一个基类Shape,其中包含一个绘制图形的虚函数draw。然后每个具体的图形类(如Circle、Rectangle、Triangle)都继承自Shape类,并各自实现draw函数。当需要绘制一组不同图形时,只需要将这些图形对象的指针或引用存储在一个容器中,然后遍历容器调用draw函数,根据对象的实际类型,多态机制会自动调用相应图形类的draw函数来绘制出不同的图形。
多态可以分为静态多态和动态多态。
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态什么叫运行期间决定调用的函数呢???一般情况下我们不区分这两个时所说的多态就是指动态多态。动态多态的实现与虚函数表,虚函数指针相关。
虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型时派生类,就调用派生类的函数,如果是基类,就调用基类的函数。实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。
编译器处理虚函数表应该如何处理
编译器建立虚函数表的过程其实一共是三个步骤:拷贝基类的虚函数表,如果是多继承,就拷贝每个虚函数基类的虚函数表还有一个基类的虚函数和派生类自身的虚函数共用了一个虚函数表,也称为某个基类为派生类的主基类查看派生类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址,查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中

析构函数一般协程虚函数的原因
是为了降低内存泄露的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数,仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄露
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存
构造函数为什么一般不定义为虚函数
虚函数调用只需要知道部分的信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的花,是需要知道对象的完整信息的。特别是,需要知道创建对象的确切类型,因此,构造函数不应该被定义成虚函数
从目前编译器实现虚函数进行多态的方法来看,虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数时虚的,那么虚函数表指针则是不存在的,无法找到队形的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则
析构函数的作用,如何起作用
构造函数只是起初始化值的作用,当实例化一个对象的时候,可以通过实例区传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,哪怕不写编辑器也自动调用一次析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员
构造函数的执行顺序?析构函数的执行顺序
构造函数顺序
- 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是他们在成员初始化表中的顺序
- 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序
- 派生类构造函数
析构函数顺序
- 调用派生类的析构函数
- 调用成员类对象的析构函数
- 调用基类的析构函数
纯虚函数(应用于接口继承和实现继承)
实际上,纯虚函数的出现就是为了让继承可以出现多种情况:有时我们希望派生类只继承成员函数的接口有时我们又希望派生类既继承成员函数的接口,又继承成员函数的实现,而且可以在派生类中重写成员函数以实现多态有的时候我们又希望派生类在继承成员函数接口和实现的情况下,不能重写缺省的实现其实,声明一个纯虚函数的目的就是为了让派生类只继承函数的接口,而且派生类中必须提供一个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进行实例化对于纯虚函数来说,我呢吧其实是可以给他提供实现代码的,但是由于抽象类不能实例化,调用这个实现的唯一方式是在派生类中指出其class名称来调用
深拷贝和浅拷贝的区别
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的但当数据成员中有指针时,如果采用简单的浅拷贝,不会创建新的内存,而是直接将原来的对象的地址给了新的对象,这样两类中的两个指针指向同一个地址,当对象快要结束时,会调用两次析构函数,而导致野指针的问题所以,这时必须采用深拷贝。深拷贝与浅拷贝之间的区别就在于深拷贝会显式的提供拷贝构造函数和赋值运算符,在堆内存中另外申请空间来存储数据,从而也就解决了野指针的问题简而言之,当数据成员中有指针时,必须要用深拷贝更加安全
什么情况下会调用拷贝构造函数
类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数
- 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象押入到栈空间中
- 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值
- 一个对象需要通过另一个对象进行初始化
为什么拷⻉构造函数必需时引⽤传递,不能是值传递?
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,
内存泄露的定义,如何检测和避免
内存泄露简单的说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行事件越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了
- 堆内存泄漏(Heap leak)堆内存指的是程序运行中根据需要分配通过malloc,realloc,new等从堆中分配的一块内存,在完成相关操作后必须通过调用对应的free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
- 系统资源泄露(Resource Leak)主要是指系统分配给程序的资源没有使用相应的函数释放掉(比如 Bitmap,handle,socket等),导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
说⼀下红⿊树(RB-tree)
一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是red或black.通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,它是一种弱平衡二叉树(由于是弱平衡,可以推出,相同的节点情况下,AVL 树的高度低于红黑树),相对于要求严格的 AVL 树来说,它的旋转次数变少,所以对于搜索,插入,删除操作多的情况下,我们就用红黑树
红⿊树的定义:性质1:每个节点要么是⿊⾊,要么是红⾊。性质2:根节点是⿊⾊。性质3:每个叶⼦节点(NIL)是⿊⾊。性质4:每个红⾊结点的两个⼦结点⼀定都是⿊⾊。性质5:任意⼀结点到每个叶⼦结点的路径都包含数
Lambda表达式
Lambda表达式实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的利用Lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且是代码更可读从本质上来讲,Lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现,但是它简便的语法却给C++带来了深远的影响从广义上说,Lambda表达式产生的是函数对象。在类中,可以重载函数调用运算符,此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象或者仿函数。相比Lambda表达式,函数对象有自己独特的优势Lambda表达式一般都是从方括号开始[],然后结束于花括号{},花括号里面就像定义函数那样,包含了Lambda表达式体,一个简单例子如下
| |
上面是最简单的lambda表达式,没有参数。如果需要参数,那么就要像函数那样放在圆括号里面,如果有返回值,返回类型要放在->后面,即拖尾返回类型,当然也可以忽略返回类型,lambda会帮你自动推断返回类型:
| |
最前面的[]时Lambda表达式的一个很重要的功能,就是闭包。
Lambda表达式的大致原理:每当你定义一个Lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型
那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所哟,我们上面的lambda表达式的结果就是一个个闭包实例
闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前方的方括号就是用来定义捕捉模式以及变量,我们又将其成为lambda捕捉块
| |
捕获的⽅式可以是引⽤也可以是复制,但是具体说来会有以下⼏种情况来捕获其所在作⽤域中的变量:
- []默认不捕获任何变量;
- [=]默认以值捕获所有变量;
- [&]默认以引用捕获所有变量;
- 仅以值捕获x,其它变量不捕获;
- [&x]仅以引用捕获x,其它变量不捕获;
- [=,&x]默认以值捕获所有变量,但是x是例外,通过引用捕获;[&,x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
- [this]通过引用捕获当前对象(其实是复制指针);。
- [*this1] 通过传值方式捕获当前对象;而 lambda 表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。其实,最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过 lambda 表达式给出条件,传递给count_if函数:
| |
最后给出Lambda表达式的完整语法:
| |
capture-list:捕捉列表,这个不⽤多说,前⾯已经讲过,它不能省略;params:参数列表,可以省略(但是后⾯必须紧跟函数体);mutable:可选,将 lambda 表达式标记为 mutable 后,函数体就可以修改传值⽅式捕获的变量;constexpr:可选, C++17 ,可以指定 lambda 表达式是⼀个常量函数exception:可选,指定lambda表达式可以抛出的异常attribute:可选,指定lambda表达式的特性ret:可选,返回值类型body:函数执行体
Lambda表达式提供了一种便捷的方式,在需要函数对象的地方直接定义并传递简短的代码块,而无需事先进行函数的声明和定义。Lambda表达式的一个关键特性是它们可以捕获其所在作用域中的变量。这使得Lambda表达式非常适合于在算法函数(如std::sort、std::for_each等)中传递自定义的操作,或者在并发编程中传递函数对象。
| |
在这个例子中,std::sort函数的第三个参数是一个Lambda表达式,它定义了排序的规则:按照元素的绝对值从小到大排序。这个Lambda表达式没有捕获任何外部变量,只接受两个参数a和b,并返回一个布尔值。
右值引用
C++11 引入了右值引用的概念,通过使用 && 符号来声明右值引用,这样可以直接操作临时对象(右值),而不需要复制。这样可以大幅提升性能。
C++03 及之前的标准中,右值是不允许被改变的,实践中也通常使用 const T&的方式传递右值。然而这是效率低下的做法,例如:
| |
上述获取右值并初始化p的过程包含了 Person 的3个构造过程和2个析构过程。 这是C++ 广受诟病的一点,但C++11 的右值引用特性允许我们对右值进行修改。借此可以实现 move语义,即从右值中直接拿数据过来初始化或修改左值,而不需要重新构造左值后再析构右值。一个 move 构造函数是这样声明的:
| |
右值引用使得“移动”(move)语义成为可能,这允许开发者从一个右值中直接获取资源而不进行深拷贝。这样可以避免不必要的内存开销。
| |
智能指针
C++11 对于 C++ 标准库的变更。C++11 把 TR1 并⼊了进来,废弃了 C++98 中的 auto_ptr , 同时将 shared_ptr 和 uniq_ptr 并⼊ std 命名空间。
| |
final和override
C++借由虚函数实现运行时多态,但 C++ 的虚函数又很多脆弱的地方
- 无法禁止子类重写它。可能到某一层级时,我们不希望子类继续来重写当前虚函数了。
- 容易不小心隐藏父类的虚函数。比如在重写时,不小心声明了一个签名不一致但有同样名称的新函数。.
C++11 提供了 final 来禁止虛函数被重写/禁止类被继承,override 来显示地重写虚函数。这样编译器给我们不小心的行为提供更多有用的错误和警告。
| |
this指针
- this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向正在被该成员函数操作的那个对象。
- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,由隐含使用 this 指针。
- 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
- this 指针被隐含地声明为:className _const this,这意味着不能给 this指针赋值:在className类的 const 成员系数中,this 指针的类型为:constclassName_const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
- this 并不是一个常规变量,而是个右值,所以不能取得 this的地址(不能 &this)
- 在以下场景中,经常需要显式引用 this 指针:i.为实现对象的链式引用:ii.为避免对同一对象进行赋值操作;iii, 在实现一些数据结构时,如list
**this指针的定义:**this指针是一个隐含于每一个非静态成员函数中的特殊指针。这意味着,当你定义一个类的非静态成员函数时,这个函数内部实际上有一个指向当前对象的指针,这个指针就是this。**this指针的作用:**this指针指向正在被该成员函数操作的那个对象。换句话说,当你对一个对象调用其成员函数时,this指针就指向那个对象。**this指针的使用时机:**当对一个对象调用成员函数时,编译程序会先将对象的地址赋给this指针。然后,调用成员函数。每次成员函数存取数据成员时,实际上是通过隐含使用this指针来实现的。例如,如果你在成员函数内部访问一个数据成员x,实际上是在访问this->x。**this指针的隐含传递:**当一个成员函数被调用时,编译器会自动向它传递一个隐含的参数。这个隐含的参数是一个指向这个成员函数所在的对象的指针,这个指针就是this。因此,在成员函数内部,你可以直接使用this来引用调用该成员函数的对象。示例:
假设有一个类MyClass,它有一个数据成员x和一个成员函数show:
| |
当你创建一个MyClass的对象obj并调用它的show成员函数时:
| |
在show函数被调用时,编译器会自动将obj的地址传递给show函数内部的this指针。因此,在show函数内部,通过this->x(或简单地x,因为this是隐含的)可以访问到obj的x成员。
也就是说当你对一个对象调用成员函数时,编译器会执行以下步骤:
- 确定成员函数:编译器首先确定你正在调用的成员函数是哪个类的成员。
- 生成函数调用:编译器生成对成员函数的调用代码。
- 传递隐含参数(this指针):在这个调用过程中,编译器会自动添加一个隐含的参数,这个参数是一个指向调用成员函数的对象的指针,我们称之为this指针。这一步就是“编译程序会先将对象的地址赋给this指针”和“编译器会自动向它传递一个隐含的参数”所描述的内容。它们实际上是在说同一件事,只是从不同的角度来描述。
- 执行成员函数:当成员函数被执行时,它可以通过this指针来访问调用它的对象的成员(数据成员和其他成员函数)。
这个过程对于C++的面向对象编程至关重要,因为它允许成员函数知道它们正在操作哪个对象。没有this指针,成员函数就无法访问或修改调用它的对象的成员。
Static
控制变量和函数的存储方式和可见性。
(1)修饰局部变量
一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。
(2)修饰全局变量
对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。
(3)修饰函数
用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。
(4)C++中的static
如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。
const的含义及实现机制
const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。

(1)const修饰基本数据类型
1.const修饰一般常量及数组
基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。
2.const修饰指针变量*及引用变量&
如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
(2)const应用到函数中
1.作为参数的const修饰符
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。 [注意]:参数const通常用于参数为指针或引用的情况;
2.作为函数返回值的const修饰符
声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
(3)const在类中的用法不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行 类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量。
(4)const修饰类对象 定义常量对象 常量对象只能调用常量函数,别的成员函数都不能调用。
list和vector有什么区别?
vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。 list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。
内存池是什么
内存池是一种内存分配方式,它预先在内存中分配一定数量的块或对象,形成一个“池”。当程序需要分配内存时,它从这个池中分配一个块;当内存被释放时,这个块返回到池中以供再次使用。内存池可以显著减少频繁分配和释放内存所带来的开销,并且有助于避免内存碎片化,提高内存使用效率。
下面展示了如何设计一个简单的内存池,这个简单的内存池设计包括以下几个关键特性:
预分配:在构造函数中预先分配了一定数量的固定大小内存块。分配与释放:allocate 方法从池中分配一个内存块,而 deallocate 方法则将不再使用的内存块返还给池。管理策略:本例中使用 std::list 来管理空闲内存块,但实际应用中可能需考虑更高效的数据结构。
C++内存模型
程序运行时的内存布局,即代码加载到内存后的实际分布。
栈(Stack):函数局部变量、函数调用信息(后进先出)。堆(Heap):动态分配的内存(new/malloc)。全局/静态存储区:全局变量、静态变量(.data 为已初始化,.bss 为未初始化)。常量存储区:字符串常量、const 全局变量(.rodata)。代码区(Text):可执行代码(机器指令)。
可执行文件(ELF格式)的磁盘结构,即编译后的二进制文件在磁盘上的组织形式,运行时内存布局由可执行文件的结构映射而来**.text**:代码段(存放指令)。.data:已初始化的全局/静态变量。.bss:未初始化的全局/静态变量(程序加载时置零)。.rodata:只读数据(如字符串常量)。其他段:如符号表、调试信息等。
为什么堆和栈不在可执行文件中?堆:动态分配的内存地址和大小在编译时无法确定。栈:函数调用链和局部变量的生命周期在运行时动态变化。
程序是怎样跑起来的
编译阶段:
- 预处理(Preprocessing)输入:.c/.cpp 源码文件操作:展开宏定义(#define)处理条件编译指令(#ifdef、#endif)递归包含头文件(#include) 删除注释输出:预处理后的 .i 文件(纯文本,仍为C/C++代码)。
- 编译(Compilation)输入:预处理后的 .i 文件操作:词法分析 → 语法分析 → 语义分析生成中间代码(LLVM IR 或三地址码)优化逻辑(如常量折叠、死代码消除)转换为目标平台的汇编代码输出:汇编文件 .s(如 main.s)。
- 汇编(Assembly)输入:汇编文件 .s操作:将汇编代码逐行翻译为机器指令(二进制)生成 可重定位目标文件(Relocatable Object File)输出:.o 文件(如 main.o),包含机器码和符号表(未解析的地址)。
- 链接(Linking)输入:多个 .o 文件 + 静态库(.a/.lib)操作:符号解析:绑定函数/变量引用到实际地址(如 printf 的库实现)重定向:修正代码和数据段的相对地址为最终内存地址合并所有目标文件,生成可执行文件(ELF/PE格式)输出:可执行文件(如 a.out 或 .exe)。
**进程创建:**操作系统分配虚拟地址空间,加载可执行文件的代码段(.text)、数据段(.data、.bss)到内存。**初始化:**设置堆栈空间,初始化全局变量,调用全局对象的构造函数。**入口点:**跳转到程序入口(如_start),最终调用main()函数。**内存访问:**程序执行过程中,访问的数据和指令都存储在内存中。CPU通过内存管理单元(MMU)将虚拟地址映射为物理地址,实现对内存的读写操作。**系统调用:**程序可能需要与操作系统进行交互,如请求IO操作、创建新进程等。此时,程序通过系统调用进入内核态执行相应的系统服务。**异常处理:**在程序执行过程中,可能发生各种异常情况,如除零错误、内存访问越界等。操作系统会捕获这些异常并进行相应的处理,如终止进程或给出错误提示。**结束执行:**程序执行完后,操作系统回收资源,释放进程占用的内存空间,并通知用户程序执行完毕
类型推导
于C++11新特性,最先提到的肯定是类型推导,C++11引入了auto和decltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。
- auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
auto a = 10; // 10是int型,可以自动推导出a是int
- decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
| |
四种cast的转换
static_cast: static_cast 是用于编译时检测到的相关类型之间的转换,比如整型和浮点数、派生类和基类之间的指针或引用转换。它不能用于含有虚函数的多态基类指针或引用到派生类的转换,因为这需要运行时信息。
示例:
float f = static_cast
dynamic_cast: dynamic_cast 主要用于处理多态性。当涉及到继承体系中的向下转换(将基类的指针或引用转换为派生类类型)时,这个转换会检查转换的安全性。如果转换无效,对于指针,它会返回nullptr;对于引用,则抛出一个std::bad_cast异常。使用dynamic_cast需要运行时类型信息(RTTI),因此它有一定的性能代价。
示例:
| |
const_cast: const_cast 用于移除或添加const或volatile属性。通常情况下,它被用于移除对象的常量性,允许修改原本被声明为const的变量。需要注意的是,去除一个本质上确实是常量的对象的const标记并进行修改可能会导致未定义行为。
示例:
| |
reinterpret_cast: reinterpret_cast 是最危险的cast,它能够执行低级的强制类型转换。尽管几乎没有任何语义检查,但它能够在几乎任意两种类型之间转换,例如整数与指针之间的转换。由于它的不安全性,应该尽可能避免使用reinterpret_cast,除非你完全理解所进行的转换。
示例:
| |
总结:static_cast在相关类型间做安全转换。dynamic_cast在类层次结构中转换,并支持运行时检查。const_cast改变类型的const或volatile限定。reinterpret_cast进行低级别、不安全的强制类型转换。
C++构造函数的类型
- 默认构造函数(Default Constructor)• 定义:无参数或所有参数都有默认值的构造函数。• 作用:创建对象时初始化默认状态。• 特点: • 若未显式定义任何构造函数,编译器会自动生成一个空的默认构造函数。 • 若定义了其他构造函数(如拷贝构造),编译器不再生成默认构造,需手动添加。• 应用场景: • 游戏对象初始化(如默认角色属性)。 • 容器类(如
std::vector)的隐式构造。
成员函数/变量与默认构造函数成员变量:类中定义的变量(如 int x;)。成员函数:类中定义的函数(如 void func() { … })。
默认构造函数:若未显式定义,编译器生成无参构造函数。不初始化内置类型:如int、指针等保持未定义值。初始化类类型成员:调用其默认构造函数(如std::string会初始化为空)。
| |
- 拷贝构造函数(Copy Constructor)• 定义:接受同类型对象的常量引用作为参数的构造函数。• 作用:深拷贝或浅拷贝对象的成员。• 特点: • 默认生成的拷贝构造函数执行浅拷贝(指针成员可能引发双重释放)。 • 需显式定义深拷贝逻辑(如复制资源句柄)。• 应用场景: • 游戏对象克隆(如复制敌人实例)。 • 传递对象到容器(如
std::vector::push_back)。
| |
- 转换构造函数(Converting Constructor)• 定义:单参数构造函数,支持隐式类型转换。• 特点: • 使用
explicit关键字可禁止隐式转换,避免意外行为。• 应用场景: • 数值类型转换(如int转Damage类)。 • 游戏资源标识符的构造(如字符串转ResourceID)。
| |
- 委托构造函数(Delegating Constructor)• 定义:一个构造函数调用同类其他构造函数(C++11)。• 作用:减少代码冗余,集中初始化逻辑。• 应用场景: • 多个构造函数共享初始化逻辑(如游戏实体的不同构造方式)。
| |
- 继承构造函数(Inherited Constructor)• 定义:派生类通过
using继承基类构造函数(C++11)。• 作用:复用基类构造逻辑,避免重复代码。• 应用场景: • 派生类仅新增成员,无需修改基类构造逻辑。
| |
智能指针中的内容
智能指针的核心组成与机制
1. 原始指针封装• 基础结构:内部封装一个原始指针(T*),指向动态分配的对象/数组,作为资源管理的目标。
2. 所有权模型• 独占所有权(**unique_ptr**): • 通过移动语义(Move Semantics)确保唯一所有者,禁止拷贝。 • 析构时直接释放资源,无额外开销。• 共享所有权(**shared_ptr**): • 引用计数(原子操作)跟踪共享资源的所有者数量。 • 最后一个所有者析构时释放资源。• 弱引用(**weak_ptr**): • 观察资源但不持有所有权,需通过 lock() 升级为 shared_ptr 使用,避免循环引用。
3. 引用计数与控制块• 控制块(Control Block): • 存储引用计数、弱引用计数、删除器和分配器。 • shared_ptr 首次构造时动态分配控制块,后续拷贝共享同一控制块。• 线程安全:引用计数增减使用原子操作,但资源访问需额外同步。
4. 删除器(Deleter)• 灵活释放策略:支持默认 delete、delete[] 或自定义释放逻辑(如文件句柄关闭)。• 存储方式: • unique_ptr:删除器作为类型的一部分(模板参数),无运行时开销。 • shared_ptr:删除器存储在控制块中,允许多个 shared_ptr 共享同一策略。
5. 运算符重载与行为模拟• 指针操作:重载 operator* 和 operator->,提供类似裸指针的访问方式。• 有效性检查:通过 explicit operator bool() 支持 if (ptr) 语法,判断是否为空。
6. 异常安全与RAII• 资源即初始化:利用栈对象析构的确定性,确保资源在异常发生时仍被释放。• 移动语义:unique_ptr 通过所有权转移避免拷贝开销;shared_ptr 拷贝时共享所有权。
7. 其他特性• 自定义分配器:支持内存池等特殊分配策略(如 std::allocate_shared)。• 类型擦除:控制块设计允许不同删除逻辑统一管理。
面试回答示例
“智能指针的核心是通过封装原始指针和RAII机制自动化资源管理。例如,unique_ptr 通过独占所有权和移动语义确保资源唯一归属,避免拷贝;shared_ptr 则利用引用计数实现共享资源,通过控制块存储计数和删除器,保证最后一个所有者释放资源。它们还重载了指针操作符,模拟裸指针行为,同时通过原子操作保证线程安全的计数增减。此外,weak_ptr 解决了 shared_ptr 的循环引用问题,而删除器和自定义分配器提供了灵活的扩展能力。”
动态链接 vs 静态链接
静态链接:库代码嵌入可执行文件,独立但体积大。动态链接:运行时加载共享库(.dll/.so),节省内存,依赖外部环境。
vector是怎么进行扩容的
在C++中,vector的扩容机制是其动态数组实现的核心特性之一,确保在插入元素时能够自动调整内存容量。以下是其扩容过程的详细步骤和关键特性:
1. 触发条件
• 容量不足:当插入新元素(如push_back、emplace_back)导致当前元素数量(size)超过当前容量(capacity)时,触发扩容。
2. 扩容策略• 增长因子:新容量通常是当前容量的固定倍数,不同编译器实现不同: • GCC(libstdc++):新容量 = 旧容量 × 2(若旧容量为0,则初始化为1)。 • MSVC(Microsoft STL):新容量 = 旧容量 + 旧容量 / 2(近似1.5倍增长)。• 标准未规定:C++标准未明确增长因子,仅要求新容量 ≥ 旧容量,且足够容纳新增元素。
3. 扩容步骤
- 分配新内存:根据新容量分配更大的连续内存块。
- 迁移元素:• 将旧内存中的元素拷贝或移动到新内存: ◦ 若元素支持移动语义(如C++11后的对象),优先使用移动构造函数。 ◦ 否则使用拷贝构造函数。
- 释放旧内存:销毁旧元素并释放原内存块。
- 更新指针与容量:• 更新内部指针指向新内存。• 新
capacity= 新分配的容量,size+= 1。
4. 时间复杂度• 分摊O(1):虽然单次扩容需要O(n)时间复制元素,但通过倍增策略,多次操作的分摊时间复杂度为O(1)。 • 例如,容量从1扩容到2、4、8…,总复制次数为1 + 2 + 4 + … + 2^k ≈ 2n,均摊到n次插入操作,每次成本为O(1)。
5. 迭代器失效• 所有引用失效:扩容后,原内存被释放,所有迭代器、指针、引用均失效。
| |
6. 性能优化• 预分配内存:使用reserve(n)提前分配足够容量,避免频繁扩容。
| |
• 移动语义:为元素实现移动构造函数,减少拷贝开销。