【C++】—— 继承(上)

【C++】—— 继承(上)

  • 1 继承的概念与定义
    • 1.1 继承的概念
    • 1.2 继承定义
      • 1.2.1 定义格式
      • 1.2.2 继承父类成员访问方式的变化
    • 1.3 继承类模板
  • 2 父类和子类对象赋值兼容转换
  • 3 继承中的作用域
    • 3.1 隐藏规则
    • 3.2 例题
  • 4 子类的默认成员函数
    • 4.1 构造函数
      • 4.1.1 父类有默认构造
      • 4.1.2 父类没有默认构造
    • 4.2 拷贝构造
      • 4.2.1 不需要自己显式写
      • 4.2.2 自己显式写
    • 4.3 赋值重载
    • 4.4 析构函数
      • 4.4.1 重载
      • 4.4.2 顺序
    • 4.5 实现不能被继承的类
      • 4.5.1 法一:设为私有
      • 4.5.2 法二:final
    • 4.6 总结

1 继承的概念与定义

1.1 继承的概念

  继承(inheritance)机制是面向对象设计使代码可以复用的最重要的手段,它允许我们在保存原有特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

  下面我们通过一个例子来初步感受一下继承:

class Student
{public :// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...} // 学习void study(){// ...}
protected:string _name = "peter"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄int _stuid; // 学号
};class Teacher
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...} // 授课void teaching(){//...}
protected:string _name = "张三"; // 姓名int _age = 18; // 年龄string _address; // 地址string _tel; // 电话string _title; // 职称
};

  上面,我们看到没有继承之前我们设计了两个类 StudentTeacher S t u d e n t Student Student T e a c h e r Teacher Teacher 都有 姓名 / 地址 / 电话 / 年龄 等成员变量,都有 i d e n t i t y identity identity ⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有⼀些独有的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课。

  既然 S t u d e n t Student Student T e a c h e r Teacher Teacher 两个类的设计有些冗余,那我们能不能把公共的信息提取出来呢?

  下面我们公共的成员都放到 Person 中,Student 和 Teacher 都继承 Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多麻烦。

class Person
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "张三"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄
};class Studen : public Person
{
public:// 学习void study(){// ...}
protected:int _stuid; // 学号
};class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title; //职称
};

  虽然 S t u d e n t Student Student类 的成员变量看起来只有int _stuid;,但它继承了Person类,它还有string _namestring _address;等等成员变量。成员函数也不止void study(),还有void identity()
  
  

1.2 继承定义

1.2.1 定义格式

  下面我们看到 Person父类,也称作基类Student子类,也称作派生类。(因为翻译的原因,所以既叫父类/子类,也叫基类/派生类)

在这里插入图片描述

  
  继承方式与访问限定符一样,都有三个公有、保护、私有

在这里插入图片描述

  

1.2.2 继承父类成员访问方式的变化

类成员/继承方式 p u b l i c public public 继承 p r o t e c t e d protected protected 继承 p r e v a t e prevate prevate 继承
基类的 p u b l i c public public 成员派生类的public成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r i v a t e private private 成员
基类的 p r o t e c t e d protected protected 成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r o t e c t e d protected protected 成员派生类的 p r i v a t e private private 成员
基类的 p r i v a t e private private 成员在派生类中不可见在派生类中不可见在派生类中不可见
  • 父类的 private成员 在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外我们都不能去访问它

    • 子类想访问父类的 p r i v a t e private private 成员虽然不能直接访问,但能间接访问。虽然在子类中是不能访问,但在父类中并没有相关限制,只用父类提供相关访问 p r i v a t e private private 成员变量的成员函数,子类调用其函数就能间接访问。
  • 父类 p r i v a t e private private 成员在子类中是不能被访问的,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。

  • 实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类其他成员在子类的访问方式为: M i n Min Min(成员在父类的访问限定符, 继承方式) p u b l i c public public >  p r o t e c t e d protected protected >  p r i v a t e private private

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

    • class Student:Person //默认为private继承struct Student:Person //默认为public继承
  • 在实际运用中一般使用的都是 p u b l i c public public 继承,几乎很少使用 p r o t e c t e d protected protected / p r i v a t e private private 继承,也不提倡使用 p r o t e c t e d protected protected / p r i v a t e private private 继承,因为 p r o t e c t e d protected protected / p r i v a t e private private 继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。这里可以认为是 C++ 过度设计了。

  看起来上面的规则很复杂,实际实践过程中是很简单的,一般都是:父类我们就用公有和保护,继承方式我们就用公有。其他方式都很少使用。
  

