C++语言导学 第四章 类 - 4.2 具体类型

C++语言导学 第四章 类 - 4.2 具体类型

4.2 具体类型

具体类(concrete class)的基本思想是,它们的行为“就像内置类型一样”。例如,一个复数类型和一个无穷精度整数与内置的int非常相像,当然它们有自己的语义和操作集。同样,vector和string也很像内置的数组,只不过它们的行为更加良好(参见9.2节、10.3节、11.2节)。
具体类型的典型特征是,其表示是定义的一部分。在很多重要的例子中,如vector,其表示只是一个或几个指针,指向保存在别处的数据,但这种表示出现在具体类的每一个对象中,这令实现可以在时空上达到最优。特别是,它允许我们。

  • 将具体类型的对象置于栈、静态分配的内存或者其他对象中(参见1.5节)
  • 直接饮用对象(而非仅仅通过指针或饮用)
  • 立即进行完整的对象初始化(比如使用构造函数,参见2.3节)
  • 拷贝和移动对象(参见5.2节)

类的表示可以是私有的(就像vector一样,参见2.3节)从而只能通过成员函数访问,但它确实是存在的。因此,如果表示方式发生了任何明显的改动,使用者就必须重新编译。这就是我们令具体类型的行为与内置类型完全一样需要付出的代价。对于某种场景,不常改动的类型和局部变量提供了迫切需要的清晰性和效率,此时这种特性是可以接受的,而且通常很理想,为了提高灵活性,具体类型可以将其表示的主要部分放置在自由存储(动态内存、堆)中,然后通过存储在类对象内部的成员访问它们。vector和string就是这样实现的,我们可以将它们看成带有精心打造的接口的资源管理器。

4.2.1 一种算术类型

一种“经典的用户自定义算术类型”是complex:

class complex{
	double re, im;	//表示:两个双精度浮点数
public:
	complex(double r, double i) : re{r}, im{i}{}	//用两个标量构造该复数
	complex(double r):re{r}, im{0}{}			//用一个标量构造该复数
	complex():re{0},im{0}{}				//默认复数为{0, 0}
	double real() const{return re;}
	void real(double d){re = d;}
	double imag() const {return im;}
	void imag(double d){im = d;}

	complex& operator+=(complex z)
	{
		re+=z.re;		//加到re和im上
		im+=z.im;
		return *this;	//返回结果
	}

	complex& operator-=(complex z)
	{
		re-=z.re;
		im-=z.im;
		return *this;
	}
	
	complex& operator*=(complex);	//在类外的某处定义
	complex& operator/=(complex);	//在类外的某处定义
};

这是标准库complex(参见14.4节)略微简化的版本,类定义本身仅包含需要访问其表示的操作。它的表示是非常简单的常规方式。出于实用的需要,它必须兼容60年前Fortran语言提供的版本,还需要一组常规的运算符。除了满足逻辑上的要求外,complex还必须足够高效,否则仍旧没有实用价值。这意味着简单操作必须是内联的。也就是说,在最终生成的机器代码中,简单操作(如构造函数、+=和imag()等)不应该以函数调用的方式实现,定义在类内部的函数默认是内联的。我们也可以在函数声明前加上关键字inline显式要求将其内联。一个工业级的complex(如标准库中的那个)必须精心实现,恰当地使用内联。

无需实参就可以调用的构造函数称为默认构造函数(default constructor)。因此,complex()是complex的默认构造函数。通过定义默认构造函数,可以有效防止该类型的对象未初始化。

在返回复数实部和虚部的函数中,const说明符指出这两个函数不会修改所调用的对象。一个const成员函数对const和非const对象均可调用,但一个非const成员函数只能对非const对象调用。例如:

complex z = {1, 0};
const complex cz{1, 3};
z = cz;						//正确:向一个非const变量赋值
cz = z;						//错误:complex::operator=()是一个非const成员函数
double x = z.real();		//正确:complex::real()是一个const成员函数

很多有用的操作并不需要直接访问complex的表示,因此它们的定义可以与类的定义分离开来:

complex operator+(complex a, complex b){return a+=b;}
complex operator-(complex a, complex b){return a-=b;}
complex operator-(complex a){return {-a.real(), -a.imag()};}	//一元负号
complex operator*(complex a, complex b){return a*=b;}
complex operator/(complex a, complex b){return a/=b;}

在本例中,我们利用了一个事实:以传值方式传递实参实际上是进行拷贝,因此我可以修改实参而不会影响调用者的副本,并可以将结果作为返回值。

== 和 != 的定义非常直观:

bool operator==(complex a, complex b)	//相等
{
	return a.real() == b.real() && a.imag() == b.imag();
}

bool operator!=(complex a, complex b)	//不等
{
	return !(a == b);
}

complex sqrt(complex);			//定义在其他某处
//...

我们可以像下面这样使用类complex:

void f(complex z)
{
	complex a{2, 3};
	complex b{1/a};
	complex c{a + z*complex{1, 2, 3}};
	//...
	if (c != b)
		c = -(b/a) + 2*b;
}

编译器将complex数的运算符转换为恰当的函数调用,例如c!=b意味着operator!=(c, b),而1/a意味着operator/(complex{1}, a)。

必须小心地按常规使用用户自定义运算符(“重载运算符”)。这些运算符的语法在语言中已被固定,因此不能定义一元的/。同样,也不可能改变一个运算符操作内置类型时的含义,因此不能重新定义运算符+令其执行int的减法。

4.2.2 容器

容器(container)是包含若干元素的对象。因为Vector类型的对象都是容器,所以我们称类Vector是一种容器类型。如2.3节中的定义,Vector是一种很不错的double容器:它易于理解、建立了一个有用的不变式(参见3.5.2节),提供了带边界检查的访问(参见3.5.1节)并且提供了size()令我们可以遍历其元素。然而,它还是存在一个致命的缺陷:它使用new分配了元素,但从没有释放这些元素。这不是一个好的设计,因为尽管C++定义了一个垃圾回收器的接口(参见5.3节),但并不保证它总是可用的以将未用内存提供给新对象。在某些情况下,你不能使用回收器,而且通常出于逻辑或性能的考虑,你更想使用精确的回收控制。因此,我们需要一种机制以确保构造函数分配的内存一定会被释放,这种机制就叫作析构函数(destructor):

class Vector{
public:
	Vector(int s) : elem{new double[s]},sz{s}		//构造函数:请求资源
	{
		for(int i = 0; i!=s;++i)					//初始化元素
			elem[i]=0;
	}

	~Vector(){delete[] elem;}						//析构函数:释放资源
	double& operator[](int i);
	int size() const;
private:
	double* elem;									//elem指向一个含sz个double的数组
	int sz;
};

析构函数的命名规则是求补运算符~后接类的名字,它是构造函数的补充。Vector的构造函数使用new运算符从自由存储(也称为堆或动态存储)分配一些内存。析构函数则使用delete[]运算符释放该内存以实现清理。普通delete释放单个对象,delete[]释放数组。

这一切都无须Vector的使用者干预。使用者只需像内置类型的变量那样创建和使用Vector对象就可以了。例如:

void fct(int n)
{
	Vector v(n);
	//...使用v...
	{
		Vector v2(2*n);
		//...使用v和v2...
	}//v2在此销毁
	//...使用v...
}//v在此处销毁

Vector与int和char等内置类型遵循同样的命名、作用域、空间分配、生命周期等规则(参见1.5节)。这个版本的Vector经过了简化,没有包含错误处理,参见3.5节。

构造函数/析构函数的机制是很多优雅技术的基础,特别是大多数C++通用资源管理技术(参见5.3节、13.2节)的基础。考虑下面Vector的图示。

在这里插入图片描述

构造函数分配元素并正确初始化Vector的成员,析构函数释放元素。这就是所谓的数据句柄模型(handle-to-data model),常用来管理在对象生命周期中大小会发生变化的数据。在构造函数中请求资源,然后在析构函数中释放它们的技术称为资源请求即初始化(Resource Acquisition Is Initialization, RAII),它令我们得以规避“裸new操作”,即,避免在一般代码中分配内存,取而代之将其隐藏在行为良好的抽象的实现内部。同样,也应该避免“裸delete操作”。避免裸new和裸delete令代码更不易出错,易于避免资源泄漏(参见13.2节)。

4.2.3 初始化容器

容器的存在就是用来保存元素的,因此显然需要一种便利的方式将元素存入容器中。我们可以恰当数目的元素创建一个Vector,然后再为它们赋值,但通常有更优雅的方法。在这里,我只列举两种我更偏爱的方法:

  • 初始值列表构造函数(initializer-list constructor):用一个元素列表进行初始化。
  • push_back():在序列的末尾添加一个新元素。

它们的声明形式如下所示:

class Vector{
public:
	Vector(std::initializer_list<double>);	//用一个double列表进行初始化
	//...
	void push_back(double);					//在末尾添加一个元素,容器的长度加1
	//...
};

其中,push_back()可用于添加任意数量的元素。例如:

Vector read(istream& is)
{
	Vector v;
	for(double d; is>>d;)		//将浮点值读入d
		v.push_back(d);			//将d添加到v当中
	return v;
}

上面的输入循环在到达文件末尾或者遇到格式错误时终止。在此之前,每个读入的数都被添加到Vector中,因此最后v的大小就是读入的元素数目。我们使用了一个for语句而不是更常规的while语句,这是为了将d的作用域限制在循环内部。5.22节将介绍如何为Vector提供移动构造函数,使用它我们就能以很低的代价从read()返回非常巨大的数据量:

Vector v = read(cin);	//这里不会拷贝Vector的元素

11.2节将介绍std::Vector是如何令push_back()及其他操作能高效改变vector的大小的。

用于定义初始值列表构造函数的std::initializer_list是一种标准库类型,编译器可以辨识它:当我们使用{}列表时,如{1, 2, 3, 4},编译器会创建一个initializer_list类型的对象并将其提供给程序。因此,我们可以编写如下代码:

Vector v1 = {1, 2, 3, 4, 5};		//v1包含5个元素
Vector v2 = {1.23, 3.45, 6.7, 8};	//v2包含4个元素

Vector的初始值列表构造函数则可以定义成如下的形式:

Vector::Vector(std::initializer_list<double>lst)		//用一个列表初始化
:elem{new double[lst.size()]}, sz{static_cast<int>(lst.size())}
{
	copy(lst.begin(), lst.end(), elem);					//从lst复制到elem中(参见12.6节)
}

不幸的是,标准库中的大小和下标都用unsigned整数,因此我们需要使用丑陋的static_cast来将初始值列表的大小显式转换为一个int。这有点儿卖弄学问,毕竟一个手写列表的元素数目超过整数所能表示的范围(16位整数最大可以表示32767,32位整数最大可以表示2147483647)的可能性是非常低的。但类型系统并无这种常识,它知道变量的可能取值范围,但并不知道真实值,所以有时候它会无中生有地报告一些错误。但这种警告偶尔会拯救程序员避免产生糟糕的错误。

static_cast本身并不负责检查要转换的值,它相信程序员能正确地使用。这个并不总是一个好的假设,所以程序员如果不确定是否合法,记得检查它。最好避免使用显式类型转换(通常称为强制类型转换(cast),以提醒人们使用它们是为了避免造成某些破坏)。你只应在系统的最底层尝试使用未经检查的强制类型转换,它是很容易出错的。

其他的类型转换包括reinterpret_cast,它将对象视为简单的字节序列,以及const_cast,意为“强制去掉const”。审慎地使用类型系统和良好定义的库能令我们在高层软件中避免使用未经检查的强制类型转换。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hank_W

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值