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;