C++............................................................................................................................................................................ 2
STL............................................................................................................................................................................ 30
C++
一、C++语言的背景介绍
1.C++语言的江湖地位
C Java Objective-C C++
2.历史人物
Ken Thompson,B语言之父,UNIX发明人之一,1943-活着。
Dennis Ritchie,C语言之父,UNIX之父,黑客之父,1941-2011。
Bjarne Stroustrup,C++之父,1950-活着。
3.C++之父的贡献
1979年4月,分析UNIX系统由于内核分布而造成的网络流量,试图将simula语言的面向对象特性和B语言的高效性结合起来。
1979年10月,Cpre预编译器,为C语言增加了一些类似simula的机制。
1983年,C with Classes,后来被更名为C++。
- simula的类
- algol的操作符重载
- BCPL的注释风格——“//”
- ADA的模板和名字空间
- Clu/ML的异常
1985年,CFront 1.0发布
1987年,GNU C++发布
1990年,Borland C++发布
1992年,Microsoft C++发布
1998年,ISO标准,C++98
2003年,对C++98进行修订,C++03
2011年,大幅升级,C++11(C++0x)
2014年,对C++11做了部分扩展,C++14
二、无处不在的C++
游戏、科学计算、网络和分布式应用、操作系统和设备驱动、嵌入式、编译器、虚拟机和脚本引擎,等等。
三、C++的竞争对手
Java、C#、C
同时具备高性能和良好抽象建模能力。
四、更好C
1.纯编译语言,与C语言具有完全相同的编译模型。
2.强类型语言,比C语言的类型检查更加严格。
i = 10;
i = "hello";
int i;
3.去除了一些C中不好的特性。如函数的隐式声明等。
4.增加了一些新的特性:面向对象、操作符重载、异常和泛型编程。
5.和C相比C++更加适合从事大型系统的开发。
五、第一个C++程序
1.编译命令
g++,也可以用gcc,但是要加上-lstdc++
2.扩展名
.cpp/.cc/.C/.cxx,也可以用.c,但是用gcc命令编译时需要加上-x c++
3.头文件
#include <iostream>
4.I/O对象
cin:标准输入(stdin)
cout:标准输出(stdout)
cerr:标准错误(stderr)
<<:插入
>>:提取
a*b
*p
5.名字空间
所有标准C++语言中的函数、对象、类型等都在std名字空间中。
六、名字空间
1.为什么?
1)避免产生名字冲突。
2)将基于逻辑的划分和基于物理的划分独立开。
2.什么是?
namespace 名字空间名 {
名字空间成员1;
名字空间成员2;
...
}
namespace ns {
int var = 0;
void fun (void) { ... }
}
3.名字空间合并
namespace 名字空间名 {
名字空间成员1;
名字空间成员2;
}
namespace 名字空间名 {
名字空间成员3;
}
a.cpp:
namespace ns {
int var = 0;
void fun (void) { ... }
}
b.cpp:
namespace ns {
struct type { ... };
}
4.使用名字空间中的名字
1)通过作用域限定操作符“::”
2)名字空间指令
using namespace 名字空间名;
该条指令以后的代码中,对于所使用的名字空间中的名字可以直接引用,前提是没有冲突。
3)名字空间声明
using 名字空间名::成员名;
将指定名字空间中的特定成员引入当前作用域。
using ns::var;
using ns::fun;
using ns::type;
4)无名名字空间
C++会将不属于任何有名名字空间中的名字统统放到无名名字空间中。对于无名名字空间中的名字可以直接通过“::”访问。
5)名字空间嵌套
七、结构、联合和枚举
struct Student {
...
};
struct Student s1 = { ... }; // C
Student s1 = { ... }; // C++
union Array {
...
};
union Array a1; // C
Array a1; // C++
enum COLOR { ... };
enum COLOR c1; // C
COLOR c1; // C++
1.C++的结构体可以定义函数,而且在这些函数中可以直接访问结构体的字段(成员变量)。
2.C++中定义匿名联合。
3.C++中的枚举不能和整型通用。
八、字符串
C++语言提供了专门的字符串类型:string。
九、布尔类型
bool:true/false
一、重(chong)载(overload)
1.同一个作用域中,函数名相同,参数表不同的函数,构成重载关系。
2.调用函数时,根据实参与形参的类型匹配情况,选择一个确定的重载版本,这个过程称为重载解析。
3.通过函数指针指向具有重载关系的函数,所选择的重载版本由该函数指针的类型决定。
4.C++换名:C++编译器会按照一定的规则,将函数的参数表信息与函数的原始名混合编码,形成一个新的函数名。因此具有重载关系的函数,虽然其原始名一样,但是因为其参数表的不同,最后编译生成的实际函数名是不同的。
5.通过extern "C"可以要求C++编译器按照C的方式处理函数接口,即不换名,当然也就重载。
extern "C" int add (int a, int b);
extern "C" int sub (int a, int b);
或
extern "C" {
int add (int a, int b);
int sub (int a, int b);
}
如果包含extern "C"声明指定符的文件也需要被C编译器处理,为了防止C编译器因为无法识别extern "C"而导致报错:
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
只有在C++编译器中__cplusplus宏才有定义,在C编译器中该宏无定义。
二、缺省参数
1.可以为函数的参数指定缺省值,调用函数时若未指定实参,则与该实参相对应的形参取缺省值。
2.函数的缺省参数是在编译阶段解决的,因此只能用常量、常量表达式或者全局变量等非局部化数值作为缺省参数。
3.如果函数的声明和定义分开书写,那么该函数的缺省参数只能出现在声明部分,定义部分不能指定缺省参数,但是可以通过注释提高代码的可读性。
4.如果函数的某一个参数带有缺省值,那么该参数后面的所有参数必须都带有缺省值。
5.不要因为使用缺省参数而导致重载歧义。
三、哑元
1.只指定类型而不指定名称的函数参数,谓之哑元。
2.哑元主要应用于版本升级过程中的向下兼容和特殊的操作符重载等场合。
四、内联
1.内联就是用函数已被编译好的二进制代码,替换对该函数的调用指令。
2.内联在保证函数特性的同时,避免了函数调用的开销。通过牺牲代码空间,赢得了运行时间。
3.内联会使可执行文件的体积和进程代码区的内存变大,因此只有频繁调用的简单函数才适合内联。稀少被调用的复杂函数,调用开销远小于其执行开销,由内联而获得的时间性能改善,不足以抵消空间性能的损失,故不适合内联。
4.递归函数不能内联。
5.通过inline关键字可以显式地请求编译器将某个函数处理为内联函数。
6.inline关键字仅仅表示一种对函数实施内联优化的期望,但该函数是否真的会被处理为内联,还要由编译器的优化策略决定。
7.多数现代编译器已经把内联作为一种缺省的优化机制,即使不是显式使用inline关键字,只要该函数符合内联优化的条件,编译器也会自动将其处理为内联函数。
五、动态内存分配
1.C中动态内存分配函数在C++中可以继续使用。
#include <cstdlib>
void* malloc (size_t size);
void* calloc (size_t nmemb, size_t size);
void* realloc (void* ptr, size_t size);
void free (void* ptr);
typedef unsigned int size_t;
2.new/delete运算符
int a = 0;
sizeof (a=5);
cout << a << endl; // 0
1)动态分配/释放单个变量
int* p = new int;
int* p = new int (123);
delete p;
2)动态分配/释放数组变量
int* p = new int[5] { ... };
delete[] p;
3)动态分配/释放高维数组
int (*prow)[4] = new int[3][4];
delete[] prow;
4)new在分配内存失败的情况下,不是返回NULL指针,而是抛出bad_alloc异常。
5)定位分配
new (地址) 类型[大小]
int* pn = new (pool) int (123);
一、引用
1.引用的基本特性
1)引用即别名
声明一个标识符为引用,即表示该标识符可作为另一个有名或无名对象的别名。
int a = 10;
int& b = a; // b是a的一个引用,即b是a的一
// 个别名
++b;
cout << a << endl; // 11
int& c = b; // c是a的另一个引用
++c;
cout << a << endl; // 12
常引用的目标不可修改,只能读取。
int const& d = c;
++d; // ERROR
在C++中,无名对象(字面值常量、临时变量)都被视作右值,只能通过常引用引用之。
int const& a = 10; // 纯右值
int const& a = x + y; // 将亡右值
2)引用必须初始化
int a = 10;
int a;
int* p = NULL;
int* p;
int& r; // ERROR
int& r = a;
3)无法定义一个什么都不引用的引用。
int* p = NULL; // 什么都不指向的指针
int& r = NULL; // ERROR
4)引用一经初始化便不能再引用其它对象。
int a;
int& r = a;
int b;
r = b; // b -> a
2.引用型参数
1)函数的形参是实参的别名
可以将函数的形参声明为引用形式,该形参在参数传递过程中由对应的实参初始化,并成为该实参的别名。
2)在函数中修改实参的值
通过引用型形参,可以在函数体内部修改调用者实参的值,成为除返回值和指针参数以外,第三种由函数内部向函数外部输出数据的途径。
3)避免对象复制的开销
通过引用传递参数,形参只是实参的别名而非其副本,这就避免了从实参到形参的对象复制,这对于具有复杂数据结构的参数而言可以提高其传参的性能。
如果函数只是读取引用型参数的目标对象,那么最好通过常引用接收实参,防止在函数中意外地修改实参变量的值。
3.引用型返回值
foo () = 10;
++foo ();
1)返回左值
2)函数的局部变量只具有函数级甚至块或语句级的生命周期,函数一旦返回,所有的局部变量即刻销毁,即使通过返回值获得了对它们的引用,其目标也将是未定义的。因此不要从函数中返回对局部变量的引用,而返回全局、静态、成员、堆变量的引用是安全的。
int* foo (void) {
int a = 10;
...
return &a;
}
printf ("%d ", *foo ());
int b = 20;
int c = b + *foo ();
4.虽然引用是通过指针实现的,但是在高级语言层面引用和指针还是具有若干不同的特性
1)指针可以不初始化,其目标可以在初始化后随意变更;但是引用必须初始化,而且一旦初始化就无法变更其目标
int x = 10, y = 20;
int* p; // p没有初始化
p = &x; // p指向x
p = &y; // p指向y
--------------------
int x = 10, y = 20;
int& r = x; // r必须初始化
r = y; // r引用不了y
2)可以定义空指针,即什么也不指向的指针,但是不能定义空引用,引用必须有所引用,否则引用将失去意义。
int* p = NULL;
int& r = NULL; // ERROR
3)可以定义指向指针的指针,但是无法定义引用引用的引用。
int x = 10;
int* p = &x; // 一级指针
int** pp = &p; // 二级指针 // 指向指针的指针
------------------
int x = 10;
int& r = x;
int&& rr = r; // ERROR
C++2011中类似“int&&”的类型是合法的,但是它表示右值引用,而非二级引用。
4)可以定义引用指针的引用,但是无法定义指向引用的指针。
int x = 10;
int* p = &x;
int*& q = p; // q是p的别名
cout << *q << endl; // 10
----------------
int x = 10;
int& r = x;
int&* s = &r; // ERROR
int* s = &r; // OK
5)可以定义存放指针的数组,但是无法定义存放引用的数组。可以定义引用数组的引用。
int a = 10, b = 20, c = 30;
int* p[3] = {&a, &b, &c}; // 指针数组
-----------------
int a = 10, b = 20, c = 30;
int& r[3] = {a, b, c}; // ERROR
-----------------
int a[3] = {10, 20, 30};
int (&r)[3] = a; // 数组引用
cout << r[0] << endl; // 10
r[1]++;
cout << a[1] << endl; // 21
5.函数指针和函数引用
int x = 10;
int* p = &x;
int& r = x;
*p = 20;
r = 20;
----------------------------
int func (double); // 函数
int (*pfunc) (double) = &func; //函数指针
int (&rfunc) (double) = func; //函数引用
(*pfunc) (3.14);
rfunc (3.14);
二、显式(强制)类型转换
1.C风格的显式类型转换
(目标类型)源类型变量
int i = 1234;
char c = (char)i;
2.C++风格的显示类型转换
1)静态类型转换
static_cast<目标类型> (源类型变量)
编译器会对源类型和目标类型做相容性检查,检查不通过报错。
A.如果在源类型和目标类型之间,至少有一个方向可以做隐式类型转换,那么这两个类型就是相容类型。
short x = 10;
void* v = &x;
short* p = static_cast<short*> (v);
--------------------
short x = 10;
short* v = &x;
int* p = static_cast<int*> (v); // ERROR
B.如果从源类型到目标类型存在自定义的转换规则(类型转换构造函数/类型转换运算符函数),那么它们也可以被称为相容类型。
2)动态类型转换
dynamic_cast<目标类型> (源类型变量)
用于具有多态性的父子类型的指针或引用。
3)去常类型转换
const_cast<目标类型> (源类型变量)
去除指针或引用的常属性。
int x = 10;
int const* cp = &x;
int* p = const_cast<int*> (cp);
char* q = const_cast<char*> (cp); // 错误
char* q = (char*)cp; // OK,风险
4)重解释类型转换
reinterpret_cast<目标类型> (源类型变量)
转换任意类型的指针或引用。
在任意类型的指针和整型之间转换。
三、面向对象
1.为什么要面向对象
1)相比于分而治之的结构程序设计,强调大处着眼的面向对象程序设计思想,更适合于开发大型软件。
2)得益于数据抽象、代码复用等面向对象的固有技术,软件开发的效率获得极大的提升,成本却大幅降低。
3)面向对象技术在数据库、网络通信、图形界面等领域的广泛应用,已催生出各种设计模式和应用框架。
4)面向对象已经成为现代计算机程序设计语言的发展潮流,不但几乎所有新诞生的语言都是面向对象的,而且很多传统的结构化语言也在不断地引入面向对象的机制。
2.什么是面向对象
1)万物皆对象。
2)把大型软件看成一个由对象组成的社会。
3)对象拥有足够的智能,能够理解来自其它对象的信息,并以适当的方式作出反应。
4)对象能够从高层对象继承某些特性,并允许低层对象从自己继承某些特性。
5)编写程序的过程就是一个描述对象的过程,最终是问题域和解域获得完美的统一。
6)面向对象的四大要素:封装、继承、多态、抽象。
一、类和对象
1.通过属性和行为描述具体的对象,其中属性表示对象的静态特征,而行为则表示对象的动态特征。
2.拥有相同属性和行为的对象被分为一组,即一个类。
属性 行为
狗 犬种 进食
犬龄 睡眠
体重 玩耍
毛色
学生 姓名 吃饭
年龄 睡觉
学号 学习
手机 品牌 接打电话
型号 收发短信
价格 上网
玩游戏
3.类即逻辑抽象
1)简单类型:只能表示一个属性(变量)。
2)数组类型:可以表示多个属性(元素),但是类型必须相同。
3)结构体类型:可以多个类型不同的属性(字段),但缺少对行为(函数)的描述。
4)类类型:既可以表示多个不同类型的属性(成员变量),同时也可以表示多个不同的行为(成员函数)。
现实世界 逻辑空间 虚拟世界
小狗 -> 狗类 -> 狗对象
真实对象 抽象描述 逻辑对象
^
OOP
二、类的定义与实例化
1.类的主要内容
1)成员变量:描述对象的各种属性。
2)成员函数:描述对象的各种行为。
3)构造函数:对对象做初始化。
4)析构函数:对对象做终结化。
5)访控属性:决定成员的访问特性。
public(struct缺省) - 公有,谁都可以访问;
private(class缺省) - 私有,只有自己可以访问;
protected - 保护,只有自己和自己的子类可以访问。
6)继承方式与基类:继承。
class Student {
public:
// 吃饭
void eat (string const& food) { ... }
// 睡觉
void sleep (int time) { ... }
// 学习
void learn (string const& course) { ... }
private:
string m_name; // 姓名
int m_age; // 年龄
int m_no; // 学号
};
2.构造函数
1)函数名与类名相同,且没有返回类型。
2)构造函数在创建对象时被系统自动调用。
A.直接定义变量,栈对象;
B.用new操作符创建对象,堆对象。
3)构造函数在对象整个生命期内,一定会被调用,且仅被调用一次。
4)构造函数负责对成员变量进行初始化,分配必要的资源,设置对象的初始状态。
三、构造函数与初始化表
1.构造函数可以重载
1)构造函数通过参数表的差别化以重载的形式提供不通过的初始化方法。
2)重载的构造函数通过构造实参的类型选择匹配。
3)使用缺省参数可以减少构造函数重载版本的数量。注意避免重载冲突。
2.具有特殊意义的构造函数
1)缺省构造函数
能够以无参的方式被调用的构造函数称为缺省构造函数。
缺省构造函数表示对象的默认初始化状态。
如果一个类中没有定义任何构造函数,那么编译器就会自动为其生成一个缺省构造函数。对于基本类型的成员变量不做任何处理,而对于类类型的成员变量,调用其相应类型的缺省构造函数。
2)类型转换构造函数
凡是可以通过一个参数调用的构造函数都属于类型转换构造函数。如:
class 目标类型 {
目标类型 (源类型 const& 引用) { ... }
};
通过该构造函数将源类型的对象隐式或显式地转换为目标类型的对象。如果希望该转换必须显式完成,可以在该类型转换构造函数前面加上explicit关键字。
3)拷贝构造函数
当用一个对象构造与它同类型的副本对象时,编译器会通过拷贝构造函数完成该副本对象的初始化。如:
class 类 {
public:
类 (类 const& 引用) { ... }
};
如果一个类没有定义拷贝构造函数,编译器就会自动提供一个缺省的拷贝构造函数,该函数对于基本类型的成员变量,按照字节复制,而对于类类型的成员变量,则会调用其相应类型的拷贝构造函数,构造其副本。
多数情况下不需要自己定义拷贝构造函数,编译器提供的缺省版本已经足以满足要求。但在某些特殊情况下,需要自行定义以弥补缺省版本的不足。
拷贝构造会降低应用程序的性能,设计时尽量避免,比如通过引用传递参数。
编译器会通过必要的优化策略,减少拷贝构造的机会。通过-fno-elide-constructors选项可以关闭此优化特性。
自定义构造函数 系统定义构造函数
无 缺省构造函数
缺省拷贝构造函数
非拷贝构造函数 缺省拷贝构造函数
拷贝构造函数 无
3.初始化表
1)通过在类的构造函数中使用初始化表,指明该类的成员变量如何被初始化。
2)类的类类型成员变量,要么在初始化表中显式初始化,要么通过相应类型的缺省构造函数初始化。
3)类的常量型和引用型成员变量,必须在初始化表中显式初始化,不能在构造函数体中赋初值。
4)类的成员变量按其在类中被声明的顺序依次初始化,而与其在初始化表中的排列顺序无关。
程序员写:
Student s ("张飞", 25);
编译后的机器指令相当于如下:
从栈内存中分配sizeof (Student)字节
调用Student::Student ("张飞", 25)函数初始化
程序员写:
Student* s = new Student ("张飞", 25);
被编译器处理为:
Student* s = malloc (sizeof (Stduent));
调用Student::Student ("张飞", 25)函数初始化(s->Student ("张飞", 25)/
Student::Student (s, "张飞", 25))
一、C++对象模型
class Student {
public:
Student (char const* name, int age) {
strcpy (m_name, name);
m_age = age;
}
void print (void) {
cout << m_name << "," << m_age
<< endl;
}
private:
char m_name[256];
int m_age;
};
Student s1 ("张飞", 25);
1.C++的对象模型和C的结构模型没有任何区别,包括成员的布局,以及对齐补齐的规则。
2.对象中只有成员变量,没有成员函数。类的成员变量在该类的每个对象中都一份独立的拷贝。但是类的成员函数只有一份,且为该类的所有对象共享。
3.为了在一个函数中区分出不同的调用对象,编译器会为每个成员函数提供一个隐藏的指针参数——this指针——指向调用该成员函数的对象。在成员函数中对所有成员变量的访问,以及对其它成员函数的调用,实际上都是通过this指针完成的。
4.类的构造函数中同样具有this指针参数,指向这个正在被构造的对象。
class B;
class A {
B m_b;
};
class B {
A m_a;
};
A a; sizeof (A) ?
5.显式使用this指针的场合
1)解决成员变量的名字冲突;
2)从成员函数中返回调用对象的自引用;
3)通过成员函数实现对象间的交互;
4)在成员函数函数销毁调用对象自身。
二、常函数
1.在类成员函数的参数表之后,函数体的左花括号之前,加上const关键字,该成员函数的this指针即具有常属性,这样的成员函数被称为常函数。
2.在常函数内部,因为this指针被声明为常量指针(即目标只读的指针),所以无法修改成员变量的值,除非该成员变量被mutable关键字修饰。
3.常对象只能调用常函数,非常对象既可以调用非常函数也可以调用常函数。原型相同的常函数和非常函数可以构成重载关系。
三、析构函数
1.析构函数是类的特殊的成员函数
1)析构函数的函数名就是在类名前面加“~”;
2)析构函数没有返回类型;
3)析构函数没有参数
4)析构函数不能重载
2.析构函数在对象被销毁时自动调用
1)局部对象的析构函数由其所在最小作用域的右花括号调用;
2)堆对象的析构函数被delete运算符调用;
3)全局对象的析构函数,被进程加载器调用。
3.缺省析构函数
如果一个类没有定义析构函数,编译器提供一个缺省析构函数。该函数对于基本类型的成员变量什么也不做,而对于类类型的成员变量,则会调用其相应类型的析构函数。
四、拷贝构造和拷贝赋值
1.缺省方式的拷贝构造和拷贝赋值,对包括指针在内的基本类型成员变量按字节复制,导致浅拷贝问题。
2.为了获得完整意义上的对象副本,必须自己定义拷贝构造函数和拷贝赋值运算符函数,针对指针型成员变量做深拷贝。
3.拷贝赋值运算符函数的实现
1)避免自赋值;
2)分配新资源;
3)释放旧资源;
4)拷贝新内容;
5)返回自引用。
一、静态成员
1.属于类而非对象
1)静态成员变量不包含于对象实例中,具有进程级的声明周期
2)静态成员函数没有this指针,也没有常属性
3)静态成员函数只能访问静态成员(变量或函数),非静态成员函数既可以访问静态成员,也可以访问非静态成员。
2.静态成员也受访问控制的约束。
3.静态成员变量必须在类的外部定义或初始化,静态成员函数既可以在类的外部也可以在类的内部定义。
二、成员指针
通过类型的约束,表达指向类特定类型成员的指针。
三、操作符(运算符)重载
1.操作符标记
1)单目操作符:只有一个操作数的操作符。
-/++/--/&/*/->/!/~/()/类型转换,等等
2)双目操作符:有左右两个操作数的操作符。
算术运算:*///%/+/-
关系运算:>/>=/</<=/==/!=
逻辑运算:&&/||
位运算:&/|/^/<</>>
赋值和复合赋值:=/+=/-=/*=//=/...
下标运算:[]
int a[5] = { ... };
cout << a[3] << endl;
3)三目运算符:包含三个操作数的操作符。
条件运算:A ? B : C
2.操作符函数
1)在特定条件下,编译器有能力把一个由操作数和操作符组成的表达式,解释成为一个全局或者成员函数调用,该全局或者成员函数就被成为操作符函数。
复数(3+4i)
2)通过定义操作符函数,可以实现针对自定义类型的运算法则,并使之与内置类型具有一致的语义。
3.双目操作符
L#R
-> L.operator# (R) // 成员,左调右参
-> ::operator# (L, R) // 全局,左一右二
1)运算类:左右操作数都可以为左值或右值,表达式的值必须是右值。
友元:可以通过friend关键字,把一个函数或者类声明为一个类友元。被声明有关的函数或类可以自由访问授权类的任何私有成员。友元声明可以位于授权类的公有/私有/保护任何区域,其效果都一样。
2)赋值类:右操作数可为左值或右值,但左操作数必须是左值,表达式的值是左值且为左操作数本身(而非副本)。
3)输入输出:左操作数是ostream/istream,右操作数对于输出可以是左值也可以是右值,对于输入只能是左值,表达式的值是左操作数本身。
Complex c1 (...);
Complex const c2 (...);
cout << c1; // cout.operator<< (c1)
// ::operator<< (cout, c1)
cout << c2;
cin >> c1;
cin >> c2; // ERROR
cout << c1 << c2 << endl;
4.单目操作符
#O/O#
->O.operator# ()
->::operator# (O)
1)运算类: 操作数既可以是左值也可以是右值,操作数在运算前后不发生变化,表达式的值是右值。
-x = 10; // ERROR
(0-x) = 10; // ERROR
2)前缀类:操作数为左值,表达式的值是左值,而且就是操作数本身。运算前后操作数的值会发生变化,表达式的值是变化以后的值。
3)后缀类:操作数为左值,表达式的值是右值,而是操作数运算之前的历史备份。运算前后操作数的值发生变化,表达式的值是变化以前的值。
5.三目操作符:无法重载
6.其它操作符:下标、函数、类型转换
7.不能重载的操作符
::/./.*/?:/sizeof/typeid
8.操作符重载的限制
1)所有操作数都是基本类型的不能重载
int a = 1, b = 1;
int c = a + b; // c = 10000 ?
2)无法改变操作符的优先级
z + x ^ y -> (z + x) ^ y
3)无法改变操作符的目数
4)无法发明新的操作符
**4
s1 @ s2
5)保持操作符的语义一致性
c1 + c2
str1 + str2
一、继承的基本概念
1.共性和个性
学生:姓名、年龄、学号,吃饭、睡觉、学习
教师:姓名、年龄、工资,吃饭、睡觉、授课
---------------------------------------------
人类:姓名、年龄,吃饭、睡觉 - 共性
学生是人:学号,学习 - 个性
教师是人:工资,授课 - 个性
1)共性表达了不同类型事物之间共有的属性和行为。
2)个性则着意刻画每种类型事物特有的属性和行为。
2.超集与子集
1)超集体现了基于共性的一般。
2)子集体现了针对个性的特殊。
3.基(父)类和子类
1)基类表示超集,体现共性,描述共有的属性和行为。
2)子类表示子集,体现个性,描述特有的属性和行为。
4.继承与派生
基类: 人类(姓名、年龄、吃饭、睡觉)
派生 V / ^ 继承
子类: 学生 教师
(学号、学习) (工资、授课)
5.继承语法
class 子类 : 继承方式1 基类1, 继承方式2 基类2, ... {};
class Human {...}; // 人类
class Student : public Human {...}; // 学生
class Teacher : public Human {...}; // 教师
class Assistant : public Student, public Teacher {...}; // 助教
继承方式:
public - 公有继承,最常见的继承方式
protected - 保护继承
private - 私有继承
6.公有继承的特点
1)皆然性:子类即基类
学生是人
教师是人
任何子类对象中都包含着它的基类子对象,而且通常位于子类对象的低地址部分。因此把一个指向子类对象的指针或者引用子类对象的引用转换为其基类类型可以隐式完成。实际上指这种转换就意味着把一个子类对象的基类部分看作是一个实际的基类对象。这种类型转换亦称向上造型。
2)可访问性
子类可以访问基类中哪些成员:公有成员、保护成员。基类的私有成员不能为子类所直接访问,但是在子类中存在。
3)隐藏性
如果在子类中定义和基类同名的标识符,那么子类中标识符就会隐藏基类中的标识符。除非通过作用域限定符显式地指明所访问的标识符源自基类。
4)传导性
当子类对象被构造、析构或者复制时,其基类子对象也需要同时被构造、析构或者复制。
A->B->C
当通过delete操作一个指向子类对象的基类指针时,实际被执行的是基类的析构函数,该函数不会调用(传导)子类的析构函数,子类所特有动态资源将形成内存泄漏。
7.继承方式对访控属性的影响
考虑继承方式:通过子类访问其所继承。
class A { void foo (void) { ... } };
class B : public A {
void bar (void) {
foo(); //直接访问基类,不考虑继承方式
}
};
class C : public B {
void bar (void) {
foo (); //通过子类B访问其从基类A中
//继承的foo,需要考虑继承方式
}
};
int main (void) {
B b;
b.foo (); // 通过B访问其继承的foo
// 需要考虑继承方式
return 0;
}
基类 公子 保子 私子
公有 公有 保护 私有 //公有继承保留基类访控属性
保护 保护 保护 私有 //保护继承覆盖基类公有属性
私有 私有 私有 私有 //私有继承覆盖基类公有和保护属性
8.私有继承和保护继承
class DCT { // $1000
public:
void codec (void);
};
class Jpeg : protected DCT {
public:
void render (void) {
... codec () ...
}
};
class Jpeg2K : public Jpeg {
};
// $1
class PDF : public Jpeg {
public:
void show (void) {
... render () ...
... codec () ... // ERROR
}
};
私有继承亦称实现继承,其目的在于将基类中的公有和保护成员,在子类中私有化,防止其通过子类扩散。但是,如果希望在子类的子类可以继续访问这些成员,而只是限制在子类外部的访问,则可以将子类从基类的继承方式设置为保护继承。
保护和私有继承不具有皆然性。
二、多重继承
1.一个子类从多个基类中派生。
电话 媒体播放器 计算机
| /
智能手机
2.钻石继承问题
A
/
B C
/
D
A
/ |
B C D
/ /
E F
A
/
B D
| |
C |
/
E
1)一个子类继承自多个基类,而这些基类又源自共同的祖先(公共基类),这样的继承结构称为钻石继承。
2)派生多个中间子类的公共基类子对象,在继承自多个中间子类的汇聚子类对象中,存在多个实例。
3)在汇聚子类中,或通过汇聚子类对象,访问公共基类的成员,会因继承路径的不同而导致不一致。这种现象称为钻石继承问题。
3.虚继承
1)通过虚继承,可以保证公共基类子对象在汇聚子类对象中,仅存一份实例,且为多个中间子类对象所共享。
2)为了表示虚继承,需要在继承表中使用virtual关键字。
3)一般而言,子类的构造函数不能指定间接基类的构造方式,但是一旦这个间接基类被声明为虚基类,它的所有子类,无论是直接子类还是间接子类,都必须显式地指明该公共基类子对象的构造方式,否则系统将按照缺省方式构造该子对象。
一、类型的决定性
通过一个指针或者引用访问类的成员,编译器只是根据指针或者引用的类型决定是否可以访问该成员,而与此指针或者引用的实际目标对象无关。
二、虚函数与多态
如果将基类中的某个成员函数声明为虚函数(在其返回类型前面加上virtual关键字),那么其子类中的同型函数就也是虚函数(无论其是否带有virtual关键字),而且和基类版本形成覆盖关系。这时通过一个指向子类对象的基类指针,或者引用子类对象的基类引用,调用该虚函数,实际被执行的将是子类中的覆盖版本,而非基类的原始版本。这种现象谓之多态。
三、重载、隐藏和覆盖
重载必须在同一个作用域中。
覆盖必须是同型的虚函数。
如果不是重载也不是覆盖,而且函数名还一样,那就一定是隐藏。
四、有效覆盖的前提条件
1.只有类的非静态成员函数才能被声明为虚函数,全局函数和类的静态成员函数都不能是虚函数。
2.只有在基类中被声明为虚函数的成员函数才能在子类中覆盖。
3.虚函数在子类中的覆盖版本必须和该函数的基类版本拥有完全相同的签名,即函数名、形参表、常属性严格一致。
4.如果基类中虚函数的返回类型为基本类型或类类型的对象,那么子类的覆盖版本必须返回相同的类型。
5.如果基类中的虚函数返回类类型的指针或引用,那么该函数在子类中的覆盖版本可以返回其基类版本返回类型的公有子类的指针或引用——类型协变。
class ShapeEditor
{
……
};
class Shape
{
public:
virtual const ShapeEditor& getEditor ()const = 0; //Factory Method
};
class Circle;
class CircleEditor : public ShapeEditor
{
……
};
class Circle : public Shape
{
public:
const CircleEditor& getEditor ()const ;
};
6.子类中覆盖版本不能比基类版本说明抛出更多的异常。
7.无论基类中的虚函数位于该类的公有、私有还是保护部分,该函数在子类中的覆盖版本都可以出现在任何访控区域。
class Base {
virtual void foo (void); // 1
virtual void foo (void) const; // 2
};
class Derived : public Base {
virtual void foo (void); // 3
virtual char foo (void) const; // 4
};
1和2构成重载
3和4构成重载
3隐藏了2,覆盖了1
4隐藏了1,在试图覆盖2时出错
重载在同一个类中,覆盖在虚继承的子类实现中必须有相同的签名和返回值,否则隐藏。
五、多态的条件
多态性除了需要在子类和基类间形成有效的虚函数覆盖以外,还必须通过指针或者引用访问该虚函数。
当基类的构造函数被子类的构造函数调用时,子类对象尚不能说是子类类型的,它只表现出基类类型的外观和行为。这时调用虚函数,只能被绑定到基类版本,没有多态性。
当基类的析构函数被子类的析构函数调用时,子类对象已不再是子类类型的了,它只表现出基类类型的外观和行为。这时调用虚函数,只能被绑定到基类版本,没有多态性。
六、纯虚函数、抽象类、纯抽象类
1.形如virtual 返回类型 函数名 (形参表) [const] = 0;的虚函数成为纯虚函数。
2.至少包含一个纯虚函数的类就叫抽象类,抽象类不能实例化为对象。
3.如果一个抽象类的子类没有覆盖其基类中的全部纯虚函数,那么该子类就也是抽象类。
4.除了构造、析构和静态成员函数以外的全部成员函数都为纯虚函数的抽象类就叫做纯抽象类,亦称接口类。
七、虚函数表和动态绑定
1.对于包含虚函数的类,编译器会为其生成一张虚函数表,即存放每个虚函数地址的函数指针数组,简称虚表(vtbl),每个虚函数对应一个虚函数表的索引号。
2.当编译器看到通过指针或引用调用虚函数时,并不急于生成有关函数调用的指令,相反它会用一段代码替代该调用语句,这段代码在运行时执行,完成如下操作:
1)确定调用者指针或引用的目标对象,并从中获取到虚表指针;
2)根据所调用函数的索引号从虚表中提取相应的函数地址;
3)根据函数地址调用该虚函数。
这个过程因为是在运行时完成的,所以称为动态绑定。
3.动态绑定对性能的影响
1)虚函数表本身会增加内存空间的开销;
2)虚函数调用的时间开销会大于普通函数;
3)虚函数不能内联。
建议只有在确实需要多态性的场合才使用虚函数,否则尽量避免使用虚函数。
八、运行时类型信息(RTTI)
1.动态类型转换(dynamic_cast)
动态类型转换应用在具有多态继承关系的父子类的指针或引用之间。在运行期间检查转换源的目标对象的类型与转换目的类型是否一致,如果一致则返回实际的对象指针或引用,否则返回空指针或抛出异常。
2.typeid运算符
在运行期间动态获取对象的类型信息。
九、虚析构函数
如果将基类的析构函数声明为虚函数,那么子类的析构函数就也是虚函数,而且对基类版本构成覆盖。这时delete一个指向子类对象的基类指针,实际被执行的是子类的析构函数,该函数在释放完成子类特有的资源以后,会自动调用基类的析构函数,完成对基类资源的释放,最终释放掉所有的资源,没有内存泄漏。
十、虚与非虚
1.可以被声明为虚函数的函数
普通成员函数
成员函数形式的操作符函数
析构函数
2.不能被声明为虚函数的函数
静态成员函数
全局函数形式的操作符函数
构造函数
全局函数
一、异常
1.抛出异常:throw 异常对象;
异常对象:基本类型的变量或者类类型的对象。
throw 100;
throw "内存分配失败";
throw MemoryEerror ();
2.捕获异常
try {
可能引发异常的操作;
}
catch (异常类型1& 异常对象引用) {
对异常类型1的处理;
}
catch (异常类型2& 异常对象引用) {
对异常类型2的处理;
}
...
catch (...) {
对其它异常的处理;
}
3.异常流程
1)当代码执行到throw语句时,一方面会将所抛出的异常对象复制到系统安全区中,另一方面将流程执行到包含此throw语句的最小作用域的右花括号处,并沿着函数调用的路径,向上回溯,直到try块的右花括号处。然后根据异常对象的类型匹配一个适当的catch分支,执行其中的异常处理代码。
2)如果一个被抛出的异常没有被任何代码捕获,最终将被系统捕获,并终止进程,同时打印异常的类型信息。
3)如果一个函数没有捕获它所引发的异常,那么该异常将继续向上抛出。直到调用路径中的某个函数捕获了该异常。
4.异常说明
throw (异常类型1, 异常类型2, ...)
1)在声明函数时,可以通过异常说明,声明该函数所可能抛出的异常。该函数可以抛出异常说明以外的异常,但这些异常不能被其调用者捕获。
5.构造函数中的异常
1)在构造函数中可以抛出异常,表示在构造过程中出现的错误。
2)如果一个对象在构造过程中出现了异常,那么这个对象就会被不完整构造,而一个不完整构造的对象,它的析构函数永远不会被执行。
6.析构函数中的异常
析构函数中不要抛出异常,对可以引发异常的操作,尽量在内部捕获,不要使之被抛到析构函数外部。
7.标准库异常
#include <stdexcept>
exception : const char* what (void) const throw () = 0;
--> overflow_error - 上溢异常
--> underflow_error - 下溢异常
--> invalid_argument - 无效参数异常
--> out_of_range - 越界异常
--> bad_alloc - 内存分配失败(new)
--> bad_cast - 动态类型转换失败(引用)
--> bad_type_id - 非法类型
二、I/O流
ifstream - 输入文件流,从文件读取
ofstream - 输出文件流,向文件写入
fstream - 输入输出文件流,既可读也可写
打开->读/写/跳->关闭
operator bool (void) {}
istream& istream::read (char* buffer, streamsize num);
istream->bool : true - 读到num字节
false - 读取的字节数少于
num,或者出错
istream::gcount ()返回最近一次读取的字节数
ostream& ostream::write (
const char* buffer, streamsize num);
ostream->bool : true - 写成功
false - 写失败
istream::seekg (off_type offset,
ios::seekdir origin);
ostream::seekp (off_type offset,
ios::seekdir origin);
origin - ios::beg
ios::cur
ios::end
pos_type istream::tellg (void);
pos_type osteam::tellp (void);
STL
C/C++
静态类型
int i = 0;
char c = (char)i;
Student student ("张飞", 25);
cout << student.m_name << endl;
student.learn ("C++");
优点:效率高、安全。
缺点:灵活性差、通用性差。
i = 100;
i = "hello";
i = Student (...);
一、模板的由来
1.C/C++语言的静态类型系统在保证效率和安全性的同时,也增加了编写与类型无关的通用代码(数据结构和算法)的难度。
2.通过宏定义可以摆脱类型的约束,但同时也失去了源自类型系统的安全性保障。
3.通过宏定义实现一般化的算法表示,利用预编译器根据该宏生成针对不同类型的具体实现。
4.将预处理器生成具体函数的工作转移到编译器中——这就是模板的意义。
二、函数模板
1.定义
template<typename 模板形参1, typename 模板形参2, ...>
返回类型 函数模板名 (调用形参表) { ... }
例如:
template<typename A, typename B, typename C> A foo (B b) { C c; ...; }
2.使用
函数模板名<模板实参1, 模板实参2, ...> (调用实参表);
例如:
int a;
double b = 3.14;
a = foo<int, double, string> (b);
3.只有那些可以满足函数模板全部要求的类型才能用于实例化该函数模板。
4.二次编译
每个函数模板事实上都要被编译两次。一次是在实例化之前,先检查模板代码有无语法硬伤,如果检查通过则生成该函数模板的内部表示。另一次是在实例化该函数模板的过程中,即将函数模板变成具体函数的过程,结合所提供的模板实参再次检查模板代码的正确性,如果检查通过则生成具体函数的二进制指令。
5.隐式推断
如果函数模板调用参数的类型与该模板的模板参数相关,那么在调用该函数模板时即使不显式指定模板实参,编译器也有能力根据调用实参的类型隐式地推断出相应模板参数的类型。
6.函数模板重载
1)函数模板和普通函数一样,也可以重载,重载解析的具体规则见代码overload.cpp。
2)在重载函数模板的时候,应该尽可能地把改变限制在参数的个数或参数的具体类型上,而对于参数的引用或常属性尽量保持不变,以避免出现返回局部对象引用的情况。
三、类模板
1.类的成员变量、成员函数和基类如果包含参数化的类型,那么这个类就称为类模板。
2.定义
template<typename 模板形参1, typename 模板形参2, ...>class 类模板名 { ... };
3.使用
类模板本身并不是一个类型,只有先将类模板实例化为具体类,才能用于创建对象,声明变量。
类模板名<模板实参1, 模板实参2, ...>
类模板不能隐式推断,必须显式实例化。
4.类模板的两步实例化
实例化 实例化
类模板------>类------>对象
编译期 运行期
编译器 处理器
5.类模板所有的成员函数,包括构造函数、析构函数、运算符函数、一般成员函数、静态成员函数,无一例外地都是函数模板。
6.在类模板声明中,凡是使用类型的场合,严格的语法应该是:"类模板名<模板参数>";而对类名的引用则可以直接使用"类模板名"。
7.类模板中,只有那些被调用的成员函数才会被实例化,即产生二进制指令代码。因此某些类型虽然并没有提供类模板所需要的全部功能,但照样可以实例化该类模板,只要不直接或间接调用那些依赖于未提供功能的成员函数即可。
8.类模板的静态成员变量,在该模板的每个实例化类中各有一份独立的拷贝,并为该实例化类的所有对象所共享。
类模板->类1->对象1
->对象2
->类2->对象3
->对象4
9.类模板递归实例化
用类模板的实例化类实例化该类模板自身。
通常用这种方法表达一些在空间上具有递归特性的数据结构,如多维数组、复合链表、多叉树等。
三、类模板
9...
10.特化
1)一个针对任意类型的通用的类模板可能对于某些个别的特殊类型并不能够很好地支持,这时可以通过特化定义一个专门针对某个特定类型的实现取代通用版本,这就叫做类模板的特化(特例化)。
2)全类特化:对整个类模板进行特化。
template<>
class 类模板名<特化实参> { ... };
例如:
template<>
class Comparator<char const*> { ... };
3)成员特化:只特化类模板中那些与类型相关的成员函数。
template<>
返回类型 类模板名<特化实参>::成员函数名 (
调用形参表) [const] { ... }
例如:
template<>
char const* Comparator<
char const*>::max (void) const { ... }
4)全类特化相当于另写了一个新的类,其实现可以和通用版本完全不同,而成员特化,其声明与通用版本共享,其函数原型除了把通用版本的类型参数变成具体的特化类型以外,不能有任何不同。
11.局部特化(偏特化)
1)类模板可以被局部特化,即一方面为类模板指定特定的实现,另一个方面又允许用户对部分模板参数自行指定。
2)如果多个局部特化同等程度地匹配某个声明,那么该声明将因二义性而导致歧义错误。
3)函数模板不支持局部特化。
12.缺省参数
1)类模板的模板参数可以带有缺省值,即缺省模板参数。
2)如果某个类模板的模板参数带有了缺省值,那么它后面的所有模板参数必须都带有缺省值。
3)C++98标准规定函数模板不能带有缺省模板参数,但是C++2011标准允许函数模板带有缺省模板参数。
GCC4.6:g++ ... -std=c++0x
GCC4.8:g++ ... -std=c++11
4)模板参数的缺省值可以取前面定义的参数。
13.非类型参数
1)无论是函数模板还是类模板都可以带有数值形式的参数,即非类型参数。
2)类模板和C++2011以后的函数模板的非类型参数都可以带有缺省值。
3)传递给模板非类型参数的实参只能是常量、常量表达式、带有常属性(const)的变量,但是不能同时具有挥发性(volatile)。
4)模板的非类型参数必须是整数型,具体包括char/short/int/long/long long及其unsigned版本,还包括指针。
5)向模板传递字符串:需要用具有外部链接特性的字符数组作为非类型实参。
字面值字符串不可以。
string对象不可以。
全局指针不可以。
只有内部链接特性的字符数组不可以。
四、其它的技术细节
1.嵌套依赖
如果需要使用某个依赖于模板参数的内部类型,那么就需要在该类型前面加上typename关键字,否则编译器会将该内部类型解释为一个静态成员变量,进而导致编译错误。
class - 声明类
声明模板的
/ 类型参数
typename - 解决嵌套依赖
2.依赖于模板参数的模板成员
如果需要调用模板参数类型中的模板型成员函数,那么需要在该成员函数名前面加上template关键字,否则该函数会被编译器解释为一个普通函数,而对后面的"<>"产生编译错误。
3.在子模板中访问基模板
在子模板中访问那些在基模板中声明且依赖于模板参数的符号,应该在它们前面加上作用域限定符“基模板::”或this指针“this->”,否则编译器将只在子类和全局作用域中查找所访问的符号,而忽略基类的作用域。
4.模板形式的模板参数
5.零初始化
T t = T (); - string t = string (); //缺省构造
- int t = int (); // 整数0
- double t = double (); // 浮点0
6.类模板中可以定义模板成员函数,类模板中也可以定义虚函数,但是类模板中的虚函数不能同时又是模板成员函数。
解释:类模板的模板型成员函数不能是虚函数,即不能为类模板定义虚成员函数模板。虚函数调用机制的普遍实现需要依赖于一个名为“虚函数表”的函数指针数组。该数组在类模板被实例化的过程中生成,而此时成员函数模板尚未实例化,其入口地址和重载版本的个数,要等到编译器处理完对该函数的所有调用以后才能确定。成员函数模板的延迟编译阻碍了虚函数表的静态构建。
7.包含模型
A类 - a.h - 声明A类
- a.cpp - 实现A类
- main.cpp - 使用A类
1)优点:把模板的声明、实现和使用通过文件包含的方式约束到同一个编译单元中,使编译器看到模板被实例化的同时也能找到模板的定义,避免链接错误。
2)缺点:模板的实现源代码无法对用户保密。延长编译时间。
max.cpp // 3
|
max.h
/ |
a.cpp b.cpp c.cpp
通过预编译头可以在一定程度上提升编译速度。
容器、迭代器和泛型算法
一、容器
借助模板语法实现通用的数据结构。
二、泛型算法
以一般化的方式访问或处理容器中的数据。
三、迭代器
以一种一致且透明的方式访问任意容器中的数据。
以双向线性链表容器为例。
STL - Standard Templates Library,标准模板库
三大组件:容器、迭代器和泛型算法。
一、十大容器
1.线性容器
1)向量(vector)
2)双端队列(deque)
3)列表(list)
2.适配器容器
1)堆栈(stack)
2)队列(queue)
3)优先队列(priority_queue)
3.关联容器
1)映射(map)
2)多重映射(multimap)
3)集合(set)
4)多重集合(multiset)
一、向量
1.基本特性
1)连续存储与下标访问
2)动态内存分配
3)通过预分配内存空间降低动态内存管理的开销
4)整体性复制,支持深拷贝
int a[5] = {10, 20, 30, 40, 50};
int b[5];
// b = a; // 错误
for (int i = 0; i < 5; ++i)
b[i] = a[i];
memcpy (b, a, 5 * sizeof (int));
5)支持在任意位置的插入和删除,但是效率并不平均,插入删除的位置越靠近容器的尾端,容器中的元素个数越少,操作效率越高,反之越低。
2.实例化
#include <vector>
using namespace std;
1)创建空向量
vector<元素类型> 向量对象;
vector<int> vi;
2)指定初始大小
vector<元素类型> 向量对象 (元素个数);
vector<int> vi (5);
基本类型:用0初始化所有的元素。
类类型:用缺省构造函数初始化所有的元素。
3)指定初始大小同时指定初值
vector<元素类型> 向量对象 (元素个数,
元素初值);
vector<int> vi (5, 13);
vector<Student> vs (5,
Student ("张飞", 22));
4)通过一个已有的容器初始化
vector<元素类型> 向量对象 (起始迭代器,
终止迭代器);
int a[5] = {1, 2, 3, 4, 5};
vector<int> vi (a, a + 5);
vector<int> vi (&a[0], &a[5]);
vector<int> vi (&a[0], &a[4]); // 错误
3.元素访问
1)下标运算符:向量对象[基0索引];
2)迭代器
iterator - 正向迭代器
const_iterator - 常正向迭代器
reverse_iterator - 反向迭代器
const_reverse_iterator - 常反向迭代器
随机迭代器,连续内存容器的固有特征
可以整数做加减法运算
迭代器之间可以做"<"或">"比较运算
迭代器之间可以做减法运算
注意,任何导致容器结构发生变化操作都可能引起内存布局的改变,这时先前获取到的迭代器可能失效,需要重新获取一次再继续使用。
4.其它成员函数
push_back()
pop_back()
back()
front()
erase()
insert()
begin()/end()
rbegin()/rend()
5.泛型算法
#include <algorithm>
iterator find (iterator begin,
iterator end, value_type const& key);
查找成功返回第一个匹配元素的迭代器,失败返回end。
void sort (iterator begin, iterator end);
void sort (iterator begin, iterator end,
less cmp);
将[begin, end)范围中元素按升序排列。
6.类类型向量
元素类型需要支持深拷贝。
可能还需要支持缺省构造。
可能还需要支持小于运算或者小于比较器。
7.大小和容量
大小:实际容纳元素的个数。
容量:最多容纳元素的个数。
size() - 大小
capacity() - 容量
class Student {
char m_name[1024];
};
class Student {
Student (char* const name) :
m_name (new char[1024]) {
strcpy (m_name, name);
}
~Student {
delete[] m_name;
}
char* m_name;
};
class Student {
string m_name;
};
二、双端队列
物理结构和向量一样,也是用连续的内存空间保存数据元素,唯一区别就是前端开放,体现在多了两个成员函数:push_front/pop_front
其它方面与向量完全一致。
三、列表
1.优点:空间利用率高,随机插入删除效率高
2.缺点:随机访问效率低下
3.常用成员函数
front/push_front/pop_front
back/push_back/pop_back
insert/erase
size/clear/empty
begin/end/rbegin/rend
remove
将连续重复的元素唯一化
void unique (void);
排序
void sort (void);
拆分
void splice (iterator pos, list& ls);
将ls中的全部元素剪切到调用列表的pos之前
void splice (iterator pos, list& ls,iterator del);
将ls中的del元素剪切到调用列表的pos之前
void splice (iterator pos, list& ls,iterator begin, iterator end);
将ls中从begin到end之间的元素剪切到调用列表的pos之前
合并
void merge (list& ls);
将有序的ls合并到有序的调用列表中,保证合并后的结果依然有序。
四、堆栈
1.实例化
#include <stack>
stack<元素类型[, 底层容器类型]> 堆栈对象;
2.底层容器:vector/deque(缺省)/list
3.成员函数
push -> push_back
pop -> pop_back
top -> back
size -> size
empty -> empty
五、队列
1.实例化
#include <queue>
queue<元素类型[,底层容器类型]> 队列对象;
2.底层容器:deque(缺省)/list
3.成员函数
push -> push_back
pop -> pop_front
back -> back
front -> front
size -> size
empty -> empty
六、优先队列
1.实例化
#include <queue>
priority_queue<
元素类型[,底层容器类型[,比较器类型]]>
优先队列对象;
2.底层容器:vector/deque(缺省)
3.成员函数
push
pop - 弹出优先级最高的元素
top - 获取优先级最高的元素
size
empty
4.优先级
1)缺省情况,通过元素类型“<”运算符比较大小,以大者为优。
2)通过比较器人为规定元素的优先级
七、映射
1.通过平衡有序二叉树存放由键和值组成的一一对应序列。
2.键必须是唯一的。
3.通过pair容器封装键和值。
template<typename F, typename S>
class pair {
public:
pair (F const& f, S const& s) :
first (f), second (s) {}
F first;
S second;
};
用pair::first表示键,用pair::second表示值。:q