博主主页:Yan. yan.
C语言专栏
数据结构专栏
力扣牛客经典题目专栏
C++专栏
文章目录
- 继承的概念与定义
- 1、继承的概念
- 2、继承的定义
- 2.1、继承的语法形式
- 2.2、继承中类的叫法
- 2.3、继承后的子类成员访问权限
- 基类与派生类的赋值转换
- 1、派生类对象赋值给基类对象
- 2、派生类对象的引用赋值给基类对象
- 3、派生类对象的指针赋值给基类对象
- 4、基类指针赋值给派生类指针
- 继承的作用域
- 1、同名变量
- 2、同名函数
- 派生类中的默认成员
- 1、对象的构造和析构遵循特定的顺序
- 2、派生类构造函数调用基类构造函数
- 3、析构函数的特殊处理
- 4、拷贝构造与赋值重载必须调用基类的拷贝构造与赋值重载完成对基类的初始化
继承的概念与定义
1、继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
举一个简单的例子
一个人,他具有年龄,姓名等个人信息,然后我们可以将这些信息整合为一个Person类,如果说我们还想要定义一个Student的类,这些学生当然也是人,因此我们可以复用Person这个类,然后再添加一些其他的信息,例如学号之类的。
2、继承的定义
2.1、继承的语法形式
继承的定义时=是通过在子类的声明中使用基类, 并加上冒号“:” 和继承方式(public \ protected \ private)来实现的。
class Person
{
//.....基类
};
// 子 继承方式 父
class Student : public Person
{
//....派生类
};
例:
class Person
{
public:
void print()
{
cout << _name << endl;
cout << _address << endl;
cout << _age << endl;
}
private:
string _name = "张三 "; // 姓名
string _address = "河北 ";// 地址
int _age = 18 ;// 年龄
};
// 子 继承方式 父
class Student : public Person
{
private:
string _tel = "189 ";// 电话
int _id = 123321;
};
int main()
{
Person p;
Student s;
return 0;
}
通过监视窗口可以看出,Student类继承了Person类的成员与函数。
2.2、继承中类的叫法
(1)子类(或派生类): 这是指继承其他类(即父类)的类。子类可以使用父类的所有非私有属性和方法,同时也可以添加自己的属性和方法或重写父类的方法。
(2)父类(或基类):这是指被其他类(即子类)继承的类。父类提供了通用的属性和方法,这些可以被子类继承和使用。
2.3、继承后的子类成员访问权限
不同的继承方式产生的继承效果自然也不一样。
如图:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类与派生类的赋值转换
在面向对象编程中,基类和派生类之间的赋值转换涉及到对象的类型兼容性和多态性。
1、派生类对象赋值给基类对象
派生类对象可以赋值给基类的对象。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
相反,基类成员无法赋值给派生类成员,因为有些成员派生类有,而基类没有。
class Person
{
public:
Person(string name = "Kana", int age = 18, double height = 1.50) :
_name(name),
_age(age),
_height(height)
{
// ...
}
void Print() const
{
cout << _name << endl;
cout << _age << endl;
cout << _height << endl;
}
private:
string _name; // 姓名
int _age; // 年龄
double _height; // 身高
};
class Student : public Person
{
public:
Student(string name = "Kana", int age = 18, double height = 1.50, int id = 233333, int grade = 10)
: Person(name, age, height), _id(id), _grade(grade) {}
void Print() const
{
// 首先调用基类的 Print 方法
Person::Print();
// 然后打印学生特有的属性
cout << "ID: " << _id << endl;
cout << "Grade: " << _grade << endl;
}
private:
int _id; // 学号
int _grade; // 年级
};
int main()
{
Student s;
s.Print();
// 尝试将 Student 对象切片为 Person 对象(不推荐,因为会丢失信息)
Person p = s;
p.Print(); // 仅打印 Person 的信息(姓名、年龄、身高)
return 0;
}
2、派生类对象的引用赋值给基类对象
我们可以将一个派生类对象的引用赋值给一个基类类型的引用,而不需要const修饰符。
class Person
{
public:
void Print()
{
cout << "Person Print()" << endl;
}
};
class Student :public Person
{
public:
void Print()
{
cout << "Student Print()" << endl;
}
};
int main()
{
Student s;
Person& p = s;
p.Print();
return 0;
}
3、派生类对象的指针赋值给基类对象
派生类对象的指针可以赋值给基类对象的指针。
class Person
{
public:
void Print() const
{
cout << "Person: Name = " << _name << ", Age = " << _age << endl;
}
Person(string name, int age) : _name(name), _age(age) {}
private:
string _name;
int _age;
};
class Student : public Person
{
public:
void Print() const
{
cout << "Student: ID = " << _id << ", Grade = " << _grade << endl;
}
Student(string name, int age, int id, int grade) : Person(name, age), _id(id), _grade(grade) {}
private:
int _id; // 学号
int _grade; // 年级
};
int main()
{
Student student("Kana", 18, 12345, 10);
// 将Student对象的指针赋值给Person对象的指针
Person* p = &student;
p->Print();
// 如果要访问Student特有的成员,需要使用Student类型的指针或引用
Student* s = &student;
s->Print();
return 0;
}
4、基类指针赋值给派生类指针
在C++中,将基类指针直接强制转换为派生类指针是一种危险的做法,通常是不被推荐的,因为它违反了类型安全的原则,并且可能导致未定义行为,包括越界访问或访问无效内存。
Person p;
Student *s = (Student*) & p; // right
1.派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
继承的作用域
继承的作用域决定了从基类继承到派生类的成员(包括变量和方法)的访问权限。
在C++的继承体系中, 派生类和基类都各自拥有独立的作用域,当派生类和基类中定义了相同的变量(包括成员和函数)时,派生类的成员会“隐藏”或者“重定义”基类中的同名成员,这意味着在派生类的作用域内,直接访问该同名成员将引用派生类的成员,而不是基类的成员。
1、同名变量
class Person
{
protected:
int _id = 123;
int _age = 18;
};
class Student : public Person
{
public:
void print()
{
cout << _id << endl;
cout << _age << endl;
cout << _num << endl;
}
private:
int _age = 20;
int _num = 3;
};
int main()
{
Student s;
s.print();
return 0;
}
派生类和基类中都含有名为_age的成员变量,打印结果如下:
如果想要打印基类中的_age,则需要使用 :: 限定符:
2、同名函数
class A
{
public:
void fun()
{
cout << "fun()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "fun(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(1);
};
B 中的 fun 和 A 中的 fun 构成隐藏,成员函数满足函数名相同就构成隐藏。
由于函数重载针对的是同一个作用域的函数,而基类与派生类直接作用域不同。因此不是函数重载。
同样的,如果需要访问其他作用域的函数,我们需要使用 :: 操作符:
派生类中的默认成员
我们知道:在类中有6个默认成员函数,如果不显示定义,编译会自动生成。
那么在派生类中,这些成员函数如何生成?
1、对象的构造和析构遵循特定的顺序
对象的构造和析构遵循特定的顺序,以确保对象的正确初始化和清理。
构造函数调用顺序:
(1)创建派生类对象时,从最顶层的基类开始,逐层向下调用构造函数,直到派生类。
(2)接着,按照派生类中成员变量的声明顺序初始化成员变量(若成员是对象,则调用其构造函数)。
(3)最后,执行派生类构造函数体中的代码。
析构函数调用顺序:
(1)销毁派生类对象时,首先调用派生类的析构函数。
(2)然后,按照成员变量声明的逆序调用成员变量的析构函数(若成员是对象)。
(3)最后,从最顶层的基类开始,逐层向上调用析构函数,直到派生类的基类。
class Person
{
public:
Person(string name = "Kana")
: _name(name)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
private:
string _name; // 姓名
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
int _id;
};
int main()
{
Student s;
return 0;
}
2、派生类构造函数调用基类构造函数
(1)派生类的构造函数必须调用基类的构造函数来初始化基类的成员。
(2)如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表中显式调用基类的一个构造函数。
class Person
{
public:
// Person类的构造函数,用于初始化名字
Person(const char* name) :
_name(name)
{
// ...
}
// Person类的拷贝构造函数
Person(const Person& p) :
_name(p._name)
{
}
private:
string _name;
};
class Student : public Person
{
public:
// Student类的构造函数,接收学号和名字
Student(int id, const char* name) :
_id(id),
Person(name)
{
// ...
}
// Student类的默认构造函数
Student() :
Person("Default Student Name"),
_id(0)
{
// ...
}
private:
int _id;
};
3、析构函数的特殊处理
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor()。
4、拷贝构造与赋值重载必须调用基类的拷贝构造与赋值重载完成对基类的初始化
class Person
{
public:
// 默认构造函数
Person(const string& name) :
_name(name)
{
// ...
}
// 拷贝构造函数
Person(const Person& p) :
_name(p._name)
{
cout << "Copy Person(" << _name << ")" << endl;
}
// 赋值操作符重载
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
cout << "Assign Person(" << _name << ")" << endl;
}
return *this;
}
// 析构函数
~Person()
{
cout << "~Person(" << _name << ")" << endl;
}
string _name;
};
class Student : public Person
{
public:
// 构造函数
Student(int num, const string& name) : Person(name), _num(num) {
cout << "Student(" << _num << ", " << _name << ")" << endl;
}
// 拷贝构造函数
Student(const Student& s) : Person(s), _num(s._num) {
cout << "Copy Student(" << _num << ", " << _name << ")" << endl;
}
// 赋值操作符重载
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); // 调用基类的赋值操作符
_num = s._num;
}
return *this;
}
// 析构函数
~Student()
{
cout << "~Student(" << _num << ", " << _name << ")" << endl;
}
int _num;
};