C++笔记:OOP三大特性之继承

文章目录

  • 一、继承的概念和定义
    • 1.1 概念
    • 1.2 定义格式
    • 1.3 继承关系和访问限定符
  • 二、基类和派生类对象赋值兼容转换
    • 2.1 类型转换存在临时对象的意义
    • 2.2 赋值兼容转换不会产生临时变量
  • 三、继承中的作用域
  • 四、派生类中的默认成员函数
    • 4.1 构造
    • 4.2 拷贝构造
    • 4.3 赋值重载
    • 4.4 析构
  • 五、继承与友元
  • 六、继承与静态成员
  • 七、复杂的菱形继承和菱形虚拟继承
    • 7.1 单继承与多继承
    • 7.2 菱形继承与虚继承
    • 7.3 虚继承的原理
  • 八、继承的总结和反思

一、继承的概念和定义

1.1 概念

假设现在要去设计一个学生管理系统,从数据库设计的角度来看,系统的整体是由一个个实体和它们之间的关系组成的,典型的三大实体有学生(Student)、教师(Teacher)、学校领导(Leader)。

在面向对象的编程中,通常会使用类来表示这些实体,转换成代码得到如下三个类:
在这里插入图片描述

但是通过观察却发现,有一部分属性和方法是这三个类都有的,仔细一想也可以理解,因为像姓名、年龄、家庭住址、联系电话都是一个人应该具有的基本属性,但是同样的东西却定义多份,这会导致代码冗余。

为了解决代码冗余问题,C++之父Bjarne Stroustrup提出了一种机制,称为 “继承”。这种机制允许将多个类共有的特征提取出来放到一个称为 “父类” 或 “基类” 的类中。然后,其他类可以从这个父类继承这些共有特征,并在自己定义自己独有的成员。这些继承了共有特征的类被称为 “子类” 或 “派生类”。当需要使用父类中的特征时,子类可以直接从父类中获取。

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
public:void SetInfo(){_name = "张三";_age = 55;}
protected:int _stuid; // 学号
};class Teacher : public Person{
protected:int _jobid; // 工号
};int main()
{Person p;Student s;Teacher t;p.Print();s.Print();t.Print();return 0;
}

在这里插入图片描述

【说明】

  • Person 类是父类,Student 类和 Teacher 类是子类。
  • 子类继承了父类之后,子类对象(st)能够使用父类的的成员和方法。

1.2 定义格式

类之间继承的格式如下:
在这里插入图片描述

1.3 继承关系和访问限定符

继承方式有三种:public(公有)继承、protected(保护)继承、private(私有)继承。

访问限定符也有三种:public(公有)访问、protected(保护)访问、private(私有)访问。

基类中不同访问限定符修饰的成员在派生类中的访问权限会发生变化,总结之后会得到如下的一张表:

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

【总结】

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

  3. 实际上面的表格我们进行一下总结会发现,基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),访问限定符的权限上:public > protected > private。

  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

在这里插入图片描述

二、基类和派生类对象赋值兼容转换

2.1 类型转换存在临时对象的意义

int main()
{int i = 97;char ch = 'a';if (ch == i)cout << "等于" << endl;elsecout << "不等于" << endl;return 0;
}

在这段代码中,如果问chi是否相等,答案是肯定相等,但是chi却不是直接进行比较的。

chi在比较过程中,编译器会调用cmp指令,这个指令要求比较对象双方的类型是一致的,但是显然chi的类型不一样,所以ch会发生整型提升,然后产生一个int类型的临时对象,然后临时对象和i进行比较,这样的处理方式不会影响chi本身。

2.2 赋值兼容转换不会产生临时变量

  1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。C++特殊规定,这个赋值过程稿中不会产生临时对象,而是将派生类对象中的基类对象切割出来进行赋值。

在这里插入图片描述

  1. 基类对象不能赋值给派生类对象。
