【面试题】C++新特性(第六篇)

1.C++新标准

C++新标准是指C++编程语言的一系列更新和改进,这些标准为开发者提供了更多的工具和特性,使得C++成为一门更加现代化、高效且灵活的编程语言。以下是C++新标准的主要内容和特点,按照发布年份进行概述:

  1. C++11

    (发布于2011年):

    • 是C++98之后最具里程碑意义的版本。

    • 引入了许多现代C++的特性,如智能指针、多线程、并发编程、constexpr函数、lambda表达式、自动类型推导等。

    • 扩充了C++核心语言的功能,如右值引用和move语义、外部模板、初始化列表等。

    • 增强了C++标准程序库,包括线程支持、正则表达式、通用智能指针等。

  2. C++14

    (发布于2014年):

    • 是对C++11的小改进,主要集中在语言本身的修补和优化。

    • 放宽了constexpr函数的限制,增强了decltype关键字,并支持了lambda表达式的泛型编程。

  3. C++17

    (发布于2017年):

    • 在C++14的基础上进一步发展,引入了更多新特性。

    • 包括结构化绑定、if constexpr、折叠表达式、内联变量等。

    • 这些特性使C++的语法更加简洁、直观,提高了代码的可读性和可维护性。

  4. C++20

    (发布于2020年):

    • 是对C++17的延续和扩展。

    • 引入了概念(concepts)、协程(coroutines)、模块化(module)、三向比较运算符等新特性。

    • 这些特性提高了C++的表达能力和灵活性,使其更加适合现代软件开发的需要。

  5. C++23及以后:

    • C++标准遵循3年开发周期,并以发布年份命名。

    • 最近的C++23标准已完成更新并进入最终投票阶段,引入了模板参数捕获、可变参数模板等特性。

    • 预计中的C++26将在并发和并行性方面有重大改进,但目前尚未透露具体细节。

总之,C++新标准通过不断引入新特性和优化现有功能,使C++保持与现代编程趋势的同步,为开发者提供了更多工具和选择,以构建更高效、更安全、更易于维护的代码。

2.thread_local 线程本地化

线程局部存储(Thread Local Storage,TLS)是一种存储期(storage duration),对象的存储是在线程开始时分配,线程结束时回收,每个线程有该对象自己的实例**。

全局变量或静态变量会被放到".data"或".bss"段中,但当使用__declspec(thread)定义一个线程私有变量的时候,编译器会把这些变量放到PE文件的".tls"段中。

对于一个TLS变量来说,它有可能是一个C++的全局对象,那么每个线程在启动时不仅仅是复制".tls"的内容那么简单,还需要把这些TLS对象初始化,必须逐个地调用它们的全局构造函数,而且当线程退出时,还要逐个地将它们析构,正如普通的全局对象在进程启动和退出时都要构造、析构一样。

C++11引入了thread_local关键字用于下述情形:(1).命名空间(全局)变量;(2).文件静态变量;(3).函数静态变量;(4).静态成员变量。此外,不同编译器提供了各自的方法声明线程局部变量。thread_local作为类成员变量时必须是static的。

#include <iostream>
#include <thread>
​
thread_local int thread_value = 0;
​
void thread_function(int id){
    thread_value = id;
    std::cout<<"thread"<<id<<"thread_value ="<<thread_value<<std::endl;
}
​
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    std::thread t1(thread_function,1);
    std::thread t2(thread_function,2);
    t1.join();
    t2.join();
    std::cout<<"main"<<thread_value<<std::endl;
    return 0;
}
输出结果
threadthread2thread_value =2
1thread_value =1
main0

这表明每个线程都有自己独立的thread_specific_value副本,它们之间互不干扰。

thread_local 特点

  • 每个线程都有自己的变量副本,互不干扰。

  • 线程局部存储的变量在不同线程间是独立的,但在同一线程内是共享的。

  • thread_local变量的生命周期与线程的生命周期相同,即在线程创建时分配,在线程销毁时释放。

  • 可以用于解决多线程环境下的线程隔离问题,如线程特定配置或状态信息的存储。

