栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C++ Primer Plus学习】第10章 对象和类

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C++ Primer Plus学习】第10章 对象和类

写在前面:本文是书籍《C++ Primer Plus(第六版)》第10章的学习笔记,文中所提到的程序清单都是书籍中的源码,不过是自己手敲了下来,见:https://gitee.com/xuthirteen/c-plus-plus.git

目录

10.1 过程性编程和面向对象编程

10.2 抽象和类

10.2.1 类型是什么

10.2.2 C++中的类

10.2.3 实现类成员函数

10.2.4 使用类

10.2.5 修改实现

10.2.6 小结

10.3 类的构造函数和析构函数

10.3.1 声明和定义构造函数

10.3.2 使用构造函数

10.3.3 默认构造函数

10.3.4 析构函数

10.3.5 改进Stock类

10.3.6 构造函数和析构函数小结

10.4 this指针

10.5 对象数组

10.6 类作用域

10.6.1 作用域为类的常量

10.6.2 作用域内枚举(C++11)

10.7 抽象数据类型

10.8 总结


本章内容包括:

  • 过程性编程和面向对象编程
  • 类概念
  • 如何定义和实现类
  • 公有类访问和私有类访问
  • 类的数据成员
  • 类方法(类函数成员)
  • 创建和使用类对象
  • 类的构造函数和析构函数
  • const成员函数
  • this指针
  • 创建对象数组
  • 类作用域
  • 抽象数据类型

面向对象编程(OOP)是一种特殊的、设计程序的概念性方法,下面是最重要的OOP特性:

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

10.1 过程性编程和面向对象编程

采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据。

采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。


10.2 抽象和类

处理复杂性的方法之一是简化和抽象。

10.2.1 类型是什么

在内存中如何存储来考虑数据类型,指定基本类型完成了三项工作:

  • 决定数据对象需要的内存数量
  • 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同)
  • 决定可使用数据对象执行的操作或方法

10.2.2 C++中的类

类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。一般来说,类规范由两个部分组成:

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

简单来说,类声明提供了类的蓝图,而方法定义则提供了细节。

//程序清单10.1 stock00.h

(1)访问控制

防止程序直接访问数据被称为数据隐藏。

类设计尽可能将公有接口与实现细节公开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。

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

不必在类声明中使用关键字private,因为这是类对象的默认访问控制。

类和结构的唯一区别是:结构的默认访问类型是public,而类为private。

10.2.3 实现类成员函数

还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。类成员函数有两个特殊的特征:

  • 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类
  • 类方法可以访问类的private组件

类方法的完整名称中包括类名。Stock::update()是函数的限定名(qualified name);而简单的update()是全名的缩写(非限定名,unqualified name),它只能在类作用域中使用。

方法第二个特点是,方法可以访问类的私有成员。

//程序清单10.2 stock00.cpp

(1)成员函数说明

(2)内联方法

其定义位于类声明的函数都将自动成为内联函数,因此Stock::set_tot()是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot()符合这样的要求。

如果愿意,也可以在类声明之外定义成员函数,并使其称为内联函数。为此,只需在类实现部分中定义函数时使用inline限定符即可:内联函数的特殊规则要求在每个使用它们的文件中都对齐进行定义。确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中。

(3)方法使用哪个对象

使用对象时最重要的一个方面:如何将类方法应用于对象。

10.2.4 使用类
//程序清单10.3 usestock0.cpp

10.2.5 修改实现

10.2.6 小结

指定类设计的第一步是提供类声明。通常,数据成员被放在私有部分中,成员函数被放在公有部分中,因此典型的类声明的格式如下:

class className
{
private:
    date member declartions
public:
    member function prototypes
}

公有部分的内容构成了设计的抽象部分——公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。指定类设计的第二步是实现类成员函数。


10.3 类的构造函数和析构函数

构造函数和析构函数为类的标准函数。类构造函数,专门用于构造新对象、将值赋给它们的数据成员,名称与类名相同。构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有声明为void类型。实际上,构造函数没有声明类型。

10.3.1 声明和定义构造函数

程序员可能只想设置company成员,而将其他值设置为0,这可以使用默认参数来完成。因此,原型如下所示:

//constructor prototype with some default arguments
Stock(const string & co, long n = 0, double pr = 0.0);

构造函数的参数名不能和类的成员名一致,可以对成员名加上m_前缀或者_后缀。

10.3.2 使用构造函数

C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式地调用构造函数:

Stock food = Stock("World Cabbage", 250, 1.25);

另一种方式是隐式地调用构造函数:

Stock garment("Furry Mason", 50, 2.5);

下面是将构造函数与new一起使用的方法:

Stock *pstock = new Stock("Electroshock Games", 18, 19.0);

10.3.3 默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。也就是说,它是用于下面这种声明的构造函数:

Stock fluffy_the_cat;//uses the default constructor

如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。

定义默认构造函数的方式有两种,一种是给已有构造函数的所有参数提供默认值:

Stock(const string & co = "Error", int n = 0, double pr = 0.0);

另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数:

Stock();

由于只能有一个默认构造函数,因此不要同时采用这两种方式。实际上,通常应初始化所有的对象,以确保所有成员一开始就是已知的合理值。

默认构造函数可以显示地调用、隐式地调用,隐式地调用不要使用圆括号:

Stock first;//calls default constructor implicitly
Stock first = Stock();//calls it explicitly
Stock *prelief = new Stock;//calls it implicitly

10.3.4 析构函数

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数。如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。Stock的构造函数没有使用new,因此析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么都不做的隐式析构函数即可。

析构函数的名称很特殊:在类名前加上~。和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此Stock析构函数的原型必须是这样的:

~Stock();

什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显示地调用析构函数(但是有例外情形)。如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内容时,其析构函数将自动被调用。

由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。

10.3.5 改进Stock类

(1)头文件

//程序清单10.4 stock10.h

(2)实现文件

//程序清单10.5 stock10.cpp

注:将文件名放在双引号而不是方括号意味着编译器将源文件所在的目录中搜索它。

(3)客户文件

//程序清单10.6 usestock2.cpp

(4)程序说明

在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。

输出表明,下面两条语句有根本性的差别:

Stock stock2 = Stock("Boffo Objects", 2, 2.0);
stock1 = Stock("Nifty Foods", 10, 50.0);//temporary object

第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会);第二条语句是赋值,像这样在赋值语句中使用构造函数总会导致在赋值前面创建一个临时对象。如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式,通常这种方式的效率更高。

(5)C++11列表初始化

在C++11中,可将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起:

Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};
Stock jock {"Sport Age Storage, Inc"};
Stock temp {};

另外,C++11还提供了名为std::initialize_list的类,可将其用作函数参数或方法参数的类型。这个类可表示任意长度的列表,只要所有列表项的类型都相同或可转换为相同的类型。

(6)const成员函数

代码片段:

const Stock land = Stock("kludgehorn Properties");
land.show();

对于当前的C++来说,编译器将拒绝第二行。因为show()的代码无法确保调用对象不被修改,调用对象和const一样,不应被修改。我们以前通过将函数参数声明为const引用或指向const的指针来解决这种问题。但这里存在语法问题:show()方法没有任何参数,相反,它使用的对象是由方法调用隐式地提供的。需要一种新的语法,保证函数不会修改调用对象。

C++的解决方法是将const关键字放在函数括号后面。也就是说,show()声明应像这样:

void show() const;//promise not to change invoking object

以这种方式声明和定义的类函数被称为const成员函数。就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const。从现在开始,我们将遵守这一规则。

10.3.6 构造函数和析构函数小结

构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名应相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数特征标(参数列表)都不同。另外,构造函数没有声明类型。通常,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。

接收一个参数的构造函数允许使用赋值语法将对象初始化为一个值。这种特性可能导致问题,第11章将介绍,可关闭这项特性。

默认构造函数没有参数,因此如果创建对象时没有进行显式地初始化,则将调用默认构造函数。如果程序中没有提供任何构造函数,则编译器会为程序定义一个默认构造函数,否则,必须自己提供默认构造函数。默认构造函数可以没有任何参数;如果有,则必须给所有参数都提供默认值。

每个类都只能有一个析构函数。析构函数没有返回类型(连void都没有),也没有参数,其名称为类名称前加上~。如果构造函数使用了new,则必须提供使用delete的析构函数。


10.4 this指针

到目前为止,每个类成员函数都只涉及一个对象,即调用它的对象。但有时候方法可能涉及到两个对象,在这种情况下需要使用C++的this指针。

虽然Stock类声明可以显示数据,但它缺乏分析能力。例如,从show()的输出我们可以知道持有的哪一支股票价格最高,但由于程序无法直接访问total_val,因此无法作出判断。要让程序知道存储的数据,最直接的方式是让方法返回一个值。为此,通常使用内联代码,如下例所示:

class Stock
{
private:
    ...
    double total_val;
    ...
public:
    double total() const {return total_val;}
    ...
};

可以采用另一种方法,定义一个成员函数,它查看两个Stock对象,并返回股价较高的那个对象的引用。实现这种方法时,将出现一些有趣的问题,下面就来讨论这些问题:

  • 首先,如何将两个要比较的对象提供给成员函数呢?假设将该方法命名为topval(),处于效率方面的考虑,可以按引用来传递参数,topval()方法使用一个类型为const Stock &的参数。
  • 其次,如何将方法的答案传回给调用程序呢?最直接的方法时让方法返回一个引用,该引用指向股价总值较高的对象。因此,用于比较的方法的原型如下:
const Stock & topval (const Stock & s) const;

该函数隐式地访问一个对象,而显式地访问另一个对象,并返回其中一个对象的引用。括号中的const表明,该函数不会修改被显式地访问的对象;而括号后的const表明,该函数不会修改被隐式地访问的对象。由于该函数返回了两个const对象之一的引用,因此返回类型也应为const引用。

