posts - 13, comments - 4, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Imperfect C++ 读书笔记(三)

Posted on 2008-11-20 00:28 Batiliu 阅读(364) 评论(0)  编辑 收藏 引用 所属分类: 读书笔记

数组和指针

首先我们来看一个经典的C/C++求数组元素个数的解决方案:

#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof((x)[0]))

利用C/C++编译器将表达式 ar[n] 在编译器解释为 *(ar+n) 的特性,我们可以提供一个更“先进”的版本:

#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof(0[(x)]))

这种下标索引的可交换性,只对内建的下标索引操作符有效,这一限制可以被用于约束NUM_ELEMENTS宏只对数组/指针有效,而拒绝重载了下标索引操作符的类类型。

 

接下来,我们来看NUM_ELEMENTS的实际运用:

int ar[10];
cout << NUM_ELEMENTS(ar) << endl;        // 毋庸置疑,输出为10。
...
void fn(int ar[10])
{
    cout << NUM_ELEMENTS(ar) << endl;    // 本行结果呢?如果你说10,那么将会使你大失所望,
}                                        // 事实上,程序输出为1。

看起来一切都井井有条,问题究竟出在哪里呢?呃,是这样的,在C/C++中你无法将数组传给函数!在C里面,数组被传递函数时总是被转换成指针,非常干脆地阻止了你获取数组大小的企图。而C++基于兼容性的考虑,亦是如此。

所以先前给出的NUM_ELEMENTS宏定义依赖于预处理器进行的文本替换,因而存在一个严重的缺陷:如果我们(不小心)将它应用到一个指针身上,文本替换出来的结果将是错误的。

 

幸运的是,我们可以利用大多数现代编译器都支持的一个特性来将数组和指针区别对待,那就是:从数组到指针的转换(退化)在引用类型的模板实参决议中并不会发生。因而我们可以重新定义NUM_ELEMENTS宏为:

template<int N>
struct array_size_struct
{
    byte_t c[N];
};
 
template<typename T, int N>
array_size_struct<N> static_array_size_fn(T(&)[N]);
 
#define NUM_ELEMENTS(x) sizeof(static_array_size_fn(x).c)

其基本原理是:声明(但不定义)一个模板函数,它接受一个元素类型为T、大小为N的数组的引用。这样一来,指针类型以及用户自定义类型就被拒之门外了(编译报错)。并且由于C++标准中,sizeof()的操作数不会被求值,所以我们无需定义static_array_size_fn()函数体,从而上述设施完全是零代价的。没有任何运行时开销,也不会导致代码膨胀。

 

让我们回到“C/C++数组在被传递给函数时会退化成指针”的问题上来,如果我们在现实中需要将一个将任意长度的数组传递给一个期望接受数组的函数,那么该怎么办呢?困惑的实质在于数组的大小在传递过程中丢失了,因此,如果我们可以找到一种将数组大小随之传递给函数的机制,问题就会迎刃而解。有了上面宏定义的经验,通过模板我们找到一个解决方案:

template<typename T>
class array_proxy
{
public:
    typedef T               value_type;
    typedef array_proxy<T>  class_type;
    typedef value_type *    pointer;
    typedef value_type *    const_pointer;      // Non-const!
    typedef value_type &    reference;
    typedef value_type &    const_reference;    // Non-const!
    typedef size_t          size_type;
// 构造函数
public:
    template<size_t N>
    explicit array_proxy(T(&t)[N])    // 元素类型为T的数组
        : m_begin(&t[0])
        , m_end(&t[N])
    {}
    template<typename D, size_t N>
    explicit array_proxy(D(&d)[N])    // 元素类型为T兼容类型的数组
        : m_begin(&d[0])
        , m_end(&d[N])
    {
        constraint_must_be_same_size(T, D);    // 确保D和T大小相同
    }
    template<typename D>
    array_proxy(array_proxy<D> &d)
        : m_begin(d.begin())
        , m_end(d.end())
    {
        constraint_must_be_same_size(T, D);    // 确保D和T大小相同
    }
// 状态
public:
    pointer             base();
    const_pointer       base() const;
    size_type           size() const;
    bool                empty() const;
    static size_type    max_size();
// 下标索引操作符
public:
    reference        operator [](size_t index);
    const_reference  operator [](size_t index) const;
// 迭代操作
public:
    pointer          begin();
    const_pointer    begin() const;
    pointer          end();
    const_pointer    end() const;
// 数据成员
private:
    pointer const m_begin;
    pointer const m_end;
// 声明但不予实现
private:
    array_proxy & operator =(array_proxy const &);
};
 
