【C++】类和对象-继承(๑•̀ㅂ•́)و✧

1.1 概念

概念:继承 (inheritance) 就是(自顶向下)在一个已存在的类的基础上建立一个新的类。

作用:使代码可以复用。

它允许程序员在保持原有类特性的基础上进行扩展,增加功能。

一些角度:

从集合交叉(自底向上)的角度上说,可以解决【不同的类有很多相同的动作,代码重复写n变】的问题,只需要把它们共同的动作抽取出来成为父类,它们作为子类。

从层次(自顶向下)的角度上说,可以解决为父类【增加功能、扩展功能】的问题。

引用「我是小白呀」大大的说法:

已存在的类: 基类 (base class) 或父类 (father class)
新建立的类: 派生类 (derived class) 或子类 (son class)

在这里插入图片描述

一个新类从已有的类获得其已有特性, 称为类的继承.
通过继承, 一个新建的子类从已有的父类那里获得父类的特性
派生类继承了基类的所有数据成员和成员函数, 并可以对成员做必要的增加或调整
从已有的类 (父类) 产生一个新的子类, 称为类的派生.

类的继承是用已有的类来建立专用新类的编程技术
一个基类可以派生出多个派生类, 每一个派生类又可以作为基类再派生出新的派生类. 因此基类和派生类是相对而言的
派生类是基类的具体化, 而基类则是派生类的抽象

基类和父类的关系:一对多(多重继承),多对一(一个父类派生多个子类),一对一(一个父类派生一个子类)。

1.2 继承方式:单继承和多重继承

单继承 (single inheritance) 指一个派生类只从一个基类派生.

  • 单继承关系形成的层次是一个树形结构
  • 箭头表示继承的方向, 从派生类指向基类

在这里插入图片描述
1.2.2 多重继承

多重继承 (multiple inheritance) 是指一个派生类有两个或多个基类. 派生类不仅可以从一个基类派生, 也可以从多个基类派生.

 在这里插入图片描述

 ————————————————
版权声明:本文为CSDN博主「我是小白呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46274168/article/details/116592728

1.2 核心语法

1.2.1 子类的声明

class 派生类名:[继承方式]基类名{
    派生类新增加的成员
};

1.2.2 成员限定符,继承方式与继承结果(private,protected,public)

成员访问限定符 (默认 private):

  • public (公用的)
  • private (私有的)
  • protected (受保护的)

继承方式包括 (默认 private):

  • public (公用的)
  • private (私有的)
  • protected (受保护的)

表格(水管):

1.2.2 子类的构造函数(负责基类初始化!)

一般写法:

派生类构造函数名 (总形式参数表列) : 基类构造函数名 (实际参数表列) {
    派生类中新增数据成员初始化语句
}

子类构造函数的注意点:

  1. 需要负责基类初始化+子类初始化。而对于编译器来说,它先调用基类构造函数,再调用子类构造函数。
  2. 构造函数既可以类内定义,也可以类外定义。(见下文例子)

(1)类内定义:

class Student1 :public Student {
private:
    int age;
    string address;
public:
    Student1(int num, string n, char s, int a, string addr) : Student(num, n, s), age(a), address(addr) {};
    void show1();
};

类外定义:

class Student1 :public Student {
private:
    int age;
    string address;
public:
    Student1(int num, string n, char s, int a, string addr);
    void show1();
};

Student1::Student1(int num, string n, char s, int a, string addr) : Student(num, n, s) {
    age = a;
    address = addr;
}

PS:构造函数的写法还有很多种,大家不必拘泥,灵活使用,其他构造函数的方法可以参看:

【C++】结构体构造函数和实例化详解-打包解决你的所有困惑(●‘◡‘●)_Keroro军曹大人的博客-CSDN博客https://blog.csdn.net/icecreamTong/article/details/130627646?spm=1001.2014.3001.5501因为C++11结构体的构造函数参考了类的构造函数,所以两篇文章可以互通的。

(2)子类构造函数和析构函数的顺序:

调用顺序:构造时先调用基类后调用派生类,析构时相反

#include<iostream>
using namespace std;

