【C++】多态的实现及其底层原理

news/2024/5/19 4:24:55

在这里插入图片描述
在这里插入图片描述

个人主页:🍝在肯德基吃麻辣烫

我的gitee:gitee仓库
分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。


文章目录

  • 前言
  • 一、什么是多态?
  • 二、多态的构成条件
    • 2.1什么是虚函数?
    • 2.2虚函数的重写
    • 2.3 什么是虚函数表?
      • 有虚函数的对象的大小
    • 2.4普通对象调用和实现多态后的对象调用
  • 三、多态的原理
      • 经典题
      • 回到多态的两个条件
    • 多态条件的两个特例:(这一点是C++的大坑)
    • 3.1动态绑定和静态绑定
  • 四、默认成员函数和虚函数的关系
    • 4.1构造函数可以设置成虚函数吗?
    • 4.2 析构函数可以设置成虚函数吗?
    • 一道经典面试题
  • 五、单继承和多继承关系中的虚函数表
    • 5.1单继承关系中的虚函数表
    • 5.2多继承关系中的虚函数表
  • 六、抽象类
  • 写在最后的面试题
  • 总结


前言

本文继C++继承之后讲解C++多态。


一、什么是多态?

单单从概念入手不好理解,应该深入理解多态的实现后再回过头来讲解。
现在简单举个例子:我们在购买高铁票时,往往会有成人票全价,学生票半价的优惠,针对不同的人群给予不同的优惠,这个就是多态(多种形态)。

二、多态的构成条件

多态的两个构成条件为:

1.基类的指针或引用
2.满足虚函数的重写

2.1什么是虚函数?

virtual关键字修饰的类成员函数就是虚函数。

在继承中,子类要想重写父类,父类的成员函数必须是虚函数,而子类的成员函数可以不加virtual,但一般建议加上比较合适。

2.2虚函数的重写

虚函数的重写是:对父类的虚函数的实现进行覆盖,覆盖的内容是子类虚函数的实现。

满足重写的条件:三同。

函数名,参数类型,返回值相同的虚函数,就能满足重写的条件。

2.3 什么是虚函数表?

虚函数表是继承体系中,如果一个函数是虚函数,则该函数的地址会存储在一张虚函数表中,而不是存储在对象中,该虚函数表的地址才存储在对象中。通过虚函数表可以找到对应的虚函数,从而能够进一步实现多态。
在这里插入图片描述

有虚函数的对象的大小

class A
{
public:virtual void func1(){}virtual void func2(){}protected:int _a;
};int main()
{cout << sizeof(A) << endl;return 0;
}

请计算上面的代码中,A这个类的大小。
A有两个虚函数,一个成员,实际上大小为8

原因:
虚函数在内存中是存储在虚函数表中,而不是存在类对象中,类对象在内存中存储的是一个虚函数表指针和成员变量。
指针大小是4字节,成员是int类型,则共为8字节。
在这里插入图片描述

虚函数表本质上是一个函数指针数组。
一般建议:如果不实现多态,就不要设置成虚函数

2.4普通对象调用和实现多态后的对象调用

普通对象调用成员函数是在编译期间就确定了地址,而实现多态后的函数是在运行期间才确定地址。因为子类继承父类,子类先拷贝父类的虚函数表的地址,如果某个函数是虚函数,则会在子类的虚函数表中重写改函数的地址。

所以编译器在遇到父类的指针或引用调用子类的函数时,编译器在编译期间无法确定到底调用谁的函数,只能运行起来去子类的虚函数表中查找,是谁的地址就调用谁。

三、多态的原理

我们通过以下例子来看待多态。

class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

首先:子类继承了父类,父类在内存中有一张虚表,存的是虚函数的地址。当子类继承了父类后,子类中也同样有一张虚表,只不过是这张虚表上的虚函数的地址是拷贝父类的,如果子类中有虚函数,且满足重写的条件,则子类的虚函数表的虚函数的地址会被重写覆盖成子类的虚函数的地址。

在这里插入图片描述

当我们用父类的指针或引用调用子类的虚函数时,会去访问子类的虚函数表,当我们调用父类的虚函数时,会访问父类的虚函数表,这样就实现了父类指针指向父类对象就调用父类的虚函数表,指向子类对象就调用子类的虚函数表。在这里插入图片描述

