博主首页: 有趣的中国人
专栏首页: C++进阶
本篇文章主要讲解 继承 的相关内容
目录
1. 继承的概念和定义
1.1 继承的概念
1.2 继承的定义
1.2.1 继承定义格式
1.2.2 继承方式与访问修饰限定符
2. 基类和派生类对象赋值转换
3. 继承中的作用域
3.1 全局域、局部域、类域、命名空间域
3.2 继承作用域
4. 派生类的默认成员函数
4.1 构造函数和拷贝构造
4.2 赋值重载和析构函数
1. 继承的概念和定义
1.1 继承的概念
在C++中,继承是一种机制,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)获取成员变量和成员函数。子类可以继承父类的属性和行为,并且可以添加自己的属性和行为。这种继承关系可以用来建立类之间的层次结构,从而实现代码的重用和抽象。在C++中,继承有三种类型:公有继承、保护继承和私有继承,它们决定了父类的成员对子类的可见性。
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
string name = "Jesicca";
int age = 18;
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int stu_id;
};
class Teacher : public Person
{
protected:
int job_id;
};
int main()
{
Student st;
Teacher t;
st.Print();
t.Print();
return 0;
}
1.2 继承的定义
1.2.1 继承定义格式
1.2.2 继承方式与访问修饰限定符
总的来说:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。
class Person
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
string name = "Jesicca";
// age 为private类型,继承之后无论在派生类内还是类外都不能被访问
private:
int age = 18;
};
class Student : public Person
{
public:
// 这里会报错,age不能被访问
void Stuage()
{
cout << "Student age:" << age << endl;
}
protected:
int stu_id = 20;
};
虽然不能被访问,但是实际上还是被继承了:
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
class Person
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
string name = "Jesicca";
// age 变成了protected类型了
int age = 18;
};
class Student : public Person
{
public:
// 再类中能被访问
void Stuage()
{
cout << "Student age:" << age << endl;
}
protected:
int stu_id = 20;
};
3. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
4. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用。
2. 基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象、基类的指针、基类的引用。这个操作叫做切片,
对于内置类型是直接拷贝,自定义类型调用拷贝构造,且切片过程中会进行赋值兼容,不会产生临时变量。
必须是基类的指针或者引用指向派生类才是安全的!
例如以下示例:
class Person
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
string name = "Jesicca";
protected:
int age = 18;
};
class Student : public Person
{
public:
void Stuage()
{
cout << "Student age:" << age << endl;
}
protected:
int stu_id = 20;
};
int main()
{
Student st;
// 派生类对象给基类对象
Person p = st;
// 派生类给基类指针
Person* pst = &st;
// 派生类给父类引用
Person& ref = st;
ref.name += "j";
ref.Print();
pst->name += "k";
pst->Print();
return 0;
}
3. 继承中的作用域
3.1 全局域、局部域、类域、命名空间域
先回顾以下C++中其他的域:
全局域、局部域、类域、命名空间域,
其中全局域和局部域会影响生命周期,但是类域和命名空间域不会影响生命周期,再同一个域中不能出现同名的成员变量,也不能出现非重载的两个同名函数。
3.2 继承作用域
1. 在父类和派生类中可以定义两个同名的变量,因为父类和派生类是两个不同的作用域;
2. 子类和父类如果有同名的变量,子类成员会屏蔽掉父类中同名的成员变量,这叫做隐藏或者重定义,可以使用 基类::基类成员 进行显示访问;
class Person
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
string name = "Jesicca";
protected:
int age = 20;
};
class Student : public Person
{
public:
void Stuage()
{
// 父类同名变量会被屏蔽,构成隐藏
cout << "Stuage():" << age << endl;
// 显示访问父类同名成员变量
cout << "Personage:" << Person::age << endl;
}
protected:
int stu_id = 20;
int age = 19;
};
看以下这段代码:
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
B中的fun和A中的fun不是构成重载,因为不是在同一作用域
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
4. 派生类的默认成员函数
默认构造是自己不写,编译器也能自动生成的:构造函数、拷贝构造、析构函数、赋值重载、取地址重载这些都是默认构造(在类和对象的文章中讲过)。
4.1 构造函数和拷贝构造
在派生类中,成员对象大体可以分为三大类:基类中继承下来的、自定义类型、内置类型。
对于自定义类型还是调用在身的构造函数,内置类型不做处理。
对于基类中继承下来的成员,必须调用基类的构造函数初始化基类中的部分成员,如果基类中没有默认构造函数,那么必须在派生类的构造函数中显示调用基类的构造函数,不然就会报错。
例如以下的例子:
class Person
{
public:
// 没有默认构造函数
Person(const char* name)
:name(name)
{}
void Print()
{
cout << "name:" << name << endl;
//cout << "age:" << age << endl;
}
protected:
string name;
//int age = 20;
};
class Student : public Person
{
public:
// 由于基类中没有默认生成的构造函数,所以在派生类的构造函数必须显示调用基类的构造函数
Student(const char* str, const char* name, int stu_id = 20, int age = 19)
// 这边显示调用,类似于匿名对象的调用,要把基类看成一个整体进行调用
:Person(name)
,stu_id(stu_id)
,age(age)
,str(str)
{}
void Stuage()
{
cout << "Stuage():" << age << endl;
//cout << "Personage:" << Person::age << endl;
}
protected:
string str;
int stu_id;
int age;
};
对于拷贝构造,派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化,例如:
// 父类
Person(const Person& p)
:name(p.name)
{}
// 子类
Student(const Student& st)
:Person(st)
{
str = st.str;
stu_id = st.stu_id;
age = st.age;
}
4.2 赋值重载和析构函数
派生类的operator=必须要调用基类的operator=完成基类的复制。
Person& operator=(const Person& p)
{
if (this != &p)
{
name = p.name;
}
return *this;
}
Student& operator=(const Student& st)
{
if (this != &st)
{
// 这里如果写成operator=(st);会构成隐藏,会无穷递归
Person::operator=(st);
str = st.str;
stu_id = st.stu_id;
age = st.age;
}
return *this;
}
子类析构函数和父类析构函数构成隐藏关系。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
~Person()
{
cout << "~Person()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
// 会构成隐藏,因此如果写要显示写,
// 但是为了保证析构的顺序是先子后父,编译器会在程序结束时自动调用父类的析构函数,不需要显示写,
// 因为在此时可能需要访问父类中的对象,如果析构就访问不了了
//Person::~Person();
}