当前位置: 首页 > news >正文

【C++】18.继承

文章目录

  • 1.继承的概念及定义
    • 1.1 继承的概念
    • 1.2 继承定义
      • 1.2.1定义格式
      • 1.2.2继承关系和访问限定符
      • 1.2.3继承基类成员访问方式的变化
    • 1.3 继承类模板
  • 2.基类和派生类对象赋值转换
  • 3.继承中的作用域
    • 3.1 隐藏规则:
    • 3.2 考察继承作用域相关选择题
  • 4.派生类的默认成员函数
    • 4.1 4个常见默认成员函数
      • 代码运行结果分析
      • 代码解析
        • 1. 构造函数调用分析
          • 构造 `s1`:
          • 拷贝构造 `s2`:
          • 构造 `s3`:
        • 2. 赋值运算符调用分析
          • 赋值 `s1 = s3`:
        • 3. 析构函数调用分析
          • 程序结束时析构顺序:
      • 基类和派生类的构造、拷贝构造、赋值运算符、析构总结
        • 1. 构造函数
        • 2. 拷贝构造函数
        • 3. 赋值运算符
        • 4. 析构函数
      • 总结调用顺序
        • 构造顺序
        • 拷贝构造顺序
        • 赋值运算符顺序
        • 析构顺序
    • 4.2 实现一个不能被继承的类
  • 5.继承与友元
      • 代码解释与问题分析
        • 1. **代码结构及友元关系**
        • 2. **友元关系的作用范围**
        • 3. **编译报错原因**
        • 4. **“友元关系不能继承”的含义**
      • 解决方法
      • 为什么友元关系不能继承?
      • 总结
  • 6. 继承与静态成员
  • 7.复杂的菱形继承及菱形虚拟继承
    • 7.1 继承模型
    • 7.2 虚继承
      • 类层次结构概述
      • 菱形继承问题与虚继承的作用
        • 菱形继承问题
        • 虚继承的作用
      • 代码解析
      • 总结
      • 构造函数的调用顺序
      • 最终结果
    • 7.3 多继承中指针偏移问题?下面说法正确的是( )
      • 问题解析
      • 代码结构
      • 多继承时的内存布局
      • 指针值比较
      • 选项分析
      • 正确答案
    • 7.4 IO库中的菱形虚拟继承
  • 8.继承的总结和反思
  • 9.笔试面试题
      • 继承与组合的区别
      • 何时使用继承?
      • 何时使用组合?


1.继承的概念及定义

1.1 继承的概念

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

下面我们看到没有继承之前我们设计了两个类StudentTeacherStudentTeacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。

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; // 职称
};
int main()
{return 0;
}

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

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

1.2 继承定义

1.2.1定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

c3d983489a03ac0bdae6d775dfba678b


1.2.2继承关系和访问限定符

47ea67cfb9ff4950f86f4c95bb9cbf19

0317626f18957cb6d9c95392c20e6e6f


1.2.3继承基类成员访问方式的变化

类成员/继承方式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继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{public :void Print (){cout<<_name <<endl;}protected :string _name ; // 姓名private :int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{protected :int _stunum ; // 学号
};

1.3 继承类模板

namespace bit
{//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-atemplate<class T>class stack : public std::vector<T>{public:void push(const T& x){//push_back(x);直接这么写会报错// 基类是类模板时,需要指定一下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}
int main()
{bit::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}

代码讲解:

  1. 继承类模板的语法问题
template<class T>
class stack : public std::vector<T>  // 继承标准库的vector
{// ...
};

这里展示了如何继承一个类模板。stack继承了vector,T是模板参数。

  1. 类模板成员函数中访问基类成员的问题
void push(const T& x)
{// 错误写法//push_back(x);  // 编译错误// 正确写法vector<T>::push_back(x);  // 需要指定类域// 或者使用this//this->push_back(x);// 或者使用using声明//using vector<T>::push_back;//push_back(x);
}
  1. 为什么需要指定类域的原因:
template<class T>
class stack : public std::vector<T>
{void push(const T& x){// 当编译器遇到push_back时,会按以下顺序查找:// 1. 在当前类stack中查找// 2. 在当前类的base class list中查找(依赖模板参数T)// 3. 在全局作用域查找// 由于push_back依赖于模板参数T// 编译器不会在第2步中查找// 这就是为什么需要显式指定vector<T>::}
};
  1. 几种解决方案:
template<class T>
class stack : public std::vector<T>
{
public:// 方案1:使用类域限定void push(const T& x){vector<T>::push_back(x);}// 方案2:使用this指针void push(const T& x){this->push_back(x);}// 方案3:使用using声明using vector<T>::push_back;void push(const T& x){push_back(x);}
};

关键点总结:

  1. 继承类模板时,需要完整指定模板参数
  2. 在派生类中访问基类模板的成员时需要指定类域
  3. 这是因为基类成员函数的查找规则涉及到模板的依赖性查找
  4. 可以使用类域限定、this指针或using声明来解决
  5. 这个例子展示了如何通过继承vector来实现一个stack,复用了vector的实现

2.基类和派生类对象赋值转换

  • 子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片或者切割(或者赋值兼容转换)。寓意把子类中父类那部分切来赋值过去。

  • 父类对象不能赋值给子类对象。

