0. 前言
-
侯捷大佬所有C++课程之一
-
本文对应的课程: 面向过程
- 包括第二课到第十课,相关内容主要是以实现 Complex 类与 String 类为目标
- 介绍了类创建的基本语法、思路
-
一些原则:
- 构造函数使用 intialize list(即冒号)
- 成员函数尽可能添加 const(即常量成员函数)
- 参数传递尽量 pass by reference
- 函数返回值尽量 return by reference
- 函数绝大部分在 public,成员变量尽可能放在 private 中
第二课 - 头文件与类的声明
- 区分基于对象(Object Based)和面向对象(Object Oriented)
- C++代码基本形式:
.h(自己写/第三方的头文件) + .cpp(自己写/第三方的源码) + .h(标准代码库,编译器提供)
- class 的声明(即上图中的
1 部分),设计有哪些数据、哪些操作 - class template 简介,即模版
- 如一个复数类(Complex),拥有两个参数,分别表示实部与虚部。
- 实部与虚部的数据类型不确定,可能都是 int、都是 float、都是 double。
- 为了实现上面这个需求,就可以使用模版类。
第三课-构造函数
第四课-函数传递与返回值
-
构造函数放在 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 )时会报错,因为第一步就是删除内存。 - 自己删除+新建+拷贝
- 析构
- 当对象死亡时会自动调用,名称与类名称相同,前面添加波浪号
~ - 定义时形如
~String(); - 一个主要作用是防止内存泄漏。所谓内存泄漏,就是在对象死亡后,其对应的“动态分配”的内存并没有收回。
- 拷贝构造和拷贝复制的默认实现以及必要性
- 如果没有自己定义,那编译器会默认实现,默认行为就是按照数值一个一个复制,就是浅拷贝。
- 要不要自己重新实现,主要要关注的是成员变量中是否存在指针。
- 如果有指针就必须自己实现,这是因为:
- 对于拷贝构造,如果直接复制指针,会导致内存泄漏(被赋值的指针原本指向的地址空间没人处理了)
- 如下图所示,第一行是定义了两个String对象,第二行就是默认拷贝赋值操作,可以看到这只是浅拷贝,有内存泄漏产生。
String s1("hello");
String s2(s1);
String s3 = s1;
- 字符串的两种设计
- PASCAL:前面有常数,指定字符数量,后面跟着字符数量
- C/C++:以特定字符(如
\0 )结尾
第八课-堆,栈与内存管理
class Complex {...};
...
{
Complex c1(1, 2);
Complex c2 = new Complex(3);
}
- delete 的细节:先调用析构函数,再释放内存
- 析构函数删除指针指向的内容,即动态内存。这个析构函数是自己定义,而不是默认的。
operator delete 是C++的一个特殊函数,函数名包括空格,用来释放内存。
- 动态分配内存的底层细节,即 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)
- 数组动态分配内存的底层细节
- 最左边的介绍一下:首尾两个cookie各4字节,debug 模式相关的是32+4字节,保存数组长度(即图中的3)需要4字节,实际三个Complex对象,需要8*3字节,pad 8字节达到16的倍数
char *data = new char[10] 需要搭配 delete[] data ,为什么
- 左边的图,使用
delete[] p 就会调用三次析构函数 - 右边的图,使用
delete p ,只调用一次析构函数(注意,调用了析构函数)
第九课-复习String类的实现过程
第十课-扩展补充:类模版,函数模版,及其他
-
static
- 普通成员函数有隐藏参数 this
- static 没有隐藏参数 this
- 静态变量要在class定义外定义
- 静态函数可以通过 object 或 class name调用
- 单例模式的实现
-
cout
- cout 是一种 ostream
- 重载了各种类型的
<< 方法 -
类模版class template -
namespace
|