类的深入剖析:抛出异常

Time类实例研究

Time类的定义

这里涉及到一个C++软件工程的概念:在头文件中使用“包含防护”,从而避免头文件中的代码被多次包含到同一个源代码文件中的情况,具体写法如下:

#ifndef TIME_H
#define TIME_H
...
#endif

这里详细解释一下:

在构建大型程序时,头文件中还会放入其他的定义和声明。

当前述的“包含防护”在TIME_H已被定义时,可以阻止将#ifndef和#endif之间的代码包含到文件中。

如果在这些指令之间的代码先前没有包含到应用程序中,那么TIME_H这个名字将会被#define定义,并且包含该头文件的语句。

如果已经包含,那么TIME_H已经有定义,将不会包含该头文件。

建议: 1、使用预处理器指令#ifndef、#define、#endif等构成包含防护,从而避免头文件在一个程序中被多次包含。

2、按照惯例,在#ifndef和#define中,应该使用大写的头文件名,并且用下划线代替圆点。

Time类的成员函数

可以使用类内初始化器,在类定义中声明为任何数据成员之处对此数据成员进行初始化。

其实就是{}:

class X
{
private:
int a[4];
public:
X(): a{1,2,3,4} {//具体的实现逻辑{} //C++11, 初始化数组成员
}
};

Time类的成员函数setTime和异常的抛出

看以下这条语句:

throw invalid_argument("error");

这里,throw语句创建了一个类型为invalid_argument的新对象,而跟在类名称后面的圆括号表示对该invalid_argument对象构造函数的一个调用,这里需要加入头文件#include

Time类的成员函数printUniversal

这里注意一个setfill流操作符。可以用于指定当输出域宽大于输出整数值中的数字个数时所需要的显示的填充字符。

默认情况下,数的输出是右对齐的,那么填充字符出现在数中数字的左边,如果是左对齐,则出现在右边。

cout<<setfill('0')<<setw(3)<<1;

输出结果为001。

如果将要输出的数填满了指定的域宽,则不显示填充字符。

注意:一旦使用了setfill指定了填充字符,则该字符将应用在后续的显示中,只要显示的域宽大于要被显示数所需要的的实际宽度。(实际上setfill是一个黏性设置,当每个黏性设置不再需要使用时,则应当将它恢复到以前的模式,否则在后续中输出可能会存在问题)。

Time类的成员函数printStandard

还是上面一样的问题,跳过。

在类定义外部定义成员函数与类的作用域

即使声明在类定义中的成员函数可以在类定义的外部定义(并且通过二元作用域分辨运算符“绑定”到该类),这样的成员函数仍在该类的作用域中。(二元作用域分辨运算符就是::)

成员函数与全局函数(又称自由函数)

跳过,讲的是成员函数的优点。

使用Time类

定义了一个类,即可将他用于对象,数组,指针,引用中:

Time sunset;
array<Time,5>arrayOfTimes;
Time &dinnerTime=sunset;
Time *timePtr=&dinnerTime;

用无效值调用setTime

主要涉及try...catch语句。

try{
...
}
catch{
...
}

try语句块是用来判断是否有异常;

catch语句块捕捉异常,并进行处理;

throw是抛出异常;

示例如下:

#include <stdlib.h>
#include "iostream"
using namespace std;

double fuc(double x, double y) //定义函数
{
if(y==0)
{
throw y; //除数为0,抛出异常
}
return x/y; //否则返回两个数的商
}

int _tmain(int argc, _TCHAR* argv[])
{
double res;
try //定义异常
{
res=fuc(2,3);
cout<<"The result of x/y is : "<<res<<endl;
res=fuc(4,0); //出现异常
}
catch(double) //捕获并处理异常
{
cerr<<"error of dividing zero.\n";
exit(1); //异常退出程序
}
return 0;
}

这里转载自:C++:try catch语句用法_c++ try catch用法-CSDN博客

一般格式如下:

void Func()
{
  try
  {
    // 这里的程序代码完成真正复杂的计算工作,这些代码在执行过程中
    // 有可能抛出DataType1、DataType2和DataType3类型的异常对象。
  }
  catch(DataType1& d1)
  {
  }
  catch(DataType2& d2)
  {
  }
  catch(DataType3& d3)
  {
  }
  /*********************************************************
  注意上面try block中可能抛出的DataType1、DataType2和DataType3三
  种类型的异常对象在前面都已经有对应的catch block来处理。但为什么
  还要在最后再定义一个catch(…) block呢?这就是为了有更好的安全性和
  可靠性,避免上面的try block抛出了其它未考虑到的异常对象时导致的程
  序出现意外崩溃的严重后果,而且这在用VC开发的系统上更特别有效,因
  为catch(…)能捕获系统出现的异常,而系统异常往往令程序员头痛了,现
  在系统一般都比较复杂,而且由很多人共同开发,一不小心就会导致一个
  指针变量指向了其它非法区域,结果意外灾难不幸发生了。catch(…)为这种
  潜在的隐患提供了一种有效的补救措施。
  *********************************************************/

  catch(…)
  {
  }
}