class Base {
protected:
	Base() {
		cout << "Base Constructed." << endl;
	};
	~Base() {
		cout << "Base Destructed." << endl;
	};
};
class Derived :public Base {
public:
	Derived(char c) {
		cout << "Derived Constructed, value:" << c << endl;
	};
	~Derived() {
		cout << "Derived Destructed" << endl;
	};
};


int main() {
    Derived d('l');
    return 0;
}

运行结果:

总结:

  • 当不需要对派生类新增的成员函数进行任何初始化操作时, 派生类构造函数体可以为空
  • 基类没有构造函数或构造函数参数为空, 在派生类构造函数中可不写调用基类构造函数的语句, 盗用派生类构造函数时系统会自动调用基类的默认构造函数
  • 基类中定义了有参的构造函数, 派生类构造函数总必须写出基类的构造函数及其参数
  • 基类中既定义无参数的构造函数,又重载了有参数的构造函数, 派生类构造函数中可以调用带参的基类构造函数, 也可以不调用基类的构造函数

1.2.3 多次继承

一般写法:

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
//访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔

例子:

square.h

#include <iostream>
using namespace std;

// 基类 Shape
class Shape
{
public:
    Shape(int w, int h) :width(w), height(h) {};
    int getArea()
    {
        return (width * height);
    }
protected:
    int width;
    int height;
};

// 基类 PaintCost
class PaintCost
{
public:
    int getCost(int area)
    {
        return area * 70;
    }
};

// 派生类
class Rectangle : public Shape, public PaintCost
{
public:
    Rectangle(int w, int h) :Shape(w, h) {};
    int getCost1() {
        int area=getArea();
        return getCost(area);
    }
};

main.cpp

#include <iostream>
#include"square.h"

int main() {
    Rectangle Rect(5, 7);
    cout << "Total area: " << Rect.getArea() << endl;// 输出对象的面积
    cout << "Total paint cost: $" << Rect.getCost1() << endl;// 输出总花费
    return 0;
}

运行结果:

1.2.3 拷贝构造函数

1. 一个类可以设置多个构造函数。

2. 实例化时,一个对象可以选择且只能选择一种构造函数。因为类不允许对象多次初始化,哪怕选择不同的构造函数。

3.拷贝构造函数用到【切片】或【重载运算符=】的技术。

#include<string>
#include<iostream>
using namespace std;

class Person
{
public:

    string _name;
    int _age;
    char _sex;
    Person(string name, int age, char sex)// 父类没有默认构造函数
        :_name(name)
        , _age(age)
        , _sex(sex)
    {}
};

class Student :public Person
{
public:
    Student(string name = "wt", int age = 20, char sex = 'M',string id = "123")
        :Person(name, age, sex) // 显式调用父类的构造函数
        , _id(id)
    {}

    Student(const Student& s)
        :Person(s) // 调用父类的拷贝构造,使用子类对象切片拷贝给父类那部分成员
    {
        _id = s._id;
    }

    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            Person::operator=(s); // 调用父类的赋值,去赋值父类的那部分成员
            _id = s._id;
        }
    }

    ~Student()
    {
        //Person::~Person(); // 手动调用父类的析构函数时也需要加作用域
    }                      // 因为C++在类中将析构函数名统一处理成destructor
protected:                // 因为子类的析构函数胡自动调用父类的,所以自己不要显式去调用,不然会导致父类被析构两次
    string _id;
};

1.2.4 成员变量/函数的继承(包括同名变量/函数的问题)

首先了解下继承的作用域:

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同(不管参数和返回值是否相同)就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

————————————————
版权声明:本文为CSDN博主「风继续吹TT」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Edward_Asia/article/details/124035345

关于隐藏的解释:

  1. 1. 隐藏是指,在父子函数有同名函数的情况下,不执行同名的父函数,只执行同名的子函数。
  2. 2. 隐藏不是重载,因为不是在同一作用域。
  3. 3. 无论是在子类的成员函数里,或是在子类对象上,如果要使用同名的父函数,只需要加上表示父函数的作用域::即可。

以下是父子函数有同名函数的例子:

square.h

#include <iostream>
using namespace std;

// 基类 Shape
class Shape
{
public:
    Shape(int w, int h) :width(w), height(h) {};
    int getArea()
    {
        return (width * height);
    }
protected:
    int width;
    int height;
};

// 基类 PaintCost
class PaintCost
{
public:
    int extra_cost = 100;
    int getCost(int area)
    {
        cout << "using PaintCost's getCost" << endl;
        return area * 70;
    }
};

