(复习课)
一、面向对象的三大特征
封装、继承和多态
静态多态:函数重载
动态多态:
泛式/泛型编程:以不变的代码,实现多种功能。
二、多态
【1】函数重写(override)
复习重载:
函数名相同
作用域相同
参数不同
函数重写发生在父子类之间,指的是,在子类中重写父类的函数,
要求父子类中的函数:函数名相同,参数列表也相同------>函数声明相同。
只有虚函数能进行重写(virtual)
【2】虚函数(virtual)
在定义成员函数时,在函数前面加上virtual,那么该函数就是虚函数
只要父类中某个函数被定义为了虚函数,后面所有继承自该类的该成员函数都是虚函数
虚函数可以在子类中对父类继承下来的虚函数进行重写,如果不重写使用的还是父类中的函数
体现出函数重写的条件(父类的指针指向子类的成员)

虚指针,是从父类继承下来的,虚指针指向虚函数表
虚函数表:只要类中有虚函数都会给该类提供一个虚函数表,每个类有不同的虚函数表
#include <iostream>
using namespace std;
class Person
{
//对于有虚函数的类,会提供一个虚指针,指向虚函数表
private:
string name;
int age;
public:
Person(string name,int age):name(name),age(age){}
virtual void show()
{
cout << "name=" << name << "\t" << "age=" << age << endl;
}
};
class Stu:public Person
{
private:
//string name;
//int age;
int score;
public:
Stu(int s,string name,int age):Person(name,age),score(s){}
void show()override
{
cout << "score= " << score << endl;
}
};
int main()
{
Stu s1(100,"zhangsan",18);
//父类的指针指向子类的对象
Person *p = &s1;
p->show(); //如果不加virtual访问到的是Person中的show
//如果加上virtual关键字,访问到的是Stu中的show
// cout << sizeof(Person) << endl;
// cout << sizeof(Stu) << endl;
s1.Person::show();
return 0;
}
练习:
1、定义一个基类Shape和派生类Circle(圆)、Rec(长方形),基类有一个虚函数void show_area(),要求子类重写该虚函数,实现输出对应类型图形的面积,给每一个类提供构无参构造和有参构造。(写一个全局函数,实现根据传进来的图形不同,调用不同图形类中的函数)
#include <iostream>
using namespace std;
class Shape
{
private:
string name;
public:
Shape(){}
Shape(string name):name(name){}
virtual void show_area()
{}
};
class Rec:public Shape
{
private:
int len;
int wid;
public:
Rec(){}
Rec(string name,int len,int wid):Shape(name),len(len),wid(wid){}
//重写父类的虚函数
void show_area()override
{
cout << "Rec的面积=" << len*wid << endl;
}
};
class Circle:public Shape
{
private:
float r;
float PI;
public:
Circle(){}
Circle(string name,float PI,float r):Shape(name),PI(PI),r(r){}
//重写
void show_area()
{
cout << "Cir的面积=" << r*r*PI << endl;
}
};
//全局函数,使用父类的指针接收子类的对象
void fun(Shape *p)
{
p->show_area();
}
//父类的引用接收子类的对象
void fun(Shape &p)
{
p.show_area();
}
int main()
{
Rec r1("chang",8,9);
fun(&r1);
// r1.show_area();
Shape *p = &r1;
Circle c1("yuan",2,3.14);
fun(c1);
// c1.show_area();
return 0;
}
作业:
全局变量,int monster = 10000;定义英雄类hero,受保护的属性string name,int hp,int attck;公有的无参构造,有参构造,虚成员函数 void Atk(){blood-=0;},法师类继承自英雄类,私有属性 int ap_atk=50;重写虚成员函数void Atk(){blood-=(attck+ap_atk);};射手类继承自英雄类,私有属性 int ac_atk = 100;重写虚成员函数void Atk(){blood-=(attck+ac_atk);}实例化类对象,判断怪物何时被杀死。
#include <iostream>
using namespace std;
//怪兽的血量
int blood = 10000;
class Hero
{
protected:
string name;
int hp;
int attck;
public:
Hero(){}
Hero(string name,int hp,int attck):name(name),hp(hp),attck(attck){}
virtual ~Hero(){cout << "Hero的析构函数" << endl;}
virtual void Atk()
{
blood-=0;
}
};
class Mage:public Hero
{
int ap_atk = 50;
public:
Mage(){}
Mage(string name,int hp,int attck):Hero(name,hp,attck){}
void Atk()override
{
blood-=(attck+ap_atk);
}
};
//射手类
class Shooter:public Hero
{
int ac_atk=100;
public:
Shooter(){}
Shooter(string name,int hp,int attck):Hero(name,hp,attck){}
//重写父类的Atk函数
void Atk()override
{
blood-=(attck+ac_atk);
cout << "射手的成员函数" << endl;
}
~Shooter()
{cout << "Shooter的析构函数" << endl;}
};
int main()
{
// //实例化法师类对象
// Mage m1("daji",100,1000);
// //实例化射手类对象
// Shooter s1("houyi",100,2000);
// int count = 0;
// while(blood>0)
// {
// m1.Atk();
// if(blood>0)
// {
// s1.Atk();
// }
// count++;
// }
//体现出函数重写
Hero *p = new Shooter("liyuanf",2000,90);
p->Atk();
delete p;
return 0;
}
整理思维导图
【3】虚析构函数
函数重写的现象是通过父类指针,指向子类成员得到的。
当释放父类指针时,父类指针能访问的空间只有父类的部分,不能访问子类的空间,所以在使用delete释放时,默认只能释放调父类的空间,会造成内存泄漏
给父类的析构函数添加关键字virtual,把父类的析构函数编程虚析构函数,在释放空间时,父类的析构函数就会引导编译器释放掉子类的空间。
只要父类的析构函数是虚析构函数,派生类/子类的虚构函数也都是虚析构函数
所以建议大家,在写析构函数时,都直接写成虚析构函数。
三、纯虚函数和抽象类
【1】纯虚函数
前面写的虚函数:virtual void show(){}
纯虚函数:virtual void show()=0; //直接让虚函数=0就是纯虚函数
【2】抽象类
含有纯虚函数的类就叫抽象类,抽象类不能实例化类对象
纯虚函数在子类中必须重写,如果不重写,子类也无法实例化类对象
#include <iostream>
using namespace std;
//animal类不能实例化类对象
class animal
{
string colour;
int age;
public:
virtual void show() = 0;
};
class Cat:public animal
{
public:
//对父类的纯虚函数重写,但是函数体没有意义
void show()
{
}
};
int main()
{
//animal a1; 包含纯虚函数的类是抽象类不能实例化对象。
Cat c1;
c1.show();
return 0;
}
练习
1、定义包含纯虚函数的抽象类animal,私有成员string name、int age,公有的纯虚函数speak(),派生子类Cat、Dog,并重写父类中的speak函数,实现输出他们的叫声,要求(使用全局函数实现对不同类中speak函数的调用)
四、C++中的异常处理
【1】异常概念
C++中的异常指的是程序在运行阶段出现的问题,没有语法错误,存在逻辑问题。
遇到异常时,可以手动抛出异常的现象,(throw)。
【2】异常处理
抛出异常 throw ----->抛出异常的位置一定要在发生异常之前
尝试捕获和处理异常 try catch
注意:
抛出异常的位置一定要在发生异常之前
try···catch中存放的是所有可能发生异常的代码
catch可以直接通过抛出异常的类型来判断是哪一种异常情况
如果同一类型有多种情况,可以在catch里,对具体获取到的异常的数值再进行判断
#include <iostream>
using namespace std;
void fun(int a,int b)
{
if(b==0)
{
//throw typename(数值)
//throw 数据 ----->throw后面直接加数据,编译器会根据数据来自动匹配类型
throw double(1);
}
else if(b==4)
{
throw double(2);
}
else if(b==2){
throw string("1");
}else {
cout << a/b << endl;
}
}
//try···catch
int main()
{
//try是写在主调函数中,
//所有可能发生异常的情况都要写在try中,如果某一条语句遇到异常,try后面的其他语句不会执行
try
{
fun(1,0); //如果这条语句发生异常,try后面的语句都不会执行
fun(8,3);
}catch (double a) //能够捕获到所有的double类型的异常,可以通过异常的数值再做不同的处理
{
if(a==1)
{
cout << "除数不能为0" << endl;
}else if(a==2)
{
cout << "这是个测试" << endl;
}
}catch (string) //可以捕获到所有string类型的异常fun(1,2) ---->b==2抛出string异常
{
cout << "这是个测试" << endl;
}
return 0;
}
五、模板(template)
模板也可以实现多态,是泛式编程的一种
实现的是,静态多态,编译时多态。
泛式编程:用相同的代码,实现不同的功能
【1】模板函数
函数重载: ----->实现(编译时)多态
函数重写: ----->实现(运行时)多态
模板函数
实际开发的过程中,对于同一个函数,往往可以传多个不同的数据类型,需要实现多个,使用模板函数可以解决这个问题
模板函数根据调用时传的参数,来具体实现函数的功能
格式:template ----->定义了一个模板,模板可以接收两个不同的数据类型
模板函数的调用方式:1、隐式调用(不给出模板中数据类型的调用方式)。2、显式调用(调用时给出模板中的数据类型) 函数名<数据类型1,数据类型2···>(形参)
一个模板只能定义一个模板函数
函数的返回值类型,并不能获取到模板中
i)模板函数需要显性调用的时机
当模板提供了两种数据类型,但是模板函数只有一种数据类型时,需要在函数调用处显性调用模板函数:函数名<数据类型1,数据类型2···>(形参) -----> <>对应的是模板,()对应参数
对于实参是字符串常量,想要不强转实现函数调用,需要显性调用模板函数
#include <iostream>
using namespace std;
//int add(int a,int b)
//{
// return a+b;
//}
//定义了一个模板
template <typename T1>
//T1 add(T1 a,T2 b) 解决方式1
T1 add(T1 a,T1 b)
{
return a+b;
}
//一个模板只对一个函数生效
template <typename T1>
T1 arg(T1 a,T1 b)
{
return (a+b)/2;
}
int main()
{
int x = 10,y = 90;
//cout << add<int,double>(x,y) << endl;
//add<int,int>(x,y); //解决办法2:显性调用
//cout << add<double,int>(10.9,6) << endl;
//对于add函数不能直接传"hello",因为识别成const char*,不能相加,需要显性强转成string
//cout << add(string("hello"),string(" world")) << endl;
//如果项直接在模板函数中,使用字符串作为参数传递还可以显性调用模板函数
cout << add<string>("hello ","world") << endl;
arg(10.8,4.0);
arg(10,4);
return 0;
}
练习:
1、使用模板函数,实现两个数求平均值的操作,要求:函数返回平均值,在主函数内输出结果
2、使用模板函数,实现两数求最大值的操作,直接在函数内输出结果,不需要返回。
//一个模板只对一个函数生效
template <typename T1,typename T2>
T2 arg(T1 a,T1 b)
{
return (a+b)/2.0;
}
template<typename T1>
void max_(T1 a,T1 b)
{
cout << (a>b?a:b) << endl;
}
ii)模板函数的特化
#include <iostream>
using namespace std;
//定义一个模板
template <typename T1,typename T2>
//实现特化的模板函数---->指明部分数据类型
double arg(int a,double b)
{
return (a+b)/2.0;
}
//模板函数的特化,先在模板的位置,就把数据类型特化出来了
template <typename T1 = int ,typename T2>
int fun(int a,T2 b)
{
return a+b;
}
//在模板声明中,class的作用表示,该数据类型可以使用默认值
//typename应用场合比较多
template <class T1=int,typename T2=double>
int fun_c(T1 a,T1 b)
{
return a+b;
}
int main()
{
//必须显性的调用特化的模板函数
cout << arg<int,double>(12,3.0) << endl;
int x = 9;
cout << fun(10,x) << endl;
cout << fun_c<int,double>(12,9) << endl;
//显性调用特化过的模板函数arg
cout << arg<int,double>(10,4.0) << endl;
return 0;
}
【2】模板类
在开发过程中,对于同一功能的类,由于数据类型的不同,可能会出现重复定义的现象(链表中的节点)
可以通过模板解决
如果定义模板类,只能显性调用
如果模板类中的函数,想要实现类内声明,类外定义,需要在定义函数的位置重新写一遍模板
#include <iostream>
using namespace std;
//模板可以实现泛式编程:用相同的代码实现不同的功能
template<typename T1>
class A
{
T1 age;
public:
A(T1 age):age(age){}
//类内声明
void show();
};
//类外定义,需要再次写出模板并显性调用
template<typename T1>
void A<T1>::show()
{
cout << age << endl;
}
int main()
{
A<int> a1(100);
a1.show();
return 0;
}
【3】思考,模板函数和模板类实现的机制
顺序结构
分支结构
循环结构
二次编译/延时编译
在编译过程中,第一次遇到模板时,之进行语法分析,并不实现,在继续编译到模板调用时,会根据得到的类型,再次对模板部分的代码编译,并实现模板类和模板函数。
六、C++中的类型转换
reinterpret_cast(原有的变量)---->通配格式
C++中仍然支持C语言的强制类型转换
const_cast,把常量指针的常属性取消
static_cast,类型转换,基本所有的类型都可以使用,类似于C中的类型转换
dynamic_cast,把父类指针,转换成子类指针 ---->依托于多态实现,一定发生在父类和子类的指针之间,转换失败返回空地址
reinterpret_cast,重新解释数据的类型,不建议使用,不会检查任何类型匹配问题
#include <iostream>
using namespace std;
//怪兽的血量
int blood = 10000;
class Hero
{
protected:
string name;
int hp;
int attck;
public:
Hero(){}
Hero(string name,int hp,int attck):name(name),hp(hp),attck(attck){}
virtual ~Hero(){cout << "Hero的析构函数" << endl;}
virtual void Atk()
{
blood-=0;
}
};
class Mage:public Hero
{
int ap_atk = 50;
public:
Mage(){}
Mage(string name,int hp,int attck):Hero(name,hp,attck){}
void Atk()override
{
blood-=(attck+ap_atk);
}
};
//射手类
class Shooter:public Hero
{
int ac_atk=100;
public:
Shooter(){}
Shooter(string name,int hp,int attck):Hero(name,hp,attck){}
//重写父类的Atk函数
void Atk()override
{
blood-=(attck+ac_atk);
cout << "射手的成员函数" << endl;
}
~Shooter()
{cout << "Shooter的析构函数" << endl;}
};
int main()
{
// //实例化法师类对象
// Mage m1("daji",100,1000);
// //实例化射手类对象
// Shooter s1("houyi",100,2000);
// int count = 0;
// while(blood>0)
// {
// m1.Atk();
// if(blood>0)
// {
// s1.Atk();
// }
// count++;
// }
//体现出函数重写
//Hero *p = new Shooter("liyuanf",2000,90);
// Hero *p = new Hero("liyuanf",2000,90);
// cout << "p=" << p << endl; //0x48
// Shooter *p1 = static_cast<Shooter *>(p);
// cout << "p1=" << p1 << endl; //0x48
// Shooter *p2 = dynamic_cast<Shooter *>(p);
// cout << "p2=" << p2 << endl; //0
//因为dynamic_cast可以判断是否能成功的强转,如果强转失败,dynamic_cast返回空地址
const int a = 90;
int a2 = a; //不存在类型的问题,因为不涉及到对常量a的修改
int *p3 = const_cast<int *>(&a); //const_cast取消常属性
//reinterpret_cast<new type>(原有的变量)
char *p4 = reinterpret_cast<char *>(a); //reinterpret_cast重新解释数据类型
return 0;
}
七、lambda表达式
lambda表达式是C++11开始支持的特性
lambda表达式,多用于实现轻量级的函数
定义:[]()mutable->返回值{函数体};
[捕获列表](参数列表)mutable->返回值{函数体};
1、[捕获列表]:获取外部数据
[=]:按值捕获外部的所有变量
[&]:按引用捕获外部的所有变量
[=,&变量名]:引用捕获指定变量,其余的都按值捕获
[&,变量名] :值捕获指定变量,其余按引用捕获
[num1,&num2,num3]:num1和num3按值捕获,num2引用捕获
2、(参数列表):和普通的函数一致
3、->返回值类型:和普通的函数一致
4、函数体:和普通的函数一致
应用:
#include <iostream>
using namespace std;
int main()
{
//auto类型接收lambda的结果
//使用lambda表达式,实现求最大值的代码
auto fun=[](double a,double b)->int{return a>b?a:b;};
//fun(3,8)调用lambda表达式
cout << fun(3.8,8.9) << endl;
int num1 = 90,num2 = 79,num3=100,num4=78;
auto fun1=[&,num3]()mutable->int{
//num1++; 捕获列表获取外部变量时,默认自带常属性
num1++;
return num1+num2;};
cout << fun1() << endl;
cout << num1 << endl;
return 0;
}
八、STL标准模板库
【1】C++中的标准模板库STL
C++ Standard Template Library
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如queues(队列),lists(链表), 和stacks(栈)等.
C++ STL 提供给程序员以下三类数据结构的实现:
顺序结构
C++ Vectors
C++ Lists
C++ Double-Ended Queues
容器适配器
C++ Stacks
C++ Queues
C++ Priority Queues
联合容器
C++ Bitsets
C++ Maps
C++ Multimaps
C++ Sets
C++ Multisets
程序员使用复杂数据结构的最困难的部分已经由STL完成.
【2】Vectors
Vectors 包含着一系列连续存储的元素,其行为和数组类似。访问Vector中的任意元素或从末尾添加元素都可以在常量级时间复杂度内完成,而查找特定值的元素所处的位置或是在Vector中插入元素则是线性时间复杂度。
类似于数据结构中的顺序表
使用Vectors标准模板库,需要导头文件#include
常用的函数:
1、vector();
vector( size_type num, const TYPE &val );
vector( const vector &from );
vector( input_iterator start, input_iterator end );
2、求实际的大小:size_type size();
3、求最大容量:size_type capacity();
4、弹出队尾元素:void pop_back();
5、插入元素:void push_back(const TYPE &val );
6、访问:TYPE at( size_type loc ); at会检查越界问题,并且at返回的是指定位置元素的引用
7、使用insert进行插入:iterator insert( iterator loc, const TYPE &val );
8、返回第一个元素的迭代器:iterator begin();
9、清空所有元素:void clear();
10、对Vector的判空:bool empty();
11、返回起始位置的引用:TYPE front();
12、返回末尾位置的引用:TYPE back();
13、返回末尾位置下一个位置的迭代器: iterator end();
14、对Vector中的元素赋值,使用数据范围赋值: void assign( input_iterator start, input_iterator end );
代码:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
//创建了一个int类型的vector
vector<int> vec1(5,1);
// vec1.clear();
//vector的判空函数
// if(vec1.empty())
// {
// cout << "1" ;
// }
// else {
// cout << "!" << endl;
// }
cout << "实际的大小" << vec1.size() << endl;
cout << "最大容量" << vec1.capacity() << endl;
//使用pop_back函数实现,弹出最后一个元素
vec1.pop_back();
cout << "实际的大小" << vec1.size() << endl; //4
cout << "最大容量" << vec1.capacity() << endl; //5
//添加元素,不额外开空间
vec1.push_back(90);
int i;
for(i=0;i<static_cast<int>(vec1.size());i++)
{
//at返回指定位置的引用
cout << vec1.at(i) << endl;
}
//begin的返回值就是一个迭代器
// vec1.insert(vec1.begin(),78);
// vec1.insert(vec1.begin(),3,78);
// cout << vec1.size() << endl;
int ret = vec1.back();
cout << "第一个元素" << ret << endl;
vec1.erase((vec1.end()-1));
// for(i=0;i<static_cast<int>(vec1.size());i++)
// {
// //at返回指定位置的引用
// cout << vec1.at(i) << endl;
// }
int arr[10]= {12,9,8,7,5,4,0};
vector<int> vec2;
//使用assign函数给vec2赋值
vec2.assign(arr,arr+5);
for(i=0;i<=static_cast<int>(vec2.size());i++)
{
//at返回指定位置的引用
cout << vec2.at(i) << endl;
}
return 0;
}
九、迭代器
C++ Iterators(迭代器)
迭代器可被用来访问一个容器类的所包函的全部元素,其行为像一个指针。举一个例子,你可用一个迭代器来实现对vector容器中所含元素的遍历。有这么几种迭代器如下:
迭代器描述
input_iterator提供读功能的向前移动迭代器,它们可被进行增加(++),比较与解引用(*)。
output_iterator提供写功能的向前移动迭代器,它们可被进行增加(++),比较与解引用(*)。
forward_iterator可向前移动的,同时具有读写功能的迭代器。同时具有input和output迭代器的功能,并可对迭代器的值进行储存。
bidirectional_iterator双向迭代器,同时提供读写功能,同forward迭代器,但可用来进行增加(++)或减少(--)操作。
random_iterator随机迭代器,提供随机读写功能.是功能最强大的迭代器, 具有双向迭代器的全部功能,同时实现指针般的算术与比较运算。
reverse_iterator如同随机迭代器或双向迭代器,但其移动是反向的。(Either a random iterator or a bidirectional iterator that moves in reverse direction.)(我不太理解它的行为)
每种容器类都联系于一种类型的迭代器。第个STL算法的实现使用某一类型的迭代器。举个例子,vector容器类就有一个random-access随机迭代器,这也意味着其可以使用随机读写的算法。既然随机迭代器具有全部其它迭代器的特性,这也就是说为其它迭代器设计的算法也可被用在vector容器上。
cppreference提示:通过对一个迭代器的解引用操作(*),可以访问到容器所包含的元素。
作业:
整理思维导图
自己实现Vector容器的功能,封装成类