从小函数和小类看大设计----小话c++(4)

[Mac 10.7.1  Lion  Intel-based  x64  gcc4.2.1  xcode4.2]


大道理大家都会说,因为书上到处都会写。

不喜欢讲大道理,道理自在真实的事实中。


Q: 如果需要提供一个计算两个元素较大值的模块,该提供什么样的代码?

A: 当然,不能轻易将元素认为是整形,这样可能导致很多时候无法使用。既然如此,模板成为一种很好地选择。


Q: 就类似如下代码么?

template<class T>
T   max(T a, T b)
{
    return a > b ? a : b;
}

A: 当然,这个代码的逻辑是没错的。不过欠缺对于元素为对象的考虑,那样的话,传参可能耗用可以节省的空间和时间。


Q: 那就修改为如下:

template<class T>
T   max(T &a, T &b)
{
    return a > b ? a : b;
}

A: 根据此例子,写如下测试代码:
#include <iostream>

using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

namespace me
{

    template<class T>
    T   max(T &a, T &b)
    {
        return a > b ? a : b;
    }
    
}

int main (int argc, const char * argv[])
{
    me::max(1, 2);
    return 0;
}

很不幸,出现了编译错误:
No matching function for call to 'max'
Candidate function [with T = int] not viable: no known conversion from 'int' to 'int &' for 1st argument

意思就是参数1无法和int &进行适当的转换。这个可以理解,因为max的参数形式为T &,它代表外部传入的一个变量,可能内部会修改。但是却传入了整形字面量1,这让编译器很为难。const引用也就有它的用处了。


Q: 再次修改,如下:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

namespace me
{

    template<class T>
    T   max(const T &a, const T &b)
    {
        return a > b ? a : b;
    }
    
}

int main (int argc, const char * argv[])
{
    me::max(1, 2);
    return 0;
}

编译,ok了。

A: 返回值为T类型,它可能需要构造,为何不把返回值变成const T &类型呢?


Q: 问题是函数返回值为const T &类型,可行吗?

A: 首先,函数返回值只可能当做右值,所以用T &作为返回值类型是可行的; 同样,用const修饰依然不会让外部可能具有的赋值操作出现问题,const对象可以赋值给非const对象。再者,返回的为传入的a或者b,不会出现返回局部引用的问题。


Q: 修改如下:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

namespace me
{

    template<class T>
    const T& max(const T& a, const T& b)
    {
        return a > b ? a : b;
    }
    
}

int main (int argc, const char * argv[])
{
    int ret = me::max(1, 2);
    return 0;
}

编译没问题。


A: 如果,a和b都是对象,那么此函数可以通过编译的前提是T具有重载大于运算符的函数。但是在STL中,很多代码已经默认将重载小于和重载等于运算符作为了标准,即在>, < , ==, >=, <=, !=这些运算符中,只需要重载<和==运算符即可把其它的形式用已有的重载来实现。所以,代码可以修改为使用<运算符。


Q: 如下:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

namespace me
{

    template<class T>
    const T& max(const T& a, const T& b)
    {
        return a < b ? b : a;
    }
    
}

int main (int argc, const char * argv[])
{
    int ret = me::max(1, 2);
    COUT_ENDL(ret)
    return 0;
}

A: 对于max函数,它的实现很简单,使用一下inline不失为一种好选择。
#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

namespace me
{

    template<class T>
    inline 
    const T& max(const T& a, const T& b)
    {
        return a < b ? b : a;
    }
    
}

int main (int argc, const char * argv[])
{
    int ret = me::max(1, 2);
    COUT_ENDL(ret)
    return 0;
}

Q: mac系统自带的STL中max函数代码是什么样的?

A: 如下:

  template<typename _Tp>
    inline const _Tp&
    max(const _Tp& __a, const _Tp& __b)
    {
      // concept requirements
      __glibcxx_function_requires(_LessThanComparableConcept<_Tp>)
      //return  __a < __b ? __b : __a;
      if (__a < __b)
	return __b;
      return __a;
    }

Q: 正如刚刚所说,可以为对象重载<和==运算符,其它几种相关的关系运算符可以通过它们来实现,具体写代码的时候,难道每个类依然需要实现其它几种重载函数(虽然仅仅是编写<和==运算符操作的变形)吗?

