侯捷 C++面向对象开发 (1) 面向过程

0. 前言

  • 侯捷大佬所有C++课程之一

  • 本文对应的课程: 面向过程

    • 包括第二课到第十课,相关内容主要是以实现 Complex 类与 String 类为目标
    • 介绍了类创建的基本语法、思路
  • 一些原则:

    • 构造函数使用 intialize list(即冒号)
    • 成员函数尽可能添加 const(即常量成员函数)
    • 参数传递尽量 pass by reference
    • 函数返回值尽量 return by reference
    • 函数绝大部分在 public,成员变量尽可能放在 private 中

第二课 - 头文件与类的声明

  • 区分基于对象(Object Based)和面向对象(Object Oriented)
  • C++代码基本形式:.h(自己写/第三方的头文件) + .cpp(自己写/第三方的源码) + .h(标准代码库,编译器提供)

image-20210819235308001

  • 头文件防卫式声明,即 #ifndef/#define/#endif

    • 所有头文件都应该使用这种结构
  • 头文件布局

image-20210819235637169

  • class 的声明(即上图中的 1 部分),设计有哪些数据、哪些操作
  • class template 简介,即模版
    • 如一个复数类(Complex),拥有两个参数,分别表示实部与虚部。
    • 实部与虚部的数据类型不确定,可能都是 int、都是 float、都是 double。
    • 为了实现上面这个需求,就可以使用模版类。

第三课-构造函数

  • inline(内联)函数

    • 在函数体(class body)内定义的函数就是内联函数
    • 函数体外定义的函数,可以使用 inline 关键字修饰,成为内联函数
    • inline 能提高运行速度
    • 理想状态下,所有函数都是inline就最好了,但编译器不允许。代码中定义的inline function只是对编译器的建议,最终是否真正成为inline看编译器选择。
  • 访问级别:private/protected/public,可以交叉、重复出现

  • 构造函数

    • 函数名与类名相同,且没有返回类型,用于创建对象的实例
    • 只用于创建对象,已经创建好的对象不能调用
    • 尽可能使用分号构造函数形式,效率高一些
    • 构造函数位于 private 中的内容
    • 由于有默认参数,所以可能会出现歧义,这时候就会导致编译错误。
  • 重载(overloading):函数名相同,参数不同,C语言中不支持这种语法。

    • 实现原理:编译器中的函数名包括了参数数量、参数名称等

第四课-函数传递与返回值

  • 构造函数放在 private 区域:不允许别人构造函数,如单例模式

  • 常量成员函数(如 double real() const {return re;}):

    • 定义:不改变成员变量数值
    • 在定义和声明中都应加const限定
    • 常量(const对象)对象只能调用常量成员函数
  • 函数传递

    • 按值传递(by value):整包传递
    • 引用传递(by reference):
      • C中使用指针(4个字节,存储地址空间)
      • C++中使用引用(底层实现是指针,性能就像指针,但形式更漂亮)
      • 修改引用中的参数就会导致外部对象的内容改变,如果不希望这样,可以使用 const reference
      • reference 的主要作用就是参数值传递和返回值传递
      • 使用 reference 的主要原因是效率
  • 返回值传递:也分为 by value 和 by reference,尽可能使用后者

  • 友元(friend)

    • 友元函数:可以调用形参的非 public 成员变量
    • 友元不利于“封装”特性
    • 相同 class 的各个 objects 互为友元
  • 类的总体设计原则

    • 数据全部是 private
    • 传参尽可能使用 reference,需要不需要 const 看情况
    • 返回值尽可能使用 reference
    • 成员函数尽可能使用 const 修饰
    • 构造函数使用 initialization list(即冒号)
  • 不可以使用 return by reference 的情况

    • 在函数体内部创建了一个对象作为函数的返回值时,不能传递这个对象的引用
    • 因为是局部变量,函数运行完了对象就销毁了,对象的引用就没有意义
    • 传递着无需知道接受者是以 reference 接收(如果是指针,就必须先确定是指针)

