C++面向对象高级编程学习笔记
本章内容
本章为侯捷视频之面向对象高级编程的学习笔记。
参数传递与返回值
构造函数
构造函数放在 private
区,如下代码,这样就无法被外界创建对象,可以用在单例模式里。
1 | class A { |
常量成员函数
不会改变成员变量的函数,声明的时候,在其 ()
和
{}
之间加上 const
,如果不加上
const
,按下图右边的方式定义,则会产生以下报错。
原因是我们定义了一个常量 complex
类型的 c1
,不允许对其进行修改,但是我们定义的 real()
和
imag()
函数没有加 const
,编译器认为这个函数可能会改变类的成员变量,因此有矛盾,故报错。
参数传递:pass by value vs. pass by reference(to const)
尽量不用 pass by value
,因为使用这种方式传大数据的话,会将整个大数据压到栈里面,开销比较大。
建议使用 pass by
reference(引用),引用在底层的实现就是指针,所以会比较快。同时,如果只是为了提高速度使用引用而不希望函数修改传递的参数,需要在前面加上
const
。
返回值传递:pass by value vs. pass by reference(to const)
返回值尽量使用引用,非必须。
友元
相同 class 的各个 objects 互为 friends(友元),可以直接使用成员变量。
操作符重载与临时对象
返回值尽量使用引用,非必须。
当返回值是局部对象,不应该使用引用,因为在离开函数后,这个对象就会被释放。如下图,typename()
会创建一个 typename
类型的临时对象,运行至下一行就会被释放,这里
typename
指任意类型。
拷贝函数、拷贝赋值函数、析构函数
拷贝构造函数
如果类里面存在指针成员,必须要重写拷贝构造函数和拷贝赋值函数。
如果不重写这些函数,C++默认的拷贝构造函数和拷贝赋值函数是逐字节复制(浅拷贝),如下图所示,会出现
a 和 b 同时指向一个地址,同时 b
原先指向的地址存在内存泄漏的风险。因此我们需要使用深拷贝。
1 | inline MyString::MyString(const MyString& str) |
拷贝赋值函数
如果我们想将 B 拷贝给 A ,拷贝赋值函数的过程:
- 先将 A 内的值清空
- 在 A 内分配与 B 一样大的空间
- 再将 B 赋值给 A
1 | inline MyString & MyString::operator=(const MyString& str) |
栈、堆和内存管理
栈
栈,存在于某作用域的一块内存空间。例如调用函数,函数本身即会形成一个栈用来放置它所接收的参数,以及返回地址。
在函数本体内声明的任何变量,其所使用的内存块都取自上述栈。
堆
或者称为系统堆,是指由操作系统提供的一块全局内存空间,程序可动态分配从其中获得若干区块,从堆上动态分配的内存需要手动释放。
static local objects的生命期
其生命在作用域结束后仍然存在,直到整个程序结束。
new
new:先分配memory,再调用构造函数。
delete
delete:先调用析构函数,再释放memory。
如下图,首先调用析构函数,将字符串里面动态分配的内存释放,之后再调用
operator delete
删除字符串本身(指针)。
动态分配所得的内存块(memory block), in VC
如下图,绿色区域是定义的变量大小,灰色区域是 Debug 模式下分配的大小,青绿色区域是内存补齐的空间,红色区域是cookie,便于系统判断内存块的起始位置。
动态分配所得的 array
如下图,在 VC 中分配长度为 3 的 Complex,内存块包含上下的cookie、Debug下的内存块、3个 Complex 变量的内存、一个 int 来记录数组长度。
array new 一定要搭配 array delete
使用 new []
要搭配 delete[]
使用,如下图,左边可以将 3 个 String 变量都释放,而右边只用了
delete
,会导致系统只释放第一个 String
,后面两个不会被释放。
类模板、函数模板等
static
写的一行代码使得变量获得内存称之为定义,与声明的区别如下。
1 | class Account{ |
组合和继承
Composition(复合)关系下的构造和析构
Container的构造由内而外,析构由外而内。
Delegation(委托)/Composition by reference
如下图所示,在左边的类中包含 StringRep
的指针,对于
String
类的实现都放在 StringRep
类中,这样称为Delegation(委托),也是pImpl 模式。优点如下:
- 类方法定义与函数分离,适合作为API使用 类的实现对用户来说完全是黑盒,在头文件中声明的类仅包含对用户有用的信息。
- 加快编译速度 a.hpp 定义了类A,b.cpp 调用了类 A 的方法。当 A 的方式实现变动时,传统方式 b.cpp 需要重新编译,PIMPL 模式下类 A 的方法实现变动对外透明,b.cpp 无需重新编译
- 二进制兼容性
Inheritance(继承)关系下的构造和析构
构造由内而外,先调用父类的 default
构造函数,然后才执行自己的构造函数。
析构由外而内,先执行自己的析构函数,然后才调用父类的析构函数。
注:父类的析构函数最好设为虚函数。
虚函数与多态
Inheritance (繼承) with virtual functions (虛函數)
non-virtual 函数:不希望 derived class 重新定义 (override, 覆写)
。
virtual 函数:希望 derived class 重新定义(override,
覆写))它,且对它已有默认定义。
pure virtual 函数:希望 derived class 一定要重新定义 (override 覆写)
它,对它没有默认定义。
1 | class Shape { |
Inheritance+Composition 关系下的构造和析构
如下图,上方,构造的顺序是 Base -> Component ->
Derived,析构的顺序是 Derived -> Component -> Base
下方,构造的顺序是 Component -> Base -> Derived,析构的顺序是
Derived -> Base -> Component
Delegation (委托) + Inheritance (继承)
以下是两种经典的设计模式:Composite 和 Prototype