C++从0到1

news2024/10/7 11:21:06

左值和右值:

左值左值是可以位于赋值操作左边的表达式。意味着左值代表一个对象的身份内存中的具体位置。可以被取地址,并且可以位于赋值操作的左边或右边
右值右值是不能位于赋值操作左边的表达式。右值代表一个对象的值,通常是一个临时对象。右值不能被取地址,通常只能位于赋值操作的右边
C++11纯右值(Pure Rvalue):这是传统的右值,如临时对象或字面量。
将亡值(Xvalue):这是一种特殊的右值,它表示一个即将被移动的对象。在C++11中,移动语义允许资源(如动态内存)从将亡值转移到另一个对象,而不需要进行复制。
左值引用可以理解为对左值的引用,那么右边的就需要可取地址,或者用const引用形式,但const只能通过引用读取输出,不能修改数据
右值引用可以理解为对右值的引用。对一个临时对象或即将销毁的对象不想赋值时的引用, int&& ref = 10;右值引用不能绑定到左值上,但可以绑定到右值上。右值引用通常用于移动语义和性能优化。

移动语义与完美转发:

移动语义和完美转发是C++11引入的两个重要特性,目的是提高性能,减少不必要的对象复制和临时对象的创建。

移动语义允许你从一个函数返回一个大对象时不进行复制,而是移动它转移所有权,是一种优化资源管理机制。这是通过使用std::move函数来告诉编译器你希望对象被移动而不是复制节省时间和内存。std::move 是一个标准库函数,其定义在 <utility> 头文件中。它的作用是将一个左值转换为一个右值。为了支持移动语义,通常需要定义 移动构造函数 和 移动赋值运算符。默认原对象不在使用,原对象到底能不能用取决于你的移动构造函数和移动赋值函数如何实现(如果还是拷贝则原对象还可以使用,如果原对象内存里废弃掉新对象使用原对象内存,那原对象不可以用)

#include <iostream>
#include <utility> // for std::move
class MyClass {
public:
    MyClass(int size) : data(new int[size]), size(size) {
        std::cout << "Resource acquired\n";
    }
    // 移动构造函数
    MyClass(MyClass&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr; // 置空其他对象的资源,以避免双重释放
        other.size = 0;
        std::cout << "Resource moved\n";
    }

    ~MyClass() {
        delete[] data; // 释放资源
        std::cout << "Resource released\n";
    }

public:
    int* data;
    int size;
};

int main() {
    MyClass obj1(10); // 创建一个对象
    MyClass obj2 = std::move(obj1); // 移动 obj1 的资源到 obj2
    // 在此之后,obj1 的资源已被转移,制造新的内容不能再使用 obj1
    return 0;
}

完美转发允许你在模板函数中完全保留函数参数的左值和右值属性。这是通过模板中的forward函数来实现的,主要用于在模板中转发函数参数。它的主要目的是实现高效的参数传递,以保持参数的值类别(左值或右值),并避免不必要的拷贝。

移动语义完美转发
#include <iostream>
#include <vector>
#include <utility> // for std::move
std::vector<int> returnByValue(std::vector<int> vec) {
    return vec; // 按值返回,可能会发生复制
}
std::vector<int> returnByMove(std::vector<int> vec) {
    return std::move(vec); // 使用移动语义返回
}
int main() {
    std::vector<int> largeVector;
    // 填充大对象
    for (int i = 0; i < 1000000; ++i) {
        largeVector.push_back(i);
    }
    // 按值返回,可能会发生复制
    std::vector<int> copied = returnByValue(largeVector);
    // 使用移动语义返回,避免了复制
    std::vector<int> moved = returnByMove(std::move(largeVector));
    return 0;
}

#include <iostream>
#include <utility> 
// 包含 std::forward

// 一个示例函数,接受一个整数参数
void process(int& i) {
    std::cout << "Lvalue: " << i << std::endl;
}
void process(int&& i) {
    std::cout << "Rvalue: " << i << std::endl;
}
// 完美转发的模板函数
template<typename T>
void forwardToProcess(T&& arg) {
    // 使用 std::forward 将参数转发
    process(std::forward<T>(arg)); 
}
int main() {
    int x = 10;
    // 使用左值调用
    forwardToProcess(x);   // Lvalue: 10
    // 使用右值调用
    forwardToProcess(20);  // Rvalue: 20
    return 0;
}

列表初始化:

列表初始化是C++11引入的一种新的初始化方式,它允许使用大括号 {} 来初始化各种类型的对象包括基本数据类型、类对象、数组、标准库容器等。列表初始化的好处:简洁性,类型安全:可以防止窄化转换(narrowing conversion),即不会自动转换数据类型,减少错误。统一性:可以用于各种数据结构,使得初始化方式统一。
 

基础类型数组结构体和类对象

int a{5}; 或者int a={5};

double b{3.14};

int arr[]{1, 2, 3, 4}; 

struct Point { int x; int y; }; Point p{1, 2};

// 初始化一个Point对象p,x=1,y=2

标准容器

防止窄化转换 int x = {3.5};// 错误:不能从double到int窄化转换:

std::vector<int> vec{1, 2, 3, 4};

如果尝试使用列表初始化从一个较大的类型(如 double)转换为较小的类型(如 int),编译器会报错。

智能指针:

智能指针(包括std::unique_ptr、std::shared_ptr和std::weak_ptr)

