《C++11Primer》阅读随记 -- 十九、特殊工具与技术

第十九章 特殊工具与技术

控制内存分配

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

实际执行了三步步骤。

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

当用 delete 表达式删除一个动态分配的对象时

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

实际执行了两步步骤。

  • sp 所指的对象或 arr 所指的数组中的元素执行相应的析构函数
  • 编译器调用名为 operator delete( 或者 operator delete[] ) 的标准库函数释放内存空间

如果应用程序希望控制内存分配的过程,则它们需要定义自己的 operator new 函数和 operator delete 函数。

当自定义了全局的 operator new 函数和 operator delete 函数后,我们就负担起了控制动态内存分配的职责。这两个函数必须是正确的;因为它们是程序整个处理过程中至关重要的一环

operator new 接口和 operator delete 接口

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

void *operator new(size_t);					// 分配一个对象
void *operator new[](size_t);				// 分配一个数组
void *operator delete(void*) noexcept;		// 释放一个对象
void *operator delete[](void*) noexcept;	// 释放一个数组

void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;

如果我们像自定义 operator new 函数,则可以为它提供额外的形参。此时,用到这些自定义函数的 new 表达式必须使用 new 的定位形式将实参传给新增的形参。尽管在一般情况下我们可以自定义具有任何形参的 operator new,但下面的函数不能被用户重载

void *operator new(size_t, void*); // 不允许重新定义这个版本

malloc 函数和 free 函数

malloc 函数接受一个表示待分配字节数的 size_t,返回指向发呢配空间的指针或者返回 0 以表示分配失败。free 函数接收一个 void*,它是 malloc 返回的指针的副本,free 将相关内存返回 给系统。调用 free(0) 没有任何意义

如下是编写 operator newoperator 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); }

定位 new 表达式

allocator 不同的是,对于 operator new 分配的内存空间来说,我们无法使用 construct 函数构造对象。相反,我们应该使用 new定位 new( placement new ) 形式构造对象。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 使用 operator new(size_t, void*) “分配” 它的内存。该函数不分配任何内存,它只是简单地返回指针实参;然后由 new 表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位 new 允许我们在一个特定地、预先分配地内存低地址上构造对象。

当只传入一个指针类型的实参时,定位 new 表达式构造对象但是不分配内存

尽管在很多时候使用定位 newallocatorconstruct 成员非常相似,但在它们之间也有一个重要的区别。我们传给 construct 的指针必须指向同一个 allocator 对象分配的空间,但是传给定位 new 的指针无须指向 operator new 分配的内存。实际上,传给定位 new 表达式的指针甚至不需要执行动态内存

运行时类型识别

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

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

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

dynamic_cast 运算符

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

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

其中,type 必须是一个类类型,并且通常情况下该类型应该含有虚函数。

在上面的所有形式中,e 地类型必须复合下面三个条件中的任意一个:e 的类型是目标 type 的公有派生类、e 的类型是目标 type 的公有基类或者 e 的类型就是目标 type 的类型。

指针类型的 dynamic_cast

假定 base 类至少含有一个虚函数,DerivedBase 的公有派生类,如果有一个指向 Base 的指针 bp,则我们可以在运行时将它转换成指向 Derived 的指针

if( Derived* dp = dynamic_cast<Derived*>(bp) )
{
	// 使用 dp 指向的 Derived 对象
}
else
{
	// 使用 bp 指向的 Base 对象
}
引用类型的 dynamic_cast

引用类型的与指针类型在表示错误发生方式上略有不同。因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略

void f(const Base& b){
	try{
		const Derived& d = dynamic_cast<const Derived&>(b);
	// 使用 b 引用的 Derived 对象
	} catch(bad_cast){
		// 处理类型转换失败的情况
	}
}

typeid 运算符

typeid(e) e 可以是任意表达式或类型的名字。typeid 操作的结果是一个常量对象的引用,该对象的类型是标准库类型 type_infotypeid 的公有派生类型。

