【C++】面向对象之多态

2023-11-02

文章内的所有调试都是在vs2022下进行的,

部分小细节可能因编译器不同存在差异。

多态的定义和实现

概念引入

对于一个火车票售票系统,

可能会有多重角色,

比如普通成人类、学生类、军人类、儿童类等等…

这些类可能都是从某个基类派生出来的,

而且每个类都有一个基本需求,就是买票,

所以对于同一个购票函数BuyTicket()

当不同的类去调用它时它应该执行不同的功能,

比如成人要全价卖票,学生可以半价买票,军人得优先买票…

所以怎样满足这一需求呢?

通过多态的机制。

所以多态其实就是不同继承关系的类实例化出来的对象去调用同一函数

最终用同一个函数了执行不同的动作

感觉其实有点儿函数重载的意味…


多态的构成条件

首先见一见多态是个什么情况。

这是一段代码:

class Person
{
public:
    virtual void BuyTicket()
    { cout << "原价买票" << endl; }
};

class Student : public Person
{
public:
    virtual void BuyTicket()
    { cout << "半价买票" << endl; }
};

class Solider : public Person
{
public:
    virtual void BuyTicket()
    { cout << "优先买票" << endl; }
};

void QueryPriority(Person* p)
{ p->BuyTicket(); }

int main()
{
    Person p;
    QueryPriority(&p);

    Student stu;
    QueryPriority(&stu);

    Solider solider;
    QueryPriority(&solider);

    return 0;
}

运行结果如下:

image-20230428172555184

虚函数重写

在讲多态的构成条件之前要先引入一个虚函数的概念。

虚函数就是用virtual修饰的函数,

继承一文中已经初步见识过virtual关键字了,

当时是用virtual进行虚拟继承,

在菱形继承中避免数据二义性和冗余问题,

这里是用来修饰函数使之成为虚函数,

作为多态的构成条件之一。

所以构成多态的第一个条件是被调用的函数必须是虚函数,

在上面的例子中被调用的函数是BuyTicket()函数,

所以它要定义成虚函数:

class Person
{
public:
    virtual void BuyTicket()
    { cout << "原价买票" << endl; }
};

这样的话BuyTicket()函数会被继承到派生类中,

我们当然可以不加virtual

直接在派生类中重载BuyTicket()函数,

此时派生类中就有了两个BuyTicket()函数,

一个是派生类中重载的,一个是基类的,

这两个函数是构成隐藏关系的。

于是就有了数据冗余和二义性…

而我们只想保留一份函数,

基类对象调用时执行基类中定义的行为,

派生类对象调用时执行派生类中定义的行为。

显然重载是完成不了这个任务的,

所以取而代之的就是重写

class Student : public Person
{
public:
    virtual void BuyTicket()
    { cout << "半价买票" << endl; }
};

class Solider : public Person
{
public:
    virtual void BuyTicket()
    { cout << "优先买票" << endl; }
};

此前在基类和派生类中重名的成员变量或成员函数是构成隐藏/重定义关系的,

但那是对于普通函数而言,

满足一定条件的虚函数则是会构成覆盖/重写关系,

意味着在派生类中只有这么一个函数存在,

继承下来的基类的函数完全被覆盖掉了。

上面说的满足一定条件

前提一定是虚函数

此外除了函数名相同

函数的返回值类型和参数列表也要相同

但是返回值类型相同还有例外,

基类与派生类的虚函数返回值类型可以不相同,

但一定要是继承关系中基类或派生类的指针或引用,

举个简单的例子:

class Student : public Person
{
public:
    virtual Student* BuyTicket()
    { cout << "半价买票" << endl; }
}

class Solider : public Person
{
public:
    virtual Solider* BuyTicket()
    { cout << "优先买票" << endl; }
}

这种情况下仍然构成多态,

(这么鸡肋的写法应该没人用吧)…

以上就是构成多态的其一条件:

被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写


通过基类的指针或者引用调用虚函数

诸如上面的多态调用:

Person p;
Person* ptr = &p;
ptr->BuyTicket();

Student stu;
ptr = &stu;
ptr->BuyTicket();

