百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

zhezhongyun 2025-09-06 16:13 17 浏览

若文章对您有帮助,欢迎关注 程序员小迷 。助您在编程路上越走越好!




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









微风不燥,阳光正好,你就像风一样经过这里,愿你停留的片刻温暖舒心。

我是 程序员小迷 (致力于C、C++、C#、Android、iOS、Java、Kotlin、Objective-C、Swift、Shell、JavaScript、TypeScript、Python等编程技术的技巧经验分享),若作品对您有帮助,请关注、分享、点赞、收藏、在看、喜欢,您的支持是我们为您提供帮助的最大动力。

若文章对您有帮助,欢迎关注 程序员小迷 。助您在编程路上越走越好!

相关推荐

perl基础——循环控制_principle循环

在编程中,我们往往需要进行不同情况的判断,选择,重复操作。这些时候我们需要对简单语句来添加循环控制变量或者命令。if/unless我们需要在满足特定条件下再执行的语句,可以通过if/unle...

CHAPTER 2 The Antechamber of M de Treville 第二章 特雷维尔先生的前厅

CHAPTER1TheThreePresentsofD'ArtagnantheElderCHAPTER2TheAntechamber...

CHAPTER 5 The King&#39;S Musketeers and the Cardinal&#39;S Guards 第五章 国王的火枪手和红衣主教的卫士

CHAPTER3TheAudienceCHAPTER5TheKing'SMusketeersandtheCardinal'SGuard...

CHAPTER 3 The Audience 第三章 接见

CHAPTER3TheAudienceCHAPTER3TheAudience第三章接见M.DeTrévillewasatt...

别搞印象流!数据说明谁才是外线防守第一人!

来源:Reddit译者:@assholeeric编辑:伯伦WhoarethebestperimeterdefendersintheNBA?Here'sagraphofStea...

V-Day commemorations prove anti-China claims hollow

People'sLiberationArmyhonorguardstakepartinthemilitaryparademarkingthe80thanniversary...

EasyPoi使用_easypoi api

EasyPoi的主要特点:1.设计精巧,使用简单2.接口丰富,扩展简单3.默认值多,writelessdomore4.springmvc支持,web导出可以简单明了使用1.easypoi...

关于Oracle数据库12c 新特性总结_oracle数据库12514

概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...

【开发者成长】JAVA 线上故障排查完整套路!

线上故障主要会包括CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如jstack、jmap等工具也是不囿于一个方面的问题...

使用 Python 向多个地址发送电子邮件

在本文中,我们将演示如何使用Python编程语言向使用不同电子邮件地址的不同收件人发送电子邮件。具体来说,我们将向许多不同的人发送电子邮件。使用Python向多个地址发送电子邮件Python...

提高工作效率的--Linux常用命令,能够决解95%以上的问题

点击上方关注,第一时间接受干货转发,点赞,收藏,不如一次关注评论区第一条注意查看回复:Linux命令获取linux常用命令大全pdf+Linux命令行大全pdf为什么要学习Linux命令?1、因为Li...

linux常用系统命令_linux操作系统常用命令

系统信息arch显示机器的处理器架构dmidecode-q显示硬件系统部件-(SMBIOS/DMI)hdparm-i/dev/hda罗列一个磁盘的架构特性hdparm-tT/dev/s...

小白入门必知必会-PostgreSQL-15.2源码编译安装

一PostgreSQL编译安装1.1下载源码包在PostgreSQL官方主页https://www.postgresql.org/ftp/source/下载区选择所需格式的源码包下载。cd/we...

Linux操作系统之常用命令_linux系统常用命令详解

Linux操作系统一、常用命令1.系统(1)系统信息arch显示机器的处理器架构uname-m显示机器的处理器架构uname-r显示正在使用的内核版本dmidecode-q显示硬件系...

linux网络命名空间简介_linux 网络相关命令

此篇会以例子的方式介绍下linux网络命名空间。此例中会创建两个networknamespace:nsa、nsb,一个网桥bridge0,nsa、nsb中添加网络设备veth,网络设备间...