第五课-操作符重载与临时对象

  • 操作符重载-成员函数

    • 成员函数有默认形参 this,编译器会自动添加到成员函数的形参中(位置不一定,根据成员函数确定)
    • 如果只有 c2 += c1 这种用法,那么对应的函数实现为 void complex::operator += (const complex &r) 就可以了,注意,返回值类型是 void。
    • 如果要支持 c3 += c2 += c1,那么返回值类型必须是 complex&,即 complex& complex::operator += (const complex& r)
  • 操作符重载-非成员函数

    • 全局函数,没有 this,形参包括两个参数
    • 返回的是value而不是reference,因为返回的是local object(局部对象)
    • 常用的特殊语法 typename(); 用于创建临时对象,不能用于 returen by reference
complex c1(2, 1);
complex c2;

// 创建临时对象
complex();
complex(4, 5);
// 上面两个对象到这一步就没有了

第六课-复习Complex类的实现过程

  • 新手比较适合看,我就听了一遍

第七课-拷贝构造,构造复制,析构

  • 拷贝构造
    • 定义时形如 String(const String& str);
    • 使用时形如 String s3(s1);
    • 参数第一次出现
  • 构造复制
    • 定义时形如 String& operator=(const String& str);
    • 形如 s3 = s2;
    • 参数s3不是第一次出现,之前已经声明过
    • 基本构造流程:
    • 基本实现:
      • 自我检测(自我赋值),如果没有定义在进行自我赋值(如a = a)时会报错,因为第一步就是删除内存。
      • 自己删除+新建+拷贝

image-20210824024905948

  • 析构
    • 当对象死亡时会自动调用,名称与类名称相同,前面添加波浪号 ~
    • 定义时形如 ~String();
    • 一个主要作用是防止内存泄漏。所谓内存泄漏,就是在对象死亡后,其对应的“动态分配”的内存并没有收回。
  • 拷贝构造和拷贝复制的默认实现以及必要性
    • 如果没有自己定义,那编译器会默认实现,默认行为就是按照数值一个一个复制,就是浅拷贝。
    • 要不要自己重新实现,主要要关注的是成员变量中是否存在指针。
    • 如果有指针就必须自己实现,这是因为:
      • 对于拷贝构造,如果直接复制指针,会导致内存泄漏(被赋值的指针原本指向的地址空间没人处理了)
      • 如下图所示,第一行是定义了两个String对象,第二行就是默认拷贝赋值操作,可以看到这只是浅拷贝,有内存泄漏产生。

image-20210826010649207

// 拷贝构造的形式(下面三个都是)
String s1("hello");
String s2(s1);
String s3 = s1;// 因为创建了新的对象,所以就是利用构造函数实现
  • 字符串的两种设计
    • PASCAL:前面有常数,指定字符数量,后面跟着字符数量
    • C/C++:以特定字符(如\0)结尾

第八课-堆,栈与内存管理

  • String 重载 << 符号必须使用全局函数,而不是成员函数

    • 如果是成员函数,那么在使用过程中 cout 和 String 对象的位置是相反的,即 str << cout
    • 全局函数参数分为两个,一个是 ostream,令一个就是 String 对象
    • 为了实现连续操作(如 out << str1 << str2),重载函数的返回值需要是 ostream& 的形式
  • 堆与栈的定义

    • 栈(stack)是存在于某作用域(scope)内的一块内存空间(memory space)。

      • 例如,调用函数时,函数内部就会形成一个stack,放置参数、返回值地址。
    • 堆(heap),即system heap,是指由操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从中获取若干块(blocks)

      • 一般都是通过 new 来获取内存空间
    • 举例如下:

class Complex {...};
...
{
    Complex c1(1, 2); // stack
    Complex c2 = new Complex(3); // heap
}
  • 生命期

    • stack objects:作用域(scope)结束之后结束,会自动调用析构函数。
      • 作用域内object,就是局部变量,也称为 auto object,所谓 auto 就是会被自动清理
    • static local objects:
      • 写法:在一个 scope 中的成员变量通过 static 修饰
      • 在作用域结束后仍然存在,直到程序结束才会调用析构函数
    • global objects:
      • 写法:写在任何作用域之外(任何大括号之外)的对象
      • 程序结束之后才会结束
      • 可以看成是一个static对象
    • heap objects:
      • 通过 new 创建,通过 delete 结束
      • 如果没有 delete 就会造成内存泄漏
      • 所谓内存泄漏(memory leak),就是当作用域结束,p所指的heap object仍然存在,但p的声明周期已经结束,作用域之外再也看不到p(没有机会 delelete p)
  • new 的细节:先分配memory,再调用构造函数,具体情况参考下图

    • operator new 是 C++ 提供的特殊函数,函数名中包含了空格,本质就是C++版的 malloc
    • sizeof 就是两个成员变量(即两个double),8字节
    • 数据转型,即 static_cast
    • 调用构造函数。构造函数是一个函数名称与class名相同的函数。构造函数的全名就是 Complex::Complex(pc, 1, 2),其中 pc 就是对象本身

image-20210828000519627

  • delete 的细节:先调用析构函数,再释放内存
    • 析构函数删除指针指向的内容,即动态内存。这个析构函数是自己定义,而不是默认的。
    • operator delete 是C++的一个特殊函数,函数名包括空格,用来释放内存。

image-20210828005639324

  • 动态分配内存的底层细节,即 malloc 和 free 的细节
    • 下图是在VC下的实现,其他编译器可能不完全一样,但也差不了太多
    • 最左边是Debug模式下Complex对象的内存分配
      • 红色区域是编译器分配的cookie,首尾各4字节
      • 灰色区域是Debug模式特有的,前32字节后4字节
      • 绿色区域是实际Complex对象,8字节(两个double对象)
      • 深绿色区域是pad,因为VC下分配的内存(memory block)必须是16的倍数,前面全部加起来是52字节,会自动pad到64字节,所以需要增加12字节
    • 第二列是Release模式下Complex对象的内存分配,就是cookie加上Complex对象,即16字节,不需要pad
    • 第三列是Debug模式下,String对象的内存分配,灰色区域大小与之前Complex对象不变,String对象本体4字节(只有一个指针),计算后得到48字节,无序pad。
    • 第四列是Release模式下,String的内存分配,在计算cookie和本体后得到12字节,需要pad为16字节。
    • cookie的作用:记录对象的内存大小以及状态
      • 比如第一列是 41,这是16进制,对应10进制就是65。
      • 其中40就是字节数。
      • 末尾的1表示这一块是给出去(1)还是收回来(0)

image-20210828102710843

  • 数组动态分配内存的底层细节
    • 最左边的介绍一下:首尾两个cookie各4字节,debug 模式相关的是32+4字节,保存数组长度(即图中的3)需要4字节,实际三个Complex对象,需要8*3字节,pad 8字节达到16的倍数

image-20210828110132698

  • char *data = new char[10] 需要搭配 delete[] data,为什么
    • 左边的图,使用 delete[] p 就会调用三次析构函数
    • 右边的图,使用delete p,只调用一次析构函数(注意,调用了析构函数)

image-20210828112335844

第九课-复习String类的实现过程

第十课-扩展补充:类模版,函数模版,及其他

  • static

    • 普通成员函数有隐藏参数 this
    • static 没有隐藏参数 this
    • 静态变量要在class定义外定义
    • 静态函数可以通过 object 或 class name调用
    • 单例模式的实现
  • cout

    • cout 是一种 ostream
    • 重载了各种类型的 << 方法
  • 类模版class template

  • namespace

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值