  • 父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTimeType Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)

12c1a041e52d1113e958e7fdab4584f7

class Person
{
protected :string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public :int _No ; // 学号
};int main()
{Student sobj ;// 赋值兼容转换,特殊处理//之前我们讲引用的时候,出现过权限的放大缩小。double d = 1.1;//int& i = d;这么写不可以,因为d赋值给i的时候会产生一个临时变量,临时变量具有常性,所以要加个const防止权限的放大const int& i = d;string s1 = "11111";//string& s2 = "11111";这么写不可以,因为"11111"赋值给s的时候会产生一个临时变量,临时变量具有常性,所以要加个const防止权限的放大const string& s2 = "11111";// 1.子类对象可以赋值给父类的指针/引用Person* pp = &sobj;Person& rp = sobj;//但是这里没有设计权限的放大,这就是赋值兼容转换//这里的pp实际上就是子类中切割出来的父类的那块区域的指针//这里的rp实际上就是子类中切割出来的父类的那块区域的别名// 子类对象可以赋值给父类的对象是通过调用后面会讲解的父类的拷贝构造完成的Person pobj = sobj;//2.父类对象不能赋值给子类对象,这里会编译报错//sobj = pobj;return 0;
}

3.继承中的作用域

3.1 隐藏规则:

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,也就是只访问子类成员,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person {
protected:string _name = "小李子";  // Person的成员变量int _num = 111;          // Person的身份证号
};class Student : public Person {
public:void Print() {cout << " 姓名:" << _name << endl;                // 直接访问基类的_namecout << " 身份证号:" << Person::_num << endl;     // 需要用Person::来访问被隐藏的基类_numcout << " 学号:" << _num << endl;                 // 直接访问派生类的_num}
protected:int _num = 999;          // Student的学号,与Person中的_num同名
};int main()
{Student s1;s1.Print();return 0;
};

3.2 考察继承作用域相关选择题

1.AB类中的两个func构成什么关系()

A. 重载 B. 隐藏 C.没关系

答案:B

重载要在同一个作用域里面

如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

2.下面程序的编译运行结果是什么()

A. 编译报错 B. 运行报错 C. 正常运行

答案:A

b.fun();//无法运行,因为父类隐藏了func()

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();//无法运行,因为A::fun()被B::fun(int)隐藏了return 0;
};

问题原因:

  1. 在派生类B中定义的fun(int)会隐藏基类A中的所有同名函数
  2. 包括参数不同的版本
  3. 因此B类对象无法直接访问A::fun()

记住:

  1. 函数隐藏是编译时的特性
  2. 不同于虚函数的多态(运行时特性)
  3. 隐藏会影响所有同名函数,不管参数是否相同
  4. 使用作用域运算符或using声明可以访问被隐藏的函数
  5. 好的设计应该避免函数隐藏带来的问题

4.派生类的默认成员函数

4.1 4个常见默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

eeacbf7cfb710787afc98f67e35ae266

2fad0eb1c124137f3f5d1f6bcb9ab6bd

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;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};
class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s): Person(s), _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}Student& operator = (const Student& s ){cout<<"Student& operator= (const Student& s)"<< endl;if (this != &s){Person::operator =(s);_num = s ._num;}return *this ;} ~Student(){cout<<"~Student()" <<endl;}protected :int _num ; //学号
};
int main()
{Student s1 ("jack", 18);Student s2 (s1);Student s3 ("rose", 17);s1 = s3 ;return 0;
}

以下是代码中,基类 Person派生类 Student 的构造、析构、拷贝构造,以及赋值运算符的行为分析和详细讲解。


代码运行结果分析

运行程序后,打印的输出结果为:

Person()
Student()
Person(const Person& p)
Student(const Student& s)
Person()
Student()
Person operator=(const Person& p)
Student& operator= (const Student& s)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()

代码解析

1. 构造函数调用分析
构造 s1
Student s1("jack", 18);
  • 调用顺序
    1. 首先调用 Person 类的构造函数 Person(const char* name),因为 Student 类继承自 Person,构造派生类前必须先构造基类。
      • 输出:Person()
    2. 然后执行 Student 类的构造函数 Student(const char* name, int num),初始化 _num 成员变量。
      • 输出:Student()
拷贝构造 s2
Student s2(s1);
  • 调用顺序
    1. 首先调用基类 Person 的拷贝构造函数 Person(const Person& p),因为 Student 继承自 Person,需要先拷贝构造基类部分。
      • 输出:Person(const Person& p)
    2. 然后调用 Student 的拷贝构造函数 Student(const Student& s),完成派生类部分的拷贝(包括 _num 的值)。
      • 输出:Student(const Student& s)
构造 s3
Student s3("rose", 17);
  • 调用顺序
    1. 首先调用基类 Person 的构造函数 Person(const char* name),构造基类部分。
      • 输出:Person()
    2. 然后调用 Student 的构造函数 Student(const char* name, int num),初始化派生类部分。
      • 输出:Student()

2. 赋值运算符调用分析
赋值 s1 = s3
s1 = s3;
  • 调用顺序
    1. 首先调用基类 Person 的赋值运算符 Person& operator=(const Person& p),完成基类部分的赋值。
      • 输出:Person operator=(const Person& p)
    2. 然后调用派生类 Student 的赋值运算符 Student& operator=(const Student& s),完成派生类部分的赋值(包括 _num 的赋值)。
      • 输出:Student& operator= (const Student& s)

3. 析构函数调用分析
程序结束时析构顺序:
  • 对象的析构顺序与构造顺序相反,析构派生类前必须先析构基类。

  • 析构过程

    1. 首先析构 s3
      • 调用 Student 的析构函数。
        • 输出:~Student()
      • 调用 Person 的析构函数。
        • 输出:~Person()
    2. 然后析构 s2
      • 调用 Student 的析构函数。
        • 输出:~Student()
      • 调用 Person 的析构函数。
        • 输出:~Person()
    3. 最后析构 s1
      • 调用 Student 的析构函数。
        • 输出:~Student()
      • 调用 Person 的析构函数。
        • 输出:~Person()

基类和派生类的构造、拷贝构造、赋值运算符、析构总结

1. 构造函数
  • 基类构造函数会在派生类构造函数之前调用。派生类需要通过基类构造函数初始化基类部分的数据成员。
  • 如果派生类的构造函数没有显式指定基类构造函数,则会默认调用基类的无参构造函数(如果存在)。
  • 在这个例子中:
    • Student(const char* name, int num) 显式调用了基类的构造函数 Person(name)
2. 拷贝构造函数
  • 基类的拷贝构造函数会在派生类的拷贝构造函数之前调用。
  • 拷贝构造函数需要显式调用基类的拷贝构造函数来拷贝基类部分的数据成员。
  • 在这个例子中:
    • Student(const Student& s) 显式调用了基类的拷贝构造函数 Person(s)