1. std::unique_ptr
原理:std::unique_ptr 是一个独占所有权的智能指针,利用RAII模式,自动管理动态分配的资源,意味着同一时间只能有一个 unique_ptr 指向某个对象。当 unique_ptr 被销毁时,它所管理的内存会自动释放,避免内存泄漏。不允许被复制因为他的拷贝构造被声明为delete,可以进行所以权转移,可以避免双重释放问题。
当你需要唯一拥有一个对象的所有权,且不需要共享它时,可以使用 unique_ptr。常用于动态分配的对象,确保它们会在不再需要时被清理。

class A {
public:
    A() { std::cout << "A Constructor\n"; }
    ~A() { std::cout << "A Destructor\n"; }
};

int main() {
    std::unique_ptr<A> ptr1(new A()); // 创建 unique_ptr
    // std::unique_ptr<A> ptr2 = ptr1; // 错误:不能拷贝
    std::unique_ptr<A> ptr2 = std::move(ptr1); // 转移所有权
    return 0;
}

2. std::shared_ptr
原理:std::shared_ptr 允许多个指针共享同一个对象的所有权。它使用引用计数来追踪有多少个 shared_ptr 指向同一个对象。只有当最后一个 shared_ptr 被销毁后,所管理的对象才会被释放。
使用场景:适用于需要多个对象共享同一个资源的情况,比如在多个地方需要引用同一个对象,但不希望它立即被销毁。

class A {
public:
    A() { std::cout << "A Constructor\n"; }
    ~A() { std::cout << "A Destructor\n"; }
};
int main() {
    std::shared_ptr<A> ptr1(new A()); // 创建 shared_ptr
    {
        std::shared_ptr<A> ptr2 = ptr1; // 共用同一个对象
        std::cout << "Usage Count: " << ptr1.use_count() << '\n'; //2 输出使用计数
    } // ptr2 超出作用域,使用计数减少
    std::cout << "Usage Count: " << ptr1.use_count() << '\n'; //1 仍然可用
    return 0;
}

shared_ptr 本身是线程安全的,具体来说,它的引用计数操作是线程安全的因为对于引用计数做了原子级操作。这意味着你可以在多个线程中安全地读取和复制同一个 shared_ptr。然而,shared_ptr 对所指向的对象的访问并不是线程安全的。如果多个线程同时访问和修改同一个对象,你仍然需要使用其他同步机制(如互斥锁)来保证线程安全。

3. std::weak_ptr
原理:std::weak_ptr 是一种不控制所有权的智能指针,主要用于解决 shared_ptr 之间的循环引用问题。通过 weak_ptr 可以观察到 shared_ptr 指向的对象,但不增加引用计数,当 shared_ptr 被销毁时,weak_ptr 不会影响其生命周期。
使用场景:当你需要引用一个 shared_ptr 指向的对象,但不希望阻止它被销毁,例如在缓存或观察者模式中使用。

class A {
public:
    A() { std::cout << "A Constructor\n"; }
    ~A() { std::cout << "A Destructor\n"; }
};

int main() {
    std::shared_ptr<A> ptr1(new A()); // 创建 shared_ptr
    std::weak_ptr<A> weakPtr = ptr1; // 创建 weak_ptr

    std::cout << "Use Count: " << ptr1.use_count() << '\n'; //1 输出使用计数
    if (auto sharedPtr = weakPtr.lock()) { // 检查对象是否仍然存在
        std::cout << "Object is alive\n"; //Y
    } else {
        std::cout << "Object is no longer alive\n";
    }

    ptr1.reset(); // 释放 shared_ptr 指向的对象
    if (auto sharedPtr = weakPtr.lock()) {
        std::cout << "Object is alive\n";
    } else {
        std::cout << "Object is no longer alive\n"; //Y
    }
    return 0;
}

static

1. 静态变量(局部静态变量)
定义:在函数内部定义的变量,可以使用static修饰。
特点:该变量在函数调用之间保持其值,不会在每次调用时重新初始化。只在第一次调用函数时初始化一次。
使用场景:当需要在函数中记住某个状态,且不想每次调用时都初始化时。

void countCalls() {
    static int callCount = 0; // 只初始化一次
    callCount++;
    std::cout << "function called " << callCount << " times." << std::endl;
}

int main() {
    countCalls(); // 输出 1
    countCalls(); // 输出 2
    countCalls(); // 输出 3
    return 0;
}

2. 静态全局变量或函数
定义:在文件顶部声明的变量,使用static修饰。
特点:该变量只能在声明它的文件内访问,其他文件无法访问。
使用场景:有助于限制变量的作用域,使其只在当前文件中可见,从而避免与其他文件中的同名变量冲突。

3. 静态成员变量、函数
定义:在类中声明的变量、函数,使用static修饰。
特点:属于类而非类的实例,所有实例共享同一个静态变量。而函数不能访问类的非静态成员,只能访问静态成员。必须在类外进行初始化。
使用场景:当你希望跟踪与类相关的状态,而与特定实例无关时。

class A {
public:
    static int instanceCount; // 声明静态成员变量
    A() {
        instanceCount++; // 每次创建实例时增加计数
    }
    static void displayMessage() {
        std::cout << "Hello from static function!" << std::endl;
    }
};
int A::instanceCount = 0; // 定义并初始化静态成员变量
int main() {
    A obj1;
    A obj2;
    A::displayMessage(); // 调用静态成员函数
    std::cout << "Number of instances: " << A::instanceCount << std::endl; // 输出 2
    return 0;
}

STL

std::array

std::array 是 C++ 标准库中的一个容器,它封装了一个固定大小的数组。