所以多态就是:我们想调用父类的函数就传递父类对象给父类指针/引用,想调用子类的函数就传递子类对象给父类指针/引用。

经典题

这里有一道非常经典的坑人题目:

class A
{
public:virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B : public A
{
public:void func(int val=0){ std::cout<<"B->"<< val <<std::endl;}};int main(int argc ,char* argv[]){B*p = new B;p->test();return 0;}

问:结果输出什么?
对于一个子类指针p指向子类对象,通过这个指针调用函数test,因为子类继承了父类,父类就成了子类的成员,通过父类的虚表找到test函数的地址调用,在test函数中又调用func函数,因为这是B的指针,调用func函数时会去B对象的虚表中找到func的地址,这个地址不再是从父类那里拷贝的func的地址,这个地址已经被重写成子类的func的地址,所以调用的是子类的func函数。
然而,虚函数的重写只是重写函数的实现,函数的接口仍然继承父类的,这个叫做接口继承,所以参数的缺省值仍然是父类的。
结果输出B->1

回到多态的两个条件

多态的两个条件:

1.父类的指针或引用
2.虚函数的重写

(1)为什么不能是子类的对象赋值给父类的对象,而是子类对象赋值给父类的指针/引用?
因为子类对象赋值给父类对象,切片过程中不会拷贝虚表。所以父类对象只能调用父类的虚表,子类对象才能调用子类的虚表,满足不了多态。

(2)为什么子类的对象赋值给父类的对象不会拷贝虚表?
因为如果拷贝虚表,使用子类对象会调用子类的虚函数,使用父类对象也会拷贝子类的虚函数,就乱套了。

(3)为什么不能是子类的指针或引用?因为父类是子类的一部分,父类赋值给子类指针不会切片,就不能获取父类的虚函数表。

需要注意的以下几点:

(1)虚函数存在哪?虚函数表存在哪?
注意:虚函数和普通函数一样,都是存在代码段的,虚函数表也是存在代码段的。
是虚函数地址存在虚表中,虚函数表的地址存在对象中。
我们可以通过打印地址的方式验证一下虚函数表存在代码段:

void test_where()
{Person p;Student s;//栈int a;printf("栈->[%p]\n", &a);//堆区int* ptr = new int;printf("堆->[%p]\n", ptr);//数据段(静态区)static int b;printf("静态->[%p]\n", &b);//代码段(常量区)const char* str = "Hello World";printf("常量区->[%p]\n", str);printf("虚函数表1->[%p]\n", *((int*)&p));printf("虚函数表2->[%p]\n", *((int*)&s));}int main()
{test_where();return 0;
}

思路:获取父类或子类对象的虚函数表指针,也就是取出父类/子类在内存中的前4个字节,将该地址与内存中的栈区,堆区,静态区(数据段),常量区(代码段)的地址进行对比,跟谁的地址比较近,就大致在哪个区域。

(2)子类继承父类后,如果子类也有自己的虚函数,则这些虚函数的地址是按照他们声明的顺序依次存在虚表的最后,因为前面先存父类的虚函数的地址。

多态条件的两个特例:(这一点是C++的大坑)

(1)协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
(这一点是C++的大坑)

(2)析构函数的重写(父类和子类的析构函数名不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

3.1动态绑定和静态绑定

多态可以分为动态多态和静态多态,动态多态也叫做动态绑定,静态多态也叫做静态绑定。

静态绑定有:函数重载
函数重载在编译时进行匹配,cout的自动识别类型的底层也是使用函数重载实现的。
函数重载原理:函数名修饰规则
动态绑定:多态。
编译阶段编译器不知道到底调用子类的虚函数还是父类的虚函数,所以编译器只能通过给定的对象,在运行期间,通过调用虚函数表的地址来进行查看。这个过程叫做动态绑定。
多态原理:虚函数表

四、默认成员函数和虚函数的关系

4.1构造函数可以设置成虚函数吗?

构造函数不能是虚函数,因为虚函数表是在构造函数的初始化列表初始化之后才会生成的。如果构造函数是虚函数,那么调用构造函数的时候要去虚函数表里面找到构造函数的地址来进行调用。那么如何找到虚表呢?这就是一个典型的现有鸡还是先有蛋的问题了。

4.2 析构函数可以设置成虚函数吗?

析构函数可以设置成虚函数,并且强烈建议设置成虚函数。

(1)析构函数加virtual,是不是虚函数?
是,因为在编译器编译阶段,会通过函数名修饰规则,统一将子类的析构函数和父类的析构函数改成:destructor。
(2)为什么要这么处理?

因为在子类析构函数调用完成后必须要调用父类的析构函数对父类的资源释放。设置成虚函数可以完成子类对父类的析构函数的重写。
(3)为什么要让他们构成重写?
因为在下面的场景中,构成重写可以实现父类指针调用子类对象会调用子类的析构,子类析构调用结束会自动调用父类的析构,父类指针调用父类对象会调用父类的析构。从而实现不同对象传递给父类指针/引用,会调用不同对象的函数,实现多态。

如果不设置成虚函数,假如有以下的场景,会出问题:

class Person
{
public:~Person(){cout << "father:~Person" << endl;}
};class Student : public Person
{
public:~Student(){cout << "son:~Student" << endl;delete [] ptr;}protected:int* ptr = new int[10];//new ==> 构造 + operator new()
};int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}

delete p 会处理成以下方式:
p->destructor() + operator delete( p )

普通对象,看当前者的类型。
多态对象,看指向对象的类型。

p是父类对象,不管指向的对象是父类还是子类,都会调用父类的析构函数,在子类申请的空间就得不到释放,会造成内存泄露问题。
如果设置成虚函数,就能够实现虚函数的重写,从而实现多态。

一道经典面试题

设计一个不想被继承的类,如何创建?
(1)构造函数设置成私有

class A
{
private:A(){}};class B : public A
{
public:B(){}};

原理:在继承体系中,子类的构造函数必须先去调用父类的构造函数,这里父类构造设置成私有子类就无法调用了。

不过这里出现一个问题,子类无法调用父类的构造,父类也无法调用自己的构造了。
我们可以写一个函数,在函数里面创建一个父类。

class A
{
private:A Createobj(){return A();}A(){}};

但是又有一个问题:如何调用这个函数呢?因为调用该函数创建对象,而创建对象又需要在函数里面创建。
这里我们可以加一个static解决

class A
{
private:static A Createobj(){return A();}A(){}};int main()
{A a = A::Createobj();
}

这样就可以通过指定类域访问该函数解决。

五、单继承和多继承关系中的虚函数表

5.1单继承关系中的虚函数表

单继承关系中,子类会拷贝父类的虚函数表,如果子类还有自己的虚函数,则该虚函数的地址会放在虚函数表的最后。

我们通过调试窗口无法看到子类的虚函数。在这里插入图片描述
不过我们可以通过获取虚表的地址来打印虚表的各个虚函数的地址。

typedef void(*VFPTR)();class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a = 1;
};class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b = 2;
};void PrintVfptr(VFPTR* arr)
{printf("虚表地址是:[%p]\n", arr);for (int i = 0; arr[i] != nullptr; i++){printf("第%d个虚函数地址是:[%p]\n", i, arr[i]);}printf("\n");}int main()
{Base b;Derive d;int vfptrb = *((int*)&b);int vfptrd = *((int*)&d);PrintVfptr((VFPTR*)vfptrb);PrintVfptr((VFPTR*)vfptrd);return 0;
}
  • 思路:1.先获取虚表的地址,取子类对象的地址,强转成int*,再进行解引用,就取到了子类对象的前4个字节,也就是虚表指针。
  • 2.再强转成(VFPTR*)通过打印该指针指向的内容,即可打印虚表的内容。

