CPP学习笔记
1 CPP basic
1.1 main function
-
主函数完整形式
int main(int argc, char *argv[]){ // } // 或者 int main(int argc, char **argv){ // }
-
代码示例
#include <iostream> using namespace std; int main(int argc, char *argv[]) { for (int i = 0; i < argc; ++i) { cout << "第" << i << "个参数是" << argv[i] << " "; } cout << endl; return 0; // 可以不写 } // 调用 ./cpp_lab abc def ghi // 输出 第0个参数是./cpp_lab 第1个参数是abc 第2个参数是def 第3个参数是ghi
1.2 数据的位表示
-
包含头文件
#include <bitset>
。 -
指定位数,32位、64位等。
-
创建 bitset 对象
std::bitset<32> bits;
。 -
将值赋给bitset对象,
int value = -3; bits = value;
。#include <iostream> #include <bitset> int main() { // 假设我们处理一个32位的整数 const int numBits = 32; int value = -3; // 我们要获取二进制表示的值 // 创建一个 bitset 对象,大小为 numBits std::bitset<numBits> bits(value); // 输出二进制位表示 std::cout << "The binary representation of the value is: " << bits << std::endl; // 将 bitset 转换为字符串形式并输出 std::string bitString = bits.to_string(); std::cout << "The binary string representation is: " << bitString << std::endl; return 0; }
1.3 trick
- 批量更改变量名——refractor->rename
2 Compile and link
2.1 Function prototypes and definitions 函数原型和定义
-
function prototype(.h/.hpp)
int add(int a, int b);
-
function definition(.c/.cpp)
int add(int a, int b){ return a + b; }
2.2 Compile and link
-
编译
g++ -c main.cpp # -c 表示只编译不链接,编译为 main.o g++ -c func.cpp # 编译为 add.o
-
链接
g++ main.o add.o -o add # -o表示输出
2.3 Debug
- compilation errors 编译错误——语法错误
- link errors 链接错误——undefinded/not found
- runtime errors 运行时错误——除0异常
3 Preprocessor and macros 预处理器和宏
3.1 Preprocessor 预处理器
-
preprocessing instructions 预处理指令
define, undef, include, if, ifd, ef, ifndef, else, endif, line, error, pragma
-
预处理指令由预处理器执行
-
#pragma once
指令表示只include
一次,该指令不能写在main
函数中
3.2 Macros
-
使用预处理器 #define 定义宏常数 PI 代替3.14
#define PI 3.14
4 Input and output
4.1 C++ style outpout
-
#include<iostream> int v = 100; std::cout << "v is " << v << "." << std::endl; // v is 100.
4.2 C++ style input
-
int a, b; std::cin >> a >> b;
4.3 C style output
-
#include<cstdio> int v = 100; printf("v is %d\n.", v); // v is 100.
4.4 C style input
-
int v; scanf("%d", &v);
5 Integer type 整型
5.1 Int
-
declare and initialize
int i; // 声明 int j = 10; // 声明和初始化 int k; k = 10; // 赋值
-
⭐变量必须初始化
- 不确定行为,程序崩溃,数据损坏,逻辑错误,安全漏洞等
-
overflow——没有警告,没有错误
int num1 = 56789; int num2 = 56789; cout << num1 * num2 << endl; // -1069976775
5.2 Variable initialization
-
整数初始化
int num = 10; int num(10); int num{10}
5.3 signed and unsigned(64位系统)
-
至少16位
-
signed int ∈ [ − 2 31 , 2 31 − 1 ] \in[-2^{31},2^{31}-1] ∈[−231,231−1]
-
unsigned int ∈ [ 0 , 2 32 − 1 ] \in[0,2^{32}-1] ∈[0,232−1]
-
signed short int
-
unsigned short int
-
-
至少32位
- signed long int
- unsigned long int
-
至少64位
- signed long long int
- unsigned long long int
5.4 sizeof 操作符
- 类型和变量:
sizeof
可以用于数据类型或变量。当用于类型时,它返回类型的尺寸;当用于变量时,它返回变量的尺寸。 - 数组:当用于数组时,返回整个数组占用的内存大小,而不是数组单个元素的大小或指针的大小。
- 指针:当用于指针时,返回指针本身的大小,而不是指针所指向的数据的大小。
- 结构体和类:
sizeof
可以用来确定用户定义的类型,如结构体(struct
)和类class
的大小。 - 动态内存:
sizeof
也可以用来确定动态分配的内存块的大小。 - 复合字面量:C++11 引入了复合字面量,
sizeof
可以直接应用于它们。 - 类型安全:
sizeof
是类型安全的。如果操作数的类型是依赖于模板参数的,那么sizeof
将在模板实例化时计算大小。 - 宏和常量表达式:
sizeof
可以用在宏定义和常量表达式中,这使得它在编译时常用于条件编译和大小检查。 - 内存对齐:
sizeof
的结果受编译器和平台的内存对齐规则的影响。有些类型可能由于对齐要求而比实际占用的内存稍大。
5.5 typedef
-
创建别名,可以用来代替复杂的类型名
unsigned char color[3] = {255, 0, 0}; // 使用typedef typedef unsigned char vec3b[3]; vec3b color = {255, 0, 0};
6 More integer types
6.1 Char
-
char:8位整数类型
- ⭐signed and unsigned
-
表示一个 ASCII 字符
char c = 'C'; // 使用单引号包括字符 char c = 80; // 使用 ASCII 码数值 char c = 0x50; // 使用十六进制 ASCII 数值
-
中文字符
char16_t c = u'中'; char32_t c = U'中';
6.2 Bool
-
bool:C++ 关键字,非0即 true,占8个位
int a = 1; bool b = true; int c = 0; bool d = false; cout << (a == b) << "," << (c == d) << endl; // 1,1
-
在C语言中使用bool类型
-
使用
typedef
typedef char bool; // 定义一个新类型 bool,是 char 类型的别名 #define true 1 // 定义宏 true 替换1 #define false 0 // 定义宏 false 替换0
-
使用
stdbool.h
#include <stdbool.h> //从 C99 开始,包含该库可使用 bool 类型 bool tag = true;
-
6.3 size_t
- 无符号整数,用于表示内存大小或数量
sizeof
运算符的结果的类型- 32-bit,or 64-bit
6.4 <cstdint>
- type
- int8_t, int16_t, int32_t, int64_t
- uint8_t, uint16_t, uint32_t, uint64_t
- …
- macros
- INT8_MIN, INT16_MIN, INT32_MIN, INT64_MIN
- INT8_MAX, INT16_MAX, INT32_MAX, INT64_MAX
- …
7 Floating point number
7.1 Floating-point types
-
float-32位
-
double-64位
-
long double
- 128位
- 64位
-
half-float-16位
7.2 浮点型 VS 整型
-
存储
- 整数:通常使用二进制补码进行存储。
- 浮点数:遵循 IEEE 754 标准,使用科学计数法的二进制形式存储。
-
优缺点
- 整数:运算精确,没有舍入误差;整数运算比浮点数运算更快;存储和传输内存使用更高效。
- 浮点数:表示范围比整数更大,能够表示分数和小数;在高精度和大范围数值的科学计算中非常有用。
-
应用场景
- 整数:适用于计数、索引、数组大小、循环计数器等场景,以及任何需要精确数值计算的场合。
- 浮点数:适用于需要表示小数或进行科学计算的场合,如物理模拟、图形渲染、金融计算等。
7.3 Precision
-
float
类型通常有 32 位的存储大小,包括 1 位符号位、8 位指数位和 23 位尾数位(小数位)。 -
在 [ 0 , 1 ] [0,1] [0,1]之间有无穷个数,用32位/64位不能表示所有小数
-
浮点数运算操作永远会带来微小误差,这些误差不能被消除
-
近似精度损失:比较浮点数不建议使用
==
float f1= 23456789345.0f; // 转化为可以接受的精度范围 float f2= f1 + 10; cout << (f1 == f2) << endl; // 输出 1
-
设置容差:在比较浮点数时,使用一个足够小的容差值来检查两个数是否“足够接近”。
bool areAlmostEqual(float a, float b, float tolerance) { return fabs(a - b) <= tolerance; }
7.4 inf or nan
-
无穷大(Infinity):当一个数除以零时,结果趋向于无限大。在浮点数表示中,这通常用一个特殊值来表示,即
inf
。对于正数除以零,结果是+inf
;对于负数除以零,结果是-inf
。 -
NaN:NaN 是一个特殊的浮点数值,表示不是一个数字。它通常在不合法的运算中产生,比如零除零或者无穷大减去无穷大。
-
检查inf和nan
float f1 = 2.0f / 0.0f; float f2 = 0.0f / 0.0f; cout << f1 << "," << f2 << endl; // 输出inf,-nan cout << std::isinf(f1) << "," << std::isnan(f2) << endl; // 输出 1,1
8 Arithmetic operators 算数运算符
8.1 Constant numbers 常量
-
注意事项
- 常量定义后必须初始化。
- 常量一旦定义,其值在程序的整个生命周期内不能被改变。
- 使用
const
定义的常量类型安全,而宏常数(使用#define
)不具备类型安全。 - 宏常数在预处理阶段展开,不进行类型检查,而
const
定义的常量在编译时检查。
-
字面量常量:直接在代码中使用的固定值,如数字、字符或字符串。
int maxInt = 2147483647; // 整型字面量 double pi = 3.14159; // 双精度字面量 char newlineChar = '\n'; // 字符字面量 const char* greeting = "Hello"; // 字符串字面量
-
常量指针和指针常量
const int* ptr = &a; // 指针常量,指向常量的指针 int* const ptr2 = &a; // 指针常量,指针本身的值不可变 const int* const ptr3 = &a; // 同时是指针常量和常量指针
8.2 const 类型限定符
-
如果一个变量/对象是const类型,在声明时必须初始化,其值不能被改变
const float PI = 3.1415926f; PI += 1; // error!
8.3 auto (since C++11)
-
⭐注意事项
- auto 不是一个类型,而是让编译器根据上下文推断类型的一种方式。
- auto 必须被初始化,编译器不能在没有初始化的情况下推断其类型。
- 在C++11及以后的版本中,auto 的使用变得更加广泛和强大。
-
自动类型推断:使用
auto
可以让编译器根据初始化表达式来推断变量的类型。auto a = 2; // a 的类型被推断为 int auto b = 2.3; // a 的类型被推断为 double auto c = "string"; // c 的类型被推断为 const char[]
-
通用编程:
auto
在模板编程中非常有用,特别是与泛型算法和容器一起使用时,可以避免冗长的类型声明。std::vector<std::string> strings = {"Hello", "World"}; for (auto it = strings.begin(); it != strings.end(); ++it) { std::cout << *it << std::endl; }
-
范围 for 循环:
auto
在范围for循环中用于简化遍历容器元素的语法。for (auto str : strings) { std::cout << str << std::endl; }
-
lambda 表达式:
auto
在定义lambda表达式时用于声明捕获的变量。auto lambda = [capture](args) -> return_type { /* 函数体 */ };
-
函数返回类型(since C++ 14):从 C++14 开始,
auto
也可以用于函数返回类型,使函数能够返回与返回语句中表达式的类型相同的值。auto add(int a, int b) { // 返回类型自动推断为 int return a + b; }
8.4 算数运算符
- 运算符
8.5 ~ 取反运算符
-
对于正数,
~
运算符会得到一个负数。这是因为正数的二进制补码表示取反后,变成了其对应的负数的补码表示。 -
对于负数,
~
运算符的效果稍微复杂。由于负数在计算机中通常使用补码表示,取反操作会得到一个更大的负数。 -
对于
0
,~
运算符的结果取决于整数类型的大小。例如,在32位整数中,~0
的结果是-1
,因为所有位都是1
。 -
⭐注意事项
~
运算符只应用于整数类型,包括有符号和无符号整数。- 结果的符号取决于操作数的类型。对于有符号整数,取反可能会得到一个负数;对于无符号整数,结果将是一个正数,但表示为该类型的可能值之一。
- 在有符号整数的上下文中使用
~
运算符时,结果应该被解释为补码。这意味着取反操作实际上会得到该数的加法逆元(additive inverse)。 ~
运算符不适用于浮点数。
-
对32位整数2取反
2 的原码:0000 0000 0000 0000 0000 0000 0000 0010
2 的反码:1111 1111 1111 1111 1111 1111 1111 1101
-3 的原码:1000 0000 0000 0000 0000 0000 0000 0011
-3 的反码:1111 1111 1111 1111 1111 1111 1111 1100
-3 的补码:1111 1111 1111 1111 1111 1111 1111 1101
8.6 数据类型转换
-
隐式类型转换(自动类型转换):C++编译器在需要时会自动进行类型转换,通常发生在赋值或函数参数传递时。
- 小范围转换:从范围较小的类型到范围较大的类型,如
char
到int
。
- 小范围转换:从范围较小的类型到范围较大的类型,如
- 标准转换:如从整数类型到浮点类型。
```cpp
int i = 10;
double d = i; // 隐式转换 int 到 double
```
-
显式类型转换:通过强制类型转换(cast)操作符显式地将一个类型转换为另一个类型。
- static_cast(expression):用于基本的非多态类型转换。
dynamic_cast(expression):用于类层次结构中的安全向下转型。
reinterpret_cast(expression):用于低级别的重新解释转换。
const_cast(expression):用于移除或添加
const
属性。
```cpp
double d = 3.14;
int i = static_cast<int>(d); // 显式转换 double 到 int
```
-
四舍五入转换:在转换整数类型时,可能会发生四舍五入。
- 截断:丢弃小数部分,只保留整数部分。
float f = 3.99f; int i = f; // i 将被赋值为 3
-
整数提升:较小的整数类型(如
char
或short
)在参与运算时可能会被提升为int
类型。char c = 5; int result = c + 10; // c 被提升为 int 类型
-
浮点数转换:在将浮点数转换为整数时,小数部分会被丢弃。
float f = 3.99f; int i = f; // i 将被赋值为 3
-
字符串到数字的转换:可以使用
std::stoi
、std::stol
、std::stoll
、std::stof
、std::stod
、std::stold
等函数将字符串转换为数字。string str = "123"; int i = std::stoi(str);
-
数字到字符串的转换:可以使用
std::to_string
函数将数字转换为字符串。int i = 123; std::string str = std::to_string(i);
9 Branch
9.1 if and if-else
else
和最近的未匹配的if进行匹配
9.2 ? :
operator
-
三元运算符语法:
condition ? expression1 : expression2;
。int x = 0; bool condition = true; if (condition) { x = 1; } else{ x = -1; } // 使用三元运算符 x = condition ? 1 : -1; // 优化跳转,保留计算 x = (condition) * 2 -1
9.3 指针表达式
-
空指针是0(false)
int * p = new int[1024]; if (!p) // if(p == NULL) cout << "分配内存失败!" << endl;
10 Loop
10.1 基础循环
-
for 循环:是一种基本的计数器循环,通常用于已知迭代次数的情况。
-
while循环:在给定条件为真时重复执行代码块,适用于迭代次数未知的情况。
-
do-while循环:与
while
循环类似,但它至少执行一次,因为条件判断在循环体之后。
10.2 范围 for 循环(since C++11)
-
范围
for
循环允许遍历容器中的所有元素,而不需要使用索引#include <vector> #include <iostream> std::vector<int> numbers = {1, 2, 3, 4, 5}; for (const auto& number : numbers) { std::cout << number << " "; } // 输出: 1 2 3 4 5
10.3 for 循环与迭代器(STL 容器)
-
使用迭代器遍历标准模板库(STL)容器,如
std::vector
、std::list
等。#include <vector> #include <iostream> std::vector<int> numbers = {1, 2, 3, 4, 5}; for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) { std::cout << *it << " "; } // 输出: 1 2 3 4 5
10.4 for 循环与反向迭代器(STL 容器)
-
反向迭代器允许你从容器的末尾开始向前遍历。
#include <vector> #include <iostream> std::vector<int> numbers = {1, 2, 3, 4, 5}; for (std::vector<int>::reverse_iterator rit = numbers.rbegin(); rit != numbers.rend(); ++rit) { std::cout << *rit << " "; } // 输出: 5 4 3 2 1
10.5 goto 语句
-
虽然不推荐使用
goto
语句,但它们可以用于跳出多层循环。loop: // 标签 for (int i = 0; i < 10; ++i) { for (int j = 0; j < 10; ++j) { if (i * j > 50) { goto end; // 跳出两层循环 } } } end: // 标签 std::cout << "Loop exited." << std::endl;
11 Array
11.1 静态array
- 元素类型可以是任何基础类型(int, float, bool,等), structure, class, pointer, enumeration
- 数组的大小必须是常量表达式,这意味着在编译时必须已知其大小。
- 使用未初始化的局部数组(非静态)可能导致不确定的值。
- 对于静态或全局数组,如果不显式初始化,将自动初始化为0。
- 数组初始化时,元素的构造函数(如果存在)将被调用。
11.2 数组初始化
-
静态初始化(使用初始化列表):在声明数组时,可以使用花括号
{}
包围的初始化列表来初始化数组。int arr1[] = {1, 2, 3, 4, 5}; // 声明并初始化一个整型数组 std::string names[] = {"Alice", "Bob", "Charlie"}; // 声明并初始化一个字符串数组
-
指定元素初始化:如果初始化列表中的元素少于数组的大小,剩余的元素将被初始化为默认值(对于类类型是默认构造函数的结果,对于基本类型通常是0)。
int arr2[5] = {0, 1}; // arr2 = {0, 1, 0, 0, 0}
-
复制初始化:如果数组的初始化列表只有一个元素,该元素的值将被复制到数组的所有元素中。
int arr3[5] = {1}; // arr3 = {1, 1, 1, 1, 1}
-
列表初始化(since C++ 11):列表初始化允许使用花括号初始化基本数组类型,而不需要显式指定类型。
auto arr4 = {1, 2, 3, 4, 5}; // auto 推断为 int[]
-
标准数组初始化(C++17引入):C++17允许使用
{}
直接初始化具有静态存储期或线程存储期的数组。int arr5[]{1, 2, 3, 4, 5}; // 从 C++17 开始,可以省略类型前的空格
-
填充初始化:可以使用标准库函数
std::fill
或std::fill_n
来初始化数组。int arr6[5]; std::fill(arr6, arr6 + 5, 10); // 将 arr6 的所有元素初始化为 10
11.3 Variable-Length Arrays 变长数组(C99数组)
-
变长数组在运行时确定数组的大小。
-
其中
type
是数组元素的类型,array_name
是数组的名称,n
是数组的大小,必须是一个整型表达式,其值在编译时不必是常量。int size = 0; // 声明一个int变量并初始化位0 cin >> size; // 运行时获取数组长度 int arr[size]; // 声明一个变长数组
-
变长数组不能在声明时初始化,必须在声明后逐个初始化。
-
变长数组只能在函数内部使用,不能在全局或命名空间作用域内使用。
-
⭐在C++11中,变长数组被标记为已弃用(deprecated),建议使用动态分配数组或标准容器库。
11.4 Arrays of unknown size 未知长度数组
-
在声明时不指定数组长度
int arr[] = {1,2,3,4,5}; // 由初始化列表长度推断,arr长度位5
-
函数的参数列表
int array_sum(int values[], size_t length); // 传入数组 int array_sum(int * values, size_t length); // 传入指针
11.5 数组元素读写
-
⭐注意事项
- 在读取或写入之前,应确保索引在有效范围内。
- 尝试访问数组之外的索引将导致未定义行为,可能触发运行时错误。
- 数组元素的类型应与数组声明的类型一致
- 写入操作会修改数组的内容。如果数组是
const
修饰的,那么它的元素就不能被修改。 - 数组在声明时分配内存,直到数组离开作用域或被显式释放前,内存才会被回收。
-
使用指针遍历数组:数组名本身就是一个指向数组首元素的指针。
int* ptr = arr; // 获取数组首地址 while (ptr < arr + sizeof(arr) / sizeof(arr[0])) { std::cout << *ptr << " "; // 读取当前元素 ++ptr; // 移动到下一个元素 }
11.6 Multidimensional arrays 多维数组
-
多维数组在科学计算、图像处理、游戏开发等领域中非常有用。
-
由于多维数组的一些限制(如在函数间传递时的退化问题),有时会使用其他数据结构来替代,例如:
std::vector
的嵌套:使用std::vector
可以创建动态的多维数组,并且可以更灵活地处理数组的大小和内存。std::array
的嵌套:对于编译时常量大小的多维数组,可以使用std::array
。
-
二维数组的偏移量要手动确定,即数据第二维要手动赋值
void init_2d_array(int matrix[][10], size_t rows, size_t cols);
11.7 数组拷贝
-
⭐注意事项
- 当拷贝数组时,通常只拷贝数组的内容,而不是数组对象本身。
- 对于自定义类型或包含动态分配内存的数组,需要确保正确实现了拷贝构造函数和赋值操作符。
- 使用
memcpy
时要小心,因为它不会调用构造函数或析构函数,也不会处理指针或动态分配的内存。 - 对于C++标准库容器(如
std::vector
、std::array
),可以直接使用赋值操作符来拷贝数组。
-
手动拷贝:遍历数组,逐个元素赋值。
-
使用
std::copy
函数(STL算法)#include <algorithm> // 包含 std::copy int source[] = {1, 2, 3, 4, 5}; int target[4]; std::copy(source, source + 4, target);
-
使用数组的默认构造函数
class MyArray { public: MyArray(const int arr[], size_t size) : data(arr, size) {} private: std::vector<int> data; }; int main() { int source[] = {1, 2, 3, 4}; MyArray myArray(source, 4); return 0; }
-
使用
memcpy
函数#include <cstring> // 包含 memcpy int source[] = {1, 2, 3, 4, 5}; int target[4]; std::copy(target, source, sizeof(source));
-
使用
std::fill
函数#include <algorithm> // 包含 std::fill int destination[4]; std::fill(destination, destination + 4, 10); // 用10填充数组
-
拷贝构造函数
#include <iostream> #include <vector> // MyArray 类的定义 class MyArray { public: // 默认构造函数 MyArray() : data() {} // 通过 vector 初始化的构造函数 explicit MyArray(const std::vector<int> &vec) : data(vec) {} // 拷贝构造函数 MyArray(const MyArray &other) : data(other.data) { std::cout << "Copy constructor is called." << std::endl; } // 成员函数,打印数组内容 void print() const { std::cout << "Array contains:"; for (int num : data) { std::cout << " " << num; } std::cout << std::endl; } private: std::vector<int> data; // 私有成员变量 }; int main() { // 创建一个 C 风格数组并初始化 std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用 C 风格数组构造 MyArray 对象 MyArray arr(vec); // 打印 arr 的内容 arr.print(); // 创建 arr 的副本,调用拷贝构造函数 MyArray arrCopy = arr; // 打印 arrCopy 的内容 arrCopy.print(); return 0; } // 输出 // Array contains: 1 2 3 4 5 // Copy constructor is called. // Array contains: 1 2 3 4 5
11.8 常量数组
- 用
const
修饰的数组,不可更改
12 String
12.1 Array-style strings
-
⭐注意事项
- C风格字符串或字符数组,使用字符数组来表示和存储字符串,通常以空字符
\0
来结尾。
- C风格字符串或字符数组,使用字符数组来表示和存储字符串,通常以空字符
- 没有内置边界检查,错误的使用可能达农之缓冲区溢出等安全问题。
- 由于数组风格字符串以空字符
'\0'
结尾,所以在字符串的实际长度和数组长度之间可能存在差异。- 字符串字面量(如
"Hello, World!"
)在C++中自动被视为数组风格字符串,但它们通常被存储在只读内存段中。- 使用数组风格字符串时,需要手动管理字符串的长度,因为没有内置的方法来确定字符串的长度(除了计算空字符之前字符的数量)。
- 由于C++标准库提供了
std::string
类来处理字符串,它提供了更安全和方便的字符串操作,因此在现代C++编程中,推荐使用std::string
而不是数组风格字符串。
-
初始化
char hello_world[] = "Hello, World!"; // 声明并初始化一个数组风格字符串 char hello_world[20] = "Hello, World!"; // 声明一个足够大的数组并初始化 char hello_world[] = "Hello, " "World!"; // 正确,内容为 "hello, world!" char hello_world[13] = "Hello, World!"; // error! 需要至少14个位置
-
strlen:获取字符串的长度,不包括结尾的空字符
'\0'
. -
stechr和strstr:在字符串中查找特定的字符或子字符串。
char str[] = "Welcome to Shanghai!"; char *found = strchr(str, 'S'); // 返回指向 'S' 的指针 cout << *p <<endl; // S
-
strcmp:比较两个字符串,结果位两个字符串的字典顺序比较结果。
-
strcpy:将一个字符串复制到另一个字符串数组中。⭐它会一直复制直到遇到源字符串的空字符(
'\0'
),然后停止复制。// 原型 char *strcpy(char *dest, const char *src); char str1[] = "Hello, World!"; // strlen(str1) == 5 char str2[20]; strcpy(str2, str1); // 将 str1 复制到 str2
-
strncpy:防止数组越界,可指定拷贝字符串数据的个数。比
strcpy
更安全。// 原型 char *strncpy(char *dest, const char *src, size_t count); char str1[] = "Hello, World!"; // strlen(str1) == 5 char str2[20]; strncpy(str2, str1, 13); // 复制前13个字符,包括空字符 str2[13] = '\0'; // 手动添加空字符以确保字符串正确结束 cout << str1 << endl; // 输出:Hello, World!
-
strcat:连接两个字符串。
// 原型 char *strcat(char *dest, const char *src); char str1[20] = "Hello, "; // strlen(str1) == 5 char str2[] = "World!"; strcat(str1, str2); // str1 为 "Hwllo, World!"
-
strncat:防止数组越界,可指定源字符串连接到指定字符串的个数。比
strcat
更安全。// 原型 char *strncat(char *dest, const char *src, size_t count); char dest[20] = "Hello, "; char src[] = "World! This is a longer string."; strncat(dest, src, 6); // 只拼接 "World!" 到 "Hello, " 末尾 std::cout << dest << std::endl; // 输出: Hello, World!
12.2 String class
-
不同的 string 类型
std::string std::wstring std::u8string // C++20 std::u16string // C++11 std::u32string // C++11
-
⭐string 类也没有越界访问检查
13 Structures, unions, enumerations
13.1 structures 结构体
-
结构体:将不同类型的数据项组合成一个单一的实体。
struct StructName { type1 member1; type2 member2; ... typeN memberN; }; // StructName:结构体的名称。 // typeX:成员变量的数据类型。 // memberX:成员变量的名称。
-
内存对齐
- 自然对齐:每个成员的自然对齐边界是其类型大小的整数倍。例如,一个
int
类型的成员(通常大小为4字节)应该在地址为4的倍数的位置上。 - 结构体的总大小:结构体的总大小是其所有成员大小的总和,但通常会被扩展到最宽基本类型成员的整数倍。这样做是为了提高内存访问的效率。
- 填充:在成员之间可能会插入填充字节(通常是0),以确保每个成员都满足其自然对齐要求。
- 自然对齐:每个成员的自然对齐边界是其类型大小的整数倍。例如,一个
13.2 union
-
union
(联合体)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型,但是一次只能访问一个成员。联合体的主要特点是所有成员共享相同的内存地址,因此它们的内存重叠。union UnionName { type1 member1; type2 member2; ... typeN memberN; }; // UnionName:联合体的名称。 // typeX:成员的数据类型。 // memberX:成员的名称。
-
基本特性
- 共享内存:联合体的所有成员都存储在相同的内存块中。
- 大小限制:联合体的大小等于其最大成员的大小,而不是成员大小的总和。
- 访问限制:在任何给定时间,只能访问联合体的一个成员。访问一个成员可能会覆盖其他成员的值。
13.3 结构体 VS 联合体
- 内存对齐:结构体可能需要更多的内存对齐,而联合体则不需要。
- 数据完整性:结构体可以同时保留所有成员的数据,而联合体只能保留一个成员的数据。
- 使用场景:结构体适用于需要同时访问多个成员的情况,联合体适用于需要在不同时间点访问不同类型的数据的情况。
13.4 enum
-
枚举类型(
enum
)是一种数据类型,它允许你为一组整数值赋予更易读和更易理解的标签。枚举类型提供了一种方式来定义命名的常量,这些常量通常用于控制程序的流程或表示特定的状态。enum EnumName { Constant1 = value1, Constant2 = value2, ... ConstantN = valueN }; // EnumName:枚举类型的名称。 // ConstantX:枚举常量的名称。 // valueX:为常量指定的整数值(可选)。
-
枚举类:从C++11开始,引入了枚举类(
enum class
),它提供了更强的类型安全性和更好的作用域控制。
14 Pointers
14.1 pointers
-
指针是一个变量,其值为另一个变量的地址。通过指针,可以访问和操作存储在该地址上的数据。指针的大小由
CPU
的寻址能力决定,不由数据类型决定。 -
取地址运算符(&):取地址运算符
&
用于获取变量的内存地址。 -
间接寻址运算符(*):间接寻址运算符
*
用于访问指针所指向地址上的值。int var = 10; // 声明一个变量并初始化为10 int *p = &var; // 取 var 地址初始化整型指针 p cout << *p << endl; // 输出:10
14.2 结构体成员访问
-
p->member
-
(*p).member
struct Worker{ char name[5]; int id; int age; }; // 初始化 Worker wkr = {"Li", 111222, 25}; // 指针声明和初始化 Worker *p = &wkr; // 修改结构体 p->age = 10; // 正确,age是一个变量 p->name = "Wange"; // 错误,name是一个字符串数组,可以通过字符串拷贝实现 strncpy(p->name, "Wang", 5); // 通过 strncpy 方法拷贝赋值 // 打印地址 printf("%p\n", p); // C style cout << p << endl; // C++ style cout << &wkr << endl;
14.3 pointers of pointers
- 声明一个指针变量来存储另一个指针的地址。
- 动态内存分配、函数参数传递、多维数组的模拟等。
14.4 constant pointers
-
const在
type *
前面int num = 10; const int *p1 = # // 声明指针常量 *p1 = 20; // NO,不能修改指针指向内存的内容 p1 = &another; // YES,指针的指向可以改变
-
const在
type *
后面int *const p2 = # // 声明常量指针 *p3 = 3; // YES,可以修改指针指向内存的内容 p2 = &another; // NO,不能修改指针指向
-
type *
前后均有constconst int *const p3 = #
15 Pointers and arrays
15.1 数组名和指针
-
使用
&
操作符获取数组元素的地址。 -
数组名是一个指向数组首元素的指针。
char name[5] = "lbl"; // C style output printf("%p\n", name); // 数组首元素地址 printf("%p\n", &name); // 数组首元素地址 printf("%p\n", &name[0]); // 数组首元素地址 // C++ style output cout << endl << name << endl // abc << &name << endl // 数组首元素地址 << &name[0] << endl; // abc // 通过指针访问 char *p = name; p[0] = 'd'; cout << p << endl; // dbc
15.2 指针运算
-
p + num 或 num + p 指向数组 p 的第 num 个元素。
-
p - num 指向 -num 元素。
int arr[] = {1, 2, 3, 4, 5}; int *p = arr; //以下操作等价 p[2] = 10; *(p + 2) = 10; int *p2 = p + 2; p2 = 10;
15.3 数组和指针的区别
- 数组是一个常量指针
sizeof(pointer)
的大小即地址的大小(4或8),sizeof(array)
的大小即数组元素所占的全部内存大小。
16 内存分配:C style
16.1 program memory 内存模型
-
程序的地址空间包括几个数据段:
- Code:可执行代码
- Data:初始化的静态变量
- BSS:未初始化的静态数据,包括变量和常量
- Heap:动态分配的内存,
- Stack:局部变量,调用栈
16.2 memory allocation 内存分配
-
分配未初始化的
size bytes
大小的内存。void *malloc(size_t size);
-
申请4个字节,显示地转换为(int *)指针
int *p = (int *) malloc(4);
16.3 memory deallocation 内存释放
-
动态分配的内存必须显式地释放
void *fre(void *ptr);
-
内存泄漏:
- 内存泄漏通常发生在使用
malloc
、calloc
或realloc
等函数分配内存后,没有使用free
函数来释放内存,导致无法再被程序使用或回收。 - ⭐申请-释放相匹配。
- 内存泄漏通常发生在使用
17 内存分配:C++ style
17.1 new and new[]
-
new
int *p1 = new int; // 分配1个 int,默认初始化 int *p2 = new int(); // 初始化为0 int *p3 = new int(5); // 初始化为5 int *p4 = new int{}; // 初始化为0,从 C++11 开始 int *p5 = new int{5}; // 初始化为5,从 C++11 开始 // 结构体 struct Student{ char name[5]; int years; bool gender; }; Student *ps1 = new Student; // 默认构造函数初始化 Student *ps2 = new Student{"Li", 2024, 1}; // 从 C++11 开始
-
new[]
int *p1 = new int[16]; // 分配16个 int,默认初始化 int *p2 = new int[16](); // 初始化为0 int *p3 = new int[16]{}; // 初始化为0 int *p4 = new int[16]{1, 2, 3}; // 前三个数位 1,2,3,其余位0 // 结构体数组 struct Student{ char name[5]; int years; bool gender; }; Student *psa1 = new Student[16]; // 默认构造函数初始化 Student *psa2 = new Student[16]{{"Li", 2024, 1}, {"Wang", 2023, 0}}; // 从 C++11 开始
17.2 delete and delete[]
- 非数组释放:delete p1;
- 数组释放:delete []ps1;
18 Function
18.1 函数基础
- 函数的定义通常包括以下部分:返回类型、函数名、参数列表和函数体。
- 通过调用运算符
()
来执行函数,调用运算符作用于一个表达式,该表达式是函数或指向函数的指针。
18.2 函数调用
- 函数的调用主要完成两项工作:一是用实参初始化函数对应的形参;二是将控制权转移给被调用函数。
- 主调函数的执行被暂时中断,被调函数开始执行。
19 Function parameters and arguments 函数参数
19.1 函数的形参列表
-
没有形参
void f1() {/*...*/} // 隐式 void f2(void) {/*...*/} // 显式
-
形参声明
void f3(int, int) {/*...*/} void f4(int v1, int v2) {/*...*/}
19.2 默认参数
-
若多次调用中都被赋予一个相同的值,此时可以将值设为默认参数。
-
让不使用默认实参的形参出现在参数表前面,有默认实参的形参出现在后面。
int func(int a, int b, int c = 10);
-
多次声明函数也是合法的,但在给定的作用域中一个形参只能被赋予一次默认实参。
int func(int a, int b, int c = 10); // 正确 int func(int a, int b, int c = 20); // 错误,重复声明 int func(int a, int b = 10, int c); // 正确,int b 还未初始化
-
⭐局部变量不能作为默认实参。除此之外,只要表达式的类型能够转换成形参所需的类型,该表达式就能作为默认实参。
19.3 pass by value 值传递(拷贝)
- 基础类型:拷贝参数的值,在函数内部改变不影响函数外部。
- 指针:拷贝指针,可以在函数内部修改指针指向位置的内容。
- 结构体:修改成员变量本身不影响外部结构体,但通过结构体指针修改指向的内容会改变结构体的内容。
19.4 reference
- ⭐引用一定要先初始化。
- 引用比指针更安全。
19.5 pass by reference 引用传递
- 使用引用避免拷贝
- 使用引用形参返回额外信息
20 Return statement
20.1 return statement
return
语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
20.2 无返回值函数
无返回值的return语句只能用在返回类型是void
的函数中。返回void
的函数不要求非得有return
语句,在这类函数的最后一句后面会隐式的执行return
。
-
可用于无返回值函数提前退出。
void swap(int &v1, int &v2) { // 传递引用,交换两个整数的值 if (v1 == v2) // 如果两个整数值相等,直接退出 return; int temp = v1; v1 = v2; v2 = temp; // 无需显式return }
20.3 有返回值函数
-
pass by value
- 基础类型:返回常量/变量的拷贝
- 指针:返回地址的拷贝
- 结构体:返回整个结构体的拷贝
-
较多返回值
- 使用引用避免数据拷贝
- 使用常量参数避免输入参数被改变
- 使用非常量引用参数接收输出
21 Inline function
21.1 内联函数可以避免函数调用的开销
- 以空间换时间,在每个调用点上“内联地”展开。
inline
只是编译器发出的一个请求,编译器可以选择忽略这个请求。- 内联机制适用于优化规模较小、流程直接(没有循环等)、频繁调用的函数。
- ⭐有的编译器在没有加
inline
声明的情况下也会用内联机制优化函数。
21.2 内联函数 VS 宏
-
类型安全
- 宏是文本替换,没有类型安全。如果宏的参数类型不匹配,可能不会报错,导致难以发现的错误。
- 内联函数具有类型安全,编译器会在编译时检查参数类型。
-
调用开销
- 宏没有函数调用的开销,但可能因为展开产生多余的代码,导致效率降低。
- 内联函数在编译时展开,避免了函数调用的开销,但编译器会根据实际情况决定是否内联。
-
使用场景
- 宏适用于简单的文本替换,如常量定义、简单的算术运算等。
- 内联函数适用于需要类型安全、作用域和希望利用编译器优化的场景。
22 function-overloading 函数重载
22.1 为什么需要函数重载?
- 增强表达力:允许使用相同的函数名对不同的参数类型或数量进行操作,使得代码更加直观和易于理解。
- 提升接口一致性:可以对不同类型的数据使用相同的操作接口,避免创建大量功能相似但名称不同的函数。
- 支持多态性:允许在派生类中重写基类的函数,实现不同类对象调用同一函数名但行为不同。
- 简化函数调用:对于可变参数的函数,如构造函数,函数重载提供了一种方便的方式来定义和调用函数。
- 改善错误检测:编译时能够根据参数匹配来选择合适的重载版本,有助于及早发现类型不匹配的错误。
22.2 函数重载
-
在编译器中,函数名和参数列表均相同的函数即相同的函数。
-
同名不同参:函数名相同,但参数的类型、数量或顺序不同。
-
编译时决定:编译器根据函数调用时提供的参数类型和数量来决定调用哪个重载函数。
-
类型安全:提供了类型安全的方式,避免了类型不匹配的错误。
int sum(int x, int y){ /* ... */ } float sum(float x, float y){ /* ... */ } double sum(double x, double y){ /* ... */ }
23 Function template 函数模板
23.1 函数模板
C++中的函数模板(Function Template
)是一种泛型编程工具,它允许你定义一个函数,该函数可以操作任意类型的数据,而不必在编写时指定具体的数据类型。
-
函数模板通过类型参数化提供编译时的多态性,使得同一个函数能够处理多种不同的数据类型。
template <typename T> // 也可将 typename 换成 class T sum(T a, T b){ return a + b; } // 隐式实例化 // 显式实例化 template int sum<int>(int,int);
-
模板声明:使用
template <typename T>
开始声明,其中T
是一个占位符,表示任何类型。 -
类型参数:
T
可以是任何有效的C++类型,包括基本数据类型、类、结构体等。 -
实例化:当你使用特定类型调用模板函数时,编译器会自动创建(实例化)一个该类型的版本。
-
模板参数:可以有多个模板参数,例如
template <typename T, typename U>
。 -
重载:模板函数可以被重载,C++编译器会根据调用的上下文来选择正确的重载版本。
-
非类型参数:除了类型参数外,模板也可以接受非类型参数(如整数、枚举等)。
23.2 函数模板特化
-
为函数模板的特定类型或类型组合提供定制化实现。
-
全特化:为特定的类型参数提供完全定制的函数实现。
-
偏特化:为某些类型参数固定,而其他参数保持通用的情况提供定制。
#include <iostream> #include <vector> #include <cstring> using namespace std; // 声明一个结构体 struct Point { int x; int y; }; // 通用模板函数,实现加法 template <typename T> T sum(T a, T b) { cout << "调用通用模板" << endl; return a + b; } // 特化版本,针对 Point 类型 template <> // 加 <> 表示特例化,不加表示实例化 Point sum<Point>(Point a, Point b) { cout << "调用特化模板" << endl; Point p; p.x = a.x + b.x; p.y = a.y + b.y; return p; // 返回结构体 Point 类型 } // 偏特化版本,固定 string 类型 template <typename T> string sum(T a, string b) { cout << "调用偏特化模板" << endl; return to_string(a) + b; // 返回 string 类型 } int main() { int a = 10; int b = 10; int sum1 = sum(a, b); cout << sum1 << endl; Point p1{20, 20}; Point p2{30, 30}; Point sum2 = sum(p1, p2); cout << sum2.x << "," << sum2.y << endl; int c = 100; string d = "string"; string sum3 = sum(c, d); cout << sum3 << endl; return (0); } // 调用通用模板 // 20 // 调用特化模板 // 50,50 // 调用偏特化模板 // 100string
24 Function-pointers and references
24.1 函数指针
函数指针是指向函数的指针变量,它提供了一种在运行时动态调用函数的能力,在某些情况非常有用,比如实现回调函数、多态函数数组、以及在某些设计模式中。
-
函数指针的声明与使用
float normL1(float x, float y); // L1范数 float normL2(float x, float y); // L2范数 // 声明函数指针 float (*normL1_ptr)(float x, float y); float (*normL2_ptr)(float x, float y); // 给函数指针赋值,以下两种方式等价 normL1_ptr = normL1; normL2_ptr = &normL2; // 通过指针调用 float num_normL1 = normL1_ptr(1.1, 2.2); float num_normL2 = (*nornL2_ptr)(1.1, 2.2); // 声明+初始化 float (*normL1_ptr)(float x, float y) = normL1; float (*normL2_ptr)(float x, float y) = &normL2
24.2 回调函数
-
把函数作为参数传递给其他函数。
-
例如通过C/C++标准库的
qsort
函数进行排序,我们可以自定义比较规则函数,并将该函数指针传入qsort
实现自定义排序。 -
实现整数的个位数排序算法。
// qsort 原型 void qsort(void *ptr, size_t count, size_t size, int(*compare)(const void *, const coid *)); // 自定义比较规则,按个位数大小排序 int compare_int(const void *num1, const void *num2) { int *p1 = (int *)num1; int *p2 = (int *)num2; return (*p1 % 10) - (*p2 % 10); } int main() { int arr[]{7, 3, 6, 4, 5, 1, 2, 3, 4, 9, 5}; size_t size = sizeof(arr) / sizeof(int); cout << "数组的长度是:" << size << endl; // 11 qsort(arr, size, sizeof(int), compare_int); for (int i = 0; i < size; i++) { cout << arr[i] << " "; } cout << endl; return (0); }
24.3 函数引用
在C++中,函数引用是一种对函数的引用,它允许我们将函数作为参数传递给其他函数,这在实现回调机制或构建接受可调用对象的通用函数时非常有用。函数引用与函数指针类似,但它们提供了更简洁的语法和更好的类型安全。
-
⭐引用声明时,必须初始化。
-
函数引用的声明与使用
float normL1(float x, float y); // L1范数 float normL2(float x, float y); // L2范数 // 声明 + 初始化 float (&normL1_ref)(float x, float y) = normL1; float (&normL2_ref)(float x, float y) = normL2
25 Recursive function 递归
25.1 递归函数
递归函数是指在函数体内直接或间接调用自身的函数。
-
理解递归要点
- 递归函数需要有一个或多个基本情况,这是递归终止的条件。基本情况不进行递归调用,而是返回一个值或执行特定的操作。
- 递归调用必须确保问题规模逐渐减小,以避免无限递归。这通常通过修改参数或状态来实现。
- 每次递归调用都会在调用堆栈上添加一层,因此递归深度不能太大,否则可能会导致堆栈溢出。
- ⭐递归可能涉及重复计算,因此有时可以通过存储(memoization)或迭代方法来优化。
-
递归实现斐波拉契数
int fab(int num){ // 1 1 2 3 5 8 ... if (num == 0 || num == 1){ return(1); } if (num == 2){ return(2); } if (num > 2){ return fab(num - 1) + fab(num - 2); } }
25.2 优缺点
-
优点
- 对于自然以递归方式定义的问题,如树遍历、分治算法等。
- 分解问题为更小的子问题。
- 递归可以避免编写重复的循环结构,使得算法更加模块化。
- 可以自然地表达许多数学概念和算法,如阶乘、斐波那契数列等。
- 可以与记忆化技术结合使用,以优化那些具有重叠子问题和最优子结构特性的算法。
- 在某些编译器中,尾递归函数可以被优化以减少堆栈使用,从而避免堆栈溢出的风险。
-
缺点
- 消耗更多的堆栈存储,带来额外的性能开销。
- 在涉及大量重复计算时,递归没有迭代实现高效。
- 在存在多个递归调用栈或递归深度较大时,难以调试。
26 C/CPP with ARM
26.1 intel VS ARM
- 在C/CPP编译器的帮助下,C和CPP可以做到平台独立,但我们需要提前知道待运行平台的CPU的背景信息。
- RAM CPU是一种基于精简指令集计算(RISC)架构的处理器,ARM 处理器广泛应用于智能手机、平板电脑、嵌入式系统、网络设备等。
- ⭐ARM处理器以其高能效比而闻名,这使得它们非常适合于移动设备和需要长时间电池续航的应用。
26.2 Raspberry Pi 4 树莓派
- 搭载博通BCM2711 Soc,包含四个1.5GHz Cortex A72 CPU核心。
- 内存配置有1GB、2GB、4GB和8GB LPDDR4 SDRAM
- 支持双HDMI端口,可以连接两台4K显示器,VideoCore VI 显卡支持OpenGL ES 3.x,并且具备H.265 4Kp60硬件解码能力。
- 支持802.11 b/g/n/ac无线网络和蓝牙5.0,以及千兆以太网接口。
- 提供了两个USB 3.0端口和两个USB 2.0端口,以及一个USB-C端口用于供电。
- 提供了40个GPIO引脚,与以前的Raspberry Pi型号兼容,并且增加了额外的I2C、UART和SPI引脚选项。
- 可以运行多种操作系统,包括基于Debian 10 Buster的系统。
26.3 在ARM开发程序(待补充)
26 C/C++加速
26.1 优化技巧
-
选择合适的算法
-
编写清晰和简单的代码
-
考虑内存优化代码
-
不做大内存拷贝
-
尽量不在循环中写输出语句 printf()/cout
-
查表法(计算较慢如sin(),cos()等),对精度要求低、计算复杂
-
SIMD(需要指令集支持):Single instruction, multiple data
- intel: MMX, SSE, SSE2, AVX, AVX2, AVX512
- ARM: NEON
- RISC-V: RVV(RISC-V Vector Extension)
-
OpenMP:任务并行化
26.2 OpenMP
OpenMP(Open Multi-Processing)是一个用于C、C++和Fortran编程语言的API,用于在多处理器系统上实现多线程程序的并行化。OpenMP支持共享内存的并行编程,允许开发者利用多核处理器的计算能力
-
启用OpenMP
#include <omp>
-
并行区域:使用
#pragma omp parallel
指令来指定代码的并行区域。#pragma omp parallel { // 并行执行的代码 }
-
并行循环:OpenMP提供了多种用于循环的并行化指令,如
#pragma omp for
,它将循环的迭代分配给不同的线程执行。循环体内不能相互依赖。#pragma omp parallel for for (int i = 0; i < n; ++i) { // 循环体,被并行化 }
-
线程私有变量和共享变量:在并行区域中,每个线程都有自己的私有变量。如果需要在线程之间共享数据,可以使用
shared
关键字;如果每个线程需要有自己的变量副本,则可以使用private
关键字。#pragma omp parallel for private(variable) shared(anotherVaeriable) for (int i = 0; i < n; ++i) { // variable 是私有的,anotherVariable 是共享的 }
-
同步:通过
omp_set_num_threads()
函数来设置OmenMP程序中使用的线程数量。#include <omp.h> int main() { omp_set_num_threads(4); // 设置线程数量为4 #pragma omp parallel { // 并行执行的代码 } return 0; }
-
环境变量:OpenMP 的行为也可以通过环境变量来控制,如
OMP_NUM_THREADS
,它可以在不修改代码的情况下设置线程数量。export OMP_NUM_THREADS=4
-
原子操作:当多个线程需要更新同一变量时,可以使用
#pragma omp atomic
来确保更新的原子性。#pragma omp parallel for // 并行循环 for (int i = 0; i < n; ++i) { #pragma omp atomic // 保持原子性 sharedVariable++; }
27 避免内存复制-OpenCV
27.1 cv::Mat class
cv::Mat
类是表示图像和矩阵的基本数据结构。
-
cv::Mat class
class CV_EXPORTS Mat { public: // some functions int rows, cols; // 指向数据的指针 uchar *data; // size_t step 偏移量 MatStep step; };
-
cv::Mat class模型
- 其中
refcount
指示数据被引用多少次,用于内存释放
27.2 step in cv::Mat
-
表示矩阵每一行字节的偏移量。
-
step
反映了矩阵在内存中的存储方式。由于矩阵的每行可能不是按字节对齐的(特别是当矩阵的元素类型不是字节的整数倍时),step
用于计算从一行的末尾到下一行开头的字节偏移。 -
ROI: Region of interest 感兴趣区域
- 在使用
ROI
时,step
用于计算从ROI
的一行末尾到下一行的起始位置的偏移。 - 在
ROI
中通过指针更改数据会改变原始矩阵的数据。
- 在使用
28 class and object
28.1 三种访问限定符
-
public
成员可以在类内和类外被访问,可以被任何可以访问类的对象或引用访问。 -
private
成员只能在类的内部访问,不能被外部直接访问。用于封装类的内部状态和实现细节,防止外部直接修改。 -
protected
成员可以在类的内部访问,也可以被继承的子类访问,不能被类的外部访问。用于提供比private
更宽的访问范围。 -
访问限定符的规则
- 默认情况下,如果未指定访问限定符,则成员是私有的。
- 成员函数可以修改成员变量的访问级别,即使它们是私有的。
- 继承时,基类的
public
成员在派生类中仍然是公共的,基类protected
成员在派生类中也是受保护的,基类paivate
成员在派生类中不可见。
28.2 类的优缺点
-
优点
- 封装:类可以将数据和操作这些数据的函数组合在一起,隐藏内部实现细节。
继承:允许新类(子类)继承现有类(父类)的属性和方法,支持代码复用。
多态:允许使用统一的接口处理不同类型的对象。
构造函数和析构函数:提供对象创建和销毁时的初始化和清理机制。
成员函数:可以包含任意多的成员函数,实现复杂的逻辑。
-
缺点
- 代码结构复杂,难以理解
- 虚函数动态绑定可能导致运行时性能开销
- 类的设计需要预先规划,否在代码难以扩展或修改
28.3 成员函数
-
成员函数在类内声明,在类内或类外定义。
class Student { int id; char name[10]; public: void setID(int id) { // 类内定义 this->id = id; } void setName(const char *s) { strncpy(name, s, sizeof(name)); } void printInfo(); // 类内声明,类外定义 }; void Student::printInfo() { cout << name << "的ID是" << id << endl; }
28.4 CMake 管理程序
-
在项目的根目录创建 CMakeLists.txt,定义如何构建项目。
-
在根CMakeLists.txt文件中,使用
project()
命令设置项目名称和支持的语言。cmake_minimum_required(VERSION 3.10) project(MyProject VERSION 1.0 LANGUAGES CXX)
-
使用
add_executable()
命令来定义一个可执行目标,并指定源文件。add_executable(MyExecutable main.cpp)
-
根据需要设置编译器选项,例如启用C++11支持。
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True)
-
可以指定一个或多个源文件给目标。
target_sources(MyExecutable PRIVATE source1.cpp source2.cpp)
-
如果项目依赖外部库,可以使用
find_package()
来查找库,并用target_link_libraries()
链接库。find_package(OpenCV REQUIRED) target_link_libraries(MyExecutable PRIVATE ${OpenCV_LIBS})
-
使用
include_directories()
或target_include_directories()
来指定头文件搜索路径。include_directories(${OpenCV_INCLUDE_DIRS})
-
使用
add_definitions()
或target_compile_definitions()
来添加编译器定义。add_definitions(-DUSE_MY_FEATURE)
-
使用
install()
命令定义如何安装目标。install(TARGETS MyExecutable RUNTIME DESTINATION bin)
-
使用CMake的测试功能来添加测试。
enable_testing() add_test(NAME MyTest COMMAND MyExecutable)
-
在命令行中运行
cmake
命令来配置项目,并生成构建文件。cmake /path/to/source
-
使用生成的构建文件来构建项目。
cmake --build .
29 Constructors and destructors 构造函数和析构函数
29.1 constructors
构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。
-
如果没有为类定义任何构造函数,编译器将自动生成一个默认构造函数。
-
构造函数可以使用成员初始化列表来初始化成员变量,这是最高效的初始化方式。
-
可以定义多个构造函数,每个构造函数具有不同的参数列表,这称为构造函数重载。
-
C++11引入了移动构造函数,它接受一个右值引用作为参数,用于以低成本移动资源。
-
拷贝构造函数拷贝构造函数用于创建一个对象作为另一个对象的副本。
class MyClass { public: // 默认构造函数 MyClass() { std::cout << "Default constructor called" << std::endl; } // 参数化构造函数 MyClass(int value) : memberVar(value) { std::cout << "Parameterized constructor called" << std::endl; } // 拷贝构造函数 MyClass(const MyClass& other) { memberVar = other.memberVar; std::cout << "Copy constructor called" << std::endl; } // 移动构造函数 MyClass(MyClass&& other) noexcept : memberVar(other.memberVar) { other.memberVar = 0; // 移动后清空资源 std::cout << "Move constructor called" << std::endl; } private: int memberVar; }; int main() { MyClass obj1; // 调用默认构造函数 MyClass obj2(10); // 调用参数化构造函数 MyClass obj3 = obj1; // 调用拷贝构造函数 MyClass obj4 = std::move(obj2); // 调用移动构造函数 }
29.2 dectructors
析构函数是C++中类的另一个特殊成员函数,它在对象生命周期结束时被调用,用于执行清理工作。
-
析构函数用于对象销毁,其名称为
~类名
。 -
析构函数不接受任何参数,没有返回值。
-
析构函数用于释放对象在构造和使用过程中分配的资源,如内存、文件句柄、网络连接等。
-
当一个类是多态基类时,应该将析构函数声明为虚函数(
virtual
),以确保通过基类指针或引用删除派生类对象时,正确的析构函数被调用。class Resource { public: Resource() { std::cout << "Resource allocated." << std::endl; } // 虚析构函数 virtual ~Resource() { std::cout << "Resource deallocated." << std::endl; } // 其他成员函数... }; int main() { { Resource localResource; // Resource allocated 打印在此处 // 当localResource超出作用域时,析构函数自动调用 } // Resource deallocated 打印在此处 Resource* dynamicResource = new Resource(); // Resource allocated delete dynamicResource; // 手动调用析构函数并打印 Resource deallocated return 0; }
30 this 指针
this
指针是一个特殊的指针,它在每个非静态成员函数内部自动定义,指向调用该成员函数的对象本身的地址。
-
用途
- 用于区分类成员变量和局部变量,当它们的名称相同时。
- 传递对象的
this
指针给函数或方法。 - 在多态情况下,确保正确的成员函数被调用。
-
在常量成员函数中,
this
指针本身是const
类型的,意味着你不能通过this
指针修改对象的任何成员。 -
在静态成员函数中,
this
指针不存在,因为静态成员不属于类的特定对象。 -
在成员函数中,可以使用
return *this;
来返回对象本身,这在链式调用中非常有用。class MyClass { int value; public: MyClass(int v) : value(v) {} // 使用this指针区分成员变量和参数 void SetValue(int v) { value = v; // 赋值给成员变量 } // 返回对象本身的引用,用于链式调用 MyClass& GetSelf() { return *this; } // 常量成员函数中,this是const类型的 void PrintValue() const { std::cout << value << std::endl; } }; int main() { MyClass obj(10); obj.SetValue(20); // 使用this指针区分成员变量和参数 obj.PrintValue(); // 调用常量成员函数 // 链式调用示例 obj.GetSelf().SetValue(30).PrintValue(); return 0; }
31 const
and static
成员
31.1 const Variables
-
常量语句
#define PI 3.14 // 使用宏定义,3.14是字符串 // constant integer,一旦初始化就不能修改 const int pi = 100; // constant pointer,指针指向的地址可以改变,不能通过指针修改指向指向的值 const int *p_int; int const *p_int; // pointer to constant,指针指向的地址不能改变,指针指向的值可以修改(除非该值本身是常量) int *const p_int; // 常量指针和引用作为函数参数,不能在函数内部更改指针和引用指向的值 void func(const int *); void func(const int &);
31.2 const
成员变量
-
const
成员变量必须在构造函数的初始化列表中初始化,不能再构造函数体内进行赋值。class MyClass { public: MyClass(int val) : value(val) {} // 使用初始化列表value(val)初始化,不能在函数体内赋值 private: const int value; // const成员变量声明,可以在此初始化也可以在构造函数初始化列表中初始化 };
31.3 const
成员函数
-
const
成员函数不能修改任何成员变量;const
成员函数返回对象的引用或指针时,返回值自动成为const
类型。 -
声明为
const
的成员函数可以让编译器知道这个函数不会修改对象的成员变量,编译器可以利用这一点进行优化。 -
合理的添加
const
可以使函数既可以被const对象调用,也可以被非const对象调用。
class MyClass {
public:
MyClass(int val) : value(val) {}
int getValue() const { // const成员函数const在函数名后面,避免与函数返回类型混淆
return value;
}
private:
int value;
};
int main() {
const MyClass constObj(10); // const对象
int val = constObj.getValue(); // 正确:const成员函数可以被const对象调用
cout << val << endl; // 10
}
const
与类的关系- 将对象声明为
const
意味着整个对象的状态不能被修改,所有非const
成员函数都不能被调用来修改它。 const
对象只能调用类的const
成员函数。
- 将对象声明为
31.4 static
成员函数
静态(static)关键字用于定义属于类而不是类的个别对象的成员。
-
静态成员变量
- 静态成员变量由类的所有对象共享,不属于任何一个特定对象。
- 它们在类的作用域内,但在任何对象实例化之前就存在。
- 可以通过类名直接访问静态成员变量,也可以通过对象实例访问。
- 静态成员变量在程序的全局存储区分配空间,而不是在堆或栈上。
- 在类的定义之外进行初始化,通常在编译器的编译单元中。
class MyClass { public: static int staticVar; // 静态成员变量声明,类外定义 inline static int staticVar2 = 10; // C++17标准,类内定义 }; // 初始化静态成员变量 int MyClass::staticVar = 0; int main() { MyClass::staticVar = 10; // 通过类名访问和初始化 MyClass obj; MyClass::staticVar = 20; // 再次通过类名修改 }
31.5 static
成员函数
- 静态成员函数没有
this
指针,因此不能访问非静态成员变量或调用非静态成员函数。 - 静态成员函数可以访问其他静态成员(包括变量和函数),但不能访问非静态成员。
- 调用静态函数不依赖任何实例化的对象。
- 静态成员函数通常被声明为
const
,因为它们不应该修改任何静态或非静态成员的状态。 - 可以通过类名直接调用静态成员函数。
class MyClass {
public:
static void staticFunc() {
// 只能访问静态成员或执行操作,没有this指针
}
};
int main() {
MyClass::staticFunc(); // 通过类名调用静态成员函数
}
- 可以通过静态变量实现引用计数,避免多个对象共享共享内存所造成的二次释放的问题。
32 Operators overloading
32.1 运算符重载
- 大多数运算符都可以重载,除了一些特定的运算符,如
.
,::
,.*
,?:
,sizeof
等。 - 运算符可以作为类的成员函数或非成员函数重载。成员函数使用
operator
关键字定义,而非成员函数则需要在函数名前显式使用operator
。
32.2 operators overloading in string
std::atring s("hello,");
s += "world!";
s.operator += ("CPP");
32.3 自定义类MyTime
中实现operator+()
和operator+=()
重载
-
自定义
MyTime
类,在类中实现operator+
和operator+=
重载#include <iostream> #include <string> using namespace std; class MyTime { int hours; int minutes; public: MyTime() : hours(0), minutes(0) {} MyTime(int h, int m) : hours(h), minutes(m) {} // 实现 operator+ 重载,MyTime + MyTime MyTime operator+(const MyTime &mt) const { MyTime sum; sum.minutes = mt.minutes + this->minutes; sum.hours = mt.hours + this->hours; sum.hours += sum.minutes / 60; sum.minutes %= 60; return sum; } // 实现 operator+= 重载,MyTime += MyTime MyTime &operator+=(const MyTime &mt) { this->minutes += mt.minutes; this->hours += mt.hours; this->hours += this->minutes / 60; this->minutes %= 60; return *this; } std::string getTime() const { return std::to_string(this->hours) + " hours and " + std::to_string(this->minutes) + " mintues."; } }; int main() { MyTime mt1(9, 45); MyTime mt2(5, 36); cout << (mt1 + mt2).getTime() << endl; cout << "--------------------------" << endl; // mt1 += mt2; // operator mt1.operator+=(mt2); // function cout << mt1.getTime() << endl; }
32.4 可以重载的运算符
33 Friend functions
33.1 友元函数
友元函数(friend function)是一种可以访问类的私有(private)和保护(protected)成员的非成员函数。
- 友元函数可以访问类的所有成员,包括私有和保护成员,以及其他友元函数。
- 友元函数在类的内部通过
friend
关键字声明为友元函数,通常在类的外部定义。 - 友元函数不是类的成员,因此没有隐式的
this *
参数。 - 友元函数通常用于实现需要访问类内部数据,但又磕作为成员函数的情况,例如重在运算符函数或实现非成员函数。
- 友元关系不能自动推广到模板实例化,如果需要,必须为每个模板实例化明确声明友元。
friend
关键字还可以用于声明友元类,友元类的所有成员都可以访问类的私有和保护成员。
33.2 friend
重载运算符
重载运算符时,如果第一项操作数是自定义类,那么把运算符定义为普通的类成员函数即可;如果第一项操作数并非自定义类,我们则需要使用友元函数来翻转操作数的顺序。
-
friend
重载
operator+实现
int + MyTime`// 一、类内声明,类外定义 class MyTime { // ... friend MyTime operator+(int m, const MyTime &t); }; MyTime operator+(int m, const MyTime &t) // 友元函数不属于类,所以不使用对象调用 { return t + m; } // 二、类内定义 class MyTime { // ... friend MyTime operator+(int m, const MyTime &t) { return t + m; // 已经被实现 } };
-
使用
friend
关键字重载operator<<
friend std::ostream &operator<<(std::ostream &os, const MyTime &t) { std::string str = std::to_string(t.hours) + " hours and " + std::to_string(t.minutes) + " minutes。"; os << str; return os; }
-
使用
friend
关键字重载operator>>
friend std::istream &operator>>(std::istream &is, MyTime &t) { is >> t.hours >> t.minutes; if (t.minutes >= 60) { t.hours += t.minutes / 60; t.minutes %= 60; } return is; }
34 User-defined type conversion
33.1 将MyTime
对象转换为int
和float
-
explicit
关键字用于类构造函数或转换操作符,以防止隐式类型转换。operator int() const // 转为整数 { return this->hours * 60 + this->minutes; } explicit operator float() const // 转为浮点数 { return float(this->hours * 60 + this->minutes); // 强制类型转换 } MyTime t1(10, 20); int minutes = t1; // 隐式类型转换 float f = float(t1); // 显式类型转换,强制类型转换
33.2 将 int 类型转换为MyTime
对象
-
使用构造函数重载完成转换
MyTime(int m) : hours(0), minutes(m) { this->hours = m / 60; this->minutes = m % 60; } MyTime t2 = 80; // t2(1, 20)
-
重载
operator=
,将 int 类型转换为MyTime
对象MyTime &operator=(int m) { this->hours = m / 60; this->minutes = m % 60; return *this; }
35 Increment++
和decrement--
35.1 prefix increment & postfix increment
-
前置递增应该增加对象的值,并返回
*this
的引用,允许链式调用 -
它应该被定义为类的成员函数,并且不是
const
的,因为它修改了对象的状态。 -
实现前置
++
MyTime &operator++() { ++this->minutes; if (this->minutes >= 60){ ++this->hours; if (this->hours >= 24) { this-.hours = 0; } this->minutes -= 60; } return *this; }
-
后置递增运算符首先需要保存当前对象的状态,然后增加对象的值,最后返回递增前的对象的副本。
-
它应该被定义为一个重载的成员函数,接受一个整型参数(通常是一个未使用的参数,如
0
),以区分前置和后置版本。 -
实现后置
++
MyTime operator++(int) { MyTime old = *this; // 保存当前状态 operaror++(); // 调用前置递增运算符 return old; // 返回递归前的副本 }
35.2 prefix decrement & postfix decrement
-
实现前置
--
MyTime &operator--() { --this->minutes; if (this->minutes < 0) { --this->hours; this->minutes += 60; } return *this; }
-
实现后置
--
MyTime operator--(int) { MyTime old = *this; // 保存当前状态 operator--(); // 调用前置递减运算符 return old; // 返回递归前的副本 }
36 Default operations
36.1 default constructors
-
默认构造函数
- 函数名与类名相同
- 没有任何参数,不包含默认参数或可变参数
- 用于创建对象时的默认初始化
-
如果没有定义构造函数,编译器会自动生成一个默认构造函数。
MyTime::MyTime(){}
-
如果定义了构造函数,编译器将不会生成默认构造函数。
class MyTime { // ... public: MyTime(int n){ // ... } }; MyTime mt; // 错误,没有合适的构造函数
-
避免歧义
class MyTime { // ... MyTime(){} MyTime(int value = 0) { // ... } } MyTime mt; // 错误,构造函数冲突
36.2 implicitly-defined destructors
-
如果析构函数没有被定义,编译器将会自动创建析构函数。
MyTime::~MyTime(){}
-
默认构造函数不会释放已经分配的内存,需要手动释放。
36.3 default copy constructors
-
默认拷贝构造函数
- 如果没有定义,编译器自动生成一个默认拷贝构造函数。
- 拷贝所有的非静态数据成员。
-
拷贝构造函数通常只有一个参数,或者剩下的参数具有默认值。
MyTime t1(2, 49); MyTime t2(t1); // 拷贝构造函数 MyTime t3 = t1; // 拷贝构造函数
36.4 default copy assignment
-
赋值操作符:=, +=, -=, …
-
重载赋值运算符
MyTime &operator=(const MyTime &other) { if (this != &other) { this->hours = other.hours; this->minutes = other.minutes; } return *this; } MyTime t1(1, 59); MyTime t2 = t1; // 拷贝构造函数 t2 = t1; // 拷贝赋值
-
默认拷贝赋值运算符
- 如果没有自定义的拷贝构造函数,编译器将会生成。
- 拷贝所有的非静态数据成员。
37 Am example with Dynamic Memory
37.1 MyString class
-
功能描述
- 类定义
MyString
类用于表示字符串,内部使用字符数组来存储字符串数据。
私有成员变量
int buf_len
:表示分配给字符串的缓冲区长度。char * characters
:指向字符数组的指针,用于存储字符串的字符。构造函数:
MyString(int buf_len = 64, const char * data = NULL)
:是一个带有两个参数的构造函数,具有默认参数值。第一个参数buf_len
用于指定缓冲区的长度,第二个参数data
用于初始化字符串内容。- 构造函数中调用了
create
方法来实际分配内存并初始化字符串。析构函数:
~MyString()
:析构函数,用于释放characters
指针指向的动态分配的内存。
create
方法:
bool create(int buf_len, const char * data)
:这是一个私有成员函数,用于分配内存并根据提供的data
初始化字符串。如果
buf_len
不为零,则分配一个长度为buf_len
的字符数组,并使用data
(如果提供)来初始化它,使用strncpy
函数复制字符串,并使用空字符(\0
)填充剩余的缓冲区以确保字符串是正确终止的。友元函数:
friend std::ostream & operator<<(std::ostream & os, const MyString & ms)
:重载了输出流运算符<<
,使其能够输出MyString
对象的内容。这是一个友元函数,因此它可以访问MyString
类的私有成员。输出格式:
- 重载的
<<
运算符输出buf_len
和characters
指针的值,以及字符串的实际内容。使用示例:
- 该类可以像使用内置的
std::string
类似地使用,例如创建对象、打印其内容等。
#pragma once
:
- 这是一个非标准的、由某些编译器支持的预处理指令,用于包含头文件,保证头文件内容在编译过程中只被包含一次。
包含的头文件:
#include <iostream>
:包含标准输入输出流库。
#include <cstring>
:包含C风格的字符串操作函数库。
-
类实现
#include <iostream> #include <cstring> using namespace std; class MyString { int buf_len; char *characters; public: // 构造函数,初始化 MyString(int buf_len = 0, const char *data = NULL) { this->buf_len = 0; this->characters = NULL; create(buf_len, data); } // 析构函数 ~MyString() { delete[] this->characters; // 释放堆上的内存 } // 申请内存 bool create(int buf_len, const char *data) { if (buf_len > 0 && data) // 如果buf_len大于0或data不为空 { this->buf_len = buf_len; this->characters = new char[this->buf_len]; strncpy(this->characters, data, this->buf_len); return true; // 分配成功返回true } return false; } // 使用friend重载operator<<输出MyString friend std::ostream &operator<<(std::ostream &os, const MyString &ms) { os << "buf_len = " << ms.buf_len; os << ",characters = " << static_cast<void *>(ms.characters); os << " [" << ms.characters << "]"; return os; } }; int main() { MyString s1(20, "welcome to beijing!"); MyString s2 = s1; MyString s3; s3 = s1; cout << s2 << endl; }
-
程序报错
buf_len = 20,characters = 0x558bcb097eb0 [welcome to beijing!]
free(): double free detected in tcache 2
Aborted
37.2 错误分析
-
内存被错误的释放了两次,为什么?
在执行
MyString s2 = s1;
和MyString s3;s3 = s1;
时,需要进行深度拷贝,即创建charatcers
的副本,而不是仅仅复制指针。
37.3 解决方法1-Hard copy
-
拷贝构造函数
-
运算符
operator=
重载,#include <iostream> #include <cstring> using namespace std; class MyString { int buf_len; char *characters; public: // 构造函数,初始化 MyString(int buf_len = 0, const char *data = NULL) { this->buf_len = 0; this->characters = NULL; create(buf_len, data); } // 拷贝构造函数 MyString(const MyString &ms) { this->buf_len = 0; this->characters = NULL; create(ms.buf_len, ms.characters); } // 析构函数 ~MyString() { release(); } // 申请内存 bool create(int buf_len, const char *data) { release(); // 释放内存 if (buf_len > 0) { this->buf_len = buf_len; this->characters = new char[this->buf_len]; if (data) { strncpy(this->characters, data, this->buf_len); } } return true; } // 重载 operater= MyString &operator=(const MyString &ms) { create(ms.buf_len, ms.characters); return *this; } // 释放内存 bool release() { this->buf_len = 0; if (this->characters) { delete[] this->characters; this->characters = NULL; } return true; } // 使用 friend 重载 operator<< 输出 MyString friend std::ostream &operator<<(std::ostream &os, const MyString &ms) { os << "buf_len = " << ms.buf_len; os << ",characters = " << static_cast<void *>(ms.characters); os << " [" << ms.characters << "]"; return os; } }; int main() { MyString s1(20, "welcome to beijing!"); // 自定义构造函数 MyString s2 = s1; // 拷贝构造函数 MyString s3; s3 = s1; // operator= cout << "s1 : " << s1 << endl; cout << "s2 : " << s2 << endl; cout << "s3 : " << s2 << endl; }
-
程序输出
s1 : buf_len = 20,characters = 0x56209061aeb0 [welcome to beijing!]
s2 : buf_len = 20,characters = 0x56209061aed0 [welcome to beijing!]
s3 : buf_len = 20,characters = 0x56209061aed0 [welcome to beijing!] -
深度拷贝存在的问题
- 深拷贝可能涉及大量内存的复制
- 当对象管理大量数据时,会增加内存使用量
- 如果对象包含指向自身的引用或循环引用,深拷贝可能会导致无限递归或复制错误
37.4 解决方法2-Soft copy
-
引用计数,静态变量实现
#include <iostream> #include <cstring> #include<unordered_map> using namespace std; class MyString { int buf_len; char *characters; static unordered_map<char *, int> ref_count; // 引用计数映射 public: // 构造函数,初始化 MyString(int buf_len = 0, const char *data = NULL) { this->buf_len = 0; this->characters = NULL; create(buf_len, data); } // 拷贝构造函数 MyString(const MyString &ms) : buf_len(ms.buf_len), characters(ms.characters) { ref_count[characters]++; // 增加引用计数 } // 析构函数 ~MyString() { release(); // 减少引用计数并可能释放内存 } // 申请内存 bool create(int buf_len, const char *data) { release(); // 释放已有内存 if (buf_len > 0 && data) { this->buf_len = buf_len; this->characters = new char[buf_len]; strncpy(this->characters, data, buf_len); ref_count[characters] = 1; // 新分配的内存初始化引用计数为1 return true; // 分配成功返回true } return false; } // 重载赋值运算符 MyString &operator=(const MyString &ms) { if (this != &ms) { release(); // 减少当前对象的引用计数并可能释放内存 buf_len = ms.buf_len; characters = ms.characters; ref_count[characters]++; // 增加引用计数 } return *this; } // 释放内存 void release() { if (characters && --ref_count[characters] == 0) { delete[] characters; ref_count.erase(characters); // 从映射中移除引用计数 characters = NULL; buf_len = 0; } } // 使用 friend 重载 operator<< 输出 MyString friend ostream &operator<<(ostream &os, const MyString &ms) { os << "buf_len = " << ms.buf_len; os << ", characters = " << static_cast<void *>(ms.characters); if (ms.characters) { os << " [" << string(ms.characters, ms.buf_len) << "]"; } else { os << " [NULL]"; } return os; } }; // 初始化静态成员 unordered_map<char *, int> MyString::ref_count; int main() { MyString s1(20, "welcome to beijing!"); MyString s2 = s1; // 拷贝构造函数,共享内存 MyString s3; s3 = s1; // 赋值运算符,共享内存 cout << s2 << endl; cout << s3 << endl; return 0; }
-
为什么使用
unordered_map<char*, int>
?- 对于每个动态分配的内存块,可能存在多个对象(实例)共享这块内存。使用
unordered_map<char*, int>
运行我们为每个内存块维护一个独立的引用计数。 - 使用动态分配内存的指针
char*
作为unordered_map
的键,提供了平均常数时间复杂度的查找、插入和删除操作,使得引用计数的更新更具高效。 - 如果内存分配允许许多个不同的内存分配(不同的 buf_len),unordered_map 可以轻松的为每个分配维护引用计数。
- 如果使用单个
int
类型的引用计数,将无法区分不同的内存分配,可能导致多个对象引用同一个计数,从而引发内存管理错误。 - 使用
unordered_map
可以在对象销毁时快速定位到对应的内存块,并安全地减少引用计数,只在计数为零时释放内存。 - 支持多线程,易于扩展。
- 对于每个动态分配的内存块,可能存在多个对象(实例)共享这块内存。使用
38 Smart Pointers 智能指针
38.1 智能指针
智能指针是C++中一种模板类,用于自动管理动态分配的内存,确保及时释放,从而避免内存泄漏。
-
⭐基本概念
- 自动内存管理:智能指针负责自动释放它们指向的内存,当智能指针超出作用域或被显式销毁时,它们会自动调用
delete
或delete[]
。 - 异常安全:智能指针通常保证在发生异常时也能正确地释放资源。
- 所有权语义:智能指针拥有它们指向的对象,并确保当最后一个指向特定对象的智能指针被销毁时,该对象也会被销毁。
- 复制和赋值:不同类型的智能指针有不同的复制和赋值行为。例如,
std::unique_ptr
不支持复制操作,但支持移动操作,以确保唯一所有权;而std::shared_ptr
支持引用计数的复制操作。 - 转换:智能指针可以转换为普通的裸指针,以便与那些需要裸指针的API一起使用。
- 模板类:智能指针是模板类,可以用于管理任何类型的动态分配对象。
- RAII(Resource Acquisition Is Initialization):智能指针作为RAII工具,确保资源的获取和释放与对象的生命周期绑定。
- 自动内存管理:智能指针负责自动释放它们指向的内存,当智能指针超出作用域或被显式销毁时,它们会自动调用
-
C++标准库中包含以下几种智能指针:
std::unique_ptr
:提供独占所有权模型,同一时间只能有一个std::unique_ptr
实例拥有对象。std::shared_ptr
:通过引用计数机制管理多个指针实例共享同一对象的所有权。std::weak_ptr
:与std::shared_ptr
配合使用,用于解决引用计数导致的循环引用问题。std::auto_ptr
(已在C++17中弃用):提供了转移语义的指针,不支持复制操作。
38.2 std::shared_ptr
shared_ptr
类型是 C++ 标准库中的一种智能指针,专为多个所有者需要管理对象生命周期的方案而设计。
-
初始化一个
shared_ptr
之后,可以复制它,按值将其传入函数参数,然后将其分配给其他shared_ptr
实例。 -
所有实例均指向同一个对象,并共享对一个“控制块”(每当新的
shared_ptr
添加、超出范围或重置时增加和减少引用计数)的访问权限。 -
当引用计数达到零时,控制块将删除内存资源和自身。
-
推荐使用
std::make_shared
创建std::shared_ptr
对象,只有在需要自定义删除器或std::make_shared
不适用的情况下才使用new
分配内存。std::shared_ptr<MyTime> mt1 = std::make_shared<MyTime>(1, 30); std::shared_ptr<MyTime> mt2 = mt1; std::shared_ptr<MyTime> mt3(mt1); std::shared_ptr<MyTime> mt4(new MyTime(1, 30));
38.3 std::unique_ptr
std::unique_ptr
是 C++11 标准引入的一种智能指针,用于管理动态分配的对象。与 std::shared_ptr
不同,std::unique_ptr
拥有其指向对象的唯一所有权,这意味着在任何时候,要么有一个 std::unique_ptr
拥有对象,要么完全没有。
-
unique
不共享它的指针,它无法复制到其他unique_ptr
,无法通过值传递到函数,也无法用于需要副本的任何 C++ 标准库算法。 只能移动unique_ptr
。 -
推荐使用
std::make_unique
创建std::unique_ptr
对象,只有在需要自定义删除器或std::make_unique
不适用的情况下才使用new
分配内存。std::make_unique
在 C++17 标准及以上才可使用。std::unique_ptr<MyTime> mt1(new MyTime(1, 30)); std::unique_ptr<MyTime> mt2 = std::make_unique<MyTime>(1, 30); // C++17
-
使用
std::move
移动std::unique_ptr
指针std::unique_ptr<MyTime> mt3 = std::move(mt1); // 此后不能继续访问mt1
38.4 std::weak_ptr
有时,对象必须存储一种方法来访问 shared_ptr
的基础对象,而不会导致引用计数递增。 通常,在 shared_ptr
实例之间有循环引用时,会出现这种情况。weak_ptr
用于解决std::shared_ptr循环引用问题。
- 如果循环引用不可避免,甚至出于某种原因甚至更可取,可以使用
weak_ptr
为一个或多个所有者提供对另一个shared_ptr
所有者的弱引用。 - 通过使用
weak_ptr
,可以创建一个联接到现有相关实例集的shared_ptr
,但前提是基础内存资源仍然有效。 weak_ptr
本身不参与引用计数,因此,它无法阻止引用计数变为零。 但可以使用weak_ptr
尝试获取初始化该副本的shared_ptr
的新副本。- 若已删除内存,则
weak_ptr
的 bool 运算符返回false
。 - 若内存仍然有效,则新的共享指针会递增引用计数,并保证只要
shared_ptr
变量保留在作用域内,内存就会有效。
38.5 理解智能指针
-
RAII思想是一种利用对象生命周期来控制程序资源(例如内存、文件句柄、网络连接、互斥量等)的简单技术。
-
智能指针是用于自动管理动态分配内存的模板类。
template<class T> class shared_ptr; template<class T, class Deleter = std::default_deleter<T>> unique_ptr;
-
智能指针析构函数包括要删除的调用,并且由于在堆栈上声明了智能指针,当智能指针超出范围时将调用其析构函数。
-
⭐始终在单独的代码行上创建智能指针,而绝不在参数列表中创建智能指针,这样就不会由于某些参数列表分配规则而发生轻微泄露资源的情况。
39 Derived class 派生类
39.1 inheritance
-
继承来自于一个类或多个类(基类或派生类)的成员(属性和方法)。
-
C++支持多个继承和多层继承
class Base // 基类,父类 { public: int a; int b; }; class Derived: public Base // 公有继承 { public: int c; };
-
派生类有动态内存申请
- 重新定义拷贝构造函数和赋值运算符。
39.2 构造函数
-
如何创建一个派生类对象
- 申请内存
- 调用派生类的构造函数
- 调用基类构造函数
- 初始化参数列表
- 执行派生类的构造函数
39.3 析构函数
- 子类的析构函数先执行,然后再执行父类的析构函数。
39.4 Base and Derived class
-
base_derived.cpp
#include <iostream> #include <cstdio> using namespace std; class Base { public: int a; int b; public: // 默认构造函数 Base() : a(0), b(0) { cout << "构造函数:Base()!" << endl; } // 构造函数 Base(int a, int b) : a(a), b(b) { cout << "构造函数:Base(int a, int b)!" << endl; } // 析构函数 ~Base() { cout << "析构函数:~Base()!" << endl; } // 计算点积 int product() { return this->a * this->b; } // 第一项非自定义类,使用友元函数重载运算符 friend std::ostream &operator<<(std::ostream &os, const Base &bb) { os << "Base:a = " << bb.a << ", Base:b = " << bb.b << "."; return os; } }; class Derived : public Base { public: int c; public: // 派生类默认构造函数 Derived() : Base(), c(0) { cout << "构造函数:Derived()!" << endl; } // 派生类构造函数 Derived(int c) : Base(c - 1, c - 2), c(c) { cout << "构造函数:Derived(int c)!" << endl; } // 派生类析构函数 ~Derived() { cout << "析构函数:~Derived()!" << endl; } // 计算点积 int product() { return Base::product() * this->c; } // 第一项非自定义类,使用友元函数重载运算符 friend std::ostream &operator<<(std::ostream &os, const Derived &d) { os << static_cast<const Base &>(d) << endl; os << "Derived:c = " << d.c; return os; } };
-
main.cpp
#include <iostream> #include <cstdio> #include "base_derived.cpp" using namespace std; int main() { Derived d(99); // 基类构造函数->子类构造函数 cout << d << endl; // 先调用父类的运算符重载,再调用子类的运算符重载 cout << d.product() << endl; // 子类析构函数->基类析构函数 }
-
输出
/* 构造函数:Base(int a, int b)! 构造函数:Derived(int c)! Base:a = 98, Base:b = 97. Derived:c = 99 941094 析构函数:~Derived()! 析构函数:~Base()! */
40 Access control 访问控制
40.1 member access
- public members
- accissible anywhere
- private members
- 类的友元和成员可以访问
- protected member
- 基类和派生类的成员和友元可以访问
40.2 public
继承
- 基类的
public
成员- 在派生类中仍然是
public
成员
- 在派生类中仍然是
- 基类的
protected
成员- 在派生类中仍然是
protected
成员,只能通过派生类访问
- 在派生类中仍然是
- 基类的
private
成员- 在派生类中不能访问
40.3 protected
继承
- 基类的
public
成员和protected
成员- 在派生类中变成
protected
成员,只能通过派生类访问
- 在派生类中变成
- 基类的
private
成员- 在派生类中不能访问
40.4 private
继承
- 基类的
public
成员和protected
成员- 在派生类中变成
private
成员,只能通过派生类访问
- 在派生类中变成
41 Virtual functions
41.1 虚函数
虚函数是在基类中使用关键字 virtual
声明的成员函数,允许在派生类中被重写(Override)。
-
虚函数的主要作用是实现多态性(Polymorphism),即允许派生类重写基类中的方法,使得同一个函数调用根据对象的实际类型执行不同的代码。
-
为什么需要虚函数?
- 多态性:虚函数是实现多态性的关键技术,它允许通过基类接口调用派生类的方法,增加了代码的灵活性和可扩展性。
- 代码复用:通过在基类中实现通用功能,并在派生类中重写特定功能,可以减少代码重复,提高代码复用率。
- 接口定义:虚函数可以用来定义接口,强制派生类实现特定的方法,从而保证对象的行为符合预期。
- 动态绑定:虚函数支持动态绑定(也称为晚期绑定),即在运行时根据对象的实际类型确定调用哪个函数,这使得程序设计更加灵活。
-
如果一个类包含至少一个纯虚函数(使用
virtual
关键字和= 0
声明),它就成为了一个抽象类。抽象类不能被实例化,但可以作为其他类的基类。 -
虚函数动态绑定
-
static
绑定:由编译器决定调用哪个函数 -
dynamic
绑定:运行时调用函数,虚函数
-
-
一个例子
#include <iostream> #include <cstdio> using namespace std; class Person { public: string name; Person(const string &name) : name(name) { cout << "构造函数Person(const string &name)被调用!" << endl; } // 虚函数 virtual void print() { cout << "name : " << name << endl; } }; class Worker : public Person { public: string gender; Worker(string name, string gender) : Person(name), gender(gender) { cout << "构造函数Worker(string name, string gender)被调用!" << endl; } void print() { cout << "name : " << name << ", gender : " << gender << "." << endl; } }; void printInfo(Person &p) { p.print(); } int main() { Person p("lbl"); printInfo(p); Worker w("lbl", "male"); printInfo(w); }
-
输出
构造函数Person(const string &name)被调用!
name : lbl
构造函数Person(const string &name)被调用!
构造函数Worker(string name, string gender)被调用!
name : lbl, gender : male.
41.2 纯虚函数
纯虚函数(Pure Virtual Function)是C++中用于在基类中声明一个接口,但不提供具体的实现。纯虚函数使得基类成为一个抽象类(Abstract Class),这意味着不能实例化这样的类,但可以定义指向该类的指针或引用。
- 声明方式:在基类中,使用
virtual
关键字后跟=0
来声明一个纯虚函数。 - 带纯虚函数类实例化:如果一个类包含至少一个纯虚函数,那这个类就是抽象类。抽象类不能被直接实例化。任何派生类都必须实现该函数。
- 支持多态性:过基类的指针或引用,可以在运行时调用派生类中重写的纯虚函数。
41.3 虚析构函数
如果类有一个虚析构函数,它的地址会被包含在虚函数表中,以确保通过基类指针删除派生类对象时,能够调用到正确的析构函数。
-
当一个类包含至少一个虚函数时,它就允许多态的使用,即基类的指针或引用可以指向派生类的对象。
-
如果基类的析构函数不是虚的,那么当通过基类的指针删除派生类的对象时,基类的析构函数会被调用,但派生类的析构函数不会被调用。这将导致派生类特有的资源没有被正确释放,可能会引起内存泄漏或其他资源管理问题。
-
包含虚函数的类
class Base { public: virtual ~Base() { // 基类的析构代码 cout << "Base class destructor" << endl; } }; class Derived : public Base { public: ~Derived() { // 派生类的析构代码 cout << "Derived class destructor" << endl; } }; Base *ptr = new Derived(); delete ptr; // 子类和派生类的析构函数都会被调用
-
在这个例子中,
Base
类有一个虚析构函数,Derived
类是从Base
类派生的,并有自己的析构函数。创建Derived
类的对象并使用Base类的指针指向它。销毁basePtr
指向的对象时,首先会调用Derived
类的析构函数来清理派生类特有的资源,然后调用Base
类的虚析构函数来清理基类的资源。这个过程确保了对象被完全和正确地销毁,即使在多态的情况下也是如此。
42 Class template
42.1 类模板定义
类模板是为了解决类型参数化和泛型编程问题而出现的,是用来生成类的蓝图的。不同于函数模板,编译器不能为类模板推断模板参数类型。
-
类模板定义
template <typename T> class MyTemplate { // ... };
42.2 类模板实例化
-
隐式实例化是指在代码中使用模板类或函数时,编译器根据上下文自动实例化模板。程序员不需要显式指定模板参数。
-
显式实例化是由程序员直接告诉编译器实例化一个模板的特定版本。这通常在模板定义的实现文件中或在需要确保模板只被实例化一次的情况下使用。
-
类模板实例化
// 显式实例化 template class MyTemplate<int>; MyTemplate mt; // 隐式实例化 MyTemplate<int> mt;
-
一个类模板的每个实例都形成一个独立的类。类型
ClassName<int>
与其他任何ClassName<type>
类型都没有关联,也不会对任何其他ClassName<type>
类型的成员欧特殊访问权限。
43 类模板的非类型参数
43.1 非类型参数
-
声明一个模板
template <parameter-list> declaration
-
参数类型
- 类型模板参数
- 模板模板参数
- 非类型模板参数——整型、浮点型、指针、引用等
-
实例化
vector<int> vec1; vector<int, 16> vec2;
43.2 静态矩阵模板类
-
在编译时就需要知道矩阵的大小。
-
矩阵类实现
#include <iostream> #include <cstdio> using namespace std; template <typename T, size_t rows, size_t cols> class Mat { T data[rows][cols]; public: Mat() {} T getElement(size_t r, size_t c); bool setElement(size_t r, size_t c, T value); void print(); }; // 定义 T getElement(size_t r, size_t c); template <typename T, size_t rows, size_t cols> T Mat<T, rows, cols>::getElement(size_t r, size_t c) { if (r >= rows || c >= cols) { cerr << "超出范围" << endl; return 0; } return data[r][c]; } // 定义 bool setElement(size_t r, size_t c, T value); template <typename T, size_t rows, size_t cols> bool Mat<T, rows, cols>::setElement(size_t r, size_t c, T value) { if (r >= rows || c >= cols) { cerr << "超出范围" << endl; return false; } data[r][c] = value; return true; } // 定义 void print(); template <typename T, size_t rows, size_t cols> void Mat<T, rows, cols>::print() { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) cout << getElement(i, j) << " "; cout << endl; } } typedef Mat<int, 2, 2> Mati22; // 别名 int main() { Mati22 mat; mat.setElement(1, 1, 1000); mat.print(); cout << endl; Mati22 mat2(mat); // 默认拷贝构造函数,静态数组拷贝每个元素 mat2.print(); }
-
输出
1988939608 32534
1987848170 1000
1988939608 32534
1987848170 1000
类模板特化
为特定的类型或类型组合提供模板的特定实现。
-
全特化(Full Specialization)
- 当你为一个特定的类型提供完整的类模板实现时,这称为全特化。在全特化中,你定义了所有成员函数和静态成员的具体行为。
-
偏特化(Partial Specialization):
- 偏特化允许你为模板类的部分参数提供定制的实现。在这种情况下,你可能会定义一些新的成员,或者为某些模板参数提供特定的实现,而其他参数保持通用。
-
类模板特化
// 原始模板类 template <typename T> class MyTemplate { public: void function() { // 通用实现 } }; // 类模板的全特化 template <> class MyTemplate<int> { public: void function() { // 针对int类型的特定实现 } }; // 使用特化 MyTemplate<int> specializedInt; specializedInt.function(); // 调用特化版本的function
44 标准输出流和标准错误流
44.1 输出流和错误流
-
C
fprintf(stdout, "Info: ... \n", ...); printf("Info : ... \n", ...); fprintf(stderr, "Error: ... \n", ...); // 第一个参数可以是一个文件指针,把错误信息存储在电脑某个位置
-
CPP
std::cout << "Info: ..." << std::endl; std::cerr << "Error: ..." << std::endl;
44.2 Redirection 重定向
- 我们可以重定向输出到一个管道里用于调试,特别是在程序运行时间特别长的时候。可以让程序不显示
std::cerr
的内容。
-
运行
./program
并将程序的标准输出(stdout)通过管道|
传递给less
程序。less
是一个分页程序,允许你逐步查看输出内容。./program | less
-
运行
./program
并将程序的标准输出重定向到文件output.log
。如果output.log
已存在,其内容将被覆盖。以下两个写法等价./program > output.log ./pogram 1> output.log
-
运行
./program
并将程序的标准输出追加到文件output.log
的末尾,而不是覆盖它。./program >> output.log
-
运行
./program
并将程序的标准输出重定向到/dev/null
,这是一个特殊的文件,所有写入它的数据都会被丢弃。./program > /dev/null
-
运行
./program
并将程序的标准错误(stderr)重定向到文件error.log
。./program 2> error.log
-
运行
./program
,将程序的标准输出重定向到output.log
,并将标准错误重定向到error.log
。./program >output.log 2> error.log
-
运行
./program
,并将程序的标准输出和标准错误合并后重定向到all.log
。./program &> all.log
-
运行
./program
,首先尝试将标准输出重定向到文件all
,然后将标准错误重定向到标准输出(这里即文件all
)。注意:这个命令的顺序很重要,2>&1
必须在> all
之后,否则&1
将不会引用到文件all
。./program > all 2>&1
45 Assert and exceptions
45.1 assert 断言
-
assert
是一个像函数一样的宏,C语言定义在assert.h
中,C++定义在cassert
中。#ifdef NDEBUG # define assert(condition) ((void)0) #else # define assert(condition) // 具体实现的定义 #endif
-
如果条件为真,则什么都不做;如果条件为假,则输出诊断信息,然后调用
abort()
函数终止程序。 -
如果程序要发布了,可以定义一个
NDEBUG
宏,这些调试信息就不再输出了。 -
使用宏定义
#include <iostream> #define NDEBUG #include <cassert> int main(int argc, char **argv) { assert(argc == 10); // 如果没有#define NDEBUG,会在不满足条件时直接中断程序 std::cout << "output" << std::endl; } // 此时不管参数列表是什么都不考虑 assert
-
使用cmake
add_definitions(-DNDEBUG)
45.2 exceptions 异常
-
方法一:在出现未定义行为时直接使用
std::abort()
杀死程序。float ratio(float a, float b) { if (fabs(a + b) << FLT_EPSILON) { std::cerr << "除0异常!" << std::endl; std::abort(); // 直接杀死程序 } return (a - b) / (a + b); }
-
方法二:使用
bool
接口,失败返回 false,成功返回 true。bool ratio(float a, float b, float &result) // 使用引用接收结果返回值 { if (fabs(a + b) << FLT_EPSILON) { std::cerr << "除0异常!" << std::endl; return false; } result = (a - b) / (a + b); return true; }
-
方法三:使用C++特性,
throw
异常处理#include <iostream> #include <cfloat> #include <cmath> float ratio(float a, float b) { if (fabs(a + b) < FLT_EPSILON) throw "除0异常!"; // 抛出异常 return (a - b) / (a + b); } int main() { float x = 4, y = -4, z; try { // 运行 z = ratio(x, y); std::cout << "x = " << x << ", y = " << y << ", z = " << z << std::endl; } // 捕获异常 catch (const char *msg) { std::cerr << "错误:" << msg << std::endl; } } // 输出 错误:除0异常!
46 More exception handling
46.1 解决异常
-
一个
try
代码块可以捕获多个异常类型。float ratio(float a, float b) { if (a < 0) throw 1; if (b < 0) throw 2; if (fabs(a + b) < FLT_EPSILON) throw "除0异常!"; return (a - b) / (a + b); } try { z = ratio(x, y); } catch(const char *msg) { // 省略 } catch(int eid) { // 省略 }
-
如果一个异常没有被解决,那么会抛给调用者。如果调用者没有解决,就会继续抛给调用者的调用者,知道
main()
函数。 -
如果有异常没有给处理,可以在
main()
函数中匹配所有异常,防止程序被终止。...
表示所有未捕获的异常。catch(...) { // 捕获所有异常的处理 }
46.2 异常和继承
-
如果一个对象被抛出,这个对象的类派生自其他类,其基类异常的处理器会捕捉这个异常,其子类的异常处理器永远不会捕获子类的异常。
try { throw Derived(); // 抛出派生类异常 } catch (const Base &base) { std::cerr << "caught Base." << std::endl; } catch (const Derived &derived) // 不论是派生类异常还是基类异常,该异常处理器均不会捕获到任何异常 { std::cerr << "caught Derived." << std::endl; }
46.3 std::excpetion
-
std::exception
是一个类,它可以是任何异常的基类。namespace std { class logic_error; // 表示逻辑错误,即程序的逻辑本身存在问题,如违反了程序的前提条件。 class domain_error; // 表示某个操作的参数不在有效的域内,例如,对数函数的参数小于等于0。 class invalid_argument; // 表示某个操作的参数无效或不正确,如传递给函数的参数不符合期望。 class length_error; // 表示容器无法分配请求的长度,即请求的容器大小超出了其最大允许大小。 class out_of_range; // 表示访问了容器中不存在的元素,如使用无效的索引访问向量。 class runtime_error; // 用于表示运行时出现的错误,不特定于逻辑错误。当发生无法恢复的错误时抛出。 class range_error; // 表示某个操作的结果是无效的范围内,如尝试将非常大的值存入有限范围的变量。 class overflow_error; // 表示算术溢出,即计算结果超出了表示该结果的数据类型的最大值。 class underflow_error; // 表示算术下溢,即计算结果小于表示该结果的数据类型的最小值。 }
-
这些异常至少包含两个成员函数:
- what(): 返回一个描述错误的字符串。所有标准异常类都重写了这个函数,以提供更具体的错误信息。
- ~excpetion(): 异常类的虚析构函数,确保在抛出异常时能够正确地调用派生类的析构函数。
-
out_if_range
异常#include <stdexcept> #include <vector> #include <iostream> int main() { std::vector<int> v = {1, 2, 3}; try { // 尝试访问不存在的元素 std::cout << v.at(10) << std::endl; } catch (const std::out_of_range& e) { std::cerr << "Caught an out_of_range exception: " << e.what() << std::endl; } return 0; }
46.4 noexcept
-
noexcept
限定符定义一个函数,可以使其不抛出任何东西void func() noexcept; // 函数不抛异常
46.5 nothrow new
std::nothrow
是 C++ 标准库中的一个常量,其类型为 std::nothrow_t
。它作为 new
运算符的一个参数,指示在内存分配失败时不抛出异常,而是返回一个空指针(nullptr
)。这与默认的 new
行为不同,后者在分配失败时会抛出 std::bad_alloc
异常。
-
使用
try-catch
语句捕获并处理std::bad_alloc
异常。size_t length = 800000000L; int *p = NULL; try { p = new int[length]; // 分配内存失败,抛出 std::bad_alloc 异常 } catch (std::bad_alloc & ba) { std::cerr << ba.what() << std::endl; }
-
使用
std::nothrow
的一个场景是,当程序需要处理可能的内存分配失败,但又希望避免异常带来的开销或复杂性。size_t length = 800000000L; int *p = new(std::nothrow) int[length]; // 不抛异常 if (p == NULL) { // 处理语句 }
47 Friend class
47.1 friend
类
友元类(Friend Class)是一种特殊的类,它可以访问另一个类的私有(private)和保护(protected)成员。
-
⭐注意事项
- 友元类可以访问类的私有和保护成员,但这种访问不继承给友元类的对象。
- 友元类可以
public / protected / private
。 - 如果类A声明类B为友元类,这不会隐式地使类B成为类A的友元。如果需要,必须在两个类中相互声明。
- 友元关系可以应用于模板类,但需要在模板类的定义中明确指定友元类。
- 友元关系不扩展到成员函数。即使类B的成员函数需要访问类A的私有成员,它也必须在类A中单独声明为友元。
-
Sniper类的友元类Supplier类,Supplier类可以访问Sniper类的所有成员。
#include <iostream> #include <cfloat> #include <cmath> using namespace std; class Sniper { private: int bullets; public: Sniper(int bullets = 0) : bullets(bullets) {} // 声明友元类 friend class Supplier; }; class Supplier { private: int shorage; public: Supplier(int shorage = 1000) : shorage(shorage) {} bool provide(Sniper &sniper) { if (sniper.bullets < 20) { if (this->shorage > 100) { this->shorage -= 100; sniper.bullets += 100; } else if (this->shorage > 0) { sniper.bullets += this->shorage; this->shorage = 0; } else { return false; } } cout << "sniper has " << sniper.bullets << " bullets now." << endl; return true; } }; int main() { Sniper sniper(9); Supplier supplier; supplier.provide(sniper); }
47.2 friend
成员函数
友元成员函数(Friend Member Function)是被声明为友元的非成员函数,它可以访问类的私有(private)和保护(protected)成员。友元成员函数不是类的成员函数,但它具有访问类内部数据的权限。
-
⭐注意事项
- 尽管友元成员函数可以访问类的内部成员,但它不是类的成员函数,因此在函数名前不需要类名。
- 友元成员函数的定义通常位于类定义之外。
- 友元成员函数也可以用于模板类,但需要在模板类的定义中明确声明。
-
友元成员函数的循环调用可能导致编译问题,因为编译器需要在类完全定义之前看到友元函数的定义。解决这个问题的一种方法是使用类名前缀声明,确保友元函数在类定义之前声明,然后在类定义之后定义。
#include <iostream> #include <cfloat> #include <cmath> using namespace std; class Sniper; // 前置声明 class Supplier { private: int shorage; public: Supplier(int shorage = 1000) : shorage(shorage) {} bool provide(Sniper &sniper); // 如果没有前置Sniper声明,此处Sniper为未声明 }; // 该函数必须类外实现,因为前置声明中没有声明bullets bool Supplier::provide(Sniper &sniper) { if (sniper.bullets < 20) { if (this->shorage > 100) { this->shorage -= 100; sniper.bullets += 100; } else if (this->shorage > 0) { sniper.bullets += this->shorage; this->shorage = 0; } else { cout << "no bullets in the shorage." << endl; return false; } } cout << "sniper has " << sniper.bullets << " bullets now." << endl; return true; } class Sniper { private: int bullets; public: Sniper(int bullets = 0) : bullets(bullets) {} // 声明友元成员函数,友元成员函数可以访问Sniper类的所有成员 friend bool Supplier::provide(Sniper &sniper); }; int main() { Sniper sniper(9); Supplier supplier; supplier.provide(sniper); } // sniper has 109 bullets now.
48 Nested types 内部类
内部类概念
内部类特性
-
内部类是外部类的友元类,但是外部类不是内部类的友元。(即内部类可以访问外部类)。
-
内部类定义在外部类的public、protected、private()都是可以的;但是内部类受这些限制符的限定。
-
注意内部类可以直接访问外部类种的static成员,不需要外部类的对象/类名。
class A { class B // 内部类 { public: void Print() { A t1; cout << static_a << endl; // 内部类访问静态成员,可以突破类域进行访问 cout << A::_a << endl; // 报错 cout << t1._a << endl; //而普通成员只能通过对象进行访问 } }; private: //定义在A类下的成员变量 int _a; static int static_a; };
-
在类外定义内部类,受外部类的限制。
int main() { A::B t2; t2.print(); }
-
sizeof(外部类) = 外部类,和内部类无关。(不算静态成员)
int main() { cout << sizeof(A) << endl; // 4字节 //注意静态成员不计入里面。 }
内部类范围
-
private
嵌套类只能在外部类的内部访问,包括外部类的成员函数和友元,但不能被外部类的任何对象或从外部类继承的类访问。class OuterClass { // 外部类 private: class PrivateNestedClass { // private 内部类 public: void function() { // ... } }; }; int main() { OuterClass outer; // outer.PrivateNestedClass nested; // 错误:无法访问 }
-
protected
嵌套类不能被外部类的外部访问,但可以被外部类的成员函数和友元访问,以及从外部类继承的类的成员函数访问。class OuterClass { // 外部类 protected: class ProtectedNestedClass { // protected 内部类 public: void function() { // ... } }; }; class DerivedClass : public OuterClass { // 外部类的派生类 void function() { ProtectedNestedClass nested; nested.function(); // 从派生类中访问 } }; int main() { // DerivedClass derived; // derived.ProtectedNestedClass(); // 错误:在DerivedClass中无法直接访问 }
-
当内部类被声明为
public
时,它可以被外部类的任何对象访问,就像访问外部类的公共成员一样。class OuterClass { // 外部类 public: class PublicNestedClass { // public 内部类 public: void function() { // ... } }; }; int main() { OuterClass outer; OuterClass::PublicNestedClass nested; nested.function(); // 直接访问 }
49 RTTI and type cast operators
49.1 Runtime Type Identification(RTTI) 运行时类型识别
RTTI(Run-Time Type Information,运行时类型信息)是C++中的一种机制,它允许在程序执行过程中确定对象的类型。这是C++支持多态性的关键特性之一,因为在多态情况下,基类指针或引用可能指向任何派生类的对象。RTTI使得我们能够在运行时查询和操作对象的类型 。
-
RTTI核心组件
dynamic_cast
操作符:用于在运行时将基类指针或引用安全地转换为派生类指针或引用。typeid
操作符:用于获取对象的类型信息,返回一个指向std::type_info
类型对象的引用。type_info
类:包含关于类型的信息,如类型名称,可以通过typeid
操作符获得,并提供类型比较的功能
49.2 typeid
-
typeid
操作符- 确定两个对象是否为同一类型
- 接收一个类的名称或计算结果为对象的表达式
-
type_info
类- typeid 运算符返回一个对 type_info 对象的引用
- 在 头文件中定义
- 使用重载的 == 和 != 运算符比较类型
49.3 dynamic_cast
-
dynamic_cast
是C++中用于处理多态性的一种类型转换操作符。它主要用于安全的向下转型,即从基类指针或引用转换为派生类指针或引用。#include <iostream> class Base { public: virtual ~Base() {} }; class Derived : public Base { int data; }; int main() { Derived d; Base* basePtr = &d; // 使用 dynamic_cast 进行安全的向下转型 Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); if (derivedPtr) { std::cout << "dynamic_cast succeeded." << std::endl; // 使用 derivedPtr } else { std::cout << "dynamic_cast failed." << std::endl; } return 0; }
-
dynamic_cast
用于在存在继承关系的情况下,将基类指针或引用安全地转换为派生类指针或引用。 -
使用
dynamic_cast
时,通常在转换目标类型前加上<<
和目标类型的名称,例如:Derived* d = dynamic_cast<Derived*>(basePtr);
。 -
如果转换成功,
dynamic_cast
返回目标类型的指针或引用;如果转换失败,返回nullptr
(对于指针转换)或抛出std::bad_cast
异常(对于引用转换)。 -
dynamic_cast
的行为依赖于对象的typeid
信息,它使用这些信息来确定是否可以执行转换。
49.4 const_cast
-
const_cast
用于移除或添加const
属性。#include <iostream> int main() { int a = 10; const int *p = const_cast<const int*>(&a); // 为 a 添加 const 属性 // 现在 p 是指向 const int 的指针,尝试通过 p 修改 a 的值将会导致编译错误 // *p = 20; // 错误:不能通过 const 指针修改值 std::cout << "Value of a: " << a << std::endl; // 输出 a 的值 // 如果需要修改 a 的值,可以重新获取非 const 指针 int *q = const_cast<int*>(p); // 移除 const 属性 *q = 20; // 现在可以修改 a 的值 std::cout << "Modified value of a: " << a << std::endl; // 输出修改后的 a 的值 return 0; }
-
使用场景
- 当你需要修改一个被声明为
const
的对象时,可以使用const_cast
移除const
限制。 - 也可以用来将非
const
对象转换为const
对象。
- 当你需要修改一个被声明为
49.5 static_cast
-
static_cast
用于在相关类型之间进行基本的、安全的转换。double d = 3.14; int i = static_cast<int>(d); // 将 double 转换为 int
-
使用场景
- 用于内置数据类型之间的转换,例如,将
int
转换为double
。 - 用于类层次结构中基类和派生类的向上转型(upcasting)。
- 转换空指针类型,例如,从
int*
转换为void*
。
- 用于内置数据类型之间的转换,例如,将
-
特点
- 转换是在编译时检查的,因此是静态的。
- 不能用于去除
const
属性或进行向下转型(downcasting),这些情况需要使用const_cast
或dynamic_cast
。
49.6 reinterpret_cast
-
reinterpret_cast
用于将一个指针或引用转换为完全不同的类型。这种转换不进行任何的值转换,只是重新解释对象的位模式(bit pattern)来匹配新类型。#include <iostream> int main() { int a = 10; // 将 int 类型的值转换为 char 类型的指针 char* charPtr = reinterpret_cast<char*>(&a); // 打印 charPtr 解引用的结果 std::cout << "Value of charPtr: " << static_cast<int>(*charPtr) << std::endl; // 将 char 类型的指针转换回 int 类型的指针 int* intPtr = reinterpret_cast<int*>(charPtr); // 使用 intPtr 访问原始的 int 值 std::cout << "Value accessed through intPtr: " << *intPtr << std::endl; return 0; }
-
⭐注意事项
- 当你需要处理低级别的内存操作,例如将一个指针转换为整数,或者将任意类型的指针转换为
void*
指针。也可用于将void*
指针转换回原始指针类型。 - 转换是低级别的,不进行任何类型安全性检查。转换结果的正确性完全依赖于程序员的意图和保证。
- 使用
reinterpret_cast
进行不恰当的转换可能会导致未定义行为,特别是当转换违反了对象的内存布局或对齐要求时。
- 当你需要处理低级别的内存操作,例如将一个指针转换为整数,或者将任意类型的指针转换为
-
⭐
reinterpret_cast
是一种强大的工具,但应该谨慎使用,以避免潜在的危险和错误。