c++primer 第7章 类

第7章 类

  • 使用类定义自己的数据类型
  • 数据抽象
    • 将对象的具体实现与对象所能执行的操作分离开来
    • 依赖于接口和实现分离的编程技术
      • 接口:用户所能执行的操作
      • 实现:类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
  • 封装
    • 实现类的接口和实现的分离
    • 影藏实现细节,用户只能访问接口无法访问实现
  • 抽象数据类型
    • 类的设计者:考虑类的实现过程
    • 使用类的程序员:抽象思考类型做了什么,无须了解类型工作细节

7.1 定义抽象数据类型

  • Sales_item类 抽象数据类型 通过接口使用对象 不能访问类数据成员
  • Sales_data类 不是抽象数据类型 允许用户直接访问数据成员->封装(隐藏)数据成员 变成抽象数据类型

7.1.1 设计Sales_data类

  • 定义成员函数,先将其定义为普通函数,执行复合赋值运算
  • 用户 不同语境决定了不同的含义
    • 用户代码或类的使用者 程序员
    • 应用程序使用者 如书店经理
    • 设计类接口时,应考虑使类易于使用
    • 使用类时,不应顾及类的实现机理
    • 开发应用程序,须充分了解用户需求
    • 类设计者,应关注使用类的程序员的需求
    • 良好设计的类,直观易于使用的接口,高效的实现过程
  • 使用改进的Sales_data类

7.1.2 定义改进的Sales_data类

  • 定义在类内部的函数隐式内联inline
  • 定义成员函数
    • 所有成员须在类内部声明,成员函数体可定义在类内或类外
  • 引入this
    • 调用成员函数,实际是在替某个对象调用它
    • 成员函数通过this隐式参数,访问调用它的对象
    • 调用成员函数,用请求该函数的对象地址初始化this
    • 成员函数内部,可直接使用该对象成员,无须成员访问运算符,对类成员的直接访问看作this的隐式调用
    • 任何自定义名为this的参数或变量行为都是非法的
    • this总是指向调用该成员函数的对象,是一个常量指针,不允许改变this保存的地址
  • 引入const成员函数
    • const 修改隐式this指针的类型
    • 默认情况下,this指向类类型非常量版本的常量指针,意味着不能将this绑定到常量对象上,故不能在常量对象上调用普通成员函数
    • 无须修改数据成员时,将this设置为指向常量的指针有助于提高函数灵活性
    • 常量成员函数:
      • 将const关键字放在成员函数的参数列表后,表示this是一个指向常量的指针
      • 不能改变调用它的对象的内容,可以读取数据成员,不能写入新值
      • 常量对象/常量对象的引用或指针,都只能调用常量成员函数
  • 类作用域和成员函数
    • 类本身是一个作用域,类成员函数的定义嵌套在类的作用域内
    • 编译器分两步处理类
        1. 先编译成员函数声明
        1. 才轮到成员函数体
      • 故成员函数体可随意使用类成员其他函数无须在意成员出现次序
    • 在类的外部定义成员函数
      • 定义必须与声明匹配
        • 返回类型/形参列表/函数名都需与类内部声明一致
      • 被声明为常量成员函数,定义也须在参数列表后指定const属性
      • 类外部定义的成员名字必须包含它所属的类名
      • 使用作用域运算符::说明该函数被声明在类内部,剩余代码是位于内的作用域内的
    • 定义一个返回this对象的指针
      • 当定义函数类似于某个内置类型运算符时,应令该函数行为尽量模仿该运算符
      • 返回对象引用类型,需将调用函数对象当成一个整体访问
        • return *this; 解引用this指针以获得执行该函数的对象

7.1.3 定义类相关的非成员函数

  • 定义辅助函数,概念上属于类接口组成部分,实际上不属于类本身
  • 非成员函数。通常将函数声明和定义分离开来
  • 如果非成员函数是类的接口组成部分,函数声明应与类在同一个头文件内
  • 定义read和print函数
    • io类属于不能被拷贝的类型,只能通过引用来传递他们
    • 读取和写入操作会改变流的内容,故两函数都接受普通引用
  • 定义add函数
    • 默认情况下,拷贝类的对象拷贝的是对象的数据成员

