抽象和类
OOP最重要的特性:
- 抽象;
- 封装和数据隐藏;
- 多态;
- 继承;
- 代码的可重用性;
C++中的类
通常来说,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。例如: ```cpp // stock00.h —stock class interface // version 00ifndef STOCK00_H
define STOCK00_H
include
class Stock{ private: std::string company; long shares; double share_val; double total_val; void set_tol(){total_val = shares * share_val;} public: void acquire(const std::string & co, long n, double pr); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); }; // class和struct一样,末尾都需要分号结尾
endif
<a name="CM7xr"></a>
### 访问控制
在上面代码中的`private`、`public`都是用于设置访问控制的,它们都是C++新增的。使用类对象的程序,都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。所以说,公共成员函数时程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。
> 其实,这就体现了OOP的封装和数据隐藏特性。除了private、public之外,C++还有第三个访问控制关键字:protected。封装的另一个例子是将类函数定义和类声明放在不同文件中。
无论是成员变量还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP主要目标之一,**因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分**。<br />**值得一提的是,**`**private**`**修饰可以省略,因为C++默认访问控制即为**`**private**`。(显示使用private可能更加直观)
<a name="N1YRO"></a>
### 实现类成员函数
此处为类声明中的原型函数提供代码(Python貌似不能这样分开来)。成员函数定义域常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数,但是它们还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(`::`)来标识函数所属的类;
- 类方法可以访问类的`private`组件;
例如下面标识了update成员函数定义时的函数头:
```cpp
void Stock::update(double price)
作用域解析运算符指定了成员函数的归属!(update
简写只能在类作用域中使用)
下面展示类方法的实现(只实现了部分代码):
// stock00.cpp --implementing the Stock class
// version 00
#include<iostream>
#include"stock00.h"
// 这些函数都是针对某一家公司的股票而言的,实际问题中通常包含多家的情况。
void Stock::acquire(const std::string& co, long n, double pr){
company = co;
if(n<0){
std::cout << "Number of shares can't be negative"
<< company << " shares set to 0.\n";
shares = 0;
}
else{
shares = n;
}
share_val = pr;
set_tol();
}
void Stock::buy(long num, double price){
if(num < 0){
std::cout << "Number of shares purchased can't be negative."
<< "Transaction is aborted.\n";
}
else{
shares += num;
share_val = price;
set_tol();
}
}
// TODO
内联函数
事实上,定义位于类声明中的函数都将自动成为内联函数。比如,前面stock00.h里面的set_tol()
函数。在之前有讲过,内联函数常常是短小的函数,set_val
是符合这样的要求的。当然,也可以在类声明的外部定义成员函数,使之成为内联函数:
class Stock{
private:
//...
void set_tol(); // 定义分离
public:
//...
};
inline void Stock::set_tol(){
total_val = shares * share_val;
}
定义位于类声明中的函数都将自动成为内联函数。所以不要轻易将类的函数定义在类内部! 内联函数的特殊规则要求每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用,最简单的方法:将内联定义放在定义类的头文件中(可以在类里面,也可以在外面)。(内联函数不会出现重复定义的问题)
使用类
事实上,使用类和基本的数据类型差别不是很大(类是用户自定义的类型)。要创建类对象,可以声明类变量,也可以用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。
在教材中说C++提供了一些工具,可用于初始化对象、让cin,cout识别对象,甚至在相似的类对象之间进行自动类型转换?
类的构造和析构函数
之前的类型,比如整型,结构体等都能够进行初始化,但是到目前为止,我们并没有提供类的初始化。并且由于类内部的封装特性,使得很多成员变量无法在类外部访问,从而,利用之前像结构体那样的初始化方法不合适。类初始化需要用到构造函数,它是类在创建过程中自动为类进行初始化的函数。
构造函数
类的构造函数和类同名的成员函数。它是没有返回值的,但是不能用void去修饰它。实际上,构造函数没有声明类型。
下面是Stock类的原型:
Stock(const string& co, long n, double pr = 0.0);
下面是构造函数的定义:
// 构造函数定义
Stock::Stock(const string& co, long n, double pr = 0.0){
company = co;
if(n < 0){
std::cerr << "Number of shares can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else{
shares = n;
}
share_val = pr;
set_tol();
}
如果将参数名命名和类成员变量一样的名称,那么就必须用
this
关键字才能区分罗!
使用构造函数
定义了构造函数就可以调用构造函数初始化类对象了。
第一种方式是显示调用构造函数:
Stock food = Stock("World Cabbage", 250, 1.25);
第二种是隐式调用:
Stock garment("Furry Mason", 50, 2.5);
// 其等价为:Stock garment = Stock("Furry Mason", 50, 2.5);
除此之外,还可以用new来动态分配内存:
Stock *pstock = new Stock("Electronick Games", 18, 19.0);
类对象的成员函数调用,通常是类似:stack1.show()
,但是构造函数不能不能这样使用,构造函数是用来创建对象的。
默认构造函数
默认构造函数是在未提供显示初始值时,用来创建对象的构造函数。例如下面的类对象声明:
Stock fluffy_the_cat; // 采用默认构造函数
如果类定义没有声明(定义)构造函数,那么C++将自动提供构造函数。该默认函数不做任何工作:
Stock::Stock(){}
值得注意的是,只有在没有定义任何构造函数的时候,编译器才会提供默认构造函数。为类定义了构造函数之后,程序员就必须自己编写默认构造函数了。也即是说,如果提供了非默认函数,但是程序员没有编写默认函数,则下面的声明将出错:
Stock stock1;
这么做的原因可能是想禁止创建未初始化的对象。如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种:一种是给已有构造函数的所有参数提供默认值;另一种是通过函数重载来定义另一个构造函数。但是需要注意的是,默认构造函数只能有一个!(但是,默认函数通常都会设计为给所有成员提供隐式初始值)
// 第一种
Stock(const string& co = "Error", int n = 0, double pr = 0.0);
// 第二种
Stock();
析构函数
在对象过期的时候,程序会自动调用一个特殊的成员函数,即析构函数,其完成清理工作。如果我们在构造函数使用了 new 来分配内存,那么在析构函数我们就需要使用 delete 语句进行内存的释放。由于前面的例子中构造函数没有使用 new 进行内存的分配,所以析构函数实际上没有需要完成的任务,只需要一个隐式析构函数即可。
也就是说,C++中的构造函数和析构函数是可以不编写的,不编写程序将会提供隐式(默认)的函数。
析构函数名称:在类名前面加上 ~
,因此 Stock 类的析构函数为 ~Stock,其原型为:
~Stock();
由于 Stock 析构函数不承担任何重要工作,因此其定义什么也不做:
Stock::~Stock(){
}
如果想要看到析构函数被调用,可以定义如下:
Stock::~Stock(){
std::cout << "Bye!" << std::endl;
}
范围解析运算符:
::
析构函数什么时候被调用,由编译器决定。通常不应在代码中显式掉用析构函数。
- 如果创建的是静态存储类对象,则其析构函数将在程序结束时被调用;
- 如果创建的是自动存储类对象,其析构函数在程序执行完代码块时自动被调用;
- 如果是通过 new 进行创建的对象,则其驻留在栈内存或自由存储区中,当使用 delete 来释放内存是,析构函数被调用;
- 程序可以创建临时对象来完成特定操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
改进Stock类
由于最初的 Stock 类是没有构造函数和析构函数的,所以在此对其进行改进: ```cpp // stock00.h —stock class interface // version 00ifndef STOCK10_H
define STOCK10_H
include
class Stock{ private: std::string company; long shares; double share_val; double total_val; void set_tol(){total_val = shares * share_val;} public: // two constructors Stock(); Stock(const std::string & co, long n = 0; double pr = 0.0); // noisy destructor ~Stock(); void acquire(const std::string & co, long n, double pr); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); }; // class和struct一样,末尾都需要分号结尾
endif
<a name="l4R8x"></a>
#### 类实例化分析
```cpp
Stock stock1("NanoSmrt", 12, 20.0); // #1
Stock stock2 = Stock("Boffo", 2, 2.0); // #2
以上两种方式都可以创建Stock对象。但是对于第二种#2
语法,C++编译器容许两种方式来执行之:
- 第一种方式:和第一种
#1
语法完全相同; - 第二种方式:允许调用构造函数来创建一个临时对象,然后将该临时对象复制到
_stock2 _
中,并丢弃临时对象(调用其析构函数)。
上面的代码实现对stock2 = stock1;
_stock2_
的重新赋值。其与结构赋值一样,给类对象赋值时,将一个对象的成员赋值给另一个。所以上述代码中stock2
原来的内容将被覆盖。注意区分什么时候是赋值,什么时候是引用。python中很多对象之间的赋值,例如numpy,其实是引用赋值(速度快,开销小),修改引用原对象也会被修改。(这主要取决于”=”怎么被定义) 要理解C++对象之间的赋值,首先要了解结构体的赋值:
struct_s person_1 = {"yyyy", 20, 180};
显然结构体的赋值就是将列表中的值分别赋给结构体的成员,这显然不可能是地址(指针)赋值或者引用赋值的。这说明在结构体中“=”定义为结构对象对应成员的赋值。 由此观之,只有数组是非常特别的,其数组名为其内存块首地址。
// stock1之前已经被初始化了
stock1 = Stock("Niffty Foods", 10, 50.0);
上述代码,不是初始化,是将新值赋给stock1。其实现方式是:先构造临时对象,然后实现对象成员的复制,最后析构临时对象。
上述类对象的定义都是产生的自动变量,这些变量存在栈中(FILO),在代码块结束之后,对象也被析构。
C++11 列表初始化
Stock stock1 = {"Niffty Foods", 10, 50.0}
类的列表初始化和数组的列表初始化很像。列表中的元素需要和构造函数进行匹配。
下面代码都是合法的:
// Person 类构造函数为 Person(const char* name, const int age)
Person p1("muye", 24);
Person p2 = Person("feng", 23);
Person p3 = new Person("hh", 22);
Perosn p4 = {"moxiao", 21}
// 如果存在默认构造函数 Person()
// 则还可以用下面代码初始化
Person p5 = new Person;
Person p6;
const 成员函数
void show() const;
# 函数定义
void Stack::show() const {
// TODO
}
this 指针
this指针指向调用对象,如果方法需要引用整个调用对象,则可以用表示式 *this。和 Python 的 this 指针差不多,就是表示对象本身,可以通过此指针轻松获取对象的成员。
类作用域
类成员不能在外部直接进行访问,需要借助:(.)、(->)或者作用域解析运算符(::)进行访问。
抽象数据类型
在定义类的时候,成员可以只申明而不定义,那么这样的定义非常适合于抽象。