C++之对象和类(八)
本文记录了C++中与对象和类相关的容易遗忘的一些知识。
面向对象编程(OOP)最重要的特性:
- 抽象
- 封装与数据隐藏
- 多态性
- 继承
- 代码的可重用性
抽象和类
指定一种基本类型会起到三个作用:
- 它决定了一个数据对象需要多少内存。
- 它决定了内存中的位如何被解释。(长整数和浮点数在内存中可能使用相同数量的位,但它们被转换为数值的方式不同。)
- 它决定了可以使用该数据对象执行哪些操作或方法。
将实现细节整合在一起并将它们与抽象分离开来,这被称为封装。
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
类作用域
有作用域枚举
为了解决如下形式的处于同一作用域,枚举值名称冲突:
1 | enum egg {Small, Medium, Large, Jumbo}; |
C++11提供了一种新的枚举形式,其枚举器具有类作用域,从而避免了这一问题。这种形式的声明如下所示:
1 | enum class egg {Small, Medium, Large, Jumbo}; |
现在需要使用枚举名称来限定枚举数:
1 | egg choice = egg::Large; // the Large enumerator of the egg enum |
C++11还加强了作用域枚举的类型安全性。常规枚举在某些情况下会自动转换为整数类型,例如赋值给int变量或在比较表达式中使用,但作用域枚举不会隐式转换为整数类型。
默认情况下,C++11 作用域枚举的底层类型是
int,此外,有一种语法用于表示不同的选择:
1 | // underlying type for pizza is short |
: short 指定基础类型为
short。基础类型必须是整数类型。在 C++11
标准下,也可以使用这种语法来指定无作用域枚举的基础类型,但如果你不选择该类型,编译器所做的选择则取决于具体实现。
运算符重载
重载限制
重载的运算符必须至少有一个操作数是用户定义的类型。这会阻止你对标准类型重载运算符。
不能以违反原始运算符语法规则的方式使用运算符。例如,不能重载取模运算符(%),使其可以用于单个操作数。
不能创建新的运算符符号。例如,不能定义一个
operator**()函数来表示幂运算。以下运算符不能重载:
Operator Description sizeofThe sizeof operator .The membership operator .*The pointer-to-member operator ::The scope-resolution operator ?:The conditional operator typeidAn RTTI operator const_castA type cast operator dynamic_castA type cast operator reinterpret_castA type cast operator static_castA type cast operator 只能使用成员函数(不支持非成员函数)来重载以下运算符:
Operator Description =Assignment operator ()Function call operator []Subscripting operator ->Class member access by pointer operator
引入友元
C++控制对类对象私有部分的访问。通常,公共类方法是唯一的访问途径,但有时这种限制对于特定的编程问题来说过于严格。在这种情况下,C++提供了另一种访问形式:友元。友元分为三种类型:
- 友元函数
- 友元类
- 友元成员函数
类型转换
应当谨慎使用隐式转换函数。通常,显式调用的函数才是最佳选择。
类型转换构造函数
只有一个参数的类构造函数充当着将参数类型的值转换为该类类型的指令。
关键字explicit,用于关闭自动转换功能,即在构造函数声明前加explicit。
转换函数
一种被称为转换函数的特殊类成员运算符函数,充当着将类对象转换为其他某种类型的指令。
要转换为typeName类型,需使用以下形式的转换函数:
1 | operator typeName(); |
当你将类对象赋值给该类型typeName的变量,或者使用该类型typeName的强制类型转换运算符时,这个转换函数会被自动调用。
请注意以下几点:
- 转换函数必须是类方法。
- 转换函数不得指定返回类型。
- 转换函数不得有参数。
例如,一个转换为double类型的函数会有这样的原型:
1 | operator double(); |
关键字explicit,用于关闭隐式转换功能。
特殊成员函数
C++会自动提供以下成员函数:
- 如果你没有定义任何构造函数,会有一个默认构造函数
- 如果你没有定义的话,会有一个默认的析构函数
- 如果你不定义一个拷贝构造函数的话
- 如果你不定义赋值运算符的话,系统会提供一个赋值运算符
- 如果你不定义地址运算符的话
默认构造函数
默认构造函数是指没有参数的构造函数,或者所有参数都有默认值的构造函数。如果你没有定义任何构造函数,编译器会为你定义一个默认构造函数。它的存在使你能够创建对象。
自动默认构造函数还有一个作用,就是为任何基类以及任何属于其他类对象的成员调用默认构造函数。
此外,如果你在编写派生类构造函数时,没有在成员初始化列表中显式调用基类构造函数,编译器会使用基类的默认构造函数来构造新对象中的基类部分。如果不存在基类默认构造函数,在这种情况下就会出现编译时错误。
如果你定义了任何类型的构造函数,编译器都不会为你定义默认构造函数。在这种情况下,如果需要默认构造函数,就需要你自己来提供。
拷贝构造函数
类的拷贝构造函数是一种以该类类型的对象作为参数的构造函数。通常,其声明的参数是对该类类型的常量引用。
类的拷贝构造函数用于以下情况:
- 当一个新对象被初始化为同一类的对象时
- 当对象通过值传递给函数时
- 当函数通过值返回对象时
- 当编译器生成临时对象时
调用时机:拷贝构造函数在每当创建新对象并将其初始化为同一类型的现有对象时被调用。
如果一个程序没有(显式或隐式地)使用拷贝构造函数,编译器会提供一个原型,但不会提供函数定义。否则,程序会定义一个执行成员逐一初始化的拷贝构造函数。也就是说,新对象的每个成员都会被初始化为原始对象对应成员的值。如果某个成员本身是一个类对象,那么成员逐一初始化会使用为该特定类定义的拷贝构造函数。
在某些情况下,成员逐一初始化并不可取。例如,用new初始化的成员指针通常需要进行深拷贝。或者,某个类可能包含一个需要修改的静态变量。在这些情况下,你需要定义自己的拷贝构造函数。
赋值运算符
默认的赋值运算符用于处理将一个对象赋值给同一个类的另一个对象。不要将赋值与初始化混淆。如果一条语句创建了一个新对象,那它使用的是初始化;如果一条语句更改了现有对象的值,那它就是赋值。
默认赋值使用逐成员赋值。如果某个成员本身是类对象,那么默认的逐成员赋值会使用为该特定类定义的赋值运算符。如果需要显式定义复制构造函数,出于同样的原因,也需要显式定义赋值运算符。
注意事项:
- 由于目标对象可能已经引用了先前分配的数据,该函数应使用
delete []来释放之前的内存占用。 - 该函数应防止将对象赋值给自身;否则,前面描述的内存释放操作可能会在对象内容被重新赋值之前就将其清除。
- 该函数返回对调用对象的引用。
示例:
1 | StringBad & StringBad::operator=(const StringBad & st) |
赋值不会创建新对象,因此不必调整静态数据成员num_strings的值。
动态内存分配
通常情况下,在程序运行时决定许多事情(例如使用多少存储空间)要比在编译时决定好得多。
静态数据成员在类声明中声明,并在包含类方法的文件中初始化。初始化时使用作用域运算符来指明该静态成员属于哪个类。不过,如果静态成员是const整数类型或枚举类型,则可以在类声明中直接初始化。
构造函数中使用new
在使用new来初始化对象的指针成员时,必须格外小心。具体来说,应该执行以下操作:
- 如果你在构造函数中使用
new来初始化指针成员,那么你应该在析构函数中使用delete。 new和delete的使用应当兼容。你应当将new与delete配对使用,将new []与delete []配对使用。- 如果存在多个构造函数,所有构造函数都应采用相同的方式使用
new(要么都带括号,要么都不带括号)。析构函数只有一个,因此所有构造函数都必须与该析构函数兼容。不过,在一个构造函数中用new初始化指针,而在另一个构造函数中用空指针(0,或者在C++11中用nullptr)初始化指针是允许的,因为对空指针执行delete操作(带或不带括号)都是可行的。 - 应该定义一个拷贝构造函数,通过深拷贝将一个对象初始化为另一个对象。
- 应该定义一个赋值运算符,通过深拷贝将一个对象复制到另一个对象。
示例:
1 | StringBad::StringBad(const StringBad & st) |
构造函数初始化列表
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初值。
1 | // 成员初始化列表语法 |
成员初始化列表语法通过直接使用拷贝构造函数初始化,只用一个步骤。效率更高。
函数体内赋值是先为类内成员调用默认的构造函数构造出一个对象,然后调用赋值运算符对齐赋值。多了一个赋值的动作。
对于基本类型的成员而言没有区别。
C++11特征
类内初始化
C++11允许类内初始化(即在类定义中进行初始化):
1 | class Queue |
这相当于使用成员初始化列表。不过,任何使用成员初始化列表的构造函数都会覆盖相应的类内初始化。