Solider solider;
ptr = &solider;
ptr->BuyTicket();

想要实现函数的多态调用,

首先函数一定是重写过的虚函数,

再就是要通过基类指针或者引用来调用。

至于为什么,在后面的多态原理细细阐述。

此前在继承一文中提出过一个问题:

如果这么定义了一个对象(此处省略类的定义):

int main()
{
	A* p = new B;
    delete p;
    
    return 0;
}

此时运行结果如下:

此时只调用了A的析构,

对B的部分成员并没有处理,

因此造成了内存泄漏!

那么现在就可以解决这个问题,

就是将析构函数定义成虚函数

在析构时会多态调用B类的析构函数,

就不会发生内存泄漏了。

这里有一个细节,

前面提到函数构成重写的条件之一是函数名必须相同,

而析构函数显然不符合这个条件,

但为什么又能实现重写呢?

实际上编译器偷偷对析构函数进行了处理,

统一将析构函数处理成同名函数。


override和final

C++对函数重写的要求比较严格,

但是有些情况下由于疏忽,

可能会导致函数名字母次序写反而无法构成重写,

而这种错误在编译期间是不会报出的,

只有在程序运行时没有得到预期结果才来debug会得不偿失,

因此,C++11提供了overridefinal两个关键字,

可以帮助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写

class Person
{
public:
    virtual void BuyTicket() final
    {
        cout << "原价买票" << endl;
    }
};

class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }
};
image-20230425164921861

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "原价买票" << endl;
    }
};

class Student : public Person
{
public:
    virtual void BuyTickte() override
    {
        cout << "半价买票" << endl;
    }
};

上面故意错把派生类中的函数名写错了。

image-20230425165651144

抽象类

概念

在虚函数的后面写上=0

则这个函数为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类),

抽象类不能实例化出对象。

派生类继承后也不能实例化出对象,

只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,

另外纯虚函数更体现出了接口继承。

class Person
{
public:
    virtual void BuyTicket() = 0
    {
        cout << "原价买票" << endl;
    }
    
    void func()
    {}
};
image-20230425170133159

实现继承和接口继承

普通函数的继承是一种实现继承,

派生类继承了基类函数,可以使用函数,

继承的是函数的实现。

虚函数的继承是一种接口继承,

派生类继承的是基类虚函数的接口,

目的是为了重写,达成多态,

继承的是接口。

所以如果不实现多态,

不要把函数定义成虚函数。


虚函数表

64位地址太长,

所以为了方便观察,

下面统一换成32位地址。

现在定义一个基类:

class Base
{
public:
    virtual void Func1()
    {
    	cout << "Func1()" << endl;
    }
private:
	int _b = 1;
};

然后定义一个Base对象,

那这个对象的对象模型是怎样的呢?

相比于没有虚函数的普通对象多了一个_vfptr

image-20230426161739843

看样子是一个数组,

数组元素都是指针,

那看来_vfptr是一个指针数组:

image-20230426162434042

这里显示它有一个元素,

这个元素存放的是Func1函数的地址。

多出来的这个_vfptr虚函数表指针

完整应该叫做visual function table pointer

虚函数表我们一般简称为虚表,

注意和继承中的虚基表的概念区分开,

它存放的其实就是虚函数的地址

注意虚函数并不存放在这儿,

虚函数和普通函数一样是存放在代码段的

所以一个含有虚函数的类中都至少都有一个虚表指针。


单继承中的虚表

我们再进一步看在继承中虚表是怎样的,

在上面代码的基础上我们再写点东西:

class Base
{
public:
	virtual void Func1()
	{ cout << "Base::Func1()" << endl; }
    
	virtual void Func2()
	{ cout << "Base::Func2()" << endl; }
    
	void Func3()
	{ cout << "Base::Func3()" << endl; }
    
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{ cout << "Derive::Func1()" << endl; }
    
	virtual void Func4()
	{ cout << "Derive::Func4()" << endl; }
private:
	int _d = 2;
};

此时再看一下它们的对象模型有什么变化:

image-20230426175251849

