《c++ Primer Plus 第6版》读书笔记(5)

news/2024/7/5 4:05:09

第10章 对象和类

本章内容包括:

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

 OOP(面向对象编程)最重要的特性:

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

10.2 抽象和类

指定基本类型完成了三项工作:

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

类的定义,一般来说,类规范由两个部分组成:

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

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

什么是接口?

接口是一个共享框架,供两个系统之间(比如任何计算机系统之间或者人和计算机之间)交互时使用。

C++关键字中class指出了这些代码定义了一个类设计。使用类定义接口时,将会自动指定使用对象的规则。

1.访问控制

关键字private和public也是新的,描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但是只能通过公有成员函数或者友元函数来访问对象的私有成员。防止程序直接访问数据被称为数据隐藏。

类设计尽可能将公有接口和实现细节分开。公有接口表示小合集的抽象组件,将实现细节放在一起并将他们和抽象分开被称为封装。

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

由于OOP,数据项通常会放在私有部分,组成类接口的成员函数被放在公有部分。

使用私有成员函数来处理不属于公有接口的实现细节。

C++中结构和类都能够使用private和public,但是结构中默认访问类型为public,但是类中默认访问类型为private。C++中通常实用类实现类描述,而把结构限制为指标是纯粹的数据对象。

10.2.3 实现类成员函数

类成员函数相比于普通函数的两个特殊特征:

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

比如,update()成员函数:

        void Stock::update(double price)

类成员函数的内联方法

函数定义为与类声明中的函数都将自动成为内联函数。类声明常将短小的成员函数作为内联函数。

如果想在声明之外定义内联成员函数,只需要加iniline限定符号。

根据改写规则,在类声明中定义的方法等同于使用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。所以两者其实等价。

调用成员函数时,将会使用对象本身的数据成员。

每个对象都有字节的存储空间,用来存储内部变量和类成员。但是同一个类的所有对象共享一组类方法,即每种方法只有一个副本。

在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将会调用同一个方法。

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

由于类的成员变量存在私有,所以不能够使用之前常规的方法进行初始化。

所以类构造函数为了解决创建时自动初始化的问题,提供了特殊的类成员构造函数,构造函数的函数名和类名相同,且没有声明类型,也没有返回值。

构造函数中的参数名不能和类成员相同,不然会引起混乱。常见的解决方法是在成员变量之后或者之前加上特定的前缀。

10.3.2 使用构造函数

C++可以显示或者隐式调用构造函数。

// 显示方法
Stock food = Stock("Woril", 220, 1.25);

// 隐式方法
Stock food("Woril", 220, 1.25);

// 将构造函数和new一起使用的方法
Stock *food = new Stock("Woril", 220, 1.25);

两种构造方法等价。

构造函数只能够用来创建对象,不能够被对象调用。

如果没有提供任何构造函数,C++将会自动提供默认构造函数。

        Stock::Stock(){}

但只有当没有提供构造函数时,C++才会提供默认构造函数;

如果提供了构造函数,那么必须提供一个默认构造函数。

定义默认构造函数的方法有两种,一种是为构造函数的所有参数提供默认值;一种是提供一个没有参数的构造函数。

10.3.4 析构函数

在创建的对象过期后,程序将会自动调用析构函数。析构函数用来完成清理工作。

析构函数的名称是~类名。比如:

        ~Stock(){}

与构造函数不同的是,析构函数没有参数。何时调用析构函数由编译器来决定,不应该显示的调用析构函数。

如果没有提供析构函数,那么编译器将会自动添加一个隐式析构函数。

比如,如果定义一个静态类型对象,那么将会在整个程序结束后调用析构函数;如果定义一个自动变量,那么在执行完代码块时调用析构函数;如果通过new来创建对象,那么在delete时将会调用析构函数。

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

Stock s1 = Stock{"asd" , 100 . 45.0};
s2 = Stock{"asd" , 100 . 45.0};

考虑上面两个语句。

第一条语句是初始化语句,创建有指定值的对象,可能会创建临时对象;第二条语句是赋值语句,在赋值语句中使用构造函数总会创建一个临时对象。

如果既可以通过初始化也可以通过赋值来设置对象的值,则应该采用初始化的方式,通常这种方法的效率更高。

const 成员函数

const Stock land = Stock("dsadsa");
land.show();

编译器将会拒绝第二行的操作,因为无法保证调用对象不被修改,

之前会通过函数参数声明为const引用或者指向const的指针来解决这个问题,但是这里这样做会出现语法问题,因此需要一种新的语法来保证函数不会修改调用对象。

