C++进阶操作

简介: 对于thread, bind, function等复值函数,还是使用&有更好的编译器兼容性

Thread+CR

对于thread, bind, function等复值函数,还是使用&有更好的编译器兼容性

比如:

std::function<void(string)>f_str2 = std::bind((void(A::*)(string)&A::func4, &a, std::placeholders::_1);

1.1C++11多线程thread

1.1.1语法

//创建一个空的 thread 执行对象。
thread() _NOEXCEPT
{
  _Thr_set_null(_Thr);
}
  • 初始化构造函数
//创建std::thread对象,该thread对象可以被joinable,新产生的线程会调用threadFun函数,该函数的参数由args给出。
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
auto exec(int64_t timeoutMs, F&& f, Args&&... args)
  • 拷贝构造函数
//拷贝构造被禁用,意味着thread无法被拷贝构造
thread(const thread&) = delete;
thread t1;
thread t2 = t1;//错误
  • Move构造函数
#include<thread>
using namespace std;
void threadFun(int &a) // 引用传递
{
cout << "this is thread fun !" <<endl;
cout <<" a = "<<(a+=10)<<endl;
}
int main()
{
int x = 10;
thread t1(threadFun, std::ref(x));
thread t2(std::move(t1)); // t1 线程失去所有权
thread t3;
t3 = std::move(t2); // t2 线程失去所有权
//t1.join(); // ?
t3.join();
cout<<"Main End "<<"x = "<<x<<endl;
return 0;
}


主要成员函数

  • get_id()获取线程ID,返回类型std::thread::id
  • joinable()判断是否可以加入等待
  • join()等待该线程执行完后才返回
  • detach()成为守护线程,驻留后台运行,与之关联的std::thread对象失去了对目标线程的关联,无法再通过std::thread对象获取该线程的控制权。当线程主函数执行完后,线程就结束了,运行时库负责改线程相关的资源。


  • *this不再代表任何的线程执行实例。
  • joinable() == false
  • get_id() == std::thread::id()


1.1.2简单线程的创建

使用std::thread创建线程,提供线程函数或者函数对象,并且可以同时指定线程函数的参数。

//见1-thread2-pack



1.2.1独占互斥量std::mutex

std::mutex是c++11中的独占互斥量,std::mutex对象提供了独占是所有权的特性–不支持递归对std::mutex对象上锁,而std::recursive_lock可以递归对互斥量上锁。


构造函数:不允许拷贝构造,也不允许move拷贝,最初产生的mutex对象对于unlock状态

lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock(), 解锁,释放对互斥量的所有权。

try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

//1-2-mutex1
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
volatile int counter(0); // non-atomic counter
std::mutex mtx; // locks access to counter
void increases_10k()
{
    for (int i=0; i<10000; ++i) 
    {
// 1. 使用try_lock的情况
// if (mtx.try_lock()) { // only increase if currently not
        locked:
// ++counter;
// mtx.unlock();
// }
// 2. 使用lock的情况
        {
        mtx.lock();
        ++counter;
        mtx.unlock();
        }
    }
}
int main()
{
    std::thread threads[10];
    for (int i=0; i<10; ++i)
    threads[i] = std::thread(increases_10k);
    for (auto& th : threads) th.join();
    std::cout << " successful increases of the counter " << counter <<
    std::endl;
    return 0;
}

1.2.2递归互斥量std::recursive_mutex

#include <iostream>
#include <mutex>
#include <thread>
using std::cout;
using std::endl;
struct Complex
{
    std::recursive_mutex mutex;
    int i;
    Complex() : i(0) {}
    void mul(int x)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i *= x;
    }
    void div(int x)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i /= x;
    }
    void both(int x, int y)
    {
        std::lock_guard<std::recursive_mutex> lock(mutex);
        mul(x);
        div(y);
    }
};
int main(void)
{
    Complex complex;
    complex.both(32, 23);
    return 0;
}

