-
虚函数
在C++中, virtual 和 override 关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。
virtual 关键字
- 使用场景:在基类中声明虚函数。
- 目的:允许派生类重写该函数,实现多态。
- 行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本
- 示例:
class Base { public:virtual void func() {std::cout << "Function in Base" << std::endl;} };override 关键字
- 使用场景:在派生类中重写虚函数。
- 目的:明确指示函数意图重写基类的虚函数。
- 行为:确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错。
- 示例:
class Derived : public Base { public:void func() override {std::cout << "Function in Derived" << std::endl;} };注意点
只在派生类中使用 override: override 应仅用于派生类中重写基类的虚函数。
虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。
默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual关键字才会成为虚函数。
继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否使用 virtual 关键字。
正确使用 virtual 和 override 关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常见的错误,如签名不匹配导致的非预期的函数重写。
-
抽象类
抽象类的特点
- 包含至少一个纯虚函数:
- 抽象类至少有一个纯虚函数。这是一种特殊的虚函数,在抽象类中没有具体实现,而是留给派生类去实现。
- 纯虚函数的声明方式是在函数声明的末尾加上 = 0 。
- 不能直接实例化:
- 由于抽象类不完整,所以不能直接创建它的对象。就像你不能直接使用“交通工具”的概念去任何地方,你需要一个具体的交通工具。
- 用于提供基础结构:
- 抽象类的主要目的是为派生类提供一个共同的基础结构,确保所有派生类都有一致的接口和行为。
-
纯虚函数-接口
一个类作为接口可以通过以下步骤来实现:
- 定义抽象类:创建一个包含纯虚函数的抽象类,这些函数构成了接口的一部分。这些函数在抽象类
中只有声明而没有具体的实现。
- 派生类实现接口:派生类继承抽象类,并实现其中的纯虚函数,以具体实现接口定义的方法。
class LiveMove{ public:virtual void eat() = 0;virtual void bite() = 0;virtual void drink() = 0;virtual void la() = 0; };class Dog : public LiveMove{ public:void eat() override{...};void bite() override{...};void drink() override{...};void la() override{...}; };
-
线程的创建
- 通过lambda表达式创建线程
Lambda 格式:
[捕获列表] (参数) 可选修饰 -> 返回类型 { 函数体 }
创建一个lambda表达式,定义变量i,将123传入lambda表达式,输出lambda表示式的内容
int main(int argc, char const *argv[]){std::thread th([](int i){std::cout << "test lambda" << i << std::endl;}, 123);th.join(); return 0; }
在类成员函数里定义一个线程,访问类成员变量
class TestLambda{ public:void start(){std::string hr;std::thread th([this](){std::cout << this->name << std::endl;});th.join();}private:std::string name = "hello"; };int main(int argc, char const *argv[]){ TestLambda test;test.start();return 0; }
-
竞争状态、临界区、互斥锁
示例:
创建了100个线程,并调到后台运行,产生竞争状态,导致运行结果不可预测
void TestThread(){//std::this_thread::sleep_for(std::chrono::milliseconds(10));std::cout << "====================" << std::endl;std::cout << "test 001" << std::endl;std::cout << "test 002" << std::endl;std::cout << "test 003" << std::endl;std::cout << "====================" << std::endl; }int main(int argc, char const *argv[]){//创建100个进程并调至后台运行,实现并行for(int i = 0; i < 100; ++i){std::thread th(TestThread);th.detach();}return 0; }为了解决竞争状态,设置互斥锁,建立临界区
通过定义锁,我们定义获取锁资源到释放锁资源的区间叫做临界区,同一时间只能有一个进程访问临界区资源,只有当解锁后,其他被阻塞的线程才能重新获取重复上述操作
static std::mutex mtx;void TestThread(){//获取锁资源,如果获取不到进去阻塞状态mtx.lock();std::cout << "====================" << std::endl;std::cout << "test 001" << std::endl;std::cout << "test 002" << std::endl;std::cout << "test 003" << std::endl;std::cout << "====================" << std::endl;mtx.unlock(); }int main(int argc, char const *argv[]){for(int i = 0; i < 100; ++i){std::thread th(TestThread);th.detach();}return 0; }
常用的mutex函数
lock() 阻塞式获取锁:若锁未被占用则成功获取;若已被占用,线程阻塞直到锁释放。 必须获取锁才能执行临界区(等待可接受) unlock() 手动释放锁:必须在 lock()/try_lock()成功后调用,否则行为未定义(崩溃)。手动管理锁时,临界区结束后释放锁 try_lock() 非阻塞式获取锁:锁未被占用则返回 true(成功);已被占用则返回false(不阻塞)。不想阻塞,可先做其他任务(如重试、跳过) try_lock_for() 超时非阻塞获取:在 rel_time时间内尝试获取锁,超时未获取则返回false。允许短暂等待,但不想无限阻塞(如等待 1 秒) try_lock_until() 绝对时间超时获取:尝试获取锁直到 abs_time时间点,超时返回false。需精确控制等待截止时间(如凌晨 3 点前)
处理线程抢不到资源问题
我们总是会遇到,当一个线程解锁后,该进程又一次进入了临界区,使得原本等待解锁的被阻塞线程又得接着等待,使得处理效率变低。
通过每次线程解锁时让其线程睡眠,保证解锁后不会与其他被阻塞线程争夺。
示例:
void ThreadMainMtx(int i){for(;;){mtx.lock();std::cout << i << "[in]\n";std::this_thread::sleep_for(std::chrono::milliseconds(100));mtx.unlock();std::this_thread::sleep_for(std::chrono::seconds(1));//解锁后,睡眠1s,再取争锁} }int main(int argc, char const *argv[]){for(int i = 0; i < 4; ++i){std::thread th(ThreadMainMtx, i + 1);th.detach();}return 0; }
