分类
折腾

【前端笔记】使用iframe嵌入等比缩放的哔哩哔哩视频

之前在《【考前安利】助梦南开》这篇文章里,土豆哥说要告诉大家怎么样嵌入来自哔哩哔哩的iframe视频,今天拖了几天的文章总算开始写了(๑•̀ㅂ•́)و✧

今天算是给【前端笔记】系列开个头吧,作为一名(伪)前端工程师,土豆哥可能时不时会在马铃薯田地放一些关于前端的技巧和学习心得<( ̄︶ ̄)>欢迎感兴趣的小朋友们来关注哦ヽ(✿゚▽゚)ノ


这篇笔记主要讲的是如何在网页中嵌入来自哔哩哔哩的视频。

目前国内大多数视频网站都提供了分享的途径,对于嵌入到其他网页的分享大多采用iframe。iframe是HTML的一个标签,它支持在HTML页面中以框架的形式显示来自其他网页的内容。通过使用iframe,你可以把来自视频网站的播放器嵌入到你的网页。

哔哩哔哩当然也提供了用于嵌入到其他网页的iframe代码。但是,不知道为什么(可能因为哔哩哔哩的程序员和我一样懒吧╮(╯▽╰)╭),哔哩哔哩的iframe播放器是没有等比适应的o(≧A≦)o简单来说,就会造成下面的效果——

嵌入的视频宽度实现了自适应,但是高度没有实现等比缩放,因此看起来很……扁(╯-_-)╯╧╧


首先,我们需要定义一个CSS类。

.aspect-ratio {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 75%;
}

.aspect-ratio iframe {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
}

在aspect-ratio类中,宽度被设为100%,高度被设为0,padding-bottom属性(外部下边距)被设为75%。因为当padding-bottom的值为百分比时,百分比计算的基准为父元素的宽,而aspect-ratio类的宽度为父元素宽度的100%,所以它的外部下边距也就占宽度的75%。这样,aspect-ratio类的实际宽高比(包含边距的宽高比)就变为了四比三。另外,aspect-ration类的position必须定义为relative,保证它的定位是相对于原始位置定义。

在aspect-ratio类下的iframe元素宽高都被设为100%。因为当元素的position属性设为absolute且width和height属性的值为百分比时,百分比计算基准分别为父元素包含外边距的款和高。所以,此时iframe元素会沾满整个aspect-ratio类的父元素,也就是形成四比三的宽高比。

定义完CSS之后,再来写HTML内容,把哔哩哔哩提供的iframe框架嵌入页面。这里,先获取哔哩哔哩提供的iframe代码:

<iframe src="//player.bilibili.com/player.html?aid=24287094&cid=40734416&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

然后,用一个aspect-ratio类的块内容把iframe包起来——

<div class="aspect-ratio">
    <iframe src="//player.bilibili.com/player.html?aid=24287094&cid=40734416&page=1" scrolling="yes" border="0" frameborder="no" framespacing="1" allowfullscreen="true"></iframe>
</div>

这样一来,就可以实现等比缩放的自适应iframe框架啦~

手机端的显示也是正常的——

这种方法不只可以用于嵌入哔哩哔哩视频,对于其它的iframe框架都是可以的哦(~ ̄▽ ̄)~

分类
程序和算法

C++实现哈夫曼树

上学期数据结构课的练习,觉得对学弟学妹们有点帮助,就拿出来了(~ ̄▽ ̄)~

实现功能

1.文件输入输出

1)从用户输入指定位置读取文件
2)从文件中按行单词和该单词词频
3)将按照哈夫曼树生成的单词和对应编码输出到文件

2.生成哈夫曼树

1)根据单词权重生成哈夫曼树
2)权重越小的节点越靠下,权重越大的节点越靠上

3.为各节点编码

1)根节点编码为空字符串
2)左子树编码在父节点基础上后缀一个“0”,右节点编码在父节点编码基础上后追一个“1”
3)权重越大的节点编码越短,权重越小的节点编码越长

算法简述

存储结构

1.使用类作为基础结构

为保证良好的封装性和对数据便于操作,我在本次实验中使用类作为基础结构。定义treeNode类作为哈夫曼树节点(以及未暂时未编码进树的孤立节点),每个treeNode对象包含单词、权重、编码以及指向左右子树的指针。

1)对象的创建初始化

