C++面向对象编程(上)

C++面向对象编程(上)

1. 头文件的书写规范

  • 防御式声明
  • 防止一个程序多次引入一个头文件,导致编译器编译多次(多次把头文件和源文件整合)。
    在这里插入图片描述

2. 重载函数

  • 对于两个real函数,可以实现函数重载,虽然看起来函数名相同,但编译器会根据它们的参数个数、类型定义不同的名字
  • 对于两个构造函数,一个有默认参数,一个无参,它们不可以实现函数重载;使用无参的构造函数,编译器会不知道该调用哪个。
    在这里插入图片描述

3. 编写类的注意点

  • 构造函数使用列表初始化;

  • 数据放在private;

  • 成员函数如果不会改变数据,一定要加const【const成员函数,加在 ()后,{}前 】,这样const对象才能调用;

  • 函数传参尽量用reference,要不要用const看情况;

  • 函数返回值尽量用reference;

    • 如果是返回局部变量(不是已有空间,在函数内才创建的空间 ),则只能返回value。
  • 函数简单就加 inline,inline是给编译器的建议,不是要求;

  • 重点

    • 相同class的各个object(对象)互为friend(友元);

    • 在class内访问同类的其他对象,可以直接访问private数据
      在这里插入图片描述

4. 运算符重载

  • 有些运算符需要返回引用

    • 要求有返回,不能返回void:
      • 使运算可以连续进行,要求运算符一定要求有返回即可;
      • 如果运算不要求有连续运算,则返回void也是可以的;
    • 要求返回引用:
      • 其中一个点:使运算连续进行不会出错,且不会因为返回的是临时变量,导致结果出错;比如:(a += b) += c
      • 另一点:避免执行额外的拷贝构造和析构函数。
  • 对于赋值符号:

    • 如果左边的变量之前已经定义过,则是调用 = 运算符,不是拷贝构造;
    • 如果左边的变量之前没有定义过,则是调用拷贝构造
  • 运算符函数是设计为 class memebr 函数 或者 global 函数:

    • 一般情况下都可以;

    • 如果设计为成员函数,对于二元运算符,只需要传递一个参数,因为有一个隐藏的this指针参数,不需要声明出来,但在函数内可以直接使用this。
      在这里插入图片描述

    • 但运算符 member 函数不能对称地处理数据(运算符两边数据类型不同),程序员必须在(参与运算的)所有类型的内部都重载当前的运算符;这样做不但会增加运算符重载的数目,还要在许多地方修改代码,这显然不是我们所希望的,所以 C++ 进行了折中,允许以全局函数(友元函数)的形式重载运算符;

    • 特殊的运算符 ” << “(输出运算符)

      • 必须设计为全局函数

        • 按照常规写法 count << object,<< 是由库函数调用的,cout 不认识除了内置类以外的新类;
        • 所以重新定义 << 运算符;
      • 如果写成 class member 函数:

        • ① 在标准库重载,意味着修改标准库,显然不理想;
        • ② 在 object 中重载,这样想要调用 << 时,会不符合使用习惯:class object<< cout;
      • 同时考虑到可能有连续使用” << “的情况(下图),函数一定要有返回,不能是void(图右)。

      • 对所有运算符都要考虑连续运算的可能

        • 例如:cout << s1 << s2;
          在这里插入图片描述

5. 带有指针的类

1. 一定要重写的函数
  • 需要重写的函数:拷贝构造函数;赋值构造函数;析构函数
  • 【对于没有指针的类,一般编译器提供的默认拷贝构造函数/赋值构造函数/析构函数就够用;】
  • 对于有指针的类,一定要重写这三个函数;
    • 因为默认拷贝构造函数只会copy指针(属于浅拷贝),这样会导致两个类的指针指向相同的地址,即两个类中的指针指向一个数据

    • 默认赋值构造函数:只会改变原指针的值,不会改变指针指向的内容,最终会导致原来的指针指向的数据没有释放,新赋值的指针和赋值的指针指向同一个数据;

    • 默认析构函数只会释放指针,指针指向的内容没有释放

2. 构造函数和析构函数
  • 字符串除了内容以外,还有最后一位终止符号 ’\0'

  • 构造函数:

    • 为指针数据动态分配空间;
    • 进行初始化;
  • 构造函数动态分配的形式,和析构函数释放的形式相同;

  • new数组对应delete数组,[] 搭配 []。
    在这里插入图片描述