当我们对数组 a 执行 typeid(a) 所得到的结果是数组类型而非指针类型。

当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid 运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid 的结果直到运行时才会求得

Derived* dp = new Derived;
Base* bp = dp;			// 两个指针都指向 Derived 对象
// 在运行时比较两个对象的类型
if(typeid(*bp) == typeid(*dp)){
	// bp 和 dp 指向同一类型的对象
} 
// 检查运行时类型是否时某种指定的类型
if(typeid(*bp) == typeid(Derived)){
	// bp 实际指向 Derived 对象
}

注意:typeid 应该作用于对象,因此使用 *bp 而非 bp

// 下面的检查永远是失败的; bp 的类型是指向 Base 的指针
if(typeid(bp) == typeid(Derived)){
	// 此处的代码永远不会执行
}

typeid 作用于指针时,返回的结果是该指针静态编译时的类型

使用 RTTI

定义两个实例类:

class Base{
	friend bool operator==(const Base&, const Base&);
public:
	// 接口
protected:
	virtual bool equal(const Base&) const;
	// ...
};

class Derived: public Base{
public:
	// ...
protected:
	bool equal(const Base&) const;
	// ...
};

类型敏感的相等运算符

bool operator==(const Base& lhs, const Base& rhs){
	// 如果 typeid 不相同,返回 false; 否则虚调用 equal
	return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

虚 equal 函数
继承体系中的每个类必须定义自己的 equal 函数。派生类的所有函数要做的第一件事都是相同的,那就是将实参的类型转换为派生类类型

bool Derived::equal(const Base& rhs) const{
	auto r = dynamic_cast<const Derived&>(rhs);
	// 执行比较两个 Derived 对象的操作并返回结果
}

基类 equal 函数

bool Base::equal(const Base& rhs) const{
	// 执行比较 Base 对象的操作
} 

枚举类型

C++ 包含两种枚举:限定作用域和不限定作用域的。C++11 新标准引入限定作用域的枚举类型。定义限定作用域的枚举类型的一般形式是:首先是关键字 enum class 或者等价地使用 enum struct,随后是枚举类型名字以及用花括号括起来地以逗号分隔的 枚举成员 列表,随后是一个分号

enum class open_modes { input, output, append };

定义了一个名为 open_modes 的枚举类型,它包含三个枚举成员:input、output 和 append

定义不限定作用域的枚举类型时忽略掉关键字 classstruct, 枚举类型的名字时可选的

enum color{red, yellow, green};
// 未命名的、不限定作用域的枚举类型
enum{floatPrec = 6, doublePrec = 10, double_doublePrec = 10};

如果 enum 是未命名的,则我们智能在定义该 enum 时定义它的对象。和类的定义类似,我们需要在 enum 定义的右侧花括号和最后的分号之间提供逗号分隔的声明列表

枚举成员

在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同

enum color { red, yellow, green }; 		// 不限定作用域的枚举类型
enum stoplight { red, yellow, green };	// 错误:重复定义了枚举成员
enum class peppers { red, yellow, green };	// 正确:枚举成员被隐藏了
color eyes = green;		// 正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green;		// 错误:peppers 的枚举成员不再有效的作用域中
						// color::green 在有效的作用域中,但是类型错误
color hair = color::red;	// 正确:允许显式地访问枚举成员
peppers p2 = peppers::red;	// 正确:使用 peppers 的 red

默认情况下,枚举类型值从 0 开始,依次加 1.不过也能为一个或几个枚举成员指定专门的值

enum class intTypes{
	charTyp = 8, shortTyp = 16, intTyp = 16,
	longTyp = 32, long_longTyp = 64;
};

类成员指针

成员指针(point to member) 是指可以指向类的非静态成员的指针。类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别

成员指针的类型囊括了类的类型以及成员的类型。当初始化这样一个指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象

class Screen{
public:
	typedef std::string::size_type pos;
	char get_cursor() const { return contents[cursor]; }
	char get() const;
	char get(pos ht, pos wd) const;
private:
	std::string contents;
	pos cursor;
	pos height, width;
};

// pdata 可以指向一个常量 Screen 对象的 string 成员
const string Screen::*pdata;

上述语句将 pdata 声明成 “一个指向 Screen 类的 const string 成员的指针”。常量对象的数据成员本身也是常量,因此将我们的指针声明成指向 const string 成员的指针意味着 pdata 可以指向任何 Screen 对象的一个成员,而不管该 Screen 对象是否时常量。作为交换条件,我们只能使用 pdata 读取它所指的成员,而不能向它写入内容

初始化一个成员指针或者向它赋值时,需指定它所指的成员

pdata = &Screen::contents;
auto pdata = &Screen::contents;

使用数据成员指针

Screen myScreen, *pScreen = &myScreen;
// .* 解引用 pdata 以获得 myScreen 对象的 contents 成员
auto s = myScreen.*pdata;
// ->*解引用 pdata 以获得 pScreen 所指对象的 contents 成员
s = pScreen->*pdata;

从概念上来说,这些运算符执行两部操作:首先解引用成员指针以得到所需的成员;然后像成员访问运算符一样,通过对象( .* )或指针( ->* )获取成员

成员函数指针

和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则:

// pmf 指向一个 Screen 成员,该成员不接受任何实参且返回类型是 char
pmf = &Screen::get;		// 必须显式地使用取地址运算符
pmf = Screen::get;		// 错误:在成员函数和指针之间不存在自动转换规则

使用成员函数指针

Screen myScreen, *pScreen = &myScreen;
// 通过 pScreen 所指地对象调用 pmf 所指的函数
char c1 = (pScreen->*pmf)();
// 通过 myScreen 对象将私产0, 0 传给含有两个形参的 get 函数
char c2 = (myScreen.*pmf2)(0, 0);

使用成员指针的类型别名

// Action 是一种可以指向 Screen 成员函数的指针,它接收两个 pos 实参,返回一个 char
using Action = 
char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;  // get 指向 Screen 的 get 成员

和其他函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型

Screen& action(Screen&, Action = &Screen::get);
Screen myScreen;
// 等价的调用
action(myScreen);
action(myScreen, get);	// 使用之前定义的变量 get
action(myScreen, &Screen::get);	// 显式地传入地址

成员指针函数表

class Screen{
public:
	// 其他接口和实现成员与之前一致
	Screen& home();			// 光标移动函数
	Screen& forward();
	Screen& back();
	Screen& up();
	Screen& down();
};

定义一个 move 函数,使其可以调用上面地任意一个函数并执行对应的操作。为了支持这个新韩淑,将在 Screen 中添加一个静态成员,该成员是指向光标移动函数的指针的数组

class Screen{
public:
	// ...
	// Action 是一个指针,可以用任意一个光标移动函数对其赋值
	using Action = Screen& (Screen::*));
	// 指定具体要移动的方向
	enum Directions { HOME, FORWARD, BACK, UP, DOWN };
	Screen& move(Directoins);
private:
	static Action Menu[];		// 函数表
};
Screen& Screen::move(Directions cm){
	// 运行 this 对象中索引值为 cm 的元素
	return (this->*Menu[cm])();	// Menu[cm] 指向一个成员函数
}