treeNode类对象可以以两种方式创建。一种方式是将单词和权重作为参数,编码初始化为空字符串,指向左右子树的指针为空,这样创建的节点在最终建立的哈夫曼树中会成为叶节点;另一种方式是将左右子树作为参数建立父节点,编码初也始化为空字符串,指向子树的指针指向输入的参数,单词初始化为空字符串,权重为左右子树之和。

2)类的函数

除了构造函数和重载运算符,该类还有重要函数,分别是编码和输出函数。这两个函数都需要递归调用,他们在完成建树过程后从根节点递归调用遍历全树。其算法将在后文算法图中详细呈现。

2.使用元素为指针的vector作为抽象的树

使用类treeNode解决了每一个节点数据类型的问题,但整个哈夫曼树也需要一个结构存储。这个存储结构需要满足以下特性:

1)含有的元素需要能够随意增减。

在哈夫曼树构建过程中,需要随时用最小的两个节点生成新的父节点加入树中,这实质上是删除两个元素再加入一个元素的过程,因此整个树的结构应该在数据加入和删除方面较为灵活。

2)被删除的元素应该仍能以其他方式被访问。

哈夫曼树中所有已经拥有父节点的子节点都应该被“删除”,不再参与权重比较和建树过程;但是它们应该仍能被访问,因为建树完成后任何节点都不应该丢失,最终的输出和应用仍需要使用它们。

3)整个树中所有元素应该能够较为方便迅速地找到最小元素或执行排序。

哈夫曼树涉及到寻找权重最小节点的过程,而且这个过程需要反复执行,因此寻找排序或最小节点应该能够被迅速方便地操作。

所以,最终我决定使用一个vector来保存哈夫曼树,vector中的元素为指向treeNodo类对象的指针。首先,vector类能够方便地进行元素加入和删除,并且STL中提供了sort()函数可以方便地进行排序。另外,由于保存在这个vector内的元素都是通过new操作符创建的对象,只要新创建并压入vector的父节点仍保留了指向子节点的指针,那么删除vector中的子节点的指针对子节点这个对象本身并没有影响,也即就是说删除vector中元素后该元素还可以其他方式被访问。

整体算法图

编码函数算法图

关键算法简述

1.寻找最小孤立节点

在我的算法中,哈夫曼树的构建每一步都需要找到当前权重最小的两个节点,将它们作为参数构造一个新的treeNode对象作为父节点,然后删除子节点。这里,我使用的是STL中的sort()排序。通过重载比较函数,实现以vector中元素指向对象的权重为关键字将vector中元素从大到小排序的功能。

完成排序后,指向权重最小的两个的指针节点即在vector末尾。将它们保存在临时变量中,之后使用pop_up()函数删除,最后以临时变量中的两个指针作为参数构造父节点并用push_back()函数压入vector,完成一次操作。

2.使用递归函数编码节点

构建完哈夫曼树后,可以直接访问的节点只有根节点,所有其他节点都需要从根节点通过指针一层层访问,无法直接遍历。这种情况,可以通过函数递归调一层层遍历。

编码函数的参数是该对象的编码,由调用它的函数(上一层递归)赋值。函数会先检查该节点是否为叶节点,判断依据是数据域中的单词字符串是否为空,只有叶节点单词字符串非空。如果是叶节点就可以将编码赋值给数据域中的编码字符串并跳出递归。如果不是叶节点就分别调用左子树和右子树的编码函数,左子树参数为自身参数字符串后缀“0”,右子树参数为自身参数字符串后缀“1”,这样进入下一轮递归。

源代码

Main.cpp(程序入口)

#include <fstream>;
#include <vector>;
#include <string>;
#include <iostream>;
#include <algorithm>;
#include "treeNode.h"
#include "globle.h"

using namespace std;