3. 赋值运算符
  • 基类的赋值运算符会在派生类的赋值运算符之前调用。
  • 派生类在实现赋值运算符时,通常需要显式调用基类的赋值运算符来完成基类部分的赋值。
  • 在这个例子中:
    • Student& operator=(const Student& s) 显式调用了基类的赋值运算符 Person::operator=(s)
4. 析构函数
  • 析构函数调用顺序与构造函数相反。
  • 派生类的析构函数会在基类析构函数之前调用
  • 在这个例子中:
    • Student 的析构函数会先执行,然后调用 Person 的析构函数。

总结调用顺序

构造顺序
  1. 先构造基类部分(调用基类的构造函数)。
  2. 再构造派生类部分(调用派生类的构造函数)。
拷贝构造顺序
  1. 先调用基类的拷贝构造函数。
  2. 再调用派生类的拷贝构造函数。
赋值运算符顺序
  1. 先调用基类的赋值运算符。
  2. 再调用派生类的赋值运算符。
析构顺序
  1. 先析构派生类部分(调用派生类的析构函数)。
  2. 再析构基类部分(调用基类的析构函数)。

4.2 实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法2:C++11新增了一个final关键字,final修改基类,派生类就不能继承了。

// C++11的方法
class Base final
{
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;
};int main()
{Base b;Derive d;return 0;
}

5.继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

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)
{cout << p._name << endl;cout << s._stuNum << endl;
}int main()
{Person p;Student s;// 编译报错:error C2248: “Student::_stuNum”: 无法访问 protected 成员// 解决方案:Display也变成Student 的友元即可Display(p, s);return 0;
}

代码解释与问题分析

我们来分步分析这段代码以及与“友元关系不能继承”的联系。

1. 代码结构及友元关系
  • 在类 Person 中,Display 函数被声明为友元函数。这意味着 Display 函数可以访问 Person 类的私有或受保护成员(如 _name)。
  • Student 通过公有继承自 Person,但它有自己的受保护成员 _stuNum
2. 友元关系的作用范围
  • 友元关系 只作用于声明它的类本身。因此:
    • DisplayPerson 的友元,所以可以访问 Person 的受保护成员 _name
    • 但是,Display 并不是 Student 的友元,因此无法访问 Student 的受保护成员 _stuNum
3. 编译报错原因
void Display(const Person& p, const Student& s)
{cout << p._name << endl;    // 可以访问,因为 Display 是 Person 的友元cout << s._stuNum << endl; // 报错,因为 Display 不是 Student 的友元
}
  • p._namePerson 的受保护成员,DisplayPerson 的友元,可以访问。
  • s._stuNumStudent 的受保护成员,但 Display 不是 Student 的友元,因此无法访问,导致编译报错。
4. “友元关系不能继承”的含义
  • 友元关系是类之间的一种特权关系,它不能通过继承传递。
    • 即使 Student 继承自 PersonDisplay 函数作为 Person 的友元,并不会自动成为 Student 的友元。
    • 因此,Display 无法访问 Student 类的私有或受保护成员。

解决方法

为了让 Display 函数能够访问 Student 类的受保护成员 _stuNum,需要将 Display 函数显式声明为 Student 的友元:

class Student : public Person
{protected:int _stuNum; // 学号// 显式声明 Display 为友元friend void Display(const Person& p, const Student& s);
};

为什么友元关系不能继承?

这是 C++ 的设计规则,按照 封装性原则访问权限控制 的逻辑:

  • 友元关系是类设计者赋予的特权,而不是类继承链上的默认权限。
  • 如果友元关系能继承,那么继承链上的子类可能会暴露更多的内部实现细节,从而破坏封装性。
  • 通过这种设计,C++ 强制开发者显式声明友元关系,避免意外的权限泄露。

总结

这段代码报错的原因在于“友元关系不能继承”:

  • DisplayPerson 的友元,可以访问 Person 的受保护成员 _name
  • Display 不是 Student 的友元,不能访问 Student 的受保护成员 _stuNum

解决方法是将 Display 显式声明为 Student 的友元,从而允许它访问 _stuNum


6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class Person
{
public:string _name;static int _count;
};int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};int main()
{Person p;Student s;// 这里的运行结果可以看到非静态成员_name的地址是不一样的// 说明派生类继承下来了,父派生类对象各有一份cout << &p._name << endl;cout << &s._name << endl;// 这里的运行结果可以看到静态成员_count的地址是一样的// 说明派生类和基类共用同一份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,父派生类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

7.复杂的菱形继承及菱形虚拟继承

7.1 继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。

菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

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

df895d13374f21e3b0502a6f0e75b26a

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

f14184225b44d51de60e8f5650408d4d

菱形继承:菱形继承是多继承的一种特殊情况。

2b8f014f2cb5d51a232c7acf815feb08

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。

41fe40779f230190735c19b9b497d5d5

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; // 主修课程
};
int main()
{// 编译报错:error C2385: 对“_name”的访问不明确Assistant a;a._name = "peter";// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}

7.2 虚继承

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java

class Person
{
public:string _name; // 姓名/*int _tel;int _age;string _gender;string _address;*/// ...
};// 使用虚继承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;
}

让我们详细解析一下上面的代码,特别是其中关于虚继承的部分。

虚继承在菱形继承的腰部,也就是出现数据二义性的地方。


类层次结构概述

首先,让我们看一下类之间的关系:

  1. Person 类

    class Person
    {
    public:string _name; // 姓名// 其他成员变量如电话、年龄、性别、地址等被注释掉了
    };
    
    • 这是一个基类,包含一些基本的个人信息,如姓名等。
  2. Student 类(虚继承自 Person)

    class Student : virtual public Person
    {
    protected:int _num; // 学号
    };
    
    • Student 类通过 虚继承 方式继承自 Person 类。
    • 这意味着 Student 类不会直接包含 Person 的实例,而是与后续的继承关系共享同一个 Person 实例。
  3. Teacher 类(虚继承自 Person)

    class Teacher : virtual public Person
    {
    protected:int _id; // 职工编号
    };
    
    • 同样,Teacher 类也通过 虚继承 方式继承自 Person 类。
    • 这也意味着 Teacher 类不会直接包含 Person 的实例,而是与 Student 类共享同一个 Person 实例。
  4. Assistant 类(多重继承自 Student 和 Teacher)

    class Assistant : public Student, public Teacher
    {
    protected:string _majorCourse; // 主修课程
    };
    
    • Assistant 类通过 多重继承 同时继承自 StudentTeacher 类。
    • 由于 StudentTeacher 都是虚继承自 Person,因此 Assistant 类中只包含一个 Person 实例,避免了 菱形继承问题