// 派生类
class Rectangle : public Shape, public PaintCost
{
public:
    int extra_cost = 50;
    Rectangle(int w, int h) :Shape(w, h) {};
    int getCost() {
        int area=getArea();
        cout << "extra_cost is:" << extra_cost<<endl;
        return PaintCost::getCost(area);
    }
};

main.cpp

#include <iostream>
#include"square.h"

int main() {
    Rectangle Rect(5, 7);
    cout << "Total area: " << Rect.getArea() << endl;// 输出对象的面积
    cout << "Total paint cost: $" << Rect.getCost() << endl;// 输出总花费
    cout << Rect.PaintCost::getCost(10) << endl;
    //[注]这个花费是输入新的面积10得到的花费,即10*70=700,不是成员变量变量相乘后得到的面积35的花费
    //这启示我们父类的函数可以单独拎出来调用,可以传入新的参数,而不必拘泥于仅把子类的成员变量作为参数
    cout << Rect.extra_cost<<endl;
    cout << Rect.PaintCost::extra_cost<<endl;
    return 0;
}

如果试图调用基类的同名函数getCost()并传入参数,编译器会报错。说明子类对象Rect.getCost()调用的同名函数是子类的同名函数。

 如果想要在子类对象中调用父类的同名函数,应加上父类的作用域:

1.3 细节

private 成员变量的作用:隐藏和封装。

show(); //显示private成员是多少,但不可改

类的继承和派生:解决的问题

【更像是公共特性和独有特性的关系】

*****************************************************************************************

1.子类的声明

class [childname] : [继承方式] [父类名]{};

class Student: public Person{};

*****************************************************************************************

2.子类的构造函数

(1)模板

(1.1)一般情况

class Student : public Person{

public:

Student([1*]):Person([2*]){

[3*]

}

};

1*外部传入的参数列表,要写参数类型

2*一一对应赋值给Person构造函数的参数列表,不要写参数类型

3*子类特殊参数的初始化

(1.2)含有子对象的派生类的构造函数:

派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)

{

    派生类新增成员的初始化语句

}

【附录】

说明:父类有参,一定要构造;派生只负责直接基类的初始化

1、当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。

2、若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。

3、如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。

————————————————

版权声明:本文为CSDN博主「白鳯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/weixin_44368437/article/details/117563488

*****************************************************************************************

3.子类的成员函数调用父类成员函数

class Student : public Person{

public:

void show(){

Person::show();  //虽然不可直接访问父类的private,但是可以通过父类的public的函数访问

}

};

作用:

【1、一些不想被外部访问的函数之间的调用,不想给外部留接口】

【2、内部调用,实现封装,不想麻烦外部】

4.子类对象的成员函数,调用父类成员函数

class Person {

private:

string name;

public:

void Getname(){

cout << name;

}

};

class Student :public Person {...};

//实例化

Student stu("While", "110103**********23", 12, 123);

stu.Getname();

【作用:减少代码重用】

*****************************************************

【想清楚传递性关系(窄口管道)】

(1)类:本类,子类,其他类

(2)对象:本类对象,子类对象,其他类对象 (3)普通函数

父类

private:

(1)类:本类

protected:

(1)类:本类,子类(继承的方式(作用域::  Person::Getname();))

public:

(1)类:本类,子类,其他类

(2)对象:本类对象,子类对象(继承的方式(作用域:: Person::PerShow();)),其他类对象(相当于普通函数的方式) (3)普通函数([父类] [父对象名]){ 父对象名.公共函数 }

|

|

向下传递

|

|

子类(有没有发现,子类的公有保护私有的函数的限定和父类是一样的)

private:

(1)类:本类

protected:

(1)类:本类,子类的子类

public:

(1)类:本类,子类的子类,其他类

(2)对象:本类对象,子类的子类对象(继承的方式(作用域:: Person::PerShow();)),其他类对象(相当于普通函数的方式) (3)普通函数([子类] [子类对象名]){ 子类对象名.公共函数 }

子类的子类还能不能访问父类,要看继承方式。

*********************************************************************************

多继承与虚基类

*********************************************************************************

多继承:一个子类继承多个父类

格式