class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:int _No; // 学号
};int main()
{Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;// 2.基类对象不能赋值给派生类对象// error C2679: 二元“=”: 没有找到接受“Person”类型的右操作数的运算符(或没有可接受的转换)sobj = pobj;return 0;
}

三、继承中的作用域

  1. 在继承体系中基类派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
class Person
{
public:void Print(int a){cout << "Person::Print(int a)" << endl;}
protected:string _name = "张三"; // 姓名int _num = 111; // 身份证号
};
class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "身份证号:" << Person::_num << endl;cout << "学号:" << _num << endl;}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.Print();s1.Person::Print(10);return 0;
}

在这里插入图片描述

  1. Student 类的 _num 和 Person 类的 _num 构成隐藏关系,由于就近原则直接访问只会访问到 Student 类的 _num,如果想要访问 Person 类的 _num 要加Person::指定类域.
  2. 两个类中的Print由于同名也构成隐藏关系,默认只会访问Student类的Print方法,访问Person类的Print方法同样需要指定类域。

四、派生类中的默认成员函数

一个类有 6 个默认成员函数,“ 默认 ” 的意思就是指我们不写,编译器会变我们自动生成一个(类的默认构造其实有三个,不过这里默认都是编译器自动生成的),这部分的内容主要是,默认成员成员函数的行为是怎样的,以及当基类没有默认成员函数时该如何显式调用。

事先说明,这里只涉及 6 个默认成员函数中的 4 个。

4.1 构造

  1. 与一般类对象相比,派生类对象多存储了基类部分的成员,由于派生类和基类是独立的两部分,所以派生类对象中的基类部分的成员应当调用基类的构造函数去初始化,而派生类对象中派生类部分的成员则调用派生类自己的构造函数去初始化
  2. 如果派生类没有实现构造函数,编译器会自动生成一个默认的构造函数,该函数对内置类型成员不做处理,对自定义类型成员会去调用它的默认构造函数,对基类部分成员会去调用基类的默认构造函数。
  3. 如果基类没有默认构造函数,那么需要通过初始化列表显式调用基类的构造函数,调用的方式是直接调用,如 Person(name)
  4. 由于基类比派生类先定义,所以初始化列表会先调用基类构造函数再初始化其余成员。
// 基类
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}
protected:string _name; // 姓名
};// 派生类
class Student : public Person
{
public:Student(const char* name, int num): Person(name)	// 1. 父类成员调用父类构造,子类成员调用子类构造;// 2. 父类无默认构造直接显式调用父类构造;// 3. 父类比子类先定义,初始化列表顺序为先父后子, _num(num){cout << "Student()" << endl;}
protected:int _num; //学号
};int main()
{Student s1("jack", 18);return 0;
}

在这里插入图片描述

4.2 拷贝构造

  1. 与默认构造函数的行为类似,我们不实现派生类的拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,该函数对内置类型成员会进行字节序的值拷贝,对自定义类型成员会去调用它的拷贝构造,对基类部分成员会去调用基类的默认拷贝构造函数。
  2. 如果是一个深拷贝的类就需要自己去显式实现拷贝构造函数,对于派生类类成员用对象的派生类部分的成员完成拷贝,对于基类要用派生类对象中基类的那一部分来进行拷贝,函数传参的过程中通过赋值兼容转换将派生类对象中的基类部分切割出来。
  3. 在派生类调用基类的拷贝构造函数要通过初始化列表,调用方式是直接调用,如:Person(s)