int main()
{
    string inputFileName;
    cout << "请输入完整文件名(包含路径和扩展名):";
    cin >;>; inputFileName;
    ifstream inputFile;
    inputFile.open(inputFileName);
    string tmpWord;
    int tmpCount;
    vector <treeNode*>; nodeList;
    while (inputFile >;>; tmpWord&&inputFile >;>; tmpCount)
    {
        treeNode *newNode = new treeNode(tmpWord, tmpCount);
        nodeList.push_back(newNode);
    }
    while (nodeList.size()>; 1)
    {
        //将vector元素从大到小排序
        sort(nodeList.begin(), nodeList.end(), compare);
        //保存最小的两个元素到临时变量并从vector删除
        treeNode *leftTmp, *rightTmp;
        leftTmp = nodeList.back();
        nodeList.pop_back();
        rightTmp = nodeList.back();
        nodeList.pop_back();
        //构造父节点并压入vector
        treeNode *newNode = new treeNode(leftTmp, rightTmp);
        nodeList.push_back(newNode);
    }
    string outputFileName;
    cout << "请输入希望输出的完整文件名(包含路径和扩展名):";
    cin >;>; outputFileName;
    ofstream outputFile;
    outputFile.open(outputFileName);
    //从根节点开始调用编码函数和输出函数(递归)
    nodeList[0]->;encode();
    nodeList[0]->;print(outputFile);
    system("pause");
}

globle.h(不在类内的全局函数声明)

#pragma once
#include "treeNode.h"

using namespace std;

//重载对自定义类treeNode的比较函数
bool compare(const treeNode *a, const treeNode *b);

globle.cpp(不在类内的全局函数定义)

#include "globle.h"
#include <vector>;

//重载对自定义类treeNode的比较函数
bool compare(const treeNode *a, const treeNode *b)
{
    return a->;getWeight()>; b->;getWeight();
}

treeNode.h(类的声明)

#pragma once
#include <string>;
#include <vector>;
#include <fstream>;

using namespace std;

class treeNode
{
    string word;
    string code;
    int weight;
    treeNode *leftChild;
    treeNode *rightChild;
public:
    treeNode(string inputWord,int inputWeight);
    treeNode(treeNode *left, treeNode *right);
    int getWeight()const;
    void encode(string code = "");
    void operator=(const treeNode &a);
    void print(ofstream &file);
};

treeNode.cpp(类的定义)

#include "treeNode.h"
#include <iostream>;

//构造叶节点
treeNode::treeNode(string inputWord,int inputWeight)
{
    word = inputWord;
    code = "";
    weight = inputWeight;
    leftChild = NULL;
    rightChild = NULL;
}

//从子节点构造父节点
treeNode::treeNode(treeNode * left, treeNode * right)
{
    word = "";
    leftChild = left;
    rightChild = right;
    weight = left->;weight + right->;weight;
}

//向类外函数返回权重
int treeNode::getWeight()const
{
    return weight;
}

//递归编码
void treeNode::encode(string oringinCode)
{
    //如果是叶节点就将编码存储到数据域并停止递归
    if (word != "")
    {
        code = oringinCode;
        return;
    }
    //调用子节点编码函数的参数为自身参数加后缀
    leftChild->;encode(oringinCode + "0");
    rightChild->;encode(oringinCode + "1");
}

//重载赋值操作符
void treeNode::operator=(const treeNode & a)
{
    word = a.word;
    code = a.code;
    weight = a.weight;
    leftChild = a.leftChild;
    rightChild = a.rightChild;
}

//输出函数,参数为文件输出流的引用
void treeNode::print(ofstream &file)
{
    //如果是叶节点就输出单词和编码
    if (word != "")
    {
        file << word << ' ' << code << endl;
    }
    //如果有子节点就继续递归
    if (leftChild != NULL)
    {
        leftChild->;print(file);
    }
    if (rightChild != NULL)
    {
        rightChild->;print(file);
    }
}

存在的不足

性能问题

1.反复排序

在建树过程中为了找到最小节点,反复使用了std::sort()函数。虽然该函数性能较好,但针对本实验中上万次的排序还是不够迅速,整个程序执行花了两分钟左右。

2.过多递归

递归虽然是很方便使用,但从空间复杂度的角度来说有其弊端。虽然在本实验中不到四万次递归效率很高,但是如果使用更多数据,可能出现内存不足方面的问题。

封装性不够好

1.数据类型限制

本程序只能针对有权重的字符串建立哈夫曼树,如果想兼容更多数据类型需要对类本身进行修改。其实从算法上来说,本程序可以完成任何带权重数据类型的建树,但其封装性和可移植性还是略有欠缺。

2.输出模块过于独立

输出模块是接受一个输出文件流作为参数并向该文件流输出。虽然可以通过重载函数实现向控制台打印或是进行其他操作,但无法实现直接向主调函数返回数据,独立性过强,不能与主函数或其他函数结合。

分类
程序和算法

自定义模板类实现比std::vector更强大的动态数组

