【侯捷】C++面向对象程序设计
本系列相关链接:
【侯捷】C++11
0. 课程目标
- 培养正规、大器的编程习惯
- 良好方式编写class,做“有生产力”的开发工程师
- 学习class之间的关系
c++98 (1.0)
c++11 (2.0) <–目前大多数公司使用,成熟、通用
c++14 c++17 c++20
C++包括C++语言、C++标准库;
C++class分两种,一种带指针(资源)(例如string),一种不带指针(例如complex),这个区别对class的设计影响深远。
侯捷老师的该课程共有13节,本文按课程顺序重新编节。
推荐书籍:
C++ Primer 中英文
Effective C++ 中英文(中文侯捷出品)
Effective Modern C++ 中英文,这个只讲C++2.0
THE C++ STANDARD LIBRARY,STL源码剖析(侯捷出品)
C vs C++ 关于数据与函数
C语言面向过程,数据和函数分开;
C++语言面向对象,数据和处理数据的函数封装成class,class可以实例化多个对象,每个对象有独立的数据部分,而成员函数只有一份,每个对象通过函数指针和this指针共用成员函数。
Object Based vs. Object Oriented
基于对象,面对的是单一class的设计;
面向对象,面对的是多重classes的设计,考量classes之间的关系;
1. 基于对象
1.1 头文件与类的声明
1.1.1 头文件
C++代码基本形式
标准库头文件,#include<> 尖括号;
用户头文件,#include""双引号;
编译器查找顺序,见博文:C++语言;
头文件防卫式声明(declaration)
#ifndef和#pragma,见博文:C++语言;
头文件布局
防卫式声明+标准库头文件+用户头文件+前置声明+类声明+类定义(?)
推荐做法:在头文件声明class,在源文件定义class;–>防止定义多次展开;
1.1.2 类的声明
class template
模板类,用占位符将type的指定延后到实例化对象;
class的声明
有些成员函数在class body中定义;<–自动inline
有些成员函数在class body以外定义,可能是头文件,也可能是源文件;
<–区别?
内联函数
见博文:C++语言;
访问级别
见博文:C++语言;
1.2 构造函数
1.2.1 Big Six
默认构造函数
拷贝构造函数
移动构造函数
拷贝赋值运算符
移动赋值运算符
析构函数
class A { //=delete, =default;
public:
A() {...} //构造
A(const A& ) {...} //拷贝构造
A(A && ) {...} //移动构造
A& operator=(const A& a) {...} //拷贝赋值
A& operator=(A&& other) {...} //移动赋值
~A() {...} //析构
};
1.2.2 初始化列表
构造函数独有;
初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
1.2.3 构造函数的overloading(重载)
函数名相同,但函数签名不完全相同,则可以构成重载,这些函数同时存在:
- 函数名相同
- 返回值类型不看
- 形参列表个数、顺序、类型有不同
- const
-const在函数尾部,属于函数签名,加const指明不修改入参;
-const在返回值,不看;
-const在传值形参,不看,因为改不改传值没意义;
-const在传引用形参,属于函数签名;
注意与override(覆盖)区分,override是在面向对象角度的类多态中提出的,派生类通过对基类中虚函数覆盖来达到动态绑定的效果。要构成覆盖的条件是:
派生类的函数原型必须完全覆盖基类中的声明,包括:形参列表、返回类型、常量性(cv限定符)。
1.2.4 放在private的构造函数
单例模式,private配合static实现:
class A {
public:
static A& getInstance();
setup() {...}
private:
A(); //放在private的构造函数
A(const A& rhs); //拷贝构造
...
};
A& A::getInstance()
{
static A a; //被调用时才唯一一次创建
return a;
}
1.3 成员函数
1.3.1 常量成员函数
函数末尾加const,指明该成员函数不修改成员数据;
如果该加不加,但是用户在实例化时指定了对象是const,则会编译报错:
class complex
{
public:
double real() const { return re;}
private:
double re;
}
///////
const complex c1(2,1);
cout << c1.real(); //如果上面没定义为常量成员函数,则此处报错;
1.3.2 传值 vs. 传引用
数据占内存很大的时候,传值代价很大,在C语言中可以传指针,在C++语言中推荐使用引用;
引用与指针的区别见:博文:C++语言;
引用的传递也要注意指定是否const,是否修改引用的内容;
形参如此,返回值也如此!
如果函数返回值是在函数内部创建出来的,则不能传引用出去,因为出了函数这个东西就死掉了;
1.4 友元
友元打破c++的封装性,但取数据比函数方式更快;
class complex
{
private:
double re;
friend complex& __doapl(complex*, const complex&);
}
///////
inline complex& __doapl(complex* a, const complex& b) {
a->re += b.re; //哪怕是私有成员变量,友元函数也可以直接取数据
return *a;
}
相同class的各个对象互为friends;
class complex
{
public:
int funcA(const complex& param) {return param.re + param.im;}
private:
double re, im;
}
///////
complex c1(2,1);
complex c2;
c2.func(c1); //此处,c2和c1互为友元,所以c2能直接取c1的私有数据;
1.5 操作符重载
c++中操作符就是一种函数,因此操作符也可以重载overloading;
1.5.1 this指针
所有的成员函数都带有一个隐藏的this指针,可以直接使用;
class complex
{
public:
inline complex& // 有返回值,注意1.5.3
operator += (const complex& r) // 接收者1
{return __doapl(this, r);}
inline complex& // 接收者2
__doapl(complex* ths, const complex& r)
{...; return *ths;} //传递者2
}
///////
c2 += c1; // 传递者1
1.5.2 reference的传递与接受
传递者无需知道接收者是否以reference形式接收;
1.5.3 连续操作
用户可能连续操作,因此操作符重载函数的返回值不应该写成void;
c3 += c2 += c1;
1.5.4 临时对象
typename(),没有名称,它的生命在下一行就消失;
例如:
complex c1(2,1);
complex(3,4); // 临时对象,标准库中常用于return语句;
1.5.5 成员函数重载vs.全局函数重载
complex c1(3,1);
cout << conj(c1);
cout << c1 << conj(c1);
<<作用于cout,因此,<<的重载必须是全局函数形式;
ostream& // 为了能连续操作,返回值类型不能是void,同+=的重载
operator << (ostream& os, const complex& x)
{
return os << '(' << x.real() << ',' << x.img() << ')';
}
2. 带资源的基于对象
class的成员变量包含指针、资源,那么就要额外管理这份资源;
2.1 Big Three
书籍中提到的“Big Three"指:拷贝构造(创建新对象)、拷贝赋值、析构;
String s1("hello");
String s2(s1);
String s2 = s1; //等同于上一行,s2是新创建的对象,因此此处调用拷贝构造函数,而不是拷贝赋值函数
String s2;
s2 = s1; //调用拷贝赋值函数
编译器提供的默认拷贝构造、拷贝赋值,只是忠实的浅拷贝。
2.1.1 析构函数
inline String::String(const char* cstr = nullptr)
{
if(cstr == nullptr) {
m_data = new char[1];
*m_data = '\0';
} else {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr); //深拷贝
}
}
inline String::~String() { delete[] m_data;} //注意delete[]释放数组
2.1.2 拷贝构造
只要做一次深拷贝就可以了;
2.1.3 拷贝赋值
另外,拷贝赋值要注意:自拷贝、原数据释放、新数据深拷贝。
inline String& String::operator=(const String& str)
{
if(this == &str) { return *this;} //比如指针换名字了,子类父类传了多次等
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1]; //没做自拷贝检测的话,这里已经跑飞了,error
strcpy(m_data, str.m_data);
return *this;
}
2.2 栈stack与堆heap
2.2.1 stack/heap/static/global
stack,是存在于某scope的一块内存空间。例如调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。
在函数体内声明的任何变量,其所使用的内存块都取自上述stack。
因此,超出这个scope后,其生命自动结束。
heap,是指由操作系统提供的一块global的内存空间,程序可动态分配从其中获得若干区块(block)。
因此,手动申请后,还需要主动释放。
global全局变量,其生命在作用域结束后仍然存在,直到整个程序结束,且可以用extern跨文件;
详见:博文:C++语言;
static 局部变量,其生命在作用域结束后仍然存在,直到整个程序结束,但不能跨文件;
静态成员变量只有一份,所有实例、类本身都访问同一个;
静态成员变量的定义必须在class外部;
静态成员函数没有this指针,只用于存取静态数据,可以通过对象访问,也可以通过class name访问;
class Account{
public:
static double m_rate;
static void set_rate(const double& x);
};
double Account::m_rate = 8.0;
//////
Account::set_rate(5.0); //注意不是.,是::
Account a;
a.set_rate(3.0);
2.2.2 new/delete
new三步:
- 调malloc申请内存;
- static_cast数据类型转换;
- 调用构造函数;
delete两步:
- 调用析构函数;
- 调free释放内存;
调试模式下,new得到多少内存?假设一个复数本身占用8字节,则new时:
首尾各4字节cookie,32字节调试信息,8字节数据,4字节no man land,12字节填充 = 64字节;
release模式下,首尾各4字节cookie,8字节数据,16字节对齐 = 64字节;
cookie用于释放内存时计算起止地址;
64字节用十六进制表示就是0x40,最后一个bit的1表示这块内存被分配出去了正在使用。如果内存被还给操作系统,则最后一个bit就会变成0;