7.1.4 构造函数

  • 构造函数:定义类对象被初始化的方式,控制对象初始化过程,初始化类对象数据成员
  • 只要类的对象被创建,就会执行构造函数
  • 构造函数名字与类名相同,没有返回类型,也有一个参数列表和函数体
  • 类可包含多个构造函数,与其重载函数差不多,形参数量或类型须有区别
  • 构造函数不能被声明为const。当创建类const对象时,直到构造函数完成初始化过程,对象才真正取得"常量"属性。构造函数在const对象构造过程中可以向其写值。
  • 合成的默认构造函数
    • 如果类没有显式定义构造函数,编译器会隐式定义一个默认构造函数
    • 合成的默认构造函数初始化规则
      • 若存在类内初始值,用其来初始化成员
      • 否则默认初始化该成员
        • 默认初始化:定义时没指定初值变量被默认初始化;内置类型定义于函数体外初始化为0,定义于函数体内则不被初始化。(建议初始化每一个内置类型变量)
  • 某些类不能依赖于合成的默认构造函数
    • 普通类必须定义自己的默认构造函数,原因有三
        1. 编译器只会在我们没有声明任何构造函数时会隐式定义一个默认构造函数,一旦定义了其他构造函数,除非再定义一个默认构造函数,否则类没有默认构造函数
        1. 合成的默认构造函数可能执行错误的操作。定义在块中的内置类型(算术类型:字符 整型 浮点型 布尔型;空类型)/复合类型(数组和指针)的对象被默认初始化则他们的值是未定义的,同样适用于默认初始化内置类型成员。(只有当类 内置/复合类型的成员全部都被赋予类内初始值时,该类才适用于合成的默认构造函数)
        1. 有时编译器不能为某些类合成默认构造函数。(如:类中包含一个其他类类型成员且该成员类型没有默认构造函数,编译器将无法初始化该成员。)必须自定义默认构造函数,否则该类将没有可用的默认构造函数。
  • 定义Sales_data的构造函数
  • =default的含义
    • ales_data()=default;一个不接受任何实参的默认构造函数
    • 定义目的:既需要其他形式的构造函数,也需要默认构造函数,希望该默认构造函数作用完全等同于合成默认构造函数
    • =default既可以和声明一起出现在类的内部(内联),也可以作为定义出现在类的外部
    • 若编译器不支持类内初始值,默认构造函数应该使用构造函数初始值列表来初始化类的每个成员
  • 构造函数初始值列表
    • 负责为新创建的对象的一个或几个数据成员赋值,如
Sales_data(const std::string &s):bookNo(s) {
    
    }
Sales_data(const std::string &s,unsigned n,double p):
           bookNo(s),uits_sold(n),revenue(p*n) {
    
    }
  • 当某个数据成员被构造函数初始值列表忽略时,它将以合成默认构造函数相同的方式隐式初始化(如使用类内初始值)
  • 若不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员
  • 在类的外部定义构造函数
    • 构造函数无返回类型,定义从指定函数名开始,类外部定义构造函数,须指明构造函数是哪个类的成员

7.1.5 拷贝、赋值和析构

  • 拷贝:初始化变量以及以值的方式传递或返回一个对象
  • 赋值:使用赋值运算符
  • 销毁:对象不存在时执行销毁操作,局部对象会在创建它的块结束时被销毁,vector对象被销毁时存储在其中的对象也会被销毁
  • 若不主动定义拷贝、赋值和析构操作,编译器会替我们合成它们
  • 某些类不能依赖于合成的版本
    • 当类需要分配类对象之外的资源时(管理动态内存的类)
    • 使用vector或string类能避免分配和释放内存带来的复杂性,拷贝、赋值和析构的合成版本能正常工作。(对含vector成员的对象执行拷贝、赋值和析构时,会设法拷贝或赋值成员中的元素;对象被销毁时,将销毁vector对象,依次销毁vector中的每一个元素)
    • 类中所分配的资源都应该直接以类的数据成员的形式存储

