C++面向对象高级编程学习笔记

本章内容

本章为侯捷视频之面向对象高级编程的学习笔记。

参数传递与返回值

构造函数

构造函数放在 private 区,如下代码,这样就无法被外界创建对象,可以用在单例模式里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public:
static A& getInstance();
setup() { ... }
private:
A();
A(const A& rths);
...
};

A& A::getInstance()
{
static A a;
return 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
2
3
4
5
inline MyString::MyString(const MyString& str)
{
m_data = new char[ strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}

拷贝赋值函数

如果我们想将 B 拷贝给 A ,拷贝赋值函数的过程:

  • 先将 A 内的值清空
  • 在 A 内分配与 B 一样大的空间
  • 再将 B 赋值给 A
1
2
3
4
5
6
7
8
9
10
inline MyString & MyString::operator=(const MyString& str)
{
if (this == &str)
return *this;

delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}

栈、堆和内存管理

栈,存在于某作用域的一块内存空间。例如调用函数,函数本身即会形成一个用来放置它所接收的参数,以及返回地址。
在函数本体内声明的任何变量,其所使用的内存块都取自上述栈。

或者称为系统堆,是指由操作系统提供的一块全局内存空间,程序可动态分配从其中获得若干区块,从堆上动态分配的内存需要手动释放。

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
2
3
4
5
6
class Account{
public:
static double m_rate; //声明
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0; //定义

组合和继承

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 无需重新编译
  • 二进制兼容性

image-20220401204540652

Inheritance(继承)关系下的构造和析构

构造由内而外,先调用父类的 default 构造函数,然后才执行自己的构造函数。
析构由外而内,先执行自己的析构函数,然后才调用父类的析构函数。
注:父类的析构函数最好设为虚函数。

虚函数与多态

Inheritance (繼承) with virtual functions (虛函數)

non-virtual 函数:不希望 derived class 重新定义 (override, 覆写) 。
virtual 函数:希望 derived class 重新定义(override, 覆写))它,且对它已有默认定义。
pure virtual 函数:希望 derived class 一定要重新定义 (override 覆写) 它,对它没有默认定义。

1
2
3
4
5
6
7
8
9
10
class Shape {
public:
virtual void draw() const = 0; //pure virtual
virtual void error(const std::string& msg); //impure virtual
int objectID() const; //non-virtual
...
}

class Rectangle: public Shape {...};
class Ellipse: public Shape {...};

Inheritance+Composition 关系下的构造和析构

如下图,上方,构造的顺序是 Base -> Component -> Derived,析构的顺序是 Derived -> Component -> Base
下方,构造的顺序是 Component -> Base -> Derived,析构的顺序是 Derived -> Base -> Component

image-20220401212715119

Delegation (委托) + Inheritance (继承)

以下是两种经典的设计模式:Composite 和 Prototype

Composite

Prototype