菱形继承问题与虚继承的作用

菱形继承问题

假设不使用虚继承,类层次结构如下:

      Person/    \Student  Teacher\    /Assistant

在这种继承方式下,Assistant 类会通过 StudentTeacher 继承两次 Person,导致:

  • 数据冗余Assistant 类中会包含两个 Person 的实例。
  • 二义性:当访问 Person 的成员变量(如 _name)时,会出现二义性,因为编译器不知道要访问哪一个 Person 实例。

例如:

Assistant a;
a._name = "peter"; // 编译错误:_name 不明确

编译器会报错,因为 Assistant 中有两个 _name,一个来自 StudentPerson,一个来自 TeacherPerson

虚继承的作用

通过使用 虚继承,类层次结构变为:

      Person/    \Student  Teacher\    /Assistant

在这种继承方式下,StudentTeacher 都是虚继承自 Person,因此:

  • 共享基类实例Assistant 类中只包含一个 Person 的实例。
  • 避免二义性:当访问 Person 的成员变量时,不会出现二义性,因为只有一个 Person 实例。

例如:

Assistant a;
a._name = "peter"; // 正确

这样,Assistant 类中只有一个 _name,可以正常访问和赋值。

代码解析

  1. 虚继承的使用

    • StudentTeacher 都通过 virtual public Person 虚继承自 Person
    • 这确保了 Assistant 类中只有一个 Person 实例。
  2. 数据成员的访问

    • main 函数中,创建 Assistant 对象 a 后,可以直接访问 _name,因为 Assistant 类中只有一个 _name,来自共享的 Person 实例。
  3. 构造函数的调用

    • 需要注意的是,当使用虚继承时,最底层的派生类(如 Assistant)负责调用基类(Person)的构造函数。
    • 因此,Assistant 的构造函数应调用 Person 的构造函数。例如:
    class Assistant : public Student, public Teacher
    {
    protected:string _majorCourse; // 主修课程
    public:Assistant(string name, int num, int id, string majorCourse): Person(name), Student(num), Teacher(id), _majorCourse(majorCourse) {}
    };
    
    • 这样可以确保 Person 的成员变量被正确初始化。

总结

  • 虚继承 解决了多重继承中可能出现的 菱形继承问题,避免了数据冗余和二义性。
  • 在你的代码中,StudentTeacher 通过虚继承共享同一个 Person 实例,使得 Assistant 类中只有一个 Person 实例。
  • 这允许你在 Assistant 对象中直接访问 Person 的成员变量,如 _name,而不会产生冲突。

通过合理使用虚继承,可以设计出更为清晰和高效的类层次结构。


我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。

class Person
{
public:Person(const char* name):_name(name){}string _name; // 姓名
};
class Student : virtual public Person
{
public:Student(const char* name, int num):Person(name),_num(num){}
protected:int _num; //学号
};
class Teacher : virtual public Person
{
public:Teacher(const char* name, int id):Person(name), _id(id){}
protected:int _id; // 职工编号
};
// 不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:Assistant(const char* name1, const char* name2, const char* name3):Person(name3),Student(name1, 1),Teacher(name2, 2){}
protected:string _majorCourse; // 主修课程
};
int main()
{// 思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?Assistant a("张三", "李四", "王五");return 0;
}

构造函数的调用顺序

当创建 Assistant 对象 a 时,构造函数的调用顺序如下:

  1. 初始化基类 Person
    • 由于 StudentTeacher 都是虚继承自 PersonAssistant 的构造函数负责初始化 Person
    • 调用 Person(name3),即 Person("王五"),因此 _name 被赋值为 "王五"
  2. 初始化基类 Student
    • 调用 Student(name1, 1),即 Student("张三", 1)
    • 但由于 Student 是虚继承,Person 已经被 Assistant 的构造函数初始化,所以这一步不会再次初始化 Person
  3. 初始化基类 Teacher
    • 调用 Teacher(name2, 2),即 Teacher("李四", 2)
    • 同样,由于 Teacher 是虚继承,Person 已经被 Assistant 的构造函数初始化,所以这一步不会再次初始化 Person

最终结果

因此,a 对象中的 _name 最终是 "王五",因为 Assistant 的构造函数中调用了 Person(name3),即 Person("王五")


7.3 多继承中指针偏移问题?下面说法正确的是( )

A:p1 == p2 == p3

B:p1 < p2 < p3

C:p1 == p3 != p2

D:p1 != p2 != p3

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

问题解析

在 C++ 中,当一个类通过多继承继承多个基类时,每个基类的子对象在派生类中会占用不同的内存地址。这导致了指针偏移问题。我们通过代码逐步分析。


代码结构

class Base1 { public: int _b1; };  // 定义基类 Base1
class Base2 { public: int _b2; };  // 定义基类 Base2
class Derive : public Base1, public Base2 { public: int _d; };  // Derive 类多继承自 Base1 和 Base2int main()
{Derive d;          // 定义派生类对象 dBase1* p1 = &d;    // 将 d 转换为指向 Base1 的指针Base2* p2 = &d;    // 将 d 转换为指向 Base2 的指针Derive* p3 = &d;   // 将 d 转换为指向 Derive 的指针return 0;
}

多继承时的内存布局

  1. 内存布局示意图
    假设 Derive 类对象的内存布局如下:

    [Base1::_b1] [Base2::_b2] [Derive::_d]
    
    • Base1 子对象占用一部分内存。
    • Base2 子对象占用另一部分内存。
    • Derive 自己的数据成员占用剩余的内存。
  2. 指针偏移

    • p1 指向 Base1 子对象的起始地址。
    • p2 指向 Base2 子对象的起始地址(相对于 Base1 子对象,偏移了 Base1 的大小)。
    • p3 指向整个 Derive 对象的起始地址。