7.2 访问控制与封装

  • 访问说明符加强类的封装性
    • public说明符后的成员在整个程序内都可以被访问,public成员定义类的接口
    • private说明符之后的成员可以被类的成员函数访问,但不能被使用该类的代码访问,private部分封装(隐藏)类的实现过程
  • 再次定义Sales_data类
    • public接口部分:构造函数和部分成员函数
    • private实现部分:数据成员
  • 一个类可定义0/多个访问说明符,每个说明符出现次数也没有严格限定;每个说明符指定接下来的成员的访问级别,有效范围直到下一个访问说明符或者到达类的结尾为止
  • 使用class或struct关键字
    • 唯一区别,class和struct的默认访问权限不太一样
    • struct:定义在第一个说明符之前的成员是public的
    • class:定义在第一个说明符之前的成员是private的
    • 出于统一编程风格:当定义的类的所有成员是public时,使用struct;当定义的类的所有成员是private时,使用class

7.2.1 友元

  • 类允许友元(其他类或者函数)访问它的非公有成员,只需增加一条friend关键字开始的函数声明
  • 友元声明只能出现在类定义的内部,但出现位置不限;友元不是类的成员也不受所在区域访问控制级别约束
  • 但一般最好在类定义开始或结束前的位置集中声明友元
  • 封装的益处
    • 确保用户代码不会无意间破环对象状态
    • 数据成员定义为private的类作者可以较为自由地修改数据;只要类的接口不变,用户代码就无须改变
    • 将查错限制在有限范围内,极大降低维护代码以及修成程序错误地难度
    • 当类的定义发生改变无须更改用户代码,但使用该类的源文件须重新编译
  • 友元地声明
    • 仅指定访问权限,而非通常意义的函数声明;类成员调用友元函数前须在友元声明外再次声明
    • 为使友元对类成员可见,通常将友元声明与类本身放置在同一个头文件中
    • 编译器未强制限定友元函数须在使用之前在类的外部声明;一些编译器允许在尚无友元函数的初始声明的情况下就调用它

7.3 类的其他特性

  • 类型成员、类的成员类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、如何定义类类型以及友元类

7.3.1 类成员再探

  • 定义一个类型成员
    • 自定义某种类型在类中的别名,与其他成员一样存在访问限制(public/private)
    • 用来定义类型的成员必须先定义后使用,因此类型成员通常出现在类开始的地方
  • Screen类的成员函数
  • 令成员作为内联函数
    • 定义在类内部的成员函数自动inline
    • 可以在类内部作为声明的一部分显示声明成员函数,也能在类的外部用inline关键字修饰函数的定义
    • 虽无须在声明和定义同时说明inline但合法,最好只在类外部定义的地方说明inline使类更容易理解
    • 和在头文件中定义inline函数原因一样(编译器想展开函数不仅需要函数声明还需要函数定义,且多个定义须完全一致),inline成员函数也应该与相应的类定义在同一个头文件中
  • 重载成员函数
    • 参数数量/类型上有所区别
  • 可变数据成员
    • 即使在const成员函数内也能修改某个数据成员,通过在变量声明前加mutable关键字
    • 可变数据成员永远也不会是const,即使它是const对象的成员(可用于记录const成员函数被调用的次数)
  • 类数据成员的初始值
    • 类内初始值必须用=的初始化形式
    • 或花括号括起来的直接初始化形式

7.3.2 返回*this的成员函数

  • 定义返回为引用时返回对象本身而非对象副本,返回类型为非引用时返回*this的副本
  • 从const成员函数返回*this
    • 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
  • 基于const的重载
    • 通过成员函数是否是const的可对其进行重载,原因与根据指针参数是否指向const而重载差不多(非常量版本函数对常量对象不可用,只能在常量对象上调用const成员函数;非常量对象可调用常量/非常量版本,但非常量版本是更好的匹配;根据对象是否是const决定了应该调用的函数版本)
    • 当一个成员调用另一个成员是,this指针在其中隐式传递
  • 对于公共代码使用私有功能函数
    • 避免在多处使用同样的代码
    • 类规模发展,函数变得更复杂,将相同操作写在一处较好
    • 可能在函数中添加某些调试信息,在一处添/删更容易
    • 额外调用不增加开销,因为在类内部定义,隐式地被声明为内联函数

