Skip to content

Commit e7ba8e2

Browse files
committed
new: cpp 8,9,10
1 parent 6769989 commit e7ba8e2

File tree

3 files changed

+396
-0
lines changed

3 files changed

+396
-0
lines changed

content/posts/learncpp_10/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
+++
2+
date = '2025-11-13T13:58:39+08:00'
3+
draft = true
4+
title = 'cpp学习笔记(10)'
5+
+++
6+
7+
##
8+
9+
### `std::cin >>`
10+
11+
跳过输入流中前导空白符,当遇到下一个空白的时候停止,并把空白**保留**在输入缓冲区中。
12+
13+
简单来说,就是获取一个单词。
14+
15+
### C-style 的函数
16+
17+
有一些函数是C遗留下来的,所以他们不支持 `std::string`,只能使用传统的C风格字符串。
18+
19+
`std::getline()`
20+
21+
给定一个字符串缓冲的容器:
22+
23+
获取缓冲区的内容,直到遇到 `\n`,把 `\n` 以及前面的内容提取,然后**不将** `\n` 放在目标中。
24+
25+
重载函数 `(strBuf, n)` 可以提取指定的长度的字符,获取 `n -1` 个字符,因为要有 `\0` 结尾。
26+
27+
`gcount()` 用于统计提取的部分,包括 `\n`,但是不包括限定长度在后面添加的 `\0`

