目录
const关键字的作用
volatile 关键字
#define和const有什么区别
decltype和auto的区别
extern 关键字的作用
如何避免野指针
C/C++中的类型转换以及使用场景
什么是RTTI?其原理是什么?
RTTI 的原理:
C++中引用和指针的区别
C++11用过哪些新特性
简述多态实现原理
虚函数表和虚函数表指针的创建时机
为什么基类的构造函数不能定义为虚函数?
const关键字的作用
思考题
- C语言中的const局部变量可以用指针间接修改, 说明可以取地址, 那此时局部变量放在什么区域?
- C语言中全局的const变量完全不可修改, 也不可以取地址、此时放在什么区域?
- C++中的const局部变量不可修改,可以取地址、此时又放在什么区域、底层是怎么实现的?
- 用const的好处是什么?
1.
放在那里? 应该是放在虚拟地址空间的栈存储区域,所以可以修改,可以取出地址。
const
修饰的局部变量通常是存储在栈区域的,但编译器可能会根据实际情况进行优化,将其直接嵌入到目标代码中,而不需要分配栈上的存储空间。
2.
应该是放置在虚拟地址空间的静态数据区域的.rodata只读数据区域,所以不可修改,并且禁止取地址、强行取地址会报段错误。
3.
可以取地址,说明取地址的时候将这个数据放入了栈区域一份。但是在生成目标代码时,编译器(编译阶段)将该变量的值直接嵌入到使用该变量的地方。这样,在程序执行时,这个变量的值就不会被修改,因为它在目标代码中已经被固定下来了,所以就算我们通过指针强行修改了给的变量所在的内存、但是编译阶段完成的直接的替换我们又改不了,所以此时真正的const实现其实是依赖于编译器的优化/强大。
4.
好处, 修饰变量,修饰函数,不可修改,检查提醒,节约内存的作用。
volatile 关键字
- 给编译器说清楚,我这个关键字修饰的对象和函数是易变的,易挥发的。你不要进行优化。直接去读取内存。
- 它甚至可以跟const联合使用,用了它修饰的变量,哪怕是const, 它也会告诉编译器不要在编译阶段直接就用类似于预处理阶段对define的替换处理一样给我直接替换成常量了(.rodata区域/代码段),而去给我实际读取内存。
#define和const有什么区别
- 在C++中,两者的区别就在于数据替换时期不同,一个在预处理时期由预处理器完成,另一个是编译器由编译器完成数据值的直接替换。
- 两者作用时期不同、处理工具不同,自然预处理器是很简单的,他不比编译器的复杂。预处理器简单的做替换,展开。编译器在编译阶段会做词法分析,语法分析,类型检查等等工作。
- 所以相对来说,C++中推荐用const,应该就是有类型检查,更为安全一点。define太暴力了、出错了没法搞。
decltype和auto的区别
- 首先两者都是类型推导的方式。只不过,一个是根据初始化的表达式来推导,另一个是主动给出表达式来推导。并且decltype可以准确的推导出类型,auto推导自动去除const、&、*这些附加属性
decltype
和 auto
是 C++11 引入的两个关键字,用于从表达式推导变量的类型,但它们之间有一些区别。
-
decltype
:decltype
是一个类型推导关键字,用于获取表达式的类型。decltype
的结果类型与给定表达式的类型完全相同,包括 const 和引用等修饰符。decltype
的语法形式为decltype(expression)
,其中expression
是需要推导类型的表达式。- 通常用于从变量、函数调用、表达式等处推导类型。
-
auto
:auto
也是一个类型推导关键字,用于自动推导变量的类型。auto
的结果类型与表达式的类型相同,但会忽略掉顶层 const 和引用等修饰符。auto
的语法形式为auto
,直接声明变量时使用。- 通常用于定义变量时,让编译器自动推导变量的类型,可以简化代码并提高可读性。
简而言之,decltype
用于获取表达式的确切类型,包括 const 和引用等修饰符;而 auto
用于自动推导变量的类型,会忽略掉顶层 const 和引用等修饰符。
extern 关键字的作用
void Test() {
extern void Print(); // 声明一下外部的Print函数调用
Print(); // 找不到
}
void Print() {
printf("hahaha\n");
}
int main() {
Test();
return 0;
}
也可以引用外部文件的函数,用extern 关键字显示说明一下这个函数,或者变量在外部文件或者区域已经定义好了,此处编译器你不用帮我报错,或者警告,它是定义好了的。
如何避免野指针
上述再补充一个及时跟新迭代器,迭代器失效本质上也就是插入,删除操作之后,之前的迭代器指向的节点发生了改变,所以我们需要及时跟新迭代器,避免操作失效的迭代器,其实本质上也是一个失效的指针.
迭代器失效的本质确实类似于野指针或非法操作内存的情况,因为迭代器失效意味着迭代器指向的位置在容器中不再有效,可能会指向已经释放的内存地址或者容器中的其他位置.
C/C++中的类型转换以及使用场景
- C语言这个类型转换看似很爽,啥都能转,充分给足程序猿面子,但是转换不明确,限制太少了、而且没有错误检查,很容易出错。(太霸道了)
- static_cast:说白了就是最基础的类型转换,也是最常用的类型转换。基本类型,子类指向父类的安全转换(上行转换)、void*和其他类型指针的转换。(最像C转换使用场合的转换)
- const_cast:专门去除 const 或者 volatile 属性的。
- dynamic_cast:用于父子类型之间的安全转换。编译器默认上行转换安全,下行转换进行类型检查。
dynamic_cast
运算符在 C++ 中用于执行安全的向下转型(downcasting),并且在运行时进行类型检查。它主要用于在继承层次结构中的类之间进行转换,以及在执行转换之前进行类型检查。 - 类型检查: 当使用
dynamic_cast
进行向下转型时,编译器会在运行时检查指针或引用指向的对象的实际类型是否与要转换到的类型相匹配。这种匹配性检查通过查看对象的虚拟表来实现。如果指向的对象实际上是要转换到的类型或其派生类的对象,则转换成功;否则,转换失败。 -
失败处理:
- 如果
dynamic_cast
无法将基类指针或引用转换为派生类指针或引用,它将返回一个空指针(对于指针)或抛出一个std::bad_cast
异常(对于引用)。 - 如果进行指针类型转换时,指针为 null,则
dynamic_cast
也会返回 null。
- 如果
-
适用性:
dynamic_cast
只能用于具有虚函数的类,因为它需要虚拟表来检查对象的实际类型。dynamic_cast
只能用于多态类型(即基类或包含虚函数的类)之间的转换。dynamic_cast
不适用于非多态类型之间的转换。
接着上面这个dynamic_cast 是运行时检测。
什么是RTTI?其原理是什么?
RTTI(Run-Time Type Information)是 C++ 提供的一种机制,用于在运行时获取对象的类型信息。RTTI 允许程序在运行时查询对象的类型,以便进行类型安全的操作,例如动态类型转换和异常处理。
RTTI功能体现在两个运算符上:typeid运算符和dynamic_cast运算符
- typeid运算符,用于返回表达式的类型,可以通过基类的指针获取派生类的数据类型.
RTTI 的原理:
-
虚函数表(vtable): 在启用 RTTI 的情况下,C++ 编译器会为每个类生成一个虚函数表(vtable)。虚函数表是一个存储了指向虚拟函数的指针的数组,每个类都有自己的虚函数表。当一个类含有至少一个虚函数时,这个类就会拥有一个虚函数表。
-
类型信息对象(type_info): 对于每个类,编译器还会生成一个类型信息对象,其类型为
type_info
。这个对象包含有关该类的类型信息,例如类名、类型大小等。在包含 RTTI 的编译器中,这个类型信息对象也包含指向虚函数表的指针。 -
dynamic_cast 运算符的工作: 当使用
dynamic_cast
运算符进行向下转型时,编译器会查找源对象的类型信息对象,并在其中查找目标类型的信息。如果目标类型是源对象的基类或者与其兼容,则dynamic_cast
可以成功转换;否则,转换失败。这种类型信息的查询是通过访问类型信息对象的虚函数表来实现的。 -
typeid 运算符: C++ 还提供了
typeid
运算符,用于获取对象的类型信息。当使用typeid
运算符时,编译器会生成代码来获取对象的类型信息对象,从而可以得到对象的类型信息。
总之,RTTI 的原理是通过在运行时为每个类生成虚函数表和类型信息对象,并通过这些信息来实现对对象的类型进行查询和检查。这使得在运行时能够进行类型安全的操作,如动态类型转换和异常处理。
C++中引用和指针的区别
- 一般来说引用的必须初始化绑定一个对象,并且不可更改绑定,且不可绑空,使得引用相对指针更加安全一些。但是引用也会存在安全问题(也会悬空,尤其是在lambda表达式中捕获引用)
以下是一些需要注意的情况:
-
捕获局部变量的引用:如果 lambda 表达式捕获了一个局部变量的引用,并且该引用在 lambda 表达式执行时已经超出了其作用域,则会导致引用悬空。这种情况下,应该确保 lambda 表达式执行时捕获的引用仍然有效。
-
捕获指向堆内存的引用:如果 lambda 表达式捕获了指向堆内存的引用,而在 lambda 表达式执行时,堆内存已经被释放,那么捕获的引用将变为悬空。在使用指针或引用捕获对象时,应该确保对象的生命周期能够覆盖整个 lambda 表达式的执行过程。
为了避免引用悬空的问题,可以采取以下措施:
-
使用值捕获:如果捕获的对象在 lambda 表达式执行期间不会发生变化,并且对象的生命周期足够长,可以考虑使用值捕获而不是引用捕获。
-
小心捕获生命周期:确保捕获的引用或指针所引用的对象在 lambda 表达式执行期间保持有效。这可能需要在 lambda 表达式外部控制对象的生命周期,或者使用智能指针等机制来管理对象的生命周期。
-
避免悬空引用:在捕获引用时,要确保引用的对象在 lambda 表达式执行期间保持有效。如果无法保证引用的有效性,则应该避免捕获该引用。
C++11用过哪些新特性
简述多态实现原理
注意时间点差异:
- 静态的多态是通过重载和模板技术实现的,是在编译时确定。
- 动态的多态是用虚函数机制实现,是运行时确定。
虚函数表和虚函数表指针的创建时机
注意:
- 虚函数表是属于整个类的,其中无非包含的虚函数的入口地址嘛,这些函数也是属于类的。
- 虚函数表的创建时机和虚函数表指针初始化的时机是不一样的。一个是编译器创建、放在全局只读数据段,另外一个是在构造函数中初始化的这个vptr。类对象内部前面4个字节都存放的是这个vptr,故而都可以通过它找到虚函数表。
为什么基类的构造函数不能定义为虚函数?
因为虚表指针的初始化在构造函数中完成,由于此时虚表指针还没有完成初始化,所以基类的构造函数不能定义为虚函数。