1.3 继承类模板

  上述都是一些普通类的继承,那如果我们想继承类模板又该怎样呢?

  之前,我们模拟实现栈使用的适配器模式,其实还有一种方法:继承

namespace ganyu
{template<class T>class stack : public std::vector<T>{public:void push(const T& x){push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}

  当基类是类模板时,需要指定类域去访问,否则会编译报错。(普通类的继承不存在这个问题)

int main()
{ganyu::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}

在这里插入图片描述

  为什么编译报错呢?这与按需实例化有关系

  ganyu::stack<int> st;这句代码实例化栈,将 T T T 实例化成 i n t int int,也间接将 v e c t o r vector vector 实例化(严格来说只实例化了栈的构造函数)。但我们将 v e c t o r vector vector 实例化时不会把 v e c t o r vector vector 中所有的成员函数都实例化,我们调用谁才实例化谁
  我们调用 p u s h push push 函数时,编译器去找 p u s h push push_ b a c k back back 函数,在子类和父类中都找不到,因为还没有实例化。所以我们要指定类域去访问,表示调用的是 v e c t o r vector vector< T T T> 中的 p u s h push push_ b a c k back back,此时编译器看到 T T T 已经被实例化成 i n t int int 了,就会将 v e c t o r vector vector< T T T> 中的 p u s h push push_ b a c k back back 实例化出一份 i n t int int 版本的出来。

  我们可以结合 #define,能灵活更改 s t a c k stack stack 的底层容器,达到类似适配器模式的效果

#define CONTAINER vectornamespace ganyu
{template<class T>class stack : public std::CONTAINER<T>{public:void push(const T& x){CONTAINER<T>::push_back(x);}void pop(){CONTAINER<T>::pop_back();}const T& top(){return CONTAINER<T>::back();}bool empty(){return CONTAINER<T>::empty();}};
}

  
  

2 父类和子类对象赋值兼容转换

  • p u b l i c public public继承的前提下,子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切割开来赋值给父类对象/指针/引用
  • 但反过来就不成立:父类对象不能赋值给子类对象

在这里插入图片描述

  例如:现在有一个 S t u d e n t Student Student 对象, S t u d e n t Student Student 对象可以赋值给父类对象 P e r s o n Person Person,当然,指针和引用也是可以的;但反过来就不成立(总不能无中生有出一个 _ N o No No 成员吧)。

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.基类对象不能赋值给派⽣类对象,这⾥会编译报错//sobj = pobj;return 0;
}

  这里并没有发生类型转换
  虽然我们前面讲过不同类型的对象之间进行赋值,支持的是类型转换

int i = 0;
double d = i;

  将 i i i 赋值给 d d d 走的就是类型转换,中间会生成一个临时对象
  但是切片并不是类型转换,中间并没有产生临时变量,这是一种特殊处理。

  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再单独专门介绍,这里先提⼀下)
      
      

3 继承中的作用域

