文章目录
最重要的OOP特性:
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码的可重用性
为了实现这些特性并将它们组合在一起,C++所做的最重要的改进是提供了类。前面几篇文章大多数讲的是面向过程编程,下面我们正式开始介绍面向对象的编程。
1. 抽象和类
1.1 C++中的类
使用关键字class指出类设计,在这里不能用typename。类规范由两部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口
- 类方法定义:描述如何实现类成员函数
简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。在类中,成员函数可以在类中定义,也可以用原型表示。
(1)访问控制
关键字private和public描述了对类成员的访问控制,类对象可以直接访问公有部分,但是只能通过公有成员函数(或友元函数)访问对象的私有成员。 C++中还提供了protected关键字,这将在类继承中介绍。
数据隐藏不仅可以防止直接访问数据,还让开发者(类用户)无需了解数据是如何被表示的。
(2)控制对成员的访问:公有还是私有
隐藏数据是OOP主要的目标之一,因此数据项通常放在私有成员部分,组成类接口的成员函数放在公有部分。当然也可以将成员函数放在私有部分,那么就只能通过公有方法来调用它们,而无法直接从程序中调用私有成员函数。
类声明中的private可以省略,因为这是类的默认访问控制方式。
class World
{
float mass;
public:
void tellall(void);
}
为了强调数据隐藏的概念,我们这里一概显式使用private。
(3)类和结构
类描述看上去很像结构声明(都有成员函数、public和private标签)。实际上C++对结构进行了扩展,使之具有与类相同的特性。结构默认访问类型为public,而类为private。
C++程序员通常使用类来实现类描述,而把结构限制为纯粹的数据对象。
1.2 实现类成员函数
(1)成员函数的定义
成员函数与常规函数非常相似,但是成员函数有两个特殊的特征。
- 定义类成员函数时,使用作用于解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private组件。
类方法的定义:
void Stock::update(double price)
{...}
(2)内联方法
成员函数的定义位于类声明中的函数自动成为内联函数。类声明常将小的成员函数作为内联函数。
如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。只需在类实现部分中定义函数时使用inline限定符即可:
inline void Stock::set_tot()
{...}
内联函数的特殊规则要求每个使用它们的文件中都对其进行定义,所以我们通常将内联定义放在定义类的头文件中。
2. 类的构造函数和析构函数
C++的目标之一就是让使用类对象就像使用标准类型一样,为了能想出初始化int类型一样初始化类对象,我们需要进行私有成员的访问,因此需要设计合适的成员函数,才能成功地将对象初始化。为此C++提供了特殊的成员函数——类构造函数,专门用于构造新对象。
2.1 声明和定义构造函数
构造函数的原型如下,下列原型使用了默认参数。
Stock(const string& co, long n = 0, double pr = 0.0);
它的定义如下:
Stock::Stock(const string& co, long n, double pr)
{
company = co;//赋值给私有数据成员
...
}
为了区别私有数据成员与其他变量,经常增加下划线的方式,如company_。
2.2 使用构造函数
- 显示调用构造函数
Stock food = Stock("world cabbage", 250, 1.25);
- 隐士调用构造函数
Stock garment("Furry Mason", 50, 2.5);
每次创建类对象(甚至使用new动态分配内存)时,C++都使用类构造函数。
Stock * pstock = new Stock("Electroshock", 18, 19.0);
上面创建的对象没有名称,只是将创建的对象地址赋给了pstock。
- C++列表初始化
Stock hot_tip = {"Derivatives", 100, 45};
Stock jock {"Sport Age Storage"};
Stock temp { };
2.3 默认构造函数
如果没有提供初始值,则调用默认构造函数。例如:
Stock fluffy_the_cat;//使用默认构造函数
如果类中没有提供任何的构造函数,则C++自动提供默认构造函数,它将不做任何工作:
Stock::Stock( ) { };
值得注意的是,当且仅当没有定义任何构造函数的时候,编译器才会提供默认构造函数。如果类定义了构造函数,那么用户就必须为它提供默认构造函数。如果提供了非默认构造函数,而没有提供默认构造函数,则下列声明将出错。
Stock stock1;//没有默认构造函数,报错!!!
注意区别下列几种定义的区别:
Stock first("Concrete");//隐式构造函数调用,非默认构造函数调用
Stock second();//定义一个函数,它的返回值为Stock类型
Stock third;//隐式地调用默认构造函数。
2.4 析构函数
对象过期时,程序将自动调用一个特殊的成员函数——析构函数。析构函数完成清理工作,实际上很有用,例如new分配内存的清理,析构函数的定义只需要在类名前加上~,因此Stock的析构函数为~Stock()。
由于析构函数不承担任何重要的工作,因此可以将它编写为不执行任何操作的函数:
Stock::~Stock()
{...}
- 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。
- 如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动被调用。
- 如果对象是通过new创建的,则它将驻留在自由存储区,当使用delete来释放内存时,其析构函数将被自动被调用。
析构函数是必须的,如果程序员没有提供析构函数,则编译器将隐士地声明一个默认析构函数。
2.5 const成员函数
如果定义一个const对象,那么它就无法调用自己的函数。例如:
const Stock land = Stock("kludgehorn");
land.show();//不被允许,因为不能确保调用的对象的数据不被修改!!!
解决办法为,在成员函数声明和成员函数定义的后面加入const,表示确保函数不会修改调用对象(不会修改land):
void show() const;//成员函数原型
void Stock::show() const//成员函数定义
{
...
}
以这种方式定义的成员函数被称为const成员函数。const成员函数只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
3. this指针
有时候方法设计到两个对象时,就需要使用C++的this指针。比如我们要比较两个对象的值,则可以这么像下列调用一样调用,无论使用哪一种都可以。
top = stock1.topval(stock2);//显示调用stock2,隐式调用stock1
top = stock2.topval(stock1);//显示调用stock1,隐式调用stock2
具体代码定义片段:
const Stock& Stock::topval(const Stock& s) const
{
if(s.total_val > total_val)
return s;//返回被调用的对象
else
return *this;//返回自己的对象本身,this是本身的地址
}
上面的代码片段注意以下几点:
- s表示显示地访问对象,对topval进行调用的对象是隐式访问。
- 括号中的const表示不会修改被显式地访问的对象数据,括号后面的const表示不会修改隐式访问的对象数据。
- this是隐式访问对象的地址,则*this表示对象。
4. 对象数组
我们可以对象数组创建多个对象。下列的创建4个对象,每个对象都调用了默认构造函数。
Stock mystuff[4];//创建4个对象
我们可以使用进行数据成员和成员函数访问:
mystuff[0].update();
mystuff[0].show();
如果我们不想使用默认构造函数,我们可以自己选择构造函数类型。
const int STKS = 4;
Stock stocks[STKS] = {
Stock("Nano", 12.5, 20),
Stock();
Stock("Mono", 130, 3.25),
};
stocks[0]和stocks[2]使用相同的构造函数,stocks[1]使用默认构造函数,stocks[3]没有进行显示的初始化,它也是调用默认构造函数。
5. 类作用域
在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称,只在该类中是已知的,在类外是不可知的。
在类外使用成员时,必须根据上下文使用直接成员运算符( . )、间接成员运算符( -> )或作用域解析运算符( :: )。
5.1 作用域为类的常量
如果类声明的字面值对于所有对象来说都是相同的,那么我们可以使用类对它进行访问。但是下面的操作是不对的,因为定义类的时候是不分配空间的。
class Bakery
{
private:
const int Months = 12;//不可以
double consts[Months];
我们有两种方式实现上述操作:
(1)枚举
class Bakery
{
private:
enum{Months = 12};//不是类的数据成员,不能用类对它进行访问
double consts[Months];
用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象都不包含枚举。另外Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12来替换它。
(2)static
class Bakery
{
private:
static const int Months = 12;//是类的数据成员,所有的对象和类都可以访问。
//static的double类型不能在类中写
double consts[Months];
这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此只有一个Months常量,被Bakery对象共享。所以所有的类和对象都可以访问它。
C++98中,只能使用这种技术声明值与整数或枚举的静态常量,而不能存储double常量。C++11消除了这种限制。
5.2 作用域内枚举(C++)
传统的枚举存在一些问题,例如下列两个枚举类型将会产生冲突。
enum egg {Small, Medium, Large, Jumbo };
enum t_shirt {Small, Medium, Large, Xlarge};
上述代码无法通过编译,因为egg和t_shirt存在相同的枚举量,它们会发生冲突。为解决这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。
enum class egg {Small, Medium, Large, Jumbo };
enum class t_shirt {Small, Medium, Large, Xlarge};
也可使用struct代替class,无论使用哪一种方式,都需要使用枚举名来限定枚举量。
egg choice = egg::Large;
t_shirt Floyd = t_shirt::Large;
C++11还提高了作用域内枚举的类型安全,作用域内枚举不能隐式转换为整型。
enum egg {Small, Medium, Large, Jumbo };//普通类型
enum class t_shirt {Small, Medium, Large, Xlarge};//类作用域
egg one = Medium;
t_shirt rolf = t_shirt::Large;
int king = one;//可以,普通类型转换
int ring = rolf;//不可以,不能进行作用域的转换
if(king < Jumbo)//可以
if(king < t_shirt::Medium)//不可以,不能隐士转换为int比较
但是我们可以显式转换:
int Frodo = int(t_shirt::Small);
默认情况下,枚举的底层实现为int,但是C++11中,我们可以显式的使用特定类型的底层实现,底层类型必须为整型。
enum class : short t_shirt {Small, Medium, Large, Xlarge};//底层类型指定为short
6. 抽象数据类型
抽象数据类型(abstract data type,ADT),使用类表示通用的概念。类很适合表示ADT,公有成员函数接口提供ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。
总览目录
上一篇:(七)内存模型和名称空间
下一篇:(八)使用类
文章参考:《C++ Primer Plus第六版》