同时,还要注意的是topval()的实现,它将引发一个小问题,下面的部分实现强调了这个问题:

const Stock & topval (const Stock & s) const
{
    if(s.total_val > total_val)
        return s;
    else
        return ???;
}

C++解决了这种问题的方法是:使用被称为this的特殊指针。this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。这样,函数调用stock1.topval(stock2)将this设置为stock1对象的地址,使得这个指针可用于topval()方法。一般来说,所有的类方法都将this指针设置为调用它的对象的地址。确实,topval()中的total_val只不过是this->total_val的简写。

注:每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。返回类型为引用意味着返回的是调用对象本身,而不是其副本。

//程序清单10.7 stock20.h
//程序清单10.8 stock20.cpp

10.5 对象数组

声明对象数组的方法与声明标准类型数组相同:

Stock mystuff[4];//creates an array of 4 Stock objects

可以用构造函数来初始化数组元素。在这种情况下,必须为每个元素调用构造函数:

const int STKS = 4;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 20),
    Stock("Boffo Objects", 200, 2.0),
    Stock("Monolithic obelisks", 130, 3.25),
    Stock("Fleep Enterprises", 60, 6.5)
};

这里的代码使用标准格式对数组进行初始化:用括号括起的、以逗号分隔的值列表。其中,每次构造函数调用表示一个值。如果类包含多个构造函数,则可以对不同的元素使用不同的构造函数:

const int STKS = 10;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 20),
    Stock(),
    Stock("Monolithic obelisks", 130, 3.25),
};

由于该声明只初始化了数组的部分元素,因此余下的7个元素将使用默认构造函数进行初始化。初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。

//程序清单10.9 usestock2.cpp

10.6 类作用域

C++类引入了一种新的作用域:类作用域。在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。另外,类作用域意味着不能从外直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过对象。

使用类成员名时,必须根据上下文使用直接成员运算符(.)、间接成员运算符(->)或作用域解析运算符(::)。

10.6.1 作用域为类的常量

有时候,使符号常量的作用域为类很有用,创建一个由所有对象共享的常量。有两种方式可以实现这个目标,并且效果相同:

  • 第一种方式是在类中声明一个枚举。在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12来替换它。
class Bakery
{
private:
    enum {Months = 12};
    double consts[Months];
    ...
};
  • C++提供了另一种在类中定义常量的方式——使用关键字static。这将创建一个名为Months的常量,该常量将与其他静态常量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Bakery对象共享。
class Bakery
{
private:
    static const int Months = 12;
    double consts[Months];
    ...
};

10.6.2 作用域内枚举(C++11)

传统的枚举存在一些问题,其中之一是两个枚举定义中的枚举量可能发生冲突。假设有一个处理鸡蛋和T恤的项目,其中可能包含类似下面这样的代码:

enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small Medium, Large, Xlarge};

这将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。
为避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:

enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small Medium, Large, Xlarge};

也可使用关键字struct代替class。

egg choice = egg::Large;//the Large enumerator of the egg num
t_shirt Floyd = t_shirt::Large; //the Large enumerator of the t_shirt enum

C++11还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型,但在必要时,可进行显式类型转换:

int Frodo = int (t_shirt::Small);//Frodo set to 0

默认情况下,C++11作用域内枚举的底层类型为int。另外,还提供了一种语法,可用于做出不同的选择:

//underlying type for pizza is short
enum class : short pizza {Small, Medium, Large, XLarge};

但如果没有指定,编译器选择的底层类型将随实现而异。


10.7 抽象数据类型

Stock类非常具体。然而,程序员常常通过定义类来表示更通用的概念。抽象数据类型(abstractdata type,ADT),以通用的方式描述数据类型,而没有引入语言或者实现细节。栈的特征。首先,栈存储了多个数据项(该特征使得栈成为一个容器——一种更为通用的抽象);其次,栈由可对它执行的操作来描述。

  • 可创建空栈
  • 可将数据项添加到堆顶(压入)
  • 可从栈顶删除数据项(弹出)
  • 可查看栈是否填满
  • 可查看栈是否为空

可以将上述描述转换为一个类声明,其中公有成员函数提供了表示栈操作的接口,而私有数据成员负责存储栈数据。类概念非常适合于ADT方法。

//程序清单10.10 stack.h
//程序清单10.11stack.cpp
//程序清单10.12 stacker.cpp

10.8 总结

面向对象编程强调的是程序如何表示数据。

如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它,如果方法需要显示地引用调用它的对象,则可以使用this指针。由于this指针被设置为调用对象的地址,因此*this是该对象的别名。

写在后面:这个章节的学习让我知道了什么是内联函数,一个比较简单的概念,对构造函数、析构函数的理解更加深入,最后还知道了什么是this指针,还是挺有收获的。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1034655.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号