发布时间:2024-07-07 10:01
继承是C++语言和其他面向对象语言的三大特性之一
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用
class Person
{
public:
void Print()
{
cout << \"name:\" << _name << endl;
cout << \"age:\" << _age << endl;
}
protected:
string _name = \"fl\"; // 姓名
int _age = 21;
};
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher :public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
}
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用。
继承的定义格式:
我们把Person类叫做基类(或者父类)
,把Student叫做派生类(或者子类)
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成 员 | 派生类的private成 员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
关于private继承:
class Person
{
public:
void Print()
{
cout << \"name:\" << _name << endl;
cout << \"age:\" << _age << endl;
}
private:
string _name = \"fl\"; // 姓名
int _age = 21;
};
class Student :public Person
{
protected:
void Print()
{
cout << _age << _name << endl;//错误
}
int _stuid;//学号
};
int main()
{
Student s;
s._age = 10;//错误
s._name = \"fll\";//错误
}
假设我们把上述代码中基类成员的访问限定修饰符改为private,然后在子类内或者在子类外去访问这些private成员,再编译代码就会出下一下结果
出现该现象的原因是基类成员的访问限定修饰符为private,无论子类是哪种方式继承,无论在子类内还是子类外,都无法访问。如果我们不需要访问基类中private成员,通过调试看监视窗口
通过监视发现基类中的private成员依旧被继承下来了,但是不能访问。只要基类的成员是private,无论哪种方式继承,子类都不能访问
关于private和protected:private和protected在父类中没有区别,在子类中,private成员不可见,protected成员可见
class Person
{
public:
void Print()
{
cout << \"name:\" << _name << endl;
cout << \"age:\" << _age << endl;
}
protected:
string _name = \"fl\"; // 姓名
int _age = 21;
};
class Student :public Person
{
protected:
int _stuid;//学号
};
int main()
{
Person p;//创建Person对象
Student s;//创建Student对象
p = s;//将派生类赋值给基类
Person* ptr1 = &s;//将派生类的地址赋给基类指针
Person& pp = s;//将派生类引用赋给基类引用
}
注意:将派生类引用赋给基类引用不是类型转换,是语法天然支持的行为
为什么这么说呢?
int main()
{
int i = 1;
double d = 2.2;
i = d;
int& ri = d;
return 0;
}
这里会报错,因为类型转换,中间会产生临时变量,而临时变量具有常属性,需要用const修饰。将 int& ri = d 改为 const int& ri = d 就可以了。因为将派生类引用赋给基类引用时,不需要加const修饰,,所以说不是类型转换,而是天然的语法支持行为
注意:只有public才能切割,而private和protected不能被切割
int main()
{
Person p;
Student s;
Student* ptr2 = (Student*)&p;
}
如果不加强制类型转换,则会报错
这种情况转换时虽然可以,但是会存在越界访问的问题
了解继承中的作用域之前,我们先看下列代码
int a = 10;
int main()
{
int a = 0;
cout << a << endl;//打印0
cout << ::a << endl;//打印10
return 0;
}
这里在全局中定义了一个a变量,在main函数里面又定义一个a,为什么能定义两次,因为作用域不同。第一个打印为0,因为在main函数局部范围内,会现在局部范围内找,所以为0,但是如果局部范围没有a,就会去全局范围找,如果全局范围找不到,就会报错。第二个打印为10,因为在打印a时,指定了作用域为全局范围,所以为10。总的来说找变量或者函数遵循就近原则——>先局部,再全局,如果都没有,就报错。
class Person
{
protected:
string _name = \"老佛爷\"; // 姓名
int _age = 21;
};
class Student :public Person
{
public:
void Print()
{
cout << \"name:\" << Person::_name << endl;
}
protected:
string _name = \"小李子\";
int _stuid;//学号
};
int main()
{
Student s;
s.Print();
}
毫无疑问,这里打印的结果是\"小李子\",是子类中的_name。但是如果我们想要打印父类中的\"老佛爷\"怎么办?此时就需要在_name前面指定 作用域:cout << “name:” << Person::_name << endl;
class Person
{
public:
void Print()
{
cout << \"name:\" << _name << endl;
}
protected:
string _name = \"老佛爷\"; // 姓名
int _age = 21;
};
class Student :public Person
{
public:
void Print(int i)
{
cout << \"name:\" << _name << endl;
}
protected:
string _name = \"小李子\";
int _stuid;//学号
};
Person中的Print()和Student中的Print()构成隐藏
父类的成员函数和子类的成员函数只要名字相同就构成隐藏,函数参数对其不构成影响(可以相同,也可以不同),
注意:这里不是重载,因为重载的一个必要条件是要在相同的作用域
int main()
{
Student s;
s.Print(1);//可以调用子类的Print
s.Print();//不可以调用父类的Print
}
通过上述方法调用父类的Print是不可以的,因为父类的Print被隐藏了,如果需要调用就必须指明作用域:s.Person::Print();
我们把上述代码稍作修改:
class Person
{
public:
void Print(int i = 1)
{
cout << \"name:\" << _name << endl;
}
protected:
string _name = \"老佛爷\"; // 姓名
int _age = 21;
};
class Student :public Person
{
public:
void Print(int i = 1)
{
cout << \"name:\" << _name << endl;
}
protected:
string _name = \"小李子\";
int _stuid;//学号
};
int main()
{
Person p;
p.Print(1);//可以调用子类的Print
}
运行代码:
可能会有人会疑惑:这里的Print()不是构成隐藏吗?又没有指明作用域,为什么还能调到父类的Print()?
其实原因很简单:子类和父类的Print()确实构成隐藏,但是隐藏只是对子类才有的,对父类没有,这里p的类型是父类,父类对象调用成员函数是没有隐藏这个概念的。
派生类中重点的4个默认成员函数,我们不写,编译器默认生成的会干些什么事情呢?
class Person
{
public:
Person(const char* name = \"fl\")
:_name(name)
{
cout << \"Person\" << endl;
}
~Person()
{
cout << \"~Person\" << endl;
}
protected:
string _name = \"老佛爷\"; // 姓名
int _age = 21;
};
class Student :public Person
{
protected:
int _stuid;//学号
};
int main()
{
Student stu;
}
由上述可以知道子类会自动调用
父类的构造函数去初始化父类继承下来的成员
,自动调用
父类的析构函数去释放``父类继承下来的成员所占的资源`
但对于子类自身的成员呢?
a.成员是内置类型
b.成员是自定义类型
通过调试我们发现:对于子类成员,如果是内置类型,则不做处理,如果是自定义类型,则会调用自定义类型的构造函数。跟普通类一样
不仅构造和析构是这样,拷贝构造和operator=也是一样的
总结:继承下来调用父类去处理,自己的按照普通类基本规则去处理
class Person
{
public:
Person(const char* name)//这里不给默认构造函数
:_name(name)
{
cout << \"Person\" << endl;
}
~Person()
{
cout << \"~Person\" << endl;
}
protected:
string _name = \"老佛爷\"; // 姓名
int _age = 21;
};
class Student :public Person
{
protected:
int _stuid;//学号
string _s;
};
int main()
{
Student stu;
}
编译这段代码,结果报错
关于这里没有合适的默认构造函数可用,是子类(Student)中没有默认构造函数可用吗?不是,是父类(Person)没有默认构造函数,因为在子类中没写默认构造函数,编译器会自动生成一个,然后去调用父类的默认构造函数,父类没有,所以就报错。对于其它的也类似
所以:1.父类没有默认构造,需要我们自己显示写
2.如果子类有资源需要释放,就需要自己显示写析构
3.如果子类存在浅拷贝问题,就需要我们自己实现拷贝构造和赋值从而达到深拷贝
class Person
{
public:
Person(const char* name)
:_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 = \"fl\", int stuid = 2022 )
:Person(name)/*这里不能写为 _name(name) 只能调用父类的构造函数*/
, _stuid(stuid)
{}
//stu2(stu1)
Student(const Student& stu1)
:Person(stu1)//会进行切片处理,将stu1中父类的那部分赋值给stu2中父类的那部分
, _stuid(stu1._stuid)
{}
//stu2 = stu1;
Student& operator=(const Student& stu1)
{
if (this != &stu1)
{
Person::operator=(stu1);//这里一定要指定作用域,否则类似自己调用自己,会导致Stack Overflow
_stuid = stu1._stuid;
}
return *this;
}
~Student()
{
Person::~Person();
}
protected:
int _stuid;//学号
};
int main()
{
Student stu1;
Student stu2(stu1);
Student stu3(\"ffff\", 18);
stu3 = stu2;
}
注意:
1.不能在子类构造函数的初始化列表中直接给父类继承的成员赋值,需要去显示调用父类的构造函数
2.析构函数名字会被统一处理成destructor(),那么子类的析构函数跟父类的析构函数就会构成隐藏,所以在调用时需要指明作用域
3.我们明明只有三个Student的对象,却调用了六次析构函数,相当于一个对象调用了两次析构函数。原因是子类的析构函数会自动调用父类的析构函数,然后又显示的调用了一次析构函数,所以加起来是两次
对于这里的第3点:
class Person
{
public:
~Person()
{
delete[] _ptr;
}
protected:
string _name = \"fff\";
int* _ptr = new int[10];
};
class Student:public Person
{
public:
~Student()
{
Person::~Person();
}
};
int main()
{
Student st;
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) {
cout << p._name << endl;
cout << s._stuNum << endl;//这里会报错
}
void main()
{
Person p;
Student s;
Display(p, s);
}
Display函数中的第二个cout会报错,因为基类的友元访问了子类的protected
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
class Person
{
public:
Person(){ count++; }
static int count;
};
int Person::count = 0;
class Student:public Person
{};
int main()
{
Person p;
Student stu;
cout << \"Person:\" << p.count << endl;//打印2
cout <<\"Student:\"<< stu.count << endl;//打印2
}
Student继承了Person,count也被继承了,但是因为count是静态成员变量,所以只会有一个count,因此Person创建对象p,count++,Student创建对象sty,count++,所以结果都为2
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
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\";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = \"xxx\";
a.Teacher::_name = \"yyy\";
}
Student和Teacher继承了Person,各有一份_name,Assistant 又继承了Student和Person,就有两份_name,如果在Assistant中访问_name,就会发生访问出现二义性的问题。这就相当于一位老师在教室说:请你起来回答问题。这个“你”到底是班上的哪一位同学,我们不得而知,必须指明姓名才行。对于菱形继承的二义性,我们只需要指定作用域即可。
class test1
{
public:
int a[10000];
};
class test2:public test1
{};
class test3 :public test1
{};
class test4 :public test2,public test3
{};
int main()
{
test4 t;
cout << sizeof(test4) << endl;
return 0;
}
本来test4只想继承一份a[10000],但是由于菱形继承,结构导致了test4继承了两份,如果数据量小,所占空间小,还可以忽略不计,但是这里40000字节(4*10000),极大了造成了数据冗余和空间上的浪费 。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份
class Person
{
public:
string _name; // 姓名
int arr[10000];
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant A;
A._name = \"fffl\";
cout << A._name << endl;
cout << sizeof(A) << endl;
return 0;
}
为了解决数据冗余和二义性,C++采用虚拟继承来解决它们。使用关键字virtual
就可变为虚拟继承,只需要在菱形继承的中间层,也就是Student和Teacher,在它们继承方式前面加上virtual即可。
加上virtual之后,访问_name时就不需要指定作用域,Assistant创建的对象A所占空间的大小也减少了一半
注意:虚拟继承不要在其他地方去使用
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型
代码如下(示例):
先看菱形继承
class A {
public:
int _a;
};
class B : public A {
public:
int _b;
};
class C : public A {
public:
int _c;
};
//class D : public C, public B
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;
}
通过调用内存窗口发现:在D创建的对象d中,确实有两份_a,因为B和C都继承了A。此外还有就是在子类创建的对象中,先在内存的低地址为其先继承的类开辟空间,再在高地址为后继承的类开辟空间,最后再为自己开辟空间。先继承的在前面,后继承的在后面
再看虚拟继承
class A {
public:
int _a;
};
class B : virtual public A {
public:
int _b;
};
class C : virtual public A {
public:
int _c;
};
class D : public B, public C{
public:
int _d;
};
int main()
{
D d;
d._a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
通过内存窗口我们发现_a放在了最后面,对于_b(30 00 00 00)和_c(04 00 00 00 )之前多了我们不知道的一串数字,那它们到底是什么呢?难道说是是一个地址?
我们再次通过内存去观察它们:
通过验证我们确定这两个数字就是两个地址,通过这两个地址,我们找到了其中的内容,一个里面存放了14,另一个里面存放了0c,它们代表什么意思呢?
再次通过观察我们发现地址0x006FF7C0到0x006FF7D8相差了20个字节,转为16进制则为14。地址0x006FF7CC到0x006FF7D8相差了12个字节,转为16进制则为C。原来这里的14和C都是相对于_a的位置。现在我们就大致了解了这其中的细节。
这里我们将0x006FF7C0和0x006FF7CC成为虚基表指针,它们指向两张虚基表。
A一般叫虚基类。这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A
此时又有人会疑问,直接访问_a不好吗,为什么还要通过偏移量去找呢?
请看以下场景:
D d;
B b = d;
C c = d;
当我们需要将基类对象赋值给派生类对象的时候,此时就会发生切片处理。假设_a放在最前面,或者放在中间,又或者放在最后,怎么才能准确的找到_a呢?这就需要通过偏移量了