虽然递归锁能解决这种情况的锁死问题,但是尽量不要使用递归锁,主要原因如下:


  • 需要递归锁的多线程互斥处理本身是可以简化的,允许递归很容易放纵复杂的逻辑产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁。
  • 递归锁效率低。
  • 递归锁虽然允许同一个线程多次获得同一个互斥量,但是可重复获得的最大次数并未具体说明,一旦超过一定次数,在对lock进行调用就会抛出std::system错误。


1.2.3带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

std::timed_mutex比std::mutex多两个超时获取锁的接口-try_lock_for和try_lock_until


1.2.4lock_guard和unique_lock的使用和区别

相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更的编码方式。

RAII:也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。


unique_lock和lock_guard的区别
  • unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。
  • unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以解锁,lck.lock()上锁,而不必等到析构自动解锁。
#include <iostream>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <unistd.h>
#include <thread>
std::deque<int> q;
std::mutex mu;
std::condition_variable  cond;
int count = 0;
void fun1()
{
    while(true)
    {
        {
            std::unique_lock<std::mutex> locker(mu);
            q.push_front(count++);
            locker.unlock();
            cond.notify_one();
        }
        sleep(1);
    }
}
void fun2()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock(mu);
        cond.wait(lock, [](){return !q.empty();});
        auto data = q.back();
        q.pop_back();
        // lock.unlock();
        std::cout << "thread2 get value form thread1:" << data << std::endl;
    }
}
int main()
{
    std::thread t1(fun1);
    std::thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}


条件变量的目的就是为了再没有获得某种提醒时长时休眠;如果正常情况下,我们需要一直循环(+sleep),但是这样的问题就是CPU消耗+时间问题,条件变量的意思时在cond.wait这里一直休眠到cond.notify_one唤醒才执行下一句;还有cond.notify_all()接口用于唤醒所有等待的线程。


为什么必须使用unique_lock?

原因:条件变量在wait时会进行unlock再进入休眠,lock_guard没有该操作接口


wait:如果该线程被唤醒或者超时那会先进行lock解锁,再判断条件(传入参数是否成立),如果成立则wait函数返回是否释放锁继续休眠。

notify:进行notify动作并不需要获取锁。


使用场景:需要结合notify+wait的场景使用unique_lock;如果单纯互斥使用lock_guard就可以了。



总结
lock_guard
  • std::lock_guard在构造函数中进行加锁,析构函数中进行解锁。
  • 锁在多线程编程中使用较多,因此C++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的编程场景编写resource_guard RALL类,避免忘记释放资源。


std::unique_lock
  • unique_lock是通用互斥包装器,允许延迟锁定、锁定时有时限尝试,递归锁定、所有权转移和条件变量一同使用。
  • unique_lock比lock_guard使用更加灵活,功能更加强。
  • 使用unique_lock需要付出更多时间,性能成本


1.3条件变量

互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。条件变量位于头文件condition_variable下。


条件变量使用过程:


  1. 拥有条件变量的线程获取互斥量
  2. 循环检测某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行。
  3. 某个线程满足条件执行完之后调用notify_one或者notify_all唤醒一个或多个等待线程。


wait和notify两类操作构成了多线程同步的基础


1.3.1成员函数

函数原型

wait
void wait(unique_lock<mutex>& lck);
template <class Predicate>
    void wait(unique_lock<mutex>& lck, Predicate pred);


包含两种重载,第一种只包含unique_lock对象,另外一个Predicate对象(等待条件),这里必须使用unique_lock,因为wait函数的工作原理:


  • 当线程调用wait后将被阻塞并且解锁互斥量,直到另外一个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知notify,wait函数也是自动调用lock,同理不能使用lock_guard对象。
  • 如果wait函数没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并且阻塞到本行,直到某线程调用notify_one或者notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取互斥量,然后无条件执行后面的操作。
  • 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。
wait_for
tamplate <class Rep, class Period>
    cv_status wait_for (unique_lock<mutex>& lck,
                        const chrono::duration<Rep, Period>& rel_time);
