重学C++笔记之(八)对象和类


最重要的OOP特性:

  • 抽象
  • 封装和数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

为了实现这些特性并将它们组合在一起,C++所做的最重要的改进是提供了类。前面几篇文章大多数讲的是面向过程编程,下面我们正式开始介绍面向对象的编程。

1. 抽象和类

1.1 C++中的类

使用关键字class指出类设计,在这里不能用typename。类规范由两部分组成:

类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口
类方法定义:描述如何实现类成员函数

简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。在类中,成员函数可以在类中定义,也可以用原型表示

(1)访问控制

关键字private和public描述了对类成员的访问控制,类对象可以直接访问公有部分,但是只能通过公有成员函数(或友元函数)访问对象的私有成员。 C++中还提供了protected关键字,这将在类继承中介绍。

数据隐藏不仅可以防止直接访问数据,还让开发者(类用户)无需了解数据是如何被表示的。

(2)控制对成员的访问:公有还是私有

隐藏数据是OOP主要的目标之一,因此数据项通常放在私有成员部分,组成类接口的成员函数放在公有部分。当然也可以将成员函数放在私有部分,那么就只能通过公有方法来调用它们,而无法直接从程序中调用私有成员函数。

类声明中的private可以省略,因为这是类的默认访问控制方式。

class World
{
    
    
	float mass;
publicvoid 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成员函数。

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只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用30来替换它。

(2)static

class Bakery
{
    
    
	private:
		static const int Months = 12;
		double consts[Months];

这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此只有一个Months常量,被Bakery对象共享。C++98中,只能使用这种技术声明值与整数或枚举的静态常量,而不能存储double常量。C++11消除了这种限制。

注意在类中初始化的情况只能是静态数据成员是整形或枚举类型const。其它类型则需要在定义类的cpp文件中进行初始化。

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第六版》

猜你喜欢

转载自blog.csdn.net/QLeelq/article/details/111059334