程序断点的原理
程序断点的原理
调试器 Debugger 并不能控制程序的执行顺序,它之所以可以让 CPU 在需要的地方停住(随心所欲的停止程序的执行),主要通过软件断点和硬件断点两种方式。
软件断点
软件断点在 x86 / x32 / x64 系统中就是指令 INT 3 ,它的二进制代码 opcode 是 0xCC 。当程序执行到 INT 3 指令时,会引发软件中断。操作系统的 INT 3中断处理器 会寻找注册在该进程上的调试处理程序,从而像 Windbg 和 VS 等等调试器就有了上下其手的机会。
通过一个例子说明:
123456#include <iostream>int main(){ printf("Hello World"); // 这里断点}
此时调试器会将目标地址的指令替换为 int 3 指令(机器码 0xCC ),等到达断点的时候会还原目标地址的指令。
一般情况下,调试器维护了一大组调试断点,在并把他们都换成了 INT 3 。在被调度回来后,会都填回去,并通过现在的地址判断是到了那个断点。软件断点没有数目限制。
...
const 与 constexpr
const 与 constexpr 的区别
如下图:
主要是用于区分只读变量和常量
只读变量:运行时确定
常量:编译时确定
性能方面:常量 > 只读变量
常见的常量表达式
字面值(如 42)
用常量表达式初始化的对象
一个对象(或表达式)是否是常量表达式取决于类型和初始值,如下:
12345int i1 = 42; // i1 不是常量表达式:初始值 42 是字面值,但 i1 不是 const / constexpr 类型const int i2 = i1; // i2 不是常量表达式:初始值 i1 不是常量表达式const int i3 = 42; // i3 是常量表达式:用字面值 42 初始化的 const 对象const int i4 = i3 + 1; // i4 是常量表达式:用常量表达式 i3 + 1 初始化的 const 对象const int i5 = getValue(); // 如果 getValue() 是普通函数,则 i5 值要到运行时才能确定,则不是常量表达式。相反,如果 getValue() 是 ...
手撕队列(queue)
手撕队列(基于数组)
关键点在于头指针和尾指针在队列清空时的调整(MyQueue::pop()),不当的操作可能导致清空后的空间无法重复使用,不断扩容。
可对扩容操作(MyQueue::push())做进一步优化(数据前移)。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100#include<iostream>using namespace std;template <typename T>class MyQueue{private: T* _data; int _front; int _rear; int _size; int _capacity;public: MyQueue(); ...
碎玉零珠———— C++
volatile
volatile 是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如 硬件设备、操作系统 或 其他线程 。
当一个变量被声明为 volatile 时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。避免因为编译器优化而导致出现不符合预期的结果。
explicit
在 C++ 中,explicit 通常用于构造函数的声明中,用于防止隐式转换。 当将一个参数传递给构造函数时,如果构造函数声明中使用了 explicit 关键字,则只能使用显式转换进行转换,而不能进行隐式转换。这种机制可以防止编译器自动执行预期外的类型转换,提高代码的安全性。
什么是隐式类型转化
当你只有一个类型T1,但是当前表达式需要类型为T2的值,如果这时候T1自动转换为了T2,那么这就是隐式类型转换。如下:
1234567int a = 0;long b = a + 1; // int 转换为 long if (a == b) { // 默认的operator==需要a的类型和b相同,因此也 ...
C++ Lambda 相关问题
Lambda 表达式如何对应到函数对象
在 C++ 中,Lambda 表达式本质上是编译器生成的匿名函数对象(又称闭包类),其底层实现依赖于对 operator() 运算符的重载。这种机制使得 Lambda 既能保持与普通函数相似的调用方式,又能通过捕获上下文变量实现更灵活的行为。以下是其核心实现原理和对应关系的具体分析:
Lambda表达式与闭包类的映射
编译器生成的匿名类
当定义一个Lambda表达式时,编译器会隐式生成一个唯一的闭包类,该类包含以下核心结构:
成员变量:存储通过值捕获或引用捕获的外部变量(若存在捕获)。
重载的operator():实现Lambda的函数体逻辑。
可能的类型转换函数:用于无捕获Lambda隐式转换为函数指针(通过+运算符触发)。
12345678910auto lambda = [x](int y) { return x + y; };// 对于上面的语句,编译器生成类似以下的闭包类:class __lambda_anonymous {private: int x; // 值捕获的变量副本publi ...
const 与 static
const 与 static 关键字const
const修饰符用来定义常量,具有不可变性。在类中,被const修饰的成员函数,不能修改类中的数据成员;
指针常量指的是该指针本身是一个常量,不能被修改,但是指针指向的对象可以被修改;
常量指针指的是这个指针指向的对象是一个常量,不能被修改,但是指针本身可以被修改。
如果 const 变量是在全局作用域中声明的,它将存储在静态存储区(Static Storage Area)中。
如果 const 变量是在函数内部或代码块内部声明的,它将存储在栈(Stack)上,在函数返回时释放。
const 修饰的字符串常量存储在常量存储区,在程序运行期间保持不变。
const 修饰的函数能否重载?
const修饰的函数可以重载。const成员函数既不能改变类内的数据成员,也无法调用非const的成员函数;const类对象只能调用const成员函数。非const对象无论是否是const成员函数都能调用,但是如果有重载的非const函数,非const对象会优先调用重载后的非const函数。
const 修饰函数的参数
如果参数作输出用,不论它是什么数据 ...
排序算法
各种排序算法的原理和时间复杂度
快速排序:一轮划分,选择一个基准值,小于该基准值的元素放到左边,大于的放在右边,此时该基准值在整个序列中的位置就确定了,接着递归地对左边子序列和右边子序列进行划分。时间复杂度O(nlogn),最坏的时间复杂度是O(n^2)。需要排序的对象越有序,快速排序的退化程度越高,即时间复杂度越趋向于O(n ^ 2)。通过随机选择枢轴,可以有效地避免这种情况。随机选择使得每次分区都有较大概率是平衡的,从而保持了快速排序的平均时间复杂度 O(n * logn);
123456789101112131415161718192021222324252627282930313233343536void QuickSort(vector<int>& nums, int left, int right){ if (left >= right) return; int pivot = rand() % (right - left + 1) + left; swap(nums[pivot], nums[right ...
左值、右值、纯右值、将亡值
左值、右值、纯右值、将亡值
C++11使用下面两种独立的性质来区别类别:
拥有身份:指代某个非临时对象。
可被移动:可被右值引用类型匹配。
每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)
拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。
拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。
不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。
不拥有身份且不可被移动的表达式无法使用。
如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是 std::move 过的对象,该代指对象并不一定是临时值)。
左值
左值是一个数据的表达式(如变量名或引用的指针),我们可以获取到它的地址,正常情况下是可以能够对它赋值
定义const修饰后的左值,不能给它赋值,但是可以取出它的地址
...
智能指针
unique_ptr
独占资源所有权的指针。由于没有引用计数,因此性能较好。
离开 unique_ptr 对象的作用域时,会自动释放资源。
unique_ptr 本质是一个类,将复制构造函数和赋值构造函数声明为 delete 就可以实现独占式,只允许移动构造和移动赋值。unique_ptr 所持有的对象只能通过 转移语义(move) 将所有权转移到另外一个 unique_ptr 。
123// 自定义实现 unique_ptrUniquePtr(UniquePtr<T> const &) = delete;UniquePtr & operator=(UniquePtr<T> const &) = delete;
123std::unique_ptr<int> uptr = std::make_unique<int>(200);// ...// 离开 uptr 的作用域的时候自动释放内存
std::unique_ptr 是 move-only 的。
12345std::unique_ptr<int> ...
C++ 模板相关问题
为什么模板声明定义不能分离?前言
模板函数一般不能声明定义分开,但是普通函数函数就可以,为什么呢?那就要先介绍一下在软件开发过程中,从源代码到可执行程序,通常会经历预处理、编译、汇编、链接的这四个主要步骤。
预处理 ( Preprocessing ):
任务:预处理器的任务是处理源代码中的预处理指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef、#ifndef、#endif等)指令。它会删除所有注释,展开所有宏定义,处理条件编译指令,并插入包含文件的内容。
生成文件:预处理后的文件通常以 .i 或 .ii 结尾(在C/C++中),这是预处理后的文本文件,仍然保持高级语言的形式。
编译 ( Compilation ):
任务:编译器将预处理后的源代码转换成汇编语言。这个过程包括词法分析、语法分析、语义分析、中间代码生成、代码优化等步骤。编译器检查源代码中的错误,如语法错误、类型错误等,并将源代码转换成汇编指令。
生成文件:编译阶段生成的文件通常称为目标文件(Object File),以 .s结尾。这个文件包含机器代码,但是它还不能直接执行(计算 ...