类型安全固定大小: std::array 的大小在编译时确定,不能在运行时改变它的大小,这增加了类型安全性。
类型信息: 与普通数组相比,std::array 维护了数组元素的类型信息,减少了由于数据类型不匹配而导致的错误。
更好的接口

成员函数: std::array 提供了许多方便的成员函数,例如 size()、at()、front() 和 back() 等,这些函数增强了数组的易用性和可读性。
迭代器: std::array 支持迭代器,可以使用范围基于的 for 循环和 STL 算法(如 std::sort,std::copy 等)。

使用 std::array 的 std::get<N>(); 它提供了一种安全且类型安全的方式来访问特定索引的元素。

兼容性与 STL 兼容: std::array 可以与标准模板库(STL)无缝协作,这使得在需要容器的场合下,使用 std::array 可以更轻松地利用 STL 的强大功能。
内存性能内存分配: std::array 在栈上分配内存(与普通数组相同),通常比动态分配更高效,避免了动态内存管理(如使用 new 和 delete)的开销。
复制和赋值: std::array 可以很方便地进行复制和赋值操作,符合现代 C++ 的行为。
int main() {
    // 创建一个 std::array,大小为 5(模板需要大小参数),元素类型为 int
    std::array<int, 5> arr = {1, 2, 3, 4, 5};

    // 访问元素
    std::cout << "第一元素: " << arr.at(0) << std::endl; // 使用 at() 访问或进行越界检查
    std::cout << "数组大小: " << arr.size() << std::endl; // 输出数组大小

    // 遍历数组,也可以使用迭代器
    for (const auto& element : arr) {
        std::cout << element << " "; // 输出每个元素
    }
    std::cout << std::endl;
    return 0;
}

vector:

vector 是一个动态数组类。实现了自动增长、随机访问等功能。提供了一些方便的成员函数来管理元素,如 push_back、pop_back、erase 等。vector一些方法的区别

1、 size 和 capacity 的区别
size: 指当前 vector 中实际存储的元素数量,通过 vector.size() 方法。
capacity: 指 vector 在不需要重新分配内存的情况下可以容纳的最大元素数量,vector.capacity() 方法。如果一个 vector 的 capacity 是 10,则在添加更多元素之前,它可以存储最多 10 个元素。超过这个数量时,vector 会自动扩展。
2、resize 和 reserve 的区别
resize: 改变 vector 的 size,使其包含指定数量的元素。如果新 size 比当前 size 大,vector 将添加新元素(如果没有指定值,则新元素默认初始化为0)如果新 size 比当前 size 小,vector 将删除多余的元素。

3 、push_back 和 emplace_back

末尾添加元素函数原型差别
push_back

void push_back(const T& value); 
void push_back(T&& value);

这个函数接受一个已经构造好的对象,

并将其复制(或移动)到 vector 的末尾。

需要先创建一个对象,然后将其“推入”到 vector 中。这意味着可能会发生一次复制或移动操作,那这就会影响性能
emplace_back

template <class... Args>
void emplace_back(Args&&... args);
这个函数接受构造对象所需的参数,

并直接在 vector 的末尾构造这个对象,

避免了不必要的复制或移动开销。

直接在 vector 内部构造对象,因此没有复制或移动的开销,性能更好。它采用构造对象所需的所有参数,并在内部使用这些参数创建对象。
std::vector<std::string> vec;
std::string str = "Hello, World!";
vec.push_back(str); // 复制操作
// 或者
vec.push_back("Hello, World!"); // 临时对象的复制
vec.emplace_back("Hello, World!"); // 直接在 vector 内部构造

map 和 unordered_map

在C++中,map 和 unordered_map 都是用于存储键值对数据结构的容器,主要区别如下

mapunordered_map
map 是基于红黑树实现的,保持元素有序。unordered_map 是基于哈希表实现的,不保持元素的顺序。
map 的查找、插入和删除操作的时间复杂度为 O(log n),因为它需要维护元素的顺序。unordered_map 的平均查找、插入和删除操作时间复杂度为 O(1),但在最坏情况下可能会降至 O(n)(例如,哈希冲突非常严重)。 通常使用更多内存来存储哈希表的桶和处理冲突。
使用场景: 需要有序数据:较少的元素数量。需要频繁快速查找、插入和删除元素,且不关心顺序,
大规模数据集:在处理非常大的数据集时,

迭代器与指针:

在 C++ 中,迭代器和指针都是用于访问和遍历数据结构(如数组、链表、容器等)的工具

指针迭代器
是一种直接指向内存地址的变量,可以用来直接访问该地址上的数据。
语法:int* p = &var;(指向变量 var 的指针)
是一种抽象的数据类型,用于遍历容器(如 STL 中的容器:vector、list、map 等)。
迭代器与容器的具体实现无关,可以被看作是指向容器元素的一个“代理”。
语法:std::vector<int>::iterator it = vec.begin();(vec 是一个 vector 容器)
指针是基本数据类型,包含内存地址。迭代器是类类型的对象,可以有多种实现(如随机访问迭代器、双向迭代器等),通常重载了许多操作符,还可以包含额外的信息和功能。
直接操控内存,可以进行算术运算(如移动到下一个地址)。
可以指向任何类型的数据。如果不小心使用,指针可能会导致悬挂指针、内存泄漏和越界访问等问题。
提供统一的接口来访问不同类型的容器。
通常不允许进行算术运算,保持了对容器的抽象。由于抽象层次的提高,迭代器使用时更安全。例如,STL 提供的迭代器会处理边界条件。

 指针使用不好会产生野指针和悬挂指针:

野指针悬挂指针
是指向"不明"的、不确定的或已删除的对象的指针原本合法,但指向内存被释放了或者重新分配。现在的指向已非想要的结果

解决:声明后显示初始化,使用智能指针

解决:delete之后赋值nulllptr,使用智能指针

多态:

多态是一种面向对象编程中的基本概念,它允许不同的对象以相同的方式响应相同的消息或方法调用。在C++中,多态使得一个接口可以被不同的类实现,从而提高代码的灵活性和可扩展性。多态其实分为两种类型:编译时多态(静态多态)主要通过函数重载和运算符重载实现。在编译阶段,编译器决定调用哪个函数或操作符。运行时多态(动态多态):主要通过虚函数和继承实现,通过基类指针或引用来调用子类的重写函数实现。在运行时,根据对象的实际类型决定调用哪个函数。

虚函数:

虚函数是C++中实现多态的一种方式。虚函数是在基类中使用关键字 virtual 声明的成员函数。派生类重写虚函数,通过虚函数,基类指针或引用可以调用派生类中的重写函数。程序在运行时根据对象的实际类型选择调用哪个函数。

实现虚函数的关键在于虚函数表(vtable)和虚函数表指针(vptr):每个包含虚函数的都有一个虚函数表(Vtable),这是一个指针数组,存放该类的虚函数地址。对于每个对象实例,会有一个指向所在类的虚函数表的指针(内存中除了成员变量外额外一个虚指针)称为虚指针(Vptr)。

构造函数不可以是虚函数:构造函数主要用于初始化对象的状态。当一个类的对象被创建时,构造函数会被调用。编译器首先分配内存,然后调用构造函数来初始化对象。虚函数的机制依赖于虚函数表,而虚函数表的建立需要在调用构造函数之后才能完成,如果为虚函数会导致对象初始化(明确对象的类型)和多态机制矛盾(对象类型在构造时尚未确定。)。

析构函数需要是虚函数(继承体系):目的确保在删除对象时,通过将基类的析构函数声明为虚函数,基类指向派生类调用时,派生类的析构函数先被调用,然后再析构父类,确保所有资源都被正确释放,防止资源泄漏。

explicit、delete、default

explicit是一个关键字,它主要用于构造函数的声明。使用explicit可以避免某些不必要的类型转换,确保代码更加安全和可读。
隐式转换:在C++中,如果构造函数只有一个参数,编译器可以自动将该参数的类型转换为对象类型。这种行为有时会导致意想不到的错误。
 

class A
{
public:
    explicit A(int value)
    { // 加上explicit
        // 构造函数代码
    }
};
void function(A obj)
{
    // 函数代码
}
int main()
{
    A obj1(10); // 合法的构造
    // A obj2 = 20; // 不合法,编译错误
    function(A(30)); // 合法
    // function(40); // 不合法,编译错误
    return 0;
}

使用explicit可以使代码意图更加明确。程序员在创建对象时需要清楚地指定要创建的对象类型,而不是依赖于编译器的隐式转换。(适用于单个参数的构造函数)对于多个参数的构造函数,通常不需要使用explicit,因为它们不支持隐式转换。

delete和default关键词,用于管理类的构造函数、析构函数和拷贝/移动操作。delete 用于删除对象的某个特殊成员函数的默认实现,表示这个特定操作不能被使。(目地控制资源管理:如果一个类管理动态分配的资源(如内存),复制对象可能会导致资源重复释放的潜在问题。
举例单例模式:通常需要防止对象被复制以保证单例的唯一性。)

default 用于显式地请求编译器生成某些特殊成员函数的默认实现。自定义类:当你创建一个自定义类并希望使用编译器生成的成员函数时,可以明确地使用 default。

final:

final关键字主要用于控制类和虚函数的继承行为,确保某些类或函数不能被继承或重写。它通常和override关键字一起使用,可以显示指出该函数时覆盖基类的某个虚函数且不允许再被派生类覆盖

类型转换

static_cast静态转换:用于在相关类型之间进行转换,比如基本数据类型、类层次结构中的基类和派生类之间的转换。
特点: 检查基本的类型安全。可用于简单类型转换,比如 int 转 double。对于类类型,可用于向上转换(基类指针/引用指向派生类)和向下转换(派生类指针/引用指向基类),但向下转换需要确保类型安全。
dynamic_cast动态转换: 主要用于在类层次结构中安全地进行向下转换,确保转换的是有效类型。
特点: 需要类为多态(即至少有一个虚函数)。如果转换失败,返回 nullptr(对于指针)或抛出 std::bad_cast(对于引用
const_cast常量转换 :用于在常量和非常量之间转换,尤其是当你需要去掉指针或引用的常量性质时。
特点: 只能用于添加或去掉 const 或 volatile 限定符。不建议在未理解代码意图的情况下使用,因为这可能导致未定义行为。
reinterpret_cast重解释转换: 用于非常规的类型转换,允许你将一种类型的指针或引用转换为另一种完全不相关的类型。
特点: 基本上不进行任何类型安全检查。常用于低级别操作,比如将指针转换为整型,或者在与硬件相关的编程中。

volatile

volatile 关键字是一个用于类型修饰的关键字,主要用于告诉编译器特定的变量可能会被异步地改变。1)当一个变量被声明为 volatile,编译器在访问该变量时不会进行优化。这意味着每次读取该变量的值时,编译器都会从其实际内存位置重新读取,而不是使用缓存的值。2)用于多线程编程:在多线程程序中,一个线程可能会修改某个变量,而另一个线程需要读取这个变量。将该变量声明为 volatile 可以确保一个线程读取的值是最新的,防止因编译器优化导致读取到过期值。
3)与硬件寄存器交互:在嵌入式系统中,某些变量可能与硬件寄存器直接关联。使用 volatile 可以确保程序不会因为优化而漏掉对这些寄存器的读写操作。