2.2.3 array new/delete[]

array new如果不搭配delete[]而是delete,则整块内存仍然被释放,但是第一个资源后面的所有资源都没有被析构,内存泄漏!
2.3 更多细节
以下细节也很重要,详见【侯捷】c++11。

3. 面向对象
3.1 复合、继承、委托
3.1.1 复合
复合,就是A中有B,构造由内而外;composition, has a
复合,构造由内而外,析构由外而内,这些代码是编译器为你自动添加的、调用默认函数,如果不是你想要的,就得自己指定。
下例中,已有一个强大的class A,现在把它做一定限制,形成一个限制版本的class B,这种设计模式称为”装饰器“。
template <typename T>
class queue {
protected:
deque<T> c;
}

3.1.2 委托
委托,就是A中有指向B的指针;delegation, composition by reference
这种设计模式称为"pimpl",或者Handle and Body,“编译防火墙”;

一个非常常见的用法,是智能指针的引用计数。
共享很好,但是如果想修改指向的值,需要单独拷贝一份出来给它改,这种设计模式称为copy-on-write,或者“写时拷贝”,COW。

3.1.3 继承
继承,A is a B;空心三角箭头,由子类指向父类;
继承,构造先调用父类的构造函数,再构造自己;析构先执行自己,再调用父类的析构函数:
Derived::Derived(...) : Base(){...};
Derived::~Derived(){...; ~Base(); };