future

在C++中,std::future 是C++11标准引入的一个模板类,它用于表示异步操作的结果。它提供了一种机制,使得一个线程可以等待另一个线程的结果,而无需阻塞当前线程的执行。

std::future 通常与 std::asyncstd::promisestd::packaged_task 一起使用。std::async 用于在单独的线程中启动一个异步任务,并返回一个 std::future 对象,该对象将保存异步操作的结果。std::promisestd::packaged_task 则允许你在一个线程中设置结果,并在另一个线程中通过 std::future 获取该结果。

以下是一个使用 std::asyncstd::future 的简单示例:

#include <iostream>  
#include <future>  
#include <thread>  
#include <chrono>  
  
int compute_value(int x) {  
    // 假设这是一个耗时的计算  
    std::this_thread::sleep_for(std::chrono::seconds(2));  
    return x * x;  
}  
  
int main() {  
    // 使用 std::async 在单独的线程中启动异步任务  
    std::future<int> result = std::async(std::launch::async, compute_value, 42);  
  
    // 在这里,主线程可以继续执行其他任务,而不会阻塞等待 compute_value 的结果  
    // ...  
  
    // 当需要结果时,调用 std::future::get()  
    int value = result.get(); // 这将阻塞,直到异步任务完成并返回结果  
    std::cout << "The result is " << value << std::endl;  
  
    return 0;  
}

在这个示例中,std::async 在单独的线程中启动了 compute_value 函数,并立即返回了一个 std::future<int> 对象。主线程可以继续执行其他任务,而不需要等待 compute_value 的结果。当主线程需要结果时,它调用 result.get(),这将阻塞直到异步任务完成并返回结果。

需要注意的是,如果异步任务抛出了异常,并且没有通过 std::future::get() 获取结果,那么这个异常将被存储起来,直到 std::future::get() 被调用时才会被重新抛出。因此,确保总是调用 std::future::get() 来获取结果并处理可能的异常是很重要的。

3.std::async

std::async是 C++11 引入的一个函数,它用于启动一个异步任务。这个函数返回一个std::future对象,该对象持有异步操作的结果。std::async` 可以让你在不阻塞当前线程的情况下,在另一个线程中执行某个任务。

std::async 的基本语法如下:

template< class Function, class... Args >  
std::future<typename std::result_of<Function(Args...)>::type>   
async( Function&& f, Args&&... args );  
  
template< class Function, class... Args >  
std::future<typename std::result_of<Function()>::type>   
async( std::launch policy, Function&& f, Args&&... args );

这里有两个重载版本:

  1. 第一个版本接受一个可调用的对象(函数、函数对象、Lambda 表达式等)和一组参数,并在新的线程中执行该函数。返回的 std::future 对象将持有该函数的返回值。

  2. 第二个版本接受一个启动策略(std::launch::asyncstd::launch::deferred)作为第一个参数。默认情况下,如果系统有足够的资源,则使用 std::launch::async 策略,该策略将尝试在新的线程中执行函数。而 std::launch::deferred 策略会延迟函数的执行,直到你调用 std::future::get()std::future::wait() 时才在调用这些函数的线程中执行。

示例:

#include <iostream>  
#include <future>  
#include <chrono>  
#include <thread>  
  
int some_function(int x) {  
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作  
    return x * x;  
}  
  
int main() {  
    // 使用 std::async 在另一个线程中执行 some_function  
    std::future<int> result = std::async(std::launch::async, some_function, 42);  
  
    // 在这里,主线程可以继续执行其他任务,而不会阻塞等待 some_function 的结果  
    // ...  
  
    // 当需要结果时,调用 std::future::get()  
    int value = result.get(); // 这将阻塞,直到异步任务完成并返回结果  
    std::cout << "The result is " << value << std::endl;  
  
    return 0;  
}

在上面的示例中,std::async 在另一个线程中启动 some_function,并立即返回一个 std::future<int> 对象。主线程可以继续执行其他任务,直到需要结果时,再调用 result.get() 来获取结果。

请注意,尽管 std::async 提供了简单的异步编程接口,但在实践中,对于更复杂的异步操作和任务调度,可能需要使用更高级的工具,如 std::threadstd::promisestd::packaged_taskstd::condition_variablestd::mutex 等。

在C++中,std::threadstd::promisestd::packaged_taskstd::condition_variablestd::mutex 是C++11及以后版本提供的线程和同步原语,用于多线程编程和线程间的同步。

4.std::threadstd::promisestd::packaged_taskstd::condition_variablestd::mutex`