volatile 关键字并不能替代线程同步机制(如锁),它只保证了变量的可见性,并不保证操作的原子性。在多线程程序中,除了使用 volatile,还应该考虑使用其他同步手段,如互斥锁(mutex)等,以确保程序的正确性和安全性。

RAII

RAII(Resource Acquisition Is Initialization)是一种在 C++ 中管理资源(如内存、文件句柄、线程等)的技术。这种设计模式确保了资源能够在对象的生命周期内被有效管理,并在对象被销毁时自动释放资源。

RAII 的基本原则: 资源获取: 当对象被创建时,它负责获取和管理所需的资源。
资源释放: 当对象的生命周期结束(即对象被销毁)时,它的析构函数会自动释放那些资源。
异常安全: RAII 确保即使在异常发生的情况下,资源也会被正确释放。
RAII 的实现步骤:定义类: 创建一个类,该类负责管理特定的资源。
构造函数: 在构造函数中获取和分配资源。析构函数: 在析构函数中释放资源。

class Resource {
public:
    // 构造函数:分配资源
    Resource() {
        data = new int[10];  // 动态分配内存
        std::cout << "Resource acquired.\n";
    }

    // 析构函数:释放资源
    ~Resource() {
        delete[] data;  // 释放内存
        std::cout << "Resource released.\n";
    }

private:
    int* data;  // 资源指针
};

int main() {
    {
        Resource res;  // 创建 Resource 对象,获取资源
        // 在这里可以使用资源(例如,填充数据等)
    }  // 资源对象的生命周期结束,自动释放资源

    // 这里,Resource 对象已经被销毁,资源也被释放
    return 0;
}

简化资源管理: 不必手动释放资源,降低内存泄漏的风险。
异常安全: 任何时候只要对象被销毁,资源就会被释放,避免了异常情况带来的资源泄露。
清晰的语义: 对象的生命周期与资源的管理相结合,使得代码更加清晰易懂。
两种RAII形式的锁管理类lock_guard和unique_lock

lock_guardunique_lock
简单且轻量级的所管理类,在构造时自动锁定互斥体(mutex),在析构时自动解锁互斥体。不可以显示解锁也不支持锁的转移(命周期完全与作用域绑定)提供更灵活的锁定功能。允许显示的锁定与解锁操作,和锁的转移(不可赋值),可以在在构造后手动锁定和解锁互斥体。可以在需要时临时解锁,再重新锁定。
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void printMessage() {
    std::lock_guard<std::mutex> lock(mtx); // 自动锁定
    std::cout << "Hello from thread!" << std::endl;
} // 自动解锁

int main() {
    std::thread t1(printMessage);
    std::thread t2(printMessage);
    t1.join();
    t2.join();
    return 0;
}
std::mutex mtx;
void printMessage() {
    std::unique_lock<std::mutex> lock(mtx); // 锁定
    std::cout << "Hello from thread!" << std::endl;
    lock.unlock(); // 手动解锁
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 做一些事情
    lock.lock(); // 重新锁定
    std::cout << "Thread finished work!" << std::endl;
} 

int main() {
    std::thread t1(printMessage);
    std::thread t2(printMessage);
    t1.join();
    t2.join();
    return 0;
}

都是用于管理互斥量(mutex)的类,它们可以帮助我们避免死锁和资源泄漏。尽管它们都用于锁定互斥体

thread:

thread 是用于创建和管理线程的一个类。在使用 std::thread 时,通常会用到 join 和 detach 函数来处理线程的生命周期。

joindetach
join 方法会使调用线程(通常是主线程)等待被调用的线程完成执行。当一个线程调用 join 时,它会阻塞,直到被调用线程结束。其实是一种同步机制,调用 join 后,线程会处于“可合并”状态,直到其执行完毕。一旦线程被 join,该线程的资源会被清理(即被销毁)

detach 方法会将线程与调用线程分离,使调用线程不再等待该线程的完成。被分离的线程在后台运行直到完成,独立于调用线程的生命周期,并且会在线程结束时自动清理资源。调用 detach 后,主线程和它无法再通信程,也不能再调用 join。

如果主线程在 detach 线程完成之前退出,程序将会终止,可能会导致未定义行为。尽量避免在使用 detach 的线程中访问主线程的资源,以避免悬空指针或数据竞争等问题。

#include <iostream>
#include <thread>
#include <chrono>
void task() {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟工作
    std::cout << "Task completed!" << std::endl;
}
int main() {
    // 使用 join
    std::thread t1(task);
    t1.join(); // 等待线程 t1 完成
    std::cout << "Thread t1 has joined." << std::endl;

    // 使用 detach
    std::thread t2(task);
    t2.detach(); // 线程 t2 在后台运行
    std::cout << "Thread t2 has detached." << std::endl;
    // 等待一段时间,让 t2 有机会完成
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "Main thread ended." << std::endl;
    return 0;
}

那如何设计一个线程安全的类:涉及到多线程编程的几个核心概念,包括互斥量、条件变量和原子操作等。