指针值比较

根据指针的偏移,指针值的关系如下:

  • p1p3
    p1 指向 Base1 子对象的起始地址,而 p3 指向整个 Derive 对象的起始地址。在多继承情况下,Base1 子对象和 Derive 对象的起始地址是相同的,所以 p1 == p3

  • p2p3
    p2 指向 Base2 子对象的起始地址,而 Base2 子对象位于 Base1 子对象之后,因此 p2 > p3

  • p1p2
    因为 p1 指向的是 Base1 子对象的起始地址,而 p2 指向的是 Base2 子对象的起始地址,且 Base2 的内存布局在 Base1 之后,因此 p1 < p2


选项分析

  • A: p1 == p2 == p3
    错误。p1p2 指向不同的基类子对象,地址不同。

  • B: p1 < p2 < p3
    错误。p1 < p2 是对的,但 p1 == p3,所以 p1 不小于 p3

  • C: p1 == p3 != p2
    正确。p1p3 的地址相同,p2 的地址不同于二者。

  • D: p1 != p2 != p3
    错误。p1 == p3,所以 p1 != p3 是错误的。


正确答案

C: p1 == p3 != p2


7.4 IO库中的菱形虚拟继承

00edb13108b598057f2310163992c318

template<class CharT, class Traits = std::char_traits<CharT>>class basic_ostream : virtual public std::basic_ios<CharT, Traits>{};
template<class CharT, class Traits = std::char_traits<CharT>>class basic_istream : virtual public std::basic_ios<CharT, Traits>{};

8.继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的语言都没有多继承,如Java
  3. 继承和组合
  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

  • 优先使用对象组合,而不是类继承 。

  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

