文章目录
- 继承
- 概念
- 定义格式
- 单继承和多继承
- 继承权限
- 派生
- 派生类的构成
- 派生类的默认成员函数
- ①构造函数
- ②拷贝构造函数
- ③赋值运算符重载函数
- ④析构函数
- 派生类的特殊成员函数
- ①友元函数
- ②静态函数
- 派生类的内存大小
- 派生类和基类的关系
- 复杂的菱形继承及菱形虚继承
继承是面向对象三大特性之一(封装、继承、多态),所有的面向对象语言都具备这三个基本特征,那么什么是继承呢?
如上图,假设我们有一个动物类
class Animal {
public:int age;void eat() {std::cout << "吃东西!" << std::endl;}
};
又想写一个狗类,它也有年龄,也会吃,除此之外还有种类
class Dog {
public:const char* kind;int age;void eat() {std::cout << "吃东西!" << std::endl;}
};
我们发现有重复的代码,如果我们写猫类、鸟类等都要再写一遍,这样很麻烦
那么有没有一种方法能提高代码的复用性,不需要再写一遍就能达到同样的效果呢?
继承
-> 我们让Dog继承Animal
#include<iostream>
class Animal {
public:int age;void eat() {std::cout << "吃东西!" << std::endl;}
};
class Dog :public Animal {
public:const char* kind;
};
int main() {Dog dog;dog.kind = "柯基";dog.age = 3;dog.eat();
}
没有写那部分重复代码却也能给age赋值,调用eat()方法
也就是说,Dog继承了Animal,Animal所拥有的age和eat()它也就有了,就像父亲遗传给儿子一样
而且,狗本来就属于动物,我们让其继承动物也是符合思维的
概念
继承的本质就是——复用代码
继承后,父类的Animal的成员(成员函数+成员变量)都会编程子类的一部分。上面代码体现出了Dog复用了Animal。
定义格式
- Animal就叫做【父类或者基类】
- Dog就叫做【子类或者派生类】
格式为:子类:继承方式 父类
,如class Dog :public Animal
就表示Dog继承了Animal,继承方式是共有。
单继承和多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
class A {
};
class B :public A {//单继承
};
class D {
};
class E {
};
class F :public D, public E {//多继承
};
继承权限
我们看到上面的继承有"public",这其实是继承方式,继承方式就是类中的三种访问属性:
此时我们发现——访问权限有三种,继承权限有三种!
然而!!!每种访问属性通过继承方式之后在派生类中可能会有新的属性——
- 基类私有成员,不管用什么方式继承,都不能被访问
- 其他的成员访问属性和继承方式,两者看谁更严格就按严格的来:public<protected<private
根据排列组合,可以列出以下多种搭配方案
父类成员 / 继承权限 | public | protected | private |
---|---|---|---|
父类的 public 成员 | 外部可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类的 protected 成员 | 外部不可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类的 private 成员 | 都不可见 | 都不可见 | 都不可见 |
注:所谓的“外部”就是子类对象
总结
- 无论是哪种继承方式,父类中的
private
成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,只有最终权限为public
时,外部才能访问- 在实际运用中一般使用都是public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
派生
派生类的构成
- 派生类会继承除基类的构造析构函数之外的所有成员变量和成员函数
- 可以在派生类中添加新的成员,通过派生类对象来调用
- 如果派生类中添加的成员名和基类成员名相同,那么派生类会隐藏基类的成员,可以通过
.基类名::基类成员名
来访问。如果是继承的多个基类,而多个基类中也有同名的,也是通过此种方式调用同名的成员。子类对象直接访问同名成员,访问的是子类对象自己的同名成员。
【代码示例】
#include<iostream>
class Animal {
public:int age;void eat() {std::cout << "吃东西!" << std::endl;}
};
class Dog :public Animal {
public:const char* kind;void eat() {std::cout << "啃骨头!" << std::endl;}
};
int main() {Dog dog;dog.eat();//调用同名成员时隐藏基类成员dog.Animal::eat();//访问基类中同名成员
}
/*输出:啃骨头!
吃东西!
*/
派生类的默认成员函数
派生类也是类,同样会生成六个默认成员函数(用户未定义的情况下)
不同于单一的类,子类是在父类的基础之上创建的,因此它在进行相关操作时,需要为父类进行考虑。
派生类默认成员函数的调用规则如下:
①构造函数
- 派生类构造函数默认先调用基类的默认构造函数,如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的有参构造函数。例如:
#include <iostream>// 基类
class Base {
public:int base_data;Base(int x) : base_data(x) {std::cout << "Base constructor called" << std::endl;}
};// 派生类
class Derived : public Base {
public:int derived_data;// 派生类构造函数,需在初始化列表显式调用基类构造函数(因为基类没有默认构造函数)Derived(int x, int y) : Base(x), derived_data(y) {std::cout << "Derived constructor called" << std::endl;}
};int main() {Derived d(5, 10);return 0;
}
- 成员对象的构造函数在基类构造函数之后、派生类自身构造函数之前被调用。
②拷贝构造函数
- 会先调用基类的拷贝构造函数来处理基类部分的拷贝,如果没有自定义,编译器生成默认的拷贝构造函数完成此操作。
- 再调用成员对象的拷贝构造函数拷贝成员对象,最后执行派生类自身的拷贝操作。
#include <iostream>
#include <cstring>// 基类
class Base {
public:int base_data;Base(int x) : base_data(x) {}// 基类的拷贝构造函数Base(const Base& other) : base_data(other.base_data) {std::cout << "Base copy constructor called" << std::endl;}
};// 派生类
class Derived : public Base {
public:int derived_data;Derived(int x, int y) : Base(x), derived_data(y) {}// 派生类的拷贝构造函数Derived(const Derived& other) : Base(other), derived_data(other.derived_data) {std::cout << "Derived copy constructor called" << std::endl;}
};int main() {Derived d1(5, 10);Derived d2 = d1; // 调用拷贝构造函数return 0;
}
③赋值运算符重载函数
- 若没有自定义,编译器生成的默认赋值运算符重载函数会先调用基类的赋值运算符重载函数来处理基类部分。
- 再处理成员对象的赋值,最后处理派生类自身的赋值操作。
#include <iostream>// 基类
class Base {
public:int base_data;Base(int x) : base_data(x) {}Base& operator=(const Base& other) {base_data = other.base_data;std::cout << "Base assignment operator called" << std::endl;return *this;}
};// 派生类
class Derived : public Base {
public:int derived_data;Derived(int x, int y) : Base(x), derived_data(y) {}Derived& operator=(const Derived& other) {if (this!= &other) {// 先调用基类的赋值运算符重载Base::operator=(other);derived_data = other.derived_data;std::cout << "Derived assignment operator called" << std::endl;}return *this;}
};int main() {Derived d1(5, 10);Derived d2(8, 12);d1 = d2; // 调用赋值运算符重载函数return 0;
}
④析构函数
- 与构造函数相反,先执行派生类的析构函数,再调用基类的析构函数。并且析构函数不能被继承,派生类会生成自己的析构函数,编译器自动在其中添加对基类析构函数的调用代码。
#include <iostream>// 基类
class Base {
public:int base_data;Base(int x) : base_data(x) {}~Base() {std::cout << "Base destructor called" << std::endl;}
};// 派生类
class Derived : public Base {
public:int derived_data;Derived(int x, int y) : Base(x), derived_data(y) {}~Derived() {std::cout << "Derived destructor called" << std::endl;}
};int main() {{Derived d(5, 10);} // 离开作用域,先调用派生类析构函数,再调用基类析构函数return 0;
}
派生类的构造析构顺序
- 派生类对象在实例化的时候是会调用基类的构造函数的,先基类后派生类(先有父亲后有儿子),释放就是先儿子后父亲,因为栈结构(先进后出)
-
如果是多继承,其与单继承中构造顺序一致;区别在于,在构造基类时有多个基类,那么会按照基类的继承声明顺序来依次调用基类的构造函数,然后构造子对象,最后构造自己
-
在写继承的时候,要确保基类有可以调用的构造函数
-
带参构造:在构造过程中,如果基类或子对象需要参数来进行构造,就需要在派生类中通过成员初始化列表来构造
示例1:
#include<iostream>
class A {
public:A() {std::cout << "调用A的无参构造" << std::endl;}A(int a) {std::cout << "调用A的有参构造" << std::endl;}~A() {std::cout << "调用A的析构" << std::endl;}
};
class B :public A {
public:B() {std::cout << "调用B的无参构造" << std::endl;}B(int b) {std::cout << "调用B的有参构造" << std::endl;}~B() {std::cout << "调用B的析构" << std::endl;}
};
int main() {A a1;A a2(3);B b1;B b2(5);
}/*运行结果:调用A的无参构造
调用A的有参构造
调用A的无参构造
调用B的无参构造
调用A的无参构造
调用B的有参构造
调用B的析构
调用A的析构
调用B的析构
调用A的析构
调用A的析构
调用A的析构*/
注意:根据析构函数调用规则:先构造的后析构,后构造的先析构
示例2:
#include<iostream>
class A {
public:A(int a) {std::cout << a << std::endl;}
};
class B :public A {
public:B():A(1){}//在初始化列表中调用父类的带参构造//不这样写直接实例化会报错
};
int main() {B b1;
}
//输出:1
- 派生类的默认拷贝构造会自动调用基类的拷贝构造——完成基类的拷贝初始化。
【代码示例】
class Student : public Person
{
public:Student(long long st_id = 111): _st_id(st_id), Person("xas", 18){std::cout << "Student()" << std::endl;}Student(const Student& s): _st_id(s._st_id){std::cout << "Student(const Student& s)" << std::endl;}
protected:long long _st_id;
};
int main()
{Student st1;Student st2(st1);return 0;
}
/*输出:Person()
Student()
Person() //系统自动调用了基类的构造
Student(const Student& s)
~Person()
~Person()
*/
- 派生类的默认赋值运算符重载会自动调用基类的赋值运算符重载
class Student : public Person
{
public:Student(long long st_id = 111): _st_id(st_id), Person("xas", 18){std::cout << "Student()" << std::endl;}Student& operator= (const Student& s){std::cout << "operator= (const Student& s)" << std::endl;if (this != &s){_st_id = s._st_id;}return *this;}
protected:long long _st_id;
};
int main()
{Student st1, st2;st1 = st2;return 0;
}
/*输出:Person()
Student()
Person()
Student()
operator= (const Student& s)
~Person()
~Person()
*/
-
派生类的operator=必须要调用基类的operator=完成基类的复制。
-
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
派生类的特殊成员函数
①友元函数
在C++中,继承和友元函数有以下特点和关系:
- 友元函数不具有继承性
- 基类的友元函数不会自动成为派生类的友元函数。例如:
class Base {friend void baseFriendFunction(Base& b);//...
};class Derived : public Base {// baseFriendFunction不是Derived的友元函数
};void baseFriendFunction(Base& b) {// 可以访问Base类的私有成员
}
- 友元函数访问权限与类层次相关
- 友元函数可以访问类的私有和保护成员,但在继承体系中,友元函数只能访问其声明为友元的类层次结构内的成员。例如,基类的友元函数不能直接访问派生类的新增私有成员,但能通过派生类对象访问基类部分的私有成员(如果派生类是公有继承)。
派生类友元函数
- 派生类可以定义自己的友元函数,这些友元函数可以访问派生类的私有和保护成员,但不能直接访问基类的私有成员(除非通过派生类对象访问基类的公有或保护接口)。例如:
class Derived : public Base {friend void derivedFriendFunction(Derived& d);//...
};void derivedFriendFunction(Derived& d) {// 可以访问Derived类的私有成员,通过公有或保护接口访问Base类相关成员
}
②静态函数
派生类的静态函数具有以下特点:
- 继承特性
- 派生类会继承基类的静态函数,但这只是一种逻辑上的继承。因为静态函数是属于类本身而非类对象,所以派生类只是共享了基类中静态函数的定义和功能。例如:
class Base {
public:static void baseStaticFunction() {std::cout << "This is Base static function." << std::endl;}
};class Derived : public Base {};int main() {// 通过派生类调用基类的静态函数Derived::baseStaticFunction();return 0;
}
- 函数隐藏
- 如果派生类定义了与基类同名的静态函数,那么基类的静态函数会被隐藏。这意味着通过派生类对象或类名无法直接访问被隐藏的基类静态函数,除非使用作用域解析运算符显式指定基类。例如:
class Base {
public:static void staticFunction() {std::cout << "Base static function" << std::endl;}
};class Derived : public Base {
public:static void staticFunction() {std::cout << "Derived static function" << std::endl;}
};int main() {// 以下代码会调用派生类的静态函数,基类的同名静态函数被隐藏Derived::staticFunction();// 显式调用基类的静态函数Base::staticFunction();return 0;
}
- 内存分配
- 派生类的静态函数和基类的静态函数一样,在内存中只有一份副本。无论创建多少个派生类对象,静态函数都不会被复制,它在程序的整个生命周期内都存在于静态存储区。
派生类的内存大小
- 派生类的内存大小=所有父类内存大小之和+本身新增的成员内存大小
- 派生类的内存中,基类的内存在最低位,派生类的内存在最高位
派生类和基类的关系
子类不包含父类,而是子类中有父类的所有数据成员和函数成员(除构造析构)。
派生类是基类对象,但是基类不是派生类对象;派生类可以赋值给基类,而基类不能给派生类赋值。
1.派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
【代码示例】
#include<iostream>
class Animal {
public:int age;void eat() {std::cout << "吃东西!" << std::endl;}
};
class Dog :public Animal {
public:const char* kind;void eat() {std::cout << "啃骨头!" << std::endl;}
};
int main() {Dog dog;dog.eat();dog.Animal::eat();dog.Animal::age = 10;//子类给父类赋值Animal* dog1 = new Dog;//父类指针指向子类对象,子类是父类对象Dog* animal1 = new Animal;//报错:Animal类型的值不能用于初始化Dog类型的实体
}
复杂的菱形继承及菱形虚继承
菱形继承:菱形继承是多继承的一种特殊情况。
类B,C分别继承A;类D继承类B,C
这样继承会导致类D继承两份类A的成员,在类D对象,想要访问类A的成员的时候,会导致访问不明确。因为类B,C各自继承类A的成员
解决方法
(1)通过类名::成员名
来确定调用哪个成员(不常用)
(2)通过虚继承,在继承方式的前面加上关键字virtual,虚继承之后会使在虚继承的类中多个指针(内存就会变大),但是在最后的D类,不会再继承两份A的成员了
示例1:
#include<iostream>
class A {int a = 1;
};
class B :public A {int c = 2;
};
class C :public A {int d = 3;
};
class D :public B, public C {int e = 4;
};
int main() {std::cout << sizeof(A) << std::endl;//4std::cout << sizeof(B) << std::endl;//8std::cout << sizeof(C) << std::endl;//8std::cout << sizeof(D) << std::endl;//20
}
示例2:
#include<iostream>
class A {int a = 1;
};
class B :virtual public A {int c = 2;
};
class C :virtual public A {int d = 3;
};
class D :public B, public C {int e = 4;
};
int main() {std::cout << sizeof(A) << std::endl;//32位:4 64位:4std::cout << sizeof(B) << std::endl;//32位:12 64位:24std::cout << sizeof(C) << std::endl;//32位:12 64位:24std::cout << sizeof(D) << std::endl;//32位:24 64位:48
}
总结
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java。