3.1 隐藏规则

  • 在继承体系中父类和子类都有独立的作用域
  • 子类和父类中有同名成员子类成员屏蔽父类的同名成员的直接访问,这种情况叫 隐藏。(在子类成员函数中,可以使用父类::父类成员 显式访问
class Person
{
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();return 0;
}

运行结果:

在这里插入图片描述

  • 如果是成员函数的隐藏只需要函数名相同就构成隐藏
  • 注意:在实际中在继承体系里面最好不要定义重名的成员或函数

  
  

3.2 例题

class A
{public :void fun(){cout << "func()" << endl;}
};
class B : public A
{public :void fun(int i){cout << "func(int i)" << i << endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
  1. A A A B B B 类中的两个 f u n c func func函数 构成什么关系()
    A. 重载   B. 隐藏  C.没关系

  2. 下面程序的编译运行结果是什么()
    A. 编译报错  B. 运行报错  C. 正常运行

  • 第一题:第一眼看上去,他们构成重载关系:函数名相同,参数类型不同。但如果选 A 就错了,这题选B。别忘了,只有在同一作用域的函数才构成函数重载,而隐藏是父类和子类中的函数名相同就构成隐藏
  • 第二题:选A,因为子类和父类的 f u n c func func函数 构成隐藏,除非指定父类的作用域去调用,否则同名成员或函数是不会去父类中查找的。b.fun(); 没有传递参数,编译报错。

  
  

4 子类的默认成员函数

  6 个默认成员函数,意思是我们不写,编译器会给我们自动生成。父类的默认成员函数与普通类没有任何差别,但在派生类中,这几个成员函数是如何生成的呢?

4.1 构造函数

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。

4.1.1 父类有默认构造

class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:protected:int _num; //学号string _sex; //性别
};

  

首先,我们来回忆一下普通类的默认生成的构造函数的行为:

  • 内置类型:默认生成的构造函数是不确定的
  • 自定义类型:会调用它的默认构造函数

现在,比起之前多出来一部分:父类成员

  • 我们把继承的父类成员看成一个整体对象,子类的默认构造会自动调用父类的默认构造完成父类成员的初始化

  

在这里插入图片描述

  

4.1.2 父类没有默认构造

class Person
{
public:Person(const char* name, double height): _name(name),_height(height){cout << "Person()" << endl;}protected:string _name; // 姓名double _height; //身高
};class Student : public Person
{
public:protected:int _num; //学号string _sex; //性别
};

  现在,父类没有默认构造,派生类还能默认生成构造函数吗?.

在这里插入图片描述

  可见,默认生成的只能调用默认构造。这时,就需要我们在子类显式写一个构造函数了

Student(const char* name, double height, int num, const char* sex):_name(name),_height(height),_num(num),_sex(sex)
{}

  这样写可不可以呢?
  不可以。编译器不允许直接去初始化父类的成员,子类要求必须调用父类的构造函数来初始化父类的成员,要把父类当成一个整体。
  
显示调用父类方法如下:

Student(const char* name, double height, int num, const char* sex):Person(name, height),_num(num),_sex(sex)
{}int main()
{Student s1("张三", 1.80, 1, "男");Student s2("李四", 1.70, 2, "未知");return 0;
}

  有点像调用一个匿名对象一样。

  

4.2 拷贝构造

  派生类的拷贝构造函数必须调用基类的拷贝构造完成基本的拷贝初始化
  

对默认生成的拷贝构造,其行为也像上述构造函数一样分成三类

  • 内置类型:完成浅拷贝
  • 自定义类型:调用其拷贝构造
  • 父类整体:调用父类的拷贝构造

  

4.2.1 不需要自己显式写

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, const char* sex):Person(name), _num(num), _sex(sex){}//未写拷贝构造//···protected:int _num; //学号string _sex; //性别
};int main()
{Student s1("张三", 1, "男");Student s2 = s1;return 0;
}

在这里插入图片描述

  严格来说, S t u d e n t Student Student类是不用我们自己写拷贝构造的,默认生成的拷贝构造已经完成了我们的需求。前面,我们通过学习知道拷贝构造、赋值重载、析构是一体的。一个不需要写,三个都不需要写;一个要写,三个都要写。因此 S t u d e n t Student Student类 的赋值重载和析构函数都不需要自己写
  如果有需要深拷贝的资源,才需要自己实现

  

4.2.2 自己显式写

  那假设 S t u d e n t Student Student 类中有指向的资源,需要我们自己写拷贝构造,又该怎么写呢?

class Student : public Person
{
public:protected:int _num; //学号string _sex; //性别int* _ptr = new int[10];//假设有指向的资源
};
Student (const Student& s):_num(s._num),_sex(s._sex),//显示调用父类的拷贝构造
{//深拷贝memcpy(_ptr, s._ptr, sizeof(int) * 10);
}

  如何显式调用父类的拷贝构造呢?

  调用父类的拷贝构造,需要传递父类的对象,但现在没有父类的对象,咋办呢?
  这时,我们就可以运用前面学习的赋值兼容转换

Student(const Student& s):_num(s._num),_sex(s._sex),Person(s)
{//深拷贝memcpy(_ptr, s._ptr, sizeof(int) * 10);
}

  Person(s) s s s 是子类对象的引用,要拷贝父类那一部分,需要将父类那一部分拿出来, 怎么拿出来呢?我把子类对象传给父类的引用,这时父类的引用,引用的是子类对象中切割出来的父类的那一部分

  这里有个小细节,走初始化列表时,编译器会先走Person(s),在走_num(s._num)_sex(s._sex)
  这是因为初始化列表初始化的顺序与成员在列表中的顺序无关,只与声明的顺序有关
  所以继承以后,它将父类对象当成一个整体,而父类对象是最先被声明

  那如果不在初始化列表显示初始化父类呢?