基类对象的结构还是原来那样,

只不过虚表中多了一个指向Func2函数的指针,

Func3并不在这儿,因为它不是虚函数。

派生类继承了基类的虚表,

但是存的指针有些变化,

可以看到_vfptr[0]存放的是被重写后的Func1的地址。

所以就可以下一个简单的结论:

派生类先继承基类的虚表,

如果派生类重写了基类中某个虚函数,

用派生类自己的虚函数覆盖虚表中基类的虚函数。

那派生类自己的虚函数呢?

按理来说派生类有虚函数,

实例化出来的对象也应该有一个虚基表,

但是这里好像并不是这样。

实际上,

派生类自己新增加的虚函数,

会按其在派生类中的声明次序增加到基类虚表的最后,

和基类共用一个虚表。

而这里继承的基类的虚表没有显示出派生类的虚函数,

这是编译器的监视窗口故意隐藏了这个函数,

也可以认为是他的一个小bug。

那么我们如何查看d的虚表呢?


打印虚表

我们既然有虚表指针_vfptr

那我们肯定就有办法打印它指向的虚表的内容,

也就是各个虚函数的地址。

下面就来看一下怎样获取。

首先在对象的存储结构中虚表指针存放在最上面,

也就是对象头四个字节(64位指针是八个字节),

所以对象模型如下:

image-20230427161200348

因为我们最终要访问的指针类型是函数指针,

所以我们可以先typedef一下这个函数指针类型:

typedef void(*VFPTR)()

我们先取d的地址:&d

&d此时的类型是Derive*

我们需要对其进行类型转换,

我们看到_vfptr的类型是void**,

所以对其进行类型转换:(void**)&d

void*在32位平台下是4个字节,64位平台下是8个字节,

所以对void**解引用就可以访问头4/8个字节的空间。

然后对其解引用找到虚表:*(void**)&d

此时就拿到了虚表指针,但它的类型是void*

而虚表是一个函数指针数组,

所以我们再做一次类型转换就拿到了可以访问数组元素的虚表指针:(VFPTR*)(*(void**)&d)

所以我们就可以通过下标访问访问到函数指针:((VFPTR*)(*(void**)&d))[0] -> Derive::Func1()

我们还可以通过拿到的函数指针调用函数:((VFPTR*)(*(void**)&d))[0]()

所以现在我们可以通过下面的代码遍历虚表,

打印虚表中存放的函数指针,

并通过函数指针调用函数,

看看是哪个函数:

for (int i = 0; i < n; i++)  // n是虚表中有几个函数指针
{
    cout << ((VFPTR*)(*(void**)&d))[i] << "->";
    ((VFPTR*)(*(void**)&d))[i]();
}

这里有三个虚函数,所以n就是3

运行结果如下:

image-20230427164259380

验证了此前所说的。

我们还可以看一下基类对象的虚表:

image-20230427164523671

多继承中的虚表

看下面的代码:

class Base1 
{
public:
	virtual void func1() 
    { cout << "Base1::func1" << endl; }
    
	virtual void func2() 
    { cout << "Base1::func2" << endl; }
    
private:
	int _b1 = 1;
};

class Base2 
{
public:
	virtual void func1() 
    { cout << "Base2::func1" << endl; }
    
	virtual void func2() 
    { cout << "Base2::func2" << endl; }
    
private:
	int _b2 = 2;
};

class Derive : public Base1, public Base2 
{
public:
	virtual void func1() 
    { cout << "Derive::func1" << endl; }
    
	virtual void func3()
    { cout << "Derive::func3" << endl; }
    
private:
	int _d1 = 3;
};

此时派生类是继承了两个基类的派生类,

那它现在的对象模型是怎样的呢?

image-20230427171825648

可以看到普通多继承的场景下它完整继承了两个基类的虚表指针,

两个基类中都有Func1函数,

可以看到此时Func1函数都被重写后的函数覆盖了。

那派生类它自己的虚函数呢?

这里直接给出结论:多继承派生类的虚函数放在第一个继承基类部分的虚函数表中

所以第一个虚表中存放了三个函数指针,

