虚函数
虚函数
虚函数机制用以支持一个有效率的“执行期”绑定。
虚函数指针
C++对象模型:
- 每个
class
产生出一堆指向虚函数的指针,放在表格当中。这个表格被称为svirtual table
。- 没一个类对象被安插一个指针,指向相关的
virtual table
,这个指针通常被称为vptr
。vptr
的设定和重置,都由没有给类的构造、析构以及拷贝赋值运算符自动完成。- 每一个类所关联的
type_info object
也经由virtual table
被指出来,通常放在表格的第一个slot
。
虚函数表
C++的多态是通过虚函数来实现的。
有虚函数的类里,会分配一个用以存放虚函数指针的空间,这个虚函数指针指向虚函数表。
而虚函数表里,存的的这个类的虚函数的地址。
没有继承
按照声明的顺序放置虚函数的指针
单继承
子类和父类各有一张虚函数表,子类的表和父类的表相似,但是会覆盖其中
override
的函数
多继承
- 子类的虚函数被放置在声明的第一个基类的虚函数表种
override
时,所有基类中和子类同名的函数都被子类的对应函数覆盖- 内存布局中,父类按照其声明顺序排列
菱形继承
比如C多继承自A和B,而A和B又继承自Base
问题:
会有最底层派生类拥有多个间接父类实例的情况解决:
使用虚继承
虚继承
特点:
- 虚继承的子类:如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针以及一张虚函数表,该虚函数指针位于对象内存的最前面。【非虚继承的话,直接扩展父类虚函数表】
- 虚继承的子类也单独保留了父类的虚函数指针和虚函数表。这部分内容与子类的内容以一个四字节的0来分界
- 虚继承的子类对象中,含有四字节的虚表指针偏移值
构造可为虚?
- 虚函数对应一个虚函数表,如果构造是虚的,就需要通过虚函数表来调用,但是对象都还没实例化,根本没有内存空间,也就找不到虚函数表
- 构造函数不允许是虚函数,因为创建一个对象时我们总要明确指定对象的类型,而如果是虚的,具体的类型要在运行时才能确定
vptr
是在是在构造调用后才确定,所以构造不能为虚
析构可为虚?
- 如果析构不为虚,通过调用析构来释放空间时,可能会导致内存泄漏
多态
编译时多态
重载函数
运行时多态
多态性可以概括为“一个接口,多个方法”,程序运行时才决定调用哪个具象化函数。
多态通过虚函数实现,虚函数允许子类重新定义成员函数,而子类重写定义父类函数的做法叫做覆盖,
override
。
基类派生类转换
结构体对齐
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除
- 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节
- 结构体总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class A { public: int i; }; //4 class B { public; char ch; }; //1 class C { public: int i; short j; }; //4+2+2 class D { public: int i; short j; char ch; }; //4+2+1+1 class E { public: int i; int ii; short j; char ch; char chr; }; //4+4+2+1+1 class F { public: int i; int j; int k; short ii; char ch; char chr; }; //4+4+4+2+1+1 |
vector&&list
区别
vector
封装了顺序表,是一块连续的空间。list
封装了链表,空间不连续
效率
vector
随机访问很快,插入元素尤其是在头部插入元素速度很慢,在尾部插入比较快- list的访问要遍历,会比较慢。插入的话就很快
map底层
- map和set底层实现是红黑树,而红黑树是一颗自平衡的二叉检索平衡树,因而它的元素的键的值不允许重复
用宏或函数反转16进制数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main() { int x = 0x25231234; int count = 0; int result = 0; while (x) { count++; auto temp = x & 255; temp = temp << (4 - count) * 8; result += temp; x = x >> 8; } } |
TCP&&UDP
区别
TCP | UDP | |
是否连接 | 面向连接 | 面向非连接 |
传输可靠 | 可靠 | 不可靠 |
应用场合 | 文件传输 | 即时通讯,在线视频,语音电话 |
速度 | 慢 | 快 |
相关包问题
进程线程
区别
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)
线程互斥同步
同一个进程的线程共享进程内的绝大部分资源,当一段访问这些共享资源的代码块,有可能被多个线程执行时,那么这块代码块就称为临界区
当有多个线程并发的在临界区执行时,程序的执行结果会出现不确定性,这种情况叫做竞态条件
多线程编程中,避免出现竞态条件的一项重要解决方案就是,保证多个线程在临界区是互斥的。所谓互斥,就是指不能同时有多于一个线程进入临界区。
虽然临界区同步速度很快,但却只能用来同步本 进程内的线程,而不可用来同步多个进程中的线程。
使用临界区的步骤:
- 申请一个临界区遍历
CRITICAL_SECTION gSection;
- 初始化临界区
InitializeCritialSection(&gSection);
- 使用临界区
EnterCritialSection(&gSection);
//some code
LeaveCritialSection(&gSection);
- 释放临界区
DeleteCritialSection(&gSection);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
#include <iostream> #include <Windows.h> #include <process.h> using namespace std; typedef struct tagSafePtr { int* pInt; CRITICAL_SECTION cs; }StSafePtr,*PStSafePtr; CRITICAL_SECTION gSection; class auto_lock { public: auto_lock(PCRITICAL_SECTION pcs):_pcs(pcs) { EnterCriticalSection(_pcs); } ~auto_lock() { LeaveCriticalSection(_pcs); } private: PCRITICAL_SECTION _pcs; }; unsigned __stdcall thread1(void* ptr) { EnterCriticalSection(&gSection); cout << "thread 1" << endl; Sleep(2000); PStSafePtr pSafePtr = reinterpret_cast<PStSafePtr>(ptr); LeaveCriticalSection(&gSection); if(pSafePtr) { //在获取被同步的数据之前,先进入临界区,保证同时只有一个线程访问 //EnterCriticalSection(&(pSafePtr->cs)); auto_lock m(&pSafePtr->cs); Sleep(10); delete pSafePtr->pInt; pSafePtr->pInt = 0; cout << "delete ptr" << endl; } return 0; } unsigned __stdcall thread2(void* ptr) { EnterCriticalSection(&gSection); cout << "thread 2" << endl; Sleep(2000); PStSafePtr pSafePtr = reinterpret_cast<PStSafePtr>(ptr); LeaveCriticalSection(&gSection); if(pSafePtr) { //在获取被同步的数据之前,先进入临界区,保证同时只有一个线程访问 auto_lock m(&pSafePtr->cs); int* p = pSafePtr->pInt; if(p) { *p = 100; cout << *p << endl; } else { cout << "p is null" << endl; } } return 0; } int main() { StSafePtr st; //初始化临界区 InitializeCriticalSection(&st.cs); InitializeCriticalSection(&gSection); st.pInt = new int(10); HANDLE h1 = (HANDLE)_beginthreadex(0, 0, thread1, &st, 0, 0); HANDLE h2 = (HANDLE)_beginthreadex(0, 0, thread2, &st, 0, 0); ::WaitForSingleObject(h1, INFINITE); CloseHandle(h1); ::WaitForSingleObject(h2, INFINITE); CloseHandle(h2); delete st.pInt; //清理资源 DeleteCriticalSection(&st.cs); DeleteCriticalSection(&gSection); return 0; } |
保证临界区互斥的重要技术,就是互斥锁。
互斥锁的初始化,有两种方式:
- 静态初始化
- 动态初始化
使用互斥锁,保证临界区互斥的一般思路是:
- 为该临界区分配一把互斥锁
- 任何想要进入临界区的线程都必须先持有该互斥锁
- 持有互斥锁运行于临界区的线程在离开临界区后必须释放该互斥锁
- 用户模式
- 原子操作
- 临界区
- 内核模式
- 互斥量
- 信号量
- 事件
死锁
概念
指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。
此时系统处于死锁状态,或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
形成
- 互斥条件
- 指某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占用
- 不可抢占条件
- 进程锁获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有着手中夺取资源,而只能由该资源的占用进程自行释放
- 占有且申请条件
- 进程至少已经占有一个资源,但有申请新的资源。由于所申请的新的资源已被另一个进程所占有,此时该进程阻塞。但是,它在等待新资源的同时,仍继续占有已有的资源
- 循环等待条件
- 存在一个进程等待序列
{p1,p2,p3,...,pn}
,其中,p1
等待p2
所占用的某一资源,p2
等待p3
所占用的某一资源,......,而pn
又等待p1
所占用的资源,形成一个循环等待环
- 存在一个进程等待序列
解决
预防
死锁的预防是保证系统不进入死锁的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
- 打破互斥条件
- 即允许进程同时访问某些资源
- 打破不可抢占条件
- 允许进程强行从占有者那里夺取某些资源
- 打破占有且申请条件
- 可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需要的全部资源得不到满足,则不分配资源,次进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性的将所申请的资源全部分配给该进程
- 缺点如下:
- 在许多情况下,一个进程在执行之前可能不知道它所需要的全部资源。因为进程在执行时是动态的,不可预测的
- 资源利用率低
- 降低了进程的并发性
- 打破循环等待条件
- 实现资源有序分配策略。即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。
- 缺点如下:
- 现在了进程对资源的请求,同时给系统中所有资源合理编号也是件难事,并增加了系统的开销
- 为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间
避免
死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预测有发生死锁的可能性,则加以避免。
-
安全序列
- 首先引入安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列
{P1,P2,...,Pn}
就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的 - 安全序列
{P1,P2,...,Pn}
是这样组成的:若对于每一个进程Pi,它需要的附加资源可以被系统中当前可用资源加上所有进程Pj
当前占有资源之和所满足,则{P1,P2,...,Pn}
为一个安全序列,这时系统处于安全状态,不会进入死锁状态 - 缺点如下:
- 虽然存在安全序列时一定不会有死锁发生,但是系统进入不安全状态(四个死锁的必要条件同时发生)也未必会产生死锁。当然,产生死锁后,系统一定处于不安全状态
- 首先引入安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列
-
银行家算法
一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。
一个银行家拥有一定数量的资金,有若干个客户要贷款。每个客户须在一开始就声明他所需贷款的总额。若该客户贷款总额不超过银行家的资金总数,银行家可以接收客户的要求。客户贷款是以每次一个资金单位(如1万RMB等)的方式进行的,客户在借满所需的全部单位款额之前可能会等待,但银行家须保证这种等待是有限的,可完成的。
检测与恢复
检测
一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
恢复
- 最简单,最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程
- 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁
- 一次性撤消参与死锁的全部进程,剥夺全部资源
- 逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 2023_02_0502/05
- ♥ 2020_04_2204/23
- ♥ 2020_11_19_0202/17
- ♥ 2022_02_1802/18
- ♥ 2022_02_1602/16
- ♥ 2022_02_24_0103/01