5.2多继承关系中的虚函数表

这里有几种猜测,子类的未重写的虚函数会放在第一个继承的父类的虚表中,或者放在其他的父类的虚表中。

我们也可以通过打印地址的方式确定。

typedef void(*VFPTR)();class Base1 
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }private:int b1 = 1;
};class Base2 
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Derive::func2" << endl; }
private:int b2 = 2;
};class Derive1 :public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d = 2;
};void PrintVfptr(VFPTR* arr)
{printf("虚表地址是:[%p]\n", arr);for (int i = 0; arr[i] != nullptr; i++){printf("第%d个虚函数地址是:[%p]\n", i, arr[i]);}printf("\n");}int main()
{Derive1 d;int vfptrb1 = *((int*)&d);Base2* p2 = &d;//自动切片,p2就指向Base2对象的首地址int vfptrb2 = *(int*)p2;//int vfptrb2 = *((int*)((char*)&d + sizeof(Base1)));PrintVfptr((VFPTR*)vfptrb1);PrintVfptr((VFPTR*)vfptrb2);return 0;
}

在这里插入图片描述

通过打印可以看到,子类中未重写的虚函数放在第一个继承的父类的虚表中。

但是这里有一个问题:为什么重写了func1,在Base1的func1的地址和Base2的func1的地址不一样?
在这里插入图片描述