分别指向Derive::Func1()Base1::Func2()Derive::Func3()

第二个虚表中存放了两个函数指针,

分别指向Derive::Func1()Base2::Func2()

所以对象模型如下:

image-20230427173632364

可以通过打印虚表来验证一下。

这里直接对d取地址可以拿到Base1::_vfptr

但是要怎么拿到Base2::_vfptr呢?

我们可以直接让指针偏移sizeof(Base1)个字节,

也就是把&d改为(char*)&d + sizeof(Base1)

结果如下:

image-20230427174559132

虚表的存储

我们现在再明确一下概念,

虚函数是函数,

和普通函数一样,

存放在代码段

虚表是一个指针数组,

存放指向虚函数的指针。

而类实例化出来的对象中存放的是一个虚表指针,

是指向虚表的指针。

所以对象中虚表指针存放在哪是很明确的,

就看对象存放在哪,

对象在栈上,那它的虚表指针也在栈上,

对象在堆上,那它的虚表指针就在堆上。

那问题来了,虚表存在哪呢?

我们可以通过一个简单的比对来看看:

Derive d;
Derive* pd = new Derive;

cout << "栈: " << &d << endl;

Derive* pd = new Derive;
cout << "堆: " << pd << endl;

cout << "代码段: " << ((VFPTR*)(*(void**)&d))[0] << endl;

cout << "d的虚表地址: " << (void*)*(void**)&d << endl;

cout << "pd的虚表地址: " << (void*)*(void**)pd << endl;

栈、堆、代码段上的空间都是连续的,

我们我们可以将虚表的地址和它们进行比较:

通过对比可以发现虚表是存放在代码段的,

而且无论是临时对象还是动态开辟的对象,

都是共用一个虚表

当一个c++程序编译成可执行程序之后,

此时虚表已经形成了,

和函数一样存放在代码段。

当我们实例化对象时,

对应的构造函数会对对象的虚表指针进行初始化,

将虚表的地址写入到虚表指针中,

所以虚表是编译完就有了的,

而虚表指针是运行时才有的。


多态的原理

前面我们看了虚函数表,

那这个虚函数表和多态调用有什么密不可分的关系吗?

下面以文章开头的那段代码为例进行讲解:

class Person
{
public:
	virtual void BuyTicket()
	{ cout << "原价买票" << endl; }
};

class Student : public Person
{
public:
	virtual void BuyTicket()
    { cout << "半价买票" << endl; }
};

class Solider : public Person
{
public:
	virtual void BuyTicket()
	{ cout << "优先买票" << endl; }
};

void QueryPriority(Person* p)
{ p->BuyTicket(); }

int main()
{
	Person p;
	Student stu;
    Solider solider;
    
    QueryPriority(&p);
	QueryPriority(&stu);
	QueryPriority(&solider);

	return 0;
}
image-20230428173300287

首先我们有一个基类指针,

当我们使用这个指针去调用虚函数时,

会去访问这个基类指针指向的对象的虚表指针,

然后通过虚表指针找到虚表,

在虚表中找到对应的函数然后调用。

如果调用的函数不存在于虚表中,

则会发生报错,

最简单的就是使用基类指针去调用派生类自己定义的虚函数。

在文章虚表的存储部分最后说了,

对象的虚表指针是在构造函数中初始化的,是运行时才有的,

在程序运行期间,

根据具体拿到的类型确定程序的具体行为,

调用具体的函数,

这就是所谓的动态绑定,也叫动态多态。

我们可以通过汇编代码看一下普通调用和多态调用时的区别:
在这里插入图片描述

与动态绑定相对的是静态绑定,也叫静态多态,

我们常用的函数重载就是一种静态多态,

是在在程序编译期间确定了程序的行为。


