北大郭炜《程序设计与算法(三)》Mooc笔记:多态,模版,标准模版库

多态

虚函数和多态

  • 虚函数

    在类的定义中,前面有virtual关键字的成员函数

    class base{
        virtual int get();
    };
    

    ❗❗virtual关键字只用在类定义里的函数声明中,写函数体时候不用

    int base::get()//不需要在get前面加virtual!!!
    

    ❗❗构造函数和静态成员函数不能是虚函数

    ❗❗虚函数和普通函数的本质差别:虚函数可以参与多态,静态函数不能

  • 多态的表现形式一:指针

    • 派生类的对象可以赋给基类指针

    • 通过基类指针调用基类和派生类中的同名虚函数时候:

      • 若指针指向一个基类的对象,则被调用的是基类的虚函数

      • 若指针指向一个派生类的对象,则被调用的是派生类的虚函数

    class Cbase(){
     public:
           virtual void SomeVirtualFunction(){}
    };
    
    class Cderived:public Cbase(){
     public:
           virtual void SomeVirtualFunction(){}
    };
    
    
    int main(){
     Cderived Oderived;
     Cbase *p= &Oderived;
     p->SomeVirtualFunction();
     //p指向派生类的对象,调用的是派生类的虚函数
     return 0;
    }
    
  • 多态的表现形式二:对象

    • 派生类的对象可以赋给基类引用
    • 通过基类引用调用基类和派生类中的同名虚函数时:
      • 若该引用引得是一个基类的对象,那么被调用的是基类的虚函数
      • 若该引用引得是一个派生类的对象,那么被调用的是派生类的虚函数
class Cbase(){
    public:
    virtual void SomeVirtualFunction(){}
};

class Cderived(){
    public:
    virtual void SomeVirtualFunction(){}
};

int main(){
    Cderived Oderived;
    Cbase &r=Oderived;
    r.SomeVirtualFunction();//!!!引用调用函数时用"."!!!
    //r引用的是派生类的对象,调用派生类的虚函数
}

  • 多态的作用

    增强程序的可扩充性**(程序需要修改或增加功能的时候,需要改动和增加的代码较少)**

使用多态的游戏程序示例

几何形体处理程序

#include<iostream>
#include<stdlib.h>
#include<math.h>

using namespace std;

//定义基类
class CShape{
    public:
    virtual double Area()=0;
    //后面写了一个"=0",说明这是纯虚函数,连函数体都没有
    virtual void PrintInfo()=0;
    //因为我们要处理的图形只有圆形三角矩形几种,不存在CShape这样一种抽象的形状,所以不需要为CShape编写这两程序,这两程序在派生类中会分别定义
};

//定义派生类
class CRectangle:public CShape{
    public:
    int w,h;
    virtual double Area();
    virtual void PrintInfo();
};

class CCircle:public CShape{
    public:
    int r;
    virtual double Area();
    virtual void PrintInfo();
};

class CTriangle:public CShape{
    public:
    int a,b,c;
    virtual double Area();
    virtual void PrintInfo();
};


//实现派生类的成员函数

double CRectangle::Area(){
    return w*h;
}
void CRectangle::PrintInfo(){
    cout<<"Rectangle:"<<Area()<<endl;//输出的末尾要加endl!!
}

double CCircle::Area(){
    return 3.14*r*r;
}
void CCircle::PrintInfo(){
    cout<<"Circle:"<<Area()<<endl;
}

double CTriangle::Area(){
    double p=(a+b+c)/2.0;
    return sqrt(p*(p-a)*(p-b)*(p-c));
}
void CCircle::PrintInfo(){
    cout<<"Triangle:"<<Area()<<endl;
}

//存放不同类型的几何形体
CShape *pShapes[100];
int MyCompare(const void *s1,const void *s2);
//用三个数组来存浪费空间,难以增改,难以实现排序等进阶功能
//所以我们使用多态来存储
//pShapes数组中的元素都是基类指针,由于基类指针能够指向派生类对象,我们可以在后面把指针指向new出来的派生类对象