std::thread

std::thread 类是C++中用于表示和控制执行线程的对象。你可以用它来创建和管理线程。通过提供一个可调用的对象(如函数、函数对象、Lambda表达式等)给 std::thread 的构造函数,你可以在新的线程中执行这个可调用的对象。

std::promise 和 std::future

std::promisestd::future 用于在不同线程之间传递数据或异常。std::promise 允许你在一个线程中设置某个值或异常,然后通过 std::future 对象在另一个线程中获取这个值或异常。

  • std::promise:持有一个共享状态,你可以使用 set_value()set_exception()set_value_at_thread_exit() 方法来设置这个共享状态的值或异常。

  • std::future:是与 std::promise 关联的共享状态的访问点。你可以使用 get() 方法来获取 std::promise 设置的值或异常。

std::packaged_task

std::packaged_task 是一个将可调用对象(如函数、Lambda表达式等)包装成任务的类。这个任务可以异步地在另一个线程中执行,并通过 std::future 获取结果。std::packaged_task 实际上是 std::promisestd::function 的结合体。

std::condition_variable

std::condition_variable 是一个同步原语,它允许线程基于某个条件进行等待。当某个条件不满足时,线程会阻塞在 std::condition_variable 上,直到另一个线程改变了条件并通知它。这通常与 std::mutex 一起使用,以确保对共享数据的正确访问。

std::mutex

std::mutex 是一个互斥量(互斥锁),用于保护共享数据免受多个线程的并发访问。当一个线程获得了 std::mutex 的锁时,其他尝试获取该锁的线程将被阻塞,直到第一个线程释放锁。这有助于防止数据竞争和其他并发问题。

总结

这些类和原语为C++提供了强大的多线程和同步支持。std::thread 用于创建和管理线程,std::promisestd::futurestd::packaged_task 用于在不同线程之间传递数据和异常,std::condition_variablestd::mutex 用于同步线程的执行和保护共享数据。在编写多线程程序时,正确地使用这些工具可以确保程序的正确性和性能。

5.future的应用场合

std::future 在多线程编程中有多种应用场合,它主要用于处理异步操作的结果。以下是一些 std::future 的常见应用场合:

  1. 并行计算:当你有一些可以并行执行的计算任务时,可以使用 std::async 在单独的线程中启动这些任务,并使用 std::future 获取它们的结果。这样,主线程可以继续执行其他任务,而不必等待所有计算任务完成。

  2. 数据加载:如果你的程序需要从文件、数据库或其他外部源加载数据,而这些操作可能会阻塞主线程的执行,那么可以使用 std::future。你可以在一个单独的线程中启动数据加载任务,并在需要这些数据时,使用 std::future 获取它们。

  3. 任务分解:对于大型或复杂的任务,可以将它们分解为多个较小的子任务,并在单独的线程中并行执行这些子任务。每个子任务都可以使用 std::async 启动,并返回一个 std::future 对象。主线程可以等待所有子任务完成,并收集它们的结果。

  4. 回调和事件驱动编程:在某些情况下,你可能希望在一个操作完成后执行另一个操作(即回调函数)。使用 std::future,你可以启动一个异步操作,并在需要时获取其结果,从而触发回调函数。这提供了一种更灵活的方式来处理异步事件,而无需使用复杂的回调系统或事件循环。

  5. 线程池:在使用线程池时,std::future 可以用于从线程池中获取任务的结果。线程池负责管理一组线程,并在这些线程上执行提交给它的任务。每个任务都可以返回一个 std::future 对象,以便在需要时获取其结果。

  6. 异常处理:如果异步操作抛出异常,并且没有通过 std::future::get() 获取结果,那么这个异常将被存储起来,直到 std::future::get() 被调用时才会被重新抛出。这允许你在主线程中捕获和处理这些异常,从而更好地控制程序的错误处理流程。

  7. 延迟初始化:有时你可能希望延迟某个对象的初始化,直到真正需要它时再进行。使用 std::asyncstd::future,你可以在一个单独的线程中启动对象的初始化过程,并在需要该对象时通过 std::future::get() 获取它。这可以减少程序启动时的延迟,并提高响应性能。