public/private/protect继承方式见:博文:C++语言;
- 公开继承:该访问说明符之后列出的基类的公开和受保护成员在派生类中保持其访问级别,而基类的私有成员对派生类不可访问
- 受保护继承:该访问说明符之后列出的基类的公开和受保护成员在派生类中是受保护成员,而基类的私有成员对派生类不可访问
- 私有继承:该访问说明符之后列出的基类的公开和受保护成员在派生类中是私有成员,而基类的私有成员对派生类不可访问
3.2 多态、虚函数
3.2.1 virtual函数
- 非虚函数:你不希望子类重新定义(覆盖,复写,override)它;
- 虚函数:你希望子类重新定义它,但也可以用父类已有的默认定义;
父类的析构函数必须是虚函数; - 纯虚函数:你希望子类一定要定义它;
继承和虚函数搭配,非常强大的多态;
先写大框架,而把具体的关键步骤延缓到子类的实现,这种设计模式称为“Template Method",大名鼎鼎;一般性的框架不是核心竞争力,而在于你的领域知识(domain knowledge)。
3.2.2 继承+复合

情况1.
构造:Base, Component, Derived;
析构:Derived, Component, Base;
情况2.
构造:Component, Base, Derived;
析构:Derived, Base, Component;
3.2.3 委托+继承
Composite
这是最经典、超强大的设计模式”Composite";

Prototype
为了创建未来的子类的对象,设计模式称为“Prototype”,出处《Design Patterns Explained Simply》。
下图中static用下划线表示;-表示私有,#表示保护,+表示公有;
静态变量:数据类型
函数名():返回值类型

1497



被折叠的 条评论
为什么被折叠?