content/posts/learncpp_8/index.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
+++
2+
date = '2025-10-30T14:34:07+08:00'
3+
draft = true
4+
title = 'cpp学习笔记(8)'
5+
+++
6+
7+
## 左右值、`&&`、移动语义、智能指针
8+
9+
有两个重要概念需要区分:
10+
11+
### 类型(Type)
12+
13+
表示一段内存究竟是怎么样的性质。对于一段内存,它内部的结构应该是怎么样被解析的,使用什么样的运算符。
14+
15+
其实就是一开始的类型系统,包括基本类型的 `int``double`,也可以是标准库中的 `std::string`,用户在 `class` 定义类型。
16+
17+
### 值类别(Value Category)
18+
19+
在移动语义后完善的概念,重点关注此表达式本身的性质,具有什么身份,是否可以被移动。
20+
21+
在C++98,只有**左值****右值**
22+
23+
但是在C++11,我们引入了移动语义,出现了一个**将亡值**
24+
25+
将亡值有身份可以取地址,但是马上被移动也就消失了,所以我们扩大了左值的定义,认为他是**广义左值**
26+
27+
同时,因为他即将消失,所以认为是右值,那么就把以前的右值定义为**纯右值**,这样五种值类别就出来了。
28+
29+
### `&&`/`&` 作为函数形参
30+
31+
考虑下面这个函数:
32+
33+
```cpp
34+
int main(){
35+
int&& ref{ 5 };
36+
fun(ref);
37+
}
38+
```
39+
40+
给出两个 `fun` 的函数声明:
41+
42+
```cpp
43+
void fun(int&&);
44+
void fun(const int&);
45+
```
46+
47+
事实上,只有第二个能够匹配传入的参数,而不是第一个。
48+
49+
在没有引用的时候,我们的函数形参其实是匹配对象的类型的,因为只有确定了类型,函数内部才知道这个对象使用哪一种操作符,如何解释这一段内存。
50+
51+
引入引用之后,带 `&` 的参数会解释这个对象如何绑定到实参:
52+
53+
- **左值引用**:只能绑定到左值。
54+
- **常量左值引用**:能绑定到左值和右值,如果**直接**绑定到右值,会延长该对象的生命周期。
55+
- **右值引用**:绑定到右值时,在函数内部转换成一个左值;绑定到左值时,是一个左值引用。
56+
57+
但是需要注意一点,虽然 `ref` 是一个 `int&&` 类型的对象,但是他的值类别其实是一个左值,所以形参可以是一个(常量)左值引用。
58+
59+
### `&&` 在模板参数的使用
60+
61+
仅在模板以及泛型中,`&&` 才有引用折叠作用,常用于完美转发。
62+
63+
最常见的在于这样一个泛型函数:
64+
65+
```cpp
66+
template <typename T> wrapper(T&& arg){
67+
target(std::forward<T>(arg))
68+
}
69+
```
70+
71+
传入左值的时候,会被折叠成 `T&`,而传入右值的时候,保留 `T&&` 的类型,`&&`非常智能,自动判断左右值。
72+
73+
### 实现移动
74+
75+
`std::move()` 返回的值是一个将亡值(xvalue),至于如何把这个值移动,是类的移动构造函数/移动赋值运算符实现的。
76+
77+
所以如果手动编写的移动构造函数/移动赋值运算符,需要注意:
78+
79+
1. 加上 `noexcept` 关键字,避免抛出异常(要么成功,要么终止,避免移动一半),避免回退到复制。
80+
2. 手动把地址给到目标,然后把原来的指针指向空指针。
81+
3. 对于运算符,可以加上判断一下不是自己给自己,如果是就返回 `*this`
82+
83+
至于源对象如何释放,是取决它本身的生命周期的。此时我们已经完成了所有权的转移。
84+
85+
### 智能指针
86+
87+
88+
#### `unique_ptr`
89+
90+
这个指针指向的堆内存必须是它独占的,如果使用移动语义来赋值的话,会自动把上一个指针置空。
91+
92+
不要用一个裸指针来初始化一个智能指针,虽然语法上成立,但是语义上会破坏 `unique` 的独占性,仍然会发生双重释放的问题。
93+
94+
---
95+
96+
使用 `make_unique` 将一个纯右值的对象变成独占的指针,不用手动 `new` 故比较推荐。
97+
98+
`make_unique` 其实是一个函数,为一个纯右值返回一个独占指针,所以返回值一定会有一个指针接收,一般用 `auto` 配合效果很好:
99+
100+
```cpp
101+
auto ptr {std::make_unique<Fraction>(Fraction{3, 5})};
102+
```
103+
104+
---
105+
106+
如果要求传入一个类型的裸指针,但是手上只有智能指针,可以使用 `.get()` 来得到一个底层中使用的裸指针的副本。
107+
108+
#### `shared_ptr` 和 `weak_ptr`
109+
110+
共享指针允许多个指针同时指向一块内存,同时其内部会保存指向这段内存的计数。当最后一个共享指针销毁的时候,内存将会被自动释放。
111+
112+
---
113+
114+
不过 `shared_ptr` 会导致两个共享指针相互依赖的问题,这样在释放的时候认为是互相依赖使得最终内存无法释放导致内存泄漏。
115+
116+
最典型的例子就是,在一个类中有 `shared_ptr` 的成员变量,然后两个实例化的对象的共享指针指向对方,同时具有相同的生命周期,这样释放的时候互相认为对方存在,从而都不释放内存。
117+
118+
---
119+
120+
因此,`weak_ptr` 出现了。弱指针不增加引用计数,因此可以很好的解决上面的问题。
121+
122+
弱指针其实有点像 `string_view`,他们都只是一个视图(观察者)而已。
123+
124+
使用弱指针:
125+
126+
- `.expired()` 可以检测弱指针是否指向无效的内存。
127+
- `.lock()` 锁定指向的内容,把引用计数 +1,同时返回一个 `shared_ptr`;如果已过期,那么会返回 `nullptr`(这样甚至不需要 `std::optional`)。
128+
129+
#### 关系
130+
131+
`unique_ptr` 可以转化为 `shared_ptr`,反之不能转化。
132+
133+
`unique_ptr` 只能移动不能拷贝,`shared_ptr` 和 `weak_ptr` 可以拷贝。
134+
135+
`shared_ptr` 可以被 `weak_ptr` 创建一个视图。
136+
137+
`weak_ptr` 的成员函数 `.lock()` 有效时会返回一个 `shared_ptr`,否则会返回一个 `nullptr`。
138+
139+
## 组合、聚合和关联
140+
141+
| 属性 (Property) | 组合 (Composition) | 聚合 (Aggregation) | 关联 (Association) |
142+
| :----------------------- | :--------------------- | :----------------- | :----------------- |
143+
| **关系类型** | 整体/部分 | 整体/部分 | 其他不相关 |
144+
| **部分可属于多个整体** | 否 | 是 | 是 |
145+
| **部分的存在由整体管理** | 是 | 否 | 否 |
146+
| **方向性** | 单向 | 单向 | 单向或双向 |
147+
| **关系动词** | 是...的一部分(Part-of) | 拥有 (Has-a) | 使用 (Uses-a) |
148+
| **方式 | 类成员 | 引用(数组) | 引用(数组) |
149+
150+
## 初始化列表 `initializer_list`
151+
152+
如果想要让自己的类使用一个初始化列表来初始化,需要手动添加一个重载的构造函数:
153+
154+
```cpp
155+
template<typename T>
156+
class MyClass{
157+
private:
158+
T m_size{};
159+
T* m_data{};
160+
public:
161+
MyClass(int size) : m_size{size}, m_data{new T[static_cast<size_t>(size)]}{};
162+
MyClass(std::initializer_list<T> list) : MyClass(static_cast<T>(list.size())){
163+
// 用委托构造函数来创建一个
164+
std::copy(list.begin(), list.end(), m_data);
165+
}
166+
// 3/5/0法则的其他函数
167+
}

content/posts/learncpp_9/index.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
+++
2+
date = '2025-11-06T10:23:35+08:00'
3+
draft = true
4+
title = 'cpp学习笔记(9)'
5+
+++
6+
7+
## 继承,派生
8+
9+
### 继承初始化
10+
11+
在构造函数后面的 `:` 加入基类的列表初始化:
12+
13+
```cpp
14+
class Base {
15+
private:
16+
int m_baseVar{};
17+
18+
public:
19+
Base(int baseVar) : m_baseVar{baseVar} {}
20+
};
21+
class Derive : public Base {
22+
private:
23+
int m_deriveVar{};
24+
25+
public:
26+
Derive(int baseVar, int deriveVar) : Base{baseVar}, m_deriveVar{deriveVar} {}
27+
};
28+
```
29+
30+
### 修改派生类的访问权限
31+
32+
#### 访问修饰符
33+
34+
定义派生类的时候,`:` 后的访问限定符:
35+
36+
- `public`: 基类所有访问类型,即为派生类的访问类型。
37+
- `protected`: 基类的 `public` 在派生类中是 `protected`。
38+
- `private`: 基类的 `public` 和 `protected` 在派生类中是 `private`。
39+
40+
基类的 `private` 在派生类中默认永远无法访问。
41+
42+
#### `using`来强制改变单个成员
43+
44+
可以单独在派生类中修改基类中可访问的成员在派生类的访问权限。
45+
46+
```cpp
47+
class Derive : public Base {
48+
private:
49+
using Base::func; // 假设这两个在 Base 都是公开的
50+
using Base::m_var;
51+
}
52+
```
53+
54+
这样就把两个成员在派生类中限制在了 `private` 权限,重载函数会全部限制。
55+
56+
### 隐藏功能
57+
58+
如果基类和派生类存在同名的成员函数,那么派生类会隐藏基类的函数。
59+
60+
如果向派生类对象传入一个 `int`,基类里面的匹配 `int`,派生类匹配 `double`,还是会优先使用派生类的函数,然后通过数值转化把实参变成 `double`
61+
62+
解决方案是在派生类强制使用 `using Base::func` 来使用基类的函数。
63+
64+
### 删除功能
65+
66+
像复制构造函数一样,可以显示删除某些不需要的基类功能,用 `= delete`
67+
68+
但是仍然可以绕过派生类访问基类的功能:
69+
70+
```cpp
71+
std::cout << derived.Base::m_value;
72+
std::cout << static_cast<Base&>(derived).m_value;
73+
```
74+
75+
### 虚函数
76+
77+
虚函数是 `RTTI`(运行时类型信息)的典型代表。
78+
79+
在多个派生的类中,如果有签名一致的函数,前面加上 `virtual` 关键字,如果有类型为低派生的引用或者指针指向高派生的类,那么使用该指针将会自动调用尽可能高派生的类的函数。
80+
81+
如果一个派类在其继承链的中只要有一个类的函数是 `virtual`,那么其所有相同签名的函数均视为虚函数。
82+
83+
#### 覆盖
84+
85+
使用 `override` 关键字在派生类中显式的覆盖相同签名的函数,如果无法覆盖会报错,这样避免了签名不一致导致未覆盖还过编译的情况。
86+
87+
`override` 放在成员函数的 `const` 后。
88+
89+
`override` 是隐式 `virtual` 的,所以只有最底层的基类型需要 `virtual` 关键字,其他都可以写 `override`
90+
91+
`final` 放在 `override` 后,提示这是最后一个覆盖的派生类函数,后续派生的类若再覆盖,则报错。
92+
93+
#### 协变返回类型
94+
95+
当派生类中返回一个 `this` 指针的时候,这个指针指向的类型只跟调用它的引用/指针类型有关。
96+
97+
#### 虚析构函数
98+
99+
如果使用低层的引用/指针指向一个高层的派生类,释放的时候不会正确调用高层的析构函数。此时需要给析构函数添加 `virtual` 修饰符。
100+
101+
不推荐给所有的派生类添加 `virtual`,因为虚函数表会造成开销。如果不希望此类被继承,应该在定义类的时候在类名后添加 `final` 禁用对此类的继承。
102+
103+
#### 绑定和调度
104+
105+
- 早期绑定/早期调度:在编译的时候就已经确定了调用函数的地址。
106+
- 晚期绑定:使用函数指针调用函数
107+
- 晚期调度:调用虚函数,通过虚表查询,只有在运行时才知道真正调用的函数。
108+
109+
#### 纯虚函数,纯虚类,虚基类
110+
111+
纯虚函数指的是基类声明了有这个函数,但是实际上无法实例化,只能交由派生类实例化的函数。
112+
113+
在一个 `virtual` 后面加入 `= 0` 即可实现,也不需要函数体。
114+
115+
纯虚类(抽象类)其实相当于一个接口函数,这个类没有成员变量,只有纯虚函数,需要注意必须要有虚析构函数。此类不能被构造,一般在调用的时候,形参为其的引用/指针,并接受其派生类为实参。
116+
117+
虚基类在继承的 `:` 后面加入 `virtual`,目的是只有最后一个非虚的派生类创建对象,从而避免菱形继承造成的功能重复。
118+
119+
### 切片
120+
121+
当复制/移动的的目标类型是基类型时,会造成切片,只保留基类实例的部分。
122+
123+
### RTTI
124+
125+
#### `dynamic_cast`
126+
127+
用于向下转换(把一个基类型的指针转换成一个派生类型的指针),造成很大的开销,转换后需要判断是否转换成功。
128+
129+
当然用 `static_cast` 也可以转换,但是不会判断是否是否可以成功转换,访问的时候会造成内存问题。所以不推荐。
130+
131+
#### `typeid`
132+
133+
获取表达式和类型的类型信息,用于实现C++中的自省。
134+
135+
重载的运算符 `==`,用于比较两个对象是否完全一致。
136+
137+
成员函数 `.name()`,可被打印的、修饰过的编译器名字。
138+
139+
### 委派重载函数
140+
141+
`<<` 不能够被设置成虚函数,因为不是成员函数。但是可以委派给虚函数来打印,只需要把流对象的引用传递过去即可。
142+
143+
## 异常处理
144+
145+
经典的 `try-catch` 块和 `throw` 关键字。
146+
147+
在主函数里面使用 `try` 包裹功能调用函数的时候,函数也可以抛出异常,如果函数本身没有处理,会清理本块的资源,然后依次交由上一级调用栈处理,这个过程叫作栈展开。如果一直到主函数都没有处理,那么就会调用 `std::terminate()` 终止程序,此时可能未完成清理。
148+
149+
因此,建议使用 `catch(...)` 在最后捕获所有异常,此时可以保证所有资源都被清理。
150+
151+
析构函数是隐式 `noexcept` 的,因为如果析构函数又抛出异常(二次异常),那么程序将会直接终止。
152+
153+
异常类和重载不同,对于能同时接受派生类和基类的形参,会优先选择第一个 `catch`,所以需要把最匹配的放在前面。
154+
155+
### 异常对象的存储机制
156+
157+
异常对象会在抛出的时候进行复制,所以这个对象的类必须有复制构造函数,才能正常进行异常流程。
158+
159+
### `catch` 中抛出异常
160+
161+
可以在 `catch` 中抛出异常,如果希望再次抛出相同的异常向上处理,应该直接使用 `throw;` 而不是显式指定传入的名字,因为按值复制的过程会造成类型切片。
162+
163+
### 函数级异常
164+
165+
严格在函数的 `()` 后写 `try-catch` 块,此过程有隐式重抛异常,最好自己手动指定。
166+
167+
一般用于构造函数中。
168+
169+
```cpp
170+
171+
class B : public A {
172+
public:
173+
B(int x)
174+
try
175+
: A{x} // note addition of try keyword here
176+
{
177+
} catch (...) // note this is at same level of indentation as the function itself
178+
{
179+
// Exceptions from member initializer list or
180+
// from constructor body are caught here
181+
182+
std::cerr << "Exception caught\n";
183+
184+
// throw; // rethrow the existing exception
185+
}
186+
}
187+
```
188+
189+
### `noexcept`
190+
191+
没有带 `noexcept` 的函数都是潜在的抛出异常的函数。
192+
193+
要求带有此标记的函数,必须在这个块前完成异常处理,如果超出了这个块,那么整个程序就被强行终止(使用 `std::terminate`
194+
195+
使用 `noexcept` 允许编译器进行深度的优化,因为不需要考虑异常的栈展开机制。
196+
197+
### `std::move_if_noexcept`
198+
199+
在编译的时候确定移动构造函数是否有 `noexcept` 关键字,如果没有,就 `fallback` 到复制。
200+
201+
其实此函数传入的形参是可变的,如果移动构造保证无异常抛出,那么就传入将亡值,使用移动构造函数;如果没有,那么就传入左值,使用复制构造函数。
202+

0 commit comments

Comments
 (0)