STL学习笔记:map容器里find函数的第三个参数实现原理,函数对象(仿函数)

最近在看STL里的map容器,一开始是打算直接存储一个键值对map 类型,发现在调用map.find(key)是无法查到对应元素值,仔细debug一下,发现在存储key是存储的是char* 的地址,因此map.find()函数在调用内部自带的比较函数是直接比较指针的地址,这样就永远找不到合适的值。
首先解释函数对象:
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象。又称仿函数。
以上是百度百科里的定义,附上链接:https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%AF%B9%E8%B1%A1/7721014?fr=aladdin
STL map源代码:

template<class _Kty,
    class _Ty,
    class _Pr = less<_Kty>,
    class _Alloc = allocator<pair<const _Kty, _Ty>>>
    class map
        : public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false>>

通过查看源代码,STL自带的比较函数为less<_Kty>。
less<_Kty>的源代码:

template<class _Ty = void>
    struct less
    {   // functor for operator<
    _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
    _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
    _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;

    constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
        {   // apply operator< to operands
        return (_Left < _Right);
        }
    };

通过源代码知:这里重写类的()操作符,构造了一个函数对象(仿函数),内部实现是直接用<号比较值,而指针通过<号比较的是地址,即是开头问题所在。
接来下,我通过重写指针比较的()操作符:

class cmp_str
{
public:
    cmp_str()
    {
        cout << "1111111" << endl;
    }

    bool operator()(char const *a, char const *b) const
    {
        return std::strcmp(a, b) < 0;
    }

    ~cmp_str()
    {
        cout << "222222222" << endl;
    }
};

这里构造函数为直接输出一句话,一会测试代码即会发现问题,不要忘了书写()操作符结尾的const,这个限定符的作用是防止函数改变对象的this指针。
测试代码:

int main()
{
    map<char*,int, cmp_str> dict;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        char* tmp = new char[100];
        cin >> tmp;
        dict.insert(pair<char*,int>(tmp, i);
    }
    char* tmp = new char[100];
    cin >> tmp;
    map<char*, int>::iterator it1 = dict.find(tmp);
    if (it1 != dict.end())//找到
    {
        cout << it1->first << " " << it1->second << endl;
    }
    system("pause");
    return 0;
}

输出结果
STL学习笔记:map容器里find函数的第三个参数实现原理,函数对象(仿函数)_第1张图片

可以发现,在程序运行完map dict;时,cmp_str的构造函数和析构函数都已调用完毕,接下还map内部会调用cmp_str函数对象来进行查找对应的key值,最后查到元素d。

但是为什么内部实现的时候调用cmp_str函数对象时没有再次构造匿名对象,或者为什么一开始构造完毕后有析构掉呢,这样会不会每次都浪费系统的构造资源。。。
带着这个疑问,我就做了下面的测试:

class cmp_str
{
public:
    cmp_str()
    {
        cout << "1111111" << endl;
    }

    bool operator()(char const *a, char const *b) const
    {
        return std::strcmp(a, b) < 0;
    }

    ~cmp_str()
    {
        cout << "222222222" << endl;
    }
};


template <class T,class T1>

class Test
{
public:
    Test();
    ~Test();
    void find(T1 c);
private:
    T cmp;
};

template<class T, class T1>
Test::Test()
{

}

template<class T, class T1>
Test::~Test()
{

}

template<class T, class T1>
void Test::find(T1 c)
{
    if (cmp(c, "abc"))
    {
        cout << "找到了" << "  " << c << endl;
    }
}

int main()
{
    Testchar*> t;
    char* c = new char[4];
    strcpy(c, "aaa");
    t.find(c);
    strcpy(c, "aab");
    t.find(c);
    strcpy(c, "abb");
    t.find(c);
    system("pause");
    return 0;
}

运行效果图:
STL学习笔记:map容器里find函数的第三个参数实现原理,函数对象(仿函数)_第2张图片

这里可以看出,自己模拟实现的map(这里简单只存一个key值,运用Test类模拟)里通过has-a的关系内部包含一个cmp_str对象,调用find()函数时直接调用cmp仿函数进行查找。可是在运行完Test t;时,只是调用了构造函数,没有调用析构函数,说明map内部实现不仅仅是简单的包含关系。
接下来,我把Test里的私有成员T cmp;改为T *cmp;这样的话就不会主动调用cmp_str的构造函数,我们接下来在Test的构造函数自己new一个cmp_str对象:cmp = new T;
为了在执行完cmp_str的构造函数以后直接调用cmp_str的析构函数,所以new完以后直接delete。这样就可以实现输出完111111直接输出22222的现象。至于为什么这样,后面解释。
别忘了改为指针以后再()运算符里要对cmp指针进行解指针,即将if (cmp(c, “abc”))改为
if ((*cmp)(c, “abc”))。

改后的代码:


class cmp_str
{
public:
    cmp_str()
    {
        cout << "1111111" << endl;
    }

    bool operator()(char const *a, char const *b) const
    {
        return std::strcmp(a, b) < 0;
    }

    ~cmp_str()
    {
        cout << "222222222" << endl;
    }
};

template <class T,class T1>

class Test
{
public:
    Test();
    ~Test();
    void find(T1 c);
private:
    T *cmp;
};

template<class T, class T1>
Test::Test()
{
    cmp = new T;

    delete cmp;
}

template<class T, class T1>
Test::~Test()
{

}

template<class T, class T1>
void Test::find(T1 c)
{
    if ((*cmp)(c, "abc"))
    {
        cout << "找到了" << "  " << c << endl;
    }
}



int main()
{
    Testchar*> t;
    char* c = new char[4];
    strcpy(c, "aaa");
    t.find(c);
    strcpy(c, "aab");
    t.find(c);
    strcpy(c, "abb");
    t.find(c);
    system("pause");
    return 0;
}

运行效果:
STL学习笔记:map容器里find函数的第三个参数实现原理,函数对象(仿函数)_第3张图片

此时,即实现了map函数里的比较函数原理。
在这里,为什么new完以后又直接delete呢?我猜测有两点:
1.运用指针代替直接包含对象,节省空间。因为不知道对象的内存大小有多大,而指针一般都为4字节,节省了空间。
2.在Test构造函数里new一下是为了调用cmp_str的构造函数,这样就可以初始化一下cmp_str类的其他数据成员,不仅仅是只有重载()运算符,这里只是简单的举例。而后直接调用delete,运行析构函数,也是为了节省内存吧。
其实不用new和delete也可以直接运用if ((*cmp)(c, “abc”)),这样就不用调用cmp_str的构造函数了。而当直接在类的声明里实现()运算符的定义从而形成内联作用会加快程序的效率,这也可能是运用函数对象比运用函数指针效率高的原因之一吧。
总感觉哪里有点不对,分析STL的源码也没有找到合适的理由和实现。以上仅仅是我简单的猜测,有更好的解答的请评论联系我,共同学习。

你可能感兴趣的:(学习笔记)