3. 拷贝构造函数
  • 默认拷贝构造函数 == 浅拷贝(位拷贝)

    • 造成内存泄漏;
    • 两个指针指向一个数据,一个进行改变或析构,另一个会收到影响。
      在这里插入图片描述
  • 重写拷贝构造函数 == 深拷贝(值拷贝)
    在这里插入图片描述

4. 拷贝赋值函数
  • 重写拷贝赋值函数
    在这里插入图片描述

  • 步骤:

    • 检查自我赋值,通过指针判断
    • 删除自身指针指向的数据;
    • 重新分配内存空间;
    • 拷贝内容。
  • 一定要有返回值,应对连续赋值的情况

6. stack(栈)和heap(堆)

1. stack(栈)和heap(堆)介绍

在这里插入图片描述

2. 生命周期
  • stack(栈)objects的生命周期:

    • 也被叫做auto objects,在作用域结束时,会自动调用析构函数;
  • 静态局部对象(static)的生命周期:
    在这里插入图片描述

  • 全局对象的生命周期:
    在这里插入图片描述

  • heap对象的生命周期:

    • 动态分配得到的指针,在离开作用域后就会死亡,但指向的数据不会释放;
    • 所以要手动释放heap对象:左边正确,右边错误(内存泄漏);
      在这里插入图片描述
3. heap对象生成和销毁过程
  • new过程:

    • 使用operator new分配空间,底层使用malloc函数(分配的实际空间大小见下文);
    • 进行指针类型转换
    • 调用构造函数:对于带有指针的类,通过构造函数,分配指针指向的数据;
      在这里插入图片描述
  • delete过程:

    • 调用析构函数:对于带有指针的类,通过析构函数,释放指针指向的数据;
    • 使用operator delete释放分配的空间,底层使用free函数。
      在这里插入图片描述
4. malloc过程
  • 平台:vc,malloc后的大小是16的倍数

  • 对于cookie(可以理解为是内存空间块的开始和结束标志)的值 == 分类后大小的十六进制:

    • 如果该空间已经分配出去,则再 + 1
    • 对于调式模式下的Complex类,由于64的16进制 == 40h,又因为该空间已经分配出去,所以cookie == 40h + 1 == 41h。
  • 单个类分配的内容:

    • 在调试模式下(debug):
      1. 类本身需要的大小空间(针对类中的数据);

      2. 类前面32个字节的debug信息,类后面4个字节的debug信息;

      3. 头尾各4个字节的cookie;

      4. 针对 Complex(实数类)而言:

        • Complex* p = new Complex();
          
        • 大小 == 8(类本身)+(32+4)(debug信息)+(4*2)(cookie) = 52;

        • 取16的倍数 == 64,图中的pad就是填充。

      5. 针对 String(字符串类)而言:

        • String* p = new String();
          
        • 大小 == 4(类本身)+(32+4)(debug信息)+(4*2)(cookie) = 48。

    • 非调试模式下:
      1. 没有debug信息,其他都有;
      2. Complex大小 == 8(类本身)+(4*2)(cookie) = 16,已经是16的倍数;
      3. String大小 == 4(类本身)+(4*2)(cookie) = 12 == 16;
        在这里插入图片描述
5. malloc数组的过程(动态分配new)
  • 平台:vc,malloc后的大小是16的倍数

  • 数组array分配的内容:

    • 在调试模式下(debug):

      1. 多个类需要的大小空间(针对类中的数据);

      2. 类前面32个字节的debug信息,类后面4个字节的debug信息;

      3. 类前面,记录数组数量的4个字节(整数);

      4. 头尾各4个字节的cookie;

      5. 针对 Complex(实数类)而言:

        • Complex* p = new Complex[3];
          
        • 大小 == 8*3(3个类)+(32+4)(debug信息)+(4*2)(cookie)+ 4(记录数组个数) = 72 == 80;

        • 80的十六进制 == 50h,+ 1 == 51h。

      6. 针对 String(字符串类)而言:

        • String* p = new String[3];
          
        • 大小 == 4*3(3个类)+(32+4)(debug信息)+(4*2)(cookie)+4(记录数组个数) = 60 == 64。

    • 非调试模式下:

      1. 没有debug信息,其他都有;

      2. Complex大小 == 8*3(3个类)+(4*2)(cookie) + 4 = 36 == 48;

      3. String大小 == 4*3(3个类)+(4*2)(cookie)+ 4 = 24 == 32;
        在这里插入图片描述