// 转发函数
template<typename T, size_t N>
inline array_proxy<T> make_array_proxy(T(&t)[N])
{
    return array_proxy<T>(t);
}
template<typename T>
inline array_proxy<T> make_array_proxy(T * base, size_t size)
{
    return array_proxy<T>(base, size);
}

客户代码修改为:

void process_array(const array_proxy<int> & ar)
{
    std::copy(ar.begin(), ar.end(), ostream_iterator<int>(cout, " "));
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    int ar[5] = {0, 1, 2, 3, 4};
    
    process_array(make_array_proxy(ar));
 
    return 0;
}

我们的问题终于有了一个彻底的解决方案。该解决方案是高效的(在任何一个说得过去的编译器上它都没有任何额外的开销),是类型安全的,并且完全使得函数的设计者能够防止潜在的误用(更确切的说,让代码能够更强的抵御派生类数组的误用)。此外,它还足够智能,允许派生类跟父类具有相同大小的情况下,它们的数组被“代理”。

最后一个优点是现在再也不可能将错误的数组长度传给被调函数了,以前我们惯用的使用两个参数(一个传递数组指针,一个传递数组长度)的函数版本中误传长度的危险是时时存在的。这个优势使我们得以遵从DRY(Don't Repeat Yourself!)原则。

 

NULL宏

在C语言中,void*类型可以被隐式地转换为其他任何指针类型,所以我们可以将NULL定义为((void*)0),从而跟其他任何指针类型间实现互相转换。然而,C++不允许从void*到任何指针的隐式转换,又因为C++中0可以被转换为任何指针类型,因此,C++标准规定:NULL宏是一个由实现定义的C++空指针常量....其可能的定义方式包括0和0L,但绝对不是(void*)0。

由于0不可置疑的可以转换成任何整型,甚至wchar_t和bool,以及浮点类型,这就意味着,使用NULL的时候,类型检查将不再发生,我们很容易毫无察觉的走向厄运的深渊,连个警告都没有。考虑如下情况:

// 自定义的字符串类
//
class String
{
    explicit String(char const *s);                  // 接受外界传入的空指针
    explicit String(int cch, char chInit = '\0');    // 根据可能被使用的字符数估计,来初始化底层存储
};

现在当我们用NULL做参数构造String时,第二个构造函数会被调用!也许和你的初衷不同,编译器却会一声不吭的编译通过。这可不妙。如果你将int改为size_t(或short、或long、或任何不是int的内建类型),编译器将会在两个转换之间左右为难,结果是得到一个二义性错误。

 

我们想要个完善的空指针关键字!很快作者想出了办法,你不应该感到惊讶,解决方案离不开模板:

struct NULL_v
{
// 构造函数
public:
    NULL_v()
    {}
// 转换操作符
public:
    template<typename T>
    operator T* () const
    {
        return 0;
    }
    template<typename T2, typename C>
    operator T2 C::*() const
    {
        return 0;
    }
    template<typename T>
    bool equals(T const & rhs) const
    {
        return rhs == 0;
    }
// 声明但不予实现
private:
    void operator &() const;    // Scott: 纯粹是值的东西的地址是没有任何意义的。
    NULL_v(NULL_v const &);
    NULL_v& operator =(NULL_v const &);
};
 
template<typename T>
inline bool operator ==(NULL_v const & lhs, T const & rhs)
{
    return lhs.equals(rhs);
}
 
template<typename T>
inline bool operator ==(T const & lhs, NULL_v const & rhs)
{
    return rhs.equals(lhs);
}
 
template<typename T>
inline bool operator !=(NULL_v const & lhs, T const & rhs)
{
    return !lhs.equals(rhs);
}
 
template<typename T>
inline bool operator !=(T const & lhs, NULL_v const & rhs)
{
    return !rhs.equals(lhs);
}

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理