#include <iostream>
#include <mutex>
#include <thread>
//引入标准库
class ThreadSafeCounter {
public:
    ThreadSafeCounter() : count(0) {}
    // 增加计数
    void increment() {
        std::lock_guard<std::mutex> lock(mutex_); // 加锁
        ++count; // 修改共享数据
    }
    // 获取计数
    int getCount() {
        std::lock_guard<std::mutex> lock(mutex_); // 加锁
        return count; // 返回共享数据
    }
private:
    int count; // 共享数据
    std::mutex mutex_; // 互斥量
};
void incrementCounter(ThreadSafeCounter& counter) {
    for (int i = 0; i < 1000; ++i) {
        counter.increment();
    }
}

int main() {
    ThreadSafeCounter counter;

    std::thread t1(incrementCounter, std::ref(counter));
    std::thread t2(incrementCounter, std::ref(counter));
    
    t1.join(); // 等待线程t1完成
    t2.join(); // 等待线程t2完成

    std::cout << "Final count: " << counter.getCount() << std::endl; // 输出最终计数
    return 0;
}

原子操作代替互斥锁

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter(0); // 声明一个原子变量
void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter++; // 原子加法
    }
}
int main() {
    const int num_threads = 2;
    std::vector<std::thread> threads;

    // 创建多个线程
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(increment);
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter.load() << std::endl; // 输出结果
    return 0;
}

 原子操作:一个操作是原子的,即它要么完全执行,要么完全不执行,不会受到其他线程的干扰。可以使用std::atomic修饰基本类型,它通过CPU提供的原子指令来实现这些不可分割的操作,现在CPU会提供一组指令,比如CMPXCHG,XADD等原子操作的读或写,虽然在某些场景下可以替代锁,比如一些基本的计算器或标志位,但复杂场景下锁还是较优选择。

memcpy 和 memmove  

memcpy 和 memmove 都是 C 和 C++ 中用于内存拷贝的函数

memcpy memmove

void* memcpy(void* dest, const void* src, size_t n);

功能:从源地址 src 拷贝 n 字节到目标地址 dest。

void* memmove(void* dest, const void* src, size_t n);

也从源地址 src 拷贝 n 字节到目标地址 dest,但可以处理重叠的内存区

不支持重叠的内存区域。如果源和目标区域重叠,使用 memcpy 可能导致未定义行为,例如,若源地址在目标地址之前,拷贝的内容可能会被覆盖,导致错误的结果。不处理重叠效率就高支持重叠的内存区域。如果源和目标区域重叠,memmove 会以安全的方式处理拷贝,确保数据不会被错误覆盖。可能需要先检查重叠情况,所以效率会低一些
#include <cstring>
#include <iostream>
int main() {
    char src[] = "Hello, World!";
    char dest[20];

    // 使用 memcpy
    std::memcpy(dest, src, 13);
    std::cout << "Using memcpy: " << dest << std::endl;

    // 创建一个重叠的情况
    char overlap[] = "Hello, World!";
    std::memmove(overlap + 7, overlap, 6); // 把"Hello,"移动到"World!"前面
    std::cout << "Using memmove with overlap: " << overlap << std::endl;
    return 0;
}

还有一个专门针对字符串赋值的函数strcpy: 用于复制一个字符串(包括结束的空字符 \0)。   char* strcpy(char* dest, const char* src);  它会将 src 指向的字符串复制到 dest 中,并在最后添加一个空字符来标识字符串的结束。它适用于处理以空字符结尾的字符串,否则可能导致缓冲区溢出,如果 src 的长度超过了 dest 的分配空间。为了安全起见,可以使用 strncpy,指定最大拷贝长度。 

function、bind和lambda

function、bind和lambda都是处理函数和可调用对象的重要工具。

1、std::function:是C++标准库中的一个类模板,用于封装任何可调用对象(如普通函数、函数指针、成员函数、Lambda 表达式等)。
适用场景:1) 存储函数指针:当你需要将不同的函数存储在同一个容器中时,可以使用std::function。 2)  回调机制:可以用于实现回调函数,提供灵活性。3)多态性:允许将不同类型的可调用对象统一处理。

void sayHello() {
    std::cout << "Hello, World!" << std::endl;
}

int main() {
    std::function<void()> func = sayHello; // 使用std::function
    func(); // 调用
    return 0;
}

2、std::bind: 允许你绑定(或固定)函数的参数,生成新的可调用对象。
适用场景:1)参数绑定:当你想提前固定某些参数,或改变参数的顺序时。
2)适配器模式:将一个函数转变为另一个可以接受不同参数的形式。
3)简化代码:在需要传递函数作为参数时,可以减少需要传递的参数数量。

void printSum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    auto boundFunc = std::bind(printSum, std::placeholders::_1, 10); // 绑定第二个参数
    boundFunc(5); // 只需提供一个参数
    return 0;
}

 3、 Lambda 表达式:是一种在C++11引入的方式,用于定义匿名函数。
适用场景:1)简洁性:在需要快速定义小函数时,使用Lambda更加简洁。
2)临时用途:适用于临时计算,避免创建单独的函数。
3)捕获上下文:可以捕获周围的变量,可以在函数体内直接使用这些变量。

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::endl;
    });
    return 0;
}

回调函数

回调函数是指作为参数传递给另一个函数的函数。实际上是把函数的调用权从一个地方转移到另一个地方,可以在某些事件发生或特定条件满足时调用这个回调函数。回调函数在C++中非常有用,主要用于处理异步操作、事件通知和自定义行为。

