类和对象 150136

1 类和对象

C++面向对象的三大特性:封装、继承、多态。

任何物体都有对象,对象有属性和行为功能

1.1 封装

1.1.1 封装的意义

封装是C++面向对象的三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现为生活中事物

  • 将属性和行为加以权限控制

    public 类内可访问,类外也可访问

    protected 类内可访问,类外不可访问(子类可访问)

    private 类内可访问,类外不可访问(子类不可访问)

    ​ 一般将成员属性设置为私有权限

    ​ 优点:可以自己控制读写权限;对于写可以检测数据的有效性

1.1.2 struct和class的区别
  • struct的默认权限是public

  • class的默认权限是private

1.1.3 封装案例

1、设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。

#include <iostream>
using namespace std;
class Cube
{
private:
    int c;
    int k;
    int g;
public:
    Cube(int c, int k, int g)
    {
        this->c = c;
        this->k = k;
        this->g = g;
    }
    int minaji()
    {
        return ((this->c * this->k) + (this->c * this->g)
            + (this->g * this->k))* 2;
    }
    int tiji()
    {
        return this->c * this->g * this->k;
    }
    void is_deng(Cube c1 , Cube c2)
    {
        if (c1.c = c2.c && c1.g == c2.g && c1.k == c2.k)
            cout << "两个立方体完全相同" << endl;
        else
            cout << "两个立方体不完全相同" << endl;
    }
};
void Is_deng(Cube c1, Cube c2);
int main()
{
    Cube c1(10, 10, 10);
    Cube c2(10, 10, 10);
    cout << "c1的体积和面积:"<<c1.tiji()<<"\t"<<c1.minaji ()<< endl;
    cout << "c2的体积和面积:" << c2.tiji()<<"\t"<< c2.minaji() << endl;
    cout << "全局函数判断结果:" << endl;
    Is_deng(c1, c2);
    cout << "成员函数判断结果:" << endl;
    c1.is_deng(c1, c2);
    return 0;
}

void Is_deng(Cube c1, Cube c2)
{
    if (c1.minaji() == c2.minaji() && c1.tiji() == c2.tiji())
        cout << "两个立方体完全相同" << endl;
    else
        cout << "两个立方体不完全相同" << endl;
}

1.2 对象的初始化和清理

C++中的面向对象来源于生活,每个对象也都有初始化设置及其在对象销毁前的清理数据设置。

1.2.1 构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

​ 一个对象或者变量没有初始状态,对其使用后果是未知

​ 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。

  • **构造函数:**主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

  • **析构函数:**主要作用在于对象销毁前系统自动调用,执行一 些清理工作。

构造函数语法:类名(){}

1、构造函数,没有返回值

2、函数名称与类名相同

3、构造函数可以有参数,因此可发生重载

4、程序在调用对象时会自动调用构造函数,无须手动调用,且只调用一次

析构函数语法:~类名(){}

1、析构函数,没有返回值

2、函数名称与类名相同,在名称之前加~

3、析构函数不可以有参数,因此不可发生重载

4、程序在对象调用前会自动调用析构函数,无须手动调用,且只调用一次

1.2.2 构造函数的分类与调用

两种分类方式:

​ 1、按参数分为:有参构造和无参构造

​ 2、按类型分为:普通构造和拷贝构造

​ 补充:拷贝构造函数语法示例:Person(const Person &p){}

​ 作用:将传入的人身上的所有属性,拷贝到当前对象身上。

(const和引用的作用是为了保护被拷贝对象本身不会被更改)

三种调用方式:

​ 括号法:类名 对象(参数)

​ 显示法:类名 对象 = 类名(参数) 等号后相当于创建了一个匿名对象

​ 隐式转换法:类名 对象 = 参数 相当于类名 对象 = 类名(参数)

1.2.3 拷贝构造函数的调用时机

C++中拷贝构造调用时机通常有三种情况:

  • 使用一个已经创建完毕的对象初始化一个新的对象
  • 以值传递的方式给函数参数传值
  • 以值方式返回局部对象