C++将const关键字放在函数括号后面来解决这个问题。

void show() const;

void Stock::show() const;

使用这种方法声明定义的类函数被称为const成员函数。

只要类方法不修改调用对象,就应该将其声明为const。

10.4 this指针

如果类成员函数中涉及到了多个对象,那么为了区分,就需要用到this指针。

定义一个比较函数,使用const引用作为参数,函数不改变调用对象的值,返回一个满足函数条件的引用:

const Stock & topval(const Stock & s) const
{
    if (s.val > val)
    {
        return s;
    }
    else
    {
        return ???
    }
}
  • 括号中的const表明,该函数不会修改被显式访问的对象。
  • 括号后的const表明,该函数不会修改被隐式访问的对象。
  • 由于该函数返回两个const对象之一的引用,因此返回类型也是const引用。
  • 返回类型为引用意味着返回的是调用对象本身,而不是副本

此时有个问题,如何表示隐式访问的对象本身?

C++中使用this的特殊指针,指向用来调用成员函数的对象。

const Stock & topval(const Stock & s) const
{
    if (s.val > val)
    {
        return s;
    }
    else
    {
        return *this
    }
}

10.5 对象数组

用户通常会创建一个类的多个对象,此时就需要用到对象数组。

声明对象数组的方法和声明标准类型数组的方法是相同的。

        Stock mystaff[4];

此时如果没有构造函数,那么就会调用隐式构造函数。

也可以显示调用构造函数:

const int STKS = 4;
Stock stocks[STKS] = {
    Stock{"s1", 1, 1.0},
    Stock{"s2", 2, 1.0},
    Stock{"s3", 3, 1.0}
};

当然,也可以将其中的构造函数换成不同的构造函数。

显示构造函数之外的对象都将调用隐式默认构造函数。

10.6 类作用域

C++类引入了一种新的作用域叫做类作用域。

在类中定义的名称,比如数据成员名和类成员函数名,作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。

类作用域意味着不能从外部直接访问类的成员。使用公有也是一样,需要通过对象来访问。

10.6.1 作用域为类的常量

不能够在声明类的时候赋值常量(直接使用const在类内不行),但是有两种方式能够达到相同的效果。

第一种方式是在类中声明一个枚举:

class Bakery
{
private:
    enum {Months = 12};
    double costs{Months};
    ...
}

使用这种方式声明枚举并不会创建类数据成员,也就是说,所有对象中都不会包含枚举。

第二种方式是使用关键字static:

class Bakery
{
private:
    static const int Months= 12;
    double costs{Months};
    ...
}

此时将会创建一个名为Months的常量,该常量和其他静态变量储存在一起,而不是存储在对象中

此时只有一个Months常量,被所有Bakery对象共享。

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

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

 此时将无法通过编译,因为发生了冲突。

C++11提供了一种新的枚举方法,枚举量的作用域为类:

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

也可以通过struct代替class。此时使用枚举名来限制枚举量:

egg choice = egg::Large;
t_shirt Floyed = t_shirt::Small;

使用这种方法提高了类型安全性,作用域内枚举不能隐式与int进行转换。

第11章 使用类

本章内容包括:

  • 运算符重载
  • 友元函数
  • 重载<<运算符,用于输出
  • 状态成员
  • 使用rand()生成随机数
  • 类的自动转换和强制类型转换
  • 类转换函数

11.1 运算符重载

运算符重载是一种形式的C++多态。

之前的章节中介绍了函数多态的方式。而运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多重含义。

C++允许将运算符扩展到用户定义的类型,要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数的格式如下:

        operator op (argument-list)

比如说,operator + ()重载+运算符,operator *()重载*运算符。

当然,op必须是有效的C++运算符,不能够虚构一个新的符号。

举例来说,如果重载了Saleperson类的+操作,那么就可以在代码中使用如下的形式:

        ans = sid + sara;

编译器此时发现数据类型为Saleperson类,那么将会替换成如下形式:

        ans = sid.operator+ (sara);

 总之,使用operator+()或者使用运算符表示法都可以进行调用。

11.2.2 重载限制

C++对用户定义的运算符重载有限制:

  1. 重载后的运算符必须至少有一个操作数是用户定义类型。防止用户将标准类型进行重载。
  2. 使用运算符不能违反运算符原来的句法规则。比如不能将%(使用两个操作数)重载成使用一个操作数。同样,也不能修改运算符的优先级。
  3. 不能创建新的运算符。
  4. 以下运算符不能够被重载:
    sizeof内存量
    .成员运算符
    .*成员指针运算符
    ::作用域解析运算符
    ?:条件运算符
    typeid一个RTTI运算符
    const_cast强制类型转换运算符
    dynamic_cast强制类型转换运算符
    reinterpret_cast强制类型转换运算符
    static_cast强制类型转换运算符

  5. 下面的运算符只能通过成员函数进行重载:
    1.  =:赋值运算符
    2.  () :函数调用运算符
    3.  []:下标运算符
    4.  ->:通过指针访问类成员的运算符