tamplate <class Rep, class Period, class Pericate>
    cv_status wait_for (unique_lock<mutex>& lck,
                        const chrono::duration<Rep, Period>& rel_time, Predicate);

和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,改线程处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。


wait_until
template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
                const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
                const chrono::time_point<Clock,Duration>& abs_time,Predicate pred);

与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和wait类似.

notify_one
void notify_one() noexcept

解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的。

notify_all
void notigy_all() noexcept

解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。

  • 例子:使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取。
    代码:
condition-syncqueue


1.4原子变量atomic

#include <iostream>
#include <atomic>   //std::atomic memory_order_relaxed
#include <thread>
//std::atomic<int> count = 0; wrong
std::atomic<int> count(0);
void set_count(int x)
{
    std::cout << "set_count:" << x << std::endl;
    count.store(x, std::memory_order_relaxed);
}
void print_count()
{
    int x;
    do
    {
        x = count.load(std::memory_order_relaxed);
    }while(x == 0);
    std::cout << "count: " << x << '\n';
}
int main()
{
    std::thread t1(print_count);
    std::thread t2(set_count, 10);
    t1.join();
    t2.join();
    std::cout << "main finish\n";
    return 0;
}


1.5异步操作

  • std::future:异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
  • std::aysnc:异步运行某个函数
  • std::packaged_task:将任务和feature绑定在一起的模版,是一种封装对任务的封装。
  • std::promise:


1.5.1std::aysnc和std::future

std::future期待一个返回,从一个异步调用的角度来说,feture更像是执行函数的返回值,C++标准库使用std::future为一次性事件模版,如果是一个事件需要等待特定的一次性事件,这个线程可以获取一个future对象来代表这个事件。

异步调用旺旺不知道何时返回,但是如果异步调用的过程需呀同步,或者说后一个异步调用使用前一个异步调用的结果。这个时候就要用到future。

线程可以周期性在这个future上等待一段时间,检查future是否已经ready,如果没有,该线程可以先去做另外一个任务,一旦future就绪,该future无法复位(无法再次使用这个future等待这个事件)。future代表的是一次性事件。


  • future的类型

在库的头文件中声明了两种future,唯一future(std::future)和共享future(std::shared_future)这两个是参照std::unique_ptr和std_shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例,而后者可以有多个实例指向同一个关联事件吗,当事假就绪,所有指向同一个事件的std::shared_future实例会变成就绪。

  • future的使用

std::future是一个模版,例如std::future,模版参数就是期待返回的类型,虽然future被用于线程间通信,但其本身并不提供同步访问,必须通过互斥元或其他同步机制保护访问。future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程去帮你做一项任务,并期待这个任务的返回值,但是std::thread没有提供这样的机制,这就需要用std::async和std::future。std::async返回一个std::future对象,而不是给你一个确定的值(所以当你不需要立刻使用此值的时候才需要用到这个机制)。当你需要使用这个值的时候,对future使用get(),线程就会阻塞知道future就绪,然后返回该值。

  #include <iostream>
#include <future>
#include <thread>
int find_result_to_add()
{
    std::cout << "find_result_to_add" << std::endl;
    return 1 + 1;
}
int find_result_add2(int a, int b)
{
    std::this_thread::sleep_for(std::chrono::seconds(5));
    return a + b;
}
void do_other_things()
{
    std::cout << "do_other_things" << std::endl;
}
int main()
{
    std::future<int> result = std::async(find_result_to_add);
    do_other_things();
    std::cout << "result:" << result.get() << std::endl;
    std::future<decltype (find_result_add2(0, 0))> result2 = 
                    std::async(find_result_add2, 10, 10);
    std::cout << "result2:" << result2.get() << std::endl;
    return 0;
}


跟thread类似,async允许你通过将额外的参数添加到调用中,来将附加参数传递给函数。如果传入的函数指针是某个类的成员函数,则还需要将类对象指针传入(直接传入,传入指针,或者是std::ref封

装)。

默认情况下,std::async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你给的

