友元 异常

一、友元

     在前文中,我们将一些友元函数用于类的扩展接口,而类并非只能拥有友元函数,同时,也可以将作为友元。在这种情况下,友元类的所有方法都能访问原始类的私有成员和保护成员。也可将特定的成员函数指定为另一个类的友元。

  1、友元类

   友元类的声明应该类似如下:

friend class Remote; //必须在原有类中声明
//example:

class TV
{
public:
    friend class Remote;
}

  2、友元成员函数

若仅仅需要让某一个成员函数成为另一个类的友元函数,那么需要做到以下步骤:

1、先向前声明(forward declaration)原始类。

2、声明友元类,此时只能包含成员函数的声明。

3、声明基类。

4、定义友元类,(并通过使用incline,使其成为为内联方法)

例:

class TV;
class Remote
{
  ……
  ……
public:
   void set_chan(Tv & t, int c);
}


class TV
{
public:
    friend void Remote::set_chan(Tv & t, int c);
}

incline void Remote::set_chan(Tv & t, int c){t.channel = c;}

解释原因:

1、为什么需要想前声明TV类?

答:因为在Remote类中包含了Tv类的对象的引用。

2、为什么不先声明Remote类?

答:因为TV类中也包含了友元类friend class Remote 的声明。

3、那能否先提前声明Remote,在声明TV类,再声明Remote类?

答:不能,因为在编译器看到Remote的一个方法被声明为TV类的友元前,应该先看到Remote类以及set_chan的声明。(不然你为什么说set_chan()是TV的友元成员函数呢?)

4、为什么不直接在Remote类的声明中定义类方法?

答:因为类方法中包含了对TV类方法的访问,而此时编译器仍不知道TV类有什么方法。

注:

1、Tv类决定谁是它的友元。

2、友元类的声明并不需要向前定义。

 

  3、其他友元关系

 若需要一些A类方法影响B类对象的同时,B类方法也能影响A类对象,则可以让A、B类互相成为对方的友元。

但需要注意的是: 对于使用A类对象的B类方法, 其必须在声明A类后定义.

class TV
{
   friend class Remote;
public:
   void buzz(Remote & r);
};

class Remote
{
   friend class TV;
public:
   void Bool volup(TV & t){t.volup();}
};

inline void TV::buzz(Remote & r)
{...}

4、共同的友元。

需要使用友元的另一种情况,函数需要访问两个类的私有数据。在这种情况下,可以把它设置为两个类的友元:只需要在两个类中同时包含友元函数的声明即可,同时,也应该注意也要前向声明。

例:

class Analyzer;
class Probe;
{
   friend void sync(Analyzer & a, const Probe & p);
   friend void sync(Probe & p, const Analyzer & a);
};

class Analyzer
{
   friend void sync(Analyzer & a, const Probe & p);
   friend void sync(Probe & p, const Analyzer & a);
};

incline friend void sync(Analyzer & a, const Probe & p)
{}
incline friend void sync(Probe & p, const Analyzer & a)
{}

二、嵌套类。

在C++中,可以将类声明放在另一个类中。在另一个类声明的类被称为嵌套类(nested class):通常用于提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;当且仅当声明位于共有部分,才能在包含类的外面使用嵌套类,同时必须使用作用域解析运算符。

对类进行嵌套与包含不同,包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

class Queue
{
private:
   struct Node{Item item;struct Node *next;};
};

由于结构是一种其成员在默认成员下为公有的类,所以Node是一个嵌套类。但是,该定义并没有充分利用类的功能——它没有构造函数。具体的说,它没有显示构造函数。

因此,修改为:

class Queue
{
   class Node
   {
   public:
      Item item;
      Node *next;
      Node(const Item & i): item(i),next(0){}
   };
};    

   1、嵌套类和访问权限

有两种访问权限适合嵌套类。嵌套类的声明位置决定了嵌套类的作用域,即程序的哪些部分可以创建这种类的对象。

     1、作用域。

         1、若嵌套类在另一个类的私有成员中声明,只有后者知道它。

         2、若嵌套类是在另一个类的保护部分中声明,则它对于后者以及其派生类可见,同时对外界不可见。

         3、若嵌套类是在另一个类的共有部分中声明,则允许后者、后者的派生类以及外部世界使用,其中,外界使用时要同时使用类限定符。

      2、访问控制。

