第一章 引论
- 面向对象的核心概念:
- 数据封装:对内保护信息加强联系,对外提供访问接口。
- 继承:整体和部分的关系,一般和特殊的关系。基类和派生类。
- 多态性:一个符号有多种含义。表现为函数重载(普通函数重载,运算符重载)
- 泛型编程:主要依托模板(Template)
第二章 数据类型
- 指针
1 | const作用于指针 |
引用&
引用就是对象的别名
独立引用必须初始化
参数引用->实参和形参相同
非常量引用不能指向常量
引用作为返回类型(避免内存复制,直接返回对象)
1
2
3
4
5
6
7
8
9int &f(int &i){return i;}
/*
返回一个左值对象(左值 != 左值引用)
可以出现在=左边
++f() (√) b = f() (√)
f(a)返回a这个对象 f(2) (X)
int* g();
*g() = (√)
*/运算符
new delete new[] delete[]
sizeof()
数组
指向数组的指针:
1
2
3int array[10];
int (*pa)[10];
pa = &array; //请注意&的存在指针数组:
1
2
3int array[10];
int *pa[10]; //指针数组的每一个元素都是指针
pb[0] = &array[0];
第三章 异常处理
- 异常抛出
1 | viod f() try |
第四章 函数
- 函数的类型
1 | int (int) f; //error |
函数的参数
传值 传指针 传引用
const作用于参数
1
2
3
4
5
6void func(const int *pi)
{
int a = *pi; //正确
*pi = 0; //错误,因为pi指向的单元被视为常量
pi = &a; //正确,因为pi不是常量指针
}缺省参数
1
2
3
4//所有取缺省值的参数都必须出现在不取缺省值的参数的右边。亦即,一旦开始定义取缺省值的参数,就不可以再说明非缺省的参数
void f1(int x = 10, int y); //error
void f2(int x, int y = 0); //ok
void f3(int x = 10, int y = 0); //ok
函数返回值
- 返回值类型
- 函数返回一个值类型,实际上是将返回的值放到一个临时对象中。调用者可以拷贝临时对象的值以供以后使用。函数Strlen()返回一个整数值,这个值被存储在一个临时变量(对象)中。临时对象是匿名的,并且被当做常量,因此不能作为左值使用。
- 返回指针
- 函数返回指针,实际上也是返回一个值,只不过这个值是某个单元的地址。
- 该指针指向的对象必须还是有效的
- 函数内部分配了内存要在适当的时候利用返回的指针释放掉内存。
- 返回引用
- 实际上返回的是一个对象,是个左值,只不过是匿名的。
- 与返回指针一样,函数返回的引用单元也必须在被调函数返回后一直有效。
- 返回值类型
函数重载(唯一依据是参数列表)
1 | f(int i = 0)/f() //不算 |
第五章 类和对象(全部重点)
- 类的定义
1 | class className |
- 类和对象
与其它类型一样,类只是一种形式化的规格说明。要使用类提供的功能,必须使用类的实例(类的静态成员(5.3.3)例外)。类的实例称为“对象”。一个类可以定义多个对象。定义对象的过程称为“实例化(instantiation)”,而一个对象也称为类的“实例(instance)”。
关系
•类代表了一组对象的共同性;对象被赋予了具体的性质。
•类在概念上是一种抽象机制,它抽象了一类对象的存储和操作特性;对象是类的一个实现,占据了物理存储器。
•在系统实现中,类是一种共享机制,它提供了一类对象共享其类的操作实现。这些操作通过类的实例(对象)来完成。
•类是一种封装机制,它将一组数据和对该组数据的操作封装在一起;对象是这种封转机制的具体实现。
•类是对象的模型,对象承袭了类中的数据和方法(操作)。只是各实例对象的数据初始化状态和各个数据成员的值不同。
访问控制
- 如果没有访问控制修饰符默认为private
- private
- protected
- public
类的成员
在一般情况下,一个类不能包含该类类型的对象作为成员
1
2
3
4
5
6
7class Zoo
{
private:
Zoo *p; //ok
Zoo &r; //ok 这个引用成员必须被初始化。
//Zoo o; //错误,类定义依赖于自身。
};静态成员
静态成员变量是所有类对象共享的,需要在类定义外额外分配存储
静态数据成员属于类,而不属于对象。访问方式
类名::静态共有数据成员
一般使用静态成员函数来访问静态数据成员。
第六章 深入类和对象
构造函数
构造函数的调用是自动进行的。这甚至不是一种程序员的可选项,而是编译器实施的一种强制性机制。每当创建类的一个新对象时,编译器将在创建的地方自动生成调用构造函数的代码,用以完成对象的初始化工作。在必要的时候,需要给出构造函数的参数。
类的构造函数的作用是:
–(1) 分配一个对象的数据成员的存储空间;(该功能由系统自动完成。)
–(2) 执行构造函数(体),一般是初始化一个对象的部分或全体数据成员。
复制构造函数
进行两个类对象之间的复制
1
2
3
4
5class 类名
{
public:
类名(const 类名&[, other parameters]); //copy constructor
};何处会调用复制构造函数
显式定义复制对象时,如下例所示:
–array a1;
–array a2(a1); //调用a2的复制构造函数
–array a3 = a2; //这不是对象间的赋值,而是复制(初始化)!
实参和形参结合时。如例中t和a结合时,将会调用形参t的复制构造函数来复制实参对象r;
函数返回值对象(非指针和引用)时。如例中f()返回一个临时对象,这个临时对象就是用其复制构造函数从return的返回表达式t中复制而来。这个临时对象是匿名的,并且被视为常量对象。
如果一个类没有显示定义复制构造函数,c++编译器会为该类合成一个隐式的缺省复制构造函数。
浅复制与深复制
- 浅复制共享同一内存空间,析构出错。
析构函数
析构函数的作用是:
1.执行析构函数(一般没有具体的工作);
2.释放对象的存储空间。(该功能由系统自动完成。)
3.释放对象占用的资源。这项工作要有程序员设定。
对象和指针
- this指针
1 | //this指针只能出现在类的非静态成员函数中,并且常用于需要自引用的地方。 |
指向类对象的指针
1 | void f() |
指向类成员的指针
1 | //类型名 类名::*指针; |
- 友元关系(提高效率、非必须)友元能够访问一个类的所有成员
- 友元关系不具备传递性
- 不具有对称性
友元类定义在类中表示该友元类能访问本类的所有成员。
对象作为函数的参数/返回值 (复制构造函数的重要性)
- 常成员函数
- 只读取属性而不修改它们。
- 包围类的成员对嵌套类是不可见的。嵌套类的作用域对包围类来说也是封闭的。
- 可以定义友元类来解决问题。
第七章 运算符重载
- 运算符重载的原型
1 | 返回值类型 operator @ (参数列表); |
双目运算符
- 类似于+=,运算的结果可以作为左值使用
1
2
3
4
5
6complex& operator += (const complex& rhs)
{
real += rhs.real;
imag += rhs.imag;
return *this;
}类似于+,运算符作为友元重载
参与运算的两个操作数都不变,运算结果是个新产生的值。
1
2
3
4
5
6friend Comlex operator + (const complex& lhs, const comlex& rhs)
{
//返回临时对象
return complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
//调用构造函数
}运算符重载形式
对于重载的单目运算符
作为成员重载时,函数没有参数,运算作用在lhs上;函数返回lhs的左值引用;
作为友元重载是,函数有一个参数(并且是左值引用),运算作用在参数对象上;函数返回参数对象的左值引用。当然,这是不推荐的重载方式。
这条规则关于参数个数有例外,就是当@是后缀++/—时。
对于作为成员重载的双目运算符
- 函数有一个参数,这个参数就是rhs,并且是常量;
- 产生的结果保存到lhs中;
- 函数的返回值是lhs的左值引用。
对于作为友元重载的双目运算符
- 有两个参数(即左右操作数),并且都是常量,且这两个参数中至少有一个是将该运算符函数作为友元对待的类的对象。;
- 函数产生一个新值,即返回值是一个对象(非引用非指针)。这会引起复制构造函数的调用,因此,最好为类提供一个复制构造函数。
对于关系和逻辑运算符,它们应该产生一个bool类型的结果。如果使用的编译器不支持bool类型,那么应该返回一个替代的整型值:1表示真,0表示假。
如果作为成员重载的运算只是读取对象的属性而不会改变它们,那么建议将该函数设为常成员。
常用运算符的重载(掌握运算符重载(算数、赋值),特殊>>*不要求实现,要求阅读,lambda不考)
赋值运算符的重载
赋值运算符与复制构造函数不同,编译器会为类隐式重载一个赋值运算符。在多数情况下,隐式重载的赋值运算符函数工作得很好。但在某些特殊场合,例如需要深复制的场合,赋值操作可能会出现问题。所以,在这种情况下,显式地为类提供重载的赋值运算符是非常明智的选择。
赋值运算符是一个典型的双目运算符,它的左操作数是个左值,右操作数却是左右值不限。因此,赋值运算符函数最好(其实是只能)作为类的成员重载,其唯一的参数最好是右值对象的常量引用,而其返回值应该是左值对象的引用。
1
2
3
4
5
6complex& operator = (const complex& c)
{
real = c.real;
imag = c.imag;
return *this;
}算数运算符的重载
单目运算符
产生临时变量,返回值为对象值
产生左值,返回值为操作数的引用
双目运算符
除了赋值(含复合赋值)运算符外,几乎所有的双目运算符都应该作为类的友元进行重载
1
friend type operator @ (const &, const &);
重载++和—运算符
- 前缀++应该以类的成员函数形式重载,其返回值是操作数对象本身的引用;
- 后缀++应该以类的成员函数形式重载,其返回值是操作数自加前的一个副本,是一个值结果。
- c++规定后缀++重载需要有一个整形占位参数
1
2
3
4
5
6
7
8
9
10
11
12complex& operator++()
{
++real;
++imag;
return *this;
}//前缀++
complex operator++(int)
{
complex temp(this->real, this->imag);
++real;++imag;
return temp;
}//后缀++关系运算符重载
输入输出运算符重载
1
2friend ostream& operator<<(ostream& os, const complex &c);
firend istream& operator>>(istream& is, complex &c);- 装箱和拆箱P177-P180
- 重载[] * ()
第八章 继承和派生(全部重点)
继承:后代对祖先特征的全盘接受是继承的过程。
基类和派生类:派生类(子类)继承基类(父类)
继承关系确立
- 继承基类的除(构造函数、析构函数、基类重载的赋值运算符)外的所有成员
- 改造基类成员,可以通过访问控制调用或直接使用同名成员覆盖基类成员。
- 增加新的成员。
继承中的类等级
- 直接基类、间接基类。
祖先类名::祖先类成员
- 直接基类、间接基类。
访问控制
基类的protected成员
基类的某些私有成员必须对派生类是可见的,或者可访问的,而在派生类外却又是不可见的,即是受保护的。
访问声明
1
2
3
4
5
6class tiger : private felid
{
public:
using felid::prey;//恢复成员的原有访问属性
};基类静态成员的派生
所有后代和基类共享唯一的静态成员
用
基类名::静态成员名
进行访问
基类和派生类的关系
派生类对象初始化必须调用基类的构造函数。
派生类对象和基类对象的相互转换
派生类直接赋值给基类合法
基类 = 派生类
派生类直接赋给基类的引用、指针
基类& = 派生类
不会引起对象转化基类* = &派生类
如果要反过来则需要强制转换
1
2felid &fr = dynamic_cast<felid&>(c)//强制转换基类为派生类
q = dynamic_cast<felid *>(&c);复制兼容性原则
1
2
3子类对象可以直接赋值给父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象派生类中重新定义基类的成员
派生类中重新定义基类的数据成员(首先查找本类作用域中的数据成员)
派生类中重载基类的成员函数
函数重载规则:
在相同的作用域中(例如同一个类中),重载的函数原型必须不同;
在不同的作用域中(例如不同的类中),重载的函数原型可以相同。
无论如何,派生类的函数名将屏蔽基类中重名的函数,即使它们的原型不一致。
```c++
class A {f();}
class B : A {f();}//可以原型相同(作用域不同)
//如果A中virtual f() 则B中为覆盖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- 派生类继承基类重载的运算符函数
#### 第九章 虚函数和多态性
- **多态性**:一个接口,多种实现
- 静态多态性:在编译时完成,普通的函数重载。
- 动态多态性:依赖**虚函数**来实现。虽然编译器仍然认为调用的是基类的成员,但由于派生类的成员覆盖了基类的同名成员,因此可以得到正确的结果。
- **虚函数virtual关键字:**
- 关键字virtual明确地告诉编译器:该类派生类中的同名成员函数将**覆盖**基类已定义的函数。
- 包含虚函数的类被称为多态类。
- **特点:**
- ①虚特性必须赋予给类的**成员函数**;
②虚函数不能是全局函数,也不能是类的静态成员函数;
③不能将友元说明为虚函数,但虚函数可以是另一个类的友元;
④**虚特性能够被继承**。如果派生类原型一致地重载了基类的某个虚函数,那么即使在派生类中没有将这个函数显式说明成是虚的,它也会被编译器认为是虚函数。
- 派生类覆盖之后,祖先的虚成员仍然存在。
- 一旦基类中的函数被声明为虚函数,后代中原型相同的函数都是虚的。
- 虚析构函数:
若析构函数不声明为虚函数,子类析构会出错。
- 通过强制类型转换将指针p转换为派生类指针,具体做法如下:
- delete (felid *)(p);
- 将felid类的析构函数说明成是虚的:
- virtual ~felid() { … }
- override和final描述符
- override表示覆盖基类中原型相同的成员
- final表示最终版本,派生类不能覆盖
- **<font color=red>实现多态性的条件</font>**(要求阅读和编写)——程序阅读写结果/程序补充
- 父类虚函数
- 子类覆盖
- 必须要父类指针、引用访问多态(虚)函数
- **纯虚函数**
- 纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,要求任何派生类都必须定义自己的版本。
```c++
class felid
{
public:
virtual string what() = 0;
};
包含纯虚函数的类是抽象类,不能直接对抽象类进行实例化。
在抽象类的派生类中,如果纯虚函数的最终覆盖函数仍是一个纯虚函数(即仍未提供一个函数体),那么该派生类仍是一个抽象类。
抽象类不能作为参数类型、返回类型,但指针、引用可
第十章 模板和泛型编程
模板(泛型编程)
不依赖任何具体类型来编写通用代码。是某种形式上的静态多态。
模板函数
- 函数模板需要实例化之后才能使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template < typename T,
[const类 常量表达式, …] >
返回值类型 函数名(参数列表)
{
//函数体
}
PS:
template <typename T>
bool Greater(const T& a, const T& b)
{
return a > b;
}
template <typename T, const int min>
bool Greater(const T& a, const T& b)
{
return a > b && b > min;
}
//模板的非类型参数的类型不能是浮点型、类类型或void类型。它一般是整数类型、枚举类型。非类型参数如果不是引用,那么它不是左值,不能改变其值,也不能获取其地址。- 模板特化
1
2
3
4
5
6
7
8typedef char * cstring;
template <>//特化的模板没有模板参数
bool Greater<cstring>(const cstring& s1, const cstring& s2)
{
out << "C-style string comparasion: ";
return strcmp(s1, s2) > 0 ? true : false;
}
//特化后的模板仍然是模板类模板
1
2
3
4
5
6template <typename T, [const 类型 常量表达式, …]>
class 类名
{
//成员定义;
};
//类的所有成员函数都是函数模板。- 类模板的使用
- 模板名<参数列表> 对象名;