个人C++复习知识点(2)

C++中jthread和thread的区别是什么

在 C++ 中,jthread 和 std::thread 都是用于多线程编程的类,但它们之间有一些重要的区别。jthread 是 C++20 引入的新特性,旨在简化线程管理和提高线程安全性。以下是它们之间的主要区别:

1. 自动管理线程生命周期

  • std::thread: 需要手动管理线程的生命周期。你必须确保在 std::thread 对象的生命周期结束之前调用 join() 或 detach(),否则会导致程序抛出异常。

    std::thread t([]{ /* 工作逻辑*/ });
    // 必须调用 join 或 detach
    t.join(); // 或 t.detach();
    

  • jthread: 自动管理线程的生命周期。当 jthread 对象超出作用域时,它会自动调用 join(),从而避免了忘记调用 join() 或 detach() 的问题。

    std::jthread jt([]{ /* 工作逻辑 */ });
    // jt 会在超出作用域时自动调用 join
    

2. 支持中断

  • std::thread: 不直接支持线程的中断。你需要自己实现某种机制,例如使用标志位来通知线程停止工作。

  • jthread: 提供了一个中断机制。可以通过调用 request_stop() 来请求线程停止,线程可以检查中断状态并安全地终止。

    std::jthread jt([](std::stop_token st) {
        while (!st.stop_requested()) {
            // do work
        }
    });
    

3. 传递停止标记

  • std::thread: 线程函数的参数需要手动传递。

  • jthread: 线程函数可以接受一个 std::stop_token,允许线程在需要时检查是否请求停止。

4. 头文件

  • std::thread: 定义在 <thread> 头文件中。

  • jthread: 也定义在 <thread> 头文件中,但需要 C++20 的支持。

总结

  • std::thread 适用于需要手动管理线程生命周期的传统多线程编程。
  • jthread 提供了更高层次的抽象,简化了线程管理,并引入了中断机制,使得编写和管理线程的代码更加安全和简洁。

下面是使用中断的示例代码:

void myTask(std::stop_token stoken) {
    while (!stoken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Running task..." << std::endl;
    }
}

int main() {
    std::jthread jt(myTask);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    jt.request_stop();  // 请求中断
    return 0;
}

 需要注意的是虽然jthread提供了一些改善中断的接口,但是在标准库中没有真正的中断功能。

memcpy和memmove有什么区别

memcpy 和 memmove 是 C 和 C++ 中用于内存拷贝的两个函数,它们在功能上相似,但在处理内存重叠区域时存在重要区别。以下是它们的主要区别:

1. 函数原型

  • memcpy:

    void *memcpy(void *dest, const void *src, size_t n);
    
     
    • 将 src 指向的内存区域中的 n 字节复制到 dest 指向的内存区域。
  • memmove:

    void *memmove(void *dest, const void *src, size_t n);
    
    • 也将 src 指向的内存区域中的 n 字节复制到 dest 指向的内存区域,但可以安全地处理内存重叠的情况。

2. 内存重叠处理

  • memcpy:

    • 如果 src 和 dest 指向的内存区域重叠,memcpy 的行为是未定义的。这意味着在重叠的情况下,结果可能是错误的,甚至可能导致数据损坏。
  • memmove:

    • memmove 可以安全地处理内存重叠的情况。它会根据重叠的情况决定是从后向前复制还是从前向后复制,以确保数据的正确性。

3. 性能

  • memcpy:

    • 通常比 memmove 快,因为 memcpy 不需要检查内存重叠的情况,且可以使用更高效的拷贝算法。
  • memmove:

    • 由于需要处理重叠情况,memmove 的性能通常稍慢于 memcpy

4. 使用场景

  • 使用 memcpy 时,确保源和目标内存区域不重叠,且在性能要求较高的情况下更常用。
  • 使用 memmove 时,可以放心地处理可能重叠的内存区域,适用于需要安全拷贝的场景。

示例

以下是一个简单的示例,展示了这两个函数的使用:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[50] = "Hello, World!";
    char str2[50];

    // 使用 memcpy
    memcpy(str2, str1, strlen(str1) + 1);
    printf("memcpy: %s\n", str2);

    // 使用 memmove
    memmove(str1 + 7, str1, strlen(str1) + 1); // 重叠的情况
    printf("memmove: %s\n", str1); // 输出: "Hello, Hello, World!"

    return 0;
}