A: 正因为,其它几种运算符的操作是类似的,可以把它们全部抽象出来,使用模板实现。如下:

    template <class _Tp>
      inline bool
      operator!=(const _Tp& __x, const _Tp& __y)
      { return !(__x == __y); }

    template <class _Tp>
      inline bool
      operator>(const _Tp& __x, const _Tp& __y)
      { return __y < __x; }

    template <class _Tp>
      inline bool
      operator<=(const _Tp& __x, const _Tp& __y)
      { return !(__y < __x); }

    template <class _Tp>
      inline bool
      operator>=(const _Tp& __x, const _Tp& __y)
      { return !(__x < __y); }

当然这些代码可以不用再写一遍了,可以参考std命名空间内rel_ops命名空间中的实现代码。


Q: 对于类构造函数中的初始化列表,它的好处在哪里?

A: 如下例子:

#include 
  
  
   
   
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

  
  
class Student
{
public:
    Student(int age)
    {
        _age = age;
    }
    ~Student(){ }
    
public:
    int age() { return _age; }
    int age() const { return _age; }
    
private:
    int _age;
};

int main (int argc, const char * argv[])
{
    Student s(21);
    return 0;
}

编译,得到Student构造函数的汇编代码:

0x0000000100000cdc <_ZN7StudentC1Ei+0>:	push   %rbp
0x0000000100000cdd <_ZN7StudentC1Ei+1>:	mov    %rsp,%rbp
0x0000000100000ce0 <_ZN7StudentC1Ei+4>:	mov    %rdi,-0x8(%rbp)
0x0000000100000ce4 <_ZN7StudentC1Ei+8>:	mov    %esi,-0xc(%rbp)
0x0000000100000ce7 <_ZN7StudentC1Ei+11>:	mov    -0x8(%rbp),%rax
0x0000000100000ceb <_ZN7StudentC1Ei+15>:	mov    -0xc(%rbp),%ecx
0x0000000100000cee <_ZN7StudentC1Ei+18>:	mov    %ecx,(%rax)
0x0000000100000cf0 <_ZN7StudentC1Ei+20>:	pop    %rbp
0x0000000100000cf1 <_ZN7StudentC1Ei+21>:	retq 

修改Student类构造函数变成如下:

Student(int age):_age(age) { }

再次编译,得到它的汇编形式:

0x0000000100000cdc <_ZN7StudentC1Ei+0>:	push   %rbp
0x0000000100000cdd <_ZN7StudentC1Ei+1>:	mov    %rsp,%rbp
0x0000000100000ce0 <_ZN7StudentC1Ei+4>:	mov    %rdi,-0x8(%rbp)
0x0000000100000ce4 <_ZN7StudentC1Ei+8>:	mov    %esi,-0xc(%rbp)
0x0000000100000ce7 <_ZN7StudentC1Ei+11>:	mov    -0x8(%rbp),%rax
0x0000000100000ceb <_ZN7StudentC1Ei+15>:	mov    -0xc(%rbp),%ecx
0x0000000100000cee <_ZN7StudentC1Ei+18>:	mov    %ecx,(%rax)
0x0000000100000cf0 <_ZN7StudentC1Ei+20>:	pop    %rbp
0x0000000100000cf1 <_ZN7StudentC1Ei+21>:	retq  

可以发现二者是一致的,说明初始化列表不是神秘的不行,在这里二者作用一致。当然这是对于简单的类型。


Q: 对于复杂的类型,是否依然一致呢?

A: 如下代码:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

class Person
{
public:
    Person() { }
    Person(char sex):_sex(sex)
    {
        std::cout << "enter Person construct function..." << std::endl;
    }
    ~Person() { }
    Person(const Person& p)
    {
        std::cout << "enter Person copy construct function..." << std::endl;
        _sex = p._sex;
    }
    
    Person& operator=(const Person& p)
    {
        std::cout << "enter Person assign construct function..." << std::endl;
        if(&p != this)
        {
            _sex = p._sex;
        }
        return *this;
    }
    
private:
    char _sex;
};

class Student
{
public:
    Student(int age, Person &p):_age(age), _p(p) 
    {
        
    }
    ~Student(){ }
    
public:
    int age() { return _age; }
    int age() const { return _age; }
    
private:
    int     _age;
    Person  _p;
};

int main (int argc, const char * argv[])
{
    Person  p('m');
    Student s(21, p);
    return 0;
}

运行结果:

可以看到,除了p对象单独调用构造函数外,s对象的构造对于Person类仅仅调用了拷贝构造函数。


Q: 如果不是采用初始化列表呢?

A: 修改代码如下:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

class Person
{
public:
    Person() { }
    Person(char sex):_sex(sex)
    {
        std::cout << "enter Person construct function..." << std::endl;
    }
    ~Person() { }
    Person(const Person& p)
    {
        std::cout << "enter Person copy construct function..." << std::endl;
        _sex = p._sex;
    }
    
