本笔记根据侯捷老师的课程整理而来:C++面向对象高级编程(上)
pdf版本笔记的下载地址: 笔记01_面向对象高级编程(上),排版更美观一点(访问密码:3834)
侯捷C++课程笔记01: 面向对象高级编程(上)
基于对象(Object Based)的程序设计
不带有指针成员变量的类——以复数类complex
为例
头文件的结构
头文件complex.h
的结构如下,主要分为4个部分
-
防卫式声明,防止头文件被重复包含:
-
前置声明: 声明头文件中用到的类和函数
-
类声明: 声明类的函数和变量,部分简单的函数可以在这一部分加以实现
-
类定义: 实现前面声明的函数
访问级别
C++的访问级别有3种,这个程序中展示了两种:
访问级别 | 意义 |
---|---|
private | 只能被本类的函数访问 |
protected | 能被本类的函数和子类的函数访问 |
public | 可以被所有函数访问 |
类的声明内可以交叉定义多个不同级别的访问控制块:
class complex
{
public:
// public访问控制块1...
private:
// private访问控制块1...
public:
// public访问控制块2...
}
函数设计
内联函数
在类声明内定义的函数,自动成为inline
函数;在类声明外定义的函数,需要加上inline
关键字才能成为inline
函数.
类声明内定义的函数,自动成为inline 函数 | 类声明外定义的函数,需要加上inline 关键字才能成为inline 函数 |
---|---|
inline
只是编程者给编译器的一个建议,在编译时未必会真正被编译为inline
函数.因此如果函数足够简单,我们就把它声明为inline
就好了.
构造函数
与其他语言类似,C++的构造函数也可以有默认实参.C++构造函数的特殊之处在于列表初始化(initialization list).
上面两种构造函数的效果是一样的,但是使用列表初始化的效率更高,应尽量使用列表初始化.
默认实参使得类的使用者更灵活地创建对象.
complex c1(2,1); // complex(2, 1)
complex c2; // complex(0, 0)
complex* p = new complex(4); // complex(4, 0)
默认实参不应造成歧义,下图定义的两个构造函数会造成歧义,使得编译失败:
complex c1(2, 1); // 这一行编译通过
complex c2; // 这一行编译失败: error: call of overloaded 'complex()' is ambiguous
常量成员函数
若成员函数中不改变成员变量,应加以const
修饰
若这类函数不加以const
修饰,则常量对象将不能调用这些函数:
const complex c(2, 1); // 定义常量变量
c.real(); // 若 real() 函数不加以const修饰,则编译时会报错: error: passing 'const complex' as 'this' argument
参数的值传递和引用传递
为了提高效率,使用引用传递参数,避免了参数的复制.若不希望在函数体内对输入参数进行修改,应使用const
修饰输入参数
函数的参数应尽量使用引用传递.
返回值的值传递和引用传递
为提高效率,若函数的返回值是原本就存在的对象,则应以引用形式返回.
若函数的返回值是临时变量,则只能通过值传递返回.
友元
友元函数不受访问级别的控制,可以自由访问对象的所有成员.
同一类的各个对象互为友元,因此在类定义内可以访问其他对象的私有变量
complex c1, c2;
c2.func(c1); // 因为c1和c2互为友元,因此c2可以在func()函数内调用c1的私有变量
操作符重载
在C++中的操作符重载有两种形式,一种是在类内声明public
函数实现操作函重载(这种情况下,操作符是作用在左操作数上的);另一种是在类外声明全局函数实现操作符重载.
例如对于如下语句,有两种方式都可以实现操作符+
的重载.
complex c1;
c1 + 2; // 需要重载操作符 +
- 在类内声明
public
函数complex::operator += (int)
- 在类外声明全局函数
complex operator + (const complex&, double)
这两种方式均可以实现操作符重载,为便于调用该类的用户使用,不同的操作符使用不同的方式进行重载
在类内声明public
函数重载+=
重载+=
函数的complex::operator += (const complex& r)
的输入参数和输出参数均使用引用传值,输入参数在函数中不应被改动,因此使用const
修饰.
输出参数类型为complex&
,这是为了支持将多个+=
操作符串联起来.若返回参数类型设为void
,也支持形如c2+=c1
的运算,但不支持形如c3+=c2+=c1
的运算.
函数体内调用友元函数__doapl(complex *, const complex &)
实现功能,其第一个参数接收成员函数内隐含的this
指针,其内容在函数中会被改变;第二个参数接收重载函数的参数,该参数在函数中不会被改变,以const
修饰.
从这个例子中也可以看出使用引用传递参数和返回值的好处在于传送者无需知道接收者是否以引用形式接收,只需要和值传递一样写代码就行,不需要改动.
在类外声明或函数重载+
考虑到+
操作符有三种可能的用法如下:
complex c1(2,1);
complex c2;
c2 = c1 + c2; // 用法1: complex + complex
c2 = c1 + 5; // 用法2: complex + double
c2 = 7 + c1; // 用法3: double + complex
因为重载操作符的成员函数是作用在左操作数上的,若使用类内声明public
函数重载操作符的方法,就不能支持第3种用法了.因此使用在类外声明函数重载+
运算符.
这3个函数返回的是局部对象(local object),在退出函数时对象就会被销毁,因此不能使用引用传递返回值.
在类外声明函数重载<<
与重载+
的考虑方法类似,<<
操作符通常的使用方式是cout<<c1
而非c1<<cout
,因此不能使用成员函数重载<<
运算符.
考虑到形如cout<<c1<<c2<<c3
的级联用法,重载函数的返回值为ostream&
而非void
.
总结:在编写类的时候应该注意的5件事
在编写类的时候应该注意的5件事,通过这5件事可以看出你写的代码是否大气:
-
构造函数中使用列表初始化(initialization list)为成员变量赋值.
-
常量成员函数使用
const
修饰. -
参数的传递尽量考虑使用引用传递,若函数体内不改变传入的参数,应加以
const
修饰. -
返回值若非局部变量,其传递尽量考虑使用引用传递,
-
数据放入
private
中,大部分函数放入public
中.
带有指针成员变量的类——以字符串类String
为例
String
类定义在头文件string.h
中,其结构与complex.h
类似.
类声明如下,使用指针成员变量m_data
管理String
类中的字符串数据.
3个特殊函数:拷贝构造函数、拷贝赋值函数和析构函数
对于不带有指针的类,这3个函数可以使用编译器默认为我们生成的版本;但是编写带有指针的类时就有必要定义这3个特殊函数.
构造函数和析构函数
构造函数和析构函数中执行数据的深拷贝和释放.
值得注意的是使用delete[]
操作符释放数组内存,若直接使用delete
操作符释放数组内存虽然能够通过编译,但有可能产生内存泄漏.
拷贝构造函数和拷贝赋值函数
拷贝构造函数和拷贝赋值函数函数的使用场景不同,下面程序的拷贝3虽然使用了=
赋值,但是因为是在初始化过程中使用的,所以调用的是拷贝构造函数.
String s1 = "hello";
String s2(s1); // 拷贝1: 调用拷贝构造函数
String s3;
s3 = s1; // 拷贝2: 调用拷贝赋值函数
String s4 = s1; // 拷贝3: 调用拷贝构造函数
拷贝构造函数的实现较为简单,直接调用友元对象的数据指针进行拷贝即可.
拷贝赋值函数中要检测自我赋值,这不仅是为了效率考虑,也是为了防止出现bug.
若在拷贝赋值函数中不检测自我赋值,在第2步中会出现bug.
堆栈与内存管理
堆栈及对象的生命周期
栈(stack),是存在于某作用域(scope)的一块内存空间.例如当你调用函数,函数本身就会形成一个stack用来防治它所接收的参数以及返回地址.在函数本体内声明的任何变量,其所使用的内存块都取自上述stack.
堆(heap),是指由操作系统提供的一块global内存空间,程序可动态分配从其中获得若干区块.
-
stack object的生命周期:
class Complex { ... }; // ... { Complex c1(1,2); }
程序中
c1
就是stack object,其生命周期在作用域(大括号)结束之际结束.这种作用域内的对象又称为auto object,因为它会被自动清理. -
static object的生命周期
class Complex { … }; // ... { static Complex c2(1,2); }
程序中
c2
就是static object,其生命周期在作用域(大括号)结束之后仍然存在,直到整个程序结束. -
global object的生命周期
class Complex { … }; // ... Complex c3(1,2); int main() { ... }
程序中
c3
就是global object,其生命在在整个程序结束之后才结束,也可以将其视为一种static object,其作用域是整个程序. -
heap object的生命周期
class Complex { … }; // ... { Complex* p = new Complex; // ... delete p; }
程序中
p
指向的对象就是heap object,其生命周期在它被deleted之际结束.若推出作用域时忘记delete指针p
则会发生内存泄漏,即p
所指向的heap object 仍然存在,但指针p
的生命周期却结束了,作用域之外再也无法操作p
指向的heap object.
new
和delete
过程中的内存分配
-
new
操作先分配内存,再调用构造函数. -
delete
操作先调用析构函数,再释放内存.
对于带有指针的String
类,new
操作和delete
操作的示意图如下:
new | delete |
---|---|
VC中对象在debug
模式和release
模式下的内存分布如下图所示,变量在内存中所占字节数必须被补齐为16的倍数,红色代表cookie
保存内存块的大小,其最低位的1
和0
分别表示内存是否被回收.
Complex 对象 | String 对象 |
---|---|
数组中的元素是连续的,数组头部4个字节记录了数组长度:
Complex 对象 | String 对象 |
---|---|
根据数组在内存中的状态,自然可以理解为什么new[]
和delete[]
应该配对使用了: delete
操作符仅会调用一次析构函数,而delete[]
操作符依次对每个元素调用析构函数.对于String
这样带有指针的类,若将delete[]
误用为delete
会引起内存泄漏.
static成员
对于类来说,non-static
成员变量每个对象均存在一份,static
成员变量、non-static
和static
成员函数在内存中仅存在一份.其中non-static
成员函数通过指定this
指针获得函数的调用权,而non-static
函数不需要this
指针即可调用.
static
成员函数可以通过对象调用,也可以通过类名调用.
class Account {
public:
static double m_rate;
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0;
int main() {
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}
static
成员变量需要在类声明体外进行初始化.
面向对象(Object Oriented)的程序设计——类之间的关系
类之间的关系有复合(composition)、委托(aggregation)和继承(extension)3种.
类之间的关系
复合(composition)
复合表示一种has-a
的关系,STL中queue
的实现就使用了复合关系.这种结构也被称为adapter模式.
复合关系下构造由内而外,析构由外而内:
委托(aggregation;composition by reference)
委托将类的定义与类的实现分隔开来,也被称为编译防火墙.
继承(extension)
继承表示一种is-a
的关系,STL中_List_node
的实现就使用了继承关系.
继承关系下构造由内而外,析构由外而内:
虚函数
成员函数有3种: 非虚函数、虚函数和纯虚函数
-
非虚函數(non-virtual function): 不希望子类重新定义(override)的函数.
-
虚函數(virtual function): 子类可以重新定义(override)的函数,且有默认定义.
-
纯虚函數(pure virtual function): 子类必须重新定义(override)的函数,没有默认定义.
使用虚函数实现框架: 框架的作者想要实现一般的文件处理类,由框架的使用者定义具体的文件处理过程,则可以用虚函数来实现.
将框架中父类CDocument
的Serialize()
函数设为虚函数,由框架使用者编写的子类CMyDoc
定义具体的文件处理过程,流程示意图和代码如下:
流程示意图 | 代码 |
---|---|
面向对象设计范例
使用委托+继承实现Observer模式
使用Observer模式实现多个窗口订阅同一份内容并保持实时更新
类结构图如下:
使用委托+继承实现Composite模式
使用Composite模式实现多态,类结构图如下
使用委托+继承实现Prototype模式
Prototype模式示意图如下:
pdf版本笔记的下载地址: 笔记01_面向对象高级编程(上),排版更美观一点(访问密码:3834)