最近闲的没事打算复习下C++,于是写了这么个类。功能上基本和标准库中的vector相似,又多加了几个功能,更加实用一些。因为用了模板类,所以和vector一样,兼容包括自定义类型在内的所有数据类型。

此项目已发布在github,如果有更好的方法欢迎fork和修改~

基本结构

整个实现采用链表的数据结构,因此数组每个元素(链表每个节点)除了数据域以外还需要有两个指针域分别指向前后节点。我将每个节点定义为一个模板类Element。当然这里也可以用struct,但是相比于struct,我更偏爱类。

template <class T> class Element
{
private:
    T content;
    Element *next;
    Element *prev;
public:
    friend class Chain <T>;
    friend class std::ostream;
};

整个动态数组定义为另一个模板类Chain,并将Chain声明为Element类的友元类,因为Element类没有提供公有函数作为接口,所以要将Chain定义为友元类才可以访问Element的私有成员。下面是Chain类的定义。

template <class T> class Chain
{
private:
    Element <T> *first;
    Element <T> *last;
public:
    Chain();
    Chain(const Chain <T> & chain);
    ~Chain();
    void add_front(T input);
    void add_back(T input);
    void del_front();
    void del_back();
    void del(int n);
    void insert(int n, T input);
    void replace(int n, T input);
    void sort(bool(*compare)(T, T));
    void sort();
    void clear();
    void print();
    int count();
    int lookup(T input);
    void operator=(const Chain <T> & chain);
    bool operator==(Chain <T> & chain);
    bool operator!=(Chain <T> & chain);
    T & operator[](int n);
};

从类定义可以看到,这个数组有一个头指针一个尾指针,这两个指针不存储数组内容,并且在构造时被创建。这个动态数组实现了从前后添加和删除元素、删除指定元素、插入元素、替换元素、给类内元素排序、清空数组、输出数组、数组计数、查找指定元素的功能,并能够进行赋值、拷贝构造、比较以及下标访问。

各函数实现

构造函数

构造函数的实现相对简单,只需要创建头尾指针即可。

template<class T>
Chain<T>::Chain()
{
    first = new Element <T>;
    last = new Element <T>;
    first->next = last;
    first->prev = nullptr;
    last->prev = first;
    last->next = nullptr;
}

添加元素

添加元素的函数的思路是先使用new命令创建一个新的Element对象,然后调整Element的指针指向和现有数组元素的指针指向。

//从前插入元素
template<class T>
void Chain<T>::add_front(T input)
{
    Element <T> *tmp = new Element <T>;
    tmp->prev = first;
    tmp->next = first->next;
    tmp->content = input;
    first->next->prev = tmp;
    first->next = tmp;
}
//从后插入元素
template<class T>
void Chain<T>::add_back(T input)
{
    Element <T> *tmp = new Element <T>;
    tmp->prev = last->prev;
    tmp->next = last;
    tmp->content = input;
    last->prev->next = tmp;
    last->prev = tmp;
}

这里需要注意的是,调整指针指向的顺序必须注意。比如,如果在从前插入函数中先将first->next设为tmp,那么就没有指针指向原先的第一个元素了,此时我们就无法完成这个函数。所以,这个调整指针指向的过程,应该先调整tmp的前后指针,再调整原有元素的指针。

从前后删除元素

从前后删除元素的函数和添加函数类似,也需要一个tmp指针来完成。不过这里的tmp指针不需要用new申请新的内存,而是直接将要删除的元素赋给了tmp,之后调整tmp前后元素的指针指向即可。

//从前删除
template<class T>
void Chain<T>::del_front()
{
    if (first->next->next == nullptr)
    {
        return;
    }
    Element <T> *tmp = first->next;
    tmp->next->prev = first;
    first->next = tmp->next;
    delete tmp;
}
//从后删除
template<class T>
void Chain<T>::del_back()
{
    if (first->next->next == nullptr)
    {
        return;
    }
    Element <T> *tmp = last->prev;
    tmp->prev->next = last;
    last->prev = tmp->prev;
    delete tmp;
}

删除指定元素

删除指定元素和从前后删除元素类似,不同的是需要一个int型变量来计数以便找到指定的元素。

template<class T>
void Chain<T>::del(int n)
{
    Element <T> *current = first;
    for (int i = 0; i <= n; i++)
    {
        if (current->next->next == nullptr)
        {
            return;
        }
        current = current->next;
    }
    current->next->prev = current->prev;
    current->prev->next = current->next;
    delete current;
}

