C++ Primer 0x07 学习笔记
Posted 鱼竿钓鱼干
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer 0x07 学习笔记相关的知识,希望对你有一定的参考价值。
📔 C++ Primer 0x07 学习笔记
推荐阅读 《C++ Primer 5th》知识点总结&练习题解
7.1 定义抽象数据类型
- 类的基本思想是数据抽象和封装
- 数据抽象是一种依赖于接口和实现分离的编程技术
- 类的接口包括用户所能执行的操作
- 类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
- 封装实现了类的接口和实现的分离,隐藏了实现细节,类的用户只能使用接口而无法访问实现部分
- 类要实现数据抽象和封装,首先定义一个抽象数据类型,由类的设计者负责考虑类的实现过程,使用类的程序员只要抽象地思考类型做了什么,而无需了解类型的工作细节
7.1.1 定义抽象数据类型
-
运行程序的人是用户,类的设计者也是为用户设计并实现一个类的人,类的用户是程序员
-
C++ 程序员无须刻意区分程序的用户和类的用户
7.1.2 定义改进的 Salses_data 类
- 定义在类内部的函数是隐式的
inline
函数 - 成员函数通过一个名为
this
的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数是,用请求该函数的对象地址来初始化this
this
是一个常量指针,我们不允许改变this
中保存的地址- 默认情况下
this
的类型是指向类类型非常量版本的常量指针,这意味着我们不能把this
绑定到一个常量对象上。这也使得我们不能在一个常量对象上调用普通的成员函数 - 我们可以在参数列表后加
const
,将this
指针声明为指向常量的指针,这有助于提高函数的灵活性 - 常量对象,以及常量对象的引用或指针都只能调用常量成员函数 (使用
const
的成员函数) - 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体,因此成员函数体可以随意使用类中其他成员无须在意出现次序
- 当我们在类外部定义成员函数时,成员函数的定义必须与它的声明匹配,如果成员被声明成常量成员函数,那么定义也要在参数列表后明确指定
const
属性 return *this
返回调用该函数的对象
7.1.3 定义类相关的非成员函数
- 如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件里
- IO类属于不能被拷贝类型,所以如果编写读写相关函数并使用IO类要采用引用作为参数。并且因为读写会改变流的内容,所以采用普通引用
- 执行输出内容的函数应该尽量减少对格式的控制
7.1.4 构造函数
- 构造函数的名字和类名相同,没有返回类型
- 构造函数不能被声明成
const
,当我们创建类的一个const
对象直到完成对象的初始化过程才真正取得其常量属性,因此构造函数在const
对象的构造过程中可以向其写值 - 类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫默认构造函数,默认构造函数无须任何实参
- 编译器创建的构造函数又称合成的默认构造函数:如果存在类内的初始值,则用初始值来初始化成员;莫则,默认初始化该成员
- 某些类不能依赖于合成的默认构造函数
- 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
- 含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,否则可能得到未定义的值
- 如果包含其他类类型的成员且这个类型没有默认构造函数,那么编译将无法初始化该成员函数,我们必须自定义默认构造函数给它用
= default
要求编译器生成构造函数- 使用构造函数初始值列表来初始化
- 构造函数不应该轻易覆盖类内的初始值,除非新赋的值和原值不同。如果不能使用类内初始值,则所有构造函数都应该显示地初始化每个内置类型的成员
7.1.5 拷贝、赋值和析构
- 某些类不能依赖于合成的版本,当类需要分配类对象之外的资源时,合成的版本常常会失效
- 很多动态内存的类能(而且应该)使用
vector
对象或者string
对象管理必要的存储空间,这能避免分配和释放内存带来的复杂性 - 当含有
vector
成员对象执行拷贝或赋值操作时,vector
会设法拷贝或赋值成员中的元素。销毁时也会一次销毁vector
中的每一个元素。这点与string
是非常类似的
7.2 访问控制与封装
public
说明符之后的成员在整个程序内可以被访问,public
成员定义类的接口private
说明符之后的成员可以被类的成员函数访问,但是不能使用该类的代码访问,private
封装了类的实现细节- 类可以包含多个
public
和private
说明符,顺序无所谓 - 使用
class
和struct
定义类的唯一区别就是默认的访问权限,struct
默认所有成员是public
,class
默认所有成员是private
7.2.1 友元
- 友元声明让类允许其他类或函数访问它的非公有成员
- 友元的声明仅仅指定了访问权限,而非通常意义上的函数声明。我们希望类的用户能够调用某个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明,通常我们把友元的声明和类本身放在同一个头文件当中
- 一般来说,最好在类定义开始或结束前的位置集中声明友元
- 封装的两个重要优点
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现可以随时改变,无需调整用户级别的代码
7.3 类的其他特性
7.3.1 类成员再探
- 除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名
- 用来定义类型的成员(类自定义的某种类型在类中的别名)必须先定义后使用,这和普通成员有所不同,因此类型成员通常出现在类开始的地方
- 通过
mutable
关键字,使得const
成员函数也能修改类的某个数据成员 - inline 成员函数应该与相应的类定义在同一个头文件中
- 成员函数也可以重载,只要函数之间在参数的数量和或类型上有所区别就行
- 当我们提供一个类内初始值的是,必须以符号=或者花括号表示
7.3.2 返回 *this 的成员函数
return *this
返回什么看返回类型,如果返回类型是引用那就返回引用,否则返回副本- 一个
const
成员函数如果以引用的形式返回*this
,那么它返回的类型将是一个常量引用 - 通过区分成员函数是否是
const
我们可以对其重载,其原因与根据指针参数是否指向const
重载函数差不多 - 非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量函数上调用
const
成员函数,另一方面非常量对象上可以调用常量或非常量版本,但显然此时非常量版本是一个更好的匹配
7.3.3 类类型
- 即使两个类的成员列表完全一致,他们也是不同的类型
- 类的声明被称作前向声明,是一个不完全类型,不知道到底包含哪些成员
- 不完全类型只能在非常有些情境下使用(令某个成员函数为友元函数的时候有用):
- 可以定义指向这种类型的指针或引用
- 可以声明(但不能定义)以不完全类型作为从那树或返回类型的函数
- 对于一个类来说,我们创建对象之前,这个类必须被定义过,而不能仅仅被声明
7.3.4 友元再探
- 友元关系不具备传递性
- 每个类负责控制自己的友元类或者友元函数
- 要令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义彼此依赖关系
- 如果把一组重载函数声明为友元,那么需要对每个函数分别声明(看练习7.32!!!)
7.4 类的作用域
- 当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时返回类型必须指明他是哪个类的成员
7.4.1 名字查找与类的作用域
-
名字查找(一般情况)
- 首先在名字所在块内找声明语句,只考虑名字的使用前出现的声明
- 如果没找到,继续查找外层作用域
- 如果最终没有找到匹配的声明,则程序报错
-
类的定义分为两步(按这种两阶段方式处理类可以简化代码组织方式)
- 编译成员的声明
- 知道类全部可见之后才编译函数体
-
在类中,如果成员函数使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字
-
成员定义中普通块作用域的名字查找
- 首先在成员函数内查找改名字的声明,只考虑函数使用之前出现的声明
- 如果成员函数内没找到,则在类内继续查找,所有成员都考虑
- 如果类内也没找到,则在成员函数定义之前的作用域内继续查找
- 如果还是没找到,那么接着在外围作用域中查找
-
一般不建议使用其他成员的名字作为某个成员函数的参数
-
尽管类的成员被隐藏了,但我们仍然可以通过类的名字或显示使用
this
指针强制访问成员 -
当成员定义在类外部是,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还要考虑成员函数定义之前全局定义之前的全局作用域中的声明
7.5 构造函数再探
7.5.1 构造函数初始值列表
- 如果成员是
const
、引用或者属于某种为提供默认构造函数的类类型,我们必须通过构造函数初始值列表位置写成员提供初值 - 建议使用构造函数初始值
- 构造函数列表只说明用于初始化成员的值,而不限定初始化的具体顺序
- 成员的初始化顺序与它们在类定义中的出现顺序一致
- 最好令构造函数初始值的顺序与成员声明的顺序保持一指,而且如果可能的化,尽量避免使用某些成员初始化其他成员
- 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数
7.5.2 委托构造函数
- 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些职责委托给了其他构造函数
- 当一个构造函数委托给一个另一个构造函数,受委托的构造函数初始值列表和函数体依次被执行。假如函数体包含代码,先执行这些代码,然后控制权才会交还给委托者的函数体
7.5.3 默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数
默认初始化在以下情况下发生
- 在块作用域内不使用任何初始值递归一一个非静态变量或数组
- 当一个类本身含有类类型的成员且使用合成的默认构造函数是
- 当类类型的成员在构造函数初始值列表中显示地初始化时
值初始化在以下情况下发生
- 数组初始化过程中提供初始值数量少于数组大小
- 不使用初始值定义一个局部静态变量
- 通过书写
T()
的表达式显示地请求值初始化,比如用了vector<T> vec(10)
,这个T
要有默认构造函数
类必须包含一个默认构造函数以便在上述情况下使用
实际上,如果定义了其他构造函数,那么最好也提供一个默认构造函数
7.5.4 隐式的类类型转换
- 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
- 如果是隐式转换,那么只允许一步类类型转换
explicit
可以抑制构造函数定义的隐式转换explicit
构造函数只能用于直接初始化,而且编译器不会在自动转换过程中使用该函数,但可以通过显示使用构造函数或强制类型转换使用- 能用
explictit
就用 - 接受一个但参数的
const char*
的string构造函数不是explicit
- 接受一个容量参数的
vector
构造函数是explicit
的
7.5.5 聚合类
- 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式
- 当一个类满足下面条件时,我们说它是聚合的(和结构体差不多)
- 所有成员都是
public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有
virtual
函数
- 所有成员都是
- 可以用花括号的初始值列表来初始化聚合类列表,初始值顺序必须和声明顺序一致
- 显示地初始化类的对象成员有三个缺点
- 要求类的所有成员都是
public
- 将正确初始化每个对象的每个衬衣重任交给用户而不是类作者,这样初始化过程冗长而且容易出错
- 添加或删除一个成员之后,所有初始化语句都需要更新
- 要求类的所有成员都是
7.5.6 字面值常量类
- 数据成员都是字面值类型的聚合类是字面值常量类
- 如果一个类不是聚合类但它符合下述要求,则它也是一个字面值常量类
- 数据成员必须都是字面值类型
- 类必须至少包含一个
constexpr
构造函数 - 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象
- 尽管构造函数不能是
const
的,但是字面值常量类的构造函数可以是constexpr
,实际上是必须至少提供一个constexpr
构造函数
7.6 类的静态成员
- 通过加上关键字
static
声明类的静态成员 - 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
- 静态成员函数
- 不与任何对象绑定在一起,它们不含
this
指针 - 作为结果,静态成员函数不能声明成
const
的
- 不与任何对象绑定在一起,它们不含
- 看清是静态数据成员还是静态成员函数
- 当在类的外部定义静态成员函数时,不能重复
static
关键字,该关键字只出现在类内部的声明语句 - **静态数据成员****不属于类的任何一个对象,所以并不是在创建类的对象是被定义的。这意味着他们不是由类的构造函数初始化的,一般情况在类的外部定义和初始化每个静态成员(最好这么做)
- 一个静态数据成员只能定义一次
- 建议把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
- 允许为静态成员提供
const
整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的cosntexpr
,初始值必须是常量表达式,因为这些成员本身就是常量表达式 - 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员
- 静态成员可以用于一些普通成员没法做的场景
- 静态数据成员可以是不完全类型
- 静态数据成员的类型可以是它所属的类类型,非静态数据成员只能声明成它所属类的指针或引用
- 可以使用静态成员作为默认实参,非静态数据成员不可以
以上是关于C++ Primer 0x07 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章