目录
一. 继承的介绍
1. 继承是什么
在 C++ 中,继承是一种面向对象编程的重要特性,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为。通过继承,子类可以重用基类的成员,并且可以添加自己的新成员或修改继承的成员。
2. 继承的作用
继承的主要目的是实现代码的重用和封装。通过继承,可以从已有的类构建出新的类,子类可以继承基类的公共成员,并且可以根据需要添加、修改或重新实现这些成员。继承还可以建立类之间的层次关系,形成逻辑和现实世界对象之间的关系模型。
二. 继承的语法
1. 继承的方式
在C++中,我们一个类继承另一个类的语法怎么写呢?
首先,我们的C++中式有三种继承方式的:
- public:公有继承,就是父类的成员式什么就是什么。
- protected:保护继承,那么继承下来一般就变成了保护的成员。
- private:私有继承,继承下来就是私有成员。
这个只是我们的继承方式,那么如果我们的父类的成员的访问限定符号也是对继承是由很大影响的,如果父类的成员是 private 的,那么不论是什么方式继承下来,那么父类的私有成员都是不可见的,如果父类的成员是保护或者是公有,那么继承下去的访问限定就是取小的来。
2. 默认继承方式
我们都知道,在我们的C++中是有两种定义自定义类型的方式:
- struct
- class
那么这两种继承下去如果不写继承的方式默认是什么继承呢?我们在继承的时候不显示的写继承方式就是默认的继承。
class A
{
public:
int _a;
};
class AA : A
{
};
int main()
{
AA _aa;
_aa._a;
}
class 下,我们的默认继承是私有的
struct A
{
public:
int _a;
};
struct AA : A
{
};
int main()
{
AA _aa;
_aa._a;
}
我们的 struct 默认继承就是公有
3. 公有继承
公有继承:public
class person
{
public:
protected:
string _name;
string _id;
};
class student : public person
{
protected:
string _schoolId;
};
int main()
{
student s1;
}
我们打开监视看一下我们的 s1
我们的 s1 里面是由一个 person 对象的
如果我们的 person 里面的成员是公有的,那么我们还是可以在类外面通过 s1 访问到 person 里面的成员的。
class person
{
public:
//protected:
string _name;
string _id;
};
class student : public person
{
protected:
string _schoolId;
};
int main()
{
student s1;
s1._name = "张三";
}
4. 保护继承
class person
{
public:
protected:
string _name;
string _id;
};
class student : protected person
{
protected:
string _schoolId;
};
int main()
{
student s1;
}
保护继承的话,我们的父类里面的除私有外的所有成员继承后都变成了子类中的保护对象。
那么如果我们在看一下父类里面的成员是公有的话,通过保护继承,我们还能不能访问到。
class person
{
public:
//protected:
string _name;
string _id;
};
class student : protected person
{
protected:
string _schoolId;
};
int main()
{
student s1;
s1._name = "张三";
}
通过我们的保护继承后父类里面的公有成员我们的 s1 也是不能访问的。
5. 私有继承
class person
{
public:
protected:
string _name;
string _id;
};
class student : private person
{
protected:
string _schoolId;
};
int main()
{
student s1;
s1._name = "张三";
}
我们的私有继承,如果不访问继承后的成员的话,那么是和保护继承和公有继承是一样的,但是我们的访问的话,是不一样的。
6. 切片
int main()
{
int n = 10;
double d = n;
return 0;
}
我们知道这句代码就是将 n 赋值给了 d,那么我们的 子类和父类之间能否赋值呢?
可以的!
我们将子类赋值给父类成为“向上转型”,其中这个向上转型是语法自然支持的,我们还将子类给父类成为切片。
class A
{
public:
protected:
int _a;
};
class AA : public A
{
public:
protected:
int _aa;
};
int main()
{
AA aa;
A a = aa;
}
那么我们为什么说是语法天生支持的呢?我们知道如果我们这种赋值需要影视类型转换的话,,中间会什么临时变量。
class A
{
public:
protected:
int _a;
};
class AA : public A
{
public:
protected:
int _aa;
};
int main()
{
int n = 10;
double& d = n;
AA aa;
A& a = aa;
}
但是我们的向上转型是可以的,所以我们的向上转型就是天生支持的,不会生成临时变量。
当然我们的子类也可以是被父类的引用或者指针指向。
class A
{
public:
protected:
int _a;
};
class AA : public A
{
public:
protected:
int _aa;
};
int main()
{
AA aa;
A& a = aa;
A* ptrA = &aa;
}
子类给父类是天生支持的,但是父类却不可以给子类,因为我们的子类是继承父类的,所以子类里面还可能会有其他的元素,所以不能发生切片。
int main()
{
A a;
AA aa = a;
}
还有我们的指针和引用也是不可以的。
int main()
{
A a;
AA& aa = a;
AA* aa = &a;
}
但是还是有办法实现强转的, 可以通过 dynamic_cast 但是这个是C++11的语法现在不讲。
三. 隐藏(重定义)
1. 什么是隐藏(重定义)
隐藏就是我们的父类与子类中有同名成员,那么我们的子类中的同名成员就是会隐藏我们的父类中的同名成员,导致我们访问的时候如果不指定的话只能访问到子类里面的,如果想要访问父类的话,那么我们就是需要指定的访问。
class person
{
public:
protected:
string _name;
string _id = "张三";
};
class student : private person
{
public:
void fun()
{
cout << _id << endl;
}
protected:
string _id = "李四";
};
int main()
{
student s1;
s1.fun();
}
那么我们这时候会打印出什么呢?显然是我们的 "李四"。
那么如果我们想要访问我们的 person 里面的 _id 呢? 我们只需要指定就好了。
class person
{
public:
protected:
string _name;
string _id = "张三";
};
class student : private person
{
public:
void fun()
{
cout << person::_id << endl;
}
protected:
string _id = "李四";
};
int main()
{
student s1;
s1.fun();
}
2. 函数的隐藏(重定义)
我们看下面的这一段代码~
class A
{
public:
void fun()
{
cout << "class A" << endl;
}
};
class AA : public A
{
void fun(int i)
{
cout << "class AA : public A" << endl;
}
};
这一段代码里面的 fun 函数之间是什么关系呢?
- A:隐藏(重定义)
- B:重载
- C:编译报错
我们这个答案是 A 为什么呢?
我们的重载是在同一个类域中,但是我们的 父类与子类是有不同的类域的,所以我们的 fun 函数构成重写。
四. 派生类中的默认函数
1. 构造函数
在我们的派生类中,我们的构造函数要怎么写呢?
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name, const string& id)
:_name(name)
,_id(id)
{}
protected:
string _id;
};
我们可以这样写吗?
这里说明是不可以的,因为在派生类中不能之间初始化基类中的成员,需要调用构造函数来初始化基类中的成员,如果没有显示的调用基类的构造函数,那么会自动调用基类中的默认构造函数,如果没有默认构造函数的话,就会报错。
下面我们不显示的调用,让他自己调用默认构造函数。
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:_id(id)
{}
protected:
string _id;
};
int main()
{
student s1;
return 0;
}
下面我们显示的调用。
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
protected:
string _id;
};
int main()
{
student s1;
return 0;
}
2. 拷贝构造
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
person(const person& per)
:_name(per._name)
{
}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
student(const student& s)
:_name(s._name)
,_id(s._id)
{
}
protected:
string _id;
};
上面这样可以吗? 当然是不可以,我们刚才说了,如果要对基类进行初始化,就需要调用基类自己的构造函数,所以我们需要调用基类的构造函数。
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
person(const person& per)
:_name(per._name)
{
}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
student(const student& s)
:person(s)
,_id(s._id)
{
}
protected:
string _id;
};
int main()
{
student s1;
student s2(s1);
return 0;
}
我们上面调用我们的父类的拷贝构造的时候我们就是直接传入我们的子类的对象,这个是可以的,因为我们的子类转父类是语法天然支持的,转化的过程中发生的切片。
3. 赋值重载
class person
{
public:
person(const string& name = "张三")
:_name(name)
{}
person(const person& per)
:_name(per._name)
{
}
person& operator=(const person& per)
{
if (this != &per)
{
_name = per._name;
}
return *this;
}
protected:
string _name;
};
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
student(const student& s)
:person(s)
,_id(s._id)
{
}
student& operator=(const student& per)
{
if (this != &per)
{
operator=(per);
}
return *this;
}
protected:
string _id;
};
int main()
{
student s1;
student s2;
s2 = s1;
return 0;
}
我们这样写可以吗?我们看一下结果。
我们的代码直接崩溃了,为什么呢?
我们看一下调用堆栈。
我们看到我们是一直在调用我们自己的这个赋值,这是因为我们的 operator= 识别成我们自己的 operator= 函数了,所以我们需要指定的调用。
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
student(const student& s)
:person(s)
,_id(s._id)
{
}
student& operator=(const student& per)
{
if (this != &per)
{
person::operator=(per);
}
return *this;
}
protected:
string _id;
};
我们只需要指定调用就好了。
4. 析构函数
class student : public person
{
public:
student(const string& name = "李四", const string& id = "12345")
:person(name)
,_id(id)
{}
student(const student& s)
:person(s)
,_id(s._id)
{
}
student& operator=(const student& per)
{
if (this != &per)
{
person::operator=(per);
}
return *this;
}
~student()
{
~person();
}
protected:
string _id;
};
我们这样可以吗? 不可以,为什么呢?
因为在我们的C++中,由于多态的原因(这里先不说是什么原因,到了多态自然会说),我们的析构函数都会被处理成统一的名称 " destructor " ,所以我们直接这样调用也是不可以的,需要指定的调用。
~student()
{
person::~person();
}
那处理成这样可以吗?我们可以试一下
int main()
{
student s1;
student s2;
s2 = s1;
return 0;
}
我们现在有两个对象,按理说我们会调用两次基类的析构函数。
但是实际上,我们调用了四次,为什么呢?
因为我们的派生类在定义中,我们事有一个基类的,所以我们会先定义基类的内容,然后调用派生类中的内容,既然是先定义基类的,所以我们也是后释放基类的内容,那么我们的C++并不相信我们,所以析构函数的调用就是不需要我们来调用,而是编译器自动帮我们调用,如果我们显示的调用的话,那么就会多调用一次,如果该析构函数需要释放资源的话,我们显示调用了析构函数就会多析构一次,导致程序崩溃,所以我们不用显示的调用。
~student()
{
}
int main()
{
student s1;
student s2;
s2 = s1;
return 0;
}
五. 多继承
1. 多继承语法
在特定的时候,有时候是需要一个类继承其他的多个类的。
class A
{
public:
int _a;
};
class B
{
public:
int _b;
};
class C : public A, B
{
public:
int _c;
};
现在有一个 C 要继承A 和 B,那么就是将继承的类用逗号隔开,就可以达到多继承。
2. 菱形继承
在多继承中,有一个问题就是菱形继承,什么是菱形继承呢?
就像上图一样,B 和 C 继承了 A,然后 D 又继承了B 和 C,这时候D 里面就又两份 A 的内容,但是并不是只有这种情况会造成数据菱形继承,菱形继承就是直接继承或者间接继承基类,而这两个基类又继承同一个类,所以这时候就会造成数据冗余和二义性。
看下面的代码。
class person
{
public:
protected:
string _name;
};
class student : public person
{
public:
protected:
string scholl_id;
};
class teacher : public person
{
public:
protected:
string work_id;
};
class assistant : public student, teacher
{
protected:
};
那么这时候我们的 assistant 中就有两个 person 这时候我们的数据就又冗余和二义性
int main()
{
assistant assis;
assis._name = "张三";
return 0;
}
如果这样访问的话,那么即不知道是哪一个 _name,是 teacher 里面还是 student 中的 _name,所以造成了二义性,那么这个问题怎么解决呢?我们可以指定的访问。
int main()
{
assistant assis;
assis.student::_name = "张三";
return 0;
}
但是这个这样只是解决了二义性的问题,还是没有解决数据冗余的问题,所以这里还是有一个最终的解决办法,那就是加 virtual 关键字,这个关键字要加到中间继承的类,我们看语法。
class person
{
public:
public:
string _name;
};
class student : virtual public person
{
public:
protected:
string scholl_id;
};
class teacher : virtual public person
{
public:
protected:
string work_id;
};
class assistant : public student, teacher
{
protected:
};
我们最后的 virtual 就是加到了腰的位置
3. 虚基表
通过我们的虚继承后,我们的 B 和 C 中相同的变量当然是不可以放在C 中或者是B 中的,所以我们为了不破坏公平,我们就重新找一个位置来存放相同的变量,所以我们就只存一份,而我们为了父类的指针指针或者引用也可以更快的找到该变量,我们就需要对虚继承后的类的对象的指针或者引用指向派生类的变量里面存储的值也做一些修改,也就是在对应的位置存储的是一个指向虚基表的一个指针,而虚基表里面存储的就是该位置和存储该变量的偏移量,这里也就不多说了,这里了解一下即可。