- 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation)。
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
- 封装实现了类的接口和实现的分离
一、定义抽象数据类型
|
|
1、类成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
- 使用点运算符
.
调用成员函数
1.1 类的成员函数
- 成员函数的声明必须在类的内部。
- 成员函数的定义既可以在类的内部也可以在外部。
|
|
必须对任何const或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
- 默认实参:
Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }
1.2 非成员函数
和类相关的非成员函数,定义和声明都应该在类的外部。
1.3 常成员
1.3.1 常成员函数
若将成员函数设置为常成员函数,则只能引用本类中的数据成员,而不能修改它;常成员函数可以引用const数据成员,也可以引用非const的数据成员;常成员函数的一般形式为:
|
|
这里的const关键字是函数类型的一部分,在函数声明和函数定义时都要带const关键字,否则被编译器认为是不同的两个函数,但是在函数调用的时候不必带const;
|
|
这种函数称为“常量成员函数”(this
指向的当前对象是常量)。
这样做的好处是
- 既可以将this绑定到常量对象上,也可以绑定到普通对象上([[变量和基本类型#3、指针和const|指针与const的特殊性]]),提高了函数的灵活性
- 程序是安全的
⚠️C++ 中常对象只能调用类中的常方法,但是常成员方法可以使用非常量数据成员 这是正确的。常对象是不能修改对象状态的,因此只能调用类中标记为常的方法,即常方法。如果试图调用非常方法,编译器会生成错误。因此,常对象可以保证对象状态不会被意外修改。常成员函数可以引用const数据成员,也可以引用非const的数据成员
|
|
常方法返回的任何值都具有常性 常方法返回的任何值都具有常性,不论是指针还是引用。这意味着,不能通过常方法返回的指针或引用来修改对象的值。然而,非常量方法则允许通过其返回的指针或引用来修改对象的值。
1.3.2 常数据成员
如果我们希望在创建对象后,该对象的某个数据成员就保持不变,我们可以将这个数据成员设置为常数据成员;常数据成员只能通过构造函数初始化列表进行初始化,其他形式的函数均不能进行数据的赋值;
数据成员 | 非const的普通成员函数 | const成员函数 |
---|---|---|
非const的普通数据成员 | 可以引用,也可以改变值 | 可以引用,但是不可以改变值 |
const数据成员 | 可以引用,但是不能改变值 | 可以引用,但是不可以改变值 |
const对象 | 不允许 | 可以引用,但是不可以改变值 |
1.4 this
- 每个成员函数都有一个额外的,隐含的形参
this
。 this
总是指向当前对象,因此this
是一个常量指针。return *this;
可以让成员函数连续调用。
|
|
形参表后面的const
,改变了隐含的this
形参的类型,如普通成员函数中的this类型为 CLASSNAME *const
,而当声明常量成员函数后this类型为 const CLASSNAME *const
- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向const类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
1.5 类的静态成员
非static
数据成员存在于类类型的每个对象中。
static
数据成员独立于该类的任意对象而存在。
每个static
数据成员是与类关联的对象,并不与该类的对象相关联。
声明
声明之前加上关键词static
。
使用
使用作用域运算符::
直接访问静态成员:r = Account::rate();
也可以使用对象访问:r = ac.rate();
定义
在类外部定义时不用加static
初始化
静态数据成员通常不在类的内部初始化,而是在定义时进行初始化,如 double Account::interestRate = initRate();
如果一定要在类内部定义,则要求必须是字面值常量类型的constexpr
。
|
|
interestRate
被所有Account对象共享rate
不包含this指针,不能声明成const
2、类的构造函数
类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public
部分。 - 与类同名的成员函数。
2.1 默认构造函数
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor),默认构造函数无须任何实参。编译器创建的构造函称为合成的默认构造函数。只有当类没有构造函数时,编译器才会这样做。 默认构造函数按照如下规则初始化类
-
如果类内存在成员的初始值,则用它来初始化成员
-
否则,默认初始化成员
-
=default
要求编译器合成默认的构造函数。(C++11
)
2.2 构造函数初始值列表
|
|
总之,没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在)初始化,或者执行默认初始化
1. const、引用类型的成员初始化 但还需要注意一个问题,有些时候有成员是必须通过初始值列表来初始化
|
|
随着构造函数体一开始执行,初始化就完成了!! 初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值
|
|
2. 初始化顺序问题 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序,执行顺序与其在类定义中出现的顺序一致 ^172c2c
|
|
2.3 委托构造函数 (delegating constructor, C++11
)
委托构造函数将自己的职责委托给了其他构造函数。
Sale_data(): Sale_data("", 0, 0) {}
2.4 转换构造函数
如果类的构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数成为转换构造函数
在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这面一种类型向Sales_data隐式转换的规则。 也就是说,在需要使用 Sales_data的地方,我们可以使用string或者istream作为替代:
|
|
在这里我们用一个string实参调用了 Sales_data的combine成员。该调用是合法 的,编译器用给定的string自动创建了一个Sales_data对象。新生成的这个(临时) Sales_data对象被传递给combine。因为combine的参数是一个常引用,所以我们可以给该参数传递一个临时量
但是只允许一步类类型转换
|
|
这种就是错误的,因为暗含了两种类型的转换,“9-999-99999-9”转换为string,string转换为Sales_data
|
|
explicit抑制构造函数定义的隐式转换
使用关键词explicit
,有以下限制:
- 只对一个实参的构造函数有效
- 只能在类内声明构造函数时使用关键字,类外部定义时不应重复
- 只能阻止隐式转换,但是不能阻止显示转换
- 只能用于直接初始化,不能用于拷贝形式的初始化。
|
|
|
|
|
|
3、拷贝构造函数
todo
4、赋值函数
todo
5、析构函数
当对象不在存在执行销毁操作,销毁的操作由类的析构函数完成
6、关于构造、拷贝、赋值、析构函数的总结和深入
- 这四种成员函数,编译器会在没有定义时,生成默认构造、拷贝、赋值、析构函数
- 有些类不能依赖编译器生成的版本,因为这是不安全的
7、对象
对象是类的实例化,分为普通对象和常对象 定义普通对象的方法
|
|
常对象是指该对象在其生命周期内,其所有的数据成员的值都不能被改变;定义对象时加上关键字const,该对象就是常对象,其一般形式如下:
|
|
- 对象可以用
.
来访问成员 - 指向对象的指针用
->
来访问成员
二、 聚合类 (aggregate class)
- 满足以下所有条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
|
|
- 如果初始值列表中的元素个 数少于类的成员数量,则靠后的成员被值初始化。
- 初始值列表的元素个数绝对不能超过类的成员数量。
C++中引入聚合类的主要原因是为了方便地对一组相关的数据进行组织和管理。聚合类可以看作是一个数据容器,它把多个数据成员封装在一起,形成一个单独的数据单元,从而更加方便地使用这些数据。通过将相关的数据成员放在同一个聚合类中,我们可以更好地组织代码,提高代码的可读性和可维护性。此外,聚合类还可以减少代码量,避免出现过多的全局变量或结构体定义,从而使代码更加简洁、易于理解和维护。另外,聚合类也为C++的面向对象编程提供了一种新的方式。通过定义成员函数和重载操作符等方法,我们可以对聚合类进行更加灵活和方便的操作,实现更优雅的代码设计。最后,值得注意的是,虽然聚合类和结构体看起来很相似,但它们在语义上有所不同。聚合类更强调对数据的组织和封装,而结构体则更强调对数据的描述和表示。因此,在选择使用聚合类还是结构体时,需要根据具体的应用场景和需要进行选择。
一个比较典型的必须要使用聚合类的场景是数据库中的数据表。在实际开发中,我们通常需要使用一种数据结构来表示数据库中的数据表,这个数据结构需要包含每一行数据的各个字段,以及相关的属性和方法。对于这种情况,使用聚合类可以非常方便地组织和管理数据表中的数据。我们可以将每一行数据看作是一个对象,把所有行对象放在同一个聚合类中,形成一个数据表对象。这个聚合类可以包含各种成员函数和操作符重载,用于实现数据表的各种查询、排序、更新等操作。
三、 字面值常量类
constexpr
函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
四、访问控制与封装
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏了类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类。唯一的却别在于访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
1、友元
- 允许特定的非成员函数访问一个类的私有成员.
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元声明成组地放在类定义的开始或者结尾。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
2、封装的益处
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
五、类的其他特性
- 成员函数作为内联函数
inline
:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline
。
- 可变数据成员 (mutable data member):
mutable size_t access_ctr;
- 永远不会是
const
,即使它是const
对象的成员。
- 类类型:
- 每个类定义了唯一的类型。
六、类的作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。