C++ map新知

假期闲来无事,打一打基础。


命名空间

在 C++ 中,命名空间是一个命名范围或容器,用于组织和封装代码元素的集合,例如变量、函数、类和其他命名空间。

它们主要用于划分和管理代码库,使开发人员能够控制名称冲突和代码的专业化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

namespace animals {
std::string dog = "Bobby";
std::string cat = "Lilly";
}

int main() {//访问命名空间中的内容需要加上作用域符号::
std::cout << "Dog's name: " << animals::dog << std::endl;
std::cout << "Cat's name: " << animals::cat << std::endl;

using animals::dog;//using关键字,后面可以直接使用命名空间animals中的变量dog
std::cout << "Dog's name: " << dog << std::endl;
return 0;
}

像常用的 using namespace std;

就可以直接使用std中的成员,cincoutendl等等。

多态

父类指针或引用指向子类对象,产生多态现象。

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
class Animal {
public:
virtual void makeSound() {
std::cout << "The Animal makes a sound" << std::endl;
}
};

class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks!" << std::endl;
}
};

class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Cat meows!" << std::endl;
}
};
int main() {
Animal* myAnimal = new Animal();//父类指针指向子类对象
Animal* myDog = new Dog();
Animal* myCat = new Cat();

myAnimal->makeSound(); // Outputs: The Animal makes a sound
myDog->makeSound(); // Outputs: Dog barks!
myCat->makeSound(); // Outputs: Cat meows!

delete myAnimal;
delete myDog;
delete myCat;

return 0;
}

类中的特殊成员函数

  1. 构造函数(Constructor):

    1
    MyResource() : data(new int[100]) {}

    这个构造函数用于在对象创建时分配一个包含100个整数的动态数组,并将该数组的地址存储在 data 成员变量中。

  2. 析构函数(Destructor):

    1
    ~MyResource() { delete[] data; }

    析构函数用于在对象销毁时释放动态分配的内存。这里使用 delete[] 来释放通过 new int[100] 分配的整数数组。

  3. 复制构造函数(Copy Constructor):

    1
    2
    3
    MyResource(const MyResource& other) : data(new int[100]) {
    std::copy(other.data, other.data + 100, data);
    }

    复制构造函数用于在创建一个新对象时,使用另一个对象的数据来初始化新对象。这里通过动态分配一个新的数组,并使用 std::copyother 对象的数据复制到新数组中。

    其中:

    • other.data: 是源范围的起始位置,指向 other 对象的动态分配的整数数组。
    • other.data + 100: 是源范围的结束位置,指向 other.data 之后100个元素的位置。
    • data: 是目标范围的起始位置,指向当前对象的动态分配的整数数组。
  4. 复制赋值运算符(Copy Assignment Operator):

    1
    2
    3
    4
    5
    MyResource& operator=(const MyResource& other) {
    if (&other == this) { return *this; }
    std::copy(other.data, other.data + 100, data);
    return *this;
    }

    复制赋值运算符用于将一个对象的数据复制到另一个已经存在的对象中。在这里,首先检查是否是自我赋值(if (&other == this)),如果是自我赋值,就直接返回当前对象。否则,分配一个新的数组,并使用 std::copyother 对象的数据复制到当前对象operator的数组中。

  5. 移动构造函数(Move Constructor):

    通过转移所有权来更有效地处理资源,而不必复制所有数据。

1
2
3
MyResource(MyResource&& other) noexcept : data(other.data) {
other.data = nullptr;
}

移动构造函数接受一个右值引用 MyResource&& other,表示可以使用其他对象的资源。在这里,它将 other 对象的 data 指针(指向动态分配的数组)移动到当前对象,并将 other.data 设置为 nullptr表示资源已经被移动。这样做的目的是确保在 other 对象被销毁时不会重复释放内存。

  1. 移动赋值运算符(Move Assignment Operator):
1
2
3
4
5
6
7
MyResource& operator=(MyResource&& other) noexcept {
if (&other == this) { return *this; }
delete[] data;
data = other.data;
other.data = nullptr;
return *this;
}

移动赋值运算符接受一个右值引用 MyResource&& other,表示可以使用其他对象的资源。它首先检查是否是自我赋值,然后释放当前对象已有的资源(通过 delete[] data),将 other.data 移动到当前对象,最后将 other.data 设置为 nullptr。这样做是为了确保在 other 对象被销毁时不会重复释放内存。

这些移动语义的改进可以提高对象的性能,特别是在涉及到资源管理的场景,因为它避免了不必要的深拷贝

菱形继承

菱形继承会带来二义性和资源浪费的问题

1
2
3
4
5
  A
/ \
B C
\ /
D

二义性(Ambiguity): 如果在类 D 中访问一个继承自 A 的成员,编译器可能无法确定应该使用哪个基类的成员,因为可以通过两条路径(从 B 或从 C)到达 A