异步编程:回调函数允许程序在等待某些操作(如文件读取、网络请求等)完成时继续执行其他操作。当操作完成时,程序会调用回调函数来处理结果。

事件驱动编程:在图形用户界面(GUI)中,程序通过事件(如按钮点击)驱动,而回调函数可以定义对这些事件的响应。

解耦合:回调函数使程序的不同部分之间的耦合度降低。可以在不修改主逻辑的情况下,轻松更改或添加功能。

代码复用:通过使用回调函数,可以将通用逻辑与具体实现分离,从而更好地复用代码。

// 定义一个函数类型的别名
using Callback = std::function<void(int)>;
// 一个接受回调函数的函数
void performOperation(int value, Callback callback) {
    // 执行某些操作
    value *= 2; // 将值乘以2
    // 调用回调函数
    callback(value);
}

// 一个简单的回调函数
void myCallback(int result) {
    std::cout << "Callback called with result: " << result << std::endl;
}

int main() {
    // 调用performOperation,并传入myCallback作为回调
    performOperation(5, myCallback);
    return 0;
}

模板

C++模板是C++中的一种强大特性,它允许编写与类型无关的代码,使得同一段代码可以处理不同类型的数据。模板分为函数模板和类模板

函数模板类模板

函数模板允许定义一个通用的函数,能够处理不同类型的参数。

template <typename T>
T add(T a, T b) {
    return a + b;
}

类模板允许定义一个通用的类,可以处理不同类型的成员变量。

template <typename T>
class Box {
private:
    T item;
public:
    void setItem(T item) {
        this->item = item;
    }
    T getItem() {
        return item;
    }
};

优点:
代码重用:通过模板,可以避免重复编写对不同类型的相似代码,增加代码的重用性与通用性。
类型安全:使用模板可以在编译时确保类型安全,减少运行时错误,因此在一些情况下性能可能更高。
简化接口:通过模板,可以提供统一的接口来处理不同类型,简化了用户的使用。
缺点
编译时间:由于模板代码在编译时实例化,可能导致编译时间显著增加。
代码膨胀:每种不同的实例化类型都会生成一份代码,可能导致可执行文件的大小增加。
错误信息复杂:如果模板代码出现错误,编译器生成的错误信息通常比较复杂,难以理解。
调试难度:模板代码的调试可能比较困难,尤其是当使用了深层次的模板嵌套和复杂的类型推导时。
可能的限制:某些情况下,模板可能会受到特定类型特性(如拷贝构造函数、赋值操作符等)的限制。

栈与堆内存

栈内存堆内存
分配方式由编译器自动管理。在函数调用时分配,并在函数结束时释放。存储局部变量和函数参数。由程序员手动管理。使用 new 关键字分配,使用 delete 释放。适用于动态分配内存,需要在程序运行时确定大小。
生命周期生命周期与函数调用的生存期相同。
当函数返回时,所有栈内存被自动释放。
生命周期由程序员控制。
需要手动释放,若未释放将导致内存泄漏。
存储限制一般较小,通常为几MB(取决于系统)
适合存储小型对象
较大,通常受限于系统物理内存。
可用于存储大型或不确定大小的对象
访问访问速度较快,因为栈是连续的内存区域,存储的变量是直接可访问的访问速度较慢,内存分配和释放涉及更复杂的管理,需要通过指针访问

栈内存:自动管理,存储局部变量,速度快,存储空间小,生命周期短。
堆内存:手动管理,用于动态分配,速度较慢,存储空间大,生命周期长。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2194049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

跨设备剪贴板同步服务ClipCascade

什么是 ClipCascade &#xff1f; ClipCascade 是一款开源的轻量级工具&#xff0c;可以自动同步您的剪贴板在多个设备之间&#xff0c;无需按键。它确保设备之间无缝的剪贴板共享&#xff0c;并以端对端加密优先保护隐私。无论您是在不同工作站之间切换&#xff0c;还是仅仅希…

检索增强思考 RAT(RAG+COT):提升 AI 推理能力的强大组合

在人工智能领域&#xff0c;大型语言模型&#xff08;LLMs&#xff09;已经取得了显著的进展&#xff0c;能够生成类似人类的文本并回答各种问题。然而&#xff0c;它们在推理过程中仍面临一些挑战&#xff0c;例如缺乏对事实的准确把握以及难以处理复杂的多步骤问题。为了解决…

Unity3D 单例模式

Unity3D 泛型单例 单例模式 单例模式是一种创建型设计模式&#xff0c;能够保证一个类只有一个实例&#xff0c;提供访问实例的全局节点。 通常会把一些管理类设置成单例&#xff0c;例如 GameManager、UIManager 等&#xff0c;可以很方便地使用这些管理类单例&#xff0c;…

用YOLO和LLM增强的OCR

虽然最近我花了很多时间在大型语言模型 (LLM) 上进行实验&#xff0c;但我对计算机视觉的热情始终未减。因此&#xff0c;当我有机会将两者融合在一起时&#xff0c;我迫不及待地想要立即开始。在 Goodreads 上扫描书籍封面并将其标记为已读一直感觉有点神奇&#xff0c;我很兴…

SSM外卖点餐软件APP-计算机毕业设计源码30768

目 录 摘要 1 绪论 1.1 研究背景 1.2研究目的 1.3论文结构与章节安排 2 外卖点餐软件APP系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 操作可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能…

这些编程工具竟然能让我效率翻倍?开发者必备神器盘点!

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

基于深度学习多层感知机进行手机价格预测

