第六章 函数
函数是一个命名的代码块,通过调用函数执行相应代码。函数有多个参数,通常会产生一个结果。重载函数表示同一个名字可以对应多个函数。
函数基础
- 函数定义:返回类型、函数名字、由0个或者多个形参组成的列表以及函数体。函数执行的操作在语句块中叫做函数体。
- 调用运算符:通过调用运算符来执行函数。调用运算符的形式是一对圆括号(),作用于一个表达式,这个表达式是函数或者指向函数的指针;圆括号内是用逗号隔开的实参列表,用实参初始化形参。调用表达式的类型就是函数的返回类型。
- 函数调用过程:
- 实参初始化函数对应的形参。
- 控制权转移给被调用的函数,主调函数的执行被暂时中断,被调函数开始执行。
- 执行函数首先会隐式地定义并初始化它的形参。
return
语句同时也完成两项工作:返回return语句中的值,二是将控制权从被调函数转移回主调函数。
- 形参和实参:实参是形参的初始值。实参的类型必须与对应的形参类型匹配。
- 函数返回类型:大多数的类型都能作为函数的返回类型。一种特殊的返回类型是void,表示函数不返回任何值。函数返回类型不能是数组类型或者函数类型,但是可以是指向数组的指针或者函数的指针。
局部对象
- 局部对象:在C++中名字有作用域,对象有生命周期,名字的作用域是程序文本的一部分,名字在其中可见。对象的生命周期是程序执行过程中该对象存在的一段时间。
- 生命周期:生命周期是程序程序执行过程中该对象存在的一段时间
- 局部变量:形参和函数体内部定义的变量统称为局部变量。对函数而言是局部的,对函数外部而言是隐藏的。
- 自动对象:只存在于块执行期间的对象。当块执行完成后,其值变成未定义
- 静态局部对象:
static
类型的局部变量,生命周期贯穿函数调用前后。
函数声明
- 函数声明:函数只能定义一次,但是可以声明多次。如果一个函数永远不会被用到,则可以只用声明而没有定义。函数的声明和定义唯一的区别是声明无需函数体,用一个分号代替。函数的声明主要用于描述函数的接口,也称函数原型。
- 在头文件中进行声明:建议变量在头文件中声明;在源文件中定义。
- 分离编译:
gcc a.c b.c
直接编译生成可执行文件;gcc -c a.c b.c
编译生成对象代码a.o b.o
;gcc a.o b.o
编译生成可执行文件。
参数传递
- 形参的初始化的机理和变量初始化一样
- 形参的类型决定了形参的交互方式,如果形参是引用类型,它将绑定到对应的实参上,否则实参的值拷贝后赋给形参。
- 形参是引用类型时,其对应的实参被引用传递或者函数被引用调用。
- 实参的值被拷贝给形参时,实参被值传递或者函数被传值调用。
传值参数
- 初始化一个非引用类型的变量时,初始值被拷贝给变量。对变量的改动不会影响初始值。函数对形参的所用操作都不会影响到实参。
- 指针实参:常用在C中,C++建议使用引用类型的形参代替指针。
传引用参数
- 使用引用形参,允许函数改变一个或者多个实参的值。
- 引用形参直接关联到绑定的对象,而非对象的副本。
- 使用引用形参可以用于返回额外的信息。
- 使用引用形参可以避免不必要的复制。
- 如果无需改变引用形参的值,最好将其声明为常量引用。
- void swap(int &a, int &b);
const形参和实参
- 顶层的const作用于对象本身。用实参初始化形参时会忽略掉顶层的const。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的;形参的顶层
const
被忽略。void func(const int i);
调用时既可以传入const int
也可以传入int
。 - 可以使用非常量来初始化一个底层的
const
对象,但是反过来用底层const对象初始化不行。 - 不能使用字面值来初始化一个非常量引用。
- 在函数中,不能改变实参的局部副本。
- 尽量使用常量引用。
数组形参
- 数组两个特殊性质:不允许拷贝数组、使用数组时通常会将其转换成指针。
- 当为函数传递一个数组时,实际上传递是指向数组首元素的指针。
- 注意数组的实际长度,不能越界。
- 数组通过指针的形式传递给函数,函数开始不知道数组的确切尺寸,调用者需要提供一些额外的信息。
- 通过标记指定数组的长度
- 使用标准库规范:传递数组首元素和尾后元素的指针
- 显式传递一个表示数组大小的形参
数组引用形参
- 将变量定义成数组的引用,形参也可以是数组的引用
f(int &arr[10])
和f(int (&arr)[10])
两者代表的意义不同。f(int &arr[10])
将arr声明成引用的数组,表示有10个引用f(int (&arr)[10])
arr是具有10个整数的整形数组的引用
传递多维数组
void print(int (*matrix)[10], int rowSize)
void print(int matrix[][10], int rowSize)
- 编译器会忽略掉第一个维度。
含有可变形参的函数
initializer_list
提供的操作(C++11
):
操作 | 解释 |
---|---|
initializer_list<T> lst; | 默认初始化;T 类型元素的空列表 |
initializer_list<T> lst{a,b,c...}; | lst 的元素数量和初始值一样多;lst 的元素是对应初始值的副本;列表中的元素是const 。 |
lst2(lst) | 拷贝或赋值一个initializer_list 对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。 |
lst2 = lst | 同上 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向lst 中首元素的指针 |
lst.end() | 返回指向lst 中微元素下一位置的指针 |
initializer_list
使用demo:
void err_msg(ErrCode e, initializer_list<string> il){
cout << e.msg << endl;
for (auto bed = il.begin(); beg != il.end(); ++ beg)
cout << *beg << " ";
cout << endl;
}
err_msg(ErrCode(0), {"functionX", "okay});
- 所有实参类型相同,可以使用
initializer_list
的标准库类型。 - 实参类型不同,可以使用
可变参数模板
。 - 省略形参符:
...
,便于C++
访问某些C代码,这些C代码使用了varargs
的C标准功能。
返回类型和return语句
无返回值函数
没有返回值的 return
语句只能用在返回类型是 void
的函数中,返回 void
的函数不要求非得有 return
语句。
有返回值函数
return
语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。- 值的返回:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 不要返回局部对象的引用或指针。
- 引用返回左值:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
- 列表初始化返回值:函数可以返回花括号包围的值的列表。(
C++11
) - 主函数main的返回值:如果结尾没有
return
,编译器将隐式地插入一条返回0的return
语句。返回0代表执行成功。
返回数组指针
Type (*function (parameter_list))[dimension]
- 使用类型别名:
typedef int arrT[10];
或者using arrT = int[10;]
,然后arrT* func() {...}
- 使用
decltype
:decltype(odd) *arrPtr(int i) {...}
- 尾置返回类型: 在形参列表后面以一个
->
开始:auto func(int i) -> int(*)[10]
(C++11
)
函数重载
- 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
main
函数不能重载。- 重载和const形参:
- 一个有顶层const的形参和没有它的函数无法区分。
Record lookup(Phone* const)
和Record lookup(Phone*)
无法区分。 - 相反,是否有某个底层const形参可以区分。
Record lookup(Account*)
和Record lookup(const Account*)
可以区分。
- 一个有顶层const的形参和没有它的函数无法区分。
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
特殊用途语言特性
默认实参
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
- 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。
内联(inline)函数
- 普通函数的缺点:调用函数比求解等价表达式要慢得多。
inline
函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。inline
函数应该在头文件中定义。
constexpr函数
- 指能用于常量表达式的函数。
constexpr int new_sz() {return 42;}
- 函数的返回类型及所有形参类型都要是字面值类型。
constexpr
函数应该在头文件中定义。
调试帮助
assert
预处理宏(preprocessor macro):assert(expr);
开关调试状态:
CC -D NDEBUG main.c
可以定义这个变量NDEBUG
。
void print(){
#ifndef NDEBUG
cerr << __func__ << "..." << endl;
#endif
}
函数匹配
- 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
- 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
- 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
- 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
函数指针
- 函数指针:是指向函数的指针。
bool (*pf)(const string &, const string &);
注:两端的括号不可少。- 函数指针形参:
- 形参中使用函数定义或者函数指针定义效果一样。
- 使用类型别名或者
decltype
。
- 返回指向函数的指针:1.类型别名;2.尾置返回类型。
第七章 类
定义抽象数据类型
- 类的基本思想是数据抽象和封装(隐藏)。
- 数据抽象是一种依赖于接口和实现分离的编程技术。
类成员
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数和类型别名。
类的成员函数
-
成员函数的声明必须在类的内部。
-
成员函数的定义既可以在类的内部也可以在类的外部。
-
使用点运算符
.
调用成员函数。 -
必须对任何
const
或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化。 -
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
-
默认实参:
Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }
-
this
- 每个成员函数都有一个额外的,隐含的形参
this
。 this
总是指向当前对象,因此this
是一个常量指针。- 形参表后面的
const
,改变了隐含的this
形参的类型,如bool same_isbn(const Sales_item &rhs) const
,这种函数称为“常量成员函数”(this
指向的当前对象是常量)。 return *this;
可以让成员函数连续调用。- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向const类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
- 每个成员函数都有一个额外的,隐含的形参
-
常量成员函数
- 常量成员函数不修改对象
- 常量成员函数在定义和声明中都应加const限定
- 非常量成员函数不能被常量成员函数调用,但构造函数和析构函数除外。
- 增加程序的健壮性,常量成员函数企图修改数据成员或调用非常量成员函数,编译器会指出错误。
- 对于X类型的非常量成员函数而言,其this指针的类型是 X * const,该指针自身是常量;但是对于X类型的常量成员函数而言,其this指针的类型是const X * const,是一个常量指针。
非成员函数
- 和类相关的非成员函数,定义和声明都在类的外部。
类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数被称为构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public
部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }
=default
要求编译器合成默认的构造函数。(C++11
)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
访问控制与封装
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏了类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类。唯一的却别在于访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
友元
- 允许其他类或者函数访问它的非公有成员(允许特定的非成员函数访问一个类的私有成员)。
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元的声明成组的放在类声明的开头和结尾。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
- 就算在类的内部定义友元函数,也必须在类的外部提供相应的声明使得该函数可见。
封装的益处
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现可以随时改变,而无须调整用户级别的代码
类的其他特性
- 成员函数作为内联函数
inline
;- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类的内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明的时候显式的加上
inline
。
- 可变数据成员(mutable data member):
mutable size_t access_ctr;
- 永远不会是const,即使他是const对象的成员。
- 类类型
- 每个类定义了唯一的类型。
类的作用域
- 每个类都会定义自己的作用域。在类的作用域之外,普通的数据和成员函数只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
构造函数再探
- 构造函数初始值列表
- 类似
python
使用赋值的方式有时候不行,比如const
或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别) - 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
- 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
- 类似
委托构造函数
- 委托构造函数将自己的职责委托给了其他的构造函数
Sale_data(): Sale_data("", 0, 0) {}
- 委托构造函数和构造函数:
- 相同点:两者都有一个成员初始值列表和一个函数体。
- 不同点:委托构造函数的成员初始值列表只有一个唯一的参数,就是构造函数。
隐式的类类型转换
- 如果构造函数只接受一个参数,则其实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数。
- 编译器只会自动的执行仅一步的类型转换。
- 抑制构造函数定义的隐式转换:
- 将构造函数声明为
explicit
加以阻止。 explicit
构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 将构造函数声明为
聚合类
- 当一个类满足下面条件时,是聚合的:
- 所有成员都是
public
的。 - 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有
virtual
函数
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
字面值常量类
constexpr
函数的参数和返回值必须是字面值- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值的常量类。
- 如果不是聚合类,则必须要满足下面所有条件
- 数据成员都必须是字面值类型
- 类必须至少含有一个
constexpr
构造函数 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
类的静态成员
- 非static数据成员存在于类类型的每个对象中。
- static数据成员独立于该类的任意对象而存在。
- 每个static数据成员是与类关联的对象,并不与该类的对象相关联。
- 声明:
- 声明之前加上关键字static。
- 使用
- 使用作用域运算符::直接访问静态成员。
- 也可以使用对象访问。
- 定义:
- 在类外部定义时不用加static
- 初始化
- 通常不在类的内部初始化吗,而是在定义时进行初始化。
- 如果一定要在类内部定义,则要求必须是字面值常量类型的
constexpr
。
第十三章 拷贝控制
当定义一个类时,可以显式地活着隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。
拷贝控制操作
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值函数
- 析构函数
拷贝、赋值和销毁
拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo { public: Foo(const Foo&); }
- 合成的拷贝构造函数:会将参数的成员逐个拷贝到正在创建的对象中。
- 拷贝初始化:
- 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
- 通常使用拷贝构造函数完成。
string book = "9-99";
- 出现的场景
- 用
=
定义变量时。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
- 用
拷贝赋值运算符
- 重载赋值运算符
- 重写一个名为
operater=
的函数 - 通常返回一个指向其左侧运算对象的引用
Foo& operater=(const Foo&);
- 重写一个名为
- 合成拷贝赋值运算符
- 将右侧运算对象的每个非
static
成员赋予左侧运算对象的对应成员。
- 将右侧运算对象的每个非
析构函数
- 释放对象所使用的资源,并且销毁对象的非
static
数据成员 - 名字由波浪号接类名构成。没有返回值,也不接受参数。
~Foo();
- 调用时机:
- 变量在离开其作用域时
- 当一个对象被销毁时,其成员被销毁
- 容器被销毁时,其元素被销毁
- 动态分配的对象,当对指向它的指针应用
delete
运算符时。 - 对于临时对象,当创建它的完整表达式结束时。
- 合成析构函数
- 空函数体执行完后,成员会被自动销毁
- 注意:析构函数体本身并不直接销毁成员。
三/五法则
- 需要析构函数的类也需要拷贝和赋值操作
- 需要拷贝操作的类也需要赋值操作,反之亦然。
使用=default
- 可以通过将拷贝控制成员定义为
=default
来显式地要求编译器生成合成地版本。 - 合成地函数将隐式地声明为内联地。
阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显式地还是隐式地。
- 定义删除地函数:
=delete
。 - 虽然声明了它们,但是不能以任何方式使用它们。
- 析构函数不能是删除地成员。
- 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应地成员函数将被定义为删除地。
- C++老版本中使用
private
声明来阻止拷贝。
拷贝控制和资源管理
- 类的行为可以像一个值,也可以像一个指针。
- 行为像值:对象有自己的状态,副本和原对象是完全独立的。
- 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
交换操作
- 管理资源的类通常还定义一个名为
swap
的函数。 - 经常用于重排元素顺序的算法。
- 用
swap
而不是std::swap
。
对象移动
- 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
- 在新标准中,我们可以用容器保存不可拷贝的类型,只要他们可以被移动即可。
- 标准库容器、
string
和shared_ptr
类既可以支持移动也可以支持拷贝。IO类和unique_ptr类
可以移动但不能拷贝。
右值引用
- 新标准引入右值以支持移动操作
- 通过
&&
获得右值引用 - 只能绑定到一个将要销毁的对象
- 常规引用可以称之为左值引用
- 左值持久,右值短暂
move函数
int &&rr2 = std::move(rr1);
move
告诉编译器,我们有一个左值,但我希望像右值一样处理它。- 调用
move
意味着:除了对rr1
赋值或者销毁它外,我们将不再使用它。
移动构造函数和移动赋值运算符
- 移动构造函数
- 第一个参数是该类型的一个引用,关键是,这个引用参数是一个右值引用。
StrVec::StrVec(StrVec &&s) noexcept{}
- 不分配任何新内存,只是接管给定的内存。
- 移动赋值运算符
StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
- 移动右值,拷贝左值
- 如果没有移动构造函数,右值也被拷贝。
- 更新三/五法则:如果一个类定义了任何一个拷贝操作,他就应该定义所有五个操作。
- 移动迭代器:
make_move_iterator
函数讲一个普通迭代器转换为一个移动迭代器。
- 建议:小心地使用移动操作,以获得性能提升。
第十四章 重载运算与类型转换
基本概念
- 重载运算符是具有特殊名字的函数:由关键字
operator
和其后要定义的运算符共同组成。 - 当一个重载的运算符是成员函数时,
this
绑定到左侧运算对象。动态运算符函数的参数数量比运算对象的数量少一个。 - 只能重载大多数的运算符,而不能发明新的运算符号。
- 重载运算符的优先级和结合律跟对应的内置运算符保持一致。
- 调用方式:
- data1 + data2;
- operator+(data1, data2);
- 是否是成员函数:
- 赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)运算符必须是成员。 - 复合赋值运算符一般来说是成员
- 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
- 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
- 赋值(
输入和输出运算符
- 第一个形参通常是一个非常量的
ostream
对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream
对象。 - 输入输出运算符必须是非成员函数。
重载输入运算符>>
- 第一个形参通常是运算符将要读取的流的因不用,第二个形参是将要读取到的(非常量)对象的引用。
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
算术和关系运算符
- 如果类同时定义了算术运算符和相关的符合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
相等运算符==
- 如果定义了
operator==
,则这个类也应该定义operator!=
。 - 相等运算符和不等运算符的一个应该把工作委托给另一个。
- 相等运算符应该具有传递性。
- 如果某个类在逻辑上有相等性的含义,则该类应该定义
operator==
,这样做可以使用户更容易使用标准库算法来处理这个类。
关系运算符
- 如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类定义<
运算符。如果同时还包含==
,则当且晋档<
的定义和++
产生的结果一直时才定义<
运算符。
赋值运算符=
- 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
- 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。
下标运算符[]
- 下标运算符必须是成员函数。
- 一般会定义两个版本:
- 1.返回普通引用。
- 2.类的常量成员,并返回常量引用。
递增和递减运算符(++、–)
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本。
- 通常应该被定义成类的成员。
- 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
- 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
- 后置版本接受一个额外的,不被使用的
int
类型的形参。因为不会用到,所以无需命名。
成员访问运算符(*、->)
- 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
- 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
- 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。
函数调用运算符
- 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
- 函数调用运算符必须是成员函数。
- 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
- 如果累定义了调用运算符,则该类的对象称作函数对象。
第十五章 面向对象程序设计
OOP概述
- 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
- 继承
- 通过继承联系在一起的类构成一种层次关系。
- 通常在层次关系的根部有一个基类
- 其他类直接或者间接地从基类继承过来,这些继承得到地类称为派生类。
- 基类负责定义在层次关系中所有类共同拥有地成员,而每个派生类定义各自特有的成员。
- 对于某些函数,基类希望它的派生类各自定义适合自己的版本,此时基类就将这些函数声明成虚函数。
- 派生类必须通过使用类派生列表明确指出它是从哪个基类继承过来的。形式: 一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。
class Bulk_quote : public Quote{};
- 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual
关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类地虚函数。具体地措施是在该函数地形参列表之后增加一个override关键字。
- 动态绑定 运行时绑定
- 使用同一段代码可以分别处理基类和派生类地对象
- 函数地运行版本由实参决定,即在运行时选择函数地版本。
定义基类和派生类
定义基类
- 基类通常都应该定义一个虚构函数,即使该函数不执行任何实际操作也是如此。
- 基类必须将它地两种成员函数区分开来:一种是基类希望其派生类进行覆盖地函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数。当使用指针或者引用调用虚函数时,该调用将被动态绑定。根据引用或者指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行派生类的版本。
- 如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
- 访问控制:
- protected : 基类和其派生类还有友元可以访问。
- private : 只有基类本身和友元可以访问。
- 什么是虚函数成员:对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
定义派生类
- 派生类必须通过类派生列表明确指出它是从哪个基类继承来的。
- C++11新标准允许派生类显式的注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参之后加一个
override
关键字。 - 派生类构造函数:派生类必须使用基类的构造函数去初始化他的基类部分。
- 静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。
- 派生类的声明:声明中不包含它的派生列表
- C++11新标准提供了一种防止继承的方法,在类名后面跟一个关键字final。
- 一个派生类的对象包含多个组成部分:一个含有派生类自己定义的成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
- 在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当成基类的对象来使用。而且也能够将基类的指针或引用绑定到派生类对象的基类部分上。这些被称为派生类到基类的转换。
类型转换与继承
- 理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。
- 可以将基类的指针或引用绑定到派生类对象上。
- 不存在从基类像派生类的隐式类型转换。
- 派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换。
虚函数
- 使用虚函数可以执行动态绑定
- OOP的核心思想是多态性
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型采用可能与静态类型不同。
- 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上virtual关键字,也可以不加。
- C++11新标准允许派生类显示的注明它将使用哪个成员改写基类的虚函数,即在函数的形参列表之后加一个override关键字。
- 如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上override关键字。
- 如果想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上override可以明确程序员的意图,让编译器帮忙确认参数列表是否会出错。
- 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 通常只有成员函数或者友元函数中的代码才需要使用作用域运算符来回避虚函数的机制。
抽象基类
- 纯虚函数 :清晰地告诉用户当前地函数是没有实际意义地。纯虚函数无需定义,只用在函数体地位置前书写
=0
就可以将一个虚函数说明为纯虚函数。 - 含有纯虚函数地类是抽象基类。不能创建抽象基类地对象。
访问控制与继承
- 受保护成员:
protected
说明符可以看作是public和private中地产物- 类似于私有成员,受保护地成员对类地用户来说是不可访问地。
- 类似于公有成员,受保护地成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或者友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
- 派生访问说明符:
- 对于派生类的成员和友元能否访问其直接积累的成员没什么影响。
- 友元关系不能继承
- 改变个别成员的可访问性:使用
using
。 - 默认情况下,使用
class
关键字定义的派生类是私有继承的;使用struct关键字定义派生类是公有继承的。
继承中类的作用域
- 每个类定义自己的作用域,在这个作用域内定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域中。
- 派生类的成员将隐藏同名的基类成员。
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
虚析构函数
- 基类通常应该定义一个虚析构函数,这样就能动态分配继承体系总的对象了。
- 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
- 虚析构函数将阻止合成移动操作。
合成拷贝控制与继承
- 基类或者派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似;他们对类本身的成员依次进行初始化、赋值或销毁的操作。
派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
- 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。
继承的构造函数
- C++11新标准中,派生类可以重用其直接基类定义的构造函数。
- 如
using Disc_quote::Disc_quote;
,注明了要继承Disc_quote
的构造函数。
容器与继承
- 当使用容器存放继承体系中的对象时,通常必须采用间接存储方式。
- 派生类对象直接赋值给基类对象,其中派生类部分会被切掉。
- 在容器中放置指针而非对象。