在这个示例中,memcpy 用于简单的拷贝,而 memmove 用于处理重叠内存区域的拷贝。

C++中function和bind和lambda表达式的使用场景

在 C++ 中,std::functionstd::bind 和 lambda 表达式都是用于处理可调用对象的工具,它们各自有不同的使用场景和优缺点。下面分别介绍这三者的特点和适用场景。

1. std::function

std::function 是一个通用的可调用对象包装器,可以存储、复制和调用任何可调用目标(例如函数指针、lambda 表达式、函数对象等)。它的主要特点是类型安全和灵活性。

使用场景:
  • 存储不同类型的可调用对象:当你需要在一个容器中存储多种不同类型的可调用对象时,std::function 是一个很好的选择。

  • 回调函数:在需要传递回调函数的场合,比如事件处理、异步编程等。

  • 简化函数指针的使用:比起直接使用函数指针,std::function 提供了更好的类型安全性和易用性。

  • 回调函数,作为函数参数和返回值

示例代码:
#include <iostream>
#include <functional>

void callback(int x) {
    std::cout << "Callback called with value: " << x << std::endl;
}

int main() {
    std::function<void(int)> func = callback;
    func(42);  // 调用回调函数
    return 0;
}

2. std::bind

std::bind 用于将函数或可调用对象与其参数绑定,从而生成一个新的可调用对象。它可以用于调整参数的顺序、固定某些参数等。

使用场景:
  • 固定参数:当你想要创建一个新函数,其中某些参数已经被固定时。

  • 调整参数顺序:在需要改变参数顺序的情况下,可以使用 std::bind

  • STL 算法结合:在使用 STL 算法时,可以通过 std::bind 生成符合算法要求的可调用对象。

  • 也就是将已有的函数适配为适用于接口要求的函数,或者是将成员函数和对象进行绑定

示例代码:
#include <iostream>
#include <functional>

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

int main() {
    auto add_five = std::bind(add, 5, std::placeholders::_1);
    add_five(10);  // 输出: Sum: 15
    return 0;
}

lambda表达式

Lambda 表达式是 C++11 引入的一种简洁的定义可调用对象的方式。它允许你在需要的地方定义匿名函数,并捕获外部作用域中的变量。

使用场景:
  • 简洁性:当你需要一个简单的函数对象时,使用 lambda 表达式可以减少代码量,提高可读性。

  • 局部性:当函数只在一个特定的上下文中使用时,使用 lambda 可以避免全局函数的定义。

  • 捕获外部变量:当你需要在函数内部使用外部变量时,lambda 表达式提供了一种方便的方式。

示例代码:
#include <iostream>
#include <vector>
#include <algorithm>

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

    // 使用 lambda 表达式计算和
    int sum = 0;
    std::for_each(numbers.begin(), numbers.end(), [&sum](int n) {
        sum += n;
    });

    std::cout << "Sum: " << sum << std::endl;  // 输出: Sum: 15
    return 0;
}

总结

  • std::function 适用于需要存储和传递多种可调用对象的场合。

  • std::bind 适用于需要固定某些参数或调整参数顺序的情况。

  • Lambda 表达式 适用于需要简洁、局部的可调用对象,并且可以方便地捕获外部变量的场合。

  • 通常在短期和局部使用函数时比如一次性回调函数,算法库中的自定义操作等

选择哪种工具取决于具体的需求和上下文。

C++中函数模板和类模板有什么区别

定义和使用:

函数模板:用于创建可以接收不同类型参数的函数,定义一个一次性的模板,可以生成多个不同的函数版本。

例子:

template <typename T>
T max(T a, T b) {
 return (a > b) ? a : b;
}

这个max函数可以接收所有能够支持>运算符的类型。

类模板:用于创建可以接收不同参数类型的类。通过创建一个模板类,可以生成多个不同类型的类实例。

template <typename T>
class Stack {
private:
 std::vector<T> elements;
public:
 void push(T const& elem) { elements.push_back(elem); }
 void pop() { elements.pop_back(); }
 T top() const { return elements.back(); }
};

 在上面的这个例子中Stack这个类可以处理不同类型的堆栈元素。

