【SE笔记】一些C++复习的东西

0. 一些表述问题

  • 通过一个类来访问另一个类的成员,通常指的是在类的外部通过派生类直接访问基类的数据成员

1. 绪论

下面是生存周期

  • static - 静态,单文件内
  • external - 静态,全局默认,可以跨文件访问
  • register - 不知道是个啥
  • auto - 局部变量的默认生存周期,到点就亖
  • switch就算没有break也可以正常退出

2. 数据流啊?

  • typedef - 给数据类型起别名 - typedef int ii; & typedef ii = int;
  • 没有unsigned doubleunsigned float这种抽象玩意儿
  • doublefloat能表示的数的精度和范围都不同
  • unsigned默认表示unsigned int缩写司马

3. 函数

  • 调用函数的时候传入的是实参
  • 函数定义的时候定义的是形参
  • 函数声明的时候可以没有形参名,但是得有形参
  • C++可以传引用,这属于引用传递不属于值传递,C不行
  • 内联函数不能有switch和循环语句
  • 重载函数,根据形参区分
  • 常量不能绑定成为应用,比如常数100;但是可以成为常引用

4. 类

  • C++11前不允许类内初始化。谢天谢地了真是
  • public - 都可以访问
  • protected - 仔才能访问
  • private - 都不能访问
  • 可以使用默认构造函数的初始化列表给类里面的成员变量进行初始化
1
2
3
4
5
class MyClass {
public:
int x;
MyClass() : x(0) {} // 使用初始化列表初始化 x
};
  • 常引用只能调用常函数,只有类里面有常函数。
  • 常函数不能修改类里面的成员变量。
  • 前向声明的函数在定义前不能访问关于类内容的一切东西
  • 除了静态成员数据,都可以通过构造函数的初始化列表进行初始化

5. 数据共享与保护

  • 静态变量定义的时候初始值默认是0
  • 类的静态成员被所有该类的实例共享,需要再类体外进行初始化
  • 注意静态成员和常量成员不要搞错了!
  • 类的静态成员均可以在没有实例创建的情况下进行修改,调用等操作
  • 友元xx的定义实质上是声明,不需要先定义再使用。
  • 但是使用声明类内的友元函数的时候类需要被(前向)声明,但是不需要被定义
  • 当然可以双向友元
  • 常成员函数不能改变任何类成员
  • 常对象只能调用常成员函数,包括传参的常引用对象
  • 使用匿名命名空间以确保空间内的成员只能在该源文件中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace {
int internalVar = 42;

void internalFunction() {
std::cout << "This is an internal function." << std::endl;
}
}

int main() {
internalFunction(); // 可以调用
std::cout << internalVar << std::endl; // 可以访问
return 0;
}
  • 宏定义可以引入变量,比如这样#define MAX(asdf, hjkl) ((asdf) > (hjkl) ? (asdf) : (hjkl))也是可以的,但是要用括号括起来,这样就不行了#define MAX(asdf, hjkl) asdf > hjkl ? (asdf : hjkl)。其实也能跑,但是结果不一定对。也可以这样替换成一整行
1
2
#define PRINT_SUM(a, b) \
std::cout << "The sum of " << (a) << " and " << (b) << " is " << ((a) + (b)) << std::endl;
  • #ifdef A - 如果A被定义过
  • #ifndef - 如果A没有被定义过
  • #if后面添加常量表达式
  • 使用mutable可以使常成员函数也可以修改某个变量

5.5. C++的编译过程