Student(const Student& s):_num(s._num), _sex(s._sex)	
{//深拷贝
}

  我们说过,所有成员都会走初始化列表,父类 P e r s o n Person Person 没有显示调用,也会走初始化列表。但此时编译器会调用 P e r s o n Person Person 的默认构造,虽然编译能通过,但很可能不符合你的需求;如果 P e r s o n Person Person 没有默认构造,那么编译报错

  

4.3 赋值重载

  和拷贝构造一样, S t u d e n t Student Student 类严格来说不需要写赋值。
  但如果我们需要显式写要怎么写呢

  • 派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator =
Student& operator=(const Student& s)
{if (this != &s){operator=(s);_num = s._num;_sex = s._sex;}return *this;
}

  复制拷贝与拷贝构造是类似的,都是传递子类对象的引用给父类即可。

  但是,如果运行程序会发现:程序陷入死循环
  为什么呢?
  子类中的同名函数与父类的构成了隐藏!
  operator=(s);其实一直调的是子类的 o p e r a t o r operator operator=,因此程序陷入死循环

  因此我们要指定调用定父类的 o p e r a t o r operator operator=。

Student& operator=(const Student& s)
{if (this != &s){Person::operator=(s);_num = s._num;_sex = s._sex;}return *this;
}

  总结:派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator= 完成基类的赋值。需要注意的是派生类的 o p e r a t o r operator operator= 屏蔽了基类的 o p e r a t o r operator operator=,所以显式调用基类的 operator=,需要指定基类作用域

  

4.4 析构函数

  首先,严格来说 S t u d e n t Student Student 并不需要我们显式写析构函数
  那如果有需要显式释放的资源,析构函数又该怎么写呢?

4.4.1 重载

  我们还是以 S t u d e n t Student Student类 为例
  首先,如果显式实现析构函数,_ n u m num num 和 _ s e x sex sex 是不用管的。因为int _num是内置类型,而 string _sex会自己调用其析构。我们只需要管父类即可

~Student()
{~Person();
}

  但这样会报错

在这里插入图片描述

  析构是可以显示调用的,但为什么这里调不动呢?

  这里有个小知识点:子类的析构会和父类的析构构成隐藏关系
  因为一些特殊的原因,析构函数的函数名会被特殊处理成 d e s t r u c t o r destructor destructor(),所以父类的析构函数和子类的析构函数构成隐藏关系。实际上并没有什么 ~ S t u d e n t Student Student() 和 ~ P e r s o n Person Person(),只有 d e s t r u c t o r destructor destructor()。

  所以我们要指定类域调用

~Student()
{Person::~Person();
}

  

4.4.2 顺序

  我们来尝试调用一下析构函数

class Person
{
public://成员函数//···~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public://成员函数//···~Student(){Person::~Person();}protected:int _num; //学号string _sex; //性别
};int main()
{Student s1("张三", 1, "男");Student s2("李四", 2, "未知");return 0;
}

运行结果:

在这里插入图片描述

  大家有没有发现析构函数调的有点多啊,我一个就两个对象,你怎么就调用 4 次析构函数了呢?

  像构造、赋值重载等,我们显式写的都需要显式调用父类的对应函数,但析构不需要显式调用。调用了子类析构函数之后,系统会自动调用父类的析构(这点与自定义类型的成员很像)。

  为什么要这样的。这样可以保证析构顺序是先子后父。后定义的先析构,而对象构造时,是先构造(初始化)父类,在初始化子类;析构是就需要先析构子类,在析构父类。如果显式调用就不能保证先子后父,而是取决于实现的人。
  
  

4.5 实现不能被继承的类

  要实现一个不能被继承的类,有两种方法

4.5.1 法一:设为私有

  将父类的构造函数设置为私有

class Base 
{
public :void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的⽅法Base(){}
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};

  为什么呢?因为子类的构造函数,不论是自动生成还是我们自己显式实现,都必须调用父类的构造函数。但是父类的 p r i v a t e private private成员在子类中是不可见的,因此子类调不到父类的构造函数

  但是这种方式不够明显,如果不调用子类的对象编译器是不会报错的

4.5.2 法二:final

  C++11中新增了一个关键字: f i n a l final final
  用 f i n a l final final 修饰一个类,表示该类是最终类,无法再被继承

  这种方式更直观一些,不管子类定不定义,直接报错

class Base final
{
public :Base(){}void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};

在这里插入图片描述

  
  