参数。这个参数为std::launch类型


  • std::launch::defered表明该函数会被延迟调用,直到在future上调用get()或者wait()为止。
  • std::launch::async,表明函数会在自己创建的线程上运行。
  • std::launch::any = std::launch::defered | std::launch::async。
  • std::launch::sync = std::launch::defered。

PS:默认选项参数被设置为std::launch::any。如果函数被延迟运行可能永远都不会运行。


1.5.2std::pakcaged_task

如果说std::async和std::feature还是分开看的关系的话,那么std::packaged_task就是将任务和feature绑定在一起的模板,是一种封装对任务的封装。

可以通过std::packaged_task对象获取任务相关联的feature,调用get_future()方法可以获得std::packaged_task对象绑定的函数的返回值类型的future。std::packaged_task的模板参数是函数签名。

PS:例如int add(int a, intb)的函数签名就是int(int, int)

#include <iostream>
#include <future>
using namespace std;
int add(int a, int b, int c)
{
    std::cout << "call add\n";
    return a + b + c;
}
void do_other_things()
{
    std::cout << "do_other_things" <<std::endl;
}
int main()
{
    std::packaged_task<int(int, int, int)> task(add);
    do_other_things();
    std::future<int> result = task.get_future();
    task(1, 2, 4);//让任务执行 不然get会阻塞
    std::cout << "result:" << result.get() << std::endl;
    return 0;
}

1.5.3std::promise

std::promise提供了一种设置值的方式,他可以在这之后通过相关联的std::future对象进行读取。换种说法,之前已经说过std::future可以读取一个异步函数的返回值,那么这个std::promise就提供了一种方式让future就绪。

#include <future>
#include <string>
#include <thread>
#include <iostream>
void print(std::promise<std::string>& p)
{
    p.set_value("There is result whitch you want.");
}
void do_some_other_things()
{
    std::cout << "Hello world" << std::endl;
}
int main()
{
    std::promise<std::string> promise;
    std::future<std::string> result = promise.get_future();
    std::thread t(print, std::ref(promise));
    do_some_other_things();
    std::cout << result.get() << std::endl;
    t.join();
    return 0;
}

由此可见promise创建好的时候future已经创加好了

线程在创建promise的时候会获得一个future,然后将promise传递给设置他的线程,当前线程则持有future,以便随时检查是否取值。


1.5.4总结

future的表现为期望,当前线程持有future时,期望从future获取想要的结果和返回,可以把future当作异步函数的返回值。而promise是个承诺,当前线程创建了promise对象后,这个promise对象向线程承诺他必定会被人设置一个值,和promise相关联的future就是获取返回的手段。


2function和bind

在设计回调函数的时候,无可避免的会接触到可回调对象。在C++11中,提供了std::function和std::bind两个方法来对可回调对象进行统一封装。

C++语言中有几种可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。

和其他对象一样,可调用对象也有类型。例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定。


2.1function的用法

#include <functional>
#include <iostream>
//保存普通函数
void func1(int a)
{
    std::cout << a << std::endl;
}
//保存成员函数
class A
{
public:
    A(std::string name) : name_(name){}
    void func3(int i) const {std::cout << name_ << "," << i << std::endl;}
private:
    std::string name_;
};
int main()
{
    //保存普通函数
    std::function<void(int a)> func;
    func = func1;
    func(2);
    //保存lamdba表达式
    std::function<void()> func_1 = [](){std::cout << "hello world" << std::endl;};
    func_1();
    //保存成员函数
    std::function<void(const A&, int)> func3_ = &A::func3;
    A a("jiejie");
    func3_(a, 1);
}


2.2bind用法

可将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用的对象来“适应”原对象的参数列表。

可调用的bind的一般的形式:auto newCallable = bind(callable, arg_list);


其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable参数。即,当我们调用newCallable时,newCallable会调用callable,并且传给它arg_list中的参数。


arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数“位置”。数值n表示成的可调用对象中参数的位置:1为newCallable的第一个参数,2为第二个参数,以此类推。


