文章目录

  • 一、包含关系(has_a关系)
    • 1.1 包含关系的定义
    • 1.2 包含关系的内存布局
  • 二、友元关系(use_a关系)
    • 2.1 友元函数
    • 2.2 友元类
    • 2.3 C++中分文件编程的实现
  • 三、继承关系(is_a关系)
    • 3.1 继承的定义
    • 3.2 继承的意义
    • 3.3 继承时的名字
    • 3.4 继承关系在C++中的语法形式
    • 3.5 继承方式
    • 3.6 父类没有默认构造时的解决方法
    • 3.7 父子类之间出现同名属性或函数的解决办法
    • 3.8 继承中的拷贝构造函数与拷贝赋值函数
    • 3.9 单继承关系中的内存布局
    • 3.10 C++中继承关系下的类型兼容规则
  • 四、多继承、棱形继承、虚继承
    • 4.1 多继承的含义
    • 4.2 多继承语法形式
    • 4.3 多重继承带来的问题及解决方法
    • 4.4 多继承关系中的内存布局
    • 4.5 棱形继承与虚继承
    • 4.6 棱形继承带来的问题及解决方法

一、包含关系(has_a关系)

1.1 包含关系的定义

包含关系:即一个对象由多个对象共同组成。也被称之为has_a关系,组合关系,复合关系。

代码示例:

#include using namespace std;class Man{public:Man(){cout << "Man的构造" << endl;}~Man(){cout << "Man的析构" << endl;}};class Desk{public:Desk(){cout << "Desk的构造" << endl;}~Desk(){cout << "Desk的析构" << endl;}};class Chair{public:Chair(){cout << "Chair的构造" << endl;}~Chair(){cout << "Chair的析构" << endl;}};class Room{private:Chair chair;Desk desk;Man man;public:Room(){cout << "Room的构造" << endl;}~Room(){cout << "Room的析构" << endl;}};int main(){Room room;return 0;}

结果展示:

总结:
通过结果我们可知
构造顺序:先构造类中的成员对象,最后再构造最外层的包含对象,构造顺序为类中成员对象的声明顺序相同。
析构顺序:则相反。

1.2 包含关系的内存布局

我们来打印他们的this指针看一看

通过上图我们可以发现最外层的包含对象Room和第一个声明的成员对象Chair的地址是一样的,其他因为是空类依次加一。
由此我们可以推断出包含关系的内存布局如下图所示:

二、友元关系(use_a关系)

友元关系:就是用来描述,类与函数之间,类与类之间的一种亲密关系。
功能:可以访问朋友类中的私有成员。
友元关系的两种形式:友元函数,友元类。

2.1 友元函数

声明一个函数是类的友元函数,那么在这个函数里就可以访问类的私有成员了。

代码举例:

#include using namespace std;class Man{private:int money;public:Man(int _money):money(_money){}//友元函数的声明friend void girlfriend(Man& man);};void girlfriend(Man& man){man.money=man.money-100;cout << man.money <<endl;}int main(){Man man(1000);girlfriend(man);return 0;}

结果展示:

总结:
由结果可知 当友元函数中,有被use_a的类时,那么这个类的对象将不再受到访问权限的限制。

2.2 友元类

如果声明B类是A类的友元类,那么在B类中的所有成员函数,都可以访问A类的私有成员。

代码示例:

#include using namespace std;class Man{private:string name;public:Man(string _name):name(_name){}friend class girlfriend;};class girlfriend{private:string name;public:girlfriend(string _name):name(_name){}void show(Man& man){cout << man.name << "正在陪" << name << endl;}};int main(){Man man("小明");girlfriend girl("小红");girl.show(man);return 0;}

结果展示:

总结:
由结果可知:在A类中声明friend class B,那就是B是A的友元类。如果B类中有定义A的对象时,A类的对象将不受A类的访问权限的限制。
注意:

  1. 友元关系不具有交换性:B是A的友元,但A不一定是B的。
  2. 友元关系不具有传递性:A是B的友元,B是C的友元,A不一定是C的友元。
  3. 友元关系不能被继承:父类的朋友不一定是子类的朋友。
  4. 友元关系破坏了类的封装性,让访问控制权限没有意义了,在实际开发中,不要过分依赖友元。

2.3 C++中分文件编程的实现

分文件编程实现boy与girl的相互友元
boy.h