4.6 总结

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表显示调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的 o p e r a t o r operator operator= 必须要调用基类的 o p e r a t o r operator operator= 完成基类的复制。需要注意的是派生类的 o p e r a t o r operator operator= 隐藏了基类的 o p e r a t o r operator operator=,所以显示调用基类的 operator=,需要指定基类作用域
  • 派生类的析构函数会在被调用完成之后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序
  • 派生类对象初始化先调用基类的构造再调派生类的构造
  • 派生类对象析构清理先调用派生类析构再调基类的析构
  • 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数进行特殊处理 ,处理成 d e s t r u c t o r destructor destructor(),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系
  • 大多数情况下,派生类中拷贝构造、赋值、析构都是不需要自己写的;如果需要,那这个继承的设计太过复杂,可以考虑重新设计。

  
  
  
  
  


  好啦,本期关于继承的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 C++ 的学习路上一起进步!

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle 11g RAC 节点异常重启问题分析

一、背景 在国庆期间巡检的时候&#xff0c;发现数据库alert日志中出现了异常重启的信息&#xff0c;当即对该报错进行分析处理。 二、处理过程 &#xff08;1&#xff09;数据库告警日志分析 node1 alert&#xff1a; Sat Oct 05 13:05:14 2024 Thread 1 advanced to log …

【光追模组】使命召唤7黑色行动光追mod,调色并修改光影,并且支持光追效果,游戏画质大提升

大家好&#xff0c;今天小编我给大家继续引入一款游戏mod&#xff0c;这次这个模组主要是针对使命召唤7黑色行动进行修改&#xff0c;如果你觉得游戏本身光影有缺陷&#xff0c;觉得游戏色彩有点失真的话&#xff0c;或者说你想让使命召唤7这款游戏增加对光线追踪的支持的话&am…

75 华三vlan端口隔离

华三vlan端口隔离 为了实现端口间的二层隔离&#xff0c;可以将不同的端口加入不同的VLAN&#xff0c;但VLAN资源有限。采用端口隔离特性&#xff0c;用户只需要将端口加入到隔离组中&#xff0c;就可以实现隔离组内端口之间二层隔离&#xff0c;而不关心这些端口所属VLAN&…

剖析 Redis:应对雪崩、穿透和击穿的实战秘籍

前言 用户的数据通常存储在数据库中&#xff0c;而数据库的数据存放在磁盘上。 磁盘的读写速度在计算机硬件中可以说是最慢的。 如果用户的所有请求都直接访问数据库&#xff0c;当请求数量增多时&#xff0c;数据库很容易崩溃。因此&#xff0c;为了避免用户直接访问数据库&a…

Java垃圾回收简述

什么是Java的垃圾回收&#xff1f; 自动管理内存的机制&#xff0c;负责自动释放不再被程序引用的对象所占用的内存。 怎么触发垃圾回收&#xff1f; 内存不足时&#xff1a;JVM检测到堆内存不足时&#xff0c;无法为新的对象分配内存时&#xff0c;会自动触发垃圾回收。手动…

Pandas -----------------------基础知识(八)

Pandas内置Matplotlib 加载数据 import pandas as pdanscombe pd.read_csv(/root/pandas_code_ling/data/e_anscombe.csv) anscombe dataset_1 anscombe[anscombe[dataset]I] dataset_1dataset_1.describe() 提供数据 dataset_1 anscombe[anscombe[dataset]I] dataset_2 an…

【C语言】分支和循环(2)

&#x1f914;个人主页: 起名字真南 &#x1f619;个人专栏:【数据结构初阶】 【C语言】 【C】 目录 1 关系操作符2 条件操作符3 逻辑操作符 &#xff1a;|| &#xff0c;&& &#xff0c;&#xff01;3.1 逻辑取反运算符3.2 与运算符3.3 或运算符3.4 练习闰年判断3.5 短…

仪器校准机构不符合项应该怎么签发和整改?

签发不合格项是内审工作之中非常重要的一环&#xff0c;那么如何正确签发不合格项&#xff0c;下列几个方面可以供大家参考&#xff1a; 一、目的 为便于仪器校准机构正确理解不符合项整改要求&#xff0c;特制定本指南&#xff0c;以指导企业规范、有效、高效地处理不符合项。…

旅游管理智能化:SpringBoot框架的应用

第一章 绪论 1.1 研究现状 时代的发展&#xff0c;我们迎来了数字化信息时代&#xff0c;它正在渐渐的改变着人们的工作、学习以及娱乐方式。计算机网络&#xff0c;Internet扮演着越来越重要的角色&#xff0c;人们已经离不开网络了&#xff0c;大量的图片、文字、视频冲击着我…