C++ 的编译过程通常分为以下几个阶段:

  1. 预处理

    • 处理所有的预处理指令(如 #include#define)。
    • 执行宏替换。
    • 移除注释。
    • 生成一个纯粹的 C++ 源代码文件。
  2. 编译

    • 将预处理后的代码翻译成汇编代码。
    • 语法和语义检查。
    • 生成目标文件(通常带有 .o.obj 扩展名)。
  3. 汇编

    • 将汇编代码转换成机器代码。
    • 生成二进制的目标文件。
  4. 链接

    • 将多个目标文件和库文件合并为一个可执行文件。
    • 解析外部符号(如函数和变量的引用)。
    • 处理静态库和动态库的链接。

详细过程:

  • 预处理器:处理所有的预处理指令,展开宏,包含头文件,去除注释等。

  • 编译器:将预处理后的代码转换为汇编代码,这一步会进行语法和语义分析,优化代码,生成目标代码。

  • 汇编器:将汇编代码转换成机器代码,生成二进制的目标文件。

  • 链接器:将所有目标文件和库文件链接在一起,处理符号解析,生成最终的可执行文件。

结果:

最终生成的可执行文件可以在目标平台上运行。通过这些步骤,C++ 源代码被逐步转换为机器能够理解和执行的形式。

6. 数组 & 指针

  • 定义指针时 - type* name = initialization
  • 指针常量 - int* const ptr = &value - ptr不能变,value可以变
  • 常量指针 - const int* ptr = &value - ptr可以变,value不能变
  • 指针常量和常量指针判断 - 就近原则,离const近的不能变(
  • 指针的逆天写法们

    总结一下:中扩号和*p粘一起表示一个数组里面是指针;(*p)强调指针的主导地位(这是一个指针!);如果有外露的*号就和前面的类型粘一起,通常指函数的返回值;
  • (*p)++ - 指针指代的数值++
  • *(p++) - 指针本身++,去到下一个内存地址
  • const_cast - 可以将const属性去除然后就可以更改值了。
1
2
3
4
void foo(const int* cp)
int *p = const_cast <int*> (cp);
(*p)++;
}
  • static_cast - 用于转换类型的,用法newtype a = static_cast<newtype> a,但是不能用于去除const属性,不过可以用于添加const属性。
  • 指针在一定情况下可以相减,但是无论如何都不能相加

7. 继承

  • 派生类声明 - class Derived: public Base1, public Base2;
  • 派生类构造 - Derived(): Base1(), Base2();,和初始化列表差不多
  • 虚基类 - 可以确保每个派生类实例的基类只在内存中创建一次,这样可以预防同义性
  • static_cast可以将基类转化为派生类,但是不能将虚基类转换成其派生类
  • 派生类销毁的时候会自动调用基类的析构函数销毁基类对象,但是反过来不会
  • 当然,以下是 C++ 中不同继承方式的访问权限总结表:
继承方式 基类的 public 成员 基类的 protected 成员 基类的 private 成员
Public public protected 不可访问
Protected protected protected 不可访问
Private private private 不可访问
  • 在使用virtual关键词实现虚继承且具有内嵌对象时,构造函数的调用顺序是:
  1. 虚基类的构造函数首先被调用。
  2. 然后是非虚基类的构造函数。
  3. 接着是内嵌对象的构造函数。
  4. 最后是派生类的构造函数。

8. 多态

  • 编译时多态(静态多态) - 比如函数重载,模板
  • 运行时多态(动态多态) - 比如虚函数,虚基类
  • 基类指正可以指向派生类实例(指针指向的实例只能多内容不能少内容),这是运行时多态
  • 运算符也是函数的一部分,所以可以通过友元函数访问到private的成员,但是只有该类进行改操作的时候会调用运算符重载,其他基本类不会影响
  • C++不能新建运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

class Complex {
private:
double real;
double imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 声明友元函数来重载加法运算符
friend Complex operator+(const Complex& c1, const Complex& c2);

void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};

// 定义友元函数
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);

Complex c3 = c1 + c2; // 使用重载的加法运算符

c3.display(); // 输出结果:4.0 + 6.0i

return 0;
}
  • 前置一元运算符重载 (@a) - 啥都不用传,声明返回引用Counter& operator++();,定义返回指针return *this;
  • 后置一元运算符重载 (a@) - 声明时传一个参数用于区分前置一元运算符,定义时返回更改前的数据,具体实现见下
1
2
3
4
5
Counter operator++(int) {
Counter temp = *this; // 保存当前状态
++value;
return temp; // 返回原始状态
}
  • 二元运算符重载 - 声明传入两个实例,定义返回一个新示例
  • 赋值运算符重载 - 和后置一元运算符重载一样,但是传入一个实例(等号右边的),返回更改后的实例(等号左边的)
1
2
3
4
5
6
7
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 防止自我赋值
delete data; // 释放旧内存
data = new int(*other.data); // 分配并复制新内存
}
return *this;
}

