🍼博客主页:阿博历练记
📖文章专栏:c++
🚍代码仓库:阿博编程日记
🍡欢迎关注:欢迎友友们点赞收藏+关注哦🌹
文章目录
- 🥝1.继承的概念及定义
- 📜1.1继承的概念
- 🗝️1.2继承图解
- 🔍1.3代码演示
- 🔖1.4继承定义
- (◕‿◕✿)1.5继承方式和访问限定符之间的使用
- ⭐基类的protected成员和private成员的区别
- ⭐私有成员和不可见的区别
- 🔍1.6代码验证
- ✏️1.7总结
- 🍆2.基类和派生类对象赋值转换
- 🔍2.1代码示例
- ⭐子类赋值给父类对象是隐式类型转换吗?
- 🍃3.继承中的作用域
- 🔍代码示例1
- 🔍代码示例2
- 🍂4.派生类的默认成员函数
- 💡4.1派生类怎么显式定义构造函数
- 💡4.2派生类怎么显式定义拷贝构造函数
- 💡4.3派生类怎么显式定义赋值重载
- ⭐4.4派生类怎么显式定义析构函数
- 🗞️归纳小结
🥝1.继承的概念及定义
📜1.1继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是
函数复用
,继承是类设计层次的复用。
🗝️1.2继承图解
🔍1.3代码演示
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
📒友友们,这里我们可以看出s对象和t对象里面都有_name和_age,因为它们两个都继承了Person类,而且它们两个都可以调用print函数,所以这里有个结论友友们要记好哦💰:子类在继承父类的时候不仅继承了成员变量,还有成员函数。
🔖1.4继承定义
(◕‿◕✿)1.5继承方式和访问限定符之间的使用
⭐基类的protected成员和private成员的区别
基类的private成员:无论是哪种继承方式,在子类中或者类外都是不可见的,即不能使用的,我们可以把它抽象的理解为父亲的私房钱🤪,类中类外都不可使用.
基类的protected成员:友友们这里记好,如果此时子类的继承方式是public继承和protected继承,那么继承之后就是子类的protect成员,可以在子类中访问和使用,最重要的是如果当前子类成为父类,那么这个成员就会被新的子类继承下去.如果此时子类的继承是private继承,那么继承之后就会成为子类的private成员,也可以在这个子类中访问和使用,但是如果当前子类成为父类时,这些成员继承下去就无法被新的子类访问和使用了.
⭐私有成员和不可见的区别
私有成员:
①在当前类中可以访问和使用
②在类外不能访问使用
③继承给子类时,无法访问使用
不可见:
①父类的私有成员无论是哪种继承方式子类都不可访问使用
②父类的私有成员继承给子类时,无论是在子类中还是类外都是不可访问使用的
🔍1.6代码验证
✏️1.7总结
①基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
②基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected
。可以看出保护成员限定符是因继承才出现的。
③实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private
④使用关键字class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,不过最好显示的写出继承方式。
🍆2.基类和派生类对象赋值转换
🔍2.1代码示例
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
void fun()
{
_age++;
}
protected:
string _name = "peter"; // 姓名
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
Person p = s;
Person& p = s;
s.Print();
t.Print();
return 0;
}
🚦友友们这里一定要注意只有在公有继承的条件下,子类和父类是is-a的关系,子类对象才可以赋值给父类对象,比如杯子是生活用品,这里子类就是杯子,父类就是生活用品。
⭐子类赋值给父类对象是隐式类型转换吗?
这里我们通过代码可以看出来,它们之间没有发生隐式类型转换,因为如果发生转换的话,中间会产生临时变量,而临时变量具有常性,我们在引用的时候需要加const来保证权限平移,而这里我们没有加const,所以不是隐式类型转换。下面就要给友友们引入一个新的概念了——父子类赋值兼容规则📜:在public继承条件下,子类和父类是一个
is-a
的关系,子类对象赋值给父类对象/父类指针/父类引用时,我们认为是天然的,中间不产生临时对象。这个规则也称之为切割或者切片。
友友们从这里也可以证明,我们通过让父类对象p去调用fun函数,对_age进行++操作,可以发现s对象内的_age也发生了改变,所以可以证实p就是子类对象中父类那一部分的别名。
🍃3.继承中的作用域
🔍代码示例1
#include <iostream>
using namespace std;
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; //身份证号
};
class Student : public Person
{
public:
void Print()
{
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
很多友友们可能会认为这个代码是错误的,在Student类里面为什么会有两个num变量呢?友友们这里是这样的,这两个num在不同的类域里面,虽然子类继承了父类的num变量,但是父类的num变量是在父类的类域里面定义的,所以这里编译不会出错。
🔍代码示例2
#include <iostream>
using namespace std;
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; //身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
这时候友友们可能会好奇如果我们在Student类里面打印_num,那么会是父类的值还是子类的值呢?
很显然是子类中的num变量,友友们这里记住一般编译器采用就近原则,会先在自己的类域里面查找,如果想访问父类中的num变量,我们需要指明类域:Person::_num.
📖总结
💞:父类和子类可以有同名成员,因为它们是独立作用域
💞:默认情况直接访问是子类的,子类同名成员隐藏父类同名成员
💞:继承中,同名的成员函数,函数名相同就构成隐藏,不管参数和返回值,这里注意区分一下函数重载,函数重载是在同一作用域,函数名相同,参数不同(参数类型/个数/类型顺序),所以如果我们要通过子类访问父类的同名成员函数时,我们依然需要指明类域:s.Person::fun()
🍂4.派生类的默认成员函数
💡4.1派生类怎么显式定义构造函数
❌错误案例
#include<vector>
#include<iostream>
using namespace std;
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 id)
: _name(name)
, _id(id)
{
cout << "Student()" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s;
return 0;
}
✔️正确案例
Student(const char* name, int id)
: Person(name)
, _id(id)
{
cout <<_name <<":"<<_id<< endl;
}
💡4.2派生类怎么显式定义拷贝构造函数
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 id)
: Person(name)
, _id(id)
{
cout <<_name <<":"<<_id<< endl;
}
Student(const Student& s)
:Person(s)
,_id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _id; //学号
};
友友们注意,这里我们在拷贝子类对象中父类那一部分的时候,我们在附用父类的拷贝构造函数的时候很多友友们可能会有疑惑:附用父类的拷贝构造不是应该传父类对象吗,为什么传子类对象s呢?🌠友友们一定不要忘了父子类赋值兼容规则,这里我们虽然传的是子类对象,但其实是经过"切割"把子类中父类的那一部分传了过去🌍。
💡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;
}
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 id)
: Person(name)
, _id(id)
{
cout <<_name <<":"<<_id<< endl;
}
Student(const Student& s)
:Person(s)
,_id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
Student operator=(const Student& s)
{
if (&s != this)
{
operator=(s);
_id = s._id;
}
return *this;
}
protected:
int _id; //学号
};
int main()
{
Student s("小王",20);
Student s1(s);
Student s2("小李",18);
s2 = s;
return 0;
}
✔️正确案例
Student operator=(const Student& s)
{
if (&s != this)
{
Person::operator=(s);
_id = s._id;
}
return *this;
}
⭐4.4派生类怎么显式定义析构函数
❌错误案例1
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 id)
: Person(name)
, _id(id)
{
cout <<_name <<":"<<_id<< endl;
}
Student(const Student& s)
:Person(s)
,_id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
Student operator=(const Student& s)
{
if (&s != this)
{
Person::operator=(s);
_id = s._id;
}
return *this;
}
~Student()
{
~Person();
cout << "~Student()" << endl;
}
protected:
int _id; //学号
};
int main()
{
Student s("小王",20);
Student s1(s);
Student s2("小李",18);
s2 = s;
return 0;
}
❌错误案例2
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
友友们这里我们也可以看出来我们的析构函数每一个对象都多调了一次🙄,为什么会出现这种情况呢?
✔️正确案例
~Student()
{
cout << "~Student()" << endl;
}
🗞️归纳小结
① 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
② 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
③ 派生类的operator=必须要调用基类的operator=完成基类的复制。
④ 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
⑤ 派生类对象初始化先调用基类构造再调派生类构造。
⑥ 派生类对象析构清理先调用派生类析构再调基类的析构。
⑦ 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。