总之,std::future 是一个强大的工具,它允许你在多线程环境中处理异步操作的结果,并提高程序的性能和响应性。

6.范围 for

for (auto element : container) {
    // 操作每个元素
}

范围for循环的工作原理是,它会自动遍历容器中的每个元素,并将当前元素的值赋给 element 变量,然后执行循环体中的代码块。循环体会针对容器中的每个元素执行一次。

constexpr 函数

constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。 (1)值不会改变 (2)在编译过程就能得到计算结果的表达式。 constexpr和const的区别: 两者都代表可读, const只表示read-only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量

constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。

7.final 和 override

在C++中,finaloverride是两个用于类和成员函数的修饰符,它们与类的继承和虚函数有关。

final

final关键字有两个主要用途:

  1. 禁止类被继承:如果一个类被声明为final,那么它不能被其他类继承。

class FinalClass final {  
    // ...  
};  
  
class DerivedFromFinal : public FinalClass { // 错误:FinalClass是final的  
    // ...  
};
  1. 禁止虚函数在派生类中被重写:如果一个虚函数在基类中被声明为final,那么它不能在派生类中被重写(override)。

class Base {  
public:  
    virtual void foo() final {  
        // ...  
    }  
};  
  
class Derived : public Base {  
public:  
    void foo() override { // 错误:foo在Base中是final的  
        // ...  
    }  
};

override

override关键字用于明确指出一个成员函数在派生类中重写了基类中的虚函数。使用override的好处是,如果基类中没有相应的虚函数,编译器会报错,从而帮助开发者避免一些常见的错误。

class Base {  
public:  
    virtual void foo() {  
        // ...  
    }  
};  
  
class Derived : public Base {  
public:  
    void foo() override { // 正确:foo在Base中是虚函数  
        // ...  
    }  
};  
  
class AnotherDerived : public Base {  
public:  
    void bar() override { // 错误:Base中没有名为bar的虚函数  
        // ...  
    }  
};

在上面的例子中,AnotherDerived类中的bar函数试图使用override关键字,但由于Base类中并没有名为bar的虚函数,所以编译器会报错。

总之,finaloverride关键字在C++中提供了更好的类继承和虚函数管理的机制,有助于减少错误并提高代码的可读性和可维护性。

8.智能指针

C++11 引入了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr,这些智能指针旨在帮助开发者更安全地管理动态分配的内存,避免内存泄漏和其他相关问题。

1. std::unique_ptr

std::unique_ptr 是一个独占所有权的智能指针,它在某个时刻只能有一个 unique_ptr 拥有某个对象。当 unique_ptr 被销毁(例如,离开其作用域)时,它所指向的对象也会被自动删除。unique_ptr 不允许拷贝构造或拷贝赋值,但允许移动构造和移动赋值。

示例:

#include <memory>  
  
int main() {  
    std::unique_ptr<int> ptr(new int(42));  
    // ... 使用 ptr  
    // 当 ptr 离开作用域时,它所指向的内存会被自动释放  
    return 0;  
}