3可变模版参数

C++11的新特性-可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。


3.1可变模版参数的展开

可变参数模版语法

template <class... T>
void f(T... args);

上面的可变模版参数的定义中,省略号的作用有两个:


  • 声明一个参数包T… args,这个参数包中可以包含0到任意个模版参数;
  • 在模版定义的右边,可以将参数包展开成一个一个独立的参数。


上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类,然而,模版函数不支持偏特化,所以可变模版参数函数和可变模版参数类展开可变模版参数的方法还不尽相同,下面我们来分别看看他们展开可变模版参数的方法。

#include <iostream>
template <class... T>
void f(T... args)
{
    std::cout << sizeof...(args) << std::endl;
}
int main()
{
    f();
    f(1, 2);
    f(1, 2.5, "111");
    return 0;
}

上面的例子中,f()没有传入参数,所以参数包为空,输出的size为0,后面两次调用分别传入两个和三个参数,故输出的size分别为2和3。由于可变模版参数的类型和个数是不固定的,所以我们可以传任意类型和个数的参数给函数f。这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。展开可变模版参数函数的方法一般有两种:


  • 通过递归函数展开参数包
  • 通过逗号表达式展开参数包


递归函数展开参数包
#include <iostream>
// void print()
// {
//     std::cout << "empty" << std::endl;
// }
template<class T>
void print(T t)
{
    std::cout << t << std::endl;
    std::cout << t << std::endl;
}
template <class T, class ...Args>
void print(T head, Args... rest)
{
    std::cout << "paramter " << head << std::endl;
    print(rest...);
}
int main()
{
    print(1, "string", 3, 4);
    return 0;
}


逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。有没有一种更简单的方式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

#include <iostream>
template <class T>
void printarg(T t)
{
    std::cout << t << std::endl;
}
template <class ...Args>
void expand(Args... args)
{
    int arr[] = {(printarg(args), 0)...};
}
int main()
{
    expand(1, 2, 3, 4);
    return 0;
}

这个例子将分别打印出1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:


#include <iostream>
template<class F, class ...Args>
void expand(const F& f, Args&& ...args)
{
    std::initializer_list<int>{(f(std::forward<Args>(args)), 0)...};
}
int main()
{
    expand([](int i){std::cout << i << std::endl;}, 1, 2, 3);
    return 0;
}
相关文章
|
3月前
|
C++
二叉树进阶 - (C++二叉搜索树的实现)
二叉树进阶 - (C++二叉搜索树的实现)
35 1
|
4天前
|
算法 测试技术 C++
【数论】【分类讨论】【C++算法】1611使整数变为 0 的最少操作次数
【数论】【分类讨论】【C++算法】1611使整数变为 0 的最少操作次数
|
25天前
|
存储 C++
C++ 操作重载与类型转换(二)
C++ 操作重载与类型转换(二)
31 2
|
25天前
|
自然语言处理 安全 C++
C++ 操作重载与类型转换(一)
C++ 操作重载与类型转换(一)
45 3
|
2月前
|
存储 C++ 容器
『 C++ 』二叉树进阶OJ题(下)
『 C++ 』二叉树进阶OJ题(下)
|
2月前
|
C++ 容器
『 C++ 』二叉树进阶OJ题(中)
『 C++ 』二叉树进阶OJ题(中)
|
2月前
|
C++ 容器
『 C++ 』二叉树进阶OJ题(上)
『 C++ 』二叉树进阶OJ题(上)
『 C++ 』二叉树进阶OJ题(上)
|
2月前
|
网络协议 C++
一文彻底解决C++中的重载、重写和隐藏操作
一文彻底解决C++中的重载、重写和隐藏操作
48 0
|
2月前
|
C++
C++之模板进阶
C++之模板进阶
22 0
|
2月前
|
存储 缓存 编译器
《C++ Concurrencyin Action》第5章--C++内存模型和原子类型操作
《C++ Concurrencyin Action》第5章--C++内存模型和原子类型操作