多态的实现

虚函数表

  1. 虚函数表可以理解为就是一个函数指针数组functype ptr[]
  2. 虚函数表内部存放的是函数指针,而不是函数地址
  3. 无论是基类还是子类,(前提有虚函数)都有一个自己的虚函数表,继承时候会直接复制基类的虚函数表
  4. 重写时候会改写虚函数表里面指针指向的函数地址,达到多态作用
  5. 子类添加新的虚函数(不是重写,而是新的)会追加在虚函数表最后
  6. 父类指针拿到子类对象时候,对着表格拿到函数指针,但是实际上拿到的是子类的虚函数表,因此函数指针也是指向子类的,因此实现多态
  • 多重继承时候会有多个虚函数表

面向过程区别

  • 面向对象是以功能来划分问题,而不是以步骤解决

三大特性

封装

  • 类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性
  • 增强代码可读性和可维护性

继承

  • 代码复用,将这些相同的部分,抽取到父类中,让两个子类继承父类

[!tips] 注意点 继承的时候所有虚函数不要加const, 因为不确定子类重写的时候会不会需要更改, 加上是不合适的

多态

  • 提高了代码的可扩展性。
  • 只需要根据父类指针调用函数,不用关心子类的具体实现

底层模型

虚拟继承

  • 解决菱形继承的问题
  • A作为base类会被放在最下面,作为共享部分,然后与base不同部分放在上面
                                          D VTable
                                          +---------------------+
                                          |   vbase_offset(32)  |
                                          +---------------------+
struct D                                  |   offset_to_top(0)  |
object                                    +---------------------+
    0 - struct B (primary base)           |      RTTI for D     |
    0 -   vptr_B  ----------------------> +---------------------+
    8 -   int bx                          |       D::f0()       |
   16 - struct C                          +---------------------+
   16 -   vptr_C  ------------------+     |   vbase_offset(16)  |
   24 -   int cx                    |     +---------------------+
   28 - int dx                      |     |  offset_to_top(-16) |
   32 - struct A (virtual base)     |     +---------------------+
   32 -   vptr_A --------------+    |     |      RTTI for D     |
   40 -   int ax               |    +---> +---------------------+
sizeof(D): 48    align: 8      |          |       D::f0()       |
                               |          +---------------------+
                               |          |   vcall_offset(0)   |x--------+
                               |          +---------------------+         |
                               |          |   vcall_offset(-32) |o----+   |
                               |          +---------------------+     |   |
                               |          |  offset_to_top(-32) |     |   |
                               |          +---------------------+     |   |
                               |          |      RTTI for D     |     |   |
                               +--------> +---------------------+     |   |
                                          |     Thunk D::f0()   |o----+   |
                                          +---------------------+         |
                                          |       A::bar()      |x--------+
                                          +---------------------+     

多线程

  • 当一个thread对象既没有detach也没有join时候,thread释放(可以是因为栈对象自动释放)后会直接中断程序
  • thread提供参数之后不能通过引用传递,但是可以通过指针传递,因为thread构造参数时候直接使用拷贝
  • condition_variable的wait调用后,会先释放锁,之后进入等待状态;当其它进程调用通知激活后,会再次加锁
  • std::unique_lockstd::lock_guard类似,第一个更加灵活,但是性能更加差
  • 条件变量的用法通常为生产者消费者模型,多个线程用同一个锁+条件变量阻塞
  • bind函数可以将函数和参数进行绑定生成一个新的函数对象,这样适配接口就会更加方便
void fun1(int n1, int n2, int n3)
{
    cout << n1 << " " << n2 << " " << n3 << endl;
}
struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
int main()
{
    //_1表示这个位置是新的可调用对象的第一个参数的位置
    //_2表示这个位置是新的可调用对象的第二个参数的位置
    auto f1 = bind(fun1, _2, 22, _1);
    f1(44,55);
    Foo foo;
    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);// 第二个参数必须是对象作为this指针
    f(5); // 100
}
  • thread是可以移动的move,但是不能复制copy

条件变量和信号量的区别

  1. 条件变量支持广播方式唤醒等待者;而信号机制不支持,只能一个一个通知
  2. 条件变量只能结合互斥量做同步用;信号机制除了做同步,还能用于共享资源并发访问加锁
  3. 条件变量是无状态的,如果唤醒早于等待,则唤醒会丢失;信号机制是有状态的,可以记录唤醒的次数

多线程demo

class ThreadPool{
private:
	std::queue<std::function<void(void)>> task_que_;
	std::condition_variable cond_;// 条件变量,通常和锁一起使用
	std::mutex que_mut_;// 队列锁
	std::vector<std::thread> arr_thread_;
	std::atomic<bool> is_close;
	std::atomic<int> busy_num_;// 多线程操作,原子变量
public:
	ThreadPool(int thread_num){
		is_close=false;
		for (int i = 0; i < thread_num; i++) {
			arr_thread_.emplace_back(std::thread(Consumer,this));// 本身就是右值,可以不适用std::move,如果是左值而且使用emplace_back的话需要std::move避免thread的复制行为
		}
	}
	ThreadPool():ThreadPool(5){}
	~ThreadPool(){
		is_close=true;
		cond_.notify_all();
		for(auto& thread_now:arr_thread_){
			thread_now.join();
		}
	}
	void Add(const std::function<void(void)>& call_back){
		std::lock_guard<std::mutex> guard(que_mut_);
		task_que_.emplace(call_back);
		cond_.notify_one();
	}
private:
	static void Consumer(ThreadPool* pool){
		if (pool==nullptr) {
			return;
		}
		auto& cond=pool->cond_;
		auto& mut=pool->que_mut_;
		std::function<void(void)> task{nullptr};
		while (1) {
			{// 这个作用域结束que_mut_自动释放,为了避免锁的占用
				std::unique_lock<std::mutex> unique(pool->que_mut_);
				cond.wait(unique,[&]()->bool{return !pool->task_que_.empty()||pool->is_close;});// wait函数首先通过第一个参数拿到锁的控制权,然后不会加锁或者解锁,会等待条件变量的到来,条件变量到来后尝试加锁(多个线程最后只有一个能拿到锁),拿到之后判断第二个参数是否为true,如果为true就继续执行,否则就解开锁继续等待
				if (pool->is_close) {
					return;
				}
				task=pool->task_que_.front();
				pool->task_que_.pop();
			}
			pool->busy_num_++;
			if (task!=nullptr) {
				task();
			}
			pool->busy_num_--;
		}
	}
};
void PrintFunc(int print_num){
	std::cout<<"run:"<<print_num<<std::endl;
}
int main()
{
	ThreadPool pool;
	for (int i = 0; i < 20; i++) {
		pool.Add(std::bind(PrintFunc,i));// std::bind的作用是生成一个新的函数对象,这个对象可以直接提供参数,达到参数简化的目的
	}
	sleep(2);
	return 0;
}


参考

  • 深度探索C++对象模型