插入元素到指定位置

也和从前后插入元素以及删除元素类似,另外需要一个用于计数的int型变量。

template<class T>
void Chain<T>::insert(int n, T input)
{
    Element <T> *current = first;
    Element <T> *tmp = new Element <T>;
    for (int i = 0; i < n; i++)
    {
        if (current->next->next == nullptr)
        {
            tmp->prev = last->prev;
            tmp->next = last;
            tmp->content = input;
            last->prev->next = tmp;
            last->prev = tmp;
            return;
        }
        current = current->next;
    }
    tmp->prev = current;
    tmp->next = current->next;
    tmp->content = input;
    current->next->prev = tmp;
    current->next = tmp;
}

替换元素

替换元素更简单,只需要定位并赋值,还省去了调整指针的步骤。

template<class T>
void Chain<T>::replace(int n, T input)
{
    Element <T> *current = first;
    for (int i = 0; i <= n; i++)
    {
        if (current->next == nullptr)
        {
            return;
        }
        current = current->next;
    }
    current->content = input;
}

计数函数

用一个int型变量计数并遍历整个数组。

template<class T>
int Chain<T>::count()
{
    int n=0;
    Element <T> *current = first->next;
    while (current->next != nullptr)
    {
        current = current->next;
        n++;
    }
    return n;
}

查找函数

与计数函数类似,多一个比较的过程,返回值查找元素的下标,如果找不到就返回-1。

template<class T>
int Chain<T>::lookup(T input)
{
    int n = 0;
    Element <T> *current = first->next;
    while (current->next != nullptr)
    {
        if (current->content == input)
        {
            return n;
        }
        n++;
        current = current->next;
    }
    return -1;
}

需要注意的是,当这个动态数组被实例化为可比较的数据类型(也就是T类型可比较)时,这一函数才可用。如果想要为自己的自定义数据类型创建一个动态数组并实现查找功能,需要给自己的类重载==运算符。

输出函数

遍历数组并用iostream的cout打印在屏幕上。调用的前提是T类型可以被cout。

template<class T>
void Chain<T>::print()
{
    Element <T> *current = first->next;
    while (current->next != nullptr)
    {
        std::cout << current->content << ' ';
        current = current->next;
    }
    std::cout << endl;
}

清空函数

依然是遍历整个数组,不过稍复杂,需要不断创建临时变量并释放,直到数组只剩下first和last。

template<class T>
void Chain<T>::clear()
{
    while (first->next->next != nullptr)
    {
        Element <T> *tmp = first;
        first = first->next;
        delete tmp;
    }
}

析构函数

析构函数和清空函数几乎一样,不过first和last也要释放。

template<class T>
Chain<T>::~Chain()
{
    while (first->next->next != nullptr)
    {
        Element <T> *tmp = first;
        first = first->next;
        delete tmp;
    }
    delete first;
    delete last;
}

排序函数

因为对于链表而言,遍历比下标访问更方便,所以这里使用的是冒泡排序。

//自定义比较函数作为传入参数
template<class T>
void Chain<T>::sort(bool(*compare)(T, T))
{
    Element <T> *front = first->next;
    if (front->next == nullptr)
    {
        return;
    }
    while (front->next->next != nullptr)
    {
        Element <T> *back = front;
        while (back->prev != nullptr)
        {
            if (compare(back->content, back->next->content))
            {
                T tmp;
                tmp = back->content;
                back->content = back->next->content;
                back->next->content = tmp;
            }
            back = back->prev;
        }
        front = front->next;
    }
}
//无参数
template<class T>
void Chain<T>::sort()
{
    Element <T> *front = first->next;
    if (front->next == nullptr)
    {
        return;
    }
    while (front->next->next != nullptr)
    {
        Element <T> *back = front;
        while (back->prev != nullptr)
        {
            if (back->content > back->next->content)
            {
                T tmp;
                tmp = back->content;
                back->content = back->next->content;
                back->next->content = tmp;
            }
            back = back->prev;
        }
        front = front->next;
    }
}

这里定义了一个无参数的,一个以自定义比较函数作为参数的。当调用无参数排序函数时,会调用T类型的>运算符,所以必须保证T类型的>运算符可用。

下标访问

数组的基本功能之一,通过重载[]运算符实现。和计数函数几乎一样,但返回值是该元素内容的引用。