1.2.4 拷贝构造函数的调用规则

默认情况下,C++编译器至少给一个类添加三个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,用于拷贝对象属性值

构造函数调用规则:

  • 如果自定义有有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造函数
  • 如果自定义拷贝构造函数,C++不再提供其他构造函数
1.2.5 深拷贝与浅拷贝
  • 浅拷贝:简单的赋值拷贝操作(编译器默认提供) 注:会造成堆区的内存重复释放
  • 深拷贝:在堆区重新申请空间,进行拷贝操作 可解决浅拷贝带来的问题,重新开辟堆区空间
1.2.6 静态成员

静态成员函数特点:

​ 程序共享一个函数

​ 静态成员函数只能访问静态成员遍历

​ 静态成员函数访问方式:1、对象.函数名() 2、类名::函数名()

1.3 C++对象模型和this指针

1.3.1 成员变量和成员函数分开存储

补充:空对象占用的内存空间为:1,为了区分空对象占用内存的位置

  • 非静态成员变量,属于类的对象上

  • 非静态成员函数,不属于类的对象上

  • 静态成员变量或函数,不属于类的对象上

1.3.2 this指针

通过上面的内容我们知道在C+ +中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例, 也就是说多个同类型的对象会共用一块代码

那么问题是:这- -块代码是如何区分那个对象调用自己的呢?

C++通过提供特殊的对象指针,this指针, 解决主述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分

  • 在类的非静态成员函数中返回对象本身,可使用return *this

**this指针的本质:**是一个指针常量,指针的指向不可修改

​ 示例:Person * const this

1.3.3 const修饰成员函数

常函数:

  • 成员函数后加const后,此函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时添加mutable关键字后,在常函数中可修改

常对象:

  • 在声明对象前添加const关键字后,则对象为常对象
  • 常对象只能调用常函数

1.4 友元

友元是指在类中虽然有private权限的内容,既不可被别的类访问也不可在类外被访问,但是通过友元的技术,可以进行特权访问。

友元的关键字:friend

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
1.4.1 全局函数做友元

在被作为友元类中提前声明,声明语法:friend 返回类型 函数名(参数);

1.4.2 类做友元

在被作为友元类中提前声明,声明语法:friend class 类名;

1.4.3 成员函数做友元

在被作为友元类中提前声明,声明语法:friend 返回类型 类名::函数名(参数);

1.5 运算符重载

1.5.1 加号运算符重载
  • 成员函数重载
#include <iostream>
#include<math.h>
using namespace std;

class Person
{
public:
    int age;
    int id;

    Person operator+(Person& p)
    {
        Person temp;
        temp.age = this->age + p.age;
        temp.id = this->id + p.id;
        return temp;
    }
};
int main()
{
    Person p1;
    p1.age = 10;
    p1.id = 1;

    Person p2;
    p2.age = 10;
    p2.id = 2;
    Person p3;
    p3 = p1 + p2;    //等价于p1.operator+(p2)
    cout << p3.age << endl;
    cout << p3.id << endl;
    return 0;
}
  • 全局函数重载
#include <iostream>
#include<math.h>
using namespace std;

class Person
{
public:
    int age;
    int id;
};

Person operator+(Person& p1, Person&p2)
{
    Person temp;
    temp.age = p1.age + p2.age;
    temp.id = p1.id + p2.id;
    return temp;
}

int main()
{
    Person p1;
    p1.age = 10;
    p1.id = 1;

    Person p2;
    p2.age = 10;
    p2.id = 2;
    Person p3;
    p3 = p1 + p2;    //等价于p3 = operator+( p1, p2)
    cout << p3.age << endl;
    cout << p3.id << endl;
    return 0;
}
1.5.2 左移运算符重载
#include <iostream>
#include<math.h>
using namespace std;

class Person
{
public:
    int age;
    int id;