// 基类
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}protected:string _name; // 姓名
};// 派生类
class Student : public Person
{
public:Student(const char* name, int num): Person(name)	// 1. 父类成员调用父类构造,子类成员调用子类构造;// 2. 父类无默认构造直接显式调用父类构造;// 3. 父类比子类先定义,初始化列表顺序为先父后子, _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s)		// 1. 与构造类似,拷贝构造也是类名显式调用// 2. 一般的类不需要写,默认生成的拷贝构造对自定义类型完成值拷贝,对自定义类型去调用它的拷贝构造,这里的父类同样也是去调用父类的拷贝构造//	  如果是一个深拷贝的类就需要自己去写拷贝构造,自己写拷贝构造该怎么实现//	  对于子类成员用子类部分的成员拷贝//	  对于父类要用子类对象中父类的那一部分来进行拷贝,可以该怎么把子类对象中父类的那一部分切出来?//    赋值兼容转换!Student对象作为参数传给Person(),传参会进行切片,编译器把子类对象中父类的那一部分切出来形成一个新的Person对象形参, _num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num; //学号
};int main()
{Student s1("jack", 18);Student s2(s1);return 0;
}

在这里插入图片描述

4.3 赋值重载

  1. 我们不显式实现派生类中的赋值重载,编译器自动生成的赋值重载对内置类型进行字节序的值拷贝,对自定义类型调用它的赋值重载,对派生类中的基类部分会去调用基类中的赋值重载。
  2. 由于基类中的赋值重载和派生类中的赋值重载同名,两个函数构成隐藏关系,调用基类的赋值重载需要指定类域。
// 基类
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}
protected:string _name; // 姓名
};// 派生类
class Student : public Person
{
public:Student(const char* name, int num): Person(name)	// 1. 父类成员调用父类构造,子类成员调用子类构造;// 2. 父类无默认构造直接显式调用父类构造;// 3. 父类比子类先定义,初始化列表顺序为先父后子, _num(num){cout << "Student()" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;// 如果不是自己给自己赋值if (this != &s){// 父类的调用父类的来完成赋值Person::operator =(s);// 子类的用子类成员变量完成赋值_num = s._num;}// 最后返回自己的引用return *this;}
protected:int _num; //学号
};int main()
{Student s1("jack", 18);Student s3("rose", 17);s1 = s3;return 0;
}

在这里插入图片描述

4.4 析构

特殊规定:

  1. 派生类析构函数和基类析构函数构成隐藏关系,这是因为多态的原因,析构函数都会被特殊处理,函数名会被处理成destructor()
  2. 为了保证析构顺序是先派生类后基类,基类析构会在派生类析构后自动调用,这是因为先基类后派生类是有安全隐患,可能基类的资源已经被清理了但是由于某些原因,派生类的析构函数又去访问基类的资源,存在野指针的风险。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name)	// 1. 父类成员调用父类构造,子类成员调用子类构造;// 2. 父类无默认构造直接显式调用父类构造;// 3. 父类比子类先定义,初始化列表顺序为先父后子, _num(num){cout << "Student()" << endl;}~Student(){cout << "~Student()" << endl;}
protected:int _num; //学号
};int main()
{Student s1("jack", 18);return 0;
}

在这里插入图片描述

五、继承与友元

继承指的是继承基类的成员,友元不是基类的成员,所以派生类无法继承基类的友元关系。

class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};class Student : public Person
{
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s)
{// Display是Person类的友元,能够访问Person类的_namecout << p._name << endl;// 友元关系不能继承,所以Display不是Student的友元,Display无法访问_stuNum// 如果Display也想访问Student的_stuNum,得在Student类内加上友元声明// error:cout << s._stuNum << endl;
}void main()
{Person p;Student s;Display(p, s);
}

六、继承与静态成员

  1. 静态成员存储在静态区,派生类继承的是访问权,而不是在派生类又生成一份。
  2. 在整个继承体系中,无论派生出多少个子类,都只有这一个static成员实例。
  3. 指定该继承体系中的任一类域都可以访问到static成员。
class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};
int main()
{// 统计创建了多少个对象Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl;// 继承体系中的任意类域都可以访问Student::_count = 0;cout << " 人数 :" << Person::_count << endl;// static成员只会存在一份cout << " Person::_count 地址 :" << &Person::_count << endl;cout << " Student::_count 地址 :" << &Student::_count << endl;return 0;
}

在这里插入图片描述

七、复杂的菱形继承和菱形虚拟继承

