继承的介绍

继承的概念及定义定义格式继承基类成员访问方式的变化

基类和派生类对象赋值转换继承中的作用域派生类的默认成员函数继承与友元继承与静态成员⭐复杂的菱形继承及菱形虚拟继承总结

继承的概念及定义

继承也是面向对象的三大特性之一,是为了代码能够复用的重要手段,它使得我们在原有的类特性的基础上进行扩展,产生新的功能,这样的类我们成为派生类,而原有的类则叫做基类。继承就和我们以前的函数复用一样,只是这次复用的是属于设计层次上的。

定义格式

例如:

class Person {

public:

string _name = "mingzi";

};

class Student :public Person{

public:

void print() {

cout << "name" << _name << endl;

cout << "stid" << _stid << endl;

}

private:

int _stid = 202238;//学号

};

int main() {

Student stdt;

stdt.print();

}

此时Student和Person就成为了父子类关系,Person被称为基类,也叫做父类,Students被称为派生类,也叫子类,而public则叫做继承方法。上面的代码我们可以发现子类复用了父类的成员,并且我们在子类里可以使用父类的成员变量,这就是因为用的是public的继承方式 继承方式和访问限定符都是一样的,分别为public,protected,private三种

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

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

此时我们可以发现,不管是什么类型的成员还是继承方式,最后子类获得的都是范围最小的那一种即public>protected>private,protect叫做保护成员限定符,是因继承才出现的。简单来讲,private和不可见的区别是,派生类不可见是子类无法使用父类的private成员,而private是属于类内可以使用,但类外无法使用。而protected,父类的protected成员子类可以使用,但类外无法使用。 不过我们实际运用中一般都是public

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

//同上一份类

int main(){

Person p;

Student s;

//public继承

p=s;//父类=子类

s=p;//不可以

Person* ptr=&s;//指针

Person& ref=s;//引用

return 0;

}

我们把子类对象赋值给父类对象/指针/引用的行为叫做切割,天然行为,不存在类型转换(没有const临时变量)。形象的来讲因为子类中有全部的父类成员,把多余(属于自己子类)的部分给切除,就可以把子类内的成员依次给父类赋值了

继承中的作用域

基类和派生类都有独立的作用域,当子类和父类中有同名成员时,子类成员会隐藏父类成员,这种情况叫隐藏,也叫重定义

class Person {

public:

string _name = "mingzi";

int _stid=111;

};

class Student :public Person{

public:

void print() {

cout << "name" << _name << endl;

cout << "stid" << _stid << endl;

//若想打印父类的可以使用 Person::_stid

}

private:

int _stid = 202238;//学号

};

int main() {

Student stdt;

stdt.print();

}

此时将会打印202238,因为子类的成员函数会优先调用自己的成员变量,将父类的隐藏(注:尽量不要重名,但是在虚函数中又不一样了,后面多态会讲)

class A{

public:

void fun(){

cout << "func" <

}

};

class B : public A{

public:

void fun(int i){}

};

int main(){

B b;

b.fun(10);

//b.fun();

b.A::fun();

return 0;

}

此时A类和B类的两个fun函数构成隐藏关系(只要函数名相同,不管参数怎么样,就是隐藏关系),继承中函数名相同就是隐藏值得注意的是,重载的条件是在同一个作用域中。

派生类的默认成员函数

子类的构造函数——我们不写,编译器默认生成,此时 1.继承的父类成员作为一个整体——调用父类的默认构造函数初始化 2.自己的自定义类型成员 ——调用它的默认构造函数 3.自己的内置类型成员 ——不处理(除非声明时给了缺省值)

子类的拷贝构造函数也是同理——我们不写,编译器默认生成,此时 1、继承的父类成员作为一个整体 ——调用父类的拷贝构造 2、自己的自定义类型成员 —— 调用它的拷贝构造 3、自己的内置类型成员 —— 值拷贝

子类的拷贝赋值函数也是同理——我们不写,编译器默认生成

子类析构函数 – 我们不写,编译器默认生成 ——此时 1、继承的父类成员作为一个整体 – 调用父类的析构函数 2、自己的自定义类型成员 – 调用它的析构函数 3、自己的内置类型成员 – 不处理 子类析构函数和父类析构函数构成隐藏关系 因为编译器会对析构函数名做特殊处理,所有类的析构函数名都会被处理成统一名字destructor()。编译器为什么要这么做呢,多态会讲到 子类的析构函数在执行结束会后,会自动调用父类的析构函数

class Person

{

public:

Person(const char* name = "peter")

//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; // 姓名

//int _age;

};

class Student : public Person

