C++ Primer系列 第19章 特殊工具与技术

C++语言的设计者希望它能够处理各种各样的问题。因此,C++的某些特征可能对于一些特殊的应用非常重要,而在另外一些情况下没什么作用。本章将介绍C++语言的几种未被广泛使用的特征。

19.1 控制内存分配

某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。为了实现这一目的,应用程序需要重载new运算符和delete运算符以控制内存分配的过程。

19.1.1 重载new和delete

当我们使用一条new表达式时:

// new表达式
string *sp = new string("a value"); // 分配并初始化一个string对象
string *arr = new string[10]; // 分配10个默认初始化的string对象

实际执行了3个步骤:

  1. new表达式调用了一个名为operate new(或者operate new[])的标准库函数。该函数分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象(或者对象的数组)。
  2. 编译器允许相应的构造函数以构造这些对象,并为其传入初始值。
  3. 对象被分配了空间并构造完成,返回一个指向该对象的指针。

当我们使用一条delete表达式删除一个动态分配的对象时:

delete sp; // 销毁*sp,然后释放sp指向的内存空间
delete [] arr; // 销毁数组中的 元素,然后释放对应的内存空间

实际执行了2个步骤:

  1. 对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数。
  2. 编译器调用名为operate delete(或者operate delete[])的标准库函数释放内存空间。
  • 当自定义了全局的operate new函数和operate delete函数后,我们就负担起了控制动态内存分配的职责。这两个函数必须是正确的:因为它们是程序整个处理过程中至关重要的一部分。

operate new接口和operate delete接口
标准库定义了operate new函数和operate delete函数的8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:

// 这些版本可能抛出异常
void *operate new(size_t);              // 分配一个对象
void *operate new[](size_t);            // 分配一个数组
void *operate delete(void*) noexcept;              // 释放一个对象
void *operate delete[](void*) noexcept;            // 释放一个数组

// 这些版本承诺不会抛出异常
void *operate new(size_t, nothrow_t&) noexcept;              // 分配一个对象
void *operate new[](size_t, nothrow_t&) noexcept;            // 分配一个数组
void *operate delete(void*, nothrow_t&) noexcept;              // 释放一个对象
void *operate delete[](void*, nothrow_t&) noexcept;            // 释放一个数组

与析构函数类似,operate delete也不允许抛出异常。当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。

  • 术语:new表达式与operate new函数

标准库函数operator new和operator delete的名字容易让人误解。和其他operater函数不同(比如operator=),这两个函数并没有重载new表达式或delete表达式。实际上,我们根本无法自定义new表达式或delete表达式的行为。

一条new表达式执行的过程总是先调用operator new函数以获取内存空间,然后在得到的内存空间中构造对象。与之相反,一条delete表达式的执行过程总是先销毁对象,然后调用operator delete函数释放对象所占空间。

我们提供新的operator new函数和operator delete函数的目的在于改变内存分配的方式,但是不管怎样,我们都不能改变new运算符和delete运算符的基本含义。

malloc函数与free函数
当你定义了自己的全局operator new和operator delete后,这两个函数必须以某种方式执行分配内存与释放内存的操作。
为此我们可以使用名为malloc和free的函数,C++从C语言中继承了这些函数,并将其定义在cstdlib头文件中。
如下所示是编写operator new和operator delete的一种简单方式,其他版本与之类似:

void* operator new(size_t size) {
   
	if (void* mem = malloc(size))
		return mem;
	else
		throw bad_alloc();
}

void operator delete(void* mem) noexcept {
    free(mem); }

19.1.2 定位new表达式

我们可以使用定位new传递一个地址,此时定位new的形式如下:

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {
    braced initializer list }

其中place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。

  • 当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。
  • 调用析构函数会销毁对象,但是不会释放内存。

19.2 运行时类型识别

运行时类型识别(run-time type identification, RTTI)的功能由两个运算符实现:

  • typeid运算符,用于返回表达式的类型。
  • dynamic_cast运算符,用于将基类的指针或引用安全地转换为派生类的指针或引用。

当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

  • 使用RTTI必须要加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。

19.2.1 dynamic_cast运算符

dynamic_cast运算符的使用形式如下所示:

dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。第一种形式中,e必须是一个有效指针;第二种形式中,e必须是一个左值;第三种形式中,e不能是左值。

指针类型的dynamic_cast
举个简单例子,假定Base类至少含有一个虚函数,Dervied是Base的公有派生类。如果有一个指向Base的指针bp,则我们可以在运行时将它转换成指向Derived的指针,具体代码如下:

if (Derived* dp = dynamic_cast<Derived*>(bp))
{
   
	// 使用dp指向的Derived对象
} else {
    // bp指向一个Base对象
	// 使用bp指向的Base对象
}
  • 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
  • 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。

引用类型的dynamic_cast
当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常,该异常定义在typeinfo标准库头文件中。
我们可以按照如下的形式改写之前的程序,令其使用引用类型:

void f(<
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值