【C++】16.继承
C++三大特性:封装,继承,多态
在前面的章节中,我们讲过了封装,也就是通过类和访问修饰符来进行封装。
接下来我们就来认识一下新的特性——继承
1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有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; // 职称
};
上面代码中,大部分信息是公共的我们写了两遍,会让代码变得冗余。
那怎么办呢?下面我们公共的成员都放到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 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是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)
1.2.2 继承基类成员访问方式的变化
I. 类成员访问控制
类成员的访问修饰符决定了成员的作用域和可见性:
访问修饰符 | 类内部 | 派生类 | 类外部(对象) | 说明 |
---|---|---|---|---|
public | ✔️ | ✔️ | ✔️ | 完全开放 |
protected | ✔️ | ✔️ | ❌ | 仅限类及派生类内部访问 |
private | ✔️ | ❌ | ❌ | 仅限类内部访问 |
II. 继承方式的影响
继承方式决定了基类成员在派生类中的访问权限变化:
基类成员权限 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | public (保持) | protected (降级) | private (降级) |
protected | protected (保持) | protected (保持) | private (降级) |
private | 不可访问 | 不可访问 | 不可访问 |
关键规则:
-
基类私有成员:无论继承方式如何,派生类均无法直接访问。
-
继承方式作用:调整基类
public
/protected
成员在派生类中的最高访问权限。 -
默认继承方式:C++ 中
class
默认为private
继承,struct
默认为public
继承。
III. 注意事项
-
友元(
friend
):友元关系不继承,需在派生类中重新声明。 -
访问重写:可通过
using Base::member;
显式调整成员在派生类中的访问权限。 -
实际使用:优先选择
public
继承(符合“is-a”关系),慎用protected
/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 // protected 继承 —— Print() 和 _name 均为 protected, _age 不可访问
//class Student : private Person // private 继承 —— Print() 和 _name 均为 private, _age 不可访问
class Student : public Person //public 继承 —— Print() 是 public, _name 是 protected, _age 不可访问
{
protected:int _stunum = 1; // 学号
};
1.3 继承类模板
namespace Ro
{//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){// 基类是类模板时,需要指定一下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(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()
{Ro::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
至于什么是is-a,什么是has-a,我们下面一点会讲到
2. 基类和派生类间的转换
• public继承的派生类对象 可以赋值给 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
• 基类对象不能赋值给派生类对象。
• 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)
class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{Student sobj;// 1.派生类对象可以赋值给基类的指针/引用Person* pp = &sobj;Person& rp = sobj;// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的Person pobj = sobj;//2.基类对象不能赋值给派生类对象,这里会编译报错//sobj = pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student* ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10; //这就是越界访问的情况return 0;
}
赋值转换总结
场景 | 是否合法 | 说明 |
---|---|---|
派生类对象 → 基类对象 | ✔️ | 发生对象切割 |
基类对象 → 派生类对象 | ❌ | 需要强制类型转换(可能不安全) |
基类指针 → 派生类指针 | ❌ | 需使用 dynamic_cast 或 static_cast |
派生类指针 → 基类指针 | ✔️ | 天然支持(向上转型) |
基类引用 → 派生类引用 | ❌ | 需使用 dynamic_cast |
派生类引用 → 基类引用 | ✔️ | 天然支持(向上转型) |
最佳实践
涉及到的多态和智能指针后面会讲
-
优先使用指针/引用传递对象
避免对象切割,保持多态性。 -
基类析构函数声明为虚函数
确保通过基类指针删除派生类对象时能正确析构。 -
谨慎使用强制类型转换
尽量通过设计避免类型转换,尤其是dynamic_cast
。 -
使用
override
关键字
明确派生类函数覆盖基类虚函数。
3. 继承中的作用域
3.1 隐藏规则:
1. 在继承体系中基类和派生类都有独立的作用域。
2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用 基类::基类成员 显式访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
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 考察继承作用域相关选择题
1. A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
2. 下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行
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.我们知道函数重载的核心条件之一就是在同一作用域,这里派生类和基类都有他们独立的作用域,所以不可能构成函数重载,正确答案是B—隐藏,因为不管参数是否相同,只需要函数名相同就会构成隐藏
2.由于函数名相同,在派生类中隐藏了基类的func,所以在派生类中找不到无参的func,会导致编译报错
4. 派生类的默认成员函数
4.1 4个常见默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成一个。
那么在派生类中,这几个成员函数是如何生成的呢?
派生类默认会继承以下成员函数(若未显式定义):
-
默认构造函数
-
拷贝构造函数
-
拷贝赋值运算符
-
移动构造函数(C++11 起,这里先不做讲解)
-
移动赋值运算符(C++11 起,这里先不做讲解)
-
析构函数
4.2 派生类的构造函数
I. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
II. 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
III. 派生类对象初始化先调用基类构造再调派生类构造。
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) //基类不是默认构造,必须显式调用基类的构造初始化基类的成员, _num(num){cout << "Student()" << endl;}protected:int _num; //学号
};
int main()
{Student s1("jack", 18);return 0;
}
注意:就算在初始化列表中先初始化派生类的成员,再初始化基类的成员,编译器也会跳过派生类的成员,先去初始化基类的成员
4.3 派生类的拷贝构造
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
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) //基类不是默认构造,必须显式调用基类的构造初始化基类的成员, _num(num){cout << "Student()" << endl;}//派生类的拷贝构造Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num; //学号
};
int main()
{Student s1("jack", 18);Student s2(s1);return 0;
}
4.4 派生类的赋值重载
派生类的operator=必须要调用基类的operator=完成基类的赋值。需要注意的是派生类的 operator=隐藏了基类的operator=,所以显式调用基类的operator=,需要指定基类作用域
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;}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;}protected:int _num; //学号
};
int main()
{Student s1("jack", 18);cout << endl;Student s2(s1);cout << endl;Student s3("rose", 17);s1 = s3;cout << endl;return 0;
}
4.5 派生类的析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序(前面章节讲解过后定义的先析构,这里同样适用)
派生类对象析构清理先调用派生类析构再调基类的析构。
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) //基类不是默认构造,必须显式调用基类的构造初始化基类的成员, _num(num){cout << "Student()" << endl;}//派生类的析构函数~Student(){cout << "~Student()" << endl;}
protected:int _num; //学号
};
int main()
{Student s1("jack", 18);return 0;
}
注意:因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
4.6 实现一个不能被继承的类
方法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;//先声明,避免Person的友元 Display 的参数不认识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;
}
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;// 公有的情况下,基类,派生类指定类域都可以访问静态成员,并且你变我也变Student::_count = 3;cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}
7. 多继承及其菱形继承问题
7.1 继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
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; // 姓名
};
// 使用虚继承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;
}
虚继承,在继承方式前加上virtual关键字,就可以解决数据冗余和二义性的问题,尽管指定类域如
a.Student::_name = "xxx"修改,每个类中的_name都会被修改
给出下面一段代码,思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?
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){cout << _name << endl;}
protected:int _num; //学号
};
class Teacher : virtual public Person
{
public:Teacher(const char* name, int id):Person(name), _id(id){cout << _name << endl;}
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){cout << _name << endl;}
protected:string _majorCourse; // 主修课程
};
int main()
{// 思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?Assistant a("张三", "李四", "王五");cout << a._name << endl;return 0;
}
答案是王五,因为这里是菱形虚拟继承,具体原因如下:
-
虚基类 Person 优先初始化
-
根据虚继承规则,
Person
的构造函数由最终派生类Assistant
直接调用。 -
Assistant
的初始化列表中显式调用Person(name3)
,即Person("王五")
。 -
只有这一次
Person
的构造被实际执行。
-
-
Student 和 Teacher 的构造被跳过对 Person 的初始化
-
Student
和Teacher
的初始化列表中虽然调用了Person(name)
,但由于Person
是虚基类,这些调用在构造Assistant
时被忽略。 -
Student
和Teacher
中的Person
子对象共享Assistant
初始化的唯一Person
实例。
-
-
最终
_name
的值-
所有路径的
_name
都指向Assistant
初始化的"王五"
。
-
我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。
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;
}
1. 内存布局分析
假设 int
占 4 字节且无内存对齐优化,Derive
对象的内存布局如下:
+----------------+ | Base1::_b1 | ← p1 和 p3 指向此处 +----------------+ | Base2::_b2 | ← p2 指向此处 +----------------+ | Derive::_d | +----------------+
-
p1
(Base1*
):指向Base1
子对象的起始地址(即整个对象的起始地址)。 -
p2
(Base2*
):指向Base2
子对象的起始地址(偏移 4 字节)。 -
p3
(Derive*
):指向整个对象的起始地址(与p1
相同)。
2. 指针值对比
指针 | 地址值 | 关系 |
---|---|---|
p1 (Base1*) | &d (起始地址) | p1 == p3 |
p2 (Base2*) | &d + 4 (偏移 4 字节) | p1 < p2 ,p2 < p3 + 8 |
p3 (Derive*) | &d (起始地址) | p3 == p1 |
我们也可以将指针打印出来验证一下
所以答案选C
8. 继承和组合
• public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
• 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
• 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可 见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
• 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
• 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合用组合(has-a)
归纳总结:
1. 基本定义
特性 | 继承(Inheritance) | 组合(Composition) |
---|---|---|
核心思想 | "is-a" 关系:派生类是基类的一种特殊类型。 | "has-a" 关系:类中包含其他类的对象作为成员。 |
代码复用 | 通过继承基类的成员和方法实现复用。 | 通过调用成员对象的方法实现复用。 |
耦合度 | 高耦合:派生类与基类紧密绑定。 | 低耦合:类与成员对象通过接口交互。 |
设计目标 | 强调类之间的层次关系和多态性。 | 强调模块化和功能组合。 |
2. 代码示例
// Tire(轮胎)和Car(车)更符合has-a的关系
class Tire
{
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸
};
class Car
{
protected:string _colour = "白色"; // 颜色string _num = "赣A12345"; // 车牌号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;
}
3. 核心区别
(1) 设计关系
-
继承:强调 "is-a" 关系(子类是父类的特化)。
-
例如:
Bird
是一种Animal
,Student
是一种Person
。
-
-
组合:强调 "has-a" 关系(类拥有其他类的功能)。
-
例如:
Car
有Engine
和Wheel
,Computer
有CPU
和Memory
。
-
(2) 代码复用方式
-
继承:直接复用基类的属性和方法(通过派生)。
-
组合:通过调用成员对象的方法间接复用功能。
(3) 灵活性
-
继承:
-
优点:天然支持多态,适合扩展基类行为(如重写虚函数)。
-
缺点:派生类与基类强耦合,基类修改可能影响所有派生类。
-
-
组合:
-
优点:灵活替换成员对象(例如更换引擎类型),符合 开闭原则。
-
缺点:需要显式调用成员对象的方法,无法直接复用接口。
-
(4) 内存与性能
-
继承:派生类对象包含完整的基类子对象,内存占用可能更大。
-
组合:成员对象独立存在,内存占用更模块化,但可能因间接访问略慢。
4. 优缺点对比
场景 | 继承 | 组合 |
---|---|---|
代码复用 | 直接复用基类代码 | 需显式调用成员对象的方法 |
多态支持 | ✅ 天然支持(虚函数) | ❌ 需依赖接口类或抽象类 |
耦合度 | 高(派生类依赖基类实现) | 低(通过接口交互) |
扩展性 | 修改基类可能影响派生类 | 可动态替换成员对象(如依赖注入) |
设计复杂度 | 高(多重继承、菱形继承问题) | 低(模块化设计) |
适用场景 | 明确的层次关系,需要多态 | 功能组合,模块化设计 |
5. 经典设计原则
组合优于继承(Composition over Inheritance)
-
动机:减少耦合,提高灵活性。
-
实践:优先用组合实现功能复用,仅在需要多态或严格满足 "is-a" 关系时使用继承。
6. 如何选择?
场景 | 选择继承 | 选择组合 |
---|---|---|
需要多态(虚函数) | ✅ | ❌ |
功能模块需要动态替换 | ❌ | ✅ |
类之间的关系是严格的 "is-a" | ✅ | ❌ |
类之间的关系是 "has-a" 或 "uses-a" | ❌ | ✅ |
避免基类与派生类的强耦合 | ❌ | ✅ |
7. 总结
-
继承:
-
适合建立类层次结构,实现多态。
-
缺点是耦合度高,容易导致复杂继承链(如菱形继承)。
-
-
组合:
-
适合模块化设计,提高灵活性和可维护性。
-
缺点是需要更多代码委托成员对象的方法。
-
实际开发中,优先使用组合,仅在必要时使用继承,尤其是在需要多态或严格符合 "is-a" 关系的场景。
相关文章:
【C++】16.继承
C三大特性:封装,继承,多态 在前面的章节中,我们讲过了封装,也就是通过类和访问修饰符来进行封装。 接下来我们就来认识一下新的特性——继承 1. 继承的概念及定义 1.1 继承的概念 继承(inheritance)机制是面向对…...
LlamaIndex 第七篇 结构化数据提取
大型语言模型(LLMs)在数据理解方面表现出色,这也促成了它们最重要的应用场景之一:能够将常规的人类语言(我们称之为非结构化数据)转化为特定的、规范的、可被计算机程序处理的格式。我们将这一过程的输出称…...
PHP API安全设计四要素:构建坚不可摧的接口防护体系
引言:API安全的重要性 在当今前后端分离和微服务架构盛行的时代,API已成为系统间通信的核心枢纽。然而,不安全的API可能导致: 数据泄露:敏感信息被非法获取篡改风险:传输数据被中间人修改重放攻击&#x…...
英语16种时态
时态应用场合格式例子一般现在时表示经常、反复发生的动作,客观事实或普遍真理主语 动词原形(第三人称单数作主语时动词加 -s/-es)The sun rises in the east.一般过去时表示过去某个时间发生的动作或存在的状态主语 动词的过去式I visited…...
使用 goaccess 分析 nginx 访问日志
介绍 goaccess 是一个在本地解析日志的工具, 可以直接在命令行终端环境中使用 TUI 界面查看分析结果, 也可以导出为更加丰富的 HTML 页面. 官网: https://goaccess.io/ 下载安装 常见的 Linux 包管理器中都包含了 goaccess, 直接安装就行. 以 Ubuntu 为例: sudo apt instal…...
什么是中央税
中央税(又称国家税)是指由中央政府直接征收、管理和支配的税种,其收入全额纳入中央财政,用于保障国家层面的财政支出和宏观调控。中央税通常具有税基广泛、收入稳定、涉及国家主权或全局性经济调控的特点。 --- 中央税的核心特征…...
AI Agent(10):个人助手应用
引言 本文聚焦AI Agent在个人助手领域的应用,探讨其如何在个人生产力提升、健康与生活管理、学习与教育辅助以及娱乐与社交互动四个方面,为用户创造价值并解决实际问题。 AI个人助手正从简单的指令执行者逐渐发展为具有自主性、适应性和个性化能力的智能伙伴。这一转变不仅…...
力扣70题解
记录 2025.5.8 题目: 思路: 1.初始化:p 和 q 初始化为 0,表示到达第 0 级和第 1 级前的方法数。r 初始化为 1,表示到达第 1 级台阶有 1 种方法。 2.循环迭代:从第 1 级到第 n 级台阶进行迭代: p 更新为前…...
2025御网杯wp(web,misc,crypto)
文章目录 miscxor10图片里的秘密被折叠的显影图纸 Cryptoeasy_rsagift**1. 礼物数学解析****最终答案** 草甸方阵的密语easy-签到题baby_rsa webYWB_Web_xffYWB_Web_未授权访问easywebYWB_Web_命令执行过滤绕过反序列化 misc xor10 ai一把梭 根据题目中的字符串和提示&#…...
【深度学习】将本地工程上传到Colab运行的方法
1、将本地工程(压缩包)上传到一个新的colab窗口:如下图中的 2.zip,如果工程中有数据集,可以删除掉。 2、解压压缩包。 !unzip /content/2.zip -d /content/2 如果解压出了不必要的文件夹可以递归删除: #…...
多模态大语言模型arxiv论文略读(六十九)
Prompt-Aware Adapter: Towards Learning Adaptive Visual Tokens for Multimodal Large Language Models ➡️ 论文标题:Prompt-Aware Adapter: Towards Learning Adaptive Visual Tokens for Multimodal Large Language Models ➡️ 论文作者:Yue Zha…...
Lua再学习
因为实习的项目用到了Lua,所以再来深入学习一下 函数 函数的的多返回值 Lua中的函数可以实现多返回值,实现方法是再return后列出要返回的值的列表,返回值也可以通过变量接收到,变量不够也不会影响接收对应位置的返回值 Lua中传…...
Linux计划任务与进程
at 命令使用方法 at 命令可在指定时间执行任务,适用于一次性任务调度。以下是基本用法: 安装 atd 服务(如未安装) # Debian/Ubuntu sudo apt-get install at# CentOS/RHEL sudo yum install at启动服务 sudo systemctl start atd…...
JavaEE--文件操作和IO
目录 一、认识文件 二、 树型结构组织和目录 三、文件路径 1. 绝对路径 2. 相对路径 四、文件类型 五、文件操作 1. 构造方法 2. 方法 六、文件内容的读写——数据流 1. InputStream概述 2. FileInputStream概述 2.1 构造方法 2.2 示例 3. OutputStream概述 3.…...
k8s的节点是否能直接 curl Service 名称
在 Kubernetes 中,节点(Node)默认情况下不能直接通过 Service 的 DNS 名称(如 my-svc.default.svc.cluster.local)访问 Service。以下是详细分析和解决方案: 1. 默认情况下节点无法解析 Service 的 DNS 名…...
Mask-aware Pixel-Shuffle Down-Sampling (MPD) 下采样
来源 简介:这个代码实现了一个带有掩码感知的像素重排下采样模块,主要用于图像处理任务(如图像修复或分割)。 论文题目:HINT: High-quality INpainting Transformer with Mask-Aware Encoding and Enhanced Attentio…...
本贴会成为记录贴
这几天有些心力交瘁了 一方面带着对互联网下行的伤心,一方面是对未来的担忧 一转眼好像就是20 21那个 可以在宿舍肆意玩手机 大学生活 可是我不小了 是个26岁的人了 时间很快 快的就好像和自己开了一个玩笑 我以为可以找到一个自己足够喜欢的 可爱的人 可是我没有 …...
redis数据结构-04 (HINCRBY、HDEL、HKEYS、HVALS)
哈希操作:HINCRBY、HDEL、HKEYS、HVALS Redis 中的哈希功能极其丰富,让您能够以类似于编程语言中对象的方式存储和检索数据。本课将深入探讨具体的哈希操作,这些操作为操作以下结构中的数据提供了强大的工具: HINCRBY 、 HDEL 、…...
python 写一个工作 简单 番茄钟
1、图 2、需求 番茄钟(Pomodoro Technique)是一种时间管理方法,由弗朗西斯科西里洛(Francesco Cirillo)在 20 世纪 80 年代创立。“Pomodoro”在意大利语中意为“番茄”,这个名字来源于西里洛最初使用的一个…...
复现MAET的环境问题(自用)
我的配置是3090,CUDA Version: 12.4 配置环境时总有冲突,解决好的环境如下 如果你的配置也是CUDA12.4,可以把下面的配置信息保存成 environment.yml 文件 然后执行下面的代码创建环境即可 conda env export > environment.yml name:…...
PDF2zh插件在zotero中安装并使用
1、首先根据PDF2zh说明文档,安装PDF2zh https://github.com/guaguastandup/zotero-pdf2zh/tree/v2.4.0 我没有使用conda,直接使用pip安装pdf2zh (Python版本要求3.10 < version <3.12) pip install pdf2zh1.9.6 flask pypd…...
第二十三节:图像金字塔- 图像金字塔应用 (图像融合)
一、引言:视觉信息的层次化表达 在数字图像处理领域,图像金字塔(Image Pyramid)作为一种多尺度表示方法,自20世纪80年代提出以来,始终在计算机视觉领域扮演着关键角色。这种将图像分解为不同分辨率层次的结构化表示方法,完美地模拟了人类视觉系统对场景的多尺度感知特性…...
一种混沌驱动的后门攻击检测指标
摘要 人工智能(AI)模型在各个领域的进步和应用已经改变了我们与技术互动的方式。然而,必须认识到,虽然人工智能模型带来了显著的进步,但它们也存在固有的挑战,例如容易受到对抗性攻击。目前的工作提出了一…...
LeetCode 高频题实战:如何优雅地序列化和反序列化字符串数组?
文章目录 摘要描述题解答案题解代码分析编码方法解码方法 示例测试及结果时间复杂度空间复杂度总结 摘要 在分布式系统中,数据的序列化与反序列化是常见的需求,尤其是在网络传输、数据存储等场景中。LeetCode 第 271 题“字符串的编码与解码”要求我们设…...
leetcode 15. 三数之和
题目描述 代码: class Solution { public:vector<vector<int>> threeSum(vector<int>& nums) {sort(nums.begin(),nums.end());int len nums.size();int left 0;int right 0;vector<vector<int>> res;for(int i 0;i <len…...
HTML难点小记:一些简单标签的使用逻辑和实用化
HTML难点小记:一些简单标签的使用逻辑和实用化 jarringslee 文章目录 HTML难点小记:一些简单标签的使用逻辑和实用化简单只是你的表象标签不是随便用的<div> 滥用 vs 语义化标签的本质嵌套规则的隐藏逻辑SEO 与可访问性的隐形关联 暗藏玄机的表单…...
Linux : 31个普通信号含义
Linux : 31个普通信号 信号含义特殊的两个信号 信号含义 信号编号信号名信号含义1SIGHUP如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程。2SIGINT当用户按组合键(一般…...
软件测试都有什么???
文章目录 一、白盒测试(结构测试)二、黑盒测试(功能测试)三、灰盒测试四、其他测试类型五、覆盖准则对比六、应用场景 软件测试主要根据测试目标、技术手段和覆盖准则进行分类。分为白盒测试、黑盒测试、灰盒测试及其他补充类型 一…...
LangGraph框架中针对MCP协议的变更-20250510
MCP(Model Context Protocol)的出现为AI Agent与外部工具及数据源的集成提供了标准化接口,而LangGraph作为基于LangChain的智能体开发框架,在MCP协议的影响下也进行了适配性调整,主要体现在工具调用、异步交互和多步推…...
YashanDB(崖山数据库)V23.4 LTS 正式发布
2024年回顾 2024年11月我们受邀去深圳参与了2024国产数据库创新生态大会。在大会上崖山官方发布了23.3。这个也是和Oracle一样采用的事编年体命名。 那次大会官方希望我们这些在一直从事在一线的KOL帮助产品提一些改进建议。对于这样的想法,我们都是非常乐于合作…...
二、transformers基础组件之Tokenizer
在使用神经网络处理自然语言处理任务时,我们首先需要对数据进行预处理,将数据从字符串转换为神经网络可以接受的格式,一般会分为如下几步: - Step1 分词:使用分词器对文本数据进行分词(字、字词);- Step2 构建词典:根据数据集分词的结果,构建…...
git 报错:错误:RPC 失败。curl 28 Failed to connect to github.com port 443 after 75000
错误:RPC 失败。curl 28 Failed to connect to github.com port 443 after 75000 ms: Couldnt connect to server致命错误:在引用列表之后应该有一个 flush 包 方法一: 直接换一个域名:把 git clone https://github.com/zx59530…...
软考 系统架构设计师系列知识点之杂项集萃(56)
接前一篇文章:软考 系统架构设计师系列知识点之杂项集萃(55) 第91题 商业智能关注如何从业务数据中提取有用的信息,然后采用这些信息指导企业的业务开展。商业智能系统主要包括数据预处理、建立()、数据分…...
数据库的脱敏策略
数据库的脱敏策略:就是屏蔽敏感的数据 脱敏策略三要求: (1)表对象 (2)生效条件(脱敏列、脱敏函数) (3)二元组 常见的脱敏策略规则: 替换、重排、…...
Lora原理及实现浅析
Lora 什么是Lora Lora的原始论文为《LoRA: Low-Rank Adaptation of Large Language Models》,翻译为中文为“大语言模型的低秩自适应”。最初是为了解决大型语言模在进行任务特定微调时消耗大量资源的问题;随后也用在了Diffusion等领域,用于…...
力扣热题100之合并两个有序链表
题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 代码 方法一:新建一个链表 这里就先随便新建一个节点作为一个链表的头节点,然后每次遍历都将小的那个节点放入这个链表,遍历完一…...
Linux511SSH连接 禁止root登录 服务任务解决方案 scp Vmware三种模式回顾
创造一个临时文件 引用 scp -p 3712 atthistime.txt code11.1.1.100:/at ssh connect to host 11.1.1.100 port 22:No route to host lost connection 对方虚拟机是[rootlocalhost caozx26]# ll -d /at drwxr-xr-x. 2 root root 6 5月 11 11:10 /at sshd_config文件修改了port为…...
python实现用户登录
使用python实现用户登录,输入用户名和密码,进行验证,正确登录成功,错误登录失败,允许用户输入三次。 代码: 下面展示一些 内联代码片。 for i in range(3):username input(请输入用户名:)pas…...
信息系统项目管理师-软考高级(软考高项)2025最新(十五)
个人笔记整理---仅供参考 第十五章项目风险管理 15.1管理基础 15.2项目风险管理过程 15.3规划风险管理 15.4识别风险 15.5实施定性风险分析 15.6实施定量风险分析 15.7规划风险应对 15.8实施风险应对 15.9监督风险...
力扣-二叉树-101 对称二叉树
思路 分解问题为,该节点的左孩子的左子树和右孩子的右子树是不是同一棵树 && 该节点的左孩子的右字数和右孩子的左子树是不是同一课树 && 该节点的左右孩子的值相不相同 代码 class Solution {public boolean isSymmetric(TreeNode root) {// 层…...
07.three官方示例+编辑器+AI快速学习webgl_buffergeometry_attributes_integer
本实例主要讲解内容 这个Three.js示例展示了WebGL 2环境下的整数属性渲染技术。通过创建大量随机分布的三角形,并为每个三角形分配不同的整数索引,实现了基于索引动态选择纹理的效果。 核心技术包括: WebGL 2环境下的整数属性支持顶点着色…...
Python Day 22 学习
学习讲义Day14安排的内容:SHAP图的绘制 SHAP模型的基本概念 参考学习的帖子:SHAP 可视化解释机器学习模型简介_shap图-CSDN博客 以下为学习该篇帖子的理解记录: Q. 什么是SHAP模型?它与机器学习模型的区别在哪儿? …...
OrangePi Zero 3学习笔记(Android篇)6 - hid-ft260
目录 1. 将hid-ft260.c拷贝到Android目录内 2. 修改hid-ids.h 3. 修改hid-quirks.c 4. 修改Kconfig 5. 修改Makefile 6. 配置内核 7. 编译内核 8. 增加权限 9. 验证 在Android中添加驱动模块ko文件,以hid-ft260为例。 1. 将hid-ft260.c拷贝到Android目录内…...
部署Superset BI(五)连接oracle数据库失败
折腾完了hana和sqlserver数据库的连接,开始折腾oracle数据库连接 1.requirements-local.txt配置 尝试在requirements-local.txt中设置,结果容器弄瘫痪了,拉不起来了,只要又去掉修改 rootNocobase:/usr/superset/superset/docker# …...
快速搭建一个vue前端工程
一、环境准备 1、安装node.js 下载地址:Node.js 推荐版本如下: 2、检查node.js版本 node -v npm -v 二、安装Vue脚手架 Vue脚手架是Vue官方提供的标准化开发工具。vue官网:https://cn.vuejs.org/ 全局安装vue/cli (仅第一次…...
蓝桥杯14届 数三角
问题描述 小明在二维坐标系中放置了 n 个点,他想在其中选出一个包含三个点的子集,这三个点能组成三角形。然而这样的方案太多了,他决定只选择那些可以组成等腰三角形的方案。请帮他计算出一共有多少种选法可以组成等腰三角形? 输…...
在Python中计算函数耗时并超时自动退出
更多内容请见: python3案例和总结-专栏介绍和目录 文章目录 方法1:使用装饰器结合信号模块(仅Unix-like系统)方法2:使用多线程(跨平台解决方案)方法3:使用concurrent.futures(Python 3.2+)方法4:使用 multiprocessing + Process(跨平台)方法5:使用 time 手动计…...
jna总结1
java使用JNA调用dll的方法_(jnacalldllapi) native.loadlibrary(path-CSDN博客 JNA(Java Native Access):建立在JNI之上的Java开源框架,SUN主导开发,用来调用C、C代码,尤其是底层库文件(windows中叫dll文件,…...
[Java][Leetcode simple]26. 删除有序数组中的重复项
思路 第一个元素不动从第二个元素开始:只要跟上一个元素不一样就放入数组中 public int removeDuplicates(int[] nums) {int cnt1;for(int i 1; i < nums.length; i) {if(nums[i] ! nums[i-1]) {nums[cnt] nums[i];}}return cnt;}...
BUUCTF——Ezpop
BUUCTF——Ezpop 进入靶场 给了php代码 <?php //flag is in flag.php //WTF IS THIS? //Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95 //And Crack It! class Modifier {protected $v…...