7.1 单继承与多继承

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承

在这里插入图片描述

  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

在这里插入图片描述

7.2 菱形继承与虚继承

菱形继承是多继承的一种特殊情况。
在这里插入图片描述
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
在这里插入图片描述

class Person
{
public:string _name; // 姓名
};
class Student : public Person
{
protected:int _num; //学号
};
class Teacher : public Person
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter";	// error C2385: 对“_name”的访问不明确// 显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 Student 类和 Teacher 类继承 Person 类时使用虚拟继承(添加virtual关键字),即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用,换言之,需要注意virtual关键字在菱形继承的哪些类里添加。

class Person
{
public:string _name; // 姓名
};
// 直接基类是Person使用虚继承
class Student : virtual public Person
{
protected:int _num; //学号
};
// 直接基类是Person使用虚继承
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{Assistant a;a._name = "peter";return 0;
}

7.3 虚继承的原理

语法上解决菱形继承只需要使用虚继承即可,但是底层上的解决方法是怎样的?

接下来就用一个简化的菱形继承继承体系并借助内存窗口观察对象成员的模型,来研究虚拟继承原理。
注意:以下探究过程在VS2019上进行,别的编译器使用的解决方案可能在细节上略有差异

class A
{
public:int _a;
};// class B : public A
class B : virtual public A
{
public:int _b;
};// class C : public A
class C : virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
在这里插入图片描述

下图是菱形虚拟继承的内存对象成员模型:

从内存窗口可以分析出:

  1. 原本D类对象d中存在两份的成员_a,被抽出来放到的了对象组成的最下面,此时这个成员_a同时属于B类和C类且只存在一份,解决了数据冗余。
  2. B类和C类中原本存储_a的位置分别被替换成了一个指针,通过指针能够找到一张表,这两个指针被称为虚基表指针,这两张表被称之为虚基表,虚基表中存储的是偏移量,通过偏移量能够找到成员_a

在这里插入图片描述

现有下面两句代码,pb指针该如何找到_a呢?

B* pb = &d;
pb->_a++;

第一,pb指针是一个指向 B 类对象的指针,B* pb = &d;的赋值过程会发生兼容转换,将对象d中的B类部分的指针返回给pb指针,即pb指针的内容是0x00FAF8D0

第二,pb->_a这个操作将会去读取B类中_a存储的内容,由于虚继承的缘故,读取到的是一个虚基表指针,即0x00c07be8,编译器通过虚基表指针找到虚基表,通过虚基表可以直到,现在_a相对于pb指针(0x00FAF8D0)的偏移量是0x14

最后,编译器会在0x00FAF8D0 + 0x14 的地址处找到_a的位置,然后完成++操作。

上面的模型是对象d的模型,其实B类、C类的模型也发生了改变,B类、C类对象访问成员_a时也需要通过虚基表找偏移量,以下面这几句代码为例:

B bb;
B* pb = &bb;
pb->_a++;

在这里插入图片描述

下面是上面的Person关系菱形虚拟继承的原理解释:
在这里插入图片描述

八、继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就可能存在菱形继承,有了菱形继承就有得涉及菱形虚拟继承,而它的底层实现就很复杂。因此,一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的 OOP 语言都没有多继承,如 Java。
  3. 继承被称为 “ 白箱复用 ”,术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  4. 组合被称为 “ 黑箱复用 ”,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
    在这里插入图片描述
  5. 继承和组合的选择
    • public继承是一种 is-a 的关系。 比如说Person类和Student类,学生是一个人。
    • 组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。
    • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号
};class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};// Tire和Car构成has-a的关系
class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸
};class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t; // 轮胎
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/261651.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java多线程】分析线程加锁导致的死锁问题以及解决方案

目录 1、线程加锁 2、死锁问题的三种经典场景 2.1、一个线程一把锁 2.2、两个线程两把锁 2.3、N个线程M把锁&#xff08;哲学家就餐问题&#xff09; 3、解决死锁问题 1、线程加锁 其中 locker 可以是任意对象&#xff0c;进入 synchronized 修饰的代码块, 相当于加锁&…

