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


    第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++可以显示或者隐式调用构造函数。

    1. // 显示方法
    2. Stock food = Stock("Woril", 220, 1.25);
    3. // 隐式方法
    4. Stock food("Woril", 220, 1.25);
    5. // 将构造函数和new一起使用的方法
    6. Stock *food = new Stock("Woril", 220, 1.25);

    两种构造方法等价。

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

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

            Stock::Stock(){}

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

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

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

    10.3.4 析构函数

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

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

            ~Stock(){}

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

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

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

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

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

    考虑上面两个语句。

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

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

    const 成员函数

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

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

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

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

    1. void show() const;
    2. void Stock::show() const;

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

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

    10.4 this指针

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

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

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

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

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

    1. const Stock & topval(const Stock & s) const
    2. {
    3. if (s.val > val)
    4. {
    5. return s;
    6. }
    7. else
    8. {
    9. return *this
    10. }
    11. }

    10.5 对象数组

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

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

            Stock mystaff[4];

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

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

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

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

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

    10.6 类作用域

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

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

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

    10.6.1 作用域为类的常量

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

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

    1. class Bakery
    2. {
    3. private
    4. enum {Months = 12};
    5. double costs{Months};
    6. ...
    7. }

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

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

    1. class Bakery
    2. {
    3. private
    4. static const int Months= 12
    5. double costs{Months};
    6. ...
    7. }

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

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

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

    1. enum egg{Small,Medium,Large};
    2. enum t_shirt{Small,Medium,Large};

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

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

    1. enum class egg{Small,Medium,Large};
    2. enum class t_shirt{Small,Medium,Large};

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

    1. egg choice = egg::Large;
    2. 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的关键字:

    1. Time operator* (double m , const Time & t)
    2. {
    3. Time result;
    4. long totalminutes = t.hours * 60 + t.minutes;
    5. result.hours = totalminutes / 60;
    6. result.minutes = totalminutes % 60;
    7. return result;
    8. }

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

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

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

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

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

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

    1. void operator << (ostream & os, const Time & t)
    2. {
    3. os << t.hours << " hours, "<< t.minutes << " minutes";
    4. }
    5. // 之后可以直接使用cout进行输出
    6. cout << A;

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

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

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

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

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

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

    1. ostream & operator << (ostream & os, const Time & t)
    2. {
    3. os << t.hours << " hours, "<< t.minutes << " minutes";
    4. return os;
    5. }

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

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

    11.5 再谈重载:矢量类

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

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

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

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

    1. // 两个操作数的版本
    2. Vector operator - (const Vector & b) const;
    3. // 一个操作数的版本
    4. 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();

    1. // 定义如下:
    2. Stonewt::operator int() const
    3. {
    4. return int (pandas + 0.5);
    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);

    优缺点分析:

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

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

     

  • 相关阅读:
    前端、vue、Vue3弹幕实现;前端CSS实现弹幕
    【保姆级教程】:docker搭建MongoDB三节点副本集
    建筑类企业做ISO9001时需要带GB/T50430标准
    合肥综合性国家科学中心人工智能研究院-机器学习作业(一)
    如何获取及嵌入Go二进制执行包信息
    WIN10/Ubuntu双系统安装流程
    通过Idea或命令将本地项目上传至git
    测试代码1
    文件打包下载excel导出和word导出
    day11_api_Object类_String类
  • 原文地址:https://blog.csdn.net/qq_35423190/article/details/126899588