int(*other.data)相当于给这个新指针指代的东西赋初始值了

  • <<>>重载通常为友元函数的方式(本类)
  • 基类的虚成员函数可以被重写override,而非虚成员函数只能被隐藏,而不能被重写
  • 纯虚成员函数 - 强制要求派生类自己实现 - virtual void func() = 0;
  • 构造函数不能虚,析构函数可以虚 - 考虑基类虚构的时候就要虚析构函数了

9. 群体类数据

  • 模板属于编译时多态
  • 注意非类型模板参数 - template <int N> class A; - 这属于编译时多态,在编译后N就变成常量了,和传参效果一样。
  • 可以设置模版的默认类型 - template <typename T = int> class A;
  • 可以通过特化模板或者部分特化模板来实现模板的特化。你不看看你在说些什么😅看下面的示例就知道了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>

// 通用模板
template<typename T>
class Printer {
public:
void print(const T& data) {
std::cout << "General: " << data << std::endl;
}
};

// 完全特化用于int类型,这里的"int"也可以是一个具体的数值
template<>
class Printer<int> {
public:
void print(const int& data) {
std::cout << "Integer: " << data << std::endl;
}
};

// 通用模板
template<typename T, typename U>
class Pair {
public:
void show() {
std::cout << "General template" << std::endl;
}
};


// 部分特化,当第二个参数是int时
template<typename T>
class Pair<T, int> {
public:
void show() {
std::cout << "Specialized with int as second parameter" << std::endl;
}
};


int main() {
Printer<double> doublePrinter;
doublePrinter.print(3.14); // 输出: General: 3.14

Printer<int> intPrinter;
intPrinter.print(42); // 输出: Integer: 42

Pair<double, double> generalPair;
generalPair.show(); // 输出: General template

Pair<double, int> specializedPair;
specializedPair.show(); // 输出: Specialized with int as second parameter

return 0;
}
  • 可以通过模板实现将一些任务在编译的时候就完成,以节省实际运行的时间,比如这样