资源浪费: 如果 BC 中都有 A 类的成员,而 D 继承了两者,就会导致 A 类的成员在 D 中存在两份,这可能会浪费内存。

为了解决这些问题,可以使用虚继承,通过在基类 A 的继承前面加上 virtual 关键字,确保只有一份 A 类的实例被继承,

1
2
3
class B : virtual public A {    // ... }; 

class C : virtual public A { // ... };

静态多态性

静态多态性,也称为编译时多态性,是一种在编译时而非运行时解析类型和方法调用的多态性。

通常使用 C++ 中的函数重载模板来实现的。

函数重载是一种创建多个具有相同名称但不同参数列表的函数的方法。

编译器根据调用函数时使用的参数类型和数量确定要调用的正确函数。

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

void print(int i) {
std::cout << "Printing int: " << i << std::endl;
}

void print(double d) {
std::cout << "Printing double: " << d << std::endl;
}

void print(const char* s) {
std::cout << "Printing string: " << s << std::endl;
}

int main() {
print(5); // Calls print(int i)
print(3.14); // Calls print(double d)
print("Hello"); // Calls print(const char* s)

return 0;
}

模板是 C++ 中的一项强大功能,创建通用函数或通用类。

特定类型的实际代码是在编译时生成的,这避免了运行时多态性的开销。使用模板是C++中实现静态多态性的主要技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

// Template function to print any type
template<typename T>//T为各种数据类型
void print(const T& value) {
std::cout << "Printing value: " << value << std::endl;
}

int main() {
print(42); // int
print(3.14159); // double
print("Hello"); // const char*

return 0;
}

动态多态性

动态多态性是面向对象语言(如 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
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

// Base class
class Shape {
public:
virtual void draw() {//虚函数
std::cout << "Drawing a shape" << std::endl;
}
};

// Derived class 1
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};

// Derived class 2
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle" << std::endl;
}
};

int main() {
Shape* shape;
Circle circle;
Rectangle rectangle;

// Storing the address of circle
shape = &circle;//父类指针指向子类对象

// Call circle draw function
shape->draw();

// Storing the address of rectangle
shape = &rectangle;//父类指针指向子类对象

// Call rectangle draw function
shape->draw();

return 0;
}

异常处理

C++ 中的异常处理是一种处理程序执行期间可能发生的错误、异常或意外事件的机制。

这使得程序在遇到错误时可以继续运行或优雅退出,而不是突然崩溃

C++ 提供了一组关键字和构造来实现异常处理:

  • try:定义应监视异常的代码块。
  • catch:指定要捕获的异常类型以及异常发生时应执行的代码块。
  • throw:引发异常,该异常将由适当的 catch 块捕获并处理。
  • noexcept:指定一个函数,如果在其范围内抛出异常,则不会抛出异常或终止程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int divide(int a, int b) {
if (b == 0) {
throw "Division by zero!";
}
return a / b;
}

int main() {
int num1, num2;

std::cout << "Enter two numbers for division: ";
std::cin >> num1 >> num2;

try {
int result = divide(num1, num2);
std::cout << "The result is: " << result << std::endl;
} catch (const char* msg) {
std::cerr << "Error: " << msg << std::endl;
}

return 0;
}

当除数为0时,会执行throw "Division by zero!";然后"Division by zero!"会传到catch语句块,输出错误信息。

动态数组vector

vector 是一种动态数组(dynamic array)的数据结构,它属于标准模板库(STL)的一部分。

vector 提供了一个可变大小的数组,可以在运行时动态地调整大小,而且支持在数组的末尾高效地添加或删除元素

与传统的数组相比,vector 的一个主要优势是它能够自动处理内存管理,不需要手动指定数组大小。

1
2
3
4
5
6
#include <iostream>
#include <vector>

int main() {
// 创建一个空的整数数组
std::vector<int> myVector;
1
2
3
4
5
6
7
8
9
10
11
12
// 向数组中添加元素
myVector.push_back(1);
myVector.push_back(2);
myVector.push_back(3);
//删除数组的末尾元素
myVector.pop_back();
// 访问向量中的元素
for (int i = 0; i < myVector.size(); ++i) {
std::cout << myVector[i] << " ";
}
return 0;
}

auto关键字

auto用于自动类型推导,在编译时自动从变量的初始化表达式的类型推断出变量的类型。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <vector>

int main() {
// Traditional way of declaring a variable:
int myInt = 5;

// Using auto for type deduction:
auto myAutoInt = 5; // Automatically deduces the type as 'int'
}

因为auto是根据等号右项来自动推导类型,所以必须要提供初始值,否则会报错。

类型转换