template<class T>
T & Chain<T>::operator[](int n)
{
    Element <T> *current = first;
    for (int i = 0; i <= n; i++)
    {
        if (current->next->next == nullptr)
        {
            return current->content;
        }
        current = current->next;
    }
    return current->content;
}

注意,这个函数的返回值必须是引用类型T&而不是普通类型T,否则通过下标访问对数组元素进行的修改都没有作用。

比较函数

重载==和!=运算符实现比较。只有当两个数组元素数相同且每个元素都相同时才判定为两个数组相同。

template<class T>
bool Chain<T>::operator==(Chain <T>& chain)
{
    if (this->count() != chain.count())
    {
        return false;
    }
    Element <T> *left = first->next;
    Element <T> *right = chain.first->next;
    while (left->next != nullptr)
    {
        if (left->content != right->content)
        {
            return false;
        }
        left = left->next;
        right = right->next;
    }
    return true;
}

template<class T>
bool Chain<T>::operator!=(Chain<T>& chain)
{
    if (this->count() != chain.count())
    {
        return true;
    }
    Element <T> *left = first->next;
    Element <T> *right = chain.first->next;
    while (left->next != nullptr)
    {
        if (left->content != right->content)
        {
            return true;
        }
        left = left->next;
        right = right->next;
    }
    return false;
}

T类型本身必须可以比较才能执行本函数,也就是说T类型的==和!=必须是有效的。

赋值函数

将另一个数组的值完整赋给本数组。原理是先清空,然后遍历另一个数组,把另一个数组每一个元素依次添加到本数组后面。

template<class T>
void Chain<T>::operator=(const Chain<T>& chain)
{
    this->clear();
    Element <T> *source = chain.first->next;
    Element <T> *current = first;
    while (source->next != nullptr)
    {
        Element <T> *tmp = new Element <T>;
        tmp->content = source->content;
        current->next = tmp;
        current = tmp;
        source = source->next;
    }
    current->next = last;
    last->prev = current;
}

拷贝构造函数

如果我们不定义拷贝构造函数,程序照样可以运行,但是有时会出现严重的问题。考虑下面的情况:

Chain <char> *a = new Chain <char>;
a->add_back('a');
Chain <char> b = *a;
delete a;
cout << b[0];

如果我们定义了拷贝构造函数,当然没有问题。但是如果不定义拷贝构造函数,这段代码运行时会报错。因为在编译时,编译器会创建一个默认的浅拷贝构造函数,当运行Chain <char> b = *a;时,默认拷贝构造函数会被用于创建b对象。但是,此时的b数组的元素并不独立占有内存,只是一个指向a数组元素的指针。当a被delete之后,数组b实际上已经被析构了。所以,我们必须自定义深拷贝构造函数,为b申请内存实现深拷贝。

template<class T>
Chain<T>::Chain(const Chain<T> & chain)
{
    first = new Element <T>;
    last = new Element <T>;
    first->next = last;
    first->prev = nullptr;
    last->prev = first;
    last->next = nullptr;
    Element <T> *source = chain.first->next;
    Element <T> *current = first;
    while (source->next != nullptr)
    {
        Element <T> *tmp = new Element <T>;
        tmp->content = source->content;
        current->next = tmp;
        current = tmp;
        source = source->next;
    }
    current->next = last;
    last->prev = current;
}

输出操作符重载

通过重载<<操作符,可以让你的整个数组能够被cout,使用更加方便。不过这个函数并不是Chain类的成员函数,而是ostream类的函数重载,因此需要注意访问权限的问题。在下面的代码中,所调用的全部是Chain类的公有成员函数。

template<class T>
std::ostream& operator<<(std::ostream& os, Chain <T> &chain)
{
    if (chain.count() == 0)
    {
        os << "empty";
    }
    for (int i = 0; i < chain.count(); i++)
    {
        os << chain[i] << ' ';
    }
    os << endl;
    return os;
}

注意事项

在Chain类的定义中使用了Element类,Element类的定义中也使用了Chain类,这里我们需要使用前置声明确保可以顺利调用。我将Element类写在Element.h文件中,并在预处理命令中加入了#include "Chain.h";将Chain类的定义写在了Chain.h文件中,并在定义Chain类的代码前使用template <class T> class Element;作为前置声明。这样,就可以保证互相调用不出问题。