7.3.3 类类型

  • 每个类定义唯一类型;两个类即使成员(列表)完全一样,两个类也是不同的类型
  • 一个类的成员和其他任何类(或任何其他作用域)地成员都不是一回事
  • 可直接使用类名作为类型名,也可把类名跟在关键字class或struct后面
  • 类的声明
    • 前向声明:仅仅声明类而暂时不定义它
    • 不完全类型:声明之后定义之前(只能在非常有限的的情境下使用)
      • 可以定义指向这种类型的指针或引用
      • 也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数
    • 创建类对象时该类必须被定义过,不能仅被声明(编译器无法了解对象所需储存空间)
    • 类须先被定义才能用引用或指针访问其成员(否则编译器不清楚该类有哪些成员)
    • 直到类被定义后数据成员才能被声明成这种类型,一个类成员的类型不能是类自己;但类允许包含指向它自身类型的引用或指针

7.3.4 友元再探

  • 可以将非成员函数/其他类/其他类已定义过的成员函数定义成友元
  • 友元函数能定义在类的内部 隐式内联
  • 类之间的友元关系friend class Window_mgr;
    • 友元类的成员函数可以访问此类包括非共有成员在内的所有成员
    • 友元关系不存在传递性,(即类A存在友元类B,类B存在友元类C,则类C不具有访问类A特权)
    • 每个类负责控制自己的友元类或友元函数
  • 令成员函数作为友元
    • 当将成员函数声明成友元时,须明确指出该成员函数属于哪个类
    • 须仔细组织程序的结构以满足声明和定义的彼此依赖关系,设计程序的方式如下
      • 先定义类A,声明a函数,但不能定义它;在a使用类B的成员之前必须先声明类B
      • 接下来定义类B,包括对于a的友元声明
      • 最后定义函数a,此时a才可以使用类B的成员
  • 函数重载和友元
    • 如想将一组重载函数声明成友元,需对这组函数中的每一个分别声明
  • 友元声明和作用域
    • 友元声明的作用是影响访问权限,本身并非普通意义上的声明
    • 就算在类的内部定义友元函数,也须在类的外部提供相应声明使函数可见
    • 类的成员使用友元函数时,友元函数也须先被声明过
    • 有的编译器不强制执行上述关于友元的限定规则

7.4 类的作用域

  • 一个类就是一个作用域
  • 类的作用域之外,普通数据和函数成员只能由对象、引用或者指针使用成员访问运算符访问
  • 类类型成员则使用作用域访问符访问
  • 跟在类作用域运算符之后的名字都须是对应类的成员
  • 作用域和定义在类外部的成员
    • 一个类就是一个作用域,在类的外部定义成员函数时必须同时提供类名和函数名(类的外部,成员名字被隐藏)
    • 一旦遇到类名,定义剩余部分就在类的作用域之内了,包括参数列表和函数体(内可直接使用类的其他成员而无须再次授权)
    • 函数返回类型在函数名之前,当函数定义在类外部时,返回类型名字位于作用域之外,因此返回类型须指明它是哪个类的成员