C 风格转换:这是从 C 继承的语法,只需将目标数据类型放在要转换的值之前的括号中即可完成。

1
2
int a = 10;
float b = (float)a; // C-style cast from int to float

static_cast:这是 C++ 中最常用的类型转换方法。它在编译时执行,当您在数据类型之间进行显式转换时应该使用它。

1
2
int a = 10;
float b = static_cast<float>(a); // static_cast from int to float

dynamic_cast:此方法专门用于在类层次结构中的基类和派生类之间安全地转换指针和引用。

1
2
3
4
5
class Base {};
class Derived : public Base {};

Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr); // dynamic_cast from Base* to Derived*

reinterpret_cast:此转换更改指针、引用或整数值的类型。它也称为按位转换,因为它改变了编译器解释底层位的方式。

仅当您对自己正在做的事情有深入了解时才使用reinterpret_cast,因为它并不能保证结果值有意义。

1
2
int* a = new int(42);
long b = reinterpret_cast<long>(a); // reinterpret_cast from int* to long

const_cast:此转换方法用于从常量中删除限定符。通常不建议这样做,但在某些无法控制变量常量的情况下可能很有用。

1
2
3
const int a = 10;
int* ptr = const_cast<int*>(&a); // const_cast from const int* to int*
*ptr = 20; // Not recommended, use with caution

未定义行为

未初始化的变量:声明一个变量但没有显式地初始化它时,它的值是未定义的,而不是默认初始化为0

1
2
int x;
int y = x + 5; // Undefined behavior since x is uninitialized

越界内存访问

1
2
int arr[5];
int val = arr[5]; // Undefined behavior since the valid indices are 0 to 4

空指针解引用

1
2
int *ptr = nullptr;
int val = *ptr; // Undefined behavior since ptr is a null pointer

除以零

1
2
3
int x = 5;
int y = 0;
int z = x / y; // Undefined behavior since division by zero is not allowed

C++ 宏

宏是 C++ 中的预处理指令,预处理器使用它来执行文本替换。它们是使用#define指令定义的,后跟宏名称和要替换的值。

常量宏:常量宏用于定义在代码中使用的符号常量。它们不使用任何内存,并在编译过程之前由预处理器替换。

1
2
3
#define PI 3.14159//常数宏定义

double circumference = 2 * PI * radius;

函数宏

1
2
3
#define SQUARE(x) ((x) * (x))//函数宏定义

int square_of_five = SQUARE(5); // expands to ((5) * (5))

C++ 标准模板库STL

C++ 标准模板库 (STL) 是头文件的集合,提供多种数据结构、算法和函数来简化 C++ 编码体验

STL 的主要目的是通过提供一组即用型的有用工具来节省时间并提高效率。

STL最常用的功能可以分为三大类:容器、算法迭代器

容器:C++ 中用于数据存储和操作的数据结构。它们分为四种类型:序列容器、关联容器、无序关联容器和容器适配器。

std::vector:在运行时增长和收缩的动态数组。

1
std::vector<int> my_vector;

std::list:双向链表。

1
std::list<int> my_list;

std::set:按键值排序的元素的集合。

1
std::set<int> my_set;

std::unordered_set:没有特定顺序的独特元素的集合。

1
std::unordered_set<int> my_unordered_set;

std::stack:栈

1
std::stack<int> my_stack;

std::queue:队列

1
std::queue<int> my_queue;

未完待续 ~

算法:STL 提供了几种通用算法,可用于对容器中存储的数据执行各种操作。

它们分为五类:非修改序列算法、修改序列算法、排序算法、排序范围算法和数值算法。

一些示例包括std::findstd::replacestd::sortstd::binary_search

1
2
3
//排序算法(从小到大)
std::vector<int> my_vec = {4, 2, 5, 1, 3};
std::sort(my_vec.begin(), my_vec.end());

迭代器:迭代器是 STL 中的一个基本概念,因为它们提供了访问容器中元素的统一方法。

迭代器可以被认为是指针的高级形式

每个容器都有自己的迭代器类型,可用于遍历元素和修改值

最常见的迭代器操作是begin()和 ,end()分别用于获取指向容器的第一个元素和最后一个元素之后的迭代器。

1
2
3
4
5
6
//遍历元素
std::vector<int> my_vec = {1, 2, 3, 4, 5};//定义一个动态数组

for(auto it = my_vec.begin(); it != my_vec.end(); ++it) {//for循环
std::cout << *it << " "; //通过解引用操作符 *,可以访问迭代器所指向的元素。
}

多线程

多线程是程序中多个线程的并发执行

通过同时执行多个任务来提高应用程序的性能和效率。

基本线程的创建

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <thread>

void my_function() {
std::cout << "This function is executing in a separate thread" << std::endl;
}