//主函数
int main(){
    int i;int n;
    CRectangle *pr;CCircle *pc;CTriangle *pt;
    
    cin>>n;
    for(i=0;i<n;i++){
        char c;
        cin>>c;
        switch(c){
            case'R':
                    pr=new CRectangle();
                    cin>>pr->w>>pr->h;
                    pShapes[i]=pr;
                    break;
            case 'C':
                    pc=new CCircle();                                   cin>>pc->r;
                    pShapes[i]=pc;
                    break;
            case'T':
                pt=new CTriangle();
                cin>>pt->a>>pt->b>>pt->c;
                pShapes[i]=pt;
                break;
                    
        }
    }
    qsort(pShapes,n,sizeof(CShape*),MyCompare);
    for(i=0;i<n;i++)
        pShapes[i]->PrintInfo();
    return 0;
}
//为什么变量前面要加关键字void*?然后可以将指向任何类型数据的指针赋给这个void*类型指针
int MyCompare(const void *s1,const void *s2){
    double a1,a2;
    CShape**p1;
    CShape**p2;
    //一定要写两个*,不能用"*s1"来取得s1指向的内容
    p1=(CShape**)s1;
    p2=(CShape**)s2;
    //s1,s2指向数组中的元素,数组中元素的类型是CShape*
    //因此p1,p2是指向指针的指针,类型为CShape**
    a1=(*p1)->Area();//*p1的类型是CShape*是基类指针
    a2=(*p2)->Area();
    if(a1<a2)
        return -1;
    else if(a2<a1)
        return 1;
    else
        return 0;
}

*下面是 qsort()函数的声明。

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
  • base – 指向要排序的数组的第一个元素的指针。
  • nitems – 由 base 指向的数组中元素的个数。
  • size – 数组中每个元素的大小,以字节为单位。
  • compar – 用来比较两个元素的函数。

虚析构函数,纯虚函数,抽象类

虚析构函数:

class son{
    public:
    ~son(){cout<<"bye from son"<<endl;}
};

class grandson:public son{
    public:
    ~grandson(){cout<<"bye from grandson"<<endl;}
};

int main()
{
    son *pson;
    pson=new grandson();
    //new一个对象的时候,如果对象的构造函数不需要参数就要写一对括号,跟new一个数据不一样
    delete pson;
    return 0;
}

output:

bye from son

new出来的grandson没有被删除!!!,反而执行了son的析构函数,然而我们并没有定义son类的对象

解决办法:

把基类son的析构函数变成虚函数

纯虚函数和抽象类:

  • 包含纯虚函数的类就是抽象类

  • 抽象类不能创建自己的对象

  • 抽象类的指针/引用可以指向其派生类的对象

  • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数

  • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

输入输出和模板

函数模板

  • 例子-求数组最大元素的函数模板
template<class T>
T MaxElement(T a[],int size){
    T tmpMax=a[0];
    for(int i=1;i<size;i++)
        if(tmpMax<a[i])
            tmpMax=a[i]
    return tmpMax;        
}
  • 例子-不通过参数实例化函数模板
#include<iostream>
using namespace std;
template<class T>
T Inc(T n){
    return 1+T;
    //由于Inc的返回值是T类型的,这里要对+进行重载
}

int main(){
    cout<<Inc<double>(4)/2;
    //用<double>直接规定T是double类型
}
  • 函数模板是可以重载的,只要他们的形参表和类型参数表不一样即可

  • 函数模板和函数的次序:

    参数匹配+普通函数----->参数匹配模板函数----->参数经自动类型转换后能匹配的普通函数

类模板

编写模板是为了实现泛型程序设计,即写出一个类/函数后,可以作用与多种数据类型

类模板的定义

template<typename 类型参数,typename 类型参数,...>
class 模板名字
{
    成员变量;
    成员函数;
}

类模板内成员函数定义

template<typename 类型参数,typename 类型参数,...>
(返回值)类型参数 模板名字 <类型参数名字,类型参数名字,...>::成员函数名字(参数表)
{
    ...
}

类模板定义对象的写法

模板名字<实际类型参数表> 对象名(构造函数实参表);

示例:Pair类模板