-class [子类名]: [继承方式] [父类1],[继承方式] [父类2],[继承方式] [父类3] {};

虚基类

【解决的问题】

多继承的访问相同变量,究竟都要对父类的拷贝操作,还是各自再拷贝一份,各自操作的问题。

【场景】

类的结构关系:

-Parent {};

- Child1:Parent{};     Child2:Parent{};     Child3:Parent{};

-Grandchild:Child1,Child2,Child3{};

【详解】

如果不用虚基类办法,如果C1,C2,C3对Parent的同一成员都有操作,Grandchild在初始化构造C1,C2,C3 时,

除第一个继承的子类(C1),会拷贝Parent的成员到每一个子类(C1,C2,C3)里。并且第一个继承Parent的C1的a是Parent的a,不是拷贝,C2,C3的是拷贝。

如果用虚基类,Parent只存在一个拷贝(即只有一个数据成员a),C1,C2,C3中对对a的访问都是Parent的a而不是它在C2,C3的拷贝。

总结:多继承+对基类都有操作的场景下,使用虚基类,只右父类的1份拷贝;不用虚基类,除第一个子类用父类的拷贝外,每个子类再拷贝1份

【总结】

继承:(一个/多个)子类复用父类的功能。(@多叉树)

二、友元

(1)这个类的成员函数

(2)友元。——谁是我的friend?

(1)友元函数。

可以是另一个类的成员函数 or 普通函数

在被访问的类里,要声明友元函数!声明方法:friend +函数声明。声明可以放在被访问类的公有/保护/私有(有什么区别)

访问方式:()入口传递对象名/对象指针/对象引用

(1)普通函数

(2)类的成员函数                        例子:class Score{friend void Student::show(Score &sc) }class Student { show(Score &sc)};

(2)友元类

当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,

private 成员函数的作用:

如何访问?

protected 成员变量的作用:

如何访问?

protected 成员函数的作用:

如何访问?

public 成员变量的作用:

如何访问?

public 成员函数的作用:

如何访问?

三、虚函数

*********************************************************************************

虚函数

*********************************************************************************

【语法1】

(1)抽象类的定义:virtual void funtion()=0

(在类外声明虚函数时,不用再加 virtual)

(2)在派生类中,虚函数被重新定义时,它的函数原型和基类的函数原型必须完全相同。

(5)虚函数必须是其所在类的【成员函数】,而不能是友元函数,也不能是静态成员函数,(?)因为虚函数调用要靠特定的对象来决定该激活哪个函数。

(6)使用对象名和点运算符的方式调用虚函数是在【编译时进行的】,【是静态联编】(?),(?)【没有利用虚函数的特性】。只有通过基类指针访问虚函数时才能获得运行时的多态性。

【语法2】

派生类调用基类函数,注意:{

(1)if(派生类要重写基类函数){

调用于派生类的基类函数,不用加 : : 域作用符

}

(2)if(派生类不重写基类函数){

调用于派生类的基类函数,要加 : : 域作用符

}

}

(1)的常见用法是在重写基类函数的函数(即派生类的虚函数)中,再调用基类函数,目的是减少代码量

BrassPlus::ViewAcct(){

Brass::ViewAcct();//基类虚函数的东西

...                          //派生类的虚函数新实现的东西

}

总结:继承+改写

【语法三】

【语法四】虚析构函数

如果析构函数不声明virtual,普通析构函数根据【指针类型】来找到要析构的类

声明了virtual的析构函数,将调用相应【对象类型】的析构函数。

调用虚析构函数,可以确保正确的析构函数序列(?)

什么时候基类必须用虚析构函数(?)

*********************************************************************************

虚函数表

*********************************************************************************

*********************************************************************************

纯虚函数 & 抽象类

*********************************************************************************

*纯虚函数:如果【一个类的virtual函数只有定义没有声明(/实现)】,该基类叫抽象类,该virtual函数叫纯虚函数。

纯虚函数的声明由派生类来实现。

因为抽象类的纯虚函数没有声明,所以抽象类不能直接生成对象。

为什么要有抽象类?

1、(?)防止派生类忘记实现虚函数,

2、(?)在某些场景下,创建基类对象是不合理的,

四、模板

如何访问?


 

*********************************************************************************

静态联编和动态联编

*********************************************************************************

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值