int main() {
std::thread t(my_function);//创建了一个名为 t 的线程对象,并将 my_function 作为新线程的执行函数。
t.join(); //在主线程中等待新线程执行完成
return 0;
}

互斥量

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
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;//定义了一个互斥量对象 mtx,它将用于保护共享资源,确保在任何时候只有一个线程能够访问它。

void print_block(int n, char c) {//以n为次数打印字符c。
{
std::unique_lock<std::mutex> locker(mtx);//创建一个独占锁,一旦一个线程获得了锁,其他线程就无法获得,直到释放。
for (int i = 0; i < n; ++i) {
std::cout << c;
}
std::cout << std::endl;
}
}

int main() {//创建了两个线程 t1 和 t2
std::thread t1(print_block, 50, '*');
std::thread t2(print_block, 50, '$');
//等待两个线程执行完成。确保在主线程结束之前,等待所有的子线程执行完成。
t1.join();
t2.join();

return 0;
}

使用了互斥量 std::mutex避免竞态条件

如果没有互斥量,两个线程 t1t2 可能会同时访问 std::cout,导致输出的字符交叉、混乱,从而产生竞态条件。

通过使用互斥量,每个线程在访问共享资源之前都会尝试获得锁,确保只有一个线程能够执行打印操作,从而避免了竞态条件的发生。

模板

C++ 中的模板是一项强大的功能,用来编写通用代码。模板:template关键字

这意味着可以编写能够处理不同数据类型的单个函数或类,这意味着不需要为每种数据类型编写单独的函数或类。

模板函数

1
2
3
4
5
6
7
8
template <typename T>//模板头
T max(T a, T b) {//判断哪个数更大
return (a > b) ? a : b;
}

int result = max<int>(10, 20);//可以显式指定数据类型

int result = max(10, 20);//也可以让编译器自行推断

模板类

1
2
3
4
5
6
7
8
9
10
template <typename T1, typename T2>//模板头
class Pair {
public:
T1 first;
T2 second;

Pair(T1 first, T2 second) : first(first), second(second) {}//类的构造函数,初始化成员变量
};

Pair<int, std::string> pair(1, "Hello");//实例化类对象并传参

可变参数模板

定义具有可变数量参数的模板。

当需要编写可以接受不同数量和类型的参数的函数或类时,这特别有用。

表示:template <typename... Args>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
// 基本模板
template <typename T>
T sum(T t) {
return t;
}

// 可变参数模板
template <typename T, typename... Args>
T sum(T t, Args... args) {//对多个数求和
return t + sum(args...);
}

int main() {
int result = sum(1, 2, 3, 4, 5); // expands to 1 + 2 + 3 + 4 + 5
std::cout << "The sum is: " << result << std::endl;

return 0;
}

引用

1
server_client::AddInts::Request &req

在C++中,声明一个名为 req 的变量,且该变量是 Request 类型的一个引用,这种做法在多种场景下都非常有用。以下是一些关键的应用场景和优势:

  1. 避免不必要的拷贝
    Request 类型包含大量数据或复杂结构时,通过引用传递可以避免在函数调用或参数传递过程中进行昂贵的拷贝操作。这能够显著提升程序的性能,特别是当处理大型数据结构时。
  2. 直接修改原始数据
    通过引用传递的变量(如 req)允许函数或方法直接修改其引用的原始数据。这在需要更新或修改传入参数的场景中非常有用。
  3. 保持数据一致性
    在某些情况下,确保数据的一致性和准确性至关重要。通过引用传递,可以确保所有操作都在同一份数据上进行,从而避免了数据不同步或丢失更新的风险。
  4. 提高代码可读性
    使用引用可以使代码更加清晰和易于理解。通过明确标注参数是通过引用传递的,可以更容易地推断出函数或方法可能会修改其参数。
  5. 支持多态性
    在面向对象编程中,引用常用于支持多态性。通过基类引用指向派生类对象,可以实现动态绑定和运行时多态性。
  6. 函数返回引用
    虽然不直接相关于你的例子,但值得一提的是,函数也可以返回引用。这允许函数返回一个对内部数据结构的直接引用,而不是其拷贝。
  7. 与STL容器和算法协同工作
    C++标准模板库(STL)中的许多算法和容器都使用引用来操作元素。通过引用传递,可以确保这些算法和容器能够高效地处理数据。

在你的特定情况下,如果 req 是作为某个函数或方法的参数传递的,并且该参数需要被修改或包含大量数据,那么使用引用传递是一个明智的选择。这样做不仅可以提高性能,还可以使代码更加简洁和易于维护。

请注意,当使用引用时,必须确保引用的对象在引用的生命周期内始终有效。如果引用的对象被销毁或变得无效,那么通过该引用进行的任何操作都可能导致未定义行为。因此,在使用引用时需要格外小心。

-------------本文结束-------------