目录
继承
概述:C++ 中的继承是类与类之间的关系。继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。当创建一个类时,就不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。被继承的类称为父类或基类,继承的类称为子类或派生类。派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
使用继承的场景:
-
新类与现有的类相似,只是 多出若干成员变量或成员函数时,新类会拥有基类的所有功能。
-
需要创建多个类,它们拥有很多相似的成员变量或成员函数时。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。
语法: class 派生类名:[继承方式] 基类名{ 派生类新增加的成员 }; 继承方式为可选的,如果不写,那么默认为 private。
实例
#include <iostream>
#include <string>
using namespace std;
// 基类 Pelple
class People
{
public:
void Setname(string name); // 设置名字
void Setage(int age); // 设置年龄
string Getname(); // 返回名字
int Getage(); // 返回年龄
private:
string m_name;
int m_age;
};
void People::Setname(string name) { m_name = name; }
void People::Setage(int age) { m_age = age; }
string People::Getname() { return m_name; }
int People::Getage() { return m_age; }
// 派生类 Student
class Student : public People // 公有继承People
{
public:
void Setscore(float score); // 设置分数
float Getscore(); // 返回分数
private:
float m_score;
};
void Student::Setscore(float score) { m_score = score; }
float Student::Getscore() { return m_score; }
int main()
{
Student stu;
stu.Setname("小明"); // 使用继承过来的People 成员函数
stu.Setage(16);
stu.Setscore(95.5f);
cout << stu.Getname() << "的年龄是 " << stu.Getage() << ",成绩是 " << stu.Getscore() << endl;
return 0;
}
结果
小明的年龄是 16,成绩是 95.5
总结:Student 类继承了 People 类的成员,那么 People 类中的成员就跟Student 类自己的成员一样可以通过对象访问 。
一、 继承的三种方式
概述:子类的继承方式为 public、protected 或 private ,继承方式限定了子类成员在父类中的访问权限,子类可以访问父类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
不同的访问类型的访问权限如下:
访问 | public | protected | private |
---|---|---|---|
同一个类中 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
-
公有继承(public):基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
-
保护继承(protected): 基类的公有和保护成员将成为派生类的保护成员,基类的私有成员在派生类中还是私有。
-
私有继承(private):基类的公有和保护成员将成为派生类的私有成员。
注意:
-
基类成员在派生类中的访问权限不得高于继承方式中指定的权限,也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
-
不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。但可以调用基类的公有或者保护权限的成员函数简接访问。
-
如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
-
如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
子类继承了所有父类的权限,但下列情况子类无法继承:
-
基类的构造函数、析构函数和拷贝构造函数。
-
基类的重载运算符。
-
基类的友元函数。
事实上基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了
实例
#include <iostream>
#include <string>
using namespace std;
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
//公共继承
class Son1 :public Base1
{
public:
void func()
{
m_A; //可访问 public权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
void myClass()
{
Son1 s1;
s1.m_A; //其他类只能访问到公共权限
}
//保护继承
class Base2
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son2 :protected Base2
{
public:
void func()
{
m_A; //可访问 protected权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
void myClass2()
{
Son2 s;
//s.m_A; //不可访问
}
//私有继承
class Base3
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son3 :private Base3
{
public:
void func()
{
m_A; //可访问 private权限
m_B; //可访问 private权限
//m_C; //不可访问
}
};
class GrandSon3 :public Son3
{
public:
void func()
{
//Son3是私有继承,所以继承Son3的属性在GrandSon3中都无法访问到
//m_A;
//m_B;
//m_C;
}
};
using 改变了它们的默认访问权限
实例
#include <iostream>
#include <string>
using namespace std;
// 基类People
class People
{
public:
void show();
protected:
string m_name;
int m_age;
};
void People::show()
{
cout << m_name << "的年龄是" << m_age << endl;
}
// 派生类Student
class Student : public People
{
public:
void learning();
public:
using People::m_name; // 使用using 改变权限 将protected改为public
using People::m_age; // 使用using 改变权限 将protected改为public
float m_score;
private:
using People::show; // 使用using 改变权限 将public改为private
};
void Student::learning()
{
cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
int main()
{
Student stu;
stu.m_name = "小明";
stu.m_age = 16;
stu.m_score = 99.5f;
// stu.show(); // 不能访问
stu.learning();
return 0;
}
结果
我是小明,今年16岁,这次考了99.5分!
二、 继承时的名字遮蔽问题
概述:如果子类中的成员(包括成员变量和成员函数)和父类中的成员重名,那么就会遮蔽从父类继承过来的成员。所谓遮蔽,就是在子类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是子类新增的成员,而不是从基类继承来的。
实例
#include <iostream>
#include <string>
using namespace std;
// 基类People
class People
{
public:
void show();
protected:
string m_name;
int m_age;
};
void People::show()
{
cout << "大家好,我叫" << m_name << ",今年" << m_age << "岁" << endl;
}
// 派生类Student
class Student : public People
{
public:
Student(string name, int age, float score);
public:
void show(); // 遮蔽基类的show()
private:
float m_score;
};
Student::Student(string name, int age, float score) {
m_name = name;
m_age = age;
m_score = score;
}
void Student::show()
{
cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
}
int main()
{
Student stu("小明", 16, 90.5);
// 使用的是子类新增的成员函数,而不是从父类继承的
stu.show();
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
stu.People::show();
return 0;
}
结果
小明的年龄是16,成绩是90.5
大家好,我叫小明,今年16岁
本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第39行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 45 行代码所示。
总结:
-
访问子类同名成员 直接访问即可
-
访问父类同名成员 需要加作用域
-
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
三、 继承中子类和父类的构造函数
概述: 类的构造函数不能被继承,在设计子类时,对继承过来的成员变量的初始化工作也要由子类的构造函数完成,但是大部分父类都有 private 属性的成员变量,它们在子类中无法访问,更不能使用子类的构造函数来初始化。解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。
把基类的构造函数放在子类构造函数的初始化列表上,以此实现调用基类的构造函数来为子类从基类继承的成员变量初始化。
实例
#include <iostream>
#include <string>
using namespace std;
// 父类People
class People
{
protected:
string m_name;
int m_age;
public:
People(string, int);
};
People::People(string name, int age) : m_name(name), m_age(age) {}
// 子类Student
class Student : public People
{
private:
float m_score;
public:
Student(string name, int age, float score);
void display();
};
// People(name, age)就是调用基类的构造函数
Student::Student(string name, int age, float score) : People(name, age), m_score(score) { }
void Student::display()
{
cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
}
int main()
{
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}
结果
小明的年龄是16,成绩是90.5。
第29 行People(name, age)
就是调用父类的构造函数,并将 name 和 age 作为实参传递给它,m_score(score)
是子类的参数初始化表,它们之间以逗号,
隔开。
还可以用下面的写法可以在构造函数体内赋值给m_age
和m_name
。但是要在父类先写无参构造,不然编译器会报错说:没有默认构造
Student::Student(string name, int age, float score)
{
this->m_name = name;
this->m_age = age;
this->m_score = score;
m_score = score;
}
3.1 继承中父类构造函数的调用规则
概述: 通过子类创建对象时必须要调用父类的构造函数,这是语法规定。所以,定义派生类构造函数时最好指明父类构造函数;如果不指明,就调用父类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。
#include <iostream>
#include <string>
using namespace std;
// 基类People
class People
{
public:
People(); // 基类默认构造函数
People(string name, int age);
protected:
string m_name;
int m_age;
};
People::People() : m_name("xxx"), m_age(0) { } // People 的无参构造
People::People(string name, int age) : m_name(name), m_age(age) {}
// 派生类Student
class Student : public People
{
public:
Student();
Student(string, int, float);
public:
void display();
private:
float m_score;
};
Student::Student() : m_score(0.0) { } // 派生类默认构造函数
Student::Student(string name, int age, float score) : People(name, age), m_score(score) { }
void Student::display()
{
cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
}
int main()
{
Student stu1; // 创建子类时没有指明要调用基类的哪一个构造函数,从运行结果可以看出来,默认调用了不带参数的构造函数,也就是People::People()
stu1.display();
Student stu2("小明", 16, 90.5); // 创建子类时在初始化列表里调用了父类的有参构造所以stu2 中的成员全部都初始化了
stu2.display();
return 0;
}
结果
xxx的年龄是0,成绩是0。
小明的年龄是16,成绩是90.5。
注意:如果将父类 People 中不带参数的构造函数删除,那么会发生编译错误,因为创建对象 stu1 时需要调用 People 类的默认构造函数, 而 People 类中已经显式定义了构造函数,编译器不会再生成默认的构造函数。
3.2 构造函数的序
概述: 创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数
如有几层继承
A --> B --> C
那么创建 C 类对象时构造函数的执行顺序为:
A类构造函数 --> B类构造函数 --> C类构造函数
派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。因为B 中包含了A的构造 C调用B的构造 间接的包含了A的构造,如果再调用A 的构造就造成重复了。
四、 继承中子类和父类的析构函数
概述: 析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
析构函数的执行顺序和构造函数的执行顺序相反:
-
创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
-
而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
实例
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A 的构造函数" << endl; }
~A() { cout << "A 的析构函数" << endl; }
};
class B : public A
{
public:
B() { cout << "B 的构造函数" << endl; }
~B() { cout << "B 的析构函数" << endl; }
};
class C : public B
{
public:
C() { cout << "C 的构造函数" << endl; }
~C() { cout << "C 的析构函数" << endl; }
};
int main()
{
C test;
return 0;
}
结果
A 的构造函数
B 的构造函数
C 的构造函数
C 的析构函数
B 的析构函数
A 的析构函数
由上面的实例可以看出当C 创建对象时,会先创建A 的构造,再创建 B 的构造,最后创建C 的构造,当程序执行完的时候 会先析构C 再析构B 最后析构 A
总结: 继承中 先调用父类的构造函数,再调用子类构造函数,析构顺序与构造相反,如果有多级继承会最先从最上面的构造开始执行。
五、 多继承
概述: 多继承即一个子类可以有多个父类,它继承了多个父类的特性
语法: 将多个基类用逗号隔开即可
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
class D: public A, private B, protected C
{
// 派生类类体
}
5.1 多继承下的构造函数
概述: 多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:
D(形参列表): A(实参列表), B(实参列表), C(实参列表)
{
//其他操作
}
实例
#include <iostream>
using namespace std;
// 基类
class BaseA
{
public:
BaseA(int a, int b);
~BaseA();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b) : m_a(a), m_b(b)
{
cout << "BaseA 的构造函数" << endl;
}
BaseA::~BaseA()
{
cout << "BaseA 的析构函数" << endl;
}
// 基类
class BaseB
{
public:
BaseB(int c, int d);
~BaseB();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d) : m_c(c), m_d(d)
{
cout << "BaseB 的构造函数" << endl;
}
BaseB::~BaseB() {
cout << "BaseB 析构函数" << endl;
}
// 派生类 继承了基类A 基类B
class Derived : public BaseA, public BaseB
{
public:
Derived(int a, int b, int c, int d, int e);
~Derived();
public:
void show();
private:
int m_e;
};
// 派生类的构造函数
Derived::Derived(int a, int b, int c, int d, int e) : BaseA(a, b), BaseB(c, d), m_e(e)
{
cout << "Derived 构造函数" << endl;
}
// 派生类的析构函数
Derived::~Derived()
{
cout << "Derived 析构函数" << endl;
}
void Derived::show()
{
cout << m_a << ", " << m_b << ", " << m_c << ", " << m_d << ", " << m_e << endl;
}
int main()
{
Derived obj(1, 2, 3, 4, 5);
obj.show();
return 0;
}
结果
BaseA 的构造函数
BaseB 的构造函数
Derived 构造函数
1, 2, 3, 4, 5
Derived 析构函数
BaseB 析构函数
BaseA 的析构函数
注意: 基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。上面D类继承顺序是先继承A 再继承B 最后继承C 所以先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。
5.2 多继承下的命名冲突
概述: 当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::
,以显式地指明到底使用哪个类的成员,消除二义性。
实例
#include <iostream>
using namespace std;
class Base1
{
public:
Base1()
{
m_A = 100;
}
public:
int m_A;
};
class Base2
{
public:
Base2()
{
m_A = 200; // 开始是m_B 不会出问题,但是改为mA就会出现不明确
}
public:
int m_A;
};
// 语法:class 子类:继承方式 父类1 ,继承方式 父类2
class Son : public Base2, public Base1
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
public:
int m_C;
int m_D;
};
// 多继承容易产生成员同名的情况
// 通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}
int main()
{
test01();
return 0;
}
总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域
六、 棱形继承
概述: 菱形继承,它是多重继承中的一个特殊情况两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承。
菱形继承可能引发以下问题:
-
冲突:如果类
B
和类C
都继承了类A
的成员,那么在类D
中使用时就会产生二义性。 -
内存浪费:由于类
D
继承了两个副本的类A
,会导致内存浪费。
利用虚继承可以解决菱形继承问题
实例
#include <iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
// 继承前加virtual关键字后,变为虚继承
// 此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main()
{
test01();
return 0;
}
结果
st.Sheep::m_Age = 200
st.Tuo::m_Age = 200
st.m_Age = 200
总结: 虚继承只会继承一份数据,通过父类作用域访问的数据也是同一份。
七、跳转链接
上一篇:C++ 友元详解
https://blog.csdn.net/qq_61692089/article/details/134178784?spm=1001.2014.3001.5501
下一篇:C++运算符重载详解