多态的实现
虚函数表
- 虚函数表可以理解为就是一个函数指针数组
functype ptr[]
- 虚函数表内部存放的是函数指针,而不是函数地址
- 无论是基类还是子类,(前提有虚函数)都有一个自己的虚函数表,继承时候会直接复制基类的虚函数表
- 重写时候会改写虚函数表里面指针指向的函数地址,达到多态作用
- 子类添加新的虚函数(不是重写,而是新的)会追加在虚函数表最后
- 父类指针拿到子类对象时候,对着表格拿到函数指针,但是实际上拿到的是子类的虚函数表,因此函数指针也是指向子类的,因此实现多态
- 多重继承时候会有多个虚函数表
面向过程区别
- 面向对象是以功能来划分问题,而不是以步骤解决
三大特性
封装
- 类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性
- 增强代码可读性和可维护性
继承
- 代码复用,将这些相同的部分,抽取到父类中,让两个子类继承父类
[!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_lock
和std::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
条件变量和信号量的区别
- 条件变量支持广播方式唤醒等待者;而信号机制不支持,只能一个一个通知
- 条件变量只能结合互斥量做同步用;信号机制除了做同步,还能用于共享资源并发访问加锁
- 条件变量是无状态的,如果唤醒早于等待,则唤醒会丢失;信号机制是有状态的,可以记录唤醒的次数
多线程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++对象模型