文章目录
前言
哈喽小伙伴们,你们在写关于不同类但是这些类中都有着相同的变量。一个二个的定义的话还可以接受,但是如果太多的话那是不是特别的麻烦啊?所以今天呢小编就给大家 分享一个方法就是用 继承 来解决这个问题。那继承又是什么呢?继承又有哪些优点呢?那就跟着小编一起来看看什么时继承吧。
一、继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
运用场景:
如果我们要定义两个类,但是这两个类里面的成员函数或者成员变量有一部分相同,那我们不使用继承那是不是就要定义两个类,类里面的成员函数以及成员变量是不是都要 重复定义,如果当数量很多的时候,那这是不是很繁琐呢?那我们可不可以把多个类里面相同的成员函数以及成员变量把他们 提取出来重新定义在一个新的类中,要用的时候直接复用它呢?这就是继承的思想。
举例:
我们以学生和老师为例吧。老师和学生是不是有一部分的信息变量是一样的呀?比如年龄,身份证号,姓名等,那是不是还有一部分不一样的呀?比如角色,职业等
如果按照之前的来定义那是不是就是这样啊?
class teacher//教师
{
public:
void identity()
{
//身份认证
}
void teaching()
{
//教师授课
}
private:
string name;//姓名
string number;//电话号码
int age;//年纪
string title;//职业
};
class student//学生
{
public:
void identity()
{
//身份认证
}
void study()
{
//学习
}
private:
string name;//姓名
string number;//电话号码
int age;//年纪
string ID;//学号
};
那我们使用继承的思想来定义这两个类,首先把两个类中相同的成员拿出来重新定义一个新的类,叫做Person,然后student和teacher都继承Person,这样就可以复用这些成员,就不需要重复定义了。省去去了一些麻烦 。
class Person
{
public:
void identity()
{
//身份认证
}
protected:
string name;//姓名
string number;//电话号码
int age;//年纪
};
class teacher:public Person
{
public:
void teaching()
{
//教师授课
}
private:
string title;//职业
};
class student :public Person
{
public:
void study()
{
//学习
}
private:
string ID;//学号
};
int main()
{
teacher v;
student x;
v.identity();
x.identity();
return 0;
}
二、继承的定义
1.继承的格式:
因为翻译的原因,所以既叫:基类/派⽣类,也叫⽗类/⼦类
- Person是基类,也称作⽗类。
- Student是派⽣类,也称作⼦类。
2. 继承方式以及继承基类成员访问⽅式的变化
继承方式:
- 继承方式呢跟我们在类和对象哪里学的访问限定符是一样都是公开(public),私有(private)和保护(protected)
继承基类成员访问⽅式的变化
1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
class Person
{
public:
void identity()
{
//身份认证
}
string name="子墨";//姓名
string number="1234567895";//电话号码
private:
int age=18;//年纪
};
class student :public Person
{
public:
void study()
{
//学习
cout << name << endl;//name在子类中是可以访问的,因为他是public成员
//cout << age << endl;//aeg子类中是不可访问的,但是依然继承到子类对象中
}
private:
string ID;//学号
};
int main()
{
student s;
s.study();
return 0;
}
虽然age不能访问,但是它依然还是被继承到子类的对象之中,这一点我们可以通过监视窗口可以看到:
2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
class Person
{
public:
void identity()
{
//身份认证
cout << age << endl;
}
string name="子墨";//姓名
string number="1234567895";//电话号码
protected:
int age=18;//年纪
};
class student :public Person
{
public:
void study()
{
//学习
cout << name << endl;
cout << age << endl; //在子类中是可以访问的
}
//private:
static string ID;//学号
};
int main()
{
student s;
//s.age;//这里不可访问,因为protected只能在子类中访问,不能在类外访问
return 0;
}
- 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式是 基类的其他成员在基类中的访问限定符和继承方式两者取权限最小的。一般默认为public > protected >private
基类(父类) | 共有(public) | 保护(protected) | 私有(private) |
---|---|---|---|
共有继承(public) | public | protected | 不可见,不能访问 |
保护继承(protected) | protected | protected | 不可见,不能访问 |
私有继承(private) | private | 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 ; // 学号
};
但是在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
- 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
关键字是class
#include<iostream>
using namespace std;
class Person
{
public:
void identity()
{
//身份认证
cout << age << endl;
}
protected:
string name="子墨";//姓名
string number="1234567895";//电话号码
int age=18;//年纪
};
//class student : private Person
class student : Person//class默认是私有。
{
public:
void study()
{
//学习
}
private:
static string ID;//学号
};
int main()
{
student s;
//s.identity();identity是基类中的成员函数,由于是私有继承,所以是不可调用的
return 0;
}
关键字是struct
#include<iostream>
using namespace std;
class Person
{
public:
void identity()
{
//身份认证
cout << age << endl;
}
protected:
string name="子墨";//姓名
string number="1234567895";//电话号码
int age=18;//年纪
};
//struct student : public Person
struct student : Person//struct默认是共有继承,
{
public:
void study()
{
//学习
}
private:
static string ID;//学号
};
int main()
{
student s;
s.identity();//identity是基类中的成员函数,由于默认是共有继承,所以是可调用的
return 0;
}
三、继承类模板
模板的继承特别注意按需实例化!
我们了解继承的用法之后可以来实践一下,用库里面的vector继承到stack中来完成stack的相关操作。
#include<iostream>
#include<vector>
#include<stdbool.h>
using namespace std;
template<class T>
class stack :public vector<T>//继承
{
public:
void push(const T& x)
{
push_back(x);
}
const T& top()const
{
return back();
}
void pop()
{
pop_back();
}
bool empty()
{
return empty();
}
};
int main()
{
stack <int > s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
return 0;
}
如果按我们之前的逻辑,子类继承父类所有的对象,那这里直接调用也没有问题呀!所以代码是没有错的,可以运行的。但是这里要注意的是。这里虽然代码在逻辑上是没有问题的。但是实际他是运行不出来的。因为这里是模板的继承。这里就要注意按需实例化了。
这里之所以出错的原因其实就是当stack在实例化时,也实例化vector,vector实例化只调用了vector的构造函数,但是没有实例化vector里面的成员函数,当我们在调用函数时,由于没有实例化,所以找不到。
当然我们还可以借助一下宏定义来让代码操作更方便。
#include<iostream>
#include<vector>
#include<stdbool.h>
#define CONTAINER std::vector
using namespace std;
template<class T>
class stack :public CONTAINER<T>//继承
{
public:
void push(const T& x)
{
CONTAINER<T>::push_back(x);
}
const T& top()const
{
return CONTAINER<T>::back();
}
void pop()
{
CONTAINER<T>::pop_back();
}
bool empty()
{
return CONTAINER<T>::empty();
}
};
int main()
{
stack <int > s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
return 0;
}
四、基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。派生类能赋值给基类的对象 / 基类的指针 / 基类的引用。那是因为派生类中有基类的成员,可以切片。但是如果是基类给派生类赋值的话。派生类中的成员在基类中是不一定会有的,所以不能被切片。所以基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
#include<iostream>
#include<vector>
#include<stdbool.h>
using namespace std;
class Person
{
public:
protected:
string name = "子墨";//姓名
string number = "1234567895";//电话号码
int age = 18;//年纪
};
struct student :public Person
{
public:
static string ID;//学号
};
int main()
{
student s;
Person x=s;//这里的赋值是把子类中父类的部分赋值过去,而不是某个对象
Person* p = &s;//这里的指针也是指向子类中父类的部分,而不是某个对象
Person& v = s; //这里的引用就是引用子类中父类的部分,而不是某个对象
//这里要注意一下就是在赋值的过程中是没有产生临时变量,如果产生临时变量的话,那这里的引用时编不过的,因为临时变量具有常性,要引用要用const修饰。如果不加const,那权限就被放大了,然而权限只能缩小不能被放大。
s=x;//基类对象不能赋值给派生类对象
p = &s; //基类的指针可以通过强制类型转换赋值给派生类的指针,
student * p1 = (student*)p;//这种情况转换时可以的。
return 0;
}
五、继承中的作用域
- 在继承体系中 基类 和 派生类 都有 独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类同名成员然后直接访问当前类的同名成员,这种情况叫 隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
注意:如果是成员函数的隐藏,只需要函数名 相同就构成隐藏,但是一般不推荐定义相同的成员。
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
void Function(int i)//与子类中成员函数构成隐藏,注意这里不是重载哟,重载是在同一作用域。而子类和父类是 独立的作用域
{
cout << "Function(int i)" << endl;
}
string name = "子墨";//姓名
string number = "1234567895";//电话号码
int age = 18;//年纪
};
class student : public Person
{
public:
void print()
{
cout << name << endl; //同名成员变量,优先访问当前类的成员变量
cout << Person::name << endl; //访问父类中同名成员变量
}
void Function(int n) //同名成员函数
{
cout << "Function(int n) " << endl;
}
string name="琉璃";
};
int main()
{
student s;
s.print();
s.Function(10);//优先调用子类中成员函数
s.Person::Function(10); //调用父类中的成员函数
return 0;
}
六、派生类的默认成员函数
默认构造函数小编在类和对象(二)那章就已经讲过了咯 。分为6种构造函数。那派生类的默认成员函数是怎样的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
1.当基类中有默认构造的时,调用基类的构造函数初始化基类的那一部分成员。
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
Person()
:name("子墨")
, number("1234656789")
, age(18)
{}
protected:
string name = "子墨";//姓名
string number = "1234567895";//电话号码
int age = 18;//年纪
};
class student :public Person
{
public:
//默认构造函数的行为:
//如果是内置类型-->不确定,可能初始化,也可能不初始化,这要看编译器
//如果是自定义类型-->调用自定义类型的默认构造函数
//如果是继承基类成员的-->调用基类的构造函数初始化基类的那一部分成员
private:
double height;//身高
string address;//地址
};
int main()
{
student s;
return 0;
}
从监视窗口可以看出来,基类中有默认构造情况就调用基类的默认构造,身高这里我们没有初始化,由于他是内置类型,编译器可能初始化,也让可能不初始化。取决于编译器。地址呢是一个自定义类型,那就取调用string的默认构造。
2.当基类中没有写 默认构造时,则必须在派生类构造函数的初始化列表阶段显示调用
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
Person(const char*s)//是带参构造,但不是默认构造
:name(s)
{}
protected:
string name ;//姓名
};
class student :public Person
{
public:
student(const char* name, double n, const char* s)
//:name(naem)//这里不能对基类中成员进行直接初始化
:Person(name) //规定像这样显示调用,就想调用一个匿名对象一样,
,height(n)
,address(s)
{}
double height;//身高
string address;//地址
};
int main()
{
student s("琉璃",180.20,"贵州省");
return 0;
}
显示调用哪里时C++规定的,当基类中没有默认构造时,那就则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
拷贝构造跟默认构造是差不多,内置类型调用编译器生成的拷贝构造,这里是只拷贝,是浅拷贝,自定义类型调用自定义的拷贝构造,如果有指向资源的话,那就要写深拷贝,派生类调用拷贝构造要必须调用基类的拷贝构造完成基类的拷贝初始化。一般来说编译器自己生成的就已经够用了,只是需要深拷贝的 时候才需要自己实现。
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
Person(const char*s)//带参构造
:name(s)
{}
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, double n, const char* s)//带参构造
:Person(name) //规定像这样显示调用,就想调用一个匿名对象一样,
,height(n)
,address(s)
{}
student(const student& s)//拷贝构造,传的是派生类类的对象的引用
:Person(s)
,height(s.height)
,address(s.address)
{}
double height;//身高
string address;//地址
};
int main()
{
student s("琉璃",180.20,"贵州省");
student s1(s);//拷贝s给s1
return 0;
}
大家看上面代码有没有发现有一个问题,那就是当我们调用基类的拷贝构造的时候,基类的拷贝构造需要传基类的对象,但是我们这里传的是派生类的对象呀。那代码为什么还会运行呢?其实这个就是上面的基类和派生类对象赋值转换,当我们传派生类的对象的时候,传的是派生类的引用。那这里会把派生类中基类的的对象切片出来。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类中operator=把 基类中的operator=隐藏了,这里需要显示调用基类的operator=,需要指定基类的作用域
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
Person(const char* s)//带参构造
:name(s)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
name = p.name;
}
return *this;
}
protected:
string name;//姓名
};
class student :public Person
{
public:
student(const char* name, double n, const char* s)//带参构造
:Person(name) //规定像这样显示调用,就想调用一个匿名对象一样,
, height(n)
, address(s)
{}
student& operator=(const student&s)
{
if (this != &s)
{
Person::operator=(s);//显示调用基类的operator=
height = s.height;
address = s.address;
}
return *this;
}
double height;//身高
string address;//地址
};
int main()
{
student s("琉璃", 180.20, "贵州省");
student s1("子墨", 180.20, "贵州省");
s = s1;
return 0;
}
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
#include<iostream>
#include<vector>
#include<stdbool.h>
#include<string>
using namespace std;
class Person
{
public:
Person(const char* s)//带参构造
:name(s)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
name = p.name;
}
return *this;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string name;//姓名
};
class student :public Person
{
public:
student(const char* name, double n, const char* s)//带参构造
:Person(name) //规定像这样显示调用,就想调用一个匿名对象一样,
, height(n)
, address(s)
{}
~student()
{
//~Person();//~Person是不能直接调用的,因为这里派生类和基类的析构形成隐藏关系
//Person::~Person();//当你运行代码的时候,发现析构函数调用的有点多,是你对象的两倍
//所以,这里不需要显示调用 ,在派生类析构结束之后会自动调用基类的析构函数,所以这里可以上面都不写
}
student& operator=(const student&s)
{
if (this != &s)
{
Person::operator=(s);
height = s.height;
address = s.address;
}
return *this;
}
double height;//身高
string address;//地址
};
int main()
{
student s("琉璃", 180.20, "贵州省");
student s1("子墨", 180.20, "贵州省");
s = s1;
return 0;
}
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构。
总结
由于继承的只是点比较多,所以小编今天就分享这么多把。下一章呢小编会给继承来个收尾,希望各位小伙伴们的讨论。好啦!咋们下期再见