文章目录
- 继承与派生
- 继承的基本概念与用法
- 继承方式
- 基类与派生类类型转换
- 派生类的构造函数
- 派生类的析构函数
- 派生类成员的标识与访问
- 多态性
- 运算符重载
- 运算符重载的规则
- 双目运算符重载为成员函数
- 单目运算符重载为成员函数
- 运算符重载为非成员函数
- 虚函数
- 抽象类
- override和final
- 模板
- 泛型程序设计与C++标准模板库
- 流类库与输入和输出
- 异常处理
- 异常处理的思想
- 程序实现
- 异常处理中的构造与析构
- C++标准库异常处理
继承与派生
- 继承与派生是统一过程从不同的角度看
– 保持已有类的特性而构造新类的过程叫继承
– 在已有类的基础上新增自己的特性而产生新类的过程叫派生
- 被继承的已有类叫基类(父类)
- 派生出的新类叫派生类(子类)
- 直接参与派生出某类的基类叫直接基类
- 基类的基类甚至更高层叫间接基类
继承的基本概念与用法
class 派生类名:继承方式 基类名
{
成员声明;
}
class 派生类名:继承方式1 基类名1,继承方式2 基类名2
{
成员声明;
}
class Deprived:public base
{
public:
Deprived();
~Deprived();
}
- 派生类的构成
– 吸收基类成员
– 改造基类成员
– 添加新的成员
- 默认情况下构造函数和析构函数不作继承
- 如果在派生类中声明了一个和某基类成员同明的新成员,派生的新成员就隐藏或覆盖了外层同名成员
继承方式
- 公有继承
– 派生类的成员函数可以直接访问public和protected成员,但不能访问private成员
– 派生类的对象只能访问public成员
- 私有继承
– 派生类的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
– 基类的所有成员都不能通过派生类的对象访问
- 保护继承
– 派生类的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
– 基类的所有成员都不能通过派生类的对象访问
注:保护成员的特点是成员通过该类对象不可以访问,但被继承后可以通过派生类成员访问,是公有成员和私有成员访问权限上的折中。
基类与派生类类型转换
- 公有派生类的对象可以被当做基类的对象使用,反之则不可。
– 派生类对于对象可以隐含转换为基类对象
– 派生类的对象可以初始化基类的引用
– 派生类的指针可以隐含转化为基类的指针
- 通过基类对象名、指针只能使用从基类继承的成员
派生类的构造函数
- 基类的构造函数默认情况下不被继承
– 派生类新增成员:派生类定义构造函数初始化
– 继承来的成员:自动调用基类构造函数进行初始化
– 派生类的构造函数需要给基类的构造函数传递参数
派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(参数表),本类成员(含对象成员)初始化列表
{
}
派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名1(参数表1),基类名2(参数表2),基类名3(参数表3),……,本类成员(含对象成员)初始化列表
{
}
- 当基类有默认构造函数时,派生类构造函数可以不向基类构造函数传递参数,基类的默认构造函数被调用
- 构造顺序:首先按照继承顺序进行初始化,然后按照定义顺序执行本类成员的初始化,最后执行其他初始化
- C++11规定,可以用using语句继承基类构造函数,但是只能初始化从基类继承的成员
– using B::B;
- 派生类的复制构造函数
– 复制构造函数只能接收一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数
– 基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
C::C(const C &c1): B(c1){}
派生类的析构函数
- 派生类的析构函数不被继承
- 声明方法相同
- 不需要显示调用基类的析构函数,系统会自动隐式调用
- 基类和对象成员的析构次序与继承次序相反
派生类成员的标识与访问
- 访问从基类继承的成员
– 当派生类与基类有相同成员时:默认通过派生类对象使用派生类中的成员,否则可以使用::访问基类中被隐藏的成员
– 如果不同的基类中有同名成员,就要用类名限定
- 虚基类(慎用)
– 当派生类的基类由共同的基类所派生时,会引起冗余和不一致性的访问问题
– 虚基类用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
– 为最远的派生类提供唯一的基类成员,而不产生重复的多次复制
– 在第一次继承时就要将共同基类设计为虚基类
– 所有直接或间接继承虚基类的派生类都要在初始化成员表中调用虚基类的构造函数,否则就会调用虚基类的默认构造函数;在执行时,由最远派生类负责真正调用构造函数
class C:public B,public A
{
}
class B:virtual public Base0
{
}
class A:virtual public Base0
{
}
多态性
- 对不同对象选择不同的处理方式
- 通过绑定来实现,将一个标志符与一段代码绑定,分编译和运行时的绑定
运算符重载
运算符重载的规则
- 不能重载的运算符:. .* :: ?:
- 重载后运算符的优先级和结合性不会改变
- 重载方式
– 重载为类的非静态成员函数
– 重载为非成员函数
双目运算符重载为成员函数
- 如果要重载B为类成员函数,使之能够实现表达式oprd1 B oprd2,其中oprd1为A类对象,则B应被重载为A类的成员函数,形参类型应该是oprd2所属类型。
- 经重载后,oprd1 B oprd2相当于oprd1.operator B(oprd2)
函数类型 operator运算符(形参)
{
……
}
单目运算符重载为成员函数
- 前置单目运算符重载为成员函数
– 如果要实现重载U为类成员函数,实现U oprd,其中oprd为A类函数,则将U重载为A类成员函数,无形参
– 经重载后,oprd.operator U()
- 后置单目运算符重载为成员函数
– 如果要实现重载U为类成员函数,实现oprd U,其中oprd为A类函数,则将U重载为A类成员函数,有一个int型形参
– 经重载后,oprd.operator U(0)
Clock & Clock::operator++()
{
a++;
return *this;
}
Clock Clock::operator++(int)
{
Clock old = *this;
++(*this);
return old;
}
运算符重载为非成员函数
- 函数的形参代表依自左至右排列的各操作数
- 参数个数 = 原操作数个数(++、- -除外)
- 至少应该有一个自定义类型的参数
- 后置单目运算符应该至少有一个形参int,不必写形参名
- 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元
class Complex
{
public:
friend ostream &operator<<(ostream &out, Complex &c);
private:
int imag;
int real;
}
ostream &operator<<(ostream &out, Complex &c)
{
out<<c.real<<"+"<<c.imag<<"j";
return out;
}
虚函数
- 目的:实现动态绑定,通过虚函数实现运行时的多态,在派生类中实现对基类中成员函数的覆盖
- 用virtual关键字在定义类成员时说明
- 类外实现定义
- 必须是非静态的函数体,虚函数经过派生之后就可以在运行时实现多态
- 一般成员函数和析构函数可以是虚函数,构造函数不能是虚构函数
class A
{
public:
virtual void display();
……
}
void A::display()
{
……
}
class B:public A
{
public:
virtual void display();
……
}
void B::display()
{
……
}
class C:public B
{
public:
virtual void display();
……
}
void C::display()
{
……
}
void total(A *ptr)
{
ptr -> display;
}
int main()
{
A a1;
B b1;
C c1;
total(&a1);
total(&b1);
total(&c1);
return 0;
}
- 虚表与动态绑定
– 这是虚函数得以实现上述实例功能的机制
– 个人理解:学过汇编语言的都知道,在汇编语言中有跳转或调用指令,而具体跳转到哪一行、调用哪一个函数是编写时根据标识符确定的。加入virtual后在汇编阶段不会生成这一指令提前指定下一步具体执行哪一行指令,而是通过虚表根据参数查找后确定
抽象类
- 纯虚函数:在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本
– 纯虚函数的出现是因为某些信息不具体,不能确定具体实现方法
– 可以规范整个类家族的统一接口
– 纯虚函数的声明格式为 virtual 函数类型 函数名(参数表) = 0
– 抽象类只能作为基类来使用,不能定义抽象类的对象
override和final
- override
– 多态行为的基础:基类声明虚函数,派生类声明一个函数覆盖虚函数
– 覆盖要求:函数签名完全一致(signature)
– 函数签名包括:函数名,参数列表和const
– override会检查基类是否存在虚拟函数,与派生类中的函数具有相同签名,若不存在,则会报错
- final
– 用来表示类中的函数成员被继承后不要被覆盖、修改
class Base1
{
public:
virtual void f() final;
……
}
class Base2
{
public:
void f();
}
- 虚基类的作用:去除继承和派生中的二义性和存储冗余问题
- 虚函数的作用:为了使用基类的指针访问派生类函数成员
模板
函数模板
template <模板参数表>
函数定义
- 模板参数表内容:
– 类型参数:class或template标识符
– 常量参数: 类型说明符 标识符
– 模板参数:template <参数表>class 标识符
类模板
template <模板参数表>
class 类名
{
……
}
template <模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数表)
template <typename T>
class A
{
private:
T item;
public:
T &getItem();
void putItem(const T& a);
A();
~A();
}
template<typename T>
A<T>::A()
{
……
}
template<typename T>
T &A<T>::getItem()
{
……
}
A<int> a;
泛型程序设计与C++标准模板库
泛型程序设计与STL结构
-
基本概念:
– 编写不依赖于具体数据类型的程序
– 将算法从特定的数据结构中抽象出来而通用
– C++的模板为泛型程序设计奠定了关键的基础
-
概念:用来界定具备一定功能的数据类型(如Comparable,sortable,Assignable)
– 如果对于两个不同的概念A和B,A所需求的所有功能也是B所需求的功能,那么就说概念B是概念A的子概念
-
模型:符合一个概念的数据类型称为改概念的模型
-
为概念赋予一个名称,并使用该名称作为模板类型名
-
标准模板库(Stand Template Library,STL)
– 定义了一套概念体系,为泛型程序设计提供了逻辑基础
– STL中的各个类模板、函数模板的参数都是用这么体系中的概念来规定的
– STL模板的类型参数既可以是C++标准库中已有类型,也可以是自定义类型
– 基本组件:容器、迭代器、函数对象和算法
– 使用STL中提供的或自定义的迭代器和函数对象,配合STL算法,可以组合出各种各样的功能
容器
- 容器(container):容纳、包含一组元素的对象
– 基本容器类模板:顺序容器、有序关联容器、无序关联容器
– 容器适配器
- 通用功能:
– 用默认构造函数构造空容器
– 支持关系运算符
– begin(),end()
– clear()
– empty()判断容器是否为空
– size()得到容器元素个数
– s1.swap(s2)容器互换
- 相关数据类型
– S::iterator:指向容器类型S元素的迭代器
- STL为每个可逆容器提供了逆向迭代器
– rbegin(), rend()
– S::reverse_iterator()指向容器S的元素的逆向迭代器类型
– 随机访问容器:s[n]获得容器的第n个元素
- 列表(list)的拼接:s1.splice(p,s2,q1,q2)将s2中的[q1,q2)移动到s1中p所指向元素之前
1.顺序容器
- 类型:vector, deque(双端队列), list(列表), forward_list(单向链表), array
- 顺序容器的接口(除array和forward_list类型外)
– 构造函数
– 赋值函数assign
– 插入函数insert, push_front(list, deque), push_back, emplace, emplace_front
– 删除函数:erase, clear, pop_front(list, deque), pop_back, emplace_back,
– 首尾元素的直接访问:front, back
– 改变大小:resize
#include
#include
#include
template<class T>
void printContainer(const T&s)
{
copy(s.begin(), s.end(), ostream_iterator<int>(cout," "));
cout<<endl;
}
void main()
{
deque<int> a;
for(int i = 0;i < 10; i++)
{
int s;
cin>>s;
a.push_front(s);
}
deque<int>::iterator iter = a.begin();
*iter;
printContainer(a);
list<int> l(a.rbegin(), a.rend());
printContainer(l);
}
#include
#include
#include
#include
int main()
{
istream_iterator<int> i1(cin),i2;
vector<int> v(i1,i2);
sort(v,begin(),v.end());
deque<int> s;
for(vector<int>::iterator item = v.begin(); item != v.end(); item++)
{
……
}
copy(s.begin(), s.end(), ostream_iterator<int>(cout," "));
return 0;
}
- 顺序容器的插入迭代器
– 用于向容器头部、尾部或中间指定位置插入元素的迭代器
– front_inserter, back_inserter, inserter
list<int> s;
back_inserter item(s);
*(item++) = 5;
#include
#include
#include
int main()
{
stack<char> s;
string str;
cin>>str;
for(string::iterator iter = str.begin(); iter != str.end(); iter++)
{
s.push(*(iter));
}
while(!s.empty())
{
cout<<s.top();
s.pop();
}
cout<<endl;
return 0;
}
2.关联容器
- 每个关联容器都有一个key,可以据此高效查找元素
- 接口:
– 插入insert
– 删除erase
– 查找find
– 定界 lower_bound, upper_bound, equal_bound
– 计数count
- 分类:
– 单重和多重关联容器(set,multiset),简单和二元关联容器(map,multimap)
– 无序关联容器
- set集合:有序无重复,元素由键key组成
- map映射:由键key和附加数据组成
迭代器
- 迭代器(Iterators)是算法和容器的桥梁
– 泛型指针,顺序访问容器中各个元素;指针本身就是一种迭代器
– 将迭代器作为算法的参数、通过迭代器来访问容器而不是将容器直接作为算法参数
– 将函数对象作为算法的参数而不是将函数所执行的运算作为算法的一部分
– 使用“++”运算符获得指向下一个元素的迭代器;使用“*”运算符访问迭代器所指向元素;有些迭代器支持使用“–”运算符获得上一个元素的迭代器
– 包含头文件
- 算法与容器独立,算法通过迭代器间接操作容器
- 输入流和输出流适配器
– istream_iterator:以输入流(如cin)为参数构造,可以通过*(p++)获得下一个输入元素
– ostream_iterator:构造时需要提供输出流(如cout),可以用*(p++) = x将x输出到输出流
– 二者都属于适配器,为流对象提供了迭代器的接口
- 迭代器的分类:输入、输出、前向、双向、随机访问迭代器
- 两个迭代器表示一个区间,STL算法常以迭代器的区间作为输入,传递输入数据,区间左闭右开
- advance(p, n)对迭代器p执行n次自增操作
- distance(first, end),计算两个迭代器之间的距离
#include
#include
#include
#include
template<class T, class InputIterator, class OutputIterator>
void mySort(InputIterator first, InputIterator end, OutputIterator result)
{
vector<T> s;
for(; first != end; first++)
{
s.push_back(*first);
}
sort(s.begin(),s.end());
copy(s.begin(),s.end(),result);
}
int main()
{
double a[4] = {1,2,3,4};
mySort<double>(a, a+4, ostream_iterator<double>(cout, " "));
cout<<endl;
mySort(istream_iterator<int>(cin),istream_iterator<int>(),ostream_iterator<int>(cout," "));
cout<<endl;
return 0;
}
函数对象
- 函数对象(function object)
– 一个行为类似函数的对象,可以像调用函数一样调用
– 函数对象是泛化了的函数,任何普通函数和任何重载了()运算符的类的对象都可以作为函数对象使用
– 包含头文件
- 函数适配器
– 绑定适配器:bind1st, bind2nd
将n元函数对象的指定参数绑定为一个常数,得到n-1元函数对象
– 组合适配器:not1,not2
将指定谓词的结果取反
– 函数指针适配器:ptr_fun
将一般函数指针转换为函数对象,使之能够作为其他函数适配器的输入,用以解决参数绑定和其他转换时遇到的问题
– 成员函数适配器:ptr_fun(对象的参数是对象的指针), ptr_fun_ref(对象的参数是对象的引用)
对成员函数指针使用,把n元成员函数适配为n+1元函数对象,该函数对象的第一个参数为调用该成员函数的目的对象,这样一来方便函数对象的使用。
如:object -> method(arg)转换为method(object, arg)
算法
- 算法
– 可以广泛用于不同对象和内置的数据类型,独立于数据类型和容器
– 包含70多种算法
– 包含头文件
- 通过迭代器获得输入数据
- 通过函数对象对数据进行处理
- 通过迭代器将结果输出
- 不可变序列算法
– find_if(InputIterator first, OutputIterator last, UnaryPredicate pred)
- 可变序列算法
– find_if(InputIterator first, OutputIterator last, const T& c)
- 排列和搜索算法
– sort(InputIterator first, OutputIterator last, UnaryPredicate comp)
- 数值算法
– partial_sum((InputIterator first, OutputIterator last, BinaryFunction op)
流类库与输入和输出
I/O流的概念及流类库结构
- 流是信息流动的一种抽象,负责在数据的生产者和消费者之间建立一种联系
- 过程
– 程序建立一个流对象
– 指定这个流对象与某个文件(包括屏幕、键盘等)对象建立连接
– 程序操作流对象
– 流对象通过文件系统对所连接的文件对象产生作用
输出流
- 预先定义的输出流对象
– cout
– cerr标准错误输出,没有缓冲,发送给他的内容立即被输出
– clog类似于cerr,但有输出,缓冲区满才输出
– 默认情况下他们输出到标准输出程序,也可以定向到某个文件中去,将错误信息和正常信息分开就可以分开显示
ofstream fout("b.out");
streambuf* pOld = cout.rdbuf(fout.rdbuf());
cout.rdbuf(pOld);
ofstream myFile("fileName");
ofstream myFile;
myFile.open("fileName");
ofstream myFile("filename",ios_base::out | ios_base::binary);
- 文件输出流类成员函数
– open
– put 把一个字符写到输出流中去
– write把内存中的一块内容写到一个文件输出流中去
– seekp(移动)和tellp(返回当前指针位置)操作文件流的内部指针
– close
– 错误处理函数
- 操纵符(manipulator)
– setw和width只影响跟在其后的输出项,但其他流格式操纵符会保持有效直到发生改变
– setiosflags(arg) resetiosflags(arg)
– ios_base::left ios_base::fixed (配合setprecision()用来设置小数点后的位数)、ios_base::scientific(配合setprecision()用来设置小数点后的位数,科学计数法)
#include
#include
cout.width(10);
cout<<"asc"<<endl;
cout<<setw(10)<<"width"<<endl;
cout<<setiosflags(ios_base::left)<<setw(10)<<"width"<<resetiosflags(ios_base::left)<<"width"<<endl;
#include
……
Date d = {……};
stream file("f.dat",ios_base::binary);
file.write(reinterpret_cast<char *> (&d),sizeof(d));
file.close();
……
- 向字符串中输出
– 将内存中字符串的空间输出流
– 作用是可以将数值内容转换为字符串
– ostringstream不支持open和close操作
#include
#include
#include
int main()
{
int v = 5;
ostringstream os;
os << v;
string s = os.str();
cout<<str<<endl;
}
输入流
- 输入流相关函数
– open
– get 从输入文件中提取信息,包括空白符
– getline,可以从读取内容中删除终止字符
– read读取二进制文件的字节到一个指定的内存区域,由长度参数确定要读的字节数
– seekg用来设置文件输入流中读取数据位置的指针
– tellg返回当前文件读指针的位置
– close
#include
using namespace std
int main()
{
char ch;
while((ch = cin.get()) != EOF)
cout.put(ch);
return 0;
}
#include
#include
#include
int main(int argc, char* argv[])
{
int i;
string str = "5";
ostringstream os(&str);
os >> i;
cout<< i <<endl;
return 0;
}
异常处理
异常处理的思想
- 某个模块将出现的异常向它的调用者抛出,可一直向上级抛出,直到遇到我们所设计的处理异常的调用者
- 不会干扰程序执行的主逻辑
程序实现
- 若有异常则通过throw创建一个异常对象并抛掷
- 将可能抛出异常的程序段嵌在try块中。通过正常的顺序执行到达try语句,然后执行try内的保护段
- 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后的最后一个catch子句后面的语句继续执行
- catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛出异常)
- 如果匹配的处理器未找到,则库函数terminate将被启动调用,其默认是调用abort终止程序
……
throw 表达式;
……
try
复合语句
catch(异常声明)
复合语句
catch(异常声明)
复合语句
……
- 异常接口声明
– 一个函数显式声明可能抛出的异常,就可以提前做好准备
– void fum() throw(A,B,C,D)
– 若无异常接口声明,则此函数可以抛出任何类型的异常
– 不抛掷异常的函数声明为throw()
异常处理中的构造与析构
- 一旦抛出异常,try块后的内容就不执行了
- 这种情况会带来资源未释放等问题
- 自动的析构:
– 找到一个匹配的catch异常处理后,将从对应的try块之后到异常被抛掷之间构造且尚未析构的所有对象自动析构。
C++标准库异常处理
- 标准异常类的基础
– exception标准程序库异常类的公共基类
– logic_error可以在程序中被预先检测的异常
– runtime_error表示难以被预先检测的异常
invalid_argument是exception的派生类,这里在主函数中笼统用exception捕获