11.3 友元

只能通过公有方法对数据进行访问,有时候这种限制过于严格。C++提供了另外一种形式的访问权限:友元。

友元有三种:

  • 友元函数
  • 友元类
  • 友元成员函数

考虑使用非成员函数,进行运算符重载:

        A= 2.7 * B;

编译器将会与下面的非成员函数调用进行匹配:

        A= operator * (2.7, B);

该函数的原型如下:

        Time operator * (double m,const Time & t);

使用非成员函数可以按照所需要的顺序获取操作数(先是double ,然后是Time)但是问题是,非成员函数不能够直接访问类的私有数据。然而, 有一类特殊的非成员函数,可以访问类的私有成员,它们被称为友元函数。

11.3.1 创建友元

创建友元函数的第一步是将函数的原型放在类声明中,并在原型声明前加上关键字friend。

friend Time operator* (double m , const Time & t);

该原型会说明:

  • operator *函数不是成员函数,不能够使用成员运算符来调用
  • operator *函数和成员函数的访问权限相同

第二步是编写函数定义,因为不是Time的成员函数,所以不用加Time ::的限定符。另外不要在定义中使用friend的关键字:

Time operator* (double m , const Time & t)
{
    Time result;
    long totalminutes = t.hours * 60 + t.minutes;
    result.hours = totalminutes / 60;
    result.minutes = totalminutes % 60;
    return result;
}

友元函数是否违背了C++的OOP呢?其实没有,应该将友元函数视作类的扩展接口的组成部分。

如果要为类重载运算符,并将分类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。

11.3.2 常用的友元,重载<< 运算符

1. << 的第一种重载版本

如果要将类作为第一个操作数,意味着需要这样写:A << cout;

这样写会让人迷惑,所以可以通过友元函数来进行重载:

void operator << (ostream & os, const Time & t)
{
    os << t.hours << " hours, "<< t.minutes << " minutes";
}

// 之后可以直接使用cout进行输出
cout << A;

由于只需要对于os整体进行使用,所以不需要成为os的友元函数。

2. << 的第二种重载版本

方法1实际上存在一个问题:

只能使用cout << A的形式,如果cout中有其他类型,那么就不允许了,比如cout << "Trip Time:" << A;

cout << A << B等同于(cout << A)<< B

<<运算符要求左边是一个ostream对象,所以可以对友元函数采用相同的方法,只要修改operator <()函数,让他返回ostream对象的引用即可:

ostream & operator << (ostream & os, const Time & t)
{
    os << t.hours << " hours, "<< t.minutes << " minutes";
    return os;
}

11.4 重载运算符:成员函数还是非成员函数

 在定义运算符时,必须选择其中的一种格式,而不能同时选择两种方式,否则会出现二义性错误。

11.5 再谈重载:矢量类

(这里是使用的一个例子举例进行说明,不展开)

对已重载的运算符进行重载

因为运算符重载是通过函数来实现的,所以只要运算符函数的特征值不同,使用的运算符数量与相应的内置C++运算符相同,就可以多次重载同一个运算符。

比如对 - 进行重载,有两种版本:

// 两个操作数的版本
Vector operator - (const Vector & b) const;

// 一个操作数的版本
Vector operator - () const;

谈谈随机数

标准ANSI库有一个rand()函数,会返回一个从0到某个值之间的随机整数,但直接使用一般由于种子数默认,返回的都是伪随机数。

可以使用srand(time(0))来覆盖默认的种子值,其中time(0)代表返回当前时间,这样每次运行都会设置不同的种子。

11.6 类的自动转换和强制类型转换

 如果类的构造函数只有一个参数,那么可以直接进行赋值,比如:

        Stonewt myCat;

        myCat =15.6;

此时,程序会调用构造函数创建一个临时对象,并将临时对象赋值。这一过程为隐式转换。

但这种情况只有接收一个参数的构造函数才能作为转换函数。对于两个或多个参数的构造函数,如果之后的参数都有默认值,也可以进行转换。

但是这种转换其实并不安全。

C++新增了explicit关键字,用来关闭这种自动特性。

        explicit Stonewt(double lbs);

这样将会关闭隐式转换,但是还是会允许显式进行转换。