    ostream& operator<<(ostream& cout)          //因为目标所需的格式为cout<<p , 但是如果使用类内实现等价于p.operator<<(cout) 等价于p<<cout 因此重载<<不在类内实现             {                                                                         
        cout << "age:" << this->age << endl;
        cout << "id:" << this->id << endl;
        return cout;
    }
};
ostream& operator<<(ostream &cout, Person&p)        //cout类型为ostream,因为只能有一个cout关键字,所以使用引用
{
    cout << "age:" << p.age << endl;
    cout << "id:" << p.id << endl;
    return cout;      //这里返回cout是为了可以继续追加<<运算符, 连续执行                                  
}

int main()
{
    Person p;
    p.age = 10;
    p.id = 1;
    cout << p << endl;  //等价于 operator<<(operator<<(cout, p), endl)
    return 0;
}

其他运算符重载与上述类似在此不再赘述

1.6 继承

继承是面向对象的三大特性之一

继承是指大类分出的小类,不仅有自己的特点还具有大类的特点。

1.6.1 继承的基本语法

class 子类 : 访问方式 基类

例:class A : public B;

​ A类称为子类或者派生类

​ B类称为父类或者基类

继承的作用:可以减少重复的代码

派生类中的成员,包含两大部分:

1、一类是从基类继承过来的,一类是自己增加的成员。

2、从基类继承过过来的表现其共性,而新增的成员体现了其个性。

1.6.2 继承方式

一共有三种继承方式:

  • 公共继承 public
  • 保护继承 protected
  • 私有继承 private

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sx6Aszob-1648379839018)(D:\Typora图像\image-20210810155855511.png)]

1.6.3 继承中的对象模型

子类在继承基类的属性时,会继承所有的属性,而对于private下的属性同样继承只是隐藏起来不显示

利用开发人员命令提示工具查看对象模型
跳转盘符 F:
跳转文件路径 cd 具体路径下
查看命名
cl /d1 reportSingleClassLayout类名 文件名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mszQXbAc-1648379839019)(D:\Typora图像\image-20210810162026349.png)]

1.6.4 继承中构造和析构顺序

**顺序:**父类构造函数---->子类构造函数---->子类析构函数---->父类析构函数

1.6.5 继承中同名成员的处理方式