数据集介绍 数据集采用了Kaggle实战数据集,链接如下,如有需要可自行下载 https://www.kaggle.com/datasets/atefehmirnaseri/cell-phone-price/data 数据集简要介绍 • battery_power&#xff1a;电池的总能量存储&#xff08;毫安时&#xff09; • blue&#xff1a;设备…

人工智能对未来工作影响的四种可能性

随着人工智能&#xff08;AI&#xff09;技术的迅速发展&#xff0c;其对人类工作的影响已成为讨论的热点话题。我们经常听到有关AI威胁论的观点&#xff0c;担心它将取代人类工作&#xff0c;但也有专家认为AI将成为一种辅助工具&#xff0c;帮助人类提升工作效率。宾夕法尼亚…

嵌入式硬件设计

嵌入式硬件设计是指针对嵌入式系统&#xff08;一种专用的计算机系统&#xff0c;通常嵌入到其他设备中&#xff09;进行的硬件设计工作。嵌入式系统广泛应用于消费电子、工业控制、医疗设备、汽车电子、航空航天等领域。以下是嵌入式硬件设计的主要内容和步骤&#xff1a; 1.…

括号匹配——(栈实现)

题目链接 有效的括号https://leetcode.cn/problems/valid-parentheses/description/ 题目要求 样例 解题代码 import java.util.*; class Solution {public boolean isValid(String str) {Stack<Character> stacknew Stack<>();for(int i0;i<str.length();i)…

传统流程图和N-S流程图的区别

传统流程图和N-S流程图在表示算法和逻辑结构时有不同的特点和用途。以下是它们的主要区别&#xff1a; ### 传统流程图 1. **符号多样**&#xff1a;传统流程图使用多种几何形状表示不同的操作类型&#xff0c;如椭圆表示开始和结束&#xff0c;平行四边形表示输入输出&#…

JumperServer入门

一、安装部署 官方安装文档&#xff1a;快速入门 - JumpServer 文档 机器准备 CentOS7 ip 角色 192.168.252.145 主节点 192.168.252.146 被控节点1 192.168.252.148 被控节点2 安装JumperServer curl -sSL https://resource.fit2cloud.com/jumpserver/jumpserver…

数据结构——七种排序(java)实现

文章目录 直接插入排序希尔排序选择排序冒泡排序快速排序归并排序计数排序 直接插入排序 思想&#xff1a; /*** 直接插入排序* 具有稳定性* 时间复杂度为&#xff1a;&#xff08;计算时间复杂度的时候应计算执行次数最多的语句类&#xff0c;在直接插入排序中次数最多的语句…

【AI大模型】深入Transformer架构:编码器部分的实现与解析(下)

目录 &#x1f354; 编码器介绍 &#x1f354; 前馈全连接层 2.1 前馈全连接层 2.2 前馈全连接层的代码分析 2.3 前馈全连接层总结 &#x1f354; 规范化层 3.1 规范化层的作用 3.2 规范化层的代码实现 3.3 规范化层总结 &#x1f354; 子层连接结构 4.1 子层连接结…

环境对于写作有何影响?

如果你是有灵性、热爱文学创作的人&#xff0c;多半就会喜欢安静的生活环境。因为你会感受到唯有在这样的环境里更才能够沉下心来思考创作的路径。而且此时的你&#xff0c;显得头脑清醒、思维活跃而自由&#xff0c;因之文思泉涌。 网络图&#xff1a;宁静的书房 反之&#x…

快递物流跟踪:掌握最后更新时间,高效筛选单号管理

在现代社会&#xff0c;快递物流已成为人们日常生活中不可或缺的一部分&#xff0c;无论是网购商品还是寄送文件&#xff0c;都离不开快递服务。然而&#xff0c;随着快递单量的不断增加&#xff0c;如何有效跟踪快递物流信息&#xff0c;特别是掌握最后更新时间&#xff0c;并…

SSM湘农乐市农产品交易平台-计算机毕业设计源码28246

目 录 SSM湘农乐市农产品交易平台 1 绪论 1.1研究背景 1.2研究意义 1.3研究方法 1.4论文结构与章节安排 2 湘农乐市农产品交易平台系统分析 2.1 可行性分析 2.2 系统流程分析 2.3 系统功能分析 2.4 系统用例分析 2.5本章小结 3 湘农乐市农产品交易平…

通信工程学习:什么是RIP路由信息协议

RIP&#xff1a;路由信息协议 RIP&#xff08;Routing Information Protocol&#xff09;路由信息协议是一种基于距离矢量算法的内部网关协议&#xff08;IGP&#xff09;&#xff0c;主要用于在自治系统&#xff08;AS&#xff09;内部进行路由信息的交换和传播。以下是关于RI…

第6篇:三大渗透测试框架权限维持技术

0x00 前言 在渗透测试中&#xff0c;有三个非常经典的渗透测试框架----Metasploit、Empire、Cobalt Strike。 那么&#xff0c;通过漏洞获取到目标主机权限后&#xff0c;如何利用框架获得持久性权限呢&#xff1f; 0x01 MSF权限维持 使用MSF维持权限的前提是先获得一个met…

SpringBoot驱动的明星周边产品电商解决方案

1系统概述 1.1 研究背景 如今互联网高速发展&#xff0c;网络遍布全球&#xff0c;通过互联网发布的消息能快而方便的传播到世界每个角落&#xff0c;并且互联网上能传播的信息也很广&#xff0c;比如文字、图片、声音、视频等。从而&#xff0c;这种种好处使得互联网成了信息传…