OpenWRT部署web站点并结合内网穿透实现无公网ip远程访问

文章目录 前言1. 检查uhttpd安装2. 部署web站点3. 安装cpolar内网穿透4. 配置远程访问地址5. 配置固定远程地址 前言 uhttpd 是 OpenWrt/LuCI 开发者从零开始编写的 Web 服务器&#xff0c;目的是成为优秀稳定的、适合嵌入式设备的轻量级任务的 HTTP 服务器&#xff0c;并且和…

尝试一下最新的联合办公利器ONLYOffice

下载下来一起试试吧 桌面安装版下载地址&#xff1a;https://www.onlyoffice.com/zh/download-desktop.aspx) 官网地址&#xff1a;https://www.onlyoffice.com 普通Office对联合办公的局限性 普通Office软件&#xff08;如Microsoft Office、Google Docs等&#xff09;在面对…

Socket通信---Python发送数据给C++程序

0. Problems 很多时候实现某种功能&#xff0c;需要在不同进程间发送数据&#xff0c;目前有几种主流的方法&#xff0c;如 让python和C/C程序互相发送数据&#xff0c;其实有几种方法&#xff1a; 共享内存共享文件Socket通信 在这里只提供Socket通信的例程&#xff0c;共享…

MySQL学习笔记3: MySQL数据库基础

目录 前言目标数据库操作&#xff08;针对database 的操作&#xff09;1. 创建数据库 create database 数据库名;2. 查看数据库 show databases;3. 选中数据库 use 数据库名;4. 删除数据库 drop database 数据库名; mysql中支持的数据类型1. 数值类型: NUMERIC(M,D)2. 字符串类…

如何在OpenWRT安装内网穿透工具实现远程访问本地搭建的web网站界面

文章目录 前言1. 检查uhttpd安装2. 部署web站点3. 安装cpolar内网穿透4. 配置远程访问地址5. 配置固定远程地址 前言 uhttpd 是 OpenWrt/LuCI 开发者从零开始编写的 Web 服务器&#xff0c;目的是成为优秀稳定的、适合嵌入式设备的轻量级任务的 HTTP 服务器&#xff0c;并且和…

【Vue】本地使用 axios 调用第三方接口并处理跨域

前端处理跨域 一. 开发准备 开发工具&#xff1a;VScode框架&#xff1a;Vue2项目结构&#xff1a;vue脚手架生成的标准项目&#xff08;以下仅显示主要部分&#xff09; 本地已搭建好的端口&#xff1a;8080要请求的第三方接口&#xff1a;http://1.11.1.111:端口号/xxx-api…

大型语言模型的语义搜索(一):关键词搜索

关键词搜索(Keyword Search)是文本搜索种一种常用的技术&#xff0c;很多知名的应用app比如Spotify、YouTube 或 Google map等都会使用关键词搜索的算法来实现用户的搜索任务&#xff0c;关键词搜索是构建搜索系统最常用的方法&#xff0c;最常用的搜索算法是Okapi BM25&#x…

Unity基于AssetBundle资源管理流程详解

在Unity游戏开发中&#xff0c;资源管理是一个非常重要的环节。随着游戏的发展&#xff0c;资源会变得越来越庞大&#xff0c;因此需要一种高效的资源管理方式来减少内存占用和加快加载速度。AssetBundle是Unity提供的一种资源打包和加载方式&#xff0c;可以将资源打包成一个独…

JS前端高频面试

JS数据类型有哪些&#xff0c;区别是什么 js数据类型分为原始数据类型和引用数据类型。 原始数据类型包括&#xff1a;number&#xff0c;string&#xff0c;boolean&#xff0c;null&#xff0c;undefined&#xff0c;和es6新增的两种类型&#xff1a;bigint 和 symbol。&am…