7.4.1 名字查找与类的作用域

  • 名字查找
    • 在名字所在块中寻找声明语句,只考虑在名字试用前的声明
    • 若没找到,继续查找外层作用域
    • 若最终没找到匹配的声明,程序报错
  • 定义在类内部的成员
    • 编译器先编译成员的声明
    • 直到类全部可见后才编译函数体(处理完全部声明后才会处理成员函数定义)
    • 所以定义在类内部的成员函数体能使用类定义中的任何名字
  • 用于类成员声明的名字查找
    • 两阶段处理只适用于定义在类内的成员函数体中使用的名字
    • 声明中使用的返回类型/参数列表的名字,都须在使用前确保可见
    • 若函数声明使用了类中尚未出现的名字,则编译器将在定义该类的作用域中继续查找
  • 类型名要特殊处理
    • 内层作用域可以重新定义外层作用域中的名字
    • 但若成员使用了外层作用域中的类型名字,则类中不能在之后重新定义该名字
      • 重新定义类型名字是一种错误的行为
      • 类型名定义放在类开始处,能确保所有使用类型的成员都在类名定义后
  • 成员定义中的普通块作用域的名字查找
    • 先在成员函数内查找名字声明,在使用前的才有效
    • 成员函数内没有,则在类内继续查找,类的所有成员都可以被考虑
    • 类内也没有该名字的声明则在成员函数定义前的作用域内继续查找
    • (不建议使用其他成员的名字作为某个成员函数的参数,成员函数参数会隐藏外层同名参数;但仍可通过加上类的名字或显式使用this指针来强制访问成员)
  • 类作用域之后,在外围的作用域中查找
    • 编译器在函数和类的作用域中都没找到名字,将接着在外围作用域中查找
    • 可显式通过作用域运算符来进行请求外层作用域中的名字(::前什么都没有表示全局作用域)
  • 在文件中名字的出现处对其进行解析
    • 当成员定义在类的外部时,不仅须考虑定义前的全局作用域中的声明,还需考虑成员函数定义前的全局作用域中的声明

7.5 构造函数再探

7.5.1 构造函数初始值列表

  • 定义变量时习惯立即对其进行初始化而非先定义再赋值
  • 若没有在构造函数初始值列表中显式初始化成员,则成员在构造函数体之前执行默认初始化
  • 使用构造函数初始值列表与在构造函数体内对数据成员执行赋值的效果相同。但深层次的影响完全依赖于数据成员的类型
  • 构造函数的初始值有时必不可少
    • 有时可忽略数据成员初始化与赋值之间的差异
    • 但若成员是const或引用则须将其初始化
    • 当成员属于某种没有定义默认构造函数的类的类型时也须初始化
    • (随着函数体一开始执行,初始化就完成了,初始化const或引用类型数据成员的唯一机会就是构造函数初始值)
    • 很多类中 初始化(直接初始化数据成员)和赋值(先初始化再赋值)事关底层效率
    • 一些数据成员必须被初始化,养成使用构造函数初始值的习惯,能避免一些意想不到的编译错误
  • 成员初始化的顺序
    • 构造函数初始值中每个成员只能出现一次
    • 成员初始化顺序与在类定义的出现顺序一致,与初始值列表中的前后位置无关
    • 最好令构造函数初始值的顺序与成员声明的顺序保持一致,尽量避免使用某些成员初始化其他成员,如可能最好使用构造函数参数作为成员初始值(可不必考虑成员初始化顺序)
  • 默认实参和构造函数
    • 如果一个构造函数为所有参数都提供了默认实参,则实际上也定义了默认构造函数(因为可以不提供实参也能调用上述函数)

7.5.2 委托构造函数

  • 委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程(将自己的一些/全部职责委托给其他构造函数)
    • 也有一个成员初始值列表和函数体,成员初始值列表唯一入口是类名本身,参数列表须与类中另外一个构造函数匹配
    • 当一个构造函数委托另一个构造函数时,先执行受委托函数的初始值列表和函数体执行完后控制权才会交还给委托者的函数体

7.5.3 默认构造函数的作用

  • 当对象在默认初始化或者值初始化时自动执行默认构造函数
    • 默认初始化
      • 在块作用域内不使用任何初始值定义一个非静态变量/数组
      • 当一个类本身含有类类型成员且使用合成的默认构造函数时
      • 当类类型成员没有在构造函数初始值列表中显式初始化时
    • 值初始化
      • 数组初始化时提供初始值数量少于数组大小时
      • 当不使用初始值定义一个局部静态变量时
      • 通过书写形如T()(其中T为类型名称)的表达式显式请求值初始化时
    • 如果定义了其他构造函数,那么最好也提供一个默认构造函数
  • 使用默认构造函数
    • 如想定义一个使用默认构造函数进行值初始化的对象,正确方法是去掉对象名后的空括号(有空括号会当成一个不接受任何参数,返回值为类类型的函数)

