第12讲——对象和类
Posted GGBeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第12讲——对象和类相关的知识,希望对你有一定的参考价值。
【抽象和类】
引言:生活中充满复杂性,处理复杂性的方法之一是简化和抽象。如果我们要用信息与用户之间的的接口来表示计算,那么抽象将是至关重要的。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。在上一讲的垒球统计数据示例中,接口描述了用户如何初始化、更新和显示数据。抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽奖接口的类设计。
1.1 类型是什么
当我们看到一个给定的基本数据类型,我们会想到:
- 它定义的数据对象需要的内存;
- 它定义的数据对象能执行的操作;
- 它决定如何解释内存中的位(如long和float在内存中占用的位数相同,但将它们转换为数值的方法不同)。
具体来说,我们可以根据数据在内存中如何存储来考虑其数据类型(例如,char占用1个字节的内存,而double占用8个字节的内存),也可以根据能对数据进行的操作来定义其数据类型(例如,int类型可以使用所有的算术运算,可对整数执行加、减、乘、除运算,而且还可以对它们使用求模运算符%)。而指针需要的内存数量很可能与int相同,甚至可能在内部被表示为整数,但不能对指针执行与整数相同的运算(例如,不能将两个指针相乘,这种运算将毫无意义)。因此,将变量声明为int或float指针时,不仅仅是分配内存,还规定了可对变量执行的操作。
对于内置类型来说,有关操作的信息被内置到编译器中。但在C++中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
1.2 C++中的类
我们来尝试定义一个类:
- 类声明:数据成员+成员函数(前者描述数据部分,后者描述公有接口)
- 类方法定义:描述如何实现类成员函数
我们仿佛感觉出:类声明提供了类的蓝图,而方法定义则提供了细节。
什么是接口?
接口是一个共享框架,供两个系统(如计算机和打印机之间或者用户和计算机程序之间)交互时使用。我们举一个例子:假设用户是我,程序是字处理器。我使用字处理器时,不能直接将脑子中想到的词传输到计算机内存中,而必须同程序提供的接口交互。我敲打键盘时,计算机将字符显示到屏幕上;我移动鼠标时,计算机移动屏幕上的光标,我无意间单击鼠标时,计算机对我输入的段落进行奇怪的处理。程序接口将我的意图转换为存储在计算机中的具体信息。
对于类,我们常谈公共接口。在这里,公众(public)是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。举个例子:要计算string对象中包含多少个字符,我们无需打开对象,而只需使用string类提供的size()方法。类设计禁止公共用户直接访问类,但公众可以使用方法size()。方法size()是用户和string类对象之间的公共接口的组成部分。通常,方法getline()是istream类的公共接口的组成部分,使用cin的程序不是直接与cin对象内部交互来读取一行输入,而是使用getline()。
如果希望更人性化,我们不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户。然而,要使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口。
我们编程实现类时,通常将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。在头文件中,对于数据成员,我们要进行定义,而对于成员函数,我们可以进行定义,也可以用原型表示(那这部分成员函数将在源代码文件中定义)。因此,对于描述函数接口而言,原型就够了。
下面我们看一个头文件,它是Stock类的类声明:
// stock00.h -- Stock class interface // version 00 #ifndef STOCK00_H_ //使用#ifndef访问多次包含同一个文件 #define STOCK00_H_ #include <string> class Stock // class declaration { private: std::string company; long shares; double share_val; double total_val; void set_tot() { 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(); }; // note semicolon at the end #endif
类最吸引人的特性——将数据和方法组合成一个单元。
类成员的访问控制也是我们需要着重考虑的。
C++提供了private/public/protected关键字来描述类成员的属性。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此,公有成员函数被称为是程序和对象的私有成员之间的桥梁,它提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。
由于数据隐藏是OOP主要的目标之一,因此类的数据项通常放在私有部分,组成类接口的成员函数放在公有部分(否则就无法从程序中调用这些函数)。尽管如此,我们仍可以将成员函数放在私有部分,虽然程序无法调用,但是公有方法却可以使用它。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
类和结构
类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是——结构默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。
1.3 实现类成员函数
在类声明的文件中,有部分成员函数是用原型表示的。那么我们需要为这些成员函数提供代码,我们称之为成员函数定义,这个过程在实现文件中完成。
成员函数定义有两个重要特征:
- 使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private组件。
对于第一个特征,我们举例如下:
void Stock::update(double price)
它意味着我们定义的update()函数是Stock类的成员。这不仅将update()标识为成员函数,还意味着我们可以将另一类的成员函数也命名为update()。
因此,作用域解析运算符确定了方法定义对应的类的身份。我们说,标识符update()具有类作用域。Stock类的其他成员函数不必使用作用域解析运算符就可以使用update()方法,这是因为它们属于同一个类,因此update()对它们是可见的。
类方法的完整名称中包括类名。我们说,Stock::update()是函数的限定名,而简单的update()是全名的缩写(非限定名),他只能在类作用域中使用。
对于第二个特征,我们要知道的是,如果使用非成员函数访问该类的私有数据成员,编译器将禁止这么做(友元函数例外)。
掌握了上面这两点,我们便可实现类方法了,我们将这个过程放在一个独立的实现文件中(因此需要包含头文件stock00.h,让编译器能够访问类定义):
// 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_tot(); } 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_tot(); } } void Stock::sell(long num, double price) { using std::cout; if (num < 0) { cout << "Number of shares sold can‘t be negative. " << "Transaction is aborted.\n"; } else if (num > shares) { cout << "You can‘t sell more than you have! " << "Transaction is aborted.\n"; } else { shares -= num; share_val = price; set_tot(); } } void Stock::update(double price) { share_val = price; set_tot(); } void Stock::show() { std::cout << "Company: " << company << " Shares: " << shares << ‘\n‘ << " Share Price: $" << share_val << " Total Worth: $" << total_val << ‘\n‘; }
4个成员函数都设置或重新设置了total_val成员值。这个类并非将计算代码编写4次,而是让每个函数都调用set_tot()函数。我们知道,set_tot()函数是在头文件中定义的私有成员函数,所以它只是实现代码的一种方式,而不是公有接口的组成部分。这种方法的主要价值在于:通过使用函数调用(而不是每次重新输入计算代码)可以确保执行的计算完全相同。另外,如果必须修订计算代码,则只需在一个地方进行修改即可。
我们称在类声明中定义的函数为内联函数,因此Stock::set_tot()是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot()符合这样的要求。内联函数的特殊规则要求——在每个使用它们的文件中都对其进行定义。那么确保内联定义对多文件程序中的所有文件都是可用的、最简便的方法是:将内联定义放在定义类的头文件中。
1.4 使用类
知道如何定义类及其方法后,我们便可以创建一个这样的程序,它创建并使用类对象:
// usestock0.cpp -- the client program // compile with stock00.cpp #include <iostream> #include "stock00.h" int main() { Stock fluffy_the_cat; fluffy_the_cat.acquire("NanoSmart", 20, 12.50); fluffy_the_cat.show(); fluffy_the_cat.buy(15, 18.125); fluffy_the_cat.show(); fluffy_the_cat.sell(400, 20.00); fluffy_the_cat.show(); fluffy_the_cat.buy(300000,40.125); fluffy_the_cat.show(); fluffy_the_cat.sell(300000,0.125); fluffy_the_cat.show(); return 0; }
C++的目标是使得使用类与使用基本的内置类型(如char和int)尽可能相同。要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。C++提供了一些工具,可用于初始化对象,让cin和cout识别对象,甚至在相似的类对象之间进行自动类型转换。
注意,main()只是用来测试Stock类的设计。当Stock()类的运行情况与预期的相同后,便可以在其他程序中将Stock类作为用户定义的类型使用。要使用新类型,最关键的是要了解成员函数的功能,而不必考虑其实现细节。
客户/服务器模型
OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序,类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外的影响。
【类的构造函数和析构函数】
以上是关于第12讲——对象和类的主要内容,如果未能解决你的问题,请参考以下文章
VSCode自定义代码片段12——JavaScript的Promise对象
VSCode自定义代码片段12——JavaScript的Promise对象
区块链-前端交互第三篇:JS 基础语句和函数对象和类class