#include<iostream>
#include<string.h>
using namespace std;
//类型参数就是这个类需要的参数的类型
//其个数就该类需要的参数个数
template<typename T1,typename T2>
class Pair
{
    public:
    T1 key;//关键字
    T2 value;//值
    Pair(T1 k,T2 v):key(k),value(v){};
    //构造函数及其初始化列表
    bool operator < (const Pair<T1,T2>&p)const;
    //对"<"进行重载,其形参是pair模板定义的类的常引用
    //我们希望这个"<"是不变的,最后加个const
};

template<typename T1,typename T2>
bool Pair<T1,T2>::operator < (const Pair<T1,T2>&p)const
{
    return key<p.key;
}

int main()
{
    Pair<string,int> student("Tom",19);
    //对象的名字是:student
    //注意:由类模板"pair"生成类"pair<string,int>",再由这个类生成对象"student"
     cout<<student.key<<" "<<student.value;
     return 0;
}

output:

Tom 19
  • 类模板的实例化:编译器由类模板生成类的过程

  • 同一个模板实例化出的类是不兼容的,他们根本就是两个类

    Pair<string,int> *p;
    Pair<string,double> a;
    p=a;//报错
    

函数模板作为类模板成员

#include<iostream>
using namespace std;

template<typename T>
class A
{
    public:
    
    //注意这里不能写T,因为这里成员函数的类型参数相对类的类型参数来说是形参
    
    template<typename T2>
    void Func(T2 t){cout<<t;}
    
};

int main()
{
    A<int> a;
    //类模板名: A
    //类名: A<int>
    //对象名: a,其需要的参数类型为int(虽然下面的定义里我没有用这个参数)
    
    a.Func('K');//成员函数模板Func被实例化,其参数类型为char
    a.Func("hello");//成员函数模板Func再次被实例化,其参数类型为const char*
    return 0;
}

output:

Khello
  • A<int>这个类通过上面对函数模板Func的两次实例化有了两个名为Func的成员函数:

    Func(char c);//赋给该函数的值是:K
    Func(const char* string);//赋给该函数的值是:指向字符串"hello"的指针
    

类模板的类型参数表中可以出现非类型参数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYhPavV8-1636871681968)(assets/image-20210308125420974.png)]

标准模板库STL

概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cOnJtLQA-1636871681969)(assets/image-20210318145622473.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2f3IK7j-1636871681969)(assets/image-20210318145709507.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MrcebDjt-1636871681970)(assets/image-20210318145751989.png)]

迭代器

  • 用于指向顺序容器/关联容器中的元素

  • 用法和指针类似

  • 有const和非const两种

  • 通过迭代器可以读取其指向的元素

  • 通过非const迭代器可以修改其指向的元素

代码:

定义一个容器类的迭代器的方法:

容器类名::iterator 变量名;
容器类名::const_iterator 变量名;

访问一个迭代器指向的元素:

*迭代器变量名

迭代器示例:

#include<vector>
#include<iostream>
using namespace std;
int main()
{
    
    vector<int> v;
    //v是一个存放int类型元素的动态数组,一开始里面没有元素
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    
    
    //正向迭代器
    vector<int>::const_iterator i;
    //常量迭代器,只能读取元素,不能修改元素
    for(i=v.begin();i!=v.end();i++)
    //begin()获取容器第一个元素的位置
    //end()获取容器最后一个元素后面的位置
        cout<<*i<<",";
    cout<<endl;
    
    //反向迭代器
    vector<int>::reverse_iterator r;
    for(r=v.rbegin();r!=v.rend;r++)
    //rbegin()获取容器最后一个元素的位置
    //rend()获取容器第一个元素前面的位置
        cout<<*r<<",";
    cout<<endl;
    
    //非常量迭代器
    vector<int>::iterator j;
    for(j=v.begin();j!=v.end();j++)
        *j=100;//用非常量迭代器修改指向的元素
    
    for(i=v.begin();i!=v.end();i++)
        cout<<*i<<",";//用常量迭代器读取指向的元素
    cout<<endl;    
}