SpringBoot教程(二十四) | SpringBoot实现分布式定时任务之Quartz

SpringBoot教程&#xff08;二十四&#xff09; | SpringBoot实现分布式定时任务之Quartz 简介适用场景Quartz核心概念Quartz 存储方式Quartz 版本类型引入相关依赖方式一&#xff1a;内存方式(MEMORY)存储实现定时任务1. 定义任务类2. 定义任务描述及创建任务触发器3. Quartz的…

强引用、软引用、弱引用、虚引用用法

强引用、软引用、弱引用、虚引用用法 强引用弱引用弱引用虚引用 强引用 强引用是指程序中在程序代码之中类似“Object obj new Object()”的引用关系&#xff0c;无论任何情况下&#xff0c;只要强引用关系还存在&#xff0c;垃圾回收器就不会回收掉被引用的对象。 强引用是我…

日期类(Date)的实现 (C++版)

​ &#x1f339;个人主页&#x1f339;&#xff1a;喜欢草莓熊的bear &#x1f339;专栏&#x1f339;&#xff1a;C入门 目录 前言 一、Date的头文件&#xff0c;包含函数声明 二、 Date.cpp 2.1 int GetMonthDay(int year, int month) 2.2 bool Check() 2.3 Date& …

【吊打面试官系列-MySQL面试题】什么是基本表?什么是视图?

大家好&#xff0c;我是锋哥。今天分享关于【什么是基本表&#xff1f;什么是视图&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 什么是基本表&#xff1f;什么是视图&#xff1f; 基本表是本身独立存在的表&#xff0c;在 SQL 中一个关系就对应一个表。 视图…

【含开题报告+文档+PPT+源码】闲置二手市场小程序的设计与实现

开题报告 闲置二手市场平台的背景可以追溯到互联网的普及和电子商务的兴起。随着互联网技术的不断发展&#xff0c;人们的消费观念也在不断变化&#xff0c;越来越多的人开始关注二手商品的价值和优势。同时&#xff0c;大用户群体也在不断增加&#xff0c;他们对于经济实惠的…

利用顺序栈输出对应的二进制数,找迷宫出口详解(数据结构作业04)

目录 利用顺序栈输出对应的二进制数 代码&#xff1a; 运行结果&#xff1a; 找迷宫出口 代码&#xff1a; 图解&#xff1a; 运行结果&#xff1a; 利用顺序栈输出对应的二进制数 键盘输入一个十进制正整数89&#xff0c;用C语言设计一个算法&#xff0c;利用顺序栈…

MambaAD 实验部分讲解

4 实验 4.1 设置&#xff1a;数据集、指标和细节 数据集&#xff08;6个&#xff09; 1.MVTec-AD&#xff1a; 包含5种类型的纹理和10种类型的对象&#xff0c;总共5,354张高分辨率图像。 实验&#xff1a; 3,629张正常图像被指定为训练。 剩下的 1,725 张图像被保留用于测试…

网络基础擅长组建乐队

让我们荡起双桨 来说说网络吧 现有计算机要进行协作&#xff0c;网络的产生是必然的 局域网&#xff1a;计算机数量更多了, 通过交换机和路由器连接在一起 广域网&#xff1a;将远隔千里的计算机都连在一起 交换机路由器等设备就应运而生 计算机是人的工具&#xff0c;人要协…

美国游戏发展趋势

美国拥有一些最大、最具影响力的游戏开发工作室&#xff0c;是游戏行业的全球领导者。凭借丰富地创新历史&#xff0c;美国游戏开发不断发展&#xff0c;受到尖端技术、消费者偏好和市场动态的影响。已经出现了几个趋势&#xff0c;这些趋势定义了该国游戏发展的方向&#xff0…

node高版本报错: digital envelope routines::unsupported

node高版本报错&#xff1a; digital envelope routines::unsupported 解决方案&#xff1a; package.json中&#xff0c;启动命令前加上&#xff1a; set NODE_OPTIONS--openssl-legacy-provider &&

WPF 手撸插件 八 操作数据库一

1、本文将使用SqlSugar创建Sqlite数据库&#xff0c;进行入门的增删改查等操作。擦&#xff0c;咋写着写着凌乱起来了。 SqlSugar官方文档&#xff1a;简单示例&#xff0c;1分钟入门 - SqlSugar 5x - .NET果糖网 2、环境SqlSugar V5.0版本需要.Net Framework 4.6 &#xff0…