这波确实详细嗷
gck 发布的帖子
-
Qt学习笔记
Qt中事件,信号与槽函数
一、事件
1. 事件循环
//一个简单的Qt窗口程序如下: int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); return a.exec(); }
该窗口可以一直处于打开状态,是因为在
QCoreApplication/QGuiApplication/QApplication
对象中维护了一个QEventLoop
,这个循环被称为主事件循环。执行a.exec()
其实是执行QEventLoop::exec
方法。这个事件循环可以获取windowsystem的事件,将其转换成QEvent
对象,并将其转发到对应的QObject
上。QObject
通过QObject::event(QEvent *e)
方法来处理事件。 事件会被存储在一个事件队列中,同时Qt提供
QEventLoop::processEvents
方法可以让我们在某一事件处理过程中进行其他事件的处理(如果当前的事件处理时间较长会导致GUI卡死,在当前需要较长时间处理的事件处理函数中适当的调用该方法可以避免GUI卡死)。//示例 Widget::Widget(QWidget *parent) : QWidget(parent) { QEventLoop* a = new QEventLoop; //定义按钮a QPushButton *btn1 = new QPushButton; btn1->setParent(this); btn1->setText("a"); //连接槽函数 connect(btn1,&QPushButton::clicked,[=](){ for(int i = 0;i<100;i++) { Sleep(50); //使用该方法防止GUI卡死 a->processEvents(); } }); //定义按钮b QPushButton *btn2 = new QPushButton; btn2->setParent(this); btn2->setText("b"); btn2->move(100,200); connect(btn2,&QPushButton::clicked,[=](){ qDebug()<<"press B"; }); }
可以看到使用该方法后,即使在处理点击a按钮的事件时,按下依然会被处理,不会造成GUI的卡死。
同时,Qt还提供了
QCoreApplication::sendEvent()
和QCoreApplication::postEvent()
方法,来进行手动事件的发送以及手动事件的入列。其中QCoreApplication
是QApplication
的父类QGuiApplication
的父类。其中sendEvent()
函数在事件处理结束后才返回。2. QObject::event方法与QEvent类
以下为
QObject::event
的help page: 该方法没有什么难以理解的部分,help page中展示的示例基本上可以体现用法。
对于
QEvent
类,有以下几个函数需要注意://接收事件 void accept(); //忽略事件 void ignore(); //用于获取accept flag bool isAccepted() const; //用于设定accept flag void setAccepted(bool accepted); bool spontaneous() const;
以下为Qt help page中相关函数的介绍:
最后一个函数可用于判断事件是否是手动发送的,即是否是通过上述的
sendEvent()
和PostEvent()
发送的。 自定义事件的设定:
//在Qt中也可以定义自己的事件,定义大致流程如下: //通过QEvent::registerEventType()函数返回一个可用的事件值。 const QEvent::Type MyType = (QEvent::Type)QEvent::registerEventType(); //自定义事件,在该事件中添加了一个字符串成员,用于传递字符串 //所有的事件都是QEvent的子类,自定义事件时也需要定义为QEvent的子类 class MyEvent : public QEvent { public: MyEvent(QEvent::Type type, QString param) : QEvent(type) { this->param = param; } QString param; }; //该信号的发送需要使用sendEvent()方法或者postEvent()方法 //重写接受到该信号的event函数 bool Widget::event(QEvent *event) { if(event->type() == MyType) { MyEvent *e = static_cast<MyEvent*>(event); qDebug() << e->param; return true; } return QWidget::event(event); }
3. 事件过滤器(EventFilter)
提及事件,不免得提及一下
QObject::eventFilter(QObject* watched,QEnvent* event)
,该方法可用于过滤一些事件,不进行处理,或者进行一些自定义处理。 以下为事件过滤器的两个相关方法help page中的description:
为什么需要事件过滤器:
比如在一个登录界面,你填完其中某一项时,你希望一个快捷键来跳转到下一项,此时可以定义
QLineEdit
的一个子类,并重写其keyPressEvent
函数来进行处理。class MyQLineEdit : public QLineEdit { //... } //重写keyPressEvent函数 void MyQLineEdit::keyPressEvent(QKeyEvent *event) { //假设快捷键为Key_Space if (event->key() == Qt::Key_Space) { jumpNextEdit(); } else { QLineEdit::keyPressEvent(event); } }
但如果各项使用的不是相同的控件,而是比如comboBox等等,此时需要对每一种控件进行继承,再重写函数,会十分复杂。
此时我们可以通过重写登录界面对象中的
EventFilter
,来进行处理。例:
class UserRegisterWindow :public QObject { Q_OBJECT //... protected: bool eventFilter(QObject *obj,QEvent *event) override; } //重写函数 bool UserRegisterWindow::eventFilter(QObject *obj,QEvent *event) { //此处也可以根据obj来进行处理,用来进一步的保证事件的正确处理 if(event->type()==QEvent::KeyPress/*过滤的信号类型*/) { //对应的一些处理 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_Space) { jumpNextEdit(); return true; } } else { return QObject::eventFilter(obj,event); } } UserRegisterWindow::UserRegisterWindow(QWidget *parent = nullptr) { //... //注册EventFilter usernameEdit->installEventFilter(this); ageBox->installEventFilter(this); //... }
此时,发送到usernameEdit和ageBox等的事件会先在
UserRegisterWindow::eventFilter
中进行处理。通过该对象来监听控件的事件。 事件过滤器是在一个对象中定义事件过滤器,再将这个对象注册到需要被监测的对象中(使用
installEventFilter
方法),在本例中,事件过滤器是定义在父窗口中,被注册到了对应需要的子窗口。从事件派发转发来看,可能会被理解为事件是先被发送给了父窗口,再转发给子窗口,这可能与Qt中事件是向上转发违和,导致有些难以理解。通过下文的实现机理可以很好的理解事件过滤器实现过程中,事件的派发转发流程。实现机理:
在所有Qt对象的基类: QObject中有一个类型为QObjectList的成员变量,名字为eventFilters,当某个QObject (qobjA)给另一个QObject (qobjB)安装了事件过滤器之后, qobjB会把qobjA的指针保存在eventFilters中. 在qobjB处理事件之前,会先去检查eventFilters列表, 如果非空, 就先调用列表中对象的eventFilter()函数. 一个对象可以给多个对象安装过滤器. 同样, 一个对象能同时被安装多个过滤器, 在事件到达之后, 这些过滤器以安装次序的反序被调用. 事件过滤器函数( eventFilter() ) 返回值是bool型, 如果返回true, 则表示该事件已经被处理完毕, Qt将直接返回, 进行下一事件的处理; 如果返回false, 事件将接着被送往剩下的事件过滤器或是目标对象进行处理.
(Qt5.15中未在help page中找到该成员变量,故该机理存疑,但该机理可以很好的用来理解事件过滤器的实现)
二、信号与槽函数
1. 信号与槽函数的优势
信号与槽函数其实都是函数,有别于常见的回调,使用信号和槽函数可以保证类型安全,槽函数的参数个数必须小于等于信号函数的参数并且类型得相同,同时,信号和槽函数相比于回调函数来说耦合程度 更低,回调处理方法中处理函数必须明确知道哪个函数被回调。
个人看来,信号与槽函数的实现可以理解为使用回调加一个映射表。
信号和槽函数的定义
class MyClass:public QOblect {fun_ Q_OBJECT public: //... private: //... signals: //定义信号 void signal_a(/*params*/); public slots: //定义槽函数 void slot_a(/*params*/); }
信号是可以自定义的,只需要加上关键字
signals:
即可。 信号的发送可以使用关键字
emit
,也可以直接调用信号函数。2. 信号和槽函数的连接
- 使用connect函数(connect函数也可以实现信号和信号的连接),有时可以使用lambda表达式来设定匿名函数为槽函数(如果该槽函数只需连接该信号时会显得比较简便)。
- (个人感觉本质上信号和槽函数都只是一个public方法,没啥区别~所以信号也可以连接信号),不过注意信号函数的实现是由Qt自己完成的。
- 一个信号连接多个槽函数时,按照connect的顺序依次调用。
三、小结与补充
-
系统获取到用户的操作后,向Qt程序发送信息,在Qt中,先检查是否有事件过滤器install在
QApplication
对象(QApplication
也是一个QObject
的子类)上,如果有,则进行处理,然后由QApplication::notify()
进行分析,处理信号,将其封装成QEvent对象,发送给对应的QObjest
对象。 -
对于一个普通的
QObjest
对象而言,也是先检查是否install事件过滤器,若无或者事件未被过滤器处理,则通过event()
函数进行处理,调用对应的事件处理函数,部分对应的事件处理函数会发送一些对应的信号。 -
Qt中和事件相关的函数一般分为两类:
第一类:
QApplication::notify(), QObject::eventFilter(), QObject::event()
这类通过返回值来表示信号是否已经处理,对于未处理的事件,这个事件将会向上转发给它的parent
。第二类
QEvent::ignore() 或 QEvent::accept()
这两个函数一般用于忽略事件或者阻断事件。 -
不同事件有不同的处理方式,部分事件会发送信号,此时我们需要设定槽函数来对这种信号来处理,相当于说捕获该信号。
-
事件和信号均可以自定义,事件继承
QEvent
类,信号只需要在对应的QObject
中声明即可。
-
UML类图介绍
UML图介绍
UML图的大致结构
UML图一般用于表示程序中类之间的关系,下面给出UML图的一个样例:
该图片截图自《大话设计模式》
类和接口的定义与表示
类的定义和表示
该矩形框表示一个类(Class),分为三层
- 第一层为类名,抽象类可用 斜体 表示
- 第二层为该类的一些属性和一些字段
- 第三层为该类的方法
其中,public用 “+” 表示,private用 “-” 表示,protected 用 “#” 表示。
接口的定义和表示
接口和类的定义类似,仅仅在类名的上面增加了一个
interface
标识。类,接口之间的关系
继承
继承关系用实线加三角形来表示,三角形指向的是父类,父类中实现的方法和属性在子类中不用重新表示。
接口实现
接口实现是用虚线加三角形来表示的,三角形指向的是被实现的接口。接口中的方法由于一般需要重写,所以需要在子类中表示。
关联关系
当一个类需要知道另一个类时,可以用关联方式来表示这种关系。
关联关系时用一个带箭头的实现来表示的,箭头指向的为被关联的类。
聚合关系
聚合关系表示的是一种弱拥有关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。
聚合关系是由一个空心菱形和一条带箭头的实线来表示的。箭头指向的为被聚合的类。
组合(合成)关系
合成(组合)关系是一种强拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期相同。
组合关系是由一个实心菱形和一条带箭头的实线来表示的。箭头指向的为被组合的类。
同时在连线边的1,2为基数,表示这端的类可以有几个实例,如果一个类可以有无数个实例,则可以用n表示。关联关系,聚合关系也可以有基数。
依赖关系
依赖关系体现一个类需要有另一些类的存在。
依赖关系用虚线表示,箭头指向被依赖的类。
-
vs著名插件reshaper
Reshaper使用
介绍
这周研究了下著名生产力插件Reshaper,下面给出一些在设置该插件过程中的了解。
不保证正确,仅一些个人理解(英语水平计算机水平有限,部分单词可能理解不到位)
ReShaperC++ 的设置
环境配置(Environment)
环境设置中设置一些ReShaper的基本环境
代码检查设置中会设置一些代码的提示,对于一些情况的对应处理等等
代码编辑设置中即为设置一些代码编辑方面的配置。
General的配置
User Interface中,前两个是选择一些图标主题,这里直接选 Automatic selection
第三个为在状态栏显示内存管理,开启后会在visual studio右下方出现一个内存管理的相关状态。
后面的设置不清楚了就没管了2333
Keybroad配置
键盘方案这里我选择了IDEA的快捷键。最后一个选项最好勾上,连按三次左Ctrl可以看到快捷键配置。
Editor配置
Appearance
第一项是高亮设置
第二项是神器:Alt-Enter的提示
第三项是标记栏,一般选择第三个:标记栏嵌入滑动栏中。
第四项目前不太清楚
Behavior
第一项为输入帮助
第二项为括号等的设置,全选。
第三项由于用于写C++,关闭
第四项为一个快速导航项,使用快捷键快速切换到代码块相应的括号(应该是,没用过)
Visual Studio Features
第一项为一些visual studio自带的命令的设置,全选,使用Reshaper的重构。
第二项为visual studio自带的代码分析,选择第二项:关闭其自带的灯泡提示。
后面是三个Tips的选项,这里只替换C++中的Tooltips。
Inlay Hints
第一项:参数名提示,保持默认。
第二项:类型名提示,保持默认。
Search&Navigation配置
均为一些基本配置,保持默认即可。
注意快捷键:
快捷键 效果 <Ctrl>+<shift>+<left-mouse-click> open the result in peek view <Ctrl>+<Alt>+<left-mouse-click> go to implementation <mid-mouse-click> go to declaration IntelliSense配置
General
选择使用ReShaper。
Autopopup
自动代码补全,基本上保持默认即可。
接下来是一些补全相关的设定,个人使用了默认值。
Performance Guide配置
该项为一些偏好设置,每一项都有具体的解释,在此不作说明,个人保持默认。
其余各项
其余各项均为一些有关Reshaper更新,扩展,网络等的配置。
代码检查配置(Code Inspection)
Settings
一些相关的全局配置,保持默认。
Inspection Severity
该选项选择开启一些代码审查标准,一般用于在 [Alt+Enter]中提示并修改。
该选项比较关键,具体可以看各自标准的描述,同时在写代码时,鼠标悬浮在有提示的代码上,也可以得到是哪个审查标准在给予提示。
可以在使用过程中慢慢修改,打造成自己的喜欢的方式。
其余选项
code inspection 中的其余选项暂未使用过。
代码编辑设置
Members Generation
一些成员代码生成的设定,doc的生成中可以选择None,则生成常规的文档。
Code Cleanup
代码cleanup的一些设置。
Context Actions
重点设置!!!
选择你需要的一些功能,这些功能可以体现在万能的[Alt+Enter]上。
很多项都很有必要,很多互相转换的选项可以根据喜好自己选择一项来Convert。
语言个性化设置
重点设置!!!
这里设置一些有关语言的个性化设置,比如命名规则,一些函数格式,逻辑代码结构格式等等的设置。
其余各项
暂未了解。
总结
Reshaper的使用关键在于快捷键,需要多使用,才能体会到其方便之处。
[Alt-Enter]是最重要的快捷键,能解决需要的大部分问题!!
-
Typora+PicGo+GitHub图床
Typora+PicGo+GitHub图床
要求说明
- 有相关软件(Typora 和 PicGo)
- 有一个GitHub账号
- 如果需要加速和稳定需要一个国外的云主机。
过程
1. 配置GitHub
- 首先在GitHub中创建一个仓库。
- 在GitHub个人设置界面(如下图)中点击
generate new toekn
生成一个新的token。权限中勾选上 repo 选项。
注意保存这个token,该token只会出现一次,以后如果遗忘只能再次重新生成一个token来使用了。
至此,GitHub上的配置结束,对于这其中配置的细节问题,网上还有一些更加详细的教程,可以进行一些参照。
2.配置PicGo
使用PicGo自带的Github图床
由于个人不是使用其自带的Github图床配置,所以该处没有写相关配置。
大致介绍一下配置的要求:
- 仓库名:安装 用户名/仓库名 的格式来进行填写,比如:
cc-Gao/personal-image-hosting-web-site
。 - 分支名:可以直接使用master分支。
- Token:填写之前GitHub配置中获得的token。
- 存储路径:可以不填写,填写的话会在你的仓库下新建一个文件夹用来存储图片
- 设定自定义域名:可以不填写,这样PicGo生成的访问链接就直接是GitHub 的访问链接,此处建议采取CDN加速,利用jsDelivr CDN加速访问(jsDelivr 是一个免费开源的 CDN 解决方案)此处填写:
https://cdn.jsdelivr.net/gh/用户名/图床仓库名
来对访问链接进行CDN加速,此时生成的链接应当为https://cdn.jsdelivr.net/gh/用户名/图床仓库名/图片路径
。
PicGo插件:Web图床
如果在直接使用GitHub图床时,尽管返回的链接经过CDN加速,但在上传时,可能由于网络不稳定等等问题导致无法上传成功,此时我们也可以对上传过程进行一些加速。(该方法参照了别人的博客,在后面我会贴出该博客的地址,具体的申请过程该博客中更为详细)
-
整一个国外的虚拟主机,有很多地方可以白嫖,个人使用的网址是
https://www.000webhost.com/
该网址需要科学一下。
-
整好主机后进行主机的文件管理界面(如下图)
-
配置自动转发的php文件:
项目地址: https://github.com/kjhuanhao/autoPicCdn
下载该项目后,将该项目中的up.php文件进行修改,
define("REPO","仓库");//必须是下面用户名下的公开仓库 define("USER","github用户名");//必须是当前GitHub用户名 define("MAIL","xxxxxxxx@qq.com");//该项目前来看没啥用 define("TOKEN","token");github中获得的token(注意需要写权限write:packages前打勾)
修改完成后,将该文件上传至public_html文件夹下。
-
获取该主机的域名
访问以下网址:
https://www.000webhost.com/members/website/list
把对应的网址复制下来。
- 下载PicGo插件:Web图床
打开PicGo,在插件设置中,搜索插件:web-uploader,安装插件(安装该插件需要nodejs版本足够高,如果nodejs版本过低会导致安装失败)
然后设置配置如下:
其中API地址填写之前获取的地址 + /up.php即可。
此时已经完成了PicGo的配置。
参考链接:https://mrhuanhao.cn/2020/03/28/solvepicnet/
3. Typora配置
目前Typora也已经支持PicGo图床,打开Typora,选择文件、偏好设置、图像、上传服务设定。
在上传服务中选择PicGo,再选择PicGo的exe路径,即完成配置,此时就可畅享GitHub图床了。
对于插入图片中的相关配置可以根据个人喜欢来进行对应的配置,(建议设为上传图片,再关闭对网络位置的图片应用上述规则,这样当截图后复制到markdown文件中即可自动上传)
-
C++学习笔记
C++学习
本文中记录了一些个人在学习C++过程中的一些疑惑和对这些疑惑的探究过程。在此作一个总结,由于个人能力不足,可能在其中可能会存在一些问题,欢迎交流。
C++中的引用
引用是C++新增的一项重要机制,它使得C++中本生需要指针实现的功能有了另一种实现方法。但引用的本质是什么呢?它跟指针之间有什么联系呢?
下面给出一段代码:
int main() { int a = 1; int& p1 = a; int* const p2 = &a; int b1 = p1; int b2 = *p2; p1 = 8; *p2 = 10; }
将上面这段代码在VS2019Debug模式下进行反汇编,得到如下(仅截取部分相关代码):
3: int a = 1; 00FD17F2 C7 45 F4 01 00 00 00 mov dword ptr [a],1 ;将1存到a所在的空间 4: int& p1 = a; 00FD17F9 8D 45 F4 lea eax,[a] ;将a的地址存进eax 00FD17FC 89 45 E8 mov dword ptr [p1],eax ;将eax的值(即a的地址)存入p1 5: int* const p2 = &a; 00FD17FF 8D 45 F4 lea eax,[a] ;将a的地址存进eax 00FD1802 89 45 DC mov dword ptr [p2],eax ;将eax的值(即a的地址)存入p2 6: ;读取 7: int b1 = p1; 00FD1805 8B 45 E8 mov eax,dword ptr [p1] ;将p1的值(a的地址)放入eax 00FD1808 8B 08 mov ecx,dword ptr [eax] ;将eax的值,即a的值放入ecx 00FD180A 89 4D D0 mov dword ptr [b1],ecx ;将ecx的值存到b1 8: int b2 = *p2; 00FD180D 8B 45 DC mov eax,dword ptr [p2] ;将p1的值(a的地址)放入eax 00FD1810 8B 08 mov ecx,dword ptr [eax] ;将eax的值,即a的值放入ecx 00FD1812 89 4D C4 mov dword ptr [b2],ecx ;将ecx的值存到b2 9: ;修改 10: p1 = 8; 00FD1815 8B 45 E8 mov eax,dword ptr [p1] ;将p1的值(a的地址)放入eax 00FD1818 C7 00 08 00 00 00 mov dword ptr [eax],8 ;修改eax值对应的地址处的数据为8 11: *p2 = 10; 00FD181E 8B 45 DC mov eax,dword ptr [p2] ;将p2的值(a的地址)放入eax 00FD1821 C7 00 0A 00 00 00 mov dword ptr [eax],0Ah ;修改eax值对应的地址处的数据为10
注:
lea指令和mov指令的区别:
lea eax,[ebx+8] ;该语句表示把ebx的地址加8存到eax,(存进去的可以理解为还是一个地址) mov eax,[ebx+8] ;该语句表示把ebx+8后对应地址的值存进eax
由上可以看出,在底层实现中,引用被当作指针来进行处理,读取和修改的操作均与指针无差异。
但由于引用不能重新指向另一个变量,所以在理解上可以将其当作指针常量。
函数的重载
C++与C的一个重要区别就在于,C++可以根据参数的不同来进行重载,但C++是如何实现重载的呢?
函数重载的汇编实现
下面给出一段代码:
int Fun(int i) { i = i + 1; return i; } double Fun(double i) { i = i + 1; return i; } int main() { int a = 1; double b = 1.5; int a2 = Fun(a); double b2 = Fun(b); }
将上面这段代码在VS2019下进行反汇编,得到结果如下:
15: int a = 1; 00FE1808 C7 45 F8 01 00 00 00 mov dword ptr [a],1 16: double b = 1.5; 00FE180F F2 0F 10 05 D8 7B FE 00 movsd xmm0,mmword ptr [__real@3ff8000000000000 (0FE7BD8h)] 00FE1817 F2 0F 11 45 E8 movsd mmword ptr [b],xmm0 17: 18: int a2 = Fun(a); 00FE181C 8B 45 F8 mov eax,dword ptr [a] 00FE181F 50 push eax 00FE1820 E8 A1 FA FF FF call Fun (0FE12C6h) ;call Fun函数,地址为(0FE12C6h) 00FE1825 83 C4 04 add esp,4 00FE1828 89 45 DC mov dword ptr [a2],eax 19: double b2 = Fun(b); 00FE182B 83 EC 08 sub esp,8 00FE182E F2 0F 10 45 E8 movsd xmm0,mmword ptr [b] 00FE1833 F2 0F 11 04 24 movsd mmword ptr [esp],xmm0 00FE1838 E8 0B FB FF FF call Fun (0FE1348h) ;call Fun函数 ,地址为(0FE1348h) 00FE183D 83 C4 08 add esp,8 00FE1840 DD 5D CC fstp qword ptr [b2]
可以看出,这两个函数所指向的地址已然不同,但在这之中并不明白它是如何实现的。
在此我们使用gcc -O0 -S 指令,对该cpp文件进行编译,得到最终编译结果如下:
.file "test1.cpp" .text .globl _Z3Funi .type _Z3Funi, @function _Z3Funi: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) addl $1, -4(%rbp) movl -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size _Z3Funi, .-_Z3Funi .globl _Z3Fund .type _Z3Fund, @function _Z3Fund: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movsd %xmm0, -8(%rbp) movsd -8(%rbp), %xmm1 movsd .LC0(%rip), %xmm0 addsd %xmm1, %xmm0 movsd %xmm0, -8(%rbp) movsd -8(%rbp), %xmm0 popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size _Z3Fund, .-_Z3Fund .section .rodata .align 8 .LC0: .long 0 .long 1072693248 .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits
从上述代码可以看出,代码中的Fun函数,在汇编中被重命名为了_Z3Funi 和 _Z3Fund。
不难发现,在gcc中,是通过将函数根据参数重命名来实现函数重载,i表示int,d表示double。如果是自定义的结构体,从测试结果来看,gcc是使用全称来进行重命名。(重命名的方式可能随着编译器的不同有一些不同,但机制应该是相同的)
cpp中调用c中函数的问题
此时不免会引出一个问题,那么 .cpp 文件中如何调用 .c 文件中的函数呢?C++中编译时对于函数的命名方式不同,可想而知对于函数的调用方式来说也会不同,但C语言中对于函数的编译并无相应的处理,这之间必然产生冲突。
//test2.h #pragma once int Fun(int i); //test2.c #include "test2.h" int Fun(int i) { i = i + 1; return i; } //test.cpp int main() { int a = Fun(10); return 0; }
将以上代码编译调试运行会导致报错一个无法解析的外部符号:
可以看出,这种冲突确实存在。
对此,C++引入了
extern "C"
用于解决这类型的冲突。对上方代码的头文件进行修改,即可使得对应的函数按照C函数的方式编译。
//test2.h #pragma once #ifdef __cplusplus extern "C" { #endif int Fun(int i); #ifdef __cplusplus } #endif //test2.c #include "test2.h" int Fun(int i) { i = i + 1; return i; } //test.cpp int main() { int a = Fun(10); return 0; }
对于该头文件,添加对应的extern “C” { ……},将需要按C语言编译方式的函数框选进去,即可实现.cpp 调用 .c 中的函数。
对象内存模型
C++面向对象的三大特征:封装、继承、多态。在个人看来多态可以说是最难但也同时是最能体现其优势的特征了。
提及多态,常会说:子类指针可以直接赋给父类类型的指针。也就是说,向上的类型转换(子类转换成父类)是类型安全的。
但同时,C++中存在一种private 关键词,它可以定义类的私有属性,这些属性是不可被子类继承的,那么为什么C++可以保障其向上的类型转换是类型安全的呢?
简单类继承中的类型安全
如上图,如果是非private属性,可以很轻松的理解,子类指针指向的空间只有可能比父类大,而不存在比父类小的可能,而且对应的属性位置也同样相同,所以对于指针的操作来说也同样可行。
但此时如果父类中存在private属性呢(实际上大多数属性都是设置为private属性),为什么还是可以保证类型安全呢?
使用如下代码进行测试:
#include <iostream> class Parent { public: char* name; int age; private: int weight; }; class Child :public Parent { private: int some_field; }; using namespace std; int main() { cout << sizeof(Parent) << endl; cout << sizeof(Child) << endl; }
该代码中,子类继承了父类的public属性,没有继承其private属性,同时自己增加了一个新的属性,故应当有三个属性,大小均为4字节,最后输出为12字节。
最后的输出结果:
与最初设想的不同,子类的大小为16字节而不是12字节。
故此,可以猜想到实际上该属性被继承了下来,仅仅是编译器拒绝了你对其的访问。
实际上,在vs监视中可以看到:
可以看出,实际上子类中维护一个父类的对象。这样也更好的理解了为什么在调用子类构造函数时,需要先调用父类的构造函数——因为实际上子类中就维护了一个父类对象。同样也可以理解,为什么在子类析构时,先进行子类的析构函数,再进行父类的析构函数,因为需要先析构自己的属性,再析构父类的对象,在析构父类对象时,必然需要调用父类的析构函数。
多继承中的类型安全
由上面,我们可以看出,在单继承中,子类先在内存中维护了一个父类对象,然后再开辟自己新增属性的空间,将指向子类型的指针,转换成指向父类型完全是可以实现的而且不会有任何访问越界问题。
但如果是多继承呢?
看下面这段代码:
#include <iostream> class A { public: int a; }; class B { public: int b; }; class C: public A,public B { public: int c; }; using namespace std; int main() { C* p = new C; //测试1 cout << (A*)p << endl; cout << (B*)p << endl; cout << p << endl; //测试2 A* pA = (A*)p; B* pB = (B*)pA; cout << pA << endl; cout << pB << endl; return 1; }
该段代码中,C类继承A类和B类。
通过监视:
可以得知C类对象的内存结构如图:
那么,如果将该对象类型转换为A类的指针,可以很容易实现,那么如果希望其转换为B类的指针呢,C++中又会如何实现呢?
我们运行上述程序,得到输出结果如下图:
前三行输出对应的是 (A*)p (B*)p p 的值,可以看到,在将p指针强行转换为B*类型时,指针的数值自动增加了4,即增加了1个A对象的大小,故在此可以看出,在多继承中,子类指针转换为父类时,他会进行一些相关的指针偏移运算。
再看4,5行的输出结果,这两行表示的是将p强行转换为(A*),再将A* 转换为 B*.
可以看出,在对于(A*)类型的指针来说,将其转换为(B*)是不会进行一些相关的指针偏移的,即不能在两个父类之间转换。
在此时就可能会发生一些有意思的事情,比如 A类中有一个虚函数表,其中第一个函数对应的函数为FunA() , 同时在B类中同样内存偏移的位置也有一个虚函数表,此时由于类型转换后,会导致尽管看上去是B类型指针,而且成功调用了B类中的函数,但实际上调用的为FunA(),因为这两者的内存访问方式相同。
//class A class A{ public: virtual void FunA(); //other method or field } class B{ public: virtual void FunB(); //other method or field } class C:public A,public B { } int main() { C* p = new C; A* pa1 = (A*)p; B* pb1 = (B*)p; pa1->FunA(); pb1->FunB(); //上述两者访问的是各自的函数 A* pa2 = (A*)p; B* pb2 = (B*)pa2; pa2->FunA(); pb2->FunB(); //上述两者访问的是同一个函数FunA() }
那么如何解决这个容易发生的冲突呢。在C++ 中引入了 动态交叉类型转换函数:
B* pb = dynamic_cast<B*>(pa)
将上述代码修改:
int main() { C* p = new C; A* pa1 = (A*)p; B* pb1 = (B*)p; pa1->FunA(); pb1->FunB(); //上述两者访问的是各自的函数 A* pa2 = (A*)p; B* pb2 = (B*)pa2; pa2->FunA(); pb2->FunB(); //上述两者访问的是同一个函数FunA() A* pa3 = (A*)p; B* pb3 = dynamic_cast<B*>(pa3); pa3->FunA(); pb3->FunB(); //此时两者访问的又为各自的函数 }
注:
dynamic_cast<>()
函数直接对于多态类进行使用,即被转换的类必须继承自其他类或者其必须含有虚函数。菱形继承(虚基类)
网上常说 C++ 设定的多继承不好,为什么不好呢,大部分人都会提及 会导致菱形继承问题,那么,菱形继承会导致什么问题呢,C++中又是如何实现解决的呢?
所谓菱形继承,就是在多次继承中,某一个类继承了多个类,而这多个类又同时继承了同一个类,导致可能存在的数据冲突的问题。
如下算是一种经典的菱形继承关系:
如果动物中定义了一个属性 age ,那么马和驴都会有这个属性,若骡继承了马和驴的话,会导致其内部维护了两个 age 属性(如下图)。这就会导致内存的浪费,同时访问上也会更加复杂。
那么C++中是如何解决这个问题的呢?
看下面的代码:
#include <iostream> using namespace std; class Animal { public: int age; }; class Horse:virtual public Animal { }; class Donky:virtual public Animal { }; class Mule:public Horse,public Donky { }; int main() { Mule* p = new Mule; return 1; }
在这段代码中新增加了2个关键词:
virtual
新增后,可以发现:类Mule 的内存结构发生了一些改变:
相比于上面的结构,改结构中可以看到多了一个Animal类,那么为什么会多这个Animal类呢?这不是会占用更多的内存吗?
在原代码的基础上再进行进一步测试。
class Animal { public: int age; }; class Horse:virtual public Animal { int H; }; class Donky:virtual public Animal { int D; }; class Mule:public Horse,public Donky { }; int main() { cout << sizeof(Animal) << endl; cout << sizeof(Horse) << endl; cout << sizeof(Donky) << endl; cout << sizeof(Mule) << endl; return 1; }
增加了一些属性,同时输出各个类的大小,可以得到结果:
- Animal:4个字节
- Horse:12个字节
- Donky:12个字节
- Mule:20个字节
以上输出结果有两个异常,相比于常规的没有增加virtual关键字的类来说,马和驴类多了4个字节,同时,在结构上来看骡类应该为28个字节,但此处只有20个字节。
通过vs2019开发者命令行工具,查询到了Mute类的结构如下图:
可以发现多出了一个叫做 vbptr 的东西,这个东西称作虚基类指针,指向基类。
故此,可以很好的理解,为什么Horse 和 Donky 是12个字节,而Mule 是20个字节了。
Horse和Donky 中 前4个字节为一个虚基类指针,指向Animal类,再加上自己属性的4个字节,共计12个字节。
对于Mule来说,他前16个字节维护了Horse 和Donky 中的虚基类指针和 自身的属性,再在最后维护了一个Animal类,故共有20个字节。
此时,由于Horse 和 Donky 中的虚基类指针指向同一个对象,故此时不会造成在一个Mule中维护了两个age属性,此时也不用通过作用域来访问对应的age属性,消除了继承的同名冲突。
虚继承和虚基类指针的设计,是为了解除多继承中产生的这些菱形继承问题,但实际上使用起来还是较为不便,因为大部分情况你无法得知哪些类需要设计为虚继承,哪些类不需要,所以实际编写C++中还是尽量避免这种类型的继承方式。