7.5.4 隐式的类类型转换

  • 转换构造函数
    • 如果构造函数只接受一个实参,则实际上定义了转换为此类类型的隐式转换机制
    • 通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
    • 编译器用给定的参数自动创建一个(临时)类对象(可以给常量引用的参数传递临时量)
  • 只允许一步类类型转换
    • 编译器只会自动地执行一步类型转换,当隐式使用两种或以上转换规则则是错误的
  • 类类型转换不是总有效
    • 实际上构造了一个临时对象,一旦操作完,随后就将其丢弃不能够再访问了
  • 抑制构造函数定义的隐式转换
    • 在要求隐式转换的程序的上下文,可通过将构造函数声明为explicit加以阻止隐式转换
    • 关键字explicit只对一个实参的构造函数有效;
    • 需多个实参的构造函数不能用于执行隐式转换,所以也无须指定为explicit
    • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复
  • explicit构造函数只能用于直接初始化
    • 不能将explicit构造函数用于拷贝形式的初始化过程
    • explicit关键字声明构造函数时,只能以直接初始化的形式使用,编译器将不会在自动转换过程中使用该构造函数
  • 为转换显式地使用构造函数
    • 编译器不会为explicit构造函数用于隐式转换过程,但可以使用该构造函数显式地强制进行类型转换(也可利用static_cast使用explicit构造函数),创建的都是临时类对象
  • 标准库中含有显式构造函数(单参数)的类
    • 接受一个单参数的const char*的string构造函数(不是explicit的)
    • 接受一个容器参数的vector构造函数(是explicit的)

7.5.5 聚合类

  • 聚合类 使用户可以直接访问其成员,具有特殊初始化语法形式
    • 所有成员都是public的
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数
  • 可以提供一个花括号括起来的成员初始值列表,用它初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致
    • 与初始化数组一样,如果初始值列表中的元素个数少于类的成员数量,靠后的成员被值初始化;初始化列表的元素个数绝对不能超过类的成员数量
  • 显式初始化类对象成员的三个明显缺点
    • 要求类的所有成员都是public的
    • 将正确初始化类每个对象的重任交给了类的用户,用户易忘掉某个初始值或提供一个不恰当的初始值,初始化过程冗长乏味且容易出错
    • 添加或删除一个成员后,所有初始化语句都需更新

7.5.6 字面值常量类

  • constexpr函数的参数和返回值必须是字面值类型( 字面值类型:算术类型 引用 指针; 不属于字面值类型: 某些自定义类 IO库 string)
  • 字面值类型的类可能含有constexpr函数成员,该成员须符合constexpr函数的所有要求,是隐式const的
  • 数据成员都是字面值类型的聚合类是字面值常量类,若某类不是聚合类但符合以下要求,则也是字面值常量类
    • 数据成员都须是字面值类型
    • 类须至少含有一个constexpr构造函数
    • 若数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;若成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
    • 类须使用析构函数的默认定义(负责销毁类的对象)
  • constexpr构造函数
    • 构造函数不能是const的,但字面值常量类的构造函数可以是constexpr函数
    • constexpr构造函数可声明成=default的形式或者删除函数的形式;否则构造函数就须既符合构造函数要求(不能包含返回语句),又符合constexpr函数的要求(唯一可执行语句就是返回语句)->constexpr构造函数体一般是空的
    • 前置关键字constexpr就可以声明一个constexpr构造函数
    • constexpr构造函数须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式
    • constexpr构造函数用于生成constexpr对象以及constexpr函数函数的参数或返回类型