{

public:

// 我们要自己实现子类构造函数

// 要注意的父类成是作为一个整体,调用父类的构造函数进行初始化

Student(const char* name)

:Person(name)//不可以单独给_name进行赋值

, _id(id)

, _address(address)

{}

//当我们子写了拷贝构造函数

//一般情况没必要写子类的拷贝构造,除非子类里的成员变量有深浅拷贝问题,才会需要

Student(const Student& s)

:_id(s._id)

, _address(s._address)//⭐值得注意的是,这里address用的是自定义类型的拷贝

//构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝

, Person(s)//这里我们可以直接传子类对象,给父类引用,这里发生切片

{}

//⭐若子类的拷贝构造没去调用父类的拷贝构造(即没有Person(s)),拷贝构造也是构造函

//数,构造函数规定,如果你不调用自定义类型,那会去调用它的默认构造,(和编译器生成的不同)

//当我们子写了拷贝赋值函数

Student& operator=(const Student& s){

if (this != &s){

_id = s._id;

_address = s._address;

Person::operator=(s); // 我们显示调用=的函数,⭐此时父子类都有=的重载,

//构成隐藏关系,否则自己调自己了,所以我们需要指明类域,切片

}

return *this;

}

~Student(){

//Person::~Person();Student和父类析构构成隐藏

// 清理自己的资源

} // 会自动调用父类的析构函数

private:

int _id;

string _address;

};

int main(){

Student s1("张三", 1, "西安市");

Student s2(s1);

Student s3("张思", 2, "北京市");

s1 = s3; //此时会打印Person()

// Person(const Person& p)

// Person()

// Person operator=(const Person& p)

// ~Person()

// ~Person()

// ~Person() 在有子类析构情况下,顺序父构造子构造子析构父析构

}

当父类的构造函数是无参或者给了全缺省的默认构造函数时,子类的构造函数也可以是默认构造函数(也可以不是),来调用父类的默认构造函数。但是当父类不是默认构造函数时(子类也不能使用默认构造函数),子类的构造函数必须对父类一个整体,进行初始化(如:Person(name),就像缺省值一样) ⭐⭐声明的顺序,才是初始化顺序,所以子类的初始化列表里(列表里出现的顺序不重要,重要的是声明的顺序),无论父类Person在哪个位置,都是先初始化父类的 PS:这里比较啰嗦,代码里的注释没看懂的话可以看这。

拷贝构造:⭐值得注意的是,这里address用的是自定义类型的拷贝构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝

继承与友元

Display函数声明为友元,所以Display函数可以使用类里的成员变量,但对子类来说,友元不能继承,即基类友元不能访问子类私有和保护成员

class Student;

class Person{

public:

friend void Display(const Person& p, const Student& s);

protected:

string _name; // 姓名

};

class Student : public Person{

friend void Display(const Person& p, const Student& s);

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);

}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,子类不会有这样的static的成员,并且我们可以用类名去访问静态成员(由该类所有对象共享),类名::变量名

⭐复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称整个继承关系为单继承 多继承:一个子类有两个或者以上父类时整个继承关系称为多继承 菱形继承:时多继承的一种特殊情况

class A{

public:

int _a;

};

class B : public A{

public:

int _b;

};

class C : public A{

public:

int _c;

};

class D : public B, public C{

public:

int _d;//类中产生两个A类的a成员变量

};

int main(){

D x;

//x._a=0;//此时会报错,因为这里产生了二义性

x.A::_a=0

x.B::_a=0;//这样可以暂时解决问题,需要显示指定访问

那这种情况下我们应该如何去解决这样的问题呢? 首先我们知道了D类中用拥有两个_a的成员变量,那么此时我们可以通过调试,使用内存可以看到他们的数据,首先我们先取d的地址 我们可以看到d内存和监视的地址是相同的 我们可以发现在B类和C类空间中都各有一份_a成员 此时,我们可以通过virtual,虚继承去解决二义性和数据冗余

//...略

class B : virtual public A

class C : virtual public A

class D : virtual public B, public C

//..略

int main(){

D d;

d.B::_a = 1;

d.C::_a = 2;

d._b = 3;

d._c = 4;

d._d = 5;

d._a = 0;

}

此时我们在到内存中查看

这时候我们可以发现BC空间中没有了_a的数据,在D类的空间中出现了2,但BC中产生了两个地址

我们进入到两个地址时,发现了14和0c两个数据 那么这是什么呢? 实际上这里两个值叫做偏移量,0x14=20,0x0c=12,这时候我们可以进行计算,D空间地址-B空间地址=20,D空间地址-C空间地址=12,所以我们可以通过存储偏移量,这样我们就可以只存1份_a了。 ⭐02是A类成员变量的存储的空间,05是D类成员变量存储的空间 上面两个表叫做虚基表,我们通过B和C的两个指针指向一张表,指针叫做虚基表指针 但实际上因为我们需要格外增加指针寻找变量,所以效率降低了,更复杂

B b=d;

B* p=&d;

B& r=&d;//这样也可以获取到_a

总结

在多继承中我们可以感受到C++的复杂性,所以我们一般不建议设计出多继承,一定不要有菱形继承 我们应该优先使用组合,而不是类继承,继承中基类的内部细节对子类可见,继承一定成都破坏了基类的封装,使得基类和派生类耦合度很高 所以我们尽量使用组合,降低耦合度,组合被叫做黑箱复用,内部不可见。而继承叫白箱复用 最后感谢各位看到这里噢!!喜欢的可以点个赞噢!

想要了解多态的同学可以点击这里噢,简单易懂:【多态】多态的详细介绍,简单易懂

查看原文