两种不同的的迭代器

  • 双向迭代器p&p1

    • ++p,p++

    • –p,p–

    • p*(实际上返回值是p指向的对象的引用)**

    • p=p1

    • p==p1;p!=p1

  • 随即访问迭代器p&p1

    • 双向迭代器的所有操作
    • p+=i(将p向后移动i个元素)
    • p-=i
    • p+i (p+i的值=指向p后面第i个元素的迭代器)
    • p-i
    • p[i](p[i]的值=p后面第i个元素的引用)
    • p<p1;p<=p1;p>p1;p>=p1

容器

顺序容器

vector(动态数组)

vector上的操作

#include<iostream>
#include<vector>
using namespace std;
template<class T>
void PrintVector(T s,T e)
{
    for(;s!=e;++s)
        cout<<*s<<" ";
    cout<<endl;
}

int main()
{
    int a[5]={1,2,3,4,5};
    vector<int> v(a,a+5);
    //将数组a中下标从0到4的元素都拷贝到v里面
    
    cout<<v.end()-v.begin();
    //vector的迭代器是随即迭代器可以相减
    //上面输出的v中元素的个数:5
    
    v.insert(v.begin()+2,13);
    PrintVector(v.begin(),v.end());
    //在v下标为2的位置插入一个元素13,后面的元素全部后推
    
    v.erase(v.begin()+2);
    PrintVector(v.begin(),v.end());
    //删除位于v中下标为2的元素
    
    vector<int> v2(4,100);
    //v2有4个元素,都是100
    v2.insert(v2.begin(),v.begin()+1,v.begin()+3);
    //将v的一段插入v2开头
    //v下标为1的元素到v下标为3的元素(不包括v下标为3的元素!!!)
    PrintVector(v2.begin(),v2.end());
    
    v.erase(v.begin()+1,v.begin()+3);
    PrintVector(v.begin(),v.end());
    //删除v上的一个区间,即2,3
    
    return 0;
}

用vector实现二维数组

#include<iostream>
#include<vector>
using namespace std;
int main()
{
    vector<vector<int> >v(3);
    //v中有3个元素,每个元素都是vector<int>空容器
    //注意vector<int>后面一定要加一个空格,不然编译器会把两个尖括号当成右移运算符
    
    //下面往v的元素里添加元素
    for(int i=0;i<v.size();++i)
        for(int j=0;j<4;++j)
            v[i].push_back(j);
    
    //下面显示v的元素里的元素
    for(int i=0;i<v.size();++i){
        for(int j=0;j<v[i].size();++j)
            cout<<v[i][j]<<" ";
        cout<<endl;
    }
    return 0;
}
deque(双向队列)
  • 所有vector的操作都适用于deque

  • deque多了:

    push_front:将元素插入到前面

    pop_front:删除最前面的元素

list(双向列表)

成员函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xglhscdu-1636871681970)(assets/image-20210318143445110.png)]

list上的操作

#include<list>
#include<iostream>
#include<algorithm>
using namepsace std;

class A
{
    private:
    int n;
    
    public:
    A(int n_){n=n_;}
    friend bool operator<(const A & a1,const A & a2);
    friend bool operator==(const A & a1,const A & a2);
    friend ostream & operator << (ostream & o,const A & a);
}
bool operator<(const A & a1,const A & a2)
{return a1.n<a2.n}
bool operator==(const A & a1,const A & a2)
{return a1.n==a2.n}
ostream & operator << (ostream & o,const A & a)
{o<<a.n;return o;}

//下面定义一个函数模板,其参数是"类型可变列表的引用"
template <class T>
void PrintList(const list<T> & lst)//参数是"类型可变列表的引用"
{
    typename list<T>::const_iterator i;
    //typename用来说明"list<T>::const_iterator"描述的是个类型
    i=lst.begin();
    for(i=lst.begin();i!=lst.end();i++)
        cout<<*i<<",";
}

int main()
{
    list<A> lst1,lst2;
    //定义了两个元素为A类对象的列表
    
    ...
        
    PrintLsit(lst1);
}

容器适配器

容器适配器上没有迭代器

