2. 对象工程
本文最后更新于 2024年1月27日 下午
对象工程
这是浙江大学翁恺老师的公开课,《面向对象设计C++》
视频地址:
https://www.bilibili.com/video/BV1yQ4y1A7ts/?spm_id_from=333.337.search-card.all.click&vd_source=3074f6f6ab43a114c5af8727fa4f7255
本节对应视频04-06部分。
第一个C++工程:自动售票机模拟程序
对自动售票机而言,如果是面向过程的描述,那么应当是:
- Step to the machine - Insert money into the machine - The machine
prints a ticket - Take the ticket and leave
如果是面向对象的描述,那么:
自动售票机具有价格、余额、收入总额这三个数据。
自动售票机会显示提示信息、打印余额、打印车票、接收纸币。
那么可以说自动售票机就是一个类,这个类可以表示为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class TicketMachine{
public:
void showPrompt();
void getMoney(float money);
void printTicket();
void showBalance();
void printTotal();
private:
const float PRICE;
float Balance;
float total;
};*.h
,另一个是*.cpp
。
对上面的自动售票机的例子,*.h
中的写法如下:
1 |
|
在*.cpp
文件中,有这样一个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "TicketMachine.h" //TicketMachine这个类是在文件"TicketMachine.h"中声明的
#include <iostream>
using namespace std;
// 在.cpp文件中所要描述的实体定义.h文件中所声明的
TicketMachine::TicketMachine() : PRICE(0) {
//PRICE(0)是为了初始化指针PRICE为0
// TODO Auto-generated constructor stub
}
TicketMachine::~TicketMachine(){
// TODO Auto-generated destructor stub
}
void TicketMachine::showPrompt()
{
cout << "something";
}
void TicketMachine::insertMoney(float money)
{
balcance += money;
}
void TicketMachine::showBalance()
{
cout << balance;
}
习惯上再去创建一个新的源文件进行启动,此处创建一个源文件main.cpp
,在这个文件中:
1
2
3
4
5
6
7
8
9#include "ticketmachine.h";
int main()
{
TickMachine tm;
tm.insertMoney(100);
tm.showBalance();
return 0;
}
这一大段程序中,::
称为解析符(resolver).用法为:
<ClassName>::<function_name>
或者::<function_name>
解析符的作用是为了表示这个函数是专门依附于解析符前的类而存在的。如果解析符前面没有类,则代表该函数是一个全局的函数。
比如:
1
2
3
4
5void S::f() {
::f(); // Would be recursive otherwise 可以自己递归调用自己
::a++; // Select the global a 引用全局变量a
a--; // The a at class scope 用这个类S中的一个成员变量a
}*.h
和*.cpp
文件来定义一个类。类的声明和类中的函数原型都需要写入*.h
头文件当中。这些相关的函数原型需要在这个类的*.cpp
中写好。
对于头文件来说,如果一个函数是声明在头文件当中的,那么在所有要用到这个函数和所有要定义这个函数的地方都需要使用#include
引用这个头文件。对于类也是一样,在所有要用到这个类的实体和所有要定义这个类的地方都需要使用#include
引用包含这个类的头文件。
头文件实际上是类的设计者和使用者之间的一种合同。
C++工程的结构如下:
其中*.cpp
文件中的#include
在引用这个头文件时,#
代表的是编译预处理程序,也就是include
中的内容并不是编译器解读的,而是在正式编译之前所进行的预处理。当编译预处理程序读到带有#include
的内容时,会将include
的具体内容添加在.cpp
的编译文件前,形成一个更大的编译文件。
比如:
在a.cpp
中写入如下代码:
1
2
3
4#include 'a.h'
int main{
return 0;
}a.h
文件:
1
2void f();
int global;cpp
(C pre
processor)进行编译,可以观察到编译的中间过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17~ cpp a.cpp
# 1 "a.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "a.cpp"
# 1 "a.h" 1
void f(); #declaration
int global; #definetion,这是错误的,如果需要连结两个引用a.h的.cpp文件,此时两个cpp文件中因为都包含global这个变量而导致冲突
extern int global # 改为declaration的写法
# 2 "a.cpp" 2
int global //有decleration的变量,在使用它的时候就需要定义它
int main
{
global ++;
return 0;
}.h
文件中的内容抄送合并.cpp
文件的内容,然后送给编译器编译为二进制文件。所以#include
实际上做的是文本的插入。
如果有别的程序引用了这个类,那么也需要#include
这个.h
文件。
#include
进行引用有两种方式:
- #include "xx.h"
:
会从写了这个include
的程序目录下寻找xx.h
文件。
- #include <xx.h>
:
会从系统目录,也就是编译器所认定的头文件所在的目录(对于Lin
ux,系统头文件在/usr/include/
目录后),寻找xx.h
文件。
-
#include <xx>
:类似于#include <xx.h>
,但是引用范围不限于.h
文件。
声明和定义
声明包括:
- 外部变量 - 类和结构体的声明 - 函数原型
注意,类和结构体并没有定义,只有声明。
一个.cpp文件是一个编译单元,对于头文件来说,头文件中只允许声明,而不允许定义。因为在头文件中定义变量,当出现多个.cpp文件#include
同一个文件的时候,编译完成后的linkerld
会将这些.cpp文件连结,此时就会出现变量名重复的问题。
条件编译和标准头文件结构
在linux中创建一个头文件时,会自动生成一些内容,这是标准头文件结构。比如上面例子中的:
1
2
3
4#ifndef TICKTMACHINE_H_
#define TICKTMACHINE_H_
...
#endif /* TICKTMACHINE */TICKTMACHINE
这个宏没有被定义,那么就定义这个宏。事实上预处理指令后面是可以具体给宏做一些定义的,比如#define TICKTMACHINE_H_ 12
,如果后面没有对宏给出定义,则只是告诉编译器现在需要创建一个内容为空的宏。如果编译器发现这个宏已经具有定义了,那么编译器便会跳过下面所有的程序,不会编译这些内容。
C++编辑器中默认添加这两行的原因是因为,如果.cpp文件中同时引用了两个相同的类的声明(这件事情通常发生在一个.h文件中引用了另一个.h文件,而cpp程序中同时引用了这两个.h文件的情况)那么会发生冲突,采用条件编译则可以避免这样的冲突。
所以一个头文件中应该只放一个类的声明,并且应当包含标准头文件结构。
抽象和模块化
抽象(abstraction)是能够忽略问题的细节将注意力集中在问题更高层次上的一种方法。
模块化(modularization)是把问题分为若干个定义明确的部分,这些部分可以分别编译和检查,并且以明确的方式进行交互。
比如如果要创建一个时钟hh:mm,可以将这个时钟划分为小时和分钟两个部分,这两个部分具有相同的特征,比如都有两位,且每60归零。因此,可以设计如下的两个类来完成这个时钟,NumberDisplay
用于显示小时或者分钟,ClouckDisplay
用于组合小时和分钟的显示:
其中这个increase
的用法是用于判断是否需要翻转:
1
2
3
4if ( minute.increase())
{
hour.increase();
}1
2
3
4
5
6class ClockDisplay {
NumberDisplay hours;
NumberDisplay minutes;
//以及一些其他的部分
}