可以再看看这篇文章深究:c++中try catch的用法 - 超酷小子 - 博客园 (cnblogs.com)

组成和继承概念介绍

组成:包含类对象作为其他类的成员。

继承:从已有的类派生出来的新的类。

对象大小

实际上,对象只包含数据。编译器只创建独立于类的所有对象的一份成员函数的副本,该类的所有对象都共享这份副本。

类的作用域和类成员的访问

类的数据成员和成员函数都属于该类的作用域。默认情况下,非成员函数在全局命名空间作用域中定义。

类作用域和块作用域

成员函数中声明的变量具有块作用域,只有该函数知道他们。如果成员函数定义了与类作用域内变量同名的另外一个变量,则函数中块作用域中的变量将隐藏在类作用域中的变量,需要使用类名+::进行访问。

圆点成员选择运算符(.)和箭头成员选择运算符(->)

(.)前面+对象名、对象引用,则可以直接访问该对象的成员。

(->)前面+对象指针,则可以访问该对象的成员。

示例如下:

Account account;
Account &accountRef;
Account *accountPtr;

//调用如下
account.setBalance(12345);
accountRef.setBalance(12345);
accountPtr->setBalance(12345);

访问函数与工具函数

访问函数:可以读取和显示数据。

工具函数:一个用来支持类的其他成员函数操作的private成员函数。

Time类实例研究:具有默认实参的构造函数

一个默认所有实参的构造函数也是一个默认的构造函数,即一个调用时不需要带任何实参的构造函数,每个类最多只有一个默认构造函数。

注意:对默认实参值的任何修改都要求重新编译用户代码(保证程序正常运行)。

关于Time类的设置函数、获取函数、构造函数的补充说明

类内的一些成员函数的实现可由设置函数和获取函数构成,这样,当private数据成员发生改变时(如数据成员减少),只需要修改哪些直接访问private数据的函数体(尤其是设置函数和获取函数),而其他没有直接访问的函数则不需要修改。这样可以降低因改变类的实现方法而造成编程错误的可能性。

建议: 1、如果成员函数已经实现了类的构造函数所需要的部分或全部功能,则可以在构造函数中调用这样的成员函数。既可以简化代码的维护,也可以减少由修改代码实现方法所引起出差的可能性。

2、构造函数应当适当地初始化后,再调用其他成员函数。

C++11:使用列表初始化器调用构造函数

格式如下:

Time t2{2};
Time t3{21,34};
Time t4{12,25,42};

C++11:重载的构造函数和委托构造函数

构造函数也可以重载:

Time();
Time(int);
Time(int,int);
Time(int,int int);

构造函数也可以使用同一个类中的其他构造函数,这称为委托构造函数。

例子如下:使用Time(int,int,int)来构造其他三个构造函数。

Time::Time()
:Time(0,0,0)
{

}

Time::Time(int hour)
:Time(hour,0,0)
{

}

Time::Time(int hour,int minute)
:Time(hour,minute,0)
{

}

析构函数

析构函数不接受任何参数,也不返回任何值。

本身不释放对象占用的内存空间,它只是再系统回收对象的内存空间之前执行扫尾工作,这样内存可以用于重新保存新的对象。

何时调用构造和析构

(需要再复习)

编译器隐式地调用构造和析构函数,一般两者的调用顺序相反,但是,对象的存储类别也可以该改变调用析构函数的顺序。

取决于程序进入和离开实例化对象所在作用域的顺序。

返回private数据成员的引用或者指针

如果让类的public成员函数返回对该类private数据成员的引用,注意函数返回的如果是一个const引用,则这个引用不可以作为可供修改的左值(读起来好绕口)。

换句话说,这么做是非常不合理的,因为这样,我们可以直接访问private数据成员,并且进行修改,(返回一个private类型的数据成员的引用或指针破坏了类的封装性),当然在某些情况下是合理的。

默认的逐个赋值

赋值运算符可以将一个对象赋给另外一个相同类型的对象。默认情况下是逐个成员进行赋值。

但是,当类的数据成员包含指向动态分配内存的指针时,将会引发严重的问题。以后会提到怎么解决。

复制构造函数由编译器默认提供,功能和赋值运算符基本一致,但是也存在相同的问题。