    Person& operator=(const Person& p)
    {
        std::cout << "enter Person assign construct function..." << std::endl;
        if(&p != this)
        {
            _sex = p._sex;
        }
        return *this;
    }
    
private:
    char _sex;
};

class Student
{
public:
    Student(int age, Person &p):_age(age)
    {
        _p = p;
    }
    ~Student(){ }
    
public:
    int age() { return _age; }
    int age() const { return _age; }
    
private:
    int     _age;
    Person  _p;
};

int main (int argc, const char * argv[])
{
    Person  p('m');
    Student s(21, p);
    return 0;
}

运行结果如下:

可以发现,它和上面的执行结果的差距是:它多执行了一次构造函数。这也正是可能导致效率降低的原因。同理,如果构造函数参数不是Person引用,是Person类型的话,同样也有类似的差距。同时,也可以看出初始化列表上构造子对象和在构造函数内部构造的区别。


Q: 这么说来,是不是如果采用初始化列表的形式的话,Person构造函数的默认构造函数可以不用写了?

A: 是的。如下代码,不采用初始化列表的方式,Person的默认构造函数没有写出导致编译错误:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

class Person
{
public:
    Person(char sex):_sex(sex)
    {
        std::cout << "enter Person construct function..." << std::endl;
    }
    ~Person() { }
    Person(const Person& p)
    {
        std::cout << "enter Person copy construct function..." << std::endl;
        _sex = p._sex;
    }
    
    Person& operator=(const Person& p)
    {
        std::cout << "enter Person assign construct function..." << std::endl;
        if(&p != this)
        {
            _sex = p._sex;
        }
        return *this;
    }
    
private:
    char _sex;
};

class Student
{
public:
    Student(int age, Person &p):_age(age)
    {
        _p = p;
    }
    ~Student(){ }
    
public:
    int age() { return _age; }
    int age() const { return _age; }
    
private:
    int     _age;
    Person  _p;
};

int main (int argc, const char * argv[])
{
    Person  p('m');
    Student s(21, p);
    return 0;
}

编译错误:

No matching function for call to 'Person::Person()'

Q: 构造函数为什么没有返回值?

A: 问题是有返回值,返回什么。假设返回一个整数,那么例如Person p = Person('m');之类的代码,就类似把一个整数赋值给变量p,这如何合理呢?


Q: 析构函数为什么也没有返回值?

A: 如果是函数的局部对象,c++规定局部对象最后的析构由编译器生成对应的代码,既然由编译器生成对应的代码,那么程序员怎么再取到返回值?


Q: 为什么不建议在构造函数中抛出异常?

A: 因为异常机制导致执行路径改变,最终析构函数很可能不会被执行,导致内存泄露等问题。如下代码:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

class Test
{
public:
    Test()
    {
        std::cout << "Test is called..." << std::endl;
        throw 1;
    }
    
    ~Test()
    {
        std::cout << "~Test is called..." << std::endl;
    }
};

int main (int argc, const char * argv[])
{
    Test t;
    return 0;
}

运行结果:

可以看出,析构函数没有正常执行,而是输出了异常提示。当然,处理这种问题,可以使用auto_ptr来解决。


Q: 析构函数为什么不建议直接抛出异常?

A: 这同样来源于抛出异常后,此对象究竟是什么状态。到底是可以不用管对象了,还是得管,后面还要继续操作。这些不确定也使得标准可能让它变得让程序员更无法确定。当然,不同的编译器对于析构函数抛出异常是否直接调用terminate也不尽相同,如下一个测试代码:

#include <iostream>
using namespace std;

#define COUT_ENDL(str)      std::cout << #str << " is " << (str) << std::endl;

class Test
{
public:
    Test()
    {
        std::cout << "Test is called..." << std::endl;
    }
    
    ~Test()
    {
        std::cout << "~Test is called..." << std::endl;
        throw 1;
    }
};

int main (int argc, const char * argv[])
{
    try
    {
        Test *t = new Test;
        delete t;
    }
    catch(...)
    {
        std::cout << "catch exception..." << std::endl;
    }
    
    int i = 1;
    COUT_ENDL(i)
    
    return 0;
}

运行结果:

它执行到了后面的输出i的代码。


Q: 那么,可能在构造函数和析构函数中抛出的异常,该在哪里处理呢?

A: 其实,构造完毕后,在其它函数对对象的处理可以进行相关判断,来确定是否已经发生了问题。析构函数可能出现异常的地方同样可以使用标志信息来记录传递到外层。


xichen

2012-6-1 13:16:44


  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值