1
2
3
4
5
6
7
8
9
10
template <unsigned N>
struct Factorial { //计算N的阶乘
enum {
VALUE = N * Factorial<N – 1>::VALUE
};
};
template <>
struct Factorial<0> { //设定N = 0时N的阶乘
enum { VALUE = 1 };
};
  • 可变参数模板,但是看不懂,可能不考吼,以后慢慢看((((
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void print() {
std::cout << "End of recursion" << std::endl;
}

template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // 递归调用
}

int main() {
print(1, 2.5, "Hello", 'A'); // 输出: 1 2.5 Hello A End of recursion
return 0;
}
  • 多态+模板+递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
template <class T>
void print(T a) {
std::cout << a << "\t";
}

template <typename T, typename... Args>
void print(T first, Args... rest) {
print(rest...);
}

int main() {
print(1, 2.5, "Hello", 'A');
print(42);
return 0;
}
// 输出是
// A 42

10. STL

  • 先把这图记住吧(
  • 流输入迭代器开始 - istream_iterator<int>(cin)
  • 流输入迭代结束 - istream_iterator<>()
  • 流输出迭代 - ostream_iterator<int>(cout, " ")
  • 迭代器返回的是指针
1
2
3
for (auto it = myList.begin(); it != myList.end(); ++it) {
std::cout << *it << " ";
}
  • 要学的容器
容器 顺序容器 关联容器
可逆容器 list set, multiset, map, multimap
随机访问容器 vector, deque None
  • 要学的关联容器
关联容器 单重关联容器 多重关联容器
简单关联容器 set multiset
二元关联容器 map multimap
  • 简单关联容器只能存储数值,二元关联容器可以存储数值+标签,里面用的是pair
  • 多重关联容器可以存重复的内容,否则启动去重
  • set可以自动排序和去重
  • 和模板结合使用
1
2
3
4
5
6
template <class InputIteratorBegin, 
class InputIteratorEnd,
class OutputIterator>
void func(InputIteratorBegin begin,
InputIteratorEnd end,
OutputIterator output);

10.5. Lambda表达式

Lambda 表达式是 C++11 引入的一种简洁的函数对象(functor)的表示方式。它允许你在代码中定义匿名函数,通常用于简化代码和提高可读性。以下是 lambda 表达式的基本结构和功能:

基本结构

1
2
3
[capture](parameters) -> return_type {
// function body
}
  • 捕获列表(capture):定义 lambda 表达式可以访问的外部变量。

    • [ ]:不捕获任何变量。
    • [=]:按值捕获所有外部变量。
    • [&]:按引用捕获所有外部变量。
    • [x]:按值捕获变量 x
    • [&x]:按引用捕获变量 x
  • 参数列表(parameters):函数的参数,与普通函数相同。

  • 返回类型(return_type):可选,通常编译器会自动推导返回类型。

  • 函数体:lambda 表达式的主体,包含实际的代码逻辑。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 使用 lambda 表达式打印每个元素
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;

// 使用 lambda 表达式计算平方
std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int n) {
return n * n;
});

// 打印结果
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;

return 0;
}

捕获示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main() {
int a = 5;
int b = 10;

// 按值捕获
auto add = [=]() {
return a + b;
};
std::cout << "Sum by value: " << add() << std::endl;

// 按引用捕获
auto add_ref = [&]() {
return a + b;
};
a = 10;
std::cout << "Sum by reference: " << add_ref() << std::endl;

return 0;
}

优势

  • 简洁性:减少代码量,提升可读性。
  • 灵活性:可以在函数内部定义行为,避免额外定义函数。
  • 作用域控制:通过捕获列表精确控制变量的访问。

Lambda 表达式在处理回调、排序、过滤等操作时非常有用,是现代 C++ 编程的重要工具。

11. 流

  • 控制台流 - cincout
  • 文件流 - ifstreamofstream
  • 文件流文件操作
函数 作用 方法
open 打开文件 open("{file_name}", {流方式})
close 关闭文件 close()
put 写入一个字符 put("A")
get 成员函数,用于读取字符 get({char* }, {size})
getline 独立函数,用于读取整行 getline({input_stream}, {destination}, {end_char})
write 写入一堆东西,主要用于二进制 write({content}, {size})
read 读取一堆东西,主要用于二进制 read({content}, {size})
tellp 获取当前流的位置,返回值类型为streampos streampos pos = stream.tellp();
seekp 设置文件流的指针位置 seekp({absolute_pos})或者seekp({relative_pos}, {ios::beg / ios::cur / ios::end})
  • write, read在二进制文件中与reinterpret_cast一起食用 - write/read(reinterpret_cast<char*>(&a), sizeof(a))
  • 流方式
流方式 作用
ios_base::binary 以二进制的形式处理文件
ios_base::in 读取文件
ios_base::out 写入文件
ios_base::trunc 清空文件
ios_base::app 追加文件
ios_base::ate 默认将流位置定位到末尾
  • 流的格式化方式 - 可以在cout这里setiosflagsresetiosflags
格式化方式 作用
ios_base::dec 十进制
ios_base::oct 八进制
ios_base::hex 十六进制
ios_base::left 左对齐,注意先setw
ios_base::right 右对齐,注意先setw
ios_base::uppercase 十六进制输出大写字母

12. 异常处理

  • 给个demo,差不多了,基本上就是“抛出,接住,处理”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <stdexcept>

// 自定义异常类
class CustomException : public std::exception {
public:
const char* what() const noexcept override {
return "Custom exception occurred";
}
};

// 函数可能抛出异常
void riskyFunction(int value) {
if (value < 0) {
throw std::invalid_argument("Negative value provided");
} else if (value == 0) {
throw CustomException();
} else {
std::cout << "Value is valid: " << value << std::endl;
}
}

int main() {
try {
riskyFunction(-1);
} catch (const std::invalid_argument& e) {
std::cerr << "Caught an invalid_argument exception: " << e.what() << std::endl;
} catch (const CustomException& e) {
std::cerr << "Caught a custom exception: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught a generic exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception" << std::endl;
}

std::cout << "Program continues after exception handling." << std::endl;

return 0;
}

【SE笔记】一些C++复习的东西
https://学习.fun/se-note/se-cpp-review-2024-12/
Author
Stephen Zeng
Posted on
December 26, 2024
Licensed under