C++进阶问题整理
什么是RAII原则?它有什么作用?
RAII(Resource Acquisition Is Initialization) 是 C++ 中一种资源管理方法,主要通过构造和析构来自动获取和释放资源。
其原则是:
- 在对象构造时获取所需的资源
- 在对象析构时自动释放所持有的资源
RAII的主要作用有:
自动管理资源生命周期,避免资源泄露
RAII对象会在作用域结束时被析构,从而自动释放资源,无需手动管理资源。将资源管理与使用逻辑分离
通过RAII封装资源,可以将资源的获取、释放与业务逻辑分离,保持代码整洁。通过构造异常安全性(Constructive Exception Safety)
使用RAII在构造函数获取资源,析构函数释放,可以保证即使出现异常,资源也会被释放。阻止重复获取(Double Acquisition)
RAII对象封装了独占的资源获取方式,可以阻止对同一资源的重复获取。
C++中智能指针、容器、mutex等都是基于RAII实现的,它是C++管理资源的重要方式。
什么是RTTI信息?
RTTI(RunTime Type Information)即运行时类型信息,是C++中实现运行时判断类型的一种机制。
主要提供以下几方面的信息:
- typeid运算符:可以在运行时获取对象的类型
- dynamic_cast:通过类型信息进行安全的向下转换
- type_info类:包含类型的名称等信息
RTTI工作原理:
编译器会为包含虚函数的类产生类型信息(对类进行name mangling),存储于可执行文件中。
运行时通过此信息来实现类型判断与转换。
RTTI的主要作用有:
- 判断和比较运行时对象的类型
- 安全地向下转换指针或引用
- 实现一些设计模式如访问者模式
所以RTTI允许程序在运行时确定对象类型,通过类型信息进行更安全灵活的代码逻辑处理。
需要注意RTTI会带来额外开销,应该仅在需要时使用。
RTTI的简单示例:
1 |
|
编译:
1 | g++ -o rtii rtii.cpp --std=c++0x |
输出:
1 | b is Derived |
实现了在运行时判断和转换对象类型,以及获取类型名信息。
RTTI虽好用,但需要注意它带来的额外开销。
构造函数和析构函数的执行顺序是什么?
在C++中,构造函数和析构函数的执行顺序遵循以下原则:
- 构造函数的执行顺序按照继承关系从基类到派生类。
- 析构函数的执行顺序与构造函数相反,从派生类到基类。
- 执行顺序还受到虚继承的影响。
一个具体的执行顺序示例:
1 |
|
输出
1 | Base constructor |
构造函数需要先构造父类,析构相反。这确保类的资源可以正确初始化和释放。
虚继承和多继承情况下顺序更复杂,要分析父子关系来确定执行顺序。
虚继承对构造和析构的影响
虚继承对构造和析构顺序的影响有一定复杂性。
虚继承只会影响不同基类之间的先后顺序,不会改变基类先于派生类的规则。
虚继承对构造和析构顺序的影响更具体应该是:
- 虚基类总是先于非虚基类构造和析构,不管在继承列表中的顺序
- 派生类会先构造虚基类部分,再构造非虚基类和自己的部分
- 基类仍然先于派生类构造和析构
C++类成员在内存中是怎样存储的?
C++类的成员变量通常会按以下方式存储在内存中:
- 非静态成员变量属于类的每个实例,会与实例一起分配在堆或栈上。它们不会共享内存。
- 静态成员变量属于整个类,只有一个副本,通常放在程序的静态存储区(数据段或bss段)。所有类实例都共享这段内存。
- 成员函数也属于整个类,只有一份代码拷贝。它们通常放在代码段中。通过this指针找到调用它的类实例。
- 构造函数和析构函数是特殊的成员函数,按成员函数方式存储。
- 虚函数的虚函数表储存在共享内存中,每个类实例都有一个指向虚函数表的指针。
- 继承的情况下,子类的成员会放在基类成员的后面,先父类后子类。
所以简单来说,非静态成员独享内存,静态成员和成员函数共享内存。虚函数通过虚函数表达到多态。
c++普通函数、局部变量、全局变量在内存中怎么存储
在C++中,普通函数、局部变量和全局变量在内存中的存储方式如下:
- 普通函数:存储在代码段中。代码段是一块存储只读指令的内存区域,用于存放程序执行所需的代码。函数的机器代码被编译器放在代码段中。
- 局部变量:存储在栈中。栈是一块先进后出的内存区域,用于存放函数调用时创建的临时变量。局部变量随函数调用创建,随函数返回被自动释放。
- 全局变量:存储在数据段中。数据段是一块存储程序中已初始化的全局变量的内存区域。全局变量在程序开始时分配,在程序结束时释放。不同函数里访问的是同一份全局变量的内存。
- 常量:字符串常量存储在代码段中;其他常量如整型常量,如果是全局的,存储在数据段中;如果是局部的,存储在栈中。
- 静态变量:存储在数据段中,但有局部变量的生命周期。
所以简单说,函数在代码段,局部变量在栈中,全局变量和静态变量在数据段中。它们在内存的布局和生命周期各不相同。
存储位置和生命周期的关系c++
在C++中,变量的存储位置和生命周期有着密切的关系:
- 栈区:存放局部变量,函数的参数,临时变量等。其生命周期为:进入栈时分配内存,退出栈时(函数返回)自动释放内存。
- 堆区:通过new关键字申请的内存块。其生命周期为:执行new时分配内存,执行delete时释放内存,需要程序员控制。
- 静态区:存放全局变量和静态变量。其生命周期为:程序的加载时分配内存,程序结束时释放内存。
- 常量区:存放 const 修饰的变量。其生命周期为:程序的加载时分配内存,程序结束时释放内存。
- 代码区:存放函数体的二进制代码。其生命周期为:程序加载时分配内存,程序结束时释放内存。
所以,栈区和堆区由程序员控制生命周期,静态区和常量区生命周期由系统控制,栈区和堆区内存需及时释放避免泄漏。存储位置的不同导致它们生命周期范围不同。
什么是内存泄漏和野指针?如何避免?
内存泄漏和野指针是C++编程中常见的两类内存问题。
内存泄漏是指程序中已动态分配的堆内存由于某种原因未被释放,在程序执行结束后也未被系统回收,造成内存的浪费。
内存泄漏的主要原因是忘记手动释放堆内存或者失去了指向堆内存的指针。
避免内存泄漏的方法是及时释放new分配的内存,使用智能指针等。
野指针是指指向非法内存地址的指针。它可能是一个未初始化的指针、已经释放的堆内存地址、或者非法访问其他内存空间的地址。
野指针的主要问题是可能会造成程序异常和崩溃。
避免野指针的方法是保证每个指针都在合法内存范围内,防止越界访问,以及在释放对象后将指针置为空。
总结一下,内存泄漏是持续占用未释放内存,野指针是非法访问内存。
我们要及时释放内存,初始化和判断指针有效性,才能避免这两类问题。
什么是右值?
在C++中,右值(rvalue)是指储存在寄存器而不是内存中的临时对象或直接输入的数据值。
主要有以下三种情况:
- 字面量常量,如 10, 3.14, “abc”等
- 临时对象,如函数的返回值或运算结果
- 将要被销毁的对象
右值具有以下特点:
- 不具名
- 不可取地址
- 无法修改
- 只能移动,无法复制
右值主要出现在赋值、初始化和传参的右边。
例如:
1 | int x = 10; // 10就是一个右值 |
右值引用可以让右值直接传递给函数内部,避免复制开销。
理解右值的概念对学习移动语义很重要。
std::vector的push_back和emplace_back比较
std::vector的push_back和emplace_back都是用来向vector尾部插入元素
主要区别在于:
- push_back通过拷贝或移动方式插入元素,emplace_back则通过就地(in-place)构造方式插入。
- push_back只能直接插入元素,emplace_back可以传入构造参数进行就地构造。
- 当元素类型为类对象时,emplace_back避免了不必要的拷贝或移动操作。
- 当需要使用多个参数构造对象时,emplace_back更加方便。
例如:
1 | std::vector<std::string> v; |
std::vector的emplace_back方法可以通过完美转发(perfect forwarding)将多个参数传递给元素类型的构造函数,以就地(in-place)方式构造出一个元素并插入vector。
例如:
1 | std::vector<std::string> v; |
这里发生了以下操作:
- emplace_back把参数(3, ‘a’)完美转发给std::string的构造函数
- 使用这些参数调用std::string的构造函数std::string(size_t count, char ch)来构造一个字符串
- 然后这个通过参数构造出来的字符串被就地插入到vector中
所以v.emplace_back(3, 'a');
会向vector中插入一个内容为”aaa”的字符串。
一个完整的示例代码
1 | #include <memory> |
编译和运行
1 | 编译 |
emplace_back避免了拷贝和移动操作,可以高效地用多个参数直接构造出元素并插入。这对一些需要多参数构造的类型很有用。
总结来说,对于类对象,emplace_back通常更加高效,因为它可以就地构造,避免拷贝操作。所以优先考虑使用emplace_back来插入元素。