const对象和const成员函数

对于const对象,编译器不允许进行成员函数的调用,除非成员函数也声明为const。

一个const成员函数需要在两处同时指明const限定符:

1、函数原型的参数列表后面插入关键字const

2、在函数定义时在函数体开始时的左括号之前。

注意:

1、将修改对象的数据成员的成员函数定义为const将导致编译错误。

2、定义为const的成员函数如果又调用同一类的同一实例的非const成员函数,将导致编译错误。

3、在const对象上调用非const成员函数将导致编译错误。

4、一个const对象的常量性是从构造函数完成初始化到析构函数被调用为止。

使用const和非const成员函数

尽管构造函数必须是非const函数,但是它仍可以用来初始化const对象。

有趣的是,在构造函数中,调用非const成员函数来完成const对象的初始化是允许的,与上面的内容不谋而合。

组成:对象作为类的成员

(后续补充)

friend函数和friend类

类的friend函数在类的作用域外被定义,却具有访问类的非public成员的权限。

单独的函数、整个类、或其他类的成员函数都可以被声明为另一个类的友元。

friend的声明

友元声明可以出现在类的任何地方。

如果要让ClassTwo的所有成员函数都作为ClassOne的友元,则在ClassOne的定义中加入以下这句话:

friend class ClassTwo;

注意,若使B成为A的友元,则A必须显式地声明为B是它的友元。并且友元关系不是对称的、传递的。

重载友元函数

可以指定重载函数为类的友元,每个打算成为友元的重载函数都必须在类的定义里面显式地声明为类地一个友元。

注意:

1、友元不是成员函数

2、public、private、protected这些成员访问说明符标志与友元声明无关。

3、把所有的友元关系放在最前面,并且不要添加任何成员访问说明符。

使用this指针

每个对象都可以使用一个称为this的指针来访问自己的地址,对象的this指针不是对象本身的一部分,也就是this指针占用的内存大小不会反映在对对象进行sizeof运算得到的结果中。

并且,this指针作为一个隐式的参数传递给对象的每个非静态成员函数。

使用this指针来避免名字冲突

void Time::setHour(int hour)
{
if(hour>=0&&hour<24)
{
this->hour=hour;
}
}

可以看到,在该函数定义中,参数名和数据成员hour名字相同。在setHour的作用域内,参数hour将隐藏数据成员,所以我们可以使用this指针访问该数据成员hour,并且赋值。

this指针的类型

一般情况下,this指针的类型为classtype const,如果对象的类型及使用this的成员函数被声明为const时,this指针的类型为const classtype const。

隐式和显式地使用this指针来访问对象的数据成员

分为两种:

//使用this指针和->进行访问修改
this->x=12;

//使用*this和.进行访问修改
(*this).x=12;
//注意这对圆括号是必须的,因为*的优先级高于.,不加圆括号,将被识别为*(this.x);

this指针的一个有趣的用法是防止对象自我赋值,后续补充。

使用this指针使得串联函数成为可能

this指针的另一个用法是串联的成员函数调用成为可能,也就是多个函数在一条语句中被重复调用。

关键是这些调用函数的返回值是*this(这一点在重载运算符中有所体现)。

static类成员

static类成员属于类(所有对象共享一份数据),不属于某个具体的对象,某个对象修改static的类成员,也会影响其他对象。

这样的优点是:所有对象都只需要一份数据副本即可,使用static数据成员可以节省存储空间。

静态数据成员的作用域和初始化

类的static数据成员只在类的作用域内起作用,且必须被精确地初始化。

基本类型的static数据成员默认情况下将被初始化为0。

注意如果有静态成员函数,则不需要初始化,因为它们的默认构造函数将被调用。

访问静态数据成员

即使在没有任何类的对象时,类的static成员仍然存在。

private,protected:通常使用类的public成员函数或者类的友元访问。

没有类的对象存在时,只需在静态数据成员前加上类名和::即可。

当没有类的对象时而需要访问private或protected的static类成员时,应该提供public static成员函数,并且在函数名前加上类名和::来调用该函数。

因为类的static成员需要被任何访问文件的客户代码使用时,所以不能在.cpp文件中将它们声明为static,而只在.h文件中将它们声明为static。

如果成员函数不访问类的非static的数据成员或非static的成员函数,则应该将其声明为static。

static函数不具有this指针,因为static数据成员和成员函数独立于类的任何对象而存在,而this指针必须指向具体的对象。

注意: 1、static成员函数中不能使用this指针

2、static成员函数不能声明为const,const限定符指示函数不能修改它操作的对象的内容,但是static成员函数独立于类的任何对象而存在并且进行操作。