实例化方式

函数模板:编译器在实际调用函数时,根据传入参数的类型自动生成具体类型的函数

int a = 10, b = 20;
std::cout << max(a, b); // 调用 max<int>(int a, int b)

类模板:在使用类模板的时候需要显示地声明模板类型

Stack<int> intStack;
Stack<double> doubleStack;

扩展知识

我们也需要了解一些高级概念:

1)模板特化: 模板特化是指我们可以为特定的类型定义一个专门的模板版本,对某些情况提供更合适的实现。

template <>
class Stack<bool> {
    // bool类型的特化实现
};

2)模板偏特化: 偏特化使我们为模板的某些参数提供特定类型,但同时保留其他参数的泛型性质。

template <typename T, typename Allocator = std::allocator<T>>
class MyContainer {
    //
};

另外,模板编程涉及到编译期计算,即所有模板实例化在编译阶段完成,这意味着与运行时相比,模板代码在执行期没有额外的开销,性能更优。

C++模板中的SFINAE是什么,它的原则是什么

C++模板中的SFINAE是Substitution Failure Is Not An Error的缩写,这句英文的意思是类型替换失败并不是一个编译错误。SFINAE的核心思想是,当在模板实例化过程中出现类型替换失败时,并不会导致编译错误,而是允许编译器继续寻找其他合适的模板特化或重载。

SFINAE的基本概念

  1. 模板实例化:在C++中,模板是一个蓝图,可以生成不同类型的类或函数。当使用模板时,编译器会根据提供的类型参数实例化相应的模板。

  2. 替换失败:在模板实例化过程中,如果某个类型替换导致了不合法的类型(例如,试图调用一个不存在的成员函数),根据SFINAE原则,这种失败不会导致编译错误,而是会被忽略,编译器会继续尝试其他可能的模板。

  3. 特化和重载:利用SFINAE,开发者可以创建多个重载的函数或特化的模板,以根据不同的类型参数提供不同的实现。

SFINAE的原则

SFINAE的原则可以总结为以下几点:

  1. 错误不是致命的:在模板替换过程中,如果发生类型替换失败,编译器不会报错,而是继续尝试其他候选模板。这使得模板的选择更加灵活。

  2. 类型萃取:SFINAE常与类型萃取(type traits)结合使用,可以根据类型的特性来选择不同的模板实现。这通常通过std::enable_if和其他类型特征来实现。

  3. 提供更好的错误信息:由于SFINAE允许模板选择的灵活性,开发者可以在编译时获得更清晰的错误信息,帮助理解模板的使用。

示例

下面是一个简单的示例,展示如何使用SFINAE来选择不同的模板实现:

#include <iostream>
#include <type_traits>

// 通用模板
template<typename T, typename Enable = void>
class MyClass;

// 当T是整型时的特化
template<typename T>
class MyClass<T, typename std::enable_if<std::is_integral<T>::value>::type> {
public:
    void print() {
        std::cout << "Integral type" << std::endl;
    }
};

// 当T是浮点型时的特化
template<typename T>
class MyClass<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
public:
    void print() {
        std::cout << "Floating point type" << std::endl;
    }
};

int main() {
    MyClass<int> intObj;       // 将实例化整型特化
    intObj.print();           // 输出: Integral type

    MyClass<double> doubleObj; // 将实例化浮点型特化
    doubleObj.print();        // 输出: Floating point type

    // MyClass<std::string> strObj; // 这将不会导致编译错误,只是没有匹配的特化

    return 0;
}

函数模板也是一样的:

#include <type_traits>
#include <iostream>

template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type
printIfInteger(T t) {
    std::cout << t << " is an integer.\n";
}

template<typename T>
typename std::enable_if<!std::is_integral<T>::value>::type
printIfInteger(T t) {
    std::cout << t << " is not an integer.\n";
}

int main() {
    printIfInteger(42);         // 输出:42 is an integer.
    printIfInteger(3.14);       // 输出:3.14 is not an integer.
}

通过enable_if和is_integral能够实现更加高级的类型检测,假如我们具有一个函数模板需要这个函数模板在特定的情况下才起到作用此时这个SFINAE就起到作用了。对类模板也是一样的。