7.6 类的静态成员

  • 有时类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联
  • 声明静态成员
    • 在类成员声明前加static使其与类关联在一起
    • 静态成员可以是public或private的;静态数据成员的类型可以是常量/引用/指针/类类型
    • 类静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。被所有类对象共享
    • 类静态成员函数也不与任何对象绑定在一起,不包含this指针;静态成员函数不能声明成const的,也不能在static函数体内使用this指针(这限制适用于this的显式使用,也对调用非静态成员的隐式使用有效)
  • 使用类的静态成员
    • 使用作用域运算符直接访问静态成员
    • 静态成员虽不属于类的某个对象,但仍可使用类对象、引用或指针来访问静态成员
    • 成员函数不用通过作用域运算符就能直接使用静态成员
  • 定义静态成员
    • 与其他成员函数一样,既可以在类的内部也可以在类的外部定义静态成员函数
    • 在类外部定义静态成员时,不能重复static关键字,static值出现在类内部声明语句
    • 指向类外部静态成员时,须指明成员所属类名
    • 静态成员不属于类的任何对象,它们不是在创建类的对象时被定义的,它们不由类的构造函数初始化;
    • 一般不能在类的内部初始化静态成员,须在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次
    • 类似全局变量,类静态数据成员定义在任何函数之外;一旦被定义,就将一直存在于程序的整个生命周期中
    • 在类外部定义静态数据成员,需指定对象类型名,类名、作用域运算符、成员自己的名字;从类名开始,定义语句的剩余部分都位于类的作用域之内,(可以访问类的私有成员)
    • 为确保对象只定义一次,最好把静态成员的定义与其他非内联函数的定义放在同一个文件中
  • 静态成员的类内初始化
    • 通常类静态成员不应在类内部初始化,但可为静态成员提供const整数型的类内初始值,不过要求静态成员须是字面值常量类型的constexpr。初始值须是常量表达式,因成员本身就是常量表达式,所以可用于所有适合常量表达式的地方。(初始化了的静态数据成员可用于指定数组维度)
    • 若静态成员的应用场景仅限于编译器可以替换它的值,则一个初始化的const或constexpr static不需要分别定义;若将它用于值不能替换的场景中,则该成员须有一条定义语句
    • 若在类的内部提供了一个初始值,则成员定义不能再指定一个初始值
    • 即使一个常量静态数据成员在类内部被初始化了,通常也应在内的外部定义一下该成员
  • 静态成员能用于某些场景,而普通成员不能
    • 静态成员独立于任何对象,某些非静态数据成员可能非法的场合,静态成员可以正常使用
    • 静态数据成员可以是不完全类型(声明之后定义之前)
    • 静态数据成员的类型可以是它所属类类型;非静态数据成员则受到限制,只能声明成它所属类的指针或引用
    • 可以使用静态数据成员作为默认实参,非静态成员不能作为默认实参(值本身属于对象一部分,无法真正提供一个对象以便从中获取成员的值)

小结

  • 类 定义新类型 使程序更简洁且易于修改
  • 类两项基本能力
      1. 数据抽象 定义数据成员和成员函数的能力
      1. 封装 保护类的成员不被随意访问的能力(private)
      • 类可将其他类或函数设为友元,使它们能访问类非共有成员
  • 类可定义特殊成员:构造函数
    • 控制初始化对象的方式
    • 可以重载
    • 应使用构造函数初始值列表来初始化所有数据成员
  • 类可定义可变或静态成员
    • 一个可变成员(mutable)永远不会是const,在const函数内也能修改它的值
    • 静态成员可以是函数或数据,存在于所有对象之外

术语表

  • 转换构造函数:可用一个实参调用的非显式构造函数(隐式将参数类型转换为类类型)
  • 数据抽象:关注类型接口的编程技术;忽略类型实现细节只关注类型执行操作;数据抽象是面向对象编程和泛型编程基础
  • 显式构造函数:可以用一个单独的实参调用但是不能用于隐式转换的构造函数(构造函数声明前加explicit)
  • 前向声明:尚未定义名字前的声明
  • 实现:类的成员(通常是私有的)定义了不希望使用类类型的代码所使用的数据及任何操作
  • 不完全类型:已经声明但是尚未定义的类型;不能用于定义变量或类的成员,但可用于定义指针或引用
  • 接口:类提供的(公有)操作

猜你喜欢

转载自blog.csdn.net/m0_68312479/article/details/128681129
今日推荐