stack
  • 后进先出的数据结构,

  • 可以

    • push插入
    • pop删除
    • top返回栈顶元素的引用
  • 可用vector,list,deque来实现,缺省情况下,用deque实现。(vector、deque实现的性能比list好)

    template<class T,class Container=deque<T>>
    //第一个类型参数是栈里的元素类型
    //第二个类型参数是栈用什么容器来实现,并表示缺省情况下是deque
    class stack{
        ...
    };
    
queue
  • 先进先出的数据结构,

  • 可以

    • push插入(发生在队尾)
    • pop删除
    • front返回栈顶元素的引用
    • back返回队尾元素的引用
  • 可用list,deque来实现,缺省情况下,用deque实现。(vector、deque实现的性能比list好)

    template<class T,class Container=deque<T>>
    //第一个类型参数是容器适配器里的元素类型
    //第二个类型参数是容器适配器用什么容器来实现,并表示缺省情况下是deque
    class stack{
        ...
    };
    
priority_queue
  • 可用vector和deque实现,缺省情况下用vector实现

    template<class T,class Container=vector<T>,class Compare=less<T>>
    //第一个类型参数是容器适配器里的元素类型
    //第二个类型参数是容器适配器用什么容器来实现,并表示缺省情况下是vector
    //第三个类型参数是优先队列的元素比较器用什么来实现,并表示缺省情况下是小于号
    class priority_queue;
    
  • priority_queue通常用堆排序技术实现,保证最大的元素总是在最前面,即执行pop操作时,删除的是最大的元素;执行top操作时,返回的是最大元素的引用,默认的元素比较器是less

  • push,pop时间复杂度:O(logn)

  • top时间复杂度:O(1)

所有容器适配器中都有成员函数:

  1. empty():用于判断适配器是否为空
  2. size():用于返回适配器中元素个数

算法

类型参数Pred

大多重载的算法都是有两个版本的:

  1. 一个是
    • 用“==”判断元素是否相等
    • 用“<”来比较大小
  2. 另一个多出来一 个类型参数Pred,以及函数形参Pred op
    • 表达式“op(x,y)”的返回值是ture,则x等于y
    • 表达式“op(x,y)”的返回值是false,则x小于y

不变序列算法

  • 不会修改算法作用的容器或对象
  • 适用于顺序容器和关联容器
  • 时间复杂度:O(n)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7NFIgeCl-1636871681970)(assets/image-20210315213723396.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4QEWi1p-1636871681971)(assets/image-20210315213758362.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4rujTamB-1636871681971)(assets/image-20210315213935514.png)]

  • find很重要

排序算法

  • 需要随机访问迭代器的支持
  • 不适用于关联容器和list
  • 时间复杂度:O(log(n))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QsCl9u2T-1636871681972)(assets/image-20210315215204555.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOqEcQ2f-1636871681972)(assets/image-20210315215228540.png)]

sort:快速排序

  • 模板

    template<class Ranlt>
    void sort(Ranlt first,Ranlt last);
    
    1. 按升序排列
    2. 判断x是否应该比y靠前,就看x<y是否为true
    template<class Ranlt,class Pred>
    void sort(Ranlt first,Ranlt last,Pred pr);
    
    1. 按升序排列
    2. 判断x是否应该比y靠前,就看pr(x<y)是否为true

应用:

#include<iostream>
#include<algorithm>
using namespace std;

//定义Pred pr
class MyLess
{
    public:
    bool operator()(int n1,int n2)
    {
        return(n1%10)<(n2%10);
    }   
};

int main()
{
    int a[]={14,2,9,111,78};
    int i;
    
    sort(a,a+5,MyLess());//按个位数大小排序
    for(i=0;i<5;i++)
        cout<<a[i]<<" ";
    cout<<endl;
    
    sort(a,a+5,greater<int>());//按降序排序
    for(i=0;i<5;i++)
        cout<<a[i]<<" ";
}

注意:

  • sort实际上是快速排序,时间复杂度为:O(n*log(n))

    • 平均性能最优
    • 最坏的情况性能非常差
  • stable_sort实际上是归并排序

    • 能保证相等元素之间的先后次序
    • 存储空间足够时,时间复杂度:n*log(n)
    • 存储空间不够时,时间复杂度:n*log(n)*log(n)
  • list只能使用内置的排序算法:list::sort

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值