6. array new一定要搭配array delete
  • 回顾delete过程:① 调用析构函数;② 释放分配的空间;

  • array delete会在 ① 调用多次析构函数,次数 == 数组的大小;② 只调用一次;

  • 针对带有指针的类一定要array new一定要搭配array delete !!!】:

    • 正确使用(左图):成功释放三个string中的指针,指向的数据;再释放分配的空间;
    • 错误使用(右图):只释放了一个string中指针指向的数据;再释放分配的空间。
  • 对于不带有指针的类,如果你使用了array new,但没有使用array delete,也不会造成内存泄漏;

  • 为了以防万一和统一,不管类是否带有指针,使用了array new就一定要搭配array delete。
    在这里插入图片描述

7. 进一步补充

1. static补充
  • 普通成员数据和成员函数:

    • 普通成员数据:创建多份对象时,会为每个对象分配一份数据,普通数据在内存占用多份;
    • 普通成员函数:在内存中只有一份,所有类公用;普通成员函数有this指针参数,通过传递this指针就可以访问不同对象的数据;
    • 类中的所有除了static成员数据和成员函数以外的数据和函数,都需要通过this指针进行调用和访问。
  • static成员数据和成员函数:

    • static成员数据:脱离类,不管创建多少个对象,static数据在内存中只占一份;
    • static成员函数:和普通成员函数一样,在内存中只占一份;区别在于,static函数没有this指针参数,所以它没办法访问对象中的普通数据,只能访问static数据
      在这里插入图片描述
  • static成员数据和成员函数使用方法:

    • static成员数据:必须在类外进行定义(下图黄色部分)

    • static成员函数:

      • 通过创建的对象调用;

      • 直接通过class name调用。
        在这里插入图片描述

  • 单例模式(设计模式)

    • 需求:希望某个类只有一个对象,其他人无法创建;

    • 构造函数放在private中

    • 唯一的一份对象使用static数据

    • 对外使用static函数访问这一份数据;

    • 为了在有人使用时,才创建这个唯一的类,将创建静态类写在静态函数中。
      在这里插入图片描述

2. cout补充
  • 为什么cout可以输出各种类型?

  • 因为cout实现了很多 << 运算符的重载
    在这里插入图片描述

3. template补充
  • class template:

    • 使用类模板时,必须指明 T 的类型;

    • 指明一个类型,相当于生成一份 T == 对应类型的新的类代码。
      在这里插入图片描述

  • function template:

    • 使用函数模板时,不需要指明 T 的类型,编译器会根据传入的参数,进行参数推导

    • 推导后,也相当于生成一份 T == 对应类型的新的函数代码。
      在这里插入图片描述

4. namespace补充
  • 三种使用方法:

    • using 整个空间;
    • using 某个部分;
    • 全名使用。
      在这里插入图片描述

8. 类之间的三种关系

1. Composition(复合)
  • 复合就是拥有,表示has-a
    在这里插入图片描述

  • Composition的构造和析构

    • 构造函数从内到外

      • 首先执行组件的构造函数,再执行自己的;
    • 析构函数从外到内

      • 首先执行自己的析构函数,再执行组件的;
    • 下图红色部分,编译器会自动添加:

      • 对于内部的构造函数,编译器只会调用默认构造函数
      • 如果默认构造函数不满足需求,就要自己写内部的对应构造函数
        在这里插入图片描述
2. Delegation(委托)
  • 委托就是 Compositon by reference(复合内容使用指针相连);
  • 左边是对外开放的可见类(Handle),右边是具体实现类(Body),这种模式称为pImpl(Pointer to implementation)
    • pImpl 也称为编译器防火墙:
      • 降低文件间的编译依赖关系,减少编译时间,加快编译速度;
      • 改变实现类的代码,不需要重新编译可见类,可见类的内容并没有改变。
        在这里插入图片描述
3. Inheritance(继承)
  • 继承表示is-a

    • 数据的继承直接体现在占用内存(子类大小);
    • 函数的继承是继承调用权
  • 三种继承方式:

    • public:
    • private
    • protected
  • Inheritance的构造和析构(和复合的很类似)

    • 子类的对象包含父类的成分

    • 构造函数从内到外(由父类到子类)

      • 首先执行父类的构造函数,再执行自己的;
    • 析构函数从外到内(由子类到父类)

      • 首先执行自己的析构函数,再执行父类的;
    • 父类的析构函数必须是virtual;

  • 下图红色部分,编译器会自动添加:

    • 对于父类的构造函数,编译器只会调用默认构造函数
    • 如果默认构造函数不满足需求,就要自己在红色位置写内部的对应构造函数
      在这里插入图片描述