C++中的栈溢出是什么

栈溢出(Stack Overflow)是在程序执行过程中,栈空间被耗尽的一种现象。

栈空间是操作系统为每个线程分配的有限内存,用于存储函数调用、局部变量等。当递归过深(例如无限递归)或局部变量占用内存过大时,栈空间就会被用尽,导致栈溢出。

在 C++ 中,典型的栈溢出情况包括:

  1. 无限递归调用:函数不断调用自身,导致栈帧无限增长。

  2. 大局部变量:定义了过多或过大的局部变量,超过了栈内存的限制。

  3. 直接使用自身对象:如果在拷贝构造函数中直接使用自身对象作为参数来创建新的对象,这将导致递归调用自身的拷贝构造函数。例如: 

    class MyClass {
    public:
        MyClass() {
            // 默认构造函数
        }
    
        MyClass(const MyClass& other) {
            // 错误:直接使用自身对象,导致递归
            MyClass obj(other); // 这里会调用拷贝构造函数,导致递归
        }
    };
    
    int main() {
        MyClass obj1;       // 调用默认构造函数
        MyClass obj2 = obj1; // 调用拷贝构造函数,导致递归
        return 0;
    }
    
  4. 使用 *this 作为参数:在拷贝构造函数中,如果使用 *this(当前对象的引用)作为参数,也会导致递归调用:

    class MyClass {
    public:
        MyClass(const MyClass& other) {
            // 错误:使用自身对象,导致递归
            MyClass obj(*this); // 这里会导致递归调用
        }
    };
    
    int main() {
        MyClass obj1;       // 调用默认构造函数
        MyClass obj2 = obj1; // 调用拷贝构造函数,导致递归
        return 0;
    }
    
  5. 不当的成员对象拷贝:如果一个类的成员对象的拷贝构造函数也调用了其自身的拷贝构造函数,可能会导致递归。例如:

    class OtherClass {
    public:
        OtherClass(const OtherClass& other) {
            // 错误:递归调用自身
            OtherClass obj(other); // 这里会导致递归调用
        }
    };
    
    class MyClass {
    public:
        OtherClass member;
    
        MyClass(const MyClass& other) : member(other.member) {
            // 这里调用了 OtherClass 的拷贝构造函数
        }
    };
    

其实后面的三种说白了也就是第一种,只不过我这里为了避免我自己忘记了,所以加上用于复习的。

和栈相关的知识点 

1)堆和栈的区别:

  • 栈内存是自动分配和释放的,速度快但空间有限。适用于较小的局部变量和函数调用。
  • 堆内存是手动管理的,使用 new 和 delete 进行分配和释放。它的空间较大,适合动态分配的大对象或数组。

2)递归调用的优化:

  • 尾递归优化:在尾递归中,递归调用是函数最后一个执行的操作。编译器可以将尾递归优化为迭代,避免栈溢出问题。但需要注意,并非所有编译器都支持尾递归优化(这个优化主要取决于编译器)。
  • 尾递归调用的例子
    #include <iostream>
    
    int factorial_tail_recursive(int n, int accumulator = 1) {
        if (n == 0) {
            return accumulator;
        }
        return factorial_tail_recursive(n - 1, n * accumulator); // 尾递归
    }
    
    int main() {
        int result = factorial_tail_recursive(5);
        std::cout << "Factorial: " << result << std::endl; // 输出: Factorial: 120
        return 0;
    }
    

3)常见的解决方法:

  • 使用动态内存分配:对于需要大内存的局部变量,考虑使用堆代替栈。例如,通过 new 动态分配而不是直接定义在栈上。
  • 控制递归深度:通过增加递归深度限制条件,避免无限递归的发生。
  • 增大栈空间:在某些情况下,可以通过操作系统或编译器选项增大栈的大小。

4)错误处理:

  • 检查栈溢出的线索,包括异常崩溃、Segmentation Fault(段错误)等。这些错误往往是栈溢出的直接结果。
  • 使用调试工具,例如 Valgrind、GDB 等,可以帮助追踪和定位栈溢出问题。

 希望这篇博客能对阅读的您有所帮助,发现了错误欢迎指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值