#ifndef BOY_H#define BOY_H#include using namespace std;class Girl;class Boy{private:string name;int age;public:Boy(string name,int age);void show();void show(Girl& girl);friend class Girl;};#endif // BOY_H

girl.h

#ifndef GIRL_H#define GIRL_H#include using namespace std;class Boy;class Girl{private:string name;int age;public:Girl(string name,int age);void show();void show(Boy& boy);friend class Boy;};#endif // GIRL_H

boy.cpp

#include "boy.h"#include "girl.h"Boy::Boy(string name, int age){this->name=name;this->age=age;}void Boy::show(){cout << "正在学习C++" << endl;}void Boy::show(Girl &girl){cout << girl.name << "正在陪" << name << "学习" << endl;}

girl.cpp

#include "girl.h"#include "boy.h"Girl::Girl(string name, int age){this->name=name;this->age=age;}void Girl::show(){cout << "正在逛商场" << endl;}void Girl::show(Boy &boy){cout << boy.name << "正在陪" << name << "逛商场" << endl;}

main.cpp

#include #include "boy.h"#include "girl.h"using namespace std;int main(){Boy boy("小明",18);boy.show();Girl girl("小红",18);girl.show();cout << "----------------" << endl;boy.show(girl);girl.show(boy);return 0;}

结果展示:

三、继承关系(is_a关系)

继承关系:也就是常说的父子关系,或派生关系。统称为is_a关系。
面向对象的三大特征(封装、继承、多态)
其中描述类与类之间关系的特征,就是继承。

3.1 继承的定义

基于一个已有的类,去重新定义一个新的类,这种方式就叫做继承。
简单来说:就是老子的东西,都会被儿子继承下来。

3.2 继承的意义

1.代码复用性与高拓展性;
父类的所有属性都被子类继承了下来,所以子类中就可以不用再写父类中的属性与方法了。这样就提高了代码的复用性。同时子类中我们还可以添加一些子类独有的新特性。这样就提高了代码的拓展性。
2.继承是实现多态的必要条件;

3.3 继承时的名字

一个类B 继承自类A 时,我们一般称呼:
A类:父类,基类
B类:子类,派生类
B继承自A,A派生了B。

3.4 继承关系在C++中的语法形式

单继承语法:

class + 子类类名 : 继承方式 + 父类的类名{//单继承类体};

继承:继承了父类中的所有的属性与行为。(类中的变量及方法)
继承的是父类的属性,与方法的访问权。
静态属性,也是被子类继承,继承的也是访问权。

3.5 继承方式

继承方式也有三种:public protected private

实际使用过程中,一般都用public方式继承。
如果不写继承方式,默认都是private方式继承。

代码示例:

#include using namespace std;class Transport{public:string name;public:Transport(){name="交通工具";cout << "Transport的构造" << endl;}~Transport(){cout << "Transport的析构" << endl;}void run(){cout << "正在行驶" << endl;}};class Car:public Transport{public:Car(){cout << "Car的构造" << endl;}~Car(){cout << "Car的析构" << endl;}void show(){this->run();cout << this->name << endl;}void what(){cout << "我是小汽车" << endl;}};class Bike : public Transport{public:void show(){cout << "我是自行车" << endl;}};int main(){Car car;cout << car.name << endl;car.run();car.show();car.what();cout << "------------" << endl;Bike bike;cout << bike.name << endl;bike.run();bike.show();return 0;}

结果展示:

总结:
以上代码体现了复用性与拓展性。
子类会继承父类的所有成员,包括私有成员,只不过私有成员没法直接在子类中访问需要通过父类提供的publicprotected的函数接口来访问。

3.6 父类没有默认构造时的解决方法

#include using namespace std;class A{public:A(int a){cout << "A的构造" << endl;cout << this << endl;}~A(){cout << "A的析构" << endl;}void show(){cout << "学习C++" << endl;}};class B:public A{public:B():A(1){cout << "B的构造" << endl;cout << this << endl;}~B(){cout << "B的析构" << endl;}void name(){cout << "夜猫徐" << endl;}};int main(){B b;b.show();b.name();return 0;}

结果展示:

总结:
通过以上两个代码的结果我们可以知道:

