1. 为什么要使用 OOP?
假如你想组装一台电脑,你会去硬件商店购买主板、处理器、内存条、硬盘、机箱、电源,然后将它们组装在一起,然后打开电源,电脑就能运行。 你不用考虑主板是 4 重板还是 6 重板,硬盘是什么尺寸,内存是哪里生产的诸如此类的问题。你只需要将这些硬件单元组合在一起,就能期待电脑能够运行。 当然,你需要保证你有正确的接口,比如,如果你的主板只支持 IDE 接口,而你需要购买一个 IDE 的硬盘而不是 SCSI 硬盘,又例如你需要选择一个合适速率的 内存。即便如此,将硬件组件组装成一台机器也没有任何难度。
同样的,一辆车也是由多个部分组装起来的,例如底盘、车门、引擎、车轮、刹车和传动装置。这些组件都是可复用的。比如车轮,就能够被用于很多辆同型号的骑车上。
像电脑和汽车这样的硬件能够用可复用的部分组装起来。那么软件是否也是如此呢?我们是否能够将不同地方的程序片段“组装”起来,然后期待程序能正常运行呢? 答案显然是 no!跟硬件不同的是,很难从软件片段中“组装”一个应用。自从计算机 60 年前问世以来,人们写了大量的程式码。然而,对于每一个新的应用程序,我们都需要 重新造轮子。
为什么要重新造轮子呢?
1.1 传统的面向过程的编程语言
传统的面向过程的编程语言(例如 C 和 Pascal)在创建可重用组件方面遇到了一些显著的缺陷:
程序是由函数组织起来的。函数通常是不可重用的,我们很难将一个函数直接拷贝到别的地方去使用,因为函数很有可能引用了头文件,或者全局变量,或者调用了其他函数。 换句话说,函数不能很好的封装成一个独立的可重用单元。
面向过程的语言不适合高层抽象来解决现实生活中的问题。例如 c 程序使用"if-else", “for-loop”, “array”, “function”, “pointer"等结构,这些结构很低阶而且很难抽象形如 Customer Relationship Management (CRM) 系统或者电脑足球游戏。
简而言之,传统的面向过程的编程语言将数据结构和算法单元分开了。
1.2 面向对象的编程语言
面向对象的编程语言就是被设计来克服这些问题的。
OOP 的基础单元是类。类将静态属性和动态行为封装在一起,同时指定一些公开的接口来供人使用。由于相比于函数,类有很好的封装性,所以很容易重用。换句话说,类将数据和算法结合在了一起。
面向对象的编程语言为解决现实问题的高阶抽象提供了保证。面向过程的编程语言迫使人们把注意力放在计算机结构(如:内存,位,字节,数组)上,而不是放在要解决的问题本身。面向对象的编程语言 能够让我们更专注于问题本身,使用程序对象来表示和抽象问题中的各种实体。
举个例子,假设你要写一个足球游戏,很难用面向过程的语言建立模型。但是使用 OOP,可以很容易将现实事物同程序之间建立模型:
- Player:属性包含 name, number, location 等待,操作有 run, jump, kick-the-ball…
- Ball:
- Reference:
- Field:
- Audience:
- Weather:
最重要的是,这其中的一些类(例如:Ball 和 Audience)可以在其他程序中复用。
1.3 OOP 带来的优点
面向过程的编程语言注重于过程,函数是它的基础单元。你需要在一开始就规划好所有的函数,然后考虑如何去表示数据。
面向对象的编程语言注重于用户所认知的组件,类是它的基础单元。你只需要将所有数据和数据交互的操作放进对应的类中即可。
面向对象编程技术有很多优点:
- 易于设计和开发
- 易于维护
- 可复用
2. OOP 基础
2.1 类和实例
类:类是对同种事物的抽象。换句话说,类是蓝图,是模板,或者是一种协议,类用来定义和描述同种对象共有的静态属性和动态行为。
实例:实例是一个类的特定实现。换句话说,实例是类的实例化。类的所有实例都具有类似的属性,如类定义中所描述的那样。
2.2 类是一个封装数据和操作的三室盒
一个类可以被形象的比喻成一个三室盒:
- Classname(identifier):标识类
- Data Members or Variables(or attributes, states, fields):包含了类的静态属性
- Member Functions(or methods, behaviors, operations):包含了类的动态操作
2.3 定义类
在 C++种,使用关键字class
来定义一个类。在申明类的时候可以有两张选项:public
和private
,稍后会具体说明。
class Circle {
private:
double radius;
string color;
public:
double getRadius();
double getArea();
}
class SoccerPlayer {
private:
int number;
string name;
int x, y;
public:
void run();
void kickBall();
}
类命名约定:一个类名必须是一个名词或者一个名词短语,所有单词首字母大写(驼峰),使用名词单数形式,类名必须要有 意义,能够清楚描述自己。
2.4 创建类实例
创建一个类实例,你需要:
- 申明一个特定实例的标识符。
- 调用类的构造函数来构造类的实例
假如我们有一个名为Circle
的类,我们可以通过如下方式来创建实例:
Circle c1(1.2, "red");
Circle c2(3.4);
Circle c3();
另外,你也可以显示调用构造函数:
Circle c1 = Circle(1.2, "red");
Circle c2 = Circle(3.4);
Circle c3 = Circle;
2.5 点(.)操作符
引用一个对象的成员,你需要:
- 首先确定是哪个实例,然后
- 使用点操作符来引用成员
同样的,假如我们有一个名为Circle
的类,其中有两个数据成员和两个函数,我们已经创建了三个实例,分别为c1
, c2
, c3
。
//创建实例
Circle c1(1.2, "blue");
Circle c2(3.4, "green");
//调用成员方法
cout << c1.getArea() << endl;
cout << c2.getArea() << endl;
//引用数据成员
c1.radius = 5.5;
c2.radius = 6.6;
2.6 数据成员(变量)
成员变量有一个变量名和变量类型,用以存放一个特定类型的值。成员变量也可以是一个特定类的实例。
2.7 成员方法
一个成员方法:
- 从调用者接收参数
- 执行定义好的操作
- 返回结果给调用者
2.8 将它们合在一起:一个 OOP 的例子
在这个例子中,我们会将所有代码放在一个源文件中CircleAIO.cpp
/*
* The Circle class (All source code in one file) (CircleAIO.cpp)
*/
#include <iostream> // using IO functions
#include <cstring> // using string
using namespace std;
class Circle {
private:
double radius; // Data member (Variable)
string color; // Data member (Variable)
public:
// Constructor with default values for data members.
Circle(double r = 1.0, string c = "red") {
radius = r;
color = c;
}
// Member function (Getter)
double getRadius() {
return radius;
}
// Member function (Getter)
string getColor() {
return color;
}
// Member function
double getArea() {
return radius * radius * 3.14;
}
}; // need to end the class declaration with a semi-colon
// Test driver function
int main(int argc, char *argv[])
{
Circle c1(1.2, "blue");
cout << "Radius=" << c1.getRadius() << " Area=" << c1.getArea()
<< " Color=" << c1.getColor() << endl;
Circle c2(3.4);
cout << "Radius=" << c2.getRadius() << " Area=" << c2.getArea()
<< " Color=" << c2.getColor() << endl;
Circle c3;
cout << "Radius=" << c3.getRadius() << " Area=" << c3.getArea()
<< " Color=" << c3.getColor() << endl;
return 0;
}
2.9 构造器
构造器是一个跟类名同名的特定方法。在上述的Circle
类中,我们是这样定义构造器的:
// Constructor has the same name as the class
Circle(double r = 1.0, string c = "red") {
radius = r;
color = c;
}
构造器是用来构造和初始化数据成员的。创建一个类的新实例,你需要申明一个实例的标识符然后调用构造器:
Circle c1(1.2, "blue");
Circle c2(3.4);
Circle c3;
构造器和普通方法的区别在于下面几个方面:
- 构造器函数跟类同名
- 构造器没有返回值(或者说是返回
void
类型)。也就是说,构造器中允许缺省return
语句。 - 构造器只能在初始化实例的时候被调用一次
- 构造器不能被继承
2.10 函数的默认值
在 C++中,你可以给函数参数指定默认值:
/* Test function default arguments (TestFnDefault.cpp) */
#include <iostream>
using namespace std;
// Function prototype
int sum(int n1, int n2, int n3 = 0, int n4 = 0, int n5 = 0);
int main() {
cout << sum(1, 1, 1, 1, 1) << endl; // 5
cout << sum(1, 1, 1, 1) << endl; // 4
cout << sum(1, 1, 1) << endl; // 3
cout << sum(1, 1) << endl; // 2
cout << sum(1) << endl; // error: too few arguments
}
// Function definition
// The default values shell be specified in function prototype,
// not the function implementation.
int sum(int n1, int n2, int n3, int n4, int n5) {
return n1 + n2 + n3 + n4 + n5;
}
2.11 “public” vs . “private” 访问控制符
访问控制符用来控制成员变量和成员方法的可见性。
- public:成员可见
- private:成员只能在类中可见
2.12 封装
2.13 Getters 和 Setters
为了让外部访问到private
修饰的成员变量,你需要提供 get 函数,通常命名为getXxx()
。getter 不必讲数据原样暴露出来,
它可以对数据视图做一些限制。Getters 不能修改成员属性。
为了让外部能够修改被private
修饰的成员变量,你需要提供 set 函数,通常命名为setXxx()
,setter 函数需要保证数据的合法性,
然后将其转换成类内部展示的形式。
2.14 “this"关键字
我们可以使用"this"关键字在当前类内部引用当前实例。
“this"的一个主要作用就是解决函数参数名和成员变量名冲突的问题.
class Circle {
private:
double radius;
public:
void setRadius(double radius) {
this->radius = radius;
}
}
2.15 “const"成员函数
被const
关键字修饰的成员函数不能修改任何成员属性,例如:
double getRadius() const {
radius = 0;
// error: assignment of data-member 'Circle::radius' in read-only structure
return radius;
}
2.16 Getters/Setters 和 Constructors 的命名规则
假设在类Aaa
中有一个T
类型的私有属性xxx
,那么该类的 getter,setter 和 constructor 遵循以下规则:
class Aaa {
private:
// A private variable named xxx of type T
T xxx;
public:
// Constructor
Aaa(T x) { xxx = x; }
// OR
Aaa(T xxx) { this->xxx = xxx; }
// OR using member initializer list
Aaa(T xxx) : xxx(xxx) { }
// A getter for variable xxx of type T receives no argument and return a value of type T
T getXxx() const { return xxx; }
// A setter for variable xxx of type T receives a parameter of type T and return void
void setXxx(T x) { xxx = x; }
// OR
void setXxxx(T xxx) { this->xxx = xxx; }
}
对于bool
类型的变量xxx
,其 getter 函数应该命名为isXxx()
,而不是getXxx()
:
private:
bool xxx;
public:
// Getter
bool isXxx() const { return xxx; }
// Setter
void setXxx(bool x) { xxx = x; }
// OR
void setXxx(bool xxx) { this->xxx = xxx; }
2.17 默认构造器
默认构造器就是没有任何参数的构造器,或者是所有参数都有默认值的构造器。
Circle c1; // Declare c1 as an instance of Circle, and invoke the default constructor
Circle c1(); // Error!
// (This declares c1 as a function that takes no parameters and return a Circle)
说明:
注意上述代码中的区别,Circle c1;
申明了一个Circle
类的实例,并且调用了默认构造器,而Circle c1();
则是申明了一个名为c1
的函数,该
函数不需要参数,而且返回值为Circle
类型的对象。
在 C++中,如果你没有提供任何构造器,那么编译器会自动提供一个没有任何操作的默认构造器:
ClassName::ClassName() { } // Take no argument and do nothing.
而一旦你提供了构造器,编译器就不会再提供默认构造器了。也就是说,如果你提供的所有构造器都是有参数的,那么再去调用无参构造器就会报错。
2.18 构造器的成员初始化列表
除了像下面这样在构造器函数体中初始化私有属性:
Circle(double r = 1.0, string c = "red") {
radius = r;
color = c;
}
还有一种可选的语法叫做成员初始化列表:
Circle(double r = 1.0, string c = "red") : radius(r), color(c) { }
2.19 析构函数
析构函数跟构造函数类似,都跟类同名,不同的是析构函数名前需要加上~
前缀,例如:~Circle()
。析构函数在对象销毁的时候被调用。
如果你没有提供析构函数,编译器会提供一个不做任何操作的默认析构函数。
class MyClass {
public:
// The default destructor that dose nothing.
~MyClass() { }
}
注意:
如果你的类成员包含了动态分配的(通过new
或者new[]
)的数据,那么你需要通过delete
或者delete[]
来释放内存。
2.20 拷贝构造函数
拷贝构造函数通过拷贝一个已经存在的同类型对象来创建一个新的对象。也就是说,需要向拷贝构造函数传递一个同类型的对象作为参数。
Circle c4(7.8, "blue");
cout << "Radius=" << c4.getRadius() << " Area=" << c4.getArea()
<< " Color=" << c4.getColor() << endl;
// Costruct a new object by copying an existing object
// via the so-called default copy constructor
Circle c5(c4);
cout << "Radius=" << c5.getRadius() << " Area=" << c5.getArea()
<< " Color=" << c5.getColor() << endl;
当我们想将一个对象按值传递给一个函数作为参数的时候,就需要使用拷贝构造函数。
2.21 拷贝赋值操作符
编译器还提供了一种默认的赋值操作符(=),可以通过成员拷贝的方式将一个对象赋值给另一个同类的对象。
Circle c6(5.6, "orange"), c7;
c7 = c6; // memberwise copy assignment
说明
- 可以通过重载赋值操作符来覆盖默认操作
- 在声明对象的时候用使用的拷贝构造函数而不是拷贝赋值操作:
Circle c8 = c6; // Invoke the copy constructor, NOT copy assignment operator
// Same as Circle c8(c6);
- 默认的拷贝赋值表现为shadow copy,它并不能拷贝通过
new
或者new[]
动态声明的成员。 - 拷贝赋值操作符的结构如下:
class MyClass {
private:
T1 member1;
T2 member2;
public:
// The default copy assignment operator which assigns an object via memberwise copy
MyClass & operator=(const MyClass & rhs) {
member1 = rhs.member1;
member2 = rhs.member2;
return *this;
}
}
- 拷贝赋值不同于拷贝构造的地方在于拷贝赋值必须释放动态申请的空间,而且需要防止自赋值。拷贝赋值返回自引用,以实现
链式赋值操作:
x = y = z
- 默认构造器,默认析构函数,默认拷贝构造函数,默认拷贝赋值操作符都是常见的特殊成员函数,如果没有被定义的话,编译器会自动生生成
3. 将声明和实现分离
为了更好的组织软件工程,强烈建议将类的声明和实现分别放在 2 个文件中:声明放在头文件(.h)里,实现放在”.cpp"文件中。 这样做就是公开接口,隐藏实现,而且一旦接口定义好,可以有多种不同的实现。
4. 一个示例
Circle.h - Header
/* The Circle class Header (Circle.h) */
#include <string>
using namespace std;
// Circle class declaration
class Circle {
private:
double radius;
string color;
public:
Circle(double raidus = 1.0, string color = "red");
double getRadius() const;
void setRadius(double radius);
string getColor() const;
void setColor(string color);
double getArea() const;
};
Circle.cpp - Implementation
#include "Circle.h"
Circle::Circle(double r, string c) {
radius = r;
color = c;
}
double Circle::getRadius() const {
return radius;
}
void Circle::setRadius(double r) {
radius = r;
}
string Circle::getColor() const {
return color;
}
void Circle::setColor(string c) {
color = c;
}
double Circle::getArea() const {
return radius * radius * 3.14;
}
编译Circle
类只需要将Circle.cpp
编译成Circle.o
文件
g++ -c Circle.cpp
TestCircle.cpp - Test Driver
#include <iostream>
#include "Circle.h"
using namespace std;
int main() {
Circle c1(1.2, "red");
c1.setRadius(2.1);
c1.setColor("blue");
return 0;
}
编译测试文件
g++ -o TestCircle TestCircle.cpp Circle.o
你也可以
g++ -o TestCircle TestCircle.cpp Circle.cpp
剩下的例子省略。。。
评论