4. 虚函数(和继承相关,不是类间关系)
  • 虚函数和继承息息相关;

  • 父类的成员函数有三种选择:

    • 非虚函数:子类不能重新定义;
    • 虚函数:子类可以重新定义;
    • 纯虚函数:子类必须重新定义;
      在这里插入图片描述

9. 利用类关系组合引出设计模式

1. Template Method设计模式(利用继承关系)
  • 父类的函数执行了一些固定的动作,在关键的部分延缓实现,让子类来实现;【关键部分设置为虚函数】

  • 这种函数叫做 Template Method(这和C++的模板不是一个概念);

  • 子类实现虚函数后,调用父类的函数(Template Method),在关键部分就能调用自己实现的(看下图中的灰色箭头)。

    • 为什么关键部分能调用自己的呢?
    • 因为在调用OnFileOpen()函数时,传入了子类的this指针(成员函数的隐藏参数);在执行关键部分Serialize()时,就能通过子类的指针调用对应的Serialize()函数。
      在这里插入图片描述
2. Inheritance和Composition组合
  • 不常用,Delegation和Inheritance组合更常用;

  • 构造和析构函数顺序:

    • 子类继承父类,同时子类有复合内容,构造和析构顺序;
      • 构造:父类 → 复合部分 → 子类;
      • 析构:和构造相反。
    • 子类继承父类,同时父类有复合内容,构造和析构顺序:
      • 构造:复合部分 → 父类 → 子类;
      • 析构:和构造相反。
        在这里插入图片描述
3. Observer设计模式(Delegation和Inheritance组合)
  • 想解决的问题:为同一份数据,提供多个obverser(观察者),obverser可以是同种也可以不同种。(下图为某个具体例子)
    在这里插入图片描述

  • 实现:

    • 数据类:
      • 包含放置observer指针的数组(Delegation关系);
      • 包含observer登记、注销的功能;
      • 通知observer更新的功能。
    • observer类:
      • 可以作为父类(Inheritance关系);
      • 有更新功能,且为虚函数。
        在这里插入图片描述
4. Composite设计模式(Delegation和Inheritance组合)
  • 想解决问题:

    • 以文件系统为例,目录里既可以放文件,也可以放目录。
  • 实现:

    • Primitive类可以理解为文件,Composite类理解为目录;

    • Composite类中包含一个容器,需要既可以放置Primitive类,也可以放置Composite类自身;

      • 将Primitive和Composite,继承同一个父类(Inheritance关系),容器存放类型为父类指针(Delegation关系);
    • 注意点:

      • 父类中的add(virtual)函数不能是pure virtual函数,这会强制Primitive类重新定义;

      • 以文件系统为例,add函数为目录添加内容,这对文件没有意义(文件不能包含文件或目录)。

      • 因此令add virtual函数为空函数,这样Primitive类调用add函数就什么都不会做;
        在这里插入图片描述

5. Prototype设计模式(Delegation和Inheritance组合)
  • 解决的问题:用原型实例来指定创建对象的种类,并且通过拷贝这些原型创建新的对象。【不太好懂】
  • Prototype(抽象原型类):它是声明克隆方法的接口(clone虚函数),是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类;
  • ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象;
  • 下图中的符号说明:
    • 下划线:静态类;
    • 负号:private;
    • #:protected;
    • 默认没有符号 or 正号:public
  • Prototype(抽象原型类)的内容:
    • 存放自身类指针的数组:用于存放每种具体原型类的一个对象(prototype[10]);
    • 数组添加具体原型类对象的函数(addPrototype);
    • clone虚函数:用于具体原型类重新定义;
    • 根据存放的具体原型类对象,进行克隆的函数(findAndClone):调用具体原型类对象的clone函数,返回一个新的具体原型类对象;
  • ConcretePrototype(具体原型类)的内容:
    • static的自己
    • private的默认构造函数( LandSatImage() ),同时要调用父类的插入数组函数(addPrototype);static对象会调用这个构造函数,把自己传给父类(抽象原型类)
    • 另一个private or protected的构造函数,为了和上面的构造函数区分,加入一个无用的参数( LandSatImage(int) );
    • 重新定义父类的clone函数:用于返回新的自身对象;新的对象会调用另一个构造函数,因为不能重复将自身加入父类的数组
      在这里插入图片描述
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值