问题:当子类与父类中出现同名的成员,如何通过子类对象,访问到子类或父类中的同名成员呢?

  • 访问子类同名成员 直接访问即可 (语法:子类.同名成员

  • 访问父类同名成员 需要添加作用域(语法:子类.父类::同名成员

    注意:如果子类中出现父类同名的成员函数,子类的同名函数会隐藏掉父类中所有同名函数,所有需要添加作用域

1.6.6 继承中同名静态成员的处理方式

**问题:**继承中同名的静态成员在子类对象上如何进行访问?

1、通过对象访问

  • 访问子类同名成员 直接访问即可 (语法:子类.同名成员
  • 访问父类同名成员 需要添加作用域(语法:子类.父类::同名成员

2、通过类名访问

  • 访问子类同名成员 直接访问即可 (语法:子类::同名成员
  • 访问父类同名成员 需要添加作用域(语法:子类::父类::同名成员
1.6.7 多继承

C++中允许一个类继承多个类

语法:class 子类 : 继承方式 父类1, 继承方式 父类2...

因为使用多继承时,常会导致出现多个同名成员,所以不建议使用多继承

1.6.8 菱形继承

菱形继承:指的是两个子类同继承自一个基类,然后又出现了一个子类同时继承于前两个子类

例:马和驴继承于动物类,骡子又继承于马和驴

出现的问题:

  1. 当两个父类有相同的成员,子类在访问同名成员时需要加作用域

  2. 当出现1的情况时,子类继承了两份相同的成员,造成了资源浪费

    解决方法:利用虚继承

    ​ 语法:class 子类 : virtual 访问方式 父类

    未采用虚继承:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FycKUsLE-1648379839019)(D:\Typora图像\image-20210810174441742.png)]

​ 采用虚继承后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9LoMf8a-1648379839020)(D:\Typora图像\image-20210810174524492.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PRzutNfh-1648379839021)(D:\Typora图像\image-20210810174740092.png)]

​ 使用了虚继承后SheepTuo没有继承Sheep和Tuo的全部属性,仅继承了两者的vptr(虚指针),而vptr指向vbtable(vbtable中记录了获取SheepTuo继承自Sheep和Tuo类所需偏移的地址值:Sheep需要偏移8、Tuo需要偏移4)

1.7 多态

1.7.1 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态:函数重载 和 运算符重载,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

示例:

#include <iostream>
#include<math.h>
using namespace std;

class Animal
{
public:
    virtual void speak()
    {
        cout << "动物在叫" << endl;
    }
};
class Cat : public Animal
{
public:
    void speak()
    {
        cout << "小猫在叫" << endl;
    }
};
class Dog : public Animal
{
public:
    void speak()
    {
        cout << "小狗在叫" << endl;
    }
};
class DogSon : public Dog
{
public:
    void speak()
    {
        cout << "狗儿子在叫" << endl;
    }

};
void test(Animal& animal)       //相当于 Animal &animal = cat     注:父类到子类不需要类型转化
{
    animal.speak();
}
int main()
{
    DogSon DS;
    test(DS);
    return 0;
}

输出结果:
狗儿子在叫

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 — 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 — 运行阶段确定函数地址

多态的满足条件:

  1. 有继承关系

  2. 子类重写父类中的虚函数

    注:重写和重载的区别:重载是函数的名称相同参数不同;重写是指函数名、返回值、参数都相 同

多态的使用条件:

  • 父类指针或引用指向子类对象 例:Animal * animal = new CatAnimal & animal = Cat cat

多态的优点:

  • 组织结构清晰
  • 可读性强
  • 对于前期和后期的扩展和维护性高
1.7.2 多态的原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22bddBTb-1648379839022)(D:\Typora图像\image-20210811143420506.png)]

当子类重写父类的虚函数时,vftable中的原地址替换为子类中重写后的函数地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQbmituk-1648379839022)(D:\Typora图像\image-20210811143545296.png)]

在开发人员命令行下显示:

1、若父类同名函数前未声明virtual关键字,内部结构如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oODmUPfw-1648379839023)(D:\Typora图像\image-20210811143750579.png)]

因为此时类的内部什么都没有,为空类,所以大小为1

2、若父类同名函数前声明virtual关键字,内部结构如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNNwYVyT-1648379839024)(D:\Typora图像\image-20210811143918176.png)]

生成了一个vfptr指针,所以大小为4,vfptr指向vftable,存放函数地址

3、当子类没有重写父类中的speak函数时,子类的内部结构如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwFQgApu-1648379839024)(D:\Typora图像\image-20210811144058093.png)]

因为Cat类继承了Animal类,所以内部结构完全复刻了Animal中的结构

4、当子类重写父类中的speak函数时,子类的内部结构如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryq8MaKT-1648379839026)(D:\Typora图像\image-20210811144302926.png)]

当Cat类重写了speak函数后vftable中存放的函数地址替换为了重写后的函数地址

1.7.3 纯虚函数和抽象类

在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容

所以可以将虚函数改为纯虚函数

纯虚函数的语法:virtual 返回类型 函数名(参数) = 0;

另外当一个类中有了纯虚函数,这个类也称为抽象类

抽象类的特点:
1、无法实现实例化对象

2、子类必须重写抽象类的纯虚函数,否则也属于抽象类

1.7.4 虚析构和纯虚析构

多态使用时,如果是使用父类指针指向子类对象使用多态时,例:Animal * animal = new Cat,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构语法:
virtual ~类名(){}

纯虚析构语法:

类内:virtual ~类名() = 0;

类外:类名::~类名(){}

虚析构和纯虚析构共性:

  • 都可以解决父类指针释放子类对象的问题
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

总结:

  • 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  • 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  • 拥有纯虚析构函数的类也属于抽象类


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值