在ptr1和ptr2调用func1的过程中,调用的是Derive的func1函数,因为func1已经被重写了。

而在内存中,ptr1和ptr2指向的地址如下:

在这里插入图片描述

因为ptr1指向的地址刚好是d对象的首地址,ptr1和this指针是重叠的,无需偏移,而ptr2需要偏移Base1字节才与this指针重叠。
这就导致在调用func1函数前,ptr2需要先偏移,才能调用。我们看到的是在偏移之前的ptr2的地址,这就是为什么看到的func1的地址不同,调用的确实同一个函数的原因,实际上ptr2会偏移。

六、抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class
{
public:virtual void func1() = 0{}	
};

比如上面这个就是抽象类。
抽象类的特性:

  • 1.不能实例化出对象,子类继承后也不能实例化出对象,只有重写虚函数才能实例化出对象。
  • 2.可以定义指针或引用.

普通的对象是实现继承,而实现多态的对象是接口继承。

抽象类的作用:间接强制子类虚函数必须重写,否则无法实例化对象。

写在最后的面试题

  1. 什么是多态?
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
  3. 多态的实现原理?
  4. inline函数可以是虚函数吗?
  5. 静态成员可以是虚函数吗?
  6. 构造函数可以是虚函数吗?
  7. 析构函数可以是虚函数吗?
  8. 对象访问普通函数快还是虚函数更快?
  9. 虚函数表是在什么阶段生成的,存在哪的?
  10. C++菱形继承的问题?虚继承的原理?
  11. 什么是抽象类?抽象类的作用?

