C++字符串大小比较的踩坑

 
Category: C_C++

最近学习C++11中的initializer_list这一新特性, 一个实例是关于字符串比较大小的, 代码如下:

cout << max({string("Fff"), string("Eoa"), string("Acc")}) << endl;

运行结果如下:

Fff

很显然最大值就是字符串"Fff"(依字典序), 但是我觉得string这个关键字可以去掉, 也就是将上述代码改为:

cout << max({"Fff", "Eoa", "Acc"}) << endl;

但是这时候结果就变成了:

Acc

这是为什么呢?

分析

一开始我以为问题出在max()函数的身上, 找出源码后发现其本质还是逐元素比较, 每次更新最大值, 只不过用到了一些initializer_list的东西, 这里列出核心的代码:

template<class ForwardIterator, class Compare>
ForwardIterator __max_elem(ForwardIterator first, ForwardIterator last, Compare comp) {
    if (first == last)
        return first;
    ForwardIterator result = first;
    while (++first != last)
        if (comp(result, first))//result<first
            result = first;
    return result;
}


struct Iter {
    //仿函数
    template<typename I1, typename I2>
    bool operator() (I1 it1, I2 it2) const
    {return *it1 < *it2;}
};


inline Iter __iter_less_iter() {
    return Iter();
}


template<class T>
inline T max_elem(T first, T last) {
    return __max_elem(first, last, __iter_less_iter());
}


template <typename T>
inline T max(initializer_list<T> l) {
    return *max_elem(l.begin(), l.end());
}

思路的话这里不进一步说了, 熟悉模版的话很好理解的. 其实核心就是通过迭代器读取首元素和下一个元素, 比较之后更新result, 里面的迭代器解包用到了仿函数, 通过struct构造了一个结构体实现.


那么不是这个问题的话, 又是什么呢? 由于C++的主要特性就是面向对象, 那么就可以将这些字符串的类型输出出来, 看看能不能找出问题所在:

//C 风格的char-array, size=4
cout << typeid("abc").name() << endl;
cout << typeid("a").name() << endl;
cout << typeid('a').name() << endl;
// ----------------------------------
cout << typeid(string("abc")).name() << endl;
cout << typeid(string("a")).name() << endl;
// cout << typeid(string('a')).name() << endl; //error cannot conversion from char to string

运行结果如下:

A4_c
A2_c
c
NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE

可以看出, 是否加string的区别还是很大的, 不加的话还是C-style的字符数组, 而数组之间比较大小默认比较的是首地址, 即地址对应的十六进制值, 可以看下面的一段代码:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    char s1[]="aae", s2[]="cde";
    printf("s1:%p, s2:%p, s1>s2:%d\n", s1,s2,s1>s2);
    char *s3="aae", *s4="cde";
    printf("s3:%p, s4:%p, s3>s4:%d\n", s3,s4,s3>s4);
    return 0;
}

这段代码运行结果为:

warning: array comparison always evaluates to a constant [-Wtautological-compare]
    printf("s1:%p, s2:%p, s1>s2:%d\n", s1,s2,s1>s2);
                                               ^
1 warning generated.
s1:0x16f57347c, s2:0x16f573478, s1>s2:1
s3:0x10088ff98, s4:0x10088ff9c, s3>s4:0

可以看出, 比较都是通过首地址来进行的, 这就不难解释为什么直接调用cout << max({"a", "c", "b"}) << endl;会得到最后一个元素"b"了.


这里还有另外一个知识点, 那就是单引号和双引号, 学过C语言的话大家应该非常熟悉了, 单引号只能有单个字符, 这个字符为0~255的ASCII字符, 只占用一个字节, 所以这时候直接拿来比较的话不会出错, 即:

cout << max({'a', 'c', 'b'}) << endl;
// output: c

但是一旦变成双引号, 就默认变成字符数组, 也就是char[]类型, 这里的比较也就存在于首地址之间了.


更进一步, 通过上面的例子, 还可以发现一个问题, 通过char[]char*得到的两种字符数组并不一样, 除了一般的下标赋值等问题, 剩下的就是创建变量分配内存的问题, 指针是顺序分配, 而数组是逆序, 下面的一个回答1很好地说明了这一点:

The diference is the STACK memory used.

For example when programming for microcontrollers where very little memory for the stack is allocated, makes a big difference.

  char a[] = "string"; // the compiler puts {'s','t','r','i','n','g', 0} onto STACK 
  char *a = "string";  // the compiler puts just the pointer onto STACK 
                        // and {'s','t','r','i','n','g',0} in static memory area.

所以可以说使用initializer_list读取字符串进行比较的时候, 采用的是char*的形式, 所以才会造成max总是返回initializer_list的最后一个元素这种情况.