// Tire(轮胎)和Car(车)更符合has-a的关系
class Tire {protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸
};
class Car {protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t1; // 轮胎Tire _t2; // 轮胎Tire _t3; // 轮胎Tire _t4; // 轮胎
};
class BMW : public Car {public:void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {public:void Drive() { cout << "好坐-舒适" << endl; }
};
template<class T>class vector{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>class stack : public vector<T>{};
template<class T>class stack{public:vector<T> _v;};
int main()
{return 0;
}

9.笔试面试题

  1. 什么是菱形继承?菱形继承的问题是什么?

一个类通过多条路径继承自同一个基类,导致基类在最终的派生类中出现多次。

菱形继承有数据冗余和二义性的问题。

  1. 什么是菱形虚拟继承?如何解决数据冗余和二义性的?

菱形虚拟继承 是通过虚继承机制解决菱形继承问题的方法。

菱形虚拟继承通过虚继承机制,使得最终的派生类中只包含基类的一个实例,从而避免了数据冗余和二义性问题。

  1. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承与组合的区别

特性继承 (Inheritance)组合 (Composition)
关系类型is-a 关系has-a 关系
耦合度高耦合低耦合
灵活性较不灵活,类层次结构固定灵活,可以动态组合不同的组件
复用方式通过继承复用父类的代码通过组合复用其他类的功能
多态性支持多态不直接支持多态,但可以通过接口实现
封装性继承可能破坏封装性更好的封装性
扩展性扩展性较差,修改父类会影响所有子类扩展性好,可以轻松添加或替换组件

何时使用继承?

  1. 表示“is-a”关系
    • 当一个类确实是另一个类的一种类型时。例如,DogAnimal 的一种,SquareShape 的一种。
  2. 需要实现多态性
    • 当需要通过基类引用来操作不同子类对象时,使用继承可以实现多态。
  3. 需要复用基类的代码
    • 当子类需要复用基类的属性和方法,并且这些方法在子类中不需要进行重大修改时。

何时使用组合?

  1. 表示“has-a”关系
    • 当一个类包含另一个类的实例作为其组成部分时。例如,Car 包含一个 EngineStudent 包含一个 Address
  2. 需要更大的灵活性
    • 当需要动态组合不同的组件,或者组件之间的关系是动态变化的时,组合更为合适。
  3. 需要更好的封装性
    • 当需要更好地封装各个组件的功能,避免继承带来的耦合时,组合是更好的选择。
  4. 不希望破坏封装性
    • 当不希望子类继承父类的所有方法和属性,或者不希望子类修改父类的行为时,组合可以提供更好的封装性。

相关文章:

【C++】18.继承

文章目录 1.继承的概念及定义1.1 继承的概念1.2 继承定义1.2.1定义格式1.2.2继承关系和访问限定符1.2.3继承基类成员访问方式的变化 1.3 继承类模板 2.基类和派生类对象赋值转换3.继承中的作用域3.1 隐藏规则&#xff1a;3.2 考察继承作用域相关选择题 4.派生类的默认成员函数4…...

R语言基础| 中级绘图

写在前面 前面第六章的图形主要是展示单分类变量或连续型变量的分布情况。本章主要研究二元变量或多元变量关系的可视化。更多教程可参考&#xff1a; R语言基础学习手册 图片集锦&#xff1a; 11.1 散点图 1&#xff09;添加最佳拟合曲线的散点图&#xff1a; 绘制汽车重…...

TANGO - 数字人全身动作生成

文章目录 一、关于 TANGO演示视频&#xff08;YouTube&#xff09;&#x1f4dd;发布计划 二、⚒️安装克隆存储库构建环境 三、&#x1f680;训练和推理1、推理2、为自定义字符创建图形 一、关于 TANGO TANGO 是 具有分层音频运动嵌入 和 扩散插值的共语音手势视频再现 由东…...

从configure.ac到构建环境:解析Mellanox OFED内核模块构建脚本

在软件开发过程中,特别是在处理复杂的内核模块如Mellanox OFED(OpenFabrics Enterprise Distribution)时,构建一个可移植且高效的构建系统至关重要。Autoconf和Automake等工具在此过程中扮演着核心角色。本文将深入解析一个用于准备Mellanox OFED内核模块构建环境的Autocon…...

深入理解 Android 中的 KeyguardManager

深入理解 Android 中的 KeyguardManager 引言 在 Android 系统中&#xff0c;KeyguardManager 是一个重要的系统服务&#xff0c;负责管理设备的锁屏界面&#xff08;Keyguard&#xff09;。锁屏界面是设备安全性的第一道防线&#xff0c;用于防止未经授权的用户访问设备。Ke…...

在Vue3项目中使用svg-sprite-loader

1.普通的svg图片使用方式 1.1 路径引入 正常我们会把项目中的静态资源放在指定的一个目录&#xff0c;例如assets,使用起来就像 <img src"../assets/svgicons/about.svg" /> 1.2封装组件使用 显然上面的这种方法在项目开发中不太适用&#xff0c;每次都需…...

Linux(Centos 7.6)命令详解:pwd

1.命令作用 显示当前工作目录的完整路径(Print Working Directory) 2.命令语法 Usage: pwd [-LP] 3.参数详解 -L&#xff0c;显示逻辑路径&#xff0c;遵循符号链接&#xff1b;这是默认选项。-P&#xff0c;显示物理路径&#xff0c;不遵循符号链接。 4.常用用例 1.-L参…...

【iOS Swift Moya 最新请求网络框架封装通用】

【iOS Swift Moya 最新请求网络框架封装通用】 前言框架结构1.API定义&#xff08;TargetType&#xff09;2. 配置MoyaProvider3. 网络管理器4. 使用示例注意事项进一步优化 前言 设计一个基于Moya的网络请求框架&#xff0c;可以提供灵活的网络请求管理&#xff0c;例如设置请…...

【算法学习】——设施选址问题(动态规划)

题目描述 在一条高速公路附近有 V 个村庄&#xff0c;选择 P 个村庄在其附近建立邮局&#xff0c;要求每个村庄到最近的邮局的距离和最小(1<V<300&#xff0c;1<P<30)。 问题分析 这个问题是一个经典的设施选址问题&#xff08;Facility Location Problem&#…...

Linux——修改文件夹的所属用户组和用户

一、命令 举例&#xff1a; 授权 MOT17 文件夹 给 hust_xxx 用户&#xff1a; sudo chown -R hust_xxx:hust_xxx MOT17参考 Linux授权文件夹给用户...

我用Ai学Android Jetpack Compose之Text

这篇开始学习各种UI元素&#xff0c;答案来自 通义千问&#xff0c;通义千问没法生成图片&#xff0c;图片是我补充的。 下述代码只要复制到第一个工程&#xff0c;做一些import操作&#xff0c;一般import androidx.compose包里的东西&#xff0c;即可看到预览效果。完整工程代…...

H5通过URL Scheme唤醒手机地图APP

1.高德地图 安卓URL Scheme&#xff1a;baidumap:// 官方文档&#xff1a;https://lbs.amap.com/api/amap-mobile/guide/android/navigation IOS URL Scheme&#xff1a;iosamap:// 官方文档&#xff1a;https://lbs.amap.com/api/amap-mobile/guide/ios/navi HarmonyOS NEXT U…...

【Java数据结构】二叉树

1.树型结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;由n个结点组成的具有层次关系的集合。下面是它的特点&#xff1a; 根结点是没有前驱的结点&#xff08;没有父结点的结点&#xff09;子结点之间互不相交除了根结点外&#xff0c;其它结点都只有一个父结点n个结…...

Golang设计模式目录

go语言实现设计模式 1 文章目录&#xff1a; 1.1 创建型模式 1.Golang设计模式之工厂模式2.Golang设计模式之抽象工厂模式3.Golang设计模式之单例模式4.Golang设计模式之建造者模式5.Golang设计模式之原型模式 1.2 结构型模式 6.Golang设计模式之适配器模式7.Golang设计模式之桥…...

vue3+Echarts+ts实现甘特图

项目场景&#xff1a; vue3Echartsts实现甘特图;发布任务 代码实现 封装ganttEcharts.vue <template><!-- Echarts 甘特图 --><div ref"progressChart" class"w100 h100"></div> </template> <script lang"ts&qu…...

nginx-灰度发布策略(split_clients)

一. 简述&#xff1a; 基于客户端的灰度发布&#xff08;也称为蓝绿部署或金丝雀发布&#xff09;是一种逐步将新版本的服务或应用暴露给部分用户&#xff0c;以确保在出现问题时可以快速回滚并最小化影响的技术。对于 Nginx&#xff0c;可以通过配置和使用不同的模块来实现基于…...

SQL中聚类后字段数据串联字符串方法研究

在 SQL 中&#xff0c;使用 聚类&#xff08;GROUP BY&#xff09; 后将某个字段的数据串联为一个字符串&#xff0c;常见的方法包括以下几种&#xff0c;取决于数据库管理系统&#xff08;DBMS&#xff09;的具体支持功能&#xff1a; 1. 使用 GROUP_CONCAT &#xff08;MySQL…...

vue3组件化开发优势劣势分析,及一个案例

Vue 3 组件化开发的优势和劣势 优势 可复用性&#xff1a; 组件可以重复使用&#xff0c;减少代码冗余&#xff0c;提高开发效率。 可以在不同的项目中复用组件&#xff0c;提升开发速度。 可维护性&#xff1a; 组件化开发使得代码结构清晰&#xff0c;易于维护。 每个…...

Springboot SAP Docker 镜像打包问题

问题类1&#xff0c;sapjco.jar 未识别到&#xff1a;Caused by: java.lang.NoClassDefFoundError: com/sap/conn/jco/ext/DestinationDataProvider 1./deploy/lib/ 文件下放sapjco3.jar、libsapjco3.so、sapjco3.dll 2.docker文件核心内容&#xff1a; COPY /deploy/lib/sap…...

nmap探测Web服务

HTTP服务 探测基本认证信息 nmap --script http-auth [目标]探测默认账户 nmap --scripthttp-default-accounts -p [端口] [目标]检查是否存在风险方法 nmap --script http-methods [目标]探测访问一个网页的时间 nmap --scripthttp-chrono -p 80 [目标]提取HTTP注释信息 nmap…...

【学习总结|DAY028】后端Web实战(部门管理)

在 Web 后端开发领域&#xff0c;构建高效、规范且功能完备的系统是核心目标。本文将围绕 Tlias 智能学习辅助系统的后端开发展开&#xff0c;详细阐述从开发准备工作到各部门管理功能实现&#xff0c;以及日志技术应用的全过程&#xff0c;为开发者提供全面的实践参考。 一、…...

Servlet 和 Spring MVC:区别与联系

前言 在 Java Web 开发中&#xff0c;Servlet 和 Spring MVC 是两个重要的技术。Servlet 是 Java Web 的基础组件&#xff0c;而 Spring MVC 是一个高级 Web 框架&#xff0c;建立在 Servlet 的基础之上&#xff0c;提供了强大的功能和易用性。这篇文章将从定义、原理、功能对…...

【君正T31开发记录】12.编译工具相关总结及介绍

移植交叉工具包的时候&#xff0c;发现这是很多工具的集合包&#xff1b;以及写makefile的时候&#xff0c;也需要了解下这些工具的作用及用法&#xff0c;这里总结记录一下常见的工具及相关用法。 g C编译器&#xff0c;用于编译C源代码文件&#xff0c;这个很常见&#xff0…...

Python 开发框架搭建简单博客系统:代码实践与应用

在当今数字化时代&#xff0c;博客作为一种流行的信息分享和交流平台&#xff0c;拥有广泛的受众。Python 以其强大的功能和丰富的库&#xff0c;为构建博客系统提供了理想的技术支持。本文将详细介绍如何利用 Python 开发框架搭建一个简单博客系统&#xff0c;包括功能实现、代…...

Java 正则表达式入门与应用(详细版)

正则表达式&#xff08;Regular Expression&#xff0c;简称Regex&#xff09;是一种文本模式匹配工具&#xff0c;在许多编程语言中都得到了广泛应用。Java 作为一种强大的编程语言&#xff0c;提供了对正则表达式的内建支持&#xff0c;使得在字符串处理、数据验证和文本解析…...

高效内存管理与调试技巧:深入解析 AddressSanitizer

在现代 C开发中&#xff0c;内存管理是一个至关重要但也容易出错的领域。即使使用了智能指针和其他高效工具&#xff0c;复杂的项目仍可能出现内存泄漏、非法访问等问题。为了解决这些问题&#xff0c;Google 开发了一个强大的工具——AddressSanitizer (ASan)。本文将详细介绍…...

力扣第137题:只出现一次的数字 II C语言解法

力扣第137题&#xff1a;只出现一次的数字 II C语言解法 题目描述 给定一个整数数组 nums&#xff0c;其中每个元素出现三次&#xff0c;除了一个元素出现一次。找出那个只出现一次的元素。 说明&#xff1a; 你的算法应该具有线性时间复杂度。你不可以使用额外的空间&…...

【Qt】控件概述和QWidget核心属性1(enabled、geometry、windowTitle、windowIcon、QRC机制)

一、控件概念 界面上各种元素、各种部分的统称&#xff08;如按钮、输入框、下拉框、单选复选框...&#xff09; Qt作为GUI开发框架&#xff0c;内置了各种的常用控件&#xff0c;并支持自定义控件。 二、控件体系发展 1.没有完全的控件&#xff0c;需要使用绘图API手动绘制…...

25年1月更新。Windows 上搭建 Python 开发环境:PyCharm 安装全攻略(文中有安装包不用官网下载)

python环境没有安装的可以点击这里先安装好python环境&#xff0c;python环境安装教程 安装 PyCharm IDE 获取 PyCharm PyCharm 提供两种主要版本——社区版&#xff08;免费&#xff09;和专业版&#xff08;付费&#xff09;。对于初学者和个人开发者而言&#xff0c;社区…...

软件工程大复习之(四)——面向对象与UML

4.1 面向对象概述 面向对象&#xff08;OO&#xff09;是一种编程范式&#xff0c;它将数据和处理数据的方法封装在对象中。面向对象的主要概念包括&#xff1a; 对象&#xff1a;实例化的数据和方法的集合。类&#xff1a;对象的蓝图或模板。封装&#xff1a;隐藏对象的内部…...

前端基础函数算法整理应用(sort+reduce+date+双重for循环)

文章目录 基础函数算法reduce 函数算法sort 函数算法时间排序1. 对日期字符串数组进行排序2. 对包含日期对象的数组进行排序3. 对包含时间戳的数组进行排序4. 对包含日期时间信息的对象数组进行排序 基础函数算法 一、排序算法 冒泡排序&#xff08;Bubble Sort&#xff09; …...

web系统漏洞攻击靶场

摘 要 互联网极速发展的同时&#xff0c;也会带来一些安全性的风险&#xff0c;一些不为人知的安全问题也逐渐暴露出来。近年来&#xff0c;媒体不断披露了许多网络安全事故&#xff0c;许多网络应用程序被黑客攻击&#xff0c;导致内部数据外泄&#xff0c;人们开始认识到网络…...

苍穹外卖-day07(Spring Cache 购物车业务逻辑)

内容 缓存菜品缓存套餐添加购物车查看购物车清空购物车 功能实现&#xff1a;缓存商品、购物车 效果图&#xff1a; 1. 缓存菜品 1.1 问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增…...

win10 VS2019上libtorch库配置过程

win10 VS2019上libtorch库配置过程 0 引言1 获取libtorch2 在VS上配置使用libtorch库3 结语 0 引言 &#x1f4bb;&#x1f4bb;AI一下&#x1f4bb;&#x1f4bb;   libtorch库是一个用于深度学习的C库&#xff0c;是PyTorch的官方C前端。它提供了用于构建和训练深度学习模…...

git 常用命令和本地合并解决冲突

目录 一、常用命令 二、本地可视化合并分支解决冲突 一、常用命令 最近&#xff0c;使用mac电脑&#xff0c;无法直接使用小乌龟进行可视化操作&#xff0c;现在记录一些常用命令。 拉取&#xff1a; git clone <git url> 仅拉起某个单独分支&#xff1a; git clo…...

Elasticsearch 创建索引 Mapping映射属性 索引库操作 增删改查

Mapping Type映射属性 mapping是对索引库中文档的约束&#xff0c;有以下类型。 text&#xff1a;用于分析和全文搜索&#xff0c;通常适用于长文本字段。keyword&#xff1a;用于精确匹配&#xff0c;不会进行分析&#xff0c;适用于标签、ID 等精确匹配场景。integer、long…...

Objective-C语言的数据结构

Objective-C语言中的数据结构 Objective-C是一种面向对象的编程语言&#xff0c;其在苹果公司的软件开发中得到了广泛应用。它主要用于开发macOS和iOS应用程序。虽然Objective-C有许多丰富的特性&#xff0c;但在程序设计中&#xff0c;数据结构仍然是构建任何应用程序的基础。…...

智能水文:ChatGPT等大语言模型如何提升水资源分析和模型优化的效率

大语言模型与水文水资源领域的融合具有多种具体应用&#xff0c;以下是一些主要的应用实例&#xff1a; 1、时间序列水文数据自动化处理及机器学习模型&#xff1a; ●自动分析流量或降雨量的异常值 ●参数估计&#xff0c;例如PIII型曲线的参数 ●自动分析降雨频率及重现期 ●…...

ETL的工作原理

ETL的工作原理 什么是ETL_云计算主题库-阿里云 ETL的工作原理可以分为三个主要的步骤&#xff1a;Extract&#xff08;提取&#xff09;、Transform&#xff08;转换&#xff09;、Load&#xff08;加载&#xff09;。 工作步骤 描述 Extract &#xff08;提取&#xff09;…...

黑马头条平台管理实战

黑马头条 08平台管理 1.开始准备和开发思路1.1.开发网关1.2编写admin-gateway 代码 2.开发登录微服务2.1编写登录微服务 3.频道管理4.敏感词管理5.用户认证审核6.自媒体文章人工审核99. 最后开发中碰到的问题汇总1.关于nacos 配置 问题2.在开发频道管理新增频道后端无法接收到前…...

电池管理系统(BMS)架构详细解析:原理与器件选型指南

BMS&#xff08;电池管理系统&#xff09;架构详细讲解 从你提供的BMS&#xff08;Battery Management System&#xff09;架构图来看&#xff0c;主要涉及到电池监控模块、通信模块、功率控制模块等部分。下面我将详细讲解该架构的各个功能模块及其工作原理。 1. 电池管理核…...

SpringBoot环境和Maven配置

SpringBoot环境和Maven配置 1. 环境准备2. Maven2.1 什么是Maven2.2 为什么要学 Maven2.3 创建一个 Maven项目2.4 Maven核心功能2.4.1 项目构建2.4.2 依赖管理2.4.3 Maven Help插件 2.5 Maven 仓库2.5.1本地仓库2.5.2 中央仓库2.5.3 私有服务器, 也称为私服 2.6 Maven设置国内源…...

lambda用法及其原理

目录 lambda形式lambda用法1.sort降序2.swap3.捕捉列表 习题解题 lambda形式 [capture-list](parameters)->return type{function boby}[capture-list]&#xff1a;[捕捉列表]用于捕捉函数外的参数&#xff0c;可以为空&#xff0c;但不能省略&#xff1b;(parameters) &am…...

Postgresql源码(139)vim直接修改postgresql表文件的简单实例

1 前言 PG可以用pageinspect方便的读取查看表文件。本篇介绍一种用vim查看、编辑的方法&#xff0c;案例比较简单&#xff0c;主要分享原理。 修改表文件和controlfile是非常危险的行为&#xff0c;请不要在生产尝试。 2 用例 简化问题&#xff0c;用简单编码的数据类型。 d…...

Lianwei 安全周报|2025.1.2

以下是本周「Lianwei周报」&#xff0c;我们总结推荐了本周的政策/标准/指南最新动态、热点资讯和安全事件&#xff0c;保证大家不错过本周的每一个重点&#xff01; 政策/标准/指南最新动态 01 国家数据局等五部门印发《关于促进企业数据资源开发利用的意见》 为充分释放企业…...

Vue3-跨层组件通信Provide/Inject机制详解

Vue 3 中的 Provide 和 Inject 机制是专为跨层级传递数据而设计的&#xff0c;适用于祖先组件和后代组件之间的通信。与props 和 emits 不同&#xff0c;Provide/Inject 可以跨越多个层级进行数据传递&#xff0c;而不需要逐层传递。 1. Provide provide 是一个在祖先组件中提…...

springcloud 介绍

Spring Cloud是一个基于Spring Boot的微服务架构解决方案集合&#xff0c;它提供了一套完整的工具集&#xff0c;用于快速构建分布式系统。在Spring Cloud的架构中&#xff0c;服务被拆分为一系列小型、自治的微服务&#xff0c;每个服务运行在其独立的进程中&#xff0c;并通过…...

css预处理器sass

在前端开发的世界中&#xff0c;CSS 是构建网页样式的基础。然而&#xff0c;随着项目规模的增大&#xff0c;纯 CSS 的编写和维护往往会变得复杂而繁琐。为了解决这些痛点&#xff0c;Sass&#xff08;Syntactically Awesome Style Sheets&#xff09;应运而生。Sass 是一种 C…...

匠人天工Ai浮雕网站创新发布了ZBrush插件,提效500%,为AI+数字雕刻行业带来新的活力

2025年1月6日&#xff0c;杭州——杭州仓颉造梦数字科技公司旗下产品匠人天工近日宣布推出一款创新的ZBrush插件&#xff0c;旨在为AI数字雕刻行业带来前所未有的效率提升。该插件通过一系列智能化功能&#xff0c;大幅简化了数字雕刻的建模流程&#xff0c;使建模效率提高了50…...

解决 Pangolin 版本不兼容导致的编译错误

在使用 Pangolin 库时&#xff0c;有时候会遇到由于版本不兼容而导致的编译错误。本文将通过一个具体的错误案例&#xff0c;展示如何识别和解决这种问题。 问题描述 在编译时&#xff0c;遇到如下编译错误&#xff1a; /usr/local/include/pangolin/gl/glsl.hpp: In member…...