1.多态分为静态多态和动态多态。
静态多态是函数重载,本质上是函数名修饰规则来完成。
动态多态:继承中的虚函数重写 + 父类指针或引用两个条件完成。
动态多态的本质上是由虚函数表的实现来完成。
2.重载:两个函数必须在同一作用域,函数名和参数类型必须相同。
重写(覆盖):两个函数在父类和子类的作用域,且要求满足函数名,参数类型,返回值必须相同(协变例外),两个函数必须是虚函数。
重定义(隐藏)两个函数在父类和子类的作用域,且要求满足函数名相同,两个父类和子类的重名函数不构成重写就一定是重定义。
3.多态实现的原理:
静态多态:函数名修饰规则
动态多态:虚函数表
4.不可以,因为一个函数如果设置成内联,就是一段代码,没有产生地址,无法将地址放进虚表,就不能是虚函数了。(声明:如果一个内联函数被virtual修饰,那么该函数就会自动忽略inline的属性,而成为一个虚函数。
5. 静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的方式来调用该函数,但这种方式无法访问虚函数表,所以静态成员函数无法放进虚表里,无法实现多态,就没有意义。
6.构造函数不能是虚函数,构造函数不能是虚函数,因为虚函数表是在构造函数的初始化列表中生成的,如果构造函数是虚函数,又得取到虚函数表里面找到构造函数的地址,所以是现有构造函数还是先有虚表呢?
7.析构函数可以是虚函数,并且强烈建议析构函数设置成虚函数。因为父类和子类的析构函数会被编译器统一编译成destructor,有些特殊场景必须实现多态才能解决,请参考本文内容。
8.对象访问普通函数和虚函数是一样快的,因为它们都是在编译期间就确定。但如果是指针对象或引用对象调用,是普通函数快,因为调用虚函数会去到虚表中查找地址,速度会慢一些。
9.虚函数表在编译阶段生成的,存在代码段,具体参考本文章的案例。
10.菱形继承的问题:数据冗余和二义性。菱形虚拟继承原理:生成一张虚基表,存放的是该对象相对于父类对象的偏移量,通过偏移量可以访问父类的成员,不再需要在每个子类中存一份父类的成员。
11.抽象类:具有纯虚函数的类叫做抽象类。而在虚函数后面加上 = 0,就是纯虚函数。抽象类的意义:强制子类进行虚函数的重写,并且抽象类体现了接口继承关系。

总结

多态内容就讲到这里。


http://www.mrgr.cn/p/60863208

相关文章

微服务划分的原则

微服务的划分 微服务的划分要保证的原则 单一职责原则 1、耦合性也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密&#xff0c;其耦合性就越强&#xff0c;模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及…

重生之我要学C++第四天

这篇文章的主要内容是类的默认成员函数。如果对大家有用的话&#xff0c;希望大家三连支持&#xff0c;博主会继续努力&#xff01; 目录 一.类的默认成员函数 二.构造函数 三.析构函数 四.拷贝构造函数 五.运算符重载 一.类的默认成员函数 如果一个类中什么成员都没有&…

揭秘!头条百科词条创建全过程及技巧解析

随着互联网时代的到来&#xff0c;人们获取信息的方式越来越便捷。作为国内领先的信息平台&#xff0c;头条百科成为了很多人查阅知识的首选。然而&#xff0c;如何在头条上创建百科词条&#xff0c;让更多人了解和熟知自己呢&#xff1f;本文伯乐网络传媒将为您揭开这个谜团&a…

C++模拟操作系统睡眠机制

在系统中定义一个变量bHiberable&#xff0c;如果是3分钟内休眠&#xff0c;那么每隔3分钟检测一次这个变量&#xff0c;如果为真&#xff0c;则进入睡眠&#xff0c;如果是假&#xff0c;就把这个标志设置为真。继续等待和检测。 程序阻止操作系统休眠的办法&#xff1a;操作…

基于多任务学习卷积神经网络的皮肤损伤联合分割与分类

文章目录 Joint segmentation and classification of skin lesions via a multi-task learning convolutional neural network摘要本文方法实验结果 Joint segmentation and classification of skin lesions via a multi-task learning convolutional neural network 摘要 在…

【运维】hive 终端突然不能使用:Hive Schema version does not match metastore‘s schema version

文章目录 一. 问题描述二. 常规排查1. 元数据库2. hive-site.xml相关meta连接信息检查 三. 正解 一. 问题描述 进入hive终端&#xff0c;执行如下命令报错&#xff1a; hive> show tables; FAILED: SemanticException org.apache.hadoop.hive.ql.metadata.HiveException: …

flutter 打包iOS安装包

flutter iOS Xcode打包并导出ipa文件安装包 1、 Xcode配置 1、 启动打包 1、 等待打包 1、 打包完成、准备导出ipa 1、 选择模式 1、 选择配置文件 1、 导出 1、 选择导出位置 1、 得到ipa

sql server导入.back文件

使用SQL server官方的连接工具 SQL server Management studio 有两种方式 第一种&#xff1a; 前提是&#xff0c;提前知道数据库名称&#xff0c;建好数据库 以数据库 TEST为例子 右键数据库选型&#xff0c;选择新建数据库 输入数据库名字&#xff0c;点击确定 创建完成之…

Mock-MOCO使用过程

一、jar包下载&#xff1a;https://github.com/dreamhead/moco 二、准备mock的json文件 data.json内容&#xff1a; ####GET请求 [{"description": "response使用Content-Type为charsetGBK编码格式来查看返回信息为中文的内容","request": {&q…

1230. K倍区间(超级详细,小白入!!!)

输入样例&#xff1a; 5 2 1 2 3 4 5输出样例&#xff1a; 6 这个题看到区间两个字&#xff0c;两眼一瞪可能就和前缀和差分有关 做题思路&#xff1a; 通过记录每个[1,r]区间值的和,看它前面是否出现过一个[1,l](l<r),使得[l,r]区间的值能除尽k 有同学就好奇&#xff0c;…

通过RPM方式安装,升级,卸载,以及配置使用MySQL

通过RPM方式安装&#xff0c;升级&#xff0c;卸载&#xff0c;以及配置使用MySQL 一、下载 MySQL是一种开源的关系数据库管理系统&#xff0c;被广泛应用于各种业务应用中。本文将讲解如何下载和安装MySQL的rpm安装包。 下载rmp安装包有多种方式&#xff1a; 1、官网下载 …

视频内存过大如何压缩变小?这个压缩方法了解一下

在日常生活中&#xff0c;不管是日常随手拍的视频还是在工作中遇到的视频文件&#xff0c;在编辑处理的时候&#xff0c;如果视频的内存过大&#xff0c;不仅会占用很大的内存&#xff0c;在传送的时候也会花费很长时间&#xff0c;这时候将视频给压缩一下就可以很好的解决这一…

STM32入门学习之外部中断

1.STM32的IO口可以作为外部中断输入口。本文通过按键按下作为外部中断的输入&#xff0c;点亮LED灯。在STM32的19个外部中断中&#xff0c;0-15为外部IO口的中断输入口。STM32的引脚分别对应着0-15的外部中断线。比如&#xff0c;外部中断线0对应着GPIOA.0-GPIOG.0&#xff0c;…

论文笔记:Fine-Grained Urban Flow Prediction

2021 WWW 1 intro 细粒度城市流量预测 两个挑战 细粒度数据中观察到的网格间的转移动态使得预测变得更加复杂 需要在全局范围内捕获网格单元之间的空间依赖性单独学习外部因素&#xff08;例如天气、POI、路段信息等&#xff09;对大量网格单元的影响非常具有挑战性——>论…

【客户案例】云联壹云助力某保险公司搭建公有云费用管理平台

客户介绍 客户成立于 1996 年 11 月&#xff0c;现已拥有逾 2000 名员工和 12000 名营销员&#xff0c;为 280 万客户提供专业的金融保险服务。在上海、北京、广东、浙江、江苏、四川、山东、福建、重庆、辽宁、天津、湖北、河北、湖南和陕西等地的 50 多个城市稳步发展&#…

数据结构之顺序表

一、概念及结构 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存 储。在数组上完成数据的增删查改。 顺序表一般可以分为&#xff1a; 1. 静态顺序表&#xff1a;使用定长数组存储元素。 2. 动态顺序表&#xff1a;使用动…

骨传导耳机它的工作原理是什么?骨传导耳机长期使用会有脑损伤吗?

骨传导耳机&#xff0c;就是利用头骨和额骨传导声音头骨和额骨传导声音的耳机&#xff0c;这并不是夸大其词&#xff0c;不用惊讶。 骨传导耳机的工作原理其实也很简单&#xff0c;声波通过颅骨、颌骨等头部骨头的振动&#xff0c;直接将声音传到内耳&#xff0c;直接避免接触到…

DataEase开源BI工具安装_数据全量_增量同步_大屏拖拽自动生成_多数据源支持_数据血缘分析---大数据工作笔记0183

我这里用的是Centos7.9安装的 可以通过uname -p来查看一下我们的电脑架构,可以看到是x86_64架构的 我们下第一个,这个是x86架构的,第二个arm架构的 然后解压到/opt/module中 然后再去重命名一下文件夹. 推荐200G 本地模式的功能比较多 推荐100G

立即报名 | AI +Serverless Meetup 上海站 8 月 5 日等你相约!

自 2021 年 5 月后&#xff0c;KubeSphere 社区与上海的各位小伙伴已阔别两年&#xff0c;许久不见&#xff0c;甚是想念&#xff01;2023 年 8 月 5 日&#xff0c;KubeSphere 社区将走进上海组织一场主题为 “AI Serverless” 的 Meetup。此外&#xff0c;云原生也依旧是本次…

windows中注册redis服务启动时报1067错误

注册完redis服务&#xff0c;打开计算机 服务时确实有redis服务存在&#xff0c;但是点击启动时却报1067错误&#xff0c;而命令行用redis-server.exe redis.windows.conf 命令却也可以启动 查看6379的端口也没有被占用&#xff08;netstat -ano | findstr :6379&#xff09; …