文章目录
0. 前言
-
侯捷大佬所有C++课程之一
- 全部课程参考这里
-
本文对应的课程: 面向过程
- 包括第二课到第十课,相关内容主要是以实现 Complex 类与 String 类为目标
- 介绍了类创建的基本语法、思路
-
一些原则:
- 构造函数使用 intialize list(即冒号)
- 成员函数尽可能添加 const(即常量成员函数)
- 参数传递尽量 pass by reference
- 函数返回值尽量 return by reference
- 函数绝大部分在 public,成员变量尽可能放在 private 中
第二课 - 头文件与类的声明
- 区分基于对象(Object Based)和面向对象(Object Oriented)
- C++代码基本形式:
.h(自己写/第三方的头文件) + .cpp(自己写/第三方的源码) + .h(标准代码库,编译器提供)
-
头文件防卫式声明,即
#ifndef/#define/#endif
- 所有头文件都应该使用这种结构
-
头文件布局
- 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
)时会报错,因为第一步就是删除内存。 - 自己删除+新建+拷贝
- 自我检测(自我赋值),如果没有定义在进行自我赋值(如
- 定义时形如
- 析构
- 当对象死亡时会自动调用,名称与类名称相同,前面添加波浪号
~
- 定义时形如
~String();
- 一个主要作用是防止内存泄漏。所谓内存泄漏,就是在对象死亡后,其对应的“动态分配”的内存并没有收回。
- 当对象死亡时会自动调用,名称与类名称相同,前面添加波浪号
- 拷贝构造和拷贝复制的默认实现以及必要性
- 如果没有自己定义,那编译器会默认实现,默认行为就是按照数值一个一个复制,就是浅拷贝。
- 要不要自己重新实现,主要要关注的是成员变量中是否存在指针。
- 如果有指针就必须自己实现,这是因为:
- 对于拷贝构造,如果直接复制指针,会导致内存泄漏(被赋值的指针原本指向的地址空间没人处理了)
- 如下图所示,第一行是定义了两个String对象,第二行就是默认拷贝赋值操作,可以看到这只是浅拷贝,有内存泄漏产生。
// 拷贝构造的形式(下面三个都是)
String s1("hello");
String s2(s1);
String s3 = s1;// 因为创建了新的对象,所以就是利用构造函数实现
- 字符串的两种设计
- PASCAL:前面有常数,指定字符数量,后面跟着字符数量
- C/C++:以特定字符(如
\0
)结尾
第八课-堆,栈与内存管理
-
String 重载
<<
符号必须使用全局函数,而不是成员函数- 如果是成员函数,那么在使用过程中 cout 和 String 对象的位置是相反的,即
str << cout
- 全局函数参数分为两个,一个是 ostream,令一个就是 String 对象
- 为了实现连续操作(如
out << str1 << str2
),重载函数的返回值需要是ostream&
的形式
- 如果是成员函数,那么在使用过程中 cout 和 String 对象的位置是相反的,即
-
堆与栈的定义
-
栈(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)
- 通过
- stack objects:作用域(scope)结束之后结束,会自动调用析构函数。
-
new 的细节:先分配memory,再调用构造函数,具体情况参考下图
operator new
是 C++ 提供的特殊函数,函数名中包含了空格,本质就是C++版的 malloc- sizeof 就是两个成员变量(即两个double),8字节
- 数据转型,即
static_cast
- 调用构造函数。构造函数是一个函数名称与class名相同的函数。构造函数的全名就是
Complex::Complex(pc, 1, 2)
,其中 pc 就是对象本身
- 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