CPP回炉重造(一)
逆向自己的靶场,遇到cpp内存布局+红黑树,拼尽全力无法战胜,遂回炉重造,在此记录笔记。
一、switch结构
首先存在一个贯穿的问题,不写break的话就会接着往下执行。
在分支非常多时,推荐用switch而不是if-else,因为编译器会内联优化成jump表,也就是ida里的label 44这种。
还有个跟就是不要在条件写赋值语句,比如grade=22,这是恒为true的,造成干扰了。
switch (grade) {case 'A': std::cout << "优秀\n"; break;case 'B': std::cout << "良好\n"; break;default: std::cout << "未知等级\n"; break;}二、cpp里的for循环
比c多了个优势,比如遍历数组的时候
int array = {1,2,3,4,5}for(int i:array){ std::cout<<i<<'\n';}读作”对 arr 里的每个元素 x 做……”。不用写下标、不用算长度、不怕越界——std::vector、std::string、数组、std::map 这些容器全都一套写法:
std::string s = "hello";for (char c : s) { // 字符串按字符遍历 std::cout << c << "-";}它还有三种变体
for (int x : arr) // 拷贝每个元素(只读、元素小、用这个)for (int& x : arr) // 引用,能改原数组里的元素for (const int& x : arr) // 常引用,只读不拷贝(元素大、用这个)三、auto
auto的作用是让编译器帮我们猜类型。
直白一点的用处就是省一点事,如下:
std::vector<std::pair<std::string,int>> scores;//不写auto版本for(std::vector<std::pair<std::string,int>>::iterator it=scores.begin();it !=scores.end();++it){ /*······*/}// 写 auto 的版本:for (auto it = scores.begin(); it != scores.end(); ++it) { /* ... */ }循环能借助const以及引用达到拷贝与只读的效果。
但是auto有个大坑:会剥掉引用!
int x = 10;int& ref = x; // ref 是 int&(引用)int* ptr = &x; // ptr 是 int*
auto a = ref; // a 是什么?答案是 int!a = 20; // 只改了 a,x 还是 10核心就是:auto 按”值”推导:它把引用剥掉、把顶层 const 剥掉,得到一份独立拷贝。
所以要保留引用还是得显示用&
auto& b = ref; // b 是 int&,改 b 就是改 xconst auto& c = ref; // c 是 const int&,只读const auto d = x; // d 是 const int极客的三口诀:
auto默认是”拷贝一份”,得到的是普通值;- 想改原对象就写
auto&; - 只想读不想拷就写
const auto&。
四、重载:同名不同参
int add(int a, int b) { return a + b; }double add(double a, double b) { return a + b; }std::string add(const std::string& a, const std::string& b) { return a + b;}
add(1, 2); // 调 int 版add(1.5, 2.5); // 调 double 版add(std::string("a"), std::string("b")); // 调 string 版编译器根据调用时实参的类型挑最合适的那一版。规则:参数类型、个数、顺序不同,就能构成重载;只靠返回类型区分不行
重载的底层原理是name mangling
即编译器会把函数名和函数类型添加到符号里。比如 add(int, int) 可能变成 _Z3addii。这叫 name mangling(名字改编)。

五、默认值
有个坑:默认值只能写在头文件里
void draw(int width, int color = 0xFF0000);
// foo.cppvoid draw(int width, int color) { // 这里不要写 = 0xFF0000! // ...}为什么这样?因为调用放编译的话,是看头文件的而不会去看.cpp。所以要写进头文件里。
有第二个坑:默认值和重载可能会有冲突。
void print(int n){}void print(int n,int m=3){}
print(1);//这个时候就不明确调用哪个了六、引用
引用就是起别名,引用必须初始化
int x = 20;int& ref=x;ref=10std::cout<<x; //输出10底层就是指针
int x = 10;int& ref = x; // 底层:ref 存着 x 的地址
void foo(int& r) { // 传引用,底层传的是地址 r += 1; // 编译器自动解引用}什么时候必须用指针?三个典型:可能为空(用 nullptr 表示”无”)、要重新绑定、指向动态分配的对象(new 出来的东西、容器里的元素换位置)。
七、值传递与引用传递
值传递:传入参数的值是会被拷贝成副本并在函数作用域内使用
void add(int x){ x+=10}int a = 10;add(1);std::cout<<a;//还是10引用传递:相当于传的是地址,直接操作内存
void inc_by_ref(int& n) { n += 1; // 改的就是实参}
int x = 10;inc_by_ref(x);std::cout << x; // 11大佬的经验法则:按值传 int、double 这种”拷贝跟没拷一样便宜”的类型;按值传 std::string、std::vector 这种要拷贝内部数据的,一律 const&。 比如print_log(const std::string& msg) 。
一个大坑:不要返回局部变量的引用
int& bad() { int local = 42; return local; // 危险!函数结束 local 就没了}返回局部变量的引用 = 返回一个悬垂引用(dangling reference),函数一结束内存就被回收,用它是未定义行为。能返回值就返回值,现代编译器有 RVO(返回值优化)
八、inline函数
函数调用有开销:压栈、跳转、返回。如果函数很小(比如 int max(int a, int b)),调用开销可能比函数体本身还大。inline 就是告诉编译器:“这函数别真的调用了,直接把它展开到调用处。”
inline int max(int a, int b) { return a > b ? a : b;}
int r = max(3, 5); // 可能被编译成 int r = (3 > 5 ? 3 : 5);注意”可能”——inline 是建议不是命令,编译器有完全的自由裁量权:函数太大、太复杂、有递归,它就不展开了。反过来,没写 inline 的小函数编译器在 -O2 下也经常自动内联。现代编译器面前,inline 更多是”请求”,内不内联它说了算。
在c语言里,能够实现同一目的的是宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int r = MAX(3, 5);重复调用豁免权:
inline int twice(int x) { return x * 2; } // 放头文件,OKint not_inline(int x) { return x * 2; } // 放头文件:多个 cpp include 就重复定义九、类的规则怪谈
小函数直接写在类里,类里写的成员函数默认是 inline 的,编译器可能直接展开。大函数放类外,用 :: 指出属于哪个类:
class Circle { double radius_;public: double area() const; // 只声明 void setRadius(double r);};
// Circle.cpp#include "Circle.h"
double Circle::area() const { // Circle:: 表示"这是 Circle 的成员" return 3.14159 * radius_ * radius_;}
void Circle::setRadius(double r) { if (r >= 0) radius_ = r;}这里预告一下cpp对象内存布局

成员变量按声明顺序排布,成员函数不占对象空间(所有对象共享同一份代码)
十、类的权限
分为三种:protected,public,private
默认是private
class BankAccount {private: double balance_ = 0; // 私有:只有本类成员能碰protected: int account_no_; // 保护:本类 + 子类能碰public: void deposit(double m) { // 公开:谁都能调 if (m > 0) balance_ += m; } double balance() const { return balance_; }};| 关键字 | 谁能访问 | 典型用途 |
|---|---|---|
public | 任何地方 | 对外接口:方法、构造、析构 |
protected | 本类 + 派生类 | 给继承留的后门 |
private | 仅本类(含友元) | 内部数据、内部实现 |
这里有个工程习惯就是:重要数据放private,然后提供一个public函数接口来间接操作它。
struct结构体在cpp就是另一种类的表达形式,都可以作数据聚合,不过他的只有public这种。
十一、构造函数
构造函数的特点:函数名和类名相同、没有返回类型。它在对象创建时自动执行。
class Circle { double radius_;public: Circle() : radius_(1.0) {} // 默认构造 Circle(double r) : radius_(r) {} // 带参构造};
Circle c1; // 调默认构造,radius = 1.0Circle c2(5.0); // 调带参构造,radius = 5.0Circle c3{5.0}; // C++11 起,花括号初始化同样可用不写构造函数?编译器会硬塞一个默认的构造函数给你。
编译器会自动生成默认构造函数——什么都不做,成员变量保持不确定状态(int/double 是垃圾值)。所以”不写构造”不等于”自动帮你初始化好”。
两个例外:
- 成员带了类内初始值(
int x = 0;),不写构造也会初始化成这个值; - 成员是有构造函数的类类型(比如
std::string),自动调用它的默认构造。
初始化列表:
冒号后面用 成员(值) 的形式给每个成员赋初值。注意它和函数体 { radius_ = r; } 的本质区别:
Circle(double r) : radius_(r) {} // 直接初始化:一步到位Circle(double r) { radius_ = r; } // 先默认构造,再赋值:两步对 double 这种内建类型,两种写法差别不大;但成员是类类型(std::string、std::vector)时差别立现——第一种直接构造一次,第二种先默认构造出一个空对象、再整体赋值,多了一次构造 + 一次赋值。能用初始化列表就用初始化列表,这是性能习惯,也是唯一能初始化 const 成员和引用的方式(大佬如是说):
class Config { const int limit_; // const 成员 std::string name_; // 类类型成员public: Config(int l, const std::string& n) : limit_(l), name_(n) {} // 只能在这里初始化 const 成员
Config(int l):limit_(l),name_("大厦春"){}};构造函数也是函数,所以能重载
十二、析构函数
对象销毁时自动调用
class Logger { FILE* file_;public: Logger(const char* path) : file_(fopen(path, "w")) {} ~Logger() { // 析构函数 fclose(file_); // 销毁时自动关文件 }};//栈对象的析构时会自己调用的void process() { Logger log("run.log"); // 构造:打开文件 do_something(); if (error) return; // 提前返回? do_more();} // 无论怎么离开,log 在此析构,文件被关
//堆对象则需要手动deleteLogger* p = new Logger("run.log"); // 构造// ...delete p; // 析构 + 释放内存忘了 delete 就泄漏:析构不执行、资源不释放。
所以现代 C++ 的态度是:优先栈对象,堆对象交给智能指针(std::unique_ptr 在析构时自动 delete),手工 new/delete 只在万不得已时用。
注意!!!
析构函数默认是 noexcept 的(C++11 起),在里面抛异常会导致 std::terminate 直接终止程序——因为析构往往发生在栈展开等”已经在处理异常”的路径上,再抛一个会炸掉异常机制。规则:析构函数只做不抛异常的清理。
十三、拷贝构造与拷贝赋值
二者的区别
Circle c1(5.0);Circle c2 = c1; // 拷贝构造:用 c1 创建一个新对象 c2Circle c3;c3 = c1; // 拷贝赋值:c3 已存在,把 c1 的值灌进去
//对应的两个函数签名class Circle {public: Circle(const Circle& other); // 拷贝构造 Circle& operator=(const Circle& other); // 拷贝赋值};默认行为:逐成员拷贝
对于简单函数,不写上面这两个函数,编译器自动生成:把每个成员按值复制一份。
Circle c2 = c1; // radius_ 拷贝,行为正确但是对象成员里有指针就很危险了。
浅拷贝
class Buffer{ char* data; int len;public: Buffer(int n): data(new char[n]),len(n){} ~Buffer() { delete[] data_; }}Buffer a(100); Buffer b = a; 发生什么?默认拷贝把 data_ 这个指针的值复制过去——于是 a.data_ 和 b.data_ 指向同一块堆内存。这叫浅拷贝:

深拷贝
上面的修复方法就是自己写个拷贝逻辑
class Buffer { char* data_; int size_;public: Buffer(int n) : data_(new char[n]), size_(n) {} ~Buffer() { delete[] data_; }
Buffer(const Buffer& other) // 深拷贝构造 : data_(new char[other.size_]), size_(other.size_) { std::copy(other.data_, other.data_ + size_, data_); } Buffer& operator=(const Buffer& other) { // 深拷贝赋值 if (this != &other) { // 自赋值保护 delete[] data_; // 先释放旧数据 size_ = other.size_; data_ = new char[size_]; std::copy(other.data_, other.data_ + size_, data_); } return *this; }};要点:拷贝构造”新开一块内存、逐字节复制”;拷贝赋值还要多两步——先释放自己的旧数据(否则泄漏)、检查自赋值(a = a 不炸)。
如果一个类自己写了析构函数(说明它管着资源),那么拷贝构造、拷贝赋值也大概率要自己写——因为它们仨经常一起出现:
析构函数、拷贝构造、拷贝赋值,三者要么都用默认,要么都自己写。
这就是 Rule of Three。违反的典型症状就是上面的双重释放。现代 C++ 更推荐的其实是不用指针成员、改用 std::string/std::vector/std::unique_ptr——它们自带正确的拷贝/移动语义,让 Rule of Three 大部分时候自动满足。这点在 STL 容器 和 RAII 篇会反复强调。
笔记内容来自学习网站:https://geek-doc.cn/ 之后的文章大概率也是这个,极客大佬就是牛皮
部分信息可能已经过时









