C++面向对象高级编程
笔记按照视频进行分节
¶C++ 编程简介
-
基础
- 学过某个 procedural language(C语言)
- 变量(variables)
- 类型(types):int, float, char, struct
- 作用域(scope)
- 循环(loops):while, for
- 流程控制:if-else, switch-case
- 编译,链接,而后执行
- 如何编译和链接
- 学过某个 procedural language(C语言)
-
目标
- 正规大气的编程习惯
- 以良好的方式编写 C++ class (Object Based)
- class without pointer members
—— Complex - class with pointer members
—— String
- class without pointer members
- 学习 Classes 之间的关系 (Object Oriented)
- Inheritance 继承
- Composition 复合
- Delegation 委托
-
历史
- B 语言 1969
- C 语言 1972
- C++ 语言 1983
(new C -> C with Class -> C++)
-
演化
- C++ 98 (1.0)
- C++ 03 (TR1, Technical Report 1)
- C++ 11 (2.0)
- C++ 14
-
参考书目
¶头文件与类的声明
- C vs. C++
- Data 与 Functions -> Data Members 与 Member Functions
- 把数据与处理数据的函数包在一起
- 一份函数,多份数据
1 | /* complex 类 */ |
1 | /* string 类 */ |
- C++ programs 代码基本形式
- .h(header files) Classes Declaration 声明
- .cpp main()
- .h(header files) Standard Library 标准库
- Output, C++ vs. C
- #include<iostream>
- std::cout
- #include<stdio.h>
- printf
- #include<iostream>
1 |
|
1 |
|
- Header(头文件)中的防卫式声明(guard)
- 避免重复 include
1 | // complex.h |
- Header(头文件)的布局
- 防卫式声明
- forward declarations 前置声明
- class declarations 类-声明
- class definition 类-定义
1 |
|
- class 的声明
- class head
- class body
1 | class complex |
1 | { |
- class template(模板)简介
- 需求:刚刚上面的 complex 类,实部与虚部的类型不想写死
- 将来用的时候可以指定
1 | template<typename T> |
1 | { |
- inline(内联)函数
- inline 有宏的特性,但没有宏的缺点
- 函数太复杂,即便声明成 inline,编译器也不把它 inline
- inline 只是对编译器的建议
1 | class complex |
1 | inline double |
- access level 访问级别
- public
- private
- protected
1 | // 不被允许 |
¶构造函数
- constructor(ctor,构造函数)
- 使用构造函数
- 构造函数名称与类的名称一定相同
- 构造函数可以拥有参数,参数可以有默认值(default argument 默认实参非构造函数独有)
- 没有返回类型
- 拥有 initialize list(初值列,构造函数独有),注意其与 assignments 赋值的区别
1 | // 注意初始化与赋值的时点 |
1 | { |
- 构造函数的重载 overloading
- 重载:相同函数名称却有一个以上
1 | complex (double r = 0, double i = 0) |
1 | double real() const { return re; } |
- ctor 被放在 private 区里
- 不允许被外界创建对象
- 经典:单例模式 Singleton
1 | Class A { |
- const member function(常量成员函数)
- 在 ( ) 与 { } 中间用 const 修饰
- 函数不改变数据内容
- 设计接口时就想好,这个函数不允许改变数据内容,写函数定义时若进行了数据内容的修改,编译器就会报错
- 该写时一定要写
1 | double real() const { return re; } |
¶参数传递与返回值
- 参数传递
- pass by value vs. pass by reference(to const)
- 参数传递最好传引用
- 速度上相当于传指针
- 引用传进来可以被改变,会影响外面,于是用 const 修饰
1 | // 值传递 |
-
返回值传递
- return by value vs. return by reference(to const)
- 返回尽量传引用,尽量而不是一定
-
friend(友元)
- 自由取得 friend 的 private 成员
- 相同 class 的各个 objects 互为 friends(友元)
1 | ... |
1 | class complex |
- class body 外的各种定义(definitions)
- 什么情况下可以 pass by reference
- 什么情况下可以 return by reference
- 引用的本体是局部变量,它的作用域只在函数里,函数结束就无了
- 这时候在返回时传它的引用,外面的调用者就会看到坏东西
1 | // do assignment plus |
¶操作符重载与临时对象
- operator overloading(操作符重载-1,成员函数) this
- this 指针
- 写代码时不要显式的写出来
1 | inline complex& |
- return by reference 语法分析
- 传递者无需知道接收者是以 reference 形式接收
1 | // 返回类型:complex& 实际返回:*ths |
- operator overloading(操作符重载-2,非成员函数) 无 this
- 为了对付 client 的三种可能用法,这儿对应开发三个函数
1 | { |
1 | inline complex |
- temp object(临时对象) typename();
- 上面这三个函数绝不可 return by reference
- 因为它们返回的必定是个 local object
1 | inline complex |
- operator overloading(操作符重载),非成员函数
1 | inline bool |
1 | // 共轭复数 |
- class without pointer members
—— Complex - class with pointer members
—— String - 上面的例子完了,看下面这个
¶三大函数:拷贝构造,拷贝复制,析构
- 考虑实现以下功能
- 字符串构造
- 字符串拷贝
- 字符串赋值
- 用 << 向 cout 输出字符串对象
1 | int main() |
- Big Three,三个特殊函数
1 | class String |
- 构造函数(ctor)与析构函数(dtor)
1 | inline |
- class with pointer members 必须有 copy ctor 和 copy op=
- a 赋值给 b,用 default copy ctor 或 default op= 并不会如我们所愿的有两个相同内容的对象
- 而是看起来两个有相同内容,实际上是两个指针指向同一个地址(alias),是一种浅拷贝
- 若修改 a,则 b 也会跟着受影响
- 并且原来指向的 “world” 不再有任何一个指针能够访问到,成为了一个孤儿,memory leak 内存泄漏
- 这就是为什么一定要写自己的一个版本
- copy ctor(拷贝构造函数)
- 那么深拷贝是什么样的呢?
- 先分配空间,然后把内容拷贝过去,这就是深拷贝
- 编译器的 default copy ctor 只拷贝指针,是浅拷贝
1 | inline |
- copy assignment operator(拷贝赋值函数)
- 拷贝赋值的步骤:清空、开足空间、复制内容
- 一定要在 operator= 中检测是否 self assignment
- 如果一开始左与右的 pointer 就是指的同一个 memory block
- 前述 operator= 的第一件事情就是 delete,那么两个其实都没有了
- 当企图存取(访问)右边 pointer 指向的对象时,将产生不确定行为(undefined behavior)
1 | inline |
- 符号重载 <<
1 |
|
¶堆、栈与内存管理
- 所谓 stack(栈),所谓 heap(堆)
- Stack,是存在于某作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个 stack 用来放置它所接收的参数,以及返回地址
在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述 stack - heap,或谓 system heap,是指由操作系统提供的一块 global 內存空间,程序可动态分配 (dynamic allocated) 从某中获得若干区块 (blocks)
- Stack,是存在于某作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个 stack 用来放置它所接收的参数,以及返回地址
1 | class Complex { ... }; |
-
stack objects 的生命期
- c1 便是所谓 stack object,其生命在作用域(scope)结束之际结束
这种作用域内的 object,又称为 auto object,因为它会被“自动”清理
- c1 便是所谓 stack object,其生命在作用域(scope)结束之际结束
-
static local objects 的生命期
- c2 便是所谓 static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束
-
global objects 的生命期
- c3 便是所谓 global object,其生命在整个程序结束之后才结束。其作用域是整个程序
1 | class Complex { ... }; |
- heap objects 的生命期
- p 所指的便是 heap object,其生命在它被 deleted 之际结束
- 若没有 delete p,将会出现内存泄漏(memory leak)
- 因为当作用域结束,p 所指的 heap object 仍然存在,但指针 p 的生命值却结束了
作用域之外再也看不到 p(也就没机会 delete p)
- 因为当作用域结束,p 所指的 heap object 仍然存在,但指针 p 的生命值却结束了
1 | class Complex { ... }; |
- new:先分配 memory,再调用 ctor
- delete:先调用 dtor,再释放 memory
- 注意正好相反
1 | Complex* pc = new Complex(1, 2); |
- 注意,这里的析构函数并没有做什么事情,因为我们这个复数对象里面就两个 double 的变量,析构完了就要把它删掉了,也就不需要 dtor 做什么事情
- 但是在 String 里,情况就发生了变化
- 调用 dtor 要把 m_data 指向的它动态分配的内存里的东西删掉
- 至于 String 对象本身,它里面只是一个指针而已,在 delete 这个对象的时候,只是删掉了这个指针
- 上面介绍的分配内存和释放内存,实际上使用的是 C 语言的 malloc 和 free
- 那么具体分配的情况是怎样?
- 动态分配所得的内存块(memory block),in VC
- 这是调试模式下(Debug)与构建模式下(Release)的区别(1 格为 4 个 Byte)
注意:这里的 double 为 4 字节,pointer 为 4 字节,而在现在普遍的机器(x86_64)上两者皆为 8 字节- 调试模式
- 在 Debug 模式下 new 一个 Complex
- 光一个 Complex,里面只有两个 double,共 8 字节
- 在调试模式下,会多给你灰色的区域,我们看到,上面 32 字节,下面 4 字节
- 最上面和最下面红色区域的叫做 cookie,大小为 4 * 2 字节
- 这就总共有了 52 字节,而在 VC 中,分配的内存块一定是 16 的倍数
- 所以还会有 12 个字节的填充(绿色区域)
- 虽然我们只要存两个 double 共 8 个字节,但是在调试模式下,一共分配了 64 个字节给我们,需要知道的是,这是一种必要的浪费
- 构建模式
- 在 Release 模式下 new 一个 Complex
- 将灰色的区域先去掉(不需要 debugger header),重新来算
- 总共 8 + 4 * 2 = 16 个字节,不需要进行填充
- cookie 的作用
- 记录分配的整块空间的大小
在调试模式下,分配 64 字节,即 0x40,在构建模式下,分配 16 字节,即 0x10
但与图中实际情况不符,图中为 0x41 与 0x10 - 借最后一位 bit 来表示这块空间是分配出去了(1)还是已经收回来(0)
- 为什么能借位,因为分配的空间一定是 16 的倍数,这意味着后 4 位都没有意义(正常都是 0)
- 记录分配的整块空间的大小
- 调试模式
- 如果分配的是数组,那又是何种情况呢?
- 动态分配的所得的 array
- 可以看到,在数组这边有一点不同
- 在 Debugger Header 与 data 中间,有一个空间用来记录数组的大小
- array new 一定要搭配 array delete
- 用 delete 代替 delete[],为对象分配的空间会正常回收
- 但 delete 只调用 1 次析构函数,而原本的 delete[] 会调用多次析构函数
- 每一次调用析构函数都应该会将数组其中一个元素它动态申请的内存空间回收
- 用 delete 便只回收一次(回收了数组首个元素动态申请的内存空间)
- 这样就造成了内存泄漏
- 那么如果是 Complex 数组是不是就用 delete 也不会出错?
- 是的,但不推荐
1 | String* p = new String[3]; |
¶String 类的实现过程
- String 类的实现过程
- 设计一个 class,先思考需要什么样的数据
- 字符串会放很多的字符,一种想法是,用数组存储,但无法确定大小
- 放一个指针,动态分配内存
- 在 32 位的机器中,pointer 为 4 Bytes,在 64 位机器中为 8 Bytes
- 接下来思考,要准备哪些函数,开放给外界调用
- 首先就是(同名的)构造函数
- 其次就是 Big Three:拷贝构造、拷贝复制、析构
- 设计一个辅助函数 get_c_str(),用于将字符串送入 std::cout
- 注意函数中的 const
- 在实现时尽量作 inline 的建议
- 设计一个 class,先思考需要什么样的数据
1 | class String |
- 接下来看 ctor 与 dtor 如何进行设计
- ctor
- 需要足够的空间来放初值
- 若未指定初值,应进行处理
- 注意用 strlen 与 strcpy 需引入 cstring 库
- dtor
- 用 array delete
- ctor
1 | inline |
- 再看拷贝构造函数 copy ctor
- 从来源端(参数)拷贝到目的端(该对象)
- 拷贝赋值函数 copy assignment operator
- 判断是否自我赋值(来源端与目的端相等)
- 三步走:清空、开足空间、复制内容
- 返回 this 指针
1 | inline |
¶进一步补充:static
- static
- static data members
不管有几个对象,都只有一份数据
一定在类外面进行定义 - static member functions
没有 this pointer,只能处理 static data members
- static data members
1 | class Account { |
- 调用 static 函数的方式有二
- (1) 通过 object 调用
- (2) 通过 class name 调用
- 在单例模式中,很好的使用了 static 的特性
- 用 static 在 private data members 中创建一个对象
并且将类的构造函数放在 private 区,于是保证了始终只有一个对象 - 再设计一个静态函数 getInstance,作为外界获得这个对象的唯一接口
- 用 static 在 private data members 中创建一个对象
1 | class A { |
- 进一步的,我们有 Meyers Singleton
- 把 static 对象的创建放到 getInstance 函数中去
只有使用时才会创建,不使用时不占用空间
- 把 static 对象的创建放到 getInstance 函数中去
1 | class A { |
¶进一步补充:cout
- cout 能够接收多种数据,并打印到终端
- 继承自 ostream
- 作了很多的操作符重载
1 | class _IO_ostream_withassign |
1 | class ostream : virtual public ios |
¶进一步补充:template
- class template
- 类型不能提前确定,那就不把它写死
- T 仅仅是一个符号
- 编译器会将 T 替换为使用者指定的类型,得到一份新的代码
- 模板会造成代码的膨胀,但是必要的
1 | template<typename T> |
1 | { |
- function template
- 编译器会对 function template 进行引数推导(实参推导)
- 算法写成 function template 的形式
1 | stone r1(2, 3, 4), r2(3, 3, 6), r3; |
¶进一步补充:namespace
- namespace
- 标准库的命名空间 std
- using directive
- using declaration
1 | namespace std |
1 |
|
¶面向对象编程
- Object Oriented Programming, Object Oriented Design (OOP, OOD)
- Inheritance (继承)
- Composition (复合)
- Delegation (委托)