11.6.1 转换函数

可以将数字转换为类,那么能不能将类转为数字?

可以,但是不是使用构造函数,而是使用特殊的C++运算符函数——转换函数。

转换函数是用户定义的强制类型转换,可以像使用强制类型转换一样使用。

转换函数形式:

        operator typeName();

其中:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

例如,如果要将类转换为int和double 。那么类声明中应该包含如下的原型:

        operator int();

        operator double();

// 定义如下:
Stonewt::operator int() const
{
    return int (pandas + 0.5);
}

由于二义性的原因,所以如果不能显示的指出需要转换成什么类型,那么就不能进行转换。

如果指出了,那么可以进行隐式转换:

        int w = stonewt;

如果不想进行隐式转换的话,可加上explicit关键字。

11.6.2 转换函数和友元函数

要将double类和自定义类相加,有两种选择。

第一种方法是,将函数定义为友元函数,调用构造函数,将double转换为自定义类。

        operator +(const Stonewt &,const Stonewt &);

第二种方法是,将加法运算符重载为一个显示使用double参数的函数:

        Stonewt operator+(double x);

        friend Stonewt operator+(double x, Stonewt &s);

优缺点分析:

第一种方法(依赖于因式转换)能够让程序更加简短,工作量少,不易出错。但是缺点是每次需要转换时,都需要调用转换构造函数,增加内存和时间开销。

第二种方法(增加显示匹配类型函数)则相反,程序较长,工作量大,容易出错。但是运行速度较快。

 


http://lihuaxi.xjx100.cn/news/18494.html

相关文章

艾美捷山羊抗人IgG-AP化学性质曲线展示

AP&#xff08;Alkaline Phosphatase&#xff0c;碱性磷酸酶&#xff09;是一种含锌的同源二聚体糖蛋白&#xff0c;分子量为56KDa。它是一种能够将对应底物&#xff08;核酸、蛋白、生物碱等&#xff09;去磷酸化的酶&#xff0c;即通过水解磷酸单酯将底物分子上的磷酸基团除去…

常用git命令

当前子分支&#xff1a;子分支合并master git pull origin child_branch:master 恢复到指定版本 git reset --hard 44f994dd8fc1e10c9ed557824cae50d1586d0cb3 删除本地分支 git branch -D branch-name 删除远程分支【ps&#xff1a;如果先删除远程分支&#xff0c;要切换到…

如何在公司内部统一「数据指标口径」?_光点科技

随着公司信息化程度的提高&#xff0c;各系统中的数据也越来越复杂。数据指标口径不统一逐渐成为“通病”。然而&#xff0c;面对不够准确的数据&#xff0c;管理者做出了不同的决定&#xff0c;因此统一数据指标的直径变得非常重要。 首先&#xff0c;数据指标口径不同的情况很…

模板进阶

一、非类型模板参数 模板参数分类&#xff1a;类型形参、非类型形参 类型形参&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称非类型形参&#xff1a;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当…

【BurpSuite】插件开发学习之J2EEScan(下)-主动扫描(51-60)

【BurpSuite】插件开发学习之J2EEScan&#xff08;下&#xff09;-主动扫描(51-60) 前言 插件开发学习第10套。前置文章&#xff1a; 【BurpSuite】插件开发学习之Log4shell 【BurpSuite】插件开发学习之Software Vulnerability Scanner 【BurpSuite】插件开发学习之dotnet-…

回顾React—hooks

React 是实现了组件的前端框架&#xff0c;它支持 class 和 function 两种形式的组件。 class 组件是通过继承模版类&#xff08;Component、PureComponent&#xff09;的方式&#xff0c;继承是 class 本身的特性&#xff0c;它支持设置 state&#xff0c;当 state 改变后重新…

企业运维之 kubernetes(k8s) 的存储

目录 一、Configmap配置管理 二、Secret配置管理 三、Volumes配置管理 四、kubernetes调度 五、kubernetes访问控制 一、Configmap配置管理 Configmap用于保存配置数据&#xff0c;以键值对形式存储&#xff1b; ConfigMap资源提供了向Pod注入配置数据的方法&#xff0c…

​复旦大学邱锡鹏组:CNN-NER——极其简单有效的嵌套命名实体识别方法

©PaperWeekly 原创 作者 | 陆星宇单位 | 复旦大学研究方向 | 自然语言处理本文介绍一下复旦大学 NLP 组的一篇新的 NER 论文。论文标题&#xff1a;An Embarrassingly Easy but Strong Baseline for Nested Named Entity Recognition论文链接&#xff1a;https://arxiv.o…