文章目录
0. 前言
《C++大学教程》 第9章 前10节 笔记更一下。
第9章后续内容请见下一篇。
9. 类的深入剖析:抛出异常
9.2 Time类实例研究
Time类的定义
在头文件中使用“包含防护”,从而避免头文件中的代码被多次包含到同一个源代码文件中的情况。
由于一个类只能被定义一次,因此使用这样的预处理器指令阻止了重复定义的错误。
// prevent multiple inclusions of header
#ifndef TIME_H
#define TIME_H
...
#endif
当构建大型程序时头文件中还会放入其他的定义和声明。
前述的包含防护在名字TIME_H
已被定义时,可以阻止将#ifndef
(意思是”如果没有定义“)和#endif
之间的代码包含到文件中。
如果以前没有在文件中包含此头文件,那么TIME_H
这个名字将被#define
指令定义,并且包含该头文件的语句;
如果以前已包含此头文件,那么TIME_H
已经定义,将不再包含该头文件。
试图多次包含一个头文件的这种错误,通常(不经意间)发生在具有多个头文件(这些头文件本身可能包含其他的头文件)的大型程序中。
Time类的成员函数
程序员可以为一个类定义多个重载的构造函数
Time类的成员函数setTime和异常的抛出
// set new Time value using universal time
void Time::setTime(int h, int m, int s)
{
// validate hour, minute and second
if ((h >= 0 && h < 24) && (m >= 0 && m < 60) &&
(s >= 0 && s < 60))
{
hour = h;
minute = m;
second = s;
} // end if
else
throw invalid_argument(
"hour, minute and/or second was out of range");
} // end function setTime
超出范围的值都会造成setTime
抛出一个类型为invalid_argument
(来自头文件<stdexcept>
)的异常,告诉客户端代码函数接收一个无效的实参。
throw
语句创建了一个类型为invalid_argument
的新对象。跟在类名称后面的圆括号表示对该invalid_argument
对象构造函数的一个调用,其中允许我们指定一个用户自定义的错误信息字符串。
在异常对象被创建之后,此throw
语句立刻终止函数setTime
,然后异常返回到尝试设置时间的代码处。
Time类的成员函数printUniversal
// print Time in universal-time format (HH:MM:SS)
void Time::printUniversal() const
{
cout << setfill('0') << setw(2) << hour << ":"
<< setw(2) << minute << ":" << setw(2) << second;
} // end function printUniversal
参数化的流操纵符setfill
,用于指定当输出域大于输出整数值中数字个数时所需显示的填空字符。
因为默认情况下数的输出是右对齐的,填充字符出现在数中数字的左边。
在本例中,如果minute
的值为2
,那么将会显示02
,因为填充字符被设置为'0'
.
一旦用setfill
指定了填充字符,该字符将应用在后续值的显示中,即setfill
是一个“黏性”设置。这与setw
形成了对比,setw
是一个“非黏性”设置,它只对紧接着显示的值起作用。
在类定义外部定义成员函数与类的作用域
尽管在类定义中声明的成员函数可以定义在类定义的外部(并通过二元作用域分辨运算符“绑定”到该类),然而这样的成员函数仍在该类的作用域之内。
如果成员函数定义在类定义的体内,那么该成员函数被隐式地声明为incline(内联)的。不过要注意,编译器将保留不对任何函数内联的权利。
内联的解释见参考链接1.
成员函数与全局函数(也称作自由函数)
使用面向对象编程方法常常可以通过减少传递的参数个数来简化函数调用。
这受益于:对象中封装了数据成员和成员函数,使成员函数有权访问数据成员。
使用Time类
一旦定义了Time
类,它就可以作为一种类型用在如下的对象、数组、指针和引用的声明中:
Time sunset;
array<Time, 5> arrayOfTimes;
Time &dinnerTime = sunset;
Time *timePtr = &dinnerTime;
引用补充
见参考链接2.
&表示引用。“&变量名”,就相当于给变量取的一个别名。因为主程序向子程序传递形参后不改变原变量的值。如果想改变原变量的值,就要用指针来传递变量的地址从而改变变量的值。但用指针的话可读性就不是太好,所以用“&变量名”作为变量的别名,既能改变原变量的值,也好让人看懂,就是可读性强,程序就简洁易懂多了。
用无效值调用setTime
// attempt to set the time with invalid values
try
{
t.setTime( 99, 99, 99 ); // all values out of range
} // end try
catch ( invalid_argument &e )
{
cout << "\n\nException: " << e.what() << endl;
} // end catch
通过调用异常对象的what
成员函数打印错误信息
组成和继承概念介绍
类通常不必从头开始创建。相反,类可以将其他类的对象包含进来作为其成员,或者可以由其他能为该类提供可以使用的属性和行为的类派生(derive)而来。
包含类对象作为其他类的成员称为组成(composition),或者称为聚合(aggregation),从已有的类派生出新的类称为继承。
对象大小
从逻辑上讲,程序员可以认为对象是包含了数据和函数。可是,事实并不是这样。
对象只包含数据。编译器只创建独立于类的所有对象的一份成员函数的副本。该类的所有对象共享这份副本。
9.3 类的作用域和类成员的访问
在类的作用域内,类的成员可以被类的所有成员函数直接访问,也可以通过名字引用。
在类的作用域之外,public类成员可以通过对象的句柄(handle)之一而引用。句柄可以是对象名称、对象的引用或者对象的指针。对象、引用或指针的类型指定了客户可访问的接口(即成员函数)。
类作用域和块作用域
如果成员函数定义了与类作用域内变量同名的另一个变量,那么在函数中块作用域中的变量将隐藏类作用域中的变量。
这样被隐藏的变量可以通过在其名前加类名和二元作用域分辨符(::)的方法而访问。
同样,被隐藏的全局变量可以用一元作用域分辨符来访问。
例子见参考链接3.
圆点成员选择运算符(.)和箭头成员选择运算符(->)
圆点成员选择运算符(.
)前面加对象名称或者对象的引用,则可以访问该对象的成员。
箭头成员选择运算符(->
)前面加对象的指针,则可以访问该对象的成员。
通过对象、引用、指针访问类的public成员
考虑一个含有一个public setBalance
成员函数的Account
类。给定以下声明:
Account account;
Account &accountRef = account;
Account *accountPtr = &account;
则程序员可以用圆点成员选择运算符(.
)和箭头成员选择运算符(->
)以如下的方式调用成员函数setBalance
:
account.setBalance(123.45);
accountRef.setBalance(123.45);
accountPtr->setBalance(123.45);
9.4 访问函数和工具函数
访问函数
访问函数可以读取或者显示数据。
访问函数另一个常见用法是测试条件是真还是假,常常称这样的函数为判定函数(predicate function)。
例如,任何容器类都有的isEmpty
函数就是判定函数。程序在试图从容器对象中读取另一个元素前,可能要先测试isEmpty
。
判定函数isFull可以测试一个容器类对象,确定它是否还有多余的空间。
工具函数
工具函数(也称为助手函数)是一个用来支持类的其他成员函数操作的private
成员函数。
工具函数需要被声明为private
的,因为它们不希望被类的客户所使用。
工具函数的一个非常普遍的情况是,希望将一些公共代码放在一个函数中,否则这些代码将重复出现在多个成员函数中。
9.5 Time类实例研究:具有默认实参的构造函数
每个类最多只有一个默认构造函数。
// Fig. 9.5: Time.cpp
// Member-function definitions for class Time.
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include "Time.h" // include definition of class Time from Time.h
using namespace std;
// Time constructor initializes each data member
Time::Time( int hour, int minute, int second )
{
setTime( hour, minute, second ); // validate and set time
} // end Time constructor
// set new Time value using universal time
void Time::setTime( int h, int m, int s )
{
setHour( h ); // set private field hour
setMinute( m ); // set private field minute
setSecond( s ); // set private field second
} // end function setTime
// set hour value
void Time::setHour( int h )
{
if ( h >= 0 && h < 24 )
hour = h;
else
throw invalid_argument( "hour must be 0-23" );
} // end function setHour
// set minute value
void Time::setMinute( int m )
{
if ( m >= 0 && m < 60 )
minute = m;
else
throw invalid_argument( "minute must be 0-59" );
} // end function setMinute
// set second value
void Time::setSecond( int s )
{
if ( s >= 0 && s < 60 )
second = s;
else
throw invalid_argument( "second must be 0-59" );
} // end function setSecond
// return hour value
unsigned int Time::getHour() const
{
return hour;
} // end function getHour
// return minute value
unsigned int Time::getMinute() const
{
return minute;
} // end function getMinute
// return second value
unsigned int Time::getSecond() const
{
return second;
} // end function getSecond
// print Time in universal-time format (HH:MM:SS)
void Time::printUniversal() const
{
cout << setfill( '0' ) << setw( 2 ) << getHour() << ":"
<< setw( 2 ) << getMinute() << ":" << setw( 2 ) << getSecond();
} // end function printUniversal
// print Time in standard-time format (HH:MM:SS AM or PM)
void Time::printStandard() const
{
cout << ( ( getHour() == 0 || getHour() == 12 ) ? 12 : getHour() % 12 )
<< ":" << setfill( '0' ) << setw( 2 ) << getMinute()
<< ":" << setw( 2 ) << getSecond() << ( hour < 12 ? " AM" : " PM" );
} // end function printStandard
关于Time类的设置函数、获取函数和构造函数的补充说明
setTime
函数调用了setHour
、setMinute
和setSecond
函数,而printUniversal
和printStandard
函数分别调用了getHour
、getMinute
和getSecond
函数。
在每种情况下,这些函数原本都可以不通过调用这些设置函数和获取函数而直接访问类的private
数据。
然而,考虑将时间由现在的3个int
值来表达,改变为只用一个int
值(表示从当天午夜开始逝去的总秒数)表达。
如果进行了这样的改动,那么只有那些直接访问private
数据的函数体需要改变 ,尤其是针对hour
、minute
、second
的各个设置和获取函数。
而setTime
、printUniversal
或者printStandard
函数体不需要修改,因为它们并未访问数据。
以这样的方式来对类进行设计,可以降低因改变类的实现方法而造成的编程出错的可能性。
如果类的成员函数已经提供了类的构造函数(或其他成员函数)所需要的全部或者部分功能,那么就可以在构造函数(或其他成员函数)中调用这样的成员函数。
构造函数可以调用类的其他成员函数,如设置函数或者获取函数等。
在数据成员还未适当地初始化之前就使用它们将导致逻辑错误。
C++11:重载的构造函数和委托构造函数
类的构造函数和成员函数也可以被重载。
如果要重载构造函数,需要在类的定义中为构造函数的各个版本提供相应的函数原型,并且为各个重载的版本提供独立的构造函数定义。
Time();
Time( int );
Time( int , int );
Time( int , int , int );
正如构造函数可以调用其他类的成员函数来实现功能那样,C++11现在也允许构造函数调用同一类中的其他构造函数。这样的构造函数称为委托构造函数(delegating constructor),它将自己的工作委托给其他构造函数。
这种机制对于重载的构造函数具有相同的代码时很有用,而以前的处理方式是将这些相同的代码定义在一个private工具函数中,供所有的构造函数去调用。
Time::Time()
: Time( 0, 0, 0 )
{
}
Time::Time( int hour)
: Time( hour, 0, 0 )
{
}
Time::Time( int hour, int minute)
: Time( hour, minute, 0 )
{
}
Time::Time( int hour, int minute, int second )
{
setTime( hour, minute, second );
}
9.6 析构函数
析构函数(destructor)是另一种特殊的成员函数。类的析构函数的名字是在类名之前添加发音字符(~
)。在某种意义上,析构函数和构造函数互补。析构函数不接收任何参数,也不返回任何值。
当对象撤销时,类的析构函数会隐式地调用。例如,当程序的执行离开实例化自动对象所在的作用域时,自动对象就会撤销,这时会发生析构函数的隐式调用。实际上,析构函数本身并不释放对象占用的内存空间,它只是在系统收回对象的内存空间之前执行扫尾工作,这样内存可以重新用于保存新的对象。
每个类都有一个析构函数。如果程序员没有显示地提供析构函数,那么编译器生成一个“空的”析构函数。
9.7 何时调用构造函数和析构函数
编译器隐式地调用构造函数和析构函数,调用发生的顺序由执行过程进入和离开对象实例化的作用域的顺序决定。一般而言,析构函数的调用顺序与相应的构造函数的调用顺序相反。但是,对象的存储类别可以改变调用析构函数的顺序。
// Fig. 9.9: fig09_09.cpp
// Demonstrating the order in which constructors and
// destructors are called.
#include <iostream>
#include "CreateAndDestroy.h" // include CreateAndDestroy class definition
using namespace std;
void create( void ); // prototype
CreateAndDestroy first( 1, "(global before main)" ); // global object
int main()
{
cout << "\nMAIN FUNCTION: EXECUTION BEGINS" << endl;
CreateAndDestroy second( 2, "(local automatic in main)" );
static CreateAndDestroy third( 3, "(local static in main)" );
create(); // call function to create objects
cout << "\nMAIN FUNCTION: EXECUTION RESUMES" << endl;
CreateAndDestroy fourth( 4, "(local automatic in main)" );
cout << "\nMAIN FUNCTION: EXECUTION ENDS" << endl;
} // end main
// function to create objects
void create( void )
{
cout << "\nCREATE FUNCTION: EXECUTION BEGINS" << endl;
CreateAndDestroy fifth( 5, "(local automatic in create)" );
static CreateAndDestroy sixth( 6, "(local static in create)" );
CreateAndDestroy seventh( 7, "(local automatic in create)" );
cout << "\nCREATE FUNCTION: EXECUTION ENDS" << endl;
} // end function create
全局作用域内对象的构造函数和析构函数
全局作用域内定义的对象的构造函数,在文件内任何其他函数(包括main
函数)开始执行之前调用。
当main
函数执行结束时,相应的析构函数被调用。
局部对象的构造函数和析构函数
当程序执行到自动局部对象的定义处时,该对象的构造函数被调用;
当程序执行离开对象的作用域时(也就是对象定义其中的块已经执行完毕),相应的析构函数被调用。
static局部对象的构造函数和析构函数
static
局部对象的构造函数只被调用一次,即在程序第一次执行到该对象的定义处时,而相应的析构函数发生在main
函数结束或者程序调用exit
函数时。
遇到的问题
-
点击开始调试,析构函数的调用显示不完全
须改为开始执行(不调试)。
调试一定需要程序运行,F5是运行且调试,Ctrl+F5是运行但不调试。解释见参考链接4. -
点开始执行(不调试),VS17没反应
解决方案:见参考链接5.
运行结果
Object 1 constructor runs (global before main)
MAIN FUNCTION: EXECUTION BEGINS
Object 2 constructor runs (local automatic in main)
Object 3 constructor runs (local static in main)
CREATE FUNCTION: EXECUTION BEGINS
Object 5 constructor runs (local automatic in create)
Object 6 constructor runs (local static in create)
Object 7 constructor runs (local automatic in create)
CREATE FUNCTION: EXECUTION ENDS
Object 7 destructor runs (local automatic in create)
Object 5 destructor runs (local automatic in create)
MAIN FUNCTION: EXECUTION RESUMES
Object 4 constructor runs (local automatic in main)
MAIN FUNCTION: EXECUTION ENDS
Object 4 destructor runs (local automatic in main)
Object 2 destructor runs (local automatic in main)
Object 6 destructor runs (local static in create)
Object 3 destructor runs (local static in main)
Object 1 destructor runs (global before main)
9.8 Time类实例研究:微妙的陷阱——返回private数据成员的引用或指针
成员函数badSetHour
返回private
数据成员的引用。
实际上,这样的引用返回使成员函数badSetHour
的调用成为private
数据成员hour
的一个别名。
// poor practice: returning a reference to a private data member.
unsigned int &Time::badSetHour( int hh )
{
if ( hh >= 0 && hh < 24 )
hour = hh;
else
throw invalid_argument( "hour must be 0-23" );
return hour; // dangerous reference return
} // end function badSetHour
// Fig. 9.12: fig09_12.cpp
// Demonstrating a public member function that
// returns a reference to a private data member.
#include <iostream>
#include "Time.h" // include definition of class Time
using namespace std;
int main()
{
Time t; // create Time object
// initialize hourRef with the reference returned by badSetHour
unsigned int &hourRef = t.badSetHour( 20 ); // 20 is a valid hour
cout << "Valid hour before modification: " << hourRef;
hourRef = 30; // use hourRef to set invalid value in Time object t
cout << "\nInvalid hour after modification: " << t.getHour();
// Dangerous: Function call that returns
// a reference can be used as an lvalue!
t.badSetHour( 12 ) = 74; // assign another invalid value to hour
cout << "\n\n*************************************************\n"
<< "POOR PROGRAMMING PRACTICE!!!!!!!!\n"
<< "t.badSetHour( 12 ) as an lvalue, invalid hour: "
<< t.getHour()
<< "\n*************************************************" << endl;
} // end main
程序声明了引用hourRef
,其中hourRef
在声明的同时已调用t.badSetHour( 20 )
返回的引用进行了初始化。
hourRef
破坏了类的封装性:main
函数中的语句不应该有访问该类private
数据的权利。
9.9 默认的逐个成员赋值
赋值运算符(=
)可以将一个对象赋给另一个类型相同的对象。
默认情况下,这样的赋值通过逐个成员赋值的方式进行,即赋值运算符右边对象的每个数据成员逐一赋值给赋值运算符左边对象中的同一数据成员。
// Fig. 9.15: fig09_15.cpp
// Demonstrating that class objects can be assigned
// to each other using default memberwise assignment.
#include <iostream>
#include "Date.h" // include definition of class Date from Date.h
using namespace std;
int main()
{
Date date1( 7, 4, 2004 );
Date date2; // date2 defaults to 1/1/2000
cout << "date1 = ";
date1.print();
cout << "\ndate2 = ";
date2.print();
date2 = date1; // default memberwise assignment
cout << "\n\nAfter default memberwise assignment, date2 = ";
date2.print();
cout << endl;
} // end main
对象可以作为函数的实参进行传递,也可以由函数返回,均以按值传递的方式执行。
所谓按值传递是指传递和返回对象的一份副本。C++创建一个新的对象,并使用复制构造函数将原始对象的值复制到新的对象中。
9.10 const对象和const成员函数
对于const
对象,C++编译器不允许进行成员函数的调用,除非成员函数本身也声明为const
。
一个const
成员函数要在两处同时指明const
限定符:函数原型的参数列表后插入关键字const
,在函数定义时在函数体开始的左括号之前。
试图将构造函数和析构函数声明为const
是一个编译错误。
一个const
对象的“常量性”是从构造函数完成对象的初始化到析构函数被调用之间。
使用const和非const成员函数
允许的成员函数调用:对非const
对象调用非const
成员函数,对非const
对象调用const
成员函数和对const
对象调用const
成员函数。
注意:
尽管构造函数必须是非const
函数,但它仍然可以用来初始化const
对象。
在构造函数中调用非const
成员函数来作为初始化const
对象的一部分是允许的。
结语
本章内容较多,将前10节写于本篇。
一门选修课,需要用到C++,希望能学到新东西,加深理解~
个人水平有限,有问题欢迎各位大神批评指正!
参考链接
- C++ inline(内联什么时候使用)
https://www.cnblogs.com/msdn1433/p/3569176.html - 请问,C++ 中,类名 & 变量名 是什么意思?
http://www.imooc.com/wenda/detail/571389 - C++ 二元作用域运算符(::)
https://www.cnblogs.com/ShowJoy/p/3606085.html - VS2010中启动调试(F5)与开始执行不调试(Ctrl+F5)的区别是什么?
https://www.zhihu.com/question/34824027 - 解决VS2017 按ctrl+f5执行程序窗口依然一闪而过的问题(图文)
https://blog.csdn.net/Gsdxiaohei/article/details/79720190