几个小问题

  1. 内联函数(inline)可以是虚函数吗?
    可以。
    不过编译器就忽略inline属性,
    这个函数就不再是inline,
    因为虚函数要放到虚表中去。

  2. 静态成员可以是虚函数吗?
    不能。
    因为静态成员函数没有this指针,
    使用类型::成员函数的调用方式无法访问虚函数表,
    所以静态成员函数无法放进虚函数表

  3. 构造函数可以是虚函数吗?
    不能。
    因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
    如果构造函数定义成虚函数,
    那想调用构造函数就要去虚函数表中寻找,
    而虚表指针还没有初始化,
    就找不到构造函数了。

  4. 对象访问普通函数快还是虚函数更快?
    首先如果是通过"对象.函数"的方式去调用,
    是一样快的。
    如果是指针对象或者是引用对象,
    则调用的普通函数快,
    因为调用虚函数时还需要先到虚函数表中去查找。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】面向对象之多态 的相关文章

  • 无法使用已与其底层 RCW 分离的 COM 对象。在 oledb 中

    我收到此错误 但我不知道我做错了什么 下面的代码在backrgroundworker中 将异常详细信息复制到剪贴板 System Runtime InteropServices InvalidComObjectException 未处理 通
  • 访问私人成员[关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 通过将类的私有成员转换为 void 指针 然后转换为结构来访问类的私有成员是否合适 我认为我无权修改包含我需要访问的数据成员的类 如果不道德 我
  • C# 和 Javascript SHA256 哈希的代码示例

    我有一个在服务器端运行的 C 算法 它对 Base64 编码的字符串进行哈希处理 byte salt Convert FromBase64String serverSalt Step 1 SHA256Managed sha256 new S
  • pthread_cond_timedwait() 和 pthread_cond_broadcast() 解释

    因此 我在堆栈溢出和其他资源上进行了大量搜索 但我无法理解有关上述函数的一些内容 具体来说 1 当pthread cond timedwait 因为定时器值用完而返回时 它如何自动重新获取互斥锁 互斥锁可能被锁定在其他地方 例如 在生产者
  • UML类图:抽象方法和属性是这样写的吗?

    当我第一次为一个小型 C 项目创建 uml 类图时 我在属性方面遇到了一些麻烦 最后我只是将属性添加为变量 lt
  • Newtonsoft JSON PreserveReferences处理自定义等于用法

    我目前在使用 Newtonsoft Json 时遇到一些问题 我想要的很简单 将要序列化的对象与所有属性和子属性进行比较以确保相等 我现在尝试创建自己的 EqualityComparer 但它仅与父对象的属性进行比较 另外 我尝试编写自己的
  • 当 contains() 工作正常时,xpath 函数ends-with() 工作时出现问题

    我正在尝试获取具有以特定 id 结尾的属性的标签 like span 我想获取 id 以 国家 地区 结尾的跨度我尝试以下xpath span ends with id Country 但我得到以下异常 需要命名空间管理器或 XsltCon
  • 如何将图像和 POST 数据上传到 Azure 移动服务 ApiController 终结点?

    我正在尝试上传图片and POST表单数据 尽管理想情况下我希望它是json 到我的端点Azure 移动服务应用 我有ApiController method HttpPost Route api upload databaseId sea
  • C 预处理器库

    我的任务是开发源分析工具C程序 并且我需要在分析本身之前预处理代码 我想知道什么是最好的图书馆 我需要一些重量轻 便于携带的东西 与其推出自己的 为什么不使用cpp这是的一部分gcc suite http gcc gnu org onlin
  • Json.NET - 反序列化接口属性引发错误“类型是接口或抽象类,无法实例化”

    我有一个类 其属性是接口 public class Foo public int Number get set public ISomething Thing get set 尝试反序列化Foo使用 Json NET 的类给我一条错误消息
  • 指针减法混乱

    当我们从另一个指针中减去一个指针时 差值不等于它们相距多少字节 而是等于它们相距多少个整数 如果指向整数 为什么这样 这个想法是你指向内存块 06 07 08 09 10 11 mem 18 24 17 53 7 14 data 如果你有i
  • 在 ASP.NET Core 3.1 中使用包含“System.Web.HttpContext”的旧项目

    我们有一些用 Net Framework编写的遗留项目 应该由由ASP NET Core3 1编写的API项目使用 问题是这些遗留项目正在使用 System Web HttpContext 您知道它不再存在于 net core 中 现在我们
  • 如何将图像路径保存到Live Tile的WP8本地文件夹

    我正在更新我的 Windows Phone 应用程序以使用新的 WP8 文件存储 API 本地文件夹 而不是 WP7 API 隔离存储文件 旧的工作方法 这是我如何成功地将图像保存到 共享 ShellContent文件夹使用隔离存储文件方法
  • vector 超出范围后不清除内存

    我遇到了以下问题 我不确定我是否错了或者它是一个非常奇怪的错误 我填充了一个巨大的字符串数组 并希望在某个点将其清除 这是一个最小的例子 include
  • 如何衡量两个字符串之间的相似度? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 给定两个字符串text1 and text2 public SOMEUSABLERETURNTYPE Compare string t
  • 从库中捕获主线程 SynchronizationContext 或 Dispatcher

    我有一个 C 库 希望能够将工作发送 发布到 主 ui 线程 如果存在 该库可供以下人员使用 一个winforms应用程序 本机应用程序 带 UI 控制台应用程序 没有 UI 在库中 我想在初始化期间捕获一些东西 Synchronizati
  • Process.Start 阻塞

    我正在调用 Process Start 但它会阻止当前线程 pInfo new ProcessStartInfo C Windows notepad exe Start process mProcess new Process mProce
  • Validation.ErrorTemplate 的 Wpf 动态资源查找

    在我的 App xaml 中 我定义了一个资源Validation ErrorTemplate 这取决于动态BorderBrush资源 我打算定义独特的BorderBrush在我拥有的每个窗口以及窗口内的不同块内
  • x86 上未对齐的指针

    有人可以提供一个示例 将指针从一种类型转换为另一种类型由于未对齐而失败吗 在评论中这个答案 https stackoverflow com questions 544928 reading integer size bytes from a
  • 限制C#中的并行线程数

    我正在编写一个 C 程序来生成并通过 FTP 上传 50 万个文件 我想并行处理4个文件 因为机器有4个核心 文件生成需要更长的时间 是否可以将以下 Powershell 示例转换为 C 或者是否有更好的框架 例如 C 中的 Actor 框

随机推荐

  • 创建docker容器时设置hosts内容

    方式1 在使用docker run运行一个新的容器的时候 通过参数 add host来添加域名和IP信息到容器的 etc hosts文件中 例如 docker run add host hostname 111 111 111 1 name
  • 在Mac OS中配置CMake的详细图文教程

    CMake是一个比make更高级的跨平台的安装 编译 配置工具 可以用简单的语句来描述所有平台的安装 编译过程 并根据不同平台 不同的编译器 生成相应的Makefile或者project文件 本文主要介绍在Mac OSX上安装配置CMake
  • 在html中如何使div在页面中居中显示

    在html中如何使div在页面中居中显示 最近无聊中又再温习了下html 发现好多东西都忘了 尝试着写了一个html网页 结果就连div如何在页面中居中显示都查了好久才弄出来 其实我不知道为什么这样可以实现 因为css还没仔细研究过 等我参
  • watermark前端js盲水印添加方法

    1 引入js 2 实例化对象
  • java获取一个文件的md5码

    什么是文件的MD5码 MD5信息摘要算法 英语 MD5 Message Digest Algorithm 一种被广泛使用的密码散列函数 可以产生出一个128位 16字节 的散列值 hash value 用于确保信息传输完整一致 每个文件对应
  • [知识蒸馏]

    知识蒸馏目的 需要关注的是上图的 attention transfer channel wise knowledge distillation for dense prediction ppt来源于 精读AI论文 知识蒸馏 哔哩哔哩 bil
  • 在计算机中安装Arch Linux

    Arch Linux是一个优秀的linux操作系统 其优点是滚动发行 软件包比较新 并且可以深度可定制 缺点是由于深度可定制 所以开始安装的时候只有命令行可用 对于linux新手来说会有一些难度 我也是练习安装了好多次 才逐渐从安装过程中学
  • Java比较器(Comparator接口)

    Comparator接口的使用 定制排序 1 当元素的类型没有实现java lang Comparable接口而又不方便修改代码 或者实现了java lang Comparable接口的排序规则不适合当前的操作 那么可以考虑使用 Compa
  • go语言中的读写锁以及协程通信

    0 需求分析 在一个读操作远大于写操作的时候 用读写锁 读写锁与互斥锁的运行时长相比 两个goroutine间的通信 写goroutine写完以后通过channel发一个消息给读goroutine 读goroutine收到以后开启200个并
  • Anaconda常用操作(亲测有效果)

    文章目录 Anaconda简绍 一 Anaconda下载安装 二 常用命令 1 创建自己的虚拟环境 2 查看所有的环境 3 切换环境 4 退出环境 5 卸载环境 总结 Anaconda简绍 Conda是一个开源的包 环境管理器 可以用于在同
  • CentOS下安装AMP

    1 安装mysql shell gt groupadd mysql shell gt useradd g mysql mysql shell gt tar zxvf mysql tar gz shell gt cd mysql VERSIO
  • GD32F4xx适配OpenHarmony问题踩坑记录

    1 时钟配置导致的串口乱码 现象描述 最新在适配GD32产品时 发现使用外部晶振时调试串口输出内容会出现乱码 使用内部晶振则不会出现这个问题 配置如下所示 后来研究了半天 发现GD32除了配置system gd32f4xx h中的时钟倍频宏
  • (已实测解决)Intel Optane(tm) Memory Pinning 无法加载DLLiaStorAfsServiceApi.dll:找不到指定模块。是怎么回事如何解决

    Intel Optane Memory Pinning 无法加载DLL iaStorAfsServiceApi dll 找不到指定模块 异常来自HRESULT 0x8007007E 更新windows10后 发现打开文件夹时总是弹出一个窗口
  • explicit和QObject的禁止拷贝

    常用的mainwindow h中对构造函数是这样声明的 explicit MainWindow QWidget parent 0 为什么有个explicit 其实是为了禁止构造函数的隐式转换 构造函数如果只有一个参数 那么存在一个隐式转换
  • 深度解析“区块链+物联网”与新基建

    根据国家发改委对 新基建 的界定 区块链属于新技术基础设施 物联网属于通信网络基础设施 同属于 新基建 的代表 区块链与物联网之间可能擦出什么样的火花 需要哪些软硬件支持 这是本文要讨论的问题 区块链 物联网 的核心问题 区块链兼有信息互联
  • 165. 比较版本号

    165 比较版本号 题目描述 给你两个版本号 version1 和 version2 请你比较它们 版本号由一个或多个修订号组成 各修订号由一个 连接 每个修订号由 多位数字 组成 可能包含 前导零 每个版本号至少包含一个字符 修订号从左到
  • 最挑战程序员的9大任务,你都干过吗?

    那些非程序员认为软件开发是非常困难的 确实如此 但这种困难不像那些外行人理解的那样 最近在 Quora 上的一次讨论 程序员分享了他们认为工作中的最大困难 在这里为大家精选出其中的 9 个 1 怎样才是最佳解决方案 任务描述 给你一系列的需
  • 容器化部署的微服务 远程调试(debug)

    一 项目微服务的部署采用如下方式 1 微服务的jar包 使用dockeFile文件 创建为镜像image 2 利用该镜像创建一个容器 3 启动容器 微服务即启动 二 远程调试 debug 设置 1 idea 本地设置 2 编辑 创建镜像的d
  • 2018-互联网优质资源汇总

    Jamin s Blog 个人网站 虽然时间比较早 但对很对ios的主题进行深入的讲解 HTTPS HTTPS进阶 APP启动 并发编程RunLoop 等 Spring Boot 汇总 微笑很纯洁 CSDN博主 Spring Boot资源的
  • 【C++】面向对象之多态

    文章内的所有调试都是在vs2022下进行的 部分小细节可能因编译器不同存在差异 文章目录 多态的定义和实现 概念引入 多态的构成条件 虚函数重写 通过基类的指针或者引用调用虚函数 override和final 抽象类 概念 实现继承和接口继