2. std::shared_ptr

std::shared_ptr 是一个共享所有权的智能指针,允许多个 shared_ptr 指向同一个对象。对象的生命周期会持续到最后一个拥有它的 shared_ptr 被销毁为止。当最后一个拥有它的 shared_ptr 被销毁时,它所指向的对象会被自动删除。shared_ptr 支持拷贝构造和拷贝赋值。

示例:

#include <memory>  
  
int main() {  
    std::shared_ptr<int> ptr1(new int(42));  
    std::shared_ptr<int> ptr2 = ptr1; // 拷贝构造,现在有两个 shared_ptr 指向同一个 int  
    // ... 使用 ptr1 和 ptr2  
    // 当 ptr1 和 ptr2 都离开作用域时,它们所指向的内存才会被自动释放  
    return 0;  
}

3. std::weak_ptr

std::weak_ptr 是对 std::shared_ptr 的一个补充,它保存对对象的弱引用,即 weak_ptr 不影响对象的生命周期。weak_ptr 主要用于解决 shared_ptr 之间的循环引用问题。你不能通过 weak_ptr 直接访问对象,但你可以使用 lock() 方法尝试获取一个到对象的 shared_ptr

示例:

#include <memory>  
  
struct Foo {  
    std::weak_ptr<Foo> other;  
    // ... 其他成员  
};  
  
int main() {  
    std::shared_ptr<Foo> foo1 = std::make_shared<Foo>();  
    std::shared_ptr<Foo> foo2 = std::make_shared<Foo>();  
    foo1->other = foo2;  
    foo2->other = foo1;  
    // 没有循环引用问题,因为 other 是 weak_ptr  
    // ...  
    return 0;  
}

使用智能指针可以帮助开发者更容易地编写出没有内存泄漏的代码,同时使代码更加清晰和易于维护。然而,仍然需要谨慎使用它们,特别是在处理复杂的所有权关系时。

9.shared_ptr支持线程安全嘛

std::shared_ptr 在 C++ 标准库中本身并不是线程安全的。也就是说,如果你从多个线程同时访问和修改同一个 std::shared_ptr 对象(例如,一个线程尝试增加引用计数,而另一个线程尝试减少引用计数),那么可能会出现数据竞争(data race)和未定义行为(undefined behavior)。

然而,有几个重要的点需要注意:

  1. 对象的析构是线程安全的:尽管 std::shared_ptr 的操作本身不是线程安全的,但当最后一个 shared_ptr 指向的对象被销毁时,这个对象的析构函数会被安全地调用,即使存在多个线程都在尝试减少 shared_ptr 的引用计数。这是因为 C++ 标准库保证了析构函数的调用是序列化的。

  2. 原子操作:你可以使用原子操作(如 std::atomic)来保护对 shared_ptr 的访问。但是,请注意,仅仅保护对 shared_ptr 本身的访问是不够的,因为如果多个线程都在操作同一个 shared_ptr 所指向的对象,那么对这个对象的访问也需要是线程安全的。

  3. 线程安全的共享所有权std::shared_ptr 的主要目的是在多个 shared_ptr 之间安全地共享对象的所有权。只要每个线程都通过其自己的 shared_ptr 来访问对象,并且没有线程直接修改 shared_ptr(而是通过原子操作或其他同步机制),那么对象的共享所有权就是线程安全的。

  4. 自定义删除器:如果你为 std::shared_ptr 提供了自定义的删除器(deleter),那么这个删除器应该也是线程安全的。因为当 shared_ptr 的引用计数变为零时,删除器会被调用。

总之,虽然 std::shared_ptr 本身不是线程安全的,但你可以通过其他机制(如原子操作、互斥锁等)来保护对它的访问,从而确保在多线程环境中安全地使用它。同时,还需要确保对 shared_ptr 所指向的对象的访问也是线程安全的。

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的小猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值