使用 Coze 搭建 TiDB 助手

导读 本文介绍了使用 Coze 平台搭建 TiDB 文档助手的过程。通过比较不同 AI Bot 平台&#xff0c;突出了 Coze 在插件能力和易用性方面的优势。文章深入讨论了实现原理&#xff0c;包括知识库、function call、embedding 模型等关键概念&#xff0c;最后成功演示了如何在 Coze…

【EasyV】QGIS转换至EasyV

QGIS转换至EasyV 第一步&#xff1a;导入QGIS第二步 坐标系转换第三步 集合修正第四步 重命名字段第五步 导出WGS geojson坐标第六步 导入EasyV 第一步&#xff1a;导入QGIS 第二步 坐标系转换 第三步 集合修正 第四步 重命名字段 第五步 导出WGS geojson坐标 第六步 导入EasyV…

【Git】:初识git

初识git 一.创建git仓库二.管理文件三.认识.git内部结构 一.创建git仓库 1.安装git 使用yum install git -y即可安装git。 2.创建仓库 首先创建一个git目录。 3.初始化仓库 这里面有很多内容&#xff0c;后面会将&#xff0c;主要是用来进行追踪的。 4.配置name和email 当然也…

ClickHouse快速上手

简介 ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS) 官网(https://clickhouse.com/docs/zh)给出的定义&#xff0c;其实没看懂 特性 ClickHouse支持一种基于SQL的声明式查询语言&#xff0c;它在许多情况下与ANSI SQL标准相同。使用时和MySQL有点相似&#…

Python输出函数有知道的吗?

print()函数主要用于在终端中输出程序结果。它接受可变参数&#xff0c;可输出多个数据&#xff0c;数据之间默认用空格隔开&#xff0c;输出完毕后默认以换行结尾。print()函数还接受sep和end参数来指定数据间隔和结尾符号&#xff0c;以及file参数来指定输出流。 1.print() 函…

一分钟学会MobaXterm当Linux客户端使用

一、介绍 MobaXterm是一款功能强大的远程计算机管理工具&#xff0c;它集成了各种网络工具和远程连接协议&#xff0c;可以帮助用户在Windows系统上轻松管理远程计算机。MobaXterm支持SSH、Telnet、RDP、VNC等多种远程连接协议&#xff0c;同时还集成了X11服务器&#xff0c;可…

爬虫在网页抓取的过程中可能会遇到哪些问题?

在网页抓取&#xff08;爬虫&#xff09;过程中&#xff0c;开发者可能会遇到多种问题&#xff0c;以下是一些常见问题及其解决方案&#xff1a; 1. IP封锁&#xff1a; 问题&#xff1a;封IP是最常见的问题&#xff0c;抓取的目标网站会识别并封锁频繁请求的IP地址。 解决方案…

【解决(几乎)任何机器学习问题】:处理分类变量篇(上篇)

这篇文章相当长&#xff0c;您可以添加至收藏夹&#xff0c;以便在后续有空时候悠闲地阅读。 本章因太长所以分为上下篇来上传&#xff0c;文章末尾有下篇链接 很多⼈在处理分类变量时都会遇到很多困难&#xff0c;因此这值得⽤整整⼀章的篇幅来讨论。在本章中&#xff0c;我将…

C++动态分配内存知识点!

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 大家好呀&#xff0c;又是分享干货的时间&#xff0c;今天我们来学习一下动态分配内存。 文章目录 1.动态分配内存的思想 2.动态分配内存的概念 2.1内存分配函数 2.2动态内存的申请和释放 2.3内存碎片问…

MyBatis小技巧

MyBatis小技巧 一、#{}和${}1.#{}和${}的区别2.什么情况下必须使⽤${} 二、别名机制-typeAliases1.typeAlias2.package 三、mappers的配置1.mapper&#xff08;1&#xff09;resource&#xff08;3&#xff09;URL&#xff08;3&#xff09;class 2.package 四、插⼊数据时获取…