类可见后,起作用的将是访问控制。对嵌套类访问权的控制规则与对常规类相同。即:Queue类对象只能访问Node对象的公有成员,同时,若将Node限定为Queue的私有成员,则Node对外界不可见。

  2、模板中的嵌套

将某个类转换为模板时,不会由于它包含嵌套类而带来问题。

#ifndef QUEUETP_H_
#define QUEUETP_H_

template<class Item>
class QueueTP
{
private:
   enum {Q.SIZE = 10};
   class Node
   {
   public:
      Item item;
      Node *next;
      Node(const Item & i):item(i), next(0){}
   };
   Node * front;
   Node * rear;
   int items;
   const int qsize;
   QueueTP(const QueueTP & q):qsize(0){}
   QueueTP & operator=(const QueueTP & q){return *this;}
public:
   QueueTP(int qs = Q_SIZE);
   ~Queue();
   bool isempty()const{return item == 0;}
   bool isfull()const{return item == qsize;}
   int queuecount()const{return items;}
   bool enqueue(const Item & item);
   bool dequeue(Item & item);
QueueTp<double>dp;
QueueTp<char>cq;

以上的两个声明的创建不会导致Node类型冲突:一个的类型为QueueTp<double>::Node,而另一个的类型为QueueTp<char>::Node。

三、异常。

程序在运行阶段遇到错误时,会导致程序无法正确运行。而C++提供了一种新的工具。

1、abort() 的调用

观察算式: 2.0 * x * y/(x + y),若x = -y,将会导致异常。

而一个解决方法就是调用abort函数:它的函数原型位于cstdlib或者stdlib.h头文件中,其典型实现为向标准错误流(即cerr使用的错误流)发送消息abnormal program termination,随后终止程序。它还返回一个随实现而异常的值:或操作系统或父进程。abort是否刷新文件缓冲区则取决于实现,而exit()函数将会刷新,同时不显示消息。

注:上述文件缓冲区:用于存储读写到文件中的数据的内存区域。

使用:

int main()
{
 double x, y, z;
 std::cout<<"Enter two numbers: ";
 while (std::cin>>x>>y)
 {
   z = hmean(x,y);
   std::cout<<"Harmonic mean of "<<x<<"and " <<y
   <<" is " <<z<<std::endl;
 }
 std::cout<<"Bye!\n";
 return 0;
}

double hmean(double a, double b)
{
  if( a == -b)
   {
      std::abort();
   }
  return 2.0 * x * y/(x - y);
}

2、返回错误码。

一种比异常终止更灵活的是使用函数的返回值指出问题。

如ostream类中的get(void)成员返回下一个输入字符的ASCII码,但到达文件尾时,返回EOF(end of file),对hmean()来说,这种方法不管用:任何数值都是有效的返回值——在这种情况下,可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值指出成功或失败。而非直接引发异常。

#include<iostream>
#include<cfloat>

bool hmean(double a, double b, double * ans);

int main()
{
   using namespace std;
   double x, y, z;
   while(cin>>x>>y)
   {
       if(humean(x, y, &z))……;
   }
}

bool hmean(double a, double b, double * ans)
{
   if(a == -b)
   {
      *ans = DBL_MAX;
      return false;
   }
   else
   {
      *ans = 2.0*a*b/(a+b);
      return true;
   }
}
   

可以看到,在修改过的hmean函数中,我们在函数定义中传递指针,在函数使用中创建引用,因此会产生这样的结果:函数中的指针参数指向了对z的引用——即z本身,因此可以在通过返回值确定异常与否的同时修改z的值。

3、异常机制

C++异常是对程序运行过程中发生情况的异常情况的一种响应。异常提供了将控制权从一个程序的一部分传递到另一个部分的途径。对异常的处理有三个部分:
1、引发异常。

2、使用处理程序捕获异常。

3、使用try块。

在这个部分,我们介绍三个关键字:throw、try和catch。

其中,throw表示引发异常,紧随后的值(字符串或对象等)指出异常的特征——这个过程由程序员完成。而throw的执行则类似于返回语句它终止函数的执行,然而与之不同的是,它将控制权交给沿程序调用栈后退,直到包含try块的函数,而非交给调用程序。

在介绍try和catch前,我们先介绍异常处理程序(exception handler):用来捕获异常,位于要处理问题的程序中。

而catch关键字,则表示捕获异常。其随后的类型声明,指出异常处理程序要响应的程序类型,之后再使用一个代码块指出要采取的措施。

try块标识其中特定的异常可能会被激活的代码块,其后跟随一个或多个catch块。

以下为一个实例:

#include<iostream>
double hmean(double a, double b);

int main(void)
{
    double x, y, z;
    std::cout<<"Enter two numbers: ";
    while(std::cin>>x>>y)
    {
        try{
           z = hmean(x, y);  //可能会引发异常的代码块
        }
        catch(const char* s)
        {
           std::cout<<s<<std::endl;
           std::cout<<"Enter a new pair of numbers: ";
           continue;
        }
        std::cout<<"Harmonic mean of "<<x<<" and "<<y
                 << " is "<<z<<endl;
        std::cout<<"Enter next set of numbers<q to quit>: ";
   }
   std::cout<<"Bye!\n";
   return 0;
}
   
double hmean(double a, double b)
{
    if( a == -b)
      throw "bad hmean() arguments: a = -b is not allowed";  //若出现异常,程序终止,并退回 
               //到上一个try块,寻找与之匹配的catch, 由于我们给定const char* 的类型,因此仅与
               //仅有的一个catch匹配,则调用该代码块中的内容
    return 2.0 * a * b/(a+b);

4、将对象用作异常类型
 

通常,我们选择将对象而非字符串通过throw传递:这样做可以使用不同的异常类型区分不同函数在不同情况下引发的异常,同时catch块能根据相应的对象决定采取什么措施。

在采用对象后,上述的一部分修改为:

class bad_hmean
{
private:
	double v1;
	double v2;
public:
	bad_hmean(double a = 0, double b = 0) :v1(a), v2(b) {}
	void mesg();
};

inline void bad_hmean::mesg()
{
	using namespace std;
	cout << "hmean(" << v1 << ", " << v2 << "): "
		<< "invalid arguments: a = -b\n";
}//对象声明

catch (bad_hmean& bg)
		{
			bg.mesg();
			continue;
		} //catch块中


if (a == -b)throw bad_hmean(a, b); //hmean中

5、异常规范和C++11

异常规范(exception specification)是一个在 C++98中新增,同时在C++11中不太需要(原文: 摈弃)的功能。

使用如下:

double harm(double a) throw(bad_thing);

其中throw()部分就是异常规范,出现在函数原型和函数定义中,可包含有可不包含类型列表。

作用:

1、告诉用户可能需要使用的try块。

2、让编译器添加运行阶段检查的代码,检查是否违反了异常规范。

然而问题在于第二个:实际上很难检查,因为使用marm()可能不会异常,而它调用一个函数,同时它调用的这个函数也调用了另外一个函数,而这个函数存在异常,因此:在编写库时,它不会出现异常,而库更新后,却会出现异常。

因此C++11告诫我们,最好不要使用该功能。然而,C++也支持一种特殊的异常规范:

double marm() noexcept;

它指出函数不会引发异常——用于解决上述存在的问题,因此,使用该关键字就表用程序员对它编写的这段代码做出了承诺。

 

6、栈解退

假定try块并未调用可能引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序会从引发异常的函数跳到包含try块的函数。

C++对于函数调用的处理: 程序将调用函数的指令的地址,并放在栈中,当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始执行。同时,函数调用也会将函数参数、以及新创建的自动变量(包括对象)放在栈中。若同时还调用了另外一个函数,则也将该函数放在栈中,同时,在每个函数调用结束后,都会释放其自动变量,若包含对象,则会调用相应的析构函数。

而在函数出现异常的情况下,程序也会释放内存,不仅会仅仅回到第一个返回地址,而是沿路线依次释放函数所调用的内存空间,直到找到第一个位于try块的返回地址。同时,控制权将转到块尾的异常处理程序,而非try块的第一个语句。这个过程称为栈解退。

综上,引发机制的一个十分重要的特性在于:函数的返回仅处理该函数放在栈中的对象,而throw则处理从throw到try块中所有函数序列放在栈中的对象。

注意:在catch函数块中,使用break和exit(EXIT_FAILURE)将会导致不同的运行:

  1、使用break: 将退出该循环,而程序将继续进行。

  2、使用exit(EXIT_FAILURE):程序将直接退出,而不会进行后续的操作(即便循环结束后还有代码)。

 

7、其他异常特性

综上所述,虽然throw-catch机制类似于函数参数和函数返回机制,不过实际上仍然存在不同:

 其中之一是:函数fun()中的返回语句将控制权转交给调用fun的函数,而throw语句将控制权向上返回到第一个能捕捉相应异常的try-catch组合。

 另一个不同之处在于:引发异常时,编译器总是创建一个临时拷贝,即使 异常规范 和 catch块中指定的引用。如:

class problem{};

void super()
{
  if(xxx)
  {
    problemm oops;
    throw oops;
  }
}

try // in main function
{
  super();
}

catch(problem & p)
{
//xxx
}

在学习函数的过程中,super()中创建的变量会在调用完函数后消失,因此在引发异常时,编译器总时创建一个临时副本,使得从try调用->super()->catch函数中的过程中,这一变量依然存在。

那么,既然仍然会创建一个临时变量,为何选择还是用引用呢——引入引用的目的就是为了避免创建临时变量、或者能够直接修改数据内容。答案是:基类引用可以指向派生类对象。因此,假设有一组通过继承关联起来的异常类型,那么在异常规范中只用列出一个基类引用即可。然而,这又会引出下一个问题:

catch中的类似于参数列表的项目需要不同的参数名,因此不能全部用基类引用匹配,为此,解决方法为:按照与派生相反的顺序排序catch中的项目名。

同时,将引发异常和创建对象组合到一起会更为简单。

throw problem(); //()为参数列表

同时,类似于switch中的default,在try-catch代码块中,可以将能够捕获所有异常的catch块放在最后面(例如:若捕获的异常为对象,那么便可将基类引用放在最后)

8、exception类

C++异常的主要目的是为设计容错程序提供语言级支持,即异常使得在程序设计方面包含错误处理功能更容易,以免事后采取一些严格的错误处理方式。

较新的编译器将异常合并到语言中。exception头文件中定义了exception类,C++可将其用作其他异常类的基类。代码可以引发exception异常,也可以将exception用作基类。在类方法中,what()虚方法返回一个特征随实现而异的字符串。因此可以在从exception派生而来的类中重新定义它。

#include<exception>

class bad_hmean : public std::exception
{
public:
  const char * what(){return "bad arguments to hmean()"}
};

同时,在C++库中,也定义了许多基于exception的异常类型。

1、stdexcept异常类

该文件定义了logic_error和rumtime_error类——以公有方式从exception派生而来。这两个类被用作派生类系列的基类。

class logic_error: public exception {

public:
explicit logic_error(const string& what_arg);
};

从logic_error中,还派生出了:domain_error、invalid_error、length_error、out_of_bounds等一些列类,每个类中均有一个类似于logic_error的构造函数,能提供一个供what()方法返回的字符串。

domain_error(定义域异常):表明提供参数大小超出了函数所接受的范围,arcsin函数接受的参数范围为-1到1,而提供参数则不在这个范围内,那么将会导致domain_error。

invalid_error(不合法异常):即给函数传递了一个意料之外的值:若程序需要的是仅包含0或1的字符串,而传上来的字符串不止包含这两个数字,那么则会导致invalid_error。

out_of_bound(越界(异常)):通常用于指示索引错误。在定义一个类似于数组的类时,其operator()[]在使用索引无效时将会引发out_of_bound异常。

length_error(长度异常):指出将没有足够的空间执行所需的操作,例,合并string类的append()方法在合并得到的字符串超过最大限度后,将会导致length_error。

 

在runtime_error的派生类中,描述了可能在运行期间发生但难以预计或防范的错误。由以下错误类型:

range_error、overflow_error以及underflow_error。

underflow_error(下溢错误):发生在浮点数计算中,一般而言,存在浮点类型可以表示的最小非零值,若计算结果比这还小将会产生下溢错误。overflow_error(上溢错误)发生在整数和浮点数中,当计算结果超过某一个值时,将会发生上溢错误。而计算结果不在函数允许的范围类时,可以使用range_error异常。

2、bad_alloc异常和new

对于由new导致的内存分配问题,C++的处理方法为让new引发bad_alloc异常,头文件new中包含了bad_alloc类的声明——它也是由exception派生而来。

#include<iostream>
#include<new>
#include<cstdlib>
using namespace std;

struct Big
{
   double stuff[20000];
};

int main()
{
  Big * pb
  try{
     cout<<"Trying to get a big block of memory:\n";
     pb = new Big[10000]; //可能会抛出异常的地方
     cout<<"Got past the new request:\n";
  }
  catch(bad_alloc & ba)
  {
     cout<<"Caught the exception!\n";
     cout<<ba.what()<<endl;
     exit(EXIT_FAILURE);
  }
  cout<<"Memory successfully allocated\n";
  pb[0].stuff[0] = 4;
  cout<<pb[0].stuff[0]<<endl;
  delete[]pb;
  return 0;
}

可以看到,在上面的函数中并没有使用throw来抛出异常,原因在于我们调用了C++的标准库new,其中的bad_alloc当new出现内存不够的情况下,会抛出异常。

3、空指针和new

学过C语言的知道,malloc()在内存不够的情况下会返回空指针,而C++也出现了一种在失败时返回空指针的new,用法如下:

int *pi = new(std::nothrow)int;
int *pa = new(std::nowthrow)int[500];

 

9、异常 类和继承

异常、类和继承以三种方法相互关联。

1、可以从一个类派生出另外一个。

2、可以在类定义中嵌套异常类声明组合异常。

3、这种嵌套声明本身可被继承,还可用作基类。

 

10、异常的迷失方向

     1、当引发了异常,却找不到相应的异常匹配时,会导致意外异常(unexpected exception)——它会导致程序异常终止。

     2、若异常并不在函数中引发,那么必须捕获它。若未被捕获,则会导致未捕获异常(uncaught exception)。

对于未捕获异常:程序不会立刻终止,而是先调用terminate()函数,它会调用abort()函数。同时,也可以调用set_terminate()函数来修改terminate()函数,使其调用特定的函数,例:

typedef void (*terminate_hander)();
terminate_hander set_terminate(terminate_handler)throw(); //C++98
terminate_hander set_terminate(terminate_handler)noexcept: //C++11

void terminate();    //C++98
void terminate()noexcept;//C++11

11、有关异常的注意事项:异常和动态内存分配

若有以下代码:

void test(int n)
{
   double * ar = new double[n];
   if (xxx)
      throw exception();
   

   delete[]ar;
   return;
}

在以上代码中,若出现异常,exception将会使程序终止,然而此时并没有调用ar的delete,因此会导致内存泄漏。解决如下:

void test(int n)
{
   double * ar = new double[n];
 
   try{
      if(xx)
        throw exception();
   }
   catch(exception & ex)
   {
      delete [] ar;
      return;
   }
   return;
}

显然,这是个笨办法,我们将在后面介绍智能指针模板来解决这个问题。

基于SSM框架的智能家政保洁预约系统,是一个旨在提高家政保洁服务预约效率和管理水平的平台。该系统通过集成现代信息技术,为家政公司、家政服务人员和消费者提供了一个便捷的在线预约和管理系统。 系统的主要功能包括: 1. **用户管理**:允许消费者注册、登录,并管理他们的个人资料和预约历史。 2. **家政人员管理**:家政服务人员可以注册并更新自己的个人信息、服务类别和服务时间。 3. **服务预约**:消费者可以浏览不同的家政服务选项,选择合适的服务人员,并在线预约服务。 4. **订单管理**:系统支持订单的创建、跟踪和管理,包括订单的确认、完成和评价。 5. **评价系统**:消费者可以在家政服务完成后对服务进行评价,帮助提高服务质量和透明度。 6. **后台管理**:管理员可以管理用户、家政人员信息、服务类别、预约订单以及处理用户反馈。 系统采用Java语言开发,使用MySQL数据库进行数据存储,通过B/S架构实现用户与服务的在线交互。系统设计考虑了不同用户角色的需求,包括管理员、家政服务人员和普通用户,每个角色都有相应的权限和功能。此外,系统还采用了软件组件化、精化体系结构、分离逻辑和数据等方法,以便于未来的系统升级和维护。 智能家政保洁预约系统通过提供一个集中的平台,不仅方便了消费者的预约和管理,也为家政服务人员提供了一个展示和推广自己服务的机会。同时,系统的后台管理功能为家政公司提供了强大的数据支持和决策辅助,有助于提高服务质量和管理效率。该系统的设计与实现,标志着家政保洁服务向现代化和网络化的转型,为管理决策和控制提供保障,是行业发展中的重要里程碑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值