第一章 预备知识
第二章 开始学习C++
第三章 处理数据
第四章 复合类型
第五章 循环和关系表达式
第六章 分支语句和逻辑运算符
第七章 函数——C++的编程模块
第八章 函数探幽
第九章 内存模型和名称空间
单独编译
- 头文件中常包含的内容: (不能将函数定义放在头文件中,容易出现重定义错误) p301
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明 (结构声明不创建变量,只是告诉编译器如果创建该结构变量)
- 类声明
- 模板声明
- 内联函数
在同一个文件中只能将同一个头文件包含一次,利用下述C/C++技术可以避免多次包含同一个头文件。p302
1
2
3
4#ifndef COORDIN_H_
#define COORDIN_H_
...
#endif多个库的链接: 不同的编译器可能会为同一个函数生成不同的修饰名称(取决于编译器设计人员),名称的不同将使链接器无法将一个编译器生成的函数调用与另一个编译器生成的函数定义匹配。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。(如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误)。p304
存储持续性、作用域和链接性
- C++使用三种(在C++11中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间:p304
- 自动存储持续性
- 静态存储持续性
- 线程存储持续性(C++11)
- 动态存储持续性
作用域和链接
- 作用域(scope)描述了名称在文件(翻译单元)的多大范围可见。链接性(linkage)描述了名称如何在不同单元间共享。 自动变量的名称没有链接性,因为它们不能共享。p305
- 全局作用域是名称空间作用域的特例。 p305
自动存储持续性
- C++11中的auto: 在C++11中,auto关键字用于自动类型推断。但在C语言和以前的C++版本中,auto用于显式的指出变量为自动存储(实际中很少很使用,因为默认就是自动存储类型)。在C++11中,这种用法不再合法。p307
- 函数及其中的变量存放于“栈”中——这是专门流出来的一段内存,栈的长度由具体的实现决定。p308
- 寄存器变量:在C++11中,关键字register的作用只是显示地指出变量是自动的。鉴于它只能用于原本就是自动的变量,使用它的唯一原因是,指出程序员想使用一个自动变量。保留该关键字的原因是避免使用了该关键字的现有代码非法。p309
静态持续变量
- 和C语言一样,C++也为 静态 存储持续性变量提供了3种链接性,这三种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。p309
- 外部链接性(可在其他文件中访问)
- 内部链接性(只能在当前文件中访问)
- 无链接性(只能在当前函数或代码块中访问,与自动变量不同的是,就算不在函数中,变量也存在,只是不能访问)
- 由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态 数组和结构将每个元素或成员的所有位都设置为0。p309
创建三种链接性的静态持续变量:p309
- 外部链接性:必须在代码块的外面声明
- 内部链接性:必须在代码块的外面声明,并使用static限定符
- 无链接性:必须在代码块内部声明,并使用static限定符
1
2
3
4
5
6
7
8
9int global = 1000; //静态持续变量,外部链接性,作用域为整个文件
static int one_file = 50; //静态持续变量,内部链接性,作用域为整个文件
int main(){
...
}
void funct1(int n){
static int count = 0; //静态持续变量,无链接性,作用域为局部
int llama = 0;
}
五种变量存储方式:p310
- 自动
- 寄存器
- 静态,无链接
- 静态,外部链接
- 静态,内部链接
- 关键字重载: 关键字的含义取决于上下文,static用于局部声明,以指出变量是无链接性的静态变量时,表示的是存储持续性。而用于代码块外的声明时,static表示内部链接性,因为位于代码块外的变量已经是静态持续性了。p310
- 静态变量的初始化: 静态变量有三种初始化方式:零初始化(变量设为零)、常量表达式初始化和动态初始化。 零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件时初始化变量,动态初始化意味着变量将在编译后初始化。p310
- 静态变量的初始化过程: 首先,所有静态变量都被零初始化,而不管程序员是否显式地初始化了它。接下来,如果使用常量表达式初始化了变量,且编译器仅根据文件内容(包括被包含的头文件)就可计算表达式,编译器将执行常量表达式初始化。必要时,编译器将执行简单计算。最后,剩下的变量将被动态初始化。 常量表达式并非只能是使用字面常量的算术表达式。(sizeof运算符也可以)p310
- 链接性为外部的变量通常简称为外部变量,也称全局变量,它们的存储持续性为静态,作用域为整个文件。p310
静态持续性、外部链接性
单定义规则(One Definition Rule,ODR): 变量只能定义一次。为满足这种需求,C++提供了两种变量声明:p311
- 定义声明(简称定义):为变量分配存储空间。
- 引用声明(简称声明):不给变量分配存储空间,引用已有的变量。使用关键字extern
1
2double up; //定义声明
exterm int blem; //blem在别处定义
如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。p311
1
2
3
4
5
6
7//file01.cpp
extern int cats = 20; // 由于初始化,所以这里是定义而非声明
int dogs = 22; //定义
//即使去掉file01.cpp文件中的extern也无妨,效果相同。
//file02.cpp
extern int cats; //使用extern且无初始化,说明使用的是其他文件的cats
extern int dogs; //同上
静态持续性、内部链接性
- 将作用域为整个文件的变量声明为静态外部变量(内部链接性),就不必担心其名称与其他文件中的外部变量发生冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//file1
int errors = 20;
//file2
int errors = 5;
int main(){
cout<<errors; //报错,errors与file1中的外部变量重定义
...
}
//解决方法:file2
static int errors = 5;
int main(){
cout<<errors; // 输出5
}
静态存储持续性、无链接性
- 局部静态变量:虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将 保持不变 。另外,如果初始化了静态局部变量,则程序 只在启动时进行一次初始化 。以后再调用函数时,将不会被再次初始化。p315
说明符和限定符
- 存储说明符(storage class specifier):p317
- auto(在C++11中不再是说明符)
- register
- static
- extern
- thread_local(C++11新增的)
- mutable:即使结构(或类)变量为const,其某个成员也可以被修改
- cv-限定符(cv-qualifer):p317
- const:内存被初始化后,程序便不能再对它进行修改
- volatile:即使程序代码没有对内存单元进行修改,其值也可能发生变化
- 在默认情况下全局变量的链接性为外部,但const全局变量的链接性为内部。因此,将一组常量放在头文件中,其他引用该头文件的文件都相当于自己定义了私有的常量,这就是能够将常量定义放在头文件中而不会重定义的原因。p318
- 如果处于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性,
extern const int states = 50;
,在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。p318
函数和链接性
- C++不允许在一个函数中定义另一个函数,因此所有函数的存储持续性都自动为静态,即在整个程序执行期间都一直存在。p318
- 在默认情况下,函数的链接性为外部。即可以在文件间共享,使用extern来指出函数实在另一个文件中定义的(可选)。p318
- 可以使用关键字static将函数的链接性设置为内部,使之只能在一个文件中使用,必须同时在原型和函数定义中使用该关键字。p318
- 内联函数不受单定义规则的约束,这允许程序员能够将内联函数的定义放在头文件中。但是C++要求同一个函数的所有内联定义都必须相同。 p319
- C++查找函数顺序:静态(在本文件中找)——外部(在所有的程序文件中找)——在库函数中找。因此如果定义了一个与库函数同名的函数,编译器优先使用程序员定义的版本(C++不推荐这样做)。p319
语言链接性
- 不同的语言采用了不同的链接性,为了解决这种问题,需要特别指定函数采用的链接性(默认为C++链接性)。p319
存储方案和动态分配
- 前面介绍的分配内存的5种方案(线程内存除外),它们不适用于C++运算符new分配的内存,这种内存被称为动态内存。动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。 p320
使用new运算符初始化:
1
2
3
4
5
6
7
8
9
10
11//如果要为内置的标量类型分配存储空间并初始化,可在类型名后面加上括号和初始值
int *pi = new int(6);
double *pd = new double(99.99);
//如果要初始化常规结构或数组,需要用大括号的列表初始化,这要求编译器支持C++11.
struct where {double x; double y; double z;};
where *one = new where{2.5, 5.3, 6.2};
int *ar = new int [4]{2,4,6,8};
//列表初始化也可以用于单值变量
int *pin = new int{6};
double *pdo = new doubel{99.99};new失败时,在最初的10年中,C++在这种情况下让new返回空指针,但现在将引发异常std::bad_alloc。p320
运算符new和new[]分别调用函数1和2,同样delete和delete[]调用3和4。p320
1
2
3
4
5
6
7
8
9
10
11
12void * operator new(std::size_t);
void * operator new[](std::size_t);
void * operator delete(void *);
void * operator delete[](void *);
//std::size_t是一个typedef,对应于合适的整型
int *pi = new int;//该式会被转换为下式
int *pi = new(sizeof(int));
int *pa = new int[40];//同样,转换为下式
int *pa = new(40*sizeof(int));
delete pi;//同样,转换为下式
delete(pi);定位new运算符。p321
名称空间
传统的C++名称空间
- 一些基本术语:p324
- 声名区域:变量可以进行声明的区域。对于全局变量,其声明区域为所在的文件,对于局部变量,其声明区域为所在的代码块。
- 潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声名区域小。
- 作用域:变量对程序而言可见的范围。变量并非在其潜在作用域内的任何位置都可见,如被另一个嵌套声明区域中的同名变量隐藏。作用域小于潜在作用域。
新的名称空间特性
一个名称空间中的名称不会和另一个名称空间的相同名称发生冲突,利用新的关键字namespace可以创建名称空间:p325
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22namespce Jack{
double pail;
void fetch();
}
namespace Jill{
double fetch;
int pal;
}
//名称空间是开放的,可以重复使用namespace来将名称添加到名称空间中
namespace Jack{
char * goose(const char*); //将goose添加到Jack名称空间(已有pail和fetch)
}
//可以在另一个文件中使用namespce为函数原型写出定义
namespace Jack{
void fetch(){
...
}
}
//使用作用域解析运算符来使用名称空间
Jack::pail = 12.35;
Jill::pal = 1;
Jack::fetch();名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。保持,在默认情况下,在名称空间中声明的名称的链接性是外部的(除非使用了const)。p326
- using声明和using编译指令: p326
- using声明:
using Jack::fetch
使特定的标识符可用(可以用在代码块中)。 - using编译指令:
using namespace Jack
使整个名称空间可用(可以用在代码块中,放在代码块中时,虽然它只在该代码块中可见,但是其作用域不是布局的)。p328
- using声明:
- 使用using编译指令和使用多个using声明是不一样的。假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。p328
- 推荐使用using声明而不是using编译指令,因为前者更安全。在引入的名称有相同局部名称时,前者会发出错误提示,后者只会隐藏名称空间版本而不进行提示。p329
- 名称空间可以嵌套:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20namespce elements{
namespce fire{
int flame;
}
using Jill::fetch;
using namepace Jack;
float water;
}
using namespace elements;
using namespace elements::fire;
//访问Jill::fetch,由于在elements中声明了Jill::fetch,所以以下两种名称空间都可用
Jill::fetch;
elements::fetch;
using namespace elements;//这条编译指令与下面两条编译指令等价
using namespace elements;
using namespace Jack;
namespace ele = elements; //给elements创建别名
名称空间示例
名称空间及其用途
- 指导原则:p334
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权益之计。
- 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用,另外,包含头文件的顺序可能影响程序的行为。
- 导入名称时,首选使用作用域解析运算符或using声明的办法。
- 对于using声明,首选将其作用域设置为局部而不是全局。
第十章 对象和类
过程性编程和面向对象编程
- 面向对象变成(OOP),首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。p341
抽象和类
类型是什么
- 指定基本类型完成了三项工作:p342
- 决定数据对象需要的内存数量
- 决定如何解释内存中的位(long与float位数相同,但含义不同)
- 决定可使用数据对象执行的操作或方法
10.2.2 C++中的类
- 类规范由两个部分组成:p342
- 类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口——提供了类的蓝图。
- 类方法定义:描述如何实现类成员函数——提供了类的实现细节。
- 类对象成员访问类型默认为私有private。结构体成员访问类型默认为公有public。p345
类和结构的区别: 实际上,在C++中,对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类的默认访问类型是private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯碎的数据对象。(看上去类可以完美替代结构体,事实上也是这样,C++保留结构体的主要原因是为了向C兼容)。
实现类成员函数
定义成员函数时,使用作用域解析符(::)来标识函数所属的类。类方法可以直接访问类的组件(private和public均可,并且无需使用作用域解析符)。 p345
1
2
3
4//无需使用public,因为在声明函数原型时已经指明了访问类型
void Stock::update(double price){
...
}内联方法: 方法定义位于类声明处的函数都将自动成为内联函数。类声明常常将短小的成员函数作为内联函数。内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义,因此通常将内联定义放在定义类的头文件中。p347
1
2
3
4
5
6
7
8class Stock{
private:
int shares;
double share_val;
void set_tot() {total_val = shares*share_val;} //自动成为内联函数
public:
...
}类的每个新对象都有自己的存储空间,用于存储其内部变量和类成员。但是同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。p348
使用类
- 要创建类对象,可以像基本类型一样声明类对象
Stock kate,joe; //声明了2个对象kate和joe
,也可以使用new为类对象分配存储空间。p349
修改实现
- 利用
setf()
控制输出格式,并将修改限定在实现文件中,以免影响程序的其他方面。p351
类的构造函数和析构函数
声明和定义构造函数
- 构造函数没有返回类型。并且,构造函数的形参名称不能与类成员变量的形参名称完全相同,一种常见做法是在数据成员中使用
m_
前缀,或者用this指针this->company = company
。p3531
2
3
4
5
6
7
8
9
10
11class Stock{
private:
string m_company;
...
public:
Stock(const string &company);更
}
Stock::Stock(const string &company){
m_company = company;
}
使用构造函数
- C++提供了两种使用构造函数来初始化对象的方式。p354
1
2Stock garment = Stock("Furry"); //显示调用构造函数
Stock garment("Furry"); //隐式调用构造函数,二者等价
默认构造函数
当且进党没有定义任何构造函数时,编译器会提供一个默认构造函数,它不接受任何参数,也不做任何操作。它可以使得下述语句正常运行:p354
1
Stock cat; //隐式地调用了默认构造函数
如果为类定义了构造函数,程序员就必须为它显式提供默认构造函数,除非不使用无参数的对象声明
Stock cat;
,否则会报错。定义默认构造函数的方式有两种:p3541
2Stock(const string & company = "default_company"); //为所有参数提供默认值
Stock(); //函数重载定义无参数的构造函数隐式地调用默认构造函数时,不要使用圆括号:p355
1
2
3
4Stock first("Furry"); //隐式调用非默认构造函数
Stock second(); //这是一条声明语句,指出second()是一个返回Stock对象的函数
Stock third; //隐式调用默认构造参数
Stock third = Stock(); //显式调用默认构造参数接受一个参数的构造函数(或者其它的参数提供了默认值)允许使用赋值语法将对象初始化为一个值。p362
1
Classname object = value;
带参数的构造函数也可以是默认的构造函数,只要所有参数都有默认值。但是只能有一个默认构造参数,也就是说,一旦所有参数都提供了默认值,就不能再声明无参数的构造函数,否则会产生二义性错误。 p433
析构函数
如果程序员没有提供析构函数,编译器将隐式的声明一个析构函数,析构函数没有返回类型,也没有参数,在声明时,需要在类型前加上波浪号:p355
1
2
3
4
5
6
7class Stock{
public:
~Stock(); //声明
}
Stock::~Stock(){ //定义
}编译器调用析构函数的时机:p356
- 静态存储类对象:在程序结束时自动被调用
- 自动存储类对象:在程序执行完代码块时自动被调用
- new创建的对象:当使用delete来释放对象内存时自动被调用
构造函数的另一种用法——赋值。语句1为初始化语句,语句2为赋值语句,构造函数会创建一个 临时 的对象,然后将该对象的值赋给已经存在的对象stock1,之后编译器会自动调用析构函数 删除该临时对象 。p361
1
2
3Stock stock1 = Stock("test1");
stock1 = Stock("test2");
//如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式,通常这种方式的效率更高。可以使用C++11的列表初始化方式来作用于类,前提是提供了相应的构造函数。p361
1
2Stock hot_tip = {"Plus" ,100, 45.0};
Stock jock{"Sport"};
以上两个声明中,用大括号括起的列表与下面的构造函数匹配:1
Stock::Stock(const std::string& co, long n =0, double pr = 0.0);
另外,C++11还提供了名为std::initialize_list
的类,可将其用作函数参数或方法参数的类型。这个类可表示任意长度的列表,只要所有的列表项的类型都相同或可转换为相同的类型。(在16章介绍)。
- C++的成员函数如果不修改调用对象,则应将其声明为const,将const关键字放在函数的括号后面。(放在前面就变成了返回类型为const double了)p362
1
2
3
4
5
6
7
8class Stock{
public:
double show() const; //const成员函数声明
}
double Stock::show() const{ //const成员函数定义
}
this指针
- this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。一定要注意this是一个指向对象的指针,所以在使用时要按照指针的方式。p364
1
2this->shares; //用间接成员运算符->引用对象的成员
return *this; //返回this指向的对象
对象数组
利用对象数组可以创建同一个类的多个对象。p368
1
2
3
4
5
6Stock mystuff[4]; //调用默认构造函数
Stock stocks[4]={ //为每个元素调用指定的构造函数
Stock("NanoSmar");
Stock(); //显示调用默认构造函数
//stocks[2]和stock[3]未指明构造函数,将调用默认构成函数
}初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。p369
类作用域
- C++类引入了一种新的作用域:类作用域。在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。要调用公有成员函数,必须通过对象访问。同样,在定义成员函数时,必须使用作用域解析符。
作用域为类的常量
- 直接在类中声明const常量是非法的,因为声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于储存值的空间。p371
- 实现“类的常量”的两种方式:
- 方式1:使用枚举,在类中声明一个枚举,用枚举为 整型常量 提供作用域为整个类的符号名称。
- 方式2:使用static,这将创建一个常量,该常量将于其他静态变量存储在一起,而不是存储在对象中。该常量被所有的类对象共享。
1
2
3
4
5
6class Bakery{
private:
const int Months = 12; //非法,无法编译
enum {Months = 12}; //未提供枚举名,这种方式声明枚举并不会创建类数据成员,Months只是一个符号名称,在编译时,将用12来替换它。
static const int Months = 12; //C++98中,不能存储double常量,C++11消除了这种限制
}
作用域内枚举
传统的枚举如果两个枚举定义中的枚举量名称相同,则会发生冲突,C++利用类作用域的方法消除了这种冲突。p372
1
2
3
4
5
6
7
8
9//传统枚举量,产生冲突
enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small, Medium, Large, Xlarge};
//类作用域,不冲突。 也可以利用关键字struct代替class。
enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Xlarge};
//使用时用枚举名和作用域解析符来限定枚举量:
egg choice = egg::Large;
t_shirt t_choice = t_shirt::Large;C++11还提高了作用域内枚举的类型安全,在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型。p372
- 枚举有某种底层整型类型表示,在C++98中,如何选择取决于实现,因此包含枚举的结构的长度可能随系统而异。对于作用域内枚举,C++11消除了这种依赖性。默认情况下,C++11作用域内枚举的底层类型为int。而常规枚举的底层类型依然随实现而异。另外,C++11提供了指定底层类型的语法。p372
1
enum class :short pizza {Small,Medium,Large,XLarge}; //:short将底层类型指定为short
抽象数据类型
- 类很适合描述ADT。公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。p373
第十一章 使用类
运算符重载
- 要重载运算符,需使用被成为运算符函数的特殊函数形式。
op
必须是有效的C++运算符,不能虚构一个新的符号。p3811
2
3
4
5
6
7
8
9
10
11
12operatorop(argument-list){
}
Stock::operator+(...){
}
Stock::operator*(...){
}
//当编译器发现了运算符的操作数是对应的对象是,会自动替换运算符为重载函数
stock3 = stock1 + stock2; //左侧的操作数为调用对象,右侧为重载函数的参数
stock3 = stock1.operator+(stock2); //用operaotr+重载函数替换“+”
计算时间:一个运算符重载示例
添加加法运算符
- 对于连加或连乘,需要函数返回的是正确的对象类型。p387
1
2t4 = t1 + t2 + t3;
t4 = t1.operator+(t2.opertor+(t3)); //当operator+返回的函数类型复合其参数列表要求时,合法。
重载限制
- 重载的运算符不必是成员函数,但必须至少有一个操作数是用户定义的类型,这是为了防止用户为标准类型重载运算符。p387
- 使用运算符时不能违反运算符原来的语法规则,如双目不能重载成单目,同时,重载不会修改运算符的优先级。p387
- 不能创建新的运算符。p387
- 不能重载下面的运算符: p387
- “sizeof”运算符
- “.”成员运算符
- “.* ”成员指针运算符
- “::” 作用域解析运算符
- “? :” 三目条件运算符
- “typeid” 一个RTTI运算符
- “const_cast” 强制类型转换运算符
- “dynamic_cast” 强制类型转换运算符
- “reinterpret_cast” 强制类型转换运算符。
- “static_cast” 强制类型转换运算符
- 大多数运算符都可以通过成员或非成员函数进行重载,但下面 的运算符只能通过成员函数进行重载: p387
- “=” 赋值运算符
- “()” 函数调用运算符
- “[]” 下标运算符
- “->” 通过指针访问类成员的运算符
友元
- 除了private,public和protect控制的类访问权限外,C++提供了另外一种形式的方式权限:友元。通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。友元有3种: p391
- 友元函数
- 友元类
- 友元成员函数
创建友元函数
创建友元的第一步是将其原型 放在类声明中 ,并在原型声明前加上关键字friend:p391
1
2
3friend Time operator*(double m, const Time & t);
//可以解决2.85*time的乘法重载的问题,2.85不是Time对象,因此无法调用成员重载函数,需要借助友元非成员函数实现。
//该声明意味着:1、虽然该函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用; 2、虽然该函数不是成员函数,但它与成员函数的访问权限相同。编写友元函数的定义。因为它不是成员函数,所以无需使用
类名::
限定符,另外,定义时不要在函数头使用关键字friend
。p392
常用的友元:重载<<运算符
- cout是一个ostream对象,对于每种基本类型ostream类声明中都包含了相应的重载的operator<<()定义。因此,对于不同的基本类型,<<运算符在cout对象中可以表现出不同行为。p392
<<的第一种重载版本 如果直接通过类声明来重载
operator<<()
函数,那么在使用时就会像这样,time<<cout;
,其中,time是Time类的实例,而cout是Time类重载函数的参数,为了看起来不那么迷惑,利用友元函数,使其第一个参数为ostream对象,这样一来,就可以使用cout<<time
的形式(运算符左侧操作数是第一个参数)。p3931
2
3
4
5
6void operator<<(ostream &os, const Time &t){
os<<t.hours<<t.minutes; //os是cout的引用,别名,省去了拷贝副本的时间
}
cout<<time1; //等价于下式
operator<<(cout,time1);<<的第二种重载版本 上面的重载方法有一些问题,那就是无法使用
cout<<time1<<time2<<endl;
这样的形式,解决方法如下:p3941
2
3
4ostream & operator<<(ostream & os, const Time & t){
os<<t.hours<<t.minutes;
return os; //返回os的引用,以便实现连续使用<<的操作。
}
重载运算符:作为成员函数还是非成员函数
- 成员函数和非成员函数的实现方法二者均可,但不能都实现,否则会产生二义性错误。p398
再谈重载:一个矢量类
- 一个应用了运算符重载和友元设计的例子——矢量类。p398
类的自动转换和强制类型转换
- 只有一个参数的构造函数可以作为转换函数。如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。p413
转换函数
转进行从对象到基本类型的转换,必须使用特殊的C++运算符——转换函数
operator typeName()
。创建转换函数时,需要注意以下几点:p415- 转换函数必须是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
1
2
3
4
5
6
7
8operator double() const; //转换为double类型的函数原型。
Stonewt::operator double() const{ // 转换函数的定义
return pounds;
}
double d = stonewt; //隐式调用转换函数
double d = double(stonewt); //显式调用转换函数
在进行类型转换时,一定要注意是否有二义性,如果有,编译器将产生错误。p418
- 提供执行自动、隐式的转换函数存在的问题是,在用户不希望进行转换时,转换函数也可能进行转换。消除这种隐患的方式是在转换函数原型前加上关键字
explicit
。(C++98不能将explicit用于转换函数,C++11可以)。另一种方法是使用功能相同的非转换函数,在进行转换时显式调用该函数即可。p419 - 总之,C++为类提供了下面的类型转换:p419
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。在构造函数声明中使用explicit可防止隐式转换。
- 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。没有返回类型、没有参数,名为operator typeName()。
- 将加法等二元运算符定义为友元可以让程序更容易适应自动类型转换。因为这会可以自动将对象类型转换成基本类型,或者将基本类型转换为对象类型进行运算。p420
- 将double变量与对象相加,由两种选择。一种是借助类型转换,另一种是在重载函数中显式接受double参数而不进行类型转换。 前者定义简单,但需要类型转换,增加了内存和时间开销。后面定义麻烦,需要写更多逻辑,但运行速度快。p421