  1. 父类的构造函数不会被子类继承(父子类的构造函数不一样)。
  2. 需要在子类的构造函数的初始化表中显性的调用父类的构造函数,完成对从父类中继承过来的成员的初始化。
  3. 如果没有在子类的构造函数的初始化表中显性的调用父类的构造函数,默认会调用父类的无参构造函数来完成对从父类中继承过来的成员的初始化,这时,需要父类中有无参构造,如果父类只有有参构造,会报错。
  4. 继承中构造函数调用的顺序:先调用父类的构造函数再调用子类的构造函数。
  5. 父类的析构函数不会被子类继承(父子类的析构函数不一样)。
  6. 不管子类中是否显性调用析构函数,父类的析构函数都会被调用来完成对从父类中继承过来的成员的善后工作。
  7. 子类的析构函数里不需要调用父类的析构函数。
  8. 继承中析构函数调用的顺序:先调用子类的析构函数再调用父类的析构函数.
  9. 子类的起始地址和父类的起始地址是同一个地址。

3.7 父子类之间出现同名属性或函数的解决办法

当父类中与子类中有同名属性或函数时,子类访问时,父类中同名函数或属性将自动隐藏在父类的类域之中,如果要访问父类的类域中的属性或方法,请使用::域名访问符的形式进行访问 。

代码示例:

#include using namespace std;class Car{public:int weight=1000;public:Car(){cout << "Car的构造" << endl;}~Car(){cout << "Car的析构" << endl;}void show(){cout << "车正在行驶" << endl;}};class BMW:public Car{public:int weight=2000;public:BMW(){cout << "BMW的构造" << endl;}~BMW(){cout << "BMW的析构" << endl;}void show(){cout << "BMW正在行驶" << endl;}};int main(){BMW b;cout << b.weight << endl;b.show();cout << "-------------" << endl;cout << b.Car::weight << endl;b.Car::show();return 0;}

结果展示:

总结:

  1. 当父子类中出现同名函数时,访问起来也不会有冲突,即使函数形参列表不同,他们两个也不构成重载关系,原因是这两个函数所在的空间不一样,父子类中成员变量重名也不会冲突。
  2. 对于同名成员的访问:
    如果不加任何修饰,默认是通过this 访问自己的成员。
    如果想访问父类的成员,需要加父类名::
  3. 类内访问时格式:
    父类名::父类成员变量名
    父类名::父类成员函数名(实参表)
  4. 类外访问时格式:
    子类对象名.父类名::父类成员变量名
    子类对象名.父类名::父类成员函数名(实参表)

3.8 继承中的拷贝构造函数与拷贝赋值函数

继承中的拷贝构造函数

  1. 如果子类中没有显性的给定拷贝构造函数,编译器会默认提供一个拷贝构造函数,并且提供的这个拷贝构造函数默认会调用父类的拷贝构造函数,来完成对从父类中继承过来的成员的初始化。
  2. 如果子类中显性的给定了拷贝构造函数,需要在子类的拷贝构造函数的初始化列表中,显性的调用父类的拷贝构造函数,来完成对从父类中继承过来的成员的初始化,如果没有在子类拷贝构造函数的初始化表中显性调用父类的拷贝构造函数,默认会调用父类的无参构造函数来完成对从父类中继承过来的成员的初始化。
  3. 如果父子类中没有指针成员,使用默认的拷贝构造函数是没有问题的,如果有指针成员,就需要手动给定拷贝构造函数,并且在子类的拷贝构造函数中要显性的调用父类的拷贝构造函数。因为涉及深浅拷贝的问题。

继承中的拷贝赋值函数

  1. 如果子类中没有显性的给定拷贝赋值函数,编译器会默认提供一个拷贝赋值函数,并且提供的这个拷贝赋值函数默认会调用父类的拷贝赋值函数,来完成对从父类中继承过来的成员的赋值。
  2. 如果子类中显性的给定了拷贝赋值函数,需要在子类的拷贝赋值函数的函数体中,显性的调用父类的拷贝赋值函数,来完成对从父类中继承过来的成员的赋值,如果没有在子类拷贝赋值函数的函数体中显性调用父类的拷贝赋值函数,那么从父类中继承过来的成员的值保持不变。
  3. 如果父子类中没有指针成员,使用默认的拷贝赋值函数是没有问题的,如果有指针成员,就需要手动给定拷贝赋值函数,并且在子类的拷贝赋值函数中要显性的调用父类的拷贝赋值函数。因为涉及深浅拷贝的问题。

代码示例:

#include using namespace std;class Father{private:string name;int age;public:Father(){cout << "Father的无参构造" << endl;}Father(string name,int age){this->name=name;this->age=age;cout << "Father的构造" << endl;}~Father(){cout << "Father的析构" << endl;}Father(const Father& other){cout << "Father的拷贝构造" << endl;this->name=other.name;this->age=other.age;}Father& operator=(const Father& other){cout << "Father的=号运算符重载" << endl;if(this!=&other){this->name=other.name;this->age=other.age;}return *this;}void show(){cout << "姓名:" << name << " 年龄:" << age << endl;}};class Son:public Father{private:string book;public:Son(){cout << "Son的无参构造" << endl;}//不要在子类的构造函数的函数体中调用父类的构造函数,因为构造函数不允许手动调用的,所以要使用初始化列表Son(string _name,int _age,string _book):Father(_name,_age),book(_book){cout << "Son的有参构造" << endl;}~Son(){cout << "Son的析构" << endl;}//需要在子类的拷贝构造函数的初始化表中,显性调用父类的拷贝构造函数--父类的引用可以引用子类对象Son(const Son& other):Father(other),book(other.book){cout << "Son的拷贝构造" << endl;}Son& operator=(const Son& other){cout << "Son的=号运算符重载" << endl;if(this!=&other){//显性调用父类的拷贝赋值函数来完成对从父类中继承过来的成员的赋值Father::operator=(other);this->book=other.book;}return *this;}void show(){Father::show();cout << book << endl;}};int main(){Son s1("张三",18,"小说");s1.show();Son s2=s1;s2.show();Son s3("小红",17,"散文");s3=s1;s3.show();return 0;}

结果展示:

3.9 单继承关系中的内存布局

通过3.6的代码示例和结果可以发现,在内存的地址上父类和子类的起始地址是相同的。且在继承关系中,构造与析构的顺序是先构造父类,然后再构造子类,析构顺序则反之,与has_a关系是十分类似的。
而且在子类中还可以定义一些子类特有的属性和方法。所以我们可以推断出内存布局关系如下图所示:

3.10 C++中继承关系下的类型兼容规则

通过继承关系的内存布局,我们可以知道子类无非就是在父类的基础之上进行了新的特有属性或方法的拓展。
所以使用父类指针可以指向子类的实例,是天然安全的。无需进行转型。反之则不可能。

代码示例:

#include using namespace std;class Father{public:string name="父类";public:Father(){cout << "Father的无参构造" << endl;}~Father(){cout << "Father的析构" << endl;}void show(){cout << "姓名:" << name << endl;}};class Son:public Father{public:string book="名著";public:Son(){cout << "Son的无参构造" << endl;}Son(string _book):book(_book){cout << "Son的有参构造" << endl;}~Son(){cout << "Son的析构" << endl;}void show(){cout << book << endl;}};int main(){Father *pa=new Son();cout << pa->name << endl;pa->show();//父类的show((Son*)pa)->show();//子类的showcout << "----------------" << endl;//Son *pb=(Son*)new Father();//cout <book << endl;cout << "----------------" << endl;//pb->show();return 0;}


总结:

  1. 如果在确认子类空间已经被开辟的情况下,是可以进行强转,安全访问的。
  2. 在确认子类空间已经被开辟的情况下,子例的实例在内存中合法的,可以访问。
  3. Son *pb=(Son*)new Father();如果强转的话,就有可能访问了一个非法空间。(因为子类的类域比父类的类域要大)这种访问是不安全的。
  4. 当使用父类指针指向对象时,父类指针仅可以操作父类类域之中的与子类共有那一块类域空间中的属性与方法,而不能直接访问子类中特有属性与方法。
  5. 当使用父类指针指向子类对象,这个父类指针只能调用或访问父类类域之中的方法或属性,而不能直接调用子类中的属性与方法。当果想调用子类中的特有属性或方法时,则需要强转。这种强转可以使用C风格强转,也可以使用static_case(exp)C++中的静态转换也可以。

四、多继承、棱形继承、虚继承

4.1 多继承的含义

一个子类可以由多个直接基类共同派生,这种派生方式,就叫多重继承。
子类中会继承每个基类的成员。

4.2 多继承语法形式

多继承语法:

class + 子类类名: 继承方式1 + 父类类名1 , 继承方式2 + 父类类名2, 继承方式3 + 父类类名3 ....{//多继承类体}多继承一般情况下,会继承多个抽象类,一般抽象类中是没有属性。没有属性就是不带来子类代码膨胀问题。

4.3 多重继承带来的问题及解决方法

代码示例:

#include using namespace std;class A{public:int a;A(){cout << "A的构造" << endl;cout << this << endl;}~A(){cout << "A的析构" << endl;}void show(){cout << "AAA" << endl;}};class B{public:int a;B(){cout << "B的构造" << endl;cout << this << endl;}~B(){cout << "B的析构" << endl;}void show(){cout << "BBB" << endl;}};class C:public A,public B{public:C(){cout << "C的构造" << endl;cout << this << endl;}~C(){cout << "C的析构" << endl;}};int main(){cout << sizeof(C) << endl;C c;//c.a;//c.show();//出现二义性//通过类域访问符进行访问c.A::show();c.B::show();cout << "--------------" << endl;B *b=new C;cout << "b的地址" << b <<endl;return 0;}

多重继承时构造函数和析构函数调用顺序:
与继承时的声明有关。

结果展示:

总结:

  1. 多重继承的子类起始地址与第一个父类的起始地址是相同的。
  2. 如果是多重继承,父类指针指向子类对象,要注意使用的这根指针他所保存的地址并非子类的真的起始地址,而是根据所有父类继承继承顺序偏移后地地址。

4.4 多继承关系中的内存布局

4.5 棱形继承与虚继承


图片内的方框就是菱形继承。
菱形继承:一个类由多个基类共同派生,而这多个基类又有共同的基类。此时,在汇聚子类中就会有多份公共基类的成员,访问起来会有歧义。

虚继承:就是在公共基类生成中间子类时,继承方式前加上关键字 virtual
此时,这种继承方式就叫做虚继承。
虚继承时,公共基类的成员需要在汇聚子类中完成构造如果初始化表中没有显性调用A的构造,默认会调用A的无参构造

4.6 棱形继承带来的问题及解决方法

当出现棱形继承时,在最远端父类在发继承时,会出现父类被多次构造的问题。使virtual修饰继承方式,就是避免最远端父类被多次构造的。
virtual修饰的这个继承关系,也叫虚继承,此时的最远端父类,为所有直接继承的子类的共享一份类中的属性。那么这个最远端的父类,也被称之为虚基类
虚基类的直接继承类中都会被按插一根虚基类表指针,来指向这个虚基类表,这个虚基表中就保存了子类相较于虚基类的偏移量。虚基类表指针通过偏移地址找到已经被始化的基类属性。
内部机制:
当使用virtual修饰继承权后,继承类中,编译器就会默默安插了一根虚指针。这两个直接继承类中各有一根虚基表指针,指向一张共有的虚基表。这张虚基表中存在偏移量,通过偏移量就可以找到共有的那个属性。也就是说B 与 C 是共享了一分虚基类。所以A只需要构造一份,B与C就可以虚基表中的偏移找到A中的属性。


虚基表是属于类的,即创建对象前就是存在的,由于各类中各变量(类型)的确定,偏移量是固定好的,各类创建对象时,生成虚基表指针,计算后拿到地址。

代码示例:

#include using namespace std;class A{public:A(){cout << "A的构造" << endl;cout << this << endl;}~A(){cout << "A的析构" << endl;}};class B:virtual public A//虚继承的方式{public:B(){cout << "B的构造" << endl;cout << this << endl;}~B(){cout << "B的析构" << endl;}};class C:virtual public A{public:C(){cout << "C的构造" << endl;cout << this << endl;}~C(){cout << "C的析构" << endl;}};class D:public B,public C{public:D(){cout << "D的构造" << endl;cout << this << endl;}~D(){cout << "D的析构" << endl;}};int main(){D d;cout << "-----------" << endl;return 0;}

结果展示:
没使用虚继承时

使用虚继承后

总结:

  1. 棱形继承使用虚继承的代价就是在类中安插了一根虚基表指针,编译器在编译时,还要生成一张虚基类表,带来了额外的负担。一般情况下,在工程中要尽量避免棱形继承的出现。
  2. 在实际工作中,如果使用继承与使用包含关系都可以解决,首选包含关系。
    如果单继承与多继承都可以解决,首选单继承。
    如果不可避免要要使用多继承,则要多继承多个接口类(抽象类)。
    如果不可避免会发发生棱形继承,则要使用虚继承。