Screen myScreen;
myScreen.move(Screen::HOME);		// 调用 myScreen.home
myScreen.move(Screen::DOWN);		// 调用 myScreen.down

剩下就是定义并初始化函数表本身

Screen::Action Screen::Menu[] = {
	&Screen::home,
	&Screen::forward,
	&Screen::back,
	&Screen::up,
	&Screen::down                                                                                                       
};

union: 一种节约空间的类

联合( union ) 是一种特殊的类。一个 unioin 可以有多个数据成员,但是在任一时刻只有一个数据成员可以有值。当我们给 union 的某个成员赋值之后,该 union 的其他成员就变成未定义的状态了。分配一个 union 对象的存储空间至少要能容纳它的最大的数据成员。

类的某些特性对 union 同样适用,但并非所有特性都如此。union 不能含有引用类型的成员,除此之外,他的成员可以是绝大多数类型。默认情况下 union 的成员是公有的。

union 可以定义包括构造函数和析构函数在内的成员函数。但是由于 union 既不能继承自其他类,也不能作为基类使用,所以在 union 中不能含有虚函数

定义 union

// Token 类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union Token{
	// 默认情况下成员是公有的
	char cval;
	int ival;
	double dval;
};

使用 union 类型

默认情况下,union 是未初始化的。

Token first_token = { 'a' };		// 初始化 cval 成员
Token last_token;					// 未初始化的 Token 对象
Token* pt = new Token;				// 指向一个未初始化的 Token 对象的指针

如果提供了初始值,则该初始值被用于初始化第一个成员。因此,first_token 的初始化过程实际上是给 cval 成员赋了一个初值

last_token.cval = 'z';
pt->ival = 42;

匿名 union

一旦我们定义了一个匿名 union ,编译器就自动地位该 union 创建一个未命名对象

union {
	char cval;
	int ival;
	double dval;
}; // 定义一个未命名对象,我们可以直接访问它的成员

cval = 'c';			// 为刚刚定义的未命名的匿名 union 对象赋一个新值
ival = 42;			// 该对象当前保存的值是 42

在匿名 union 的定义所在的作用域内该 union 的成员都是可以直接访问的

在这里插入图片描述

位域

类可以将其( 非静态 )数据成员定义成 位域( bit-field ), 在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域

volatile 限定符

volatile 的确切含义与及其有关,只能通过阅读编译器文档来理解。

直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile。关键字 volatile 告诉编译器不应该对这样的对象进行优化

volatile 限定符的用法和 const 相似,它起到对类型额外修饰的作用

volatile int display_register;		// 该 int 值可能发生改变
volatile Task* curr_task;			// curr_task 自画像一个 volatile 对象
volatile int iax[max_size];			// iax 的每个元素都是 volatile
volatile Screen bitmapBuf;			// bitmapBuf 的每个成员都是 volatile

合成的拷贝对 volatile 对象无效

constvolatile 的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile 对象或从 volatile 对象赋值。合成的成员接收的形参类型是 ( 非 volatile ) 常量引用,显然我们不能把一个非 volatile 引用绑定到一个 volatile 对象上

如果一个类希望拷贝、移动或赋值它的 volatile 对象,则该类必须自定义拷贝或移动操作。例如,我们可以将形参类型指定为 const volatile 引用,这样就可以利用任意类型的 Foo 进行拷贝或赋值操作了

class Foo{
public:
	Foo(const volatile Foo&);	// 从一个 volatile 对象进行拷贝
	// 将一个 volatile 对象赋值给非 volatile 对象
	Foo& operator=(volatile const Foo&);
	// 将一个 volatile 对象赋值给一个 volatile 对象
	Foo& operator=(volatile csont Foo&) volatile;
	// ...
};

extern “C”

声明一个非 C++ 的函数

链接提示可以由两种形式:单个的或者复合的。链接提示不能出现在类定义或函数定义的内部。同样的链接提示必须在函数的每个声明中都出现

// 可能出现在 C++ 头文件 <cstring> 中的链接提示
// 单语句链接提示
extern "C" size_t strlen(const char*);
// 复合语句链接提示
extern "C" {
	int strcmp(const char*, const char*);
	char* strcat(char*, const char*);
}

链接提示的第一种形式包含一个关键字 extern,后面是一个字符串字面值常量以及一个“普通的”函数声明

其中的字符串字面值常量指出了编写函数所用的语言。编译器应该支持对 C 语言的链接提示。此外,编译器也可能会支持其他语言的链接指示,如 extern "Ada"、extern "FORTRAN" 等。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Artintel

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

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

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

打赏作者

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

抵扣说明:

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

余额充值