C++ Primer Study Notes -----Chapter 19: Special Tools and Techniques

1. Control memory allocation

1.1. Overloading new and delete
insert image description hereinsert image description hereinsert image description hereinsert image description here
malloc functions and free functions

malloc和free函数是C++从C语言继承的,定义在cstdlib头文件中。
使用malloc和free来编写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); }

1.2. Locate the new expression

operator newoperator delete是标准库的两个普通函数,因此普通的代码也可以直接调用它们。
与allocator不同,对于operator new分配的内存空间来说我们无法使用construct函数构造对象。相反应该使用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*)“分配”它的内存。这是一个我们无法自定义的operator
new版本。该函数不分配任何内存,只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。

定位new和allocator的construct成员非常相似,但它们之间有一个重要区别:
传给construct的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无须指向operator new分配的内存。
甚至传给定位new表达式的指针不需要指向动态内存。

Displayed destructor calls

就像定位new与使用allocate类似一样,对析构函数的显示调用也与使用destroy很类似,既可以通过对象调用析构函数,也可以
通过对象的指针或引用调用析构函数:
string* sp = new string("value");
sp->~string();
和调用destroy类似,调用析构函数可以清楚给定对象,但是不会释放该对象所在的空间,如有需要,还可以重新使用该空间。

2. Runtime type identification

运行时类型识别(run-time type identification,RTTI)的功能由两个运算符实现:
typeid运算符,用于返回表达式的类型。
dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。

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

2.1. dynamic_cast operator

dynamic_cast<type*>(e);			//e是一个有效指针
dynamic_cast<type&>(e);			//e是一个左值
dynamic_cast<type&&>(e);		//e是一个右值

e的类型必须符合以下三个条件中的任意一个:
1.e的类型是目标	type的公有派生类
2.e的类型是目标	type的公有基类
3.e的类型是目标	type的类型

如果符合,则类型转换可以成功,否则转换失败。
如果dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0.
如果转换目标是引用类型并且失败了,dynamic_cast运算符将抛出一个bad_cast异常。

***指针类型的dynamic_cast
if(Derived* dp = dynamic_cast<Derived*>(bp)		//类型转换和结果检查在一条表达式中完成
{
    
    
	//转换成功,使用dp指向的Derived对象
}
else
{
    
    
	//bp指向一个Base对象
}

***引用类型的dynamic_cast
void fun(const Base &b)
{
    
    
	try
	{
    
    
		const Derived& d = dynamic_cast<const Derived&>(b);
	}
	catch(bad_cast)
	{
    
    
		//处理失败情况
	}	
}

2.2.typeid operator: get the type of object

typeid(e)	:返回类型是type_info或type_info公有派生类,定义在头文件typeinfo中

typeid运算符可以用于任意类型的表达式,和往常一样,忽略顶层const。
如果表达式是一个引用,返回该引用所引对象的类型。
作用于数组或函数时,返回的也是数组或函数类型,不会执行向指针的标准类型转换。
当运算对象时定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。

***使用typeid运算符
通常情况下,使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。

Derived* dp = new Derived;
Base* bp = dp;
if(typeid(*bp) == typeid(*dp)){
    
     }
if(typeid(*bp) == typeid(Derived)){
    
     }

typeid(*p);	
如果指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针。否则,*p将在运行时求值。
此时p必须是一个有效的指针。如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。

2.3. Using RTTI

class Base
{
    
    
	friend bool operator==(const Base&,const Base&);
public:
	/*其他接口成员*/
protected:
	virtural bool equal(const Base&)const;	
};

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

class Derived:public Base
{
    
    
public:
	/*其他接口成员*/
protected:
	bool equal(const Base&)const;
};

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

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

2.4.type_info class
insert image description here
insert image description here
3. Enumeration type

枚举类型使我们可以将一组整型常量组织在一起。枚举属于字面值常量类型。

enum class open_modes{
    
    input,output,append};				//限定作用域的枚举类型
enum struct open_modes{
    
    input,output,append};			//限定作用域的枚举类型
enum color{
    
    red,yellow,green};							//不限定作用域的枚举类型
enum{
    
    floatPrec=6,doublePrec=10,double_doublePre=10};	//未命名,不限定作用域
enum {
    
     A, B, C };										//未命名,不限定作用域

int a = A;		//枚举可以隐式转换为整型,整型依赖了枚举的潜在类型

insert image description here
Forward declaration of enum type

C++11新标准中,可以提前声明enumenum的前置声明必须指定其成员大小:
enum intValues:unsigned long long;	//不限定作用域的,必须指定成员类型
enum class open_modes;				//限定作用域的枚举类型可以使用默认成员类型int

4. Class member pointer

成员指针是指可以指向类的非静态成员的指针。成员指针指示的是类的成员,而非类的对象。类的静态成员不属于任何对象,因此
无须特殊的指向静态成员的指针,指向静态成员的指针域普通指针没有区别。

4.1. Data member pointer

成员指针还必须包含成员所属的类,因此必须在*之前添加classname::以表示当前定义的指针可以指向classname的成员:
const string Screen::*pdata;	//pdata可以指向一个Screen对象的string的成员
pdata = &Screen::contents;		//取地址运算符作用于Screen类的成员而非内存中的一个该类对象。

C++11可以使用autodecltypeauto pdata = &Screen::contents;

***使用数据成员指针***
当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象。
只有当解引用成员指针时我们才提供对象的信息。
与成员访问运算符.->类似,也有两种成员指针访问运算符:.*->*
Screen myScreen,*pScreen = &myScreen;
auto s = myScreen.*pdata;
s = pScreen->*pdata;
从概念上说,这些运算符执行两步操作:首先解引用成员指针已得到所需的成员;然后像成员访问运算符一样,通过对象
(.*)或指针(->*)获取成员。

***返回数据成员指针的函数***
class Screen
{
    
    
public:
	static const std::string Screen::* data() {
    
     return &Screen::contents; }
	//其他成员
};

const string Screen::* pdata = Screen::data();
pdata指向Screen类的成员而非实际数据。要想使用pdata,必须把它绑定到Screen类型的对象上:
auto s = myScreen.*pdata;

4.2. Member function pointer

也可以定义指向类的成员函数的指针:
auto pmf = &Screen::get_cursor;

char (Screen::*pmf2)(Screen::pos,Screen::pos) const;
pmf2 = &Screen::get;	//必须显示地使用取地址运算符
pmf2 = Screen::get;		//错误:在成员函数和指针之间不存在自动转换规则

***使用成员函数指针***
Screen myScreen,*pScreen = &myScree;
char c1 = (pScreen->*pmf)();			//通过pScreen所指的对象调用pmf所指的函数
char c2 = (myScreen.*pmf)(0,0);			//通过myScreen对象将实参0,0传给含有两个形参的get函数
需要加括号,原因是调用运算符的优先级要高于指针指向成员运算符的优先级

***使用成员指针的类型别名***
使用类型别名或typedef可以让成员指针更容易立即:
using Action = char (Screen::*)(Screen::pos,Screen::pos) const;
Action get = &Screen::get;
也可以用作返回类型和形参:
Screen& action(Screen&,Action = &Screen::get);
Screen myScreen;
action(myScreen);					//使用默认实参
action(myScreen,get);				//使用之前的变量
action(myScreen,&Screen::get);		//显示传入地址

4.3. Using member functions as callable objects

成员指针不是一个可调用对象,不支持函数调用运算符。
auto fp = &string::empty;	
find_if(svec.begin(),svec.end(),fp);
find_if算法需要一个可调用对象,但我们提供给它的是一个指向成员函数的指针fp。因此在内部将执行如下形式的代码:
if(fp(*it))		//错误:想要通过成员指针调用函数,必须使用->*运算符

***使用function生成一个可调用对象***
从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function:
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(),svec.end(),fcn);
内部实现可能是这种形式:
if(fcn(*it))	//it是迭代器,*it是一个对象
if(((*it).*p)())		//转换为这种形式

举例:
vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
find_if(pvec.begin(),pvec.end(),fp);

***使用mem_fn生成一个可调用对象***
使用mem_fn让编译器负责推断成员的类型,mem——fn也定义在function头文件中,并且可以从成员指针生成一个可调用对象。
find_if(svec.begin(),svec.end(),mem_fn(&string::empty));
使用mem_fn(&string::empty)生成一个可调用对象,该对象接受一个string实参,返回一个bool值。

auto f = mem_fn(&string::empty);
f(*svec.begin());		//正确:传入一个string对象,f使用.*调用empty
f(&svec[0]);			//正确:传入一个string的指针,f使用->*调用empty
实际上,可以认为mem_fn生成的可调用对象含有一对重载的函数调用运算符:一个接受string*,另一个接受string&***使用bind生成一个可调用对象***
auto it= find_if(svec.begin(),svec.end(),bind(&string::empty,_1));
和function类似的地方是,当我们使用bind时,必须将函数中用于表示执行对象的隐式形参转换成显示的。和mem_fn类似的地方是
bind生成的可调用对象的第一个实参既可以是string的指针,也可以是string的引用:
auto f = bind(&string::empty,_1);
f(*svec.begin());
f(&svec[0]);

5. Nested class: Nothing to say, just pay attention to the scope, nested class has nothing to do with the outer class

6. union: a space-saving type
insert image description here

union Token				//名字是可选的,Token可以保存一个值,这个值的类型可能是char、int、double中的一种
{
    
    
	char cval;
	int ival;
	double dval;
};

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

last_token.cval = 'z';
pt->ival = 4;union的一个数据成员赋值会令其他数据成员变成未定义的状态。

***匿名union***
匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
union
{
    
    
	char cval;
	int ival;
	double dval;
};					//定义一个未命名的对象,可以直接访问它的成员

cval = 'c';
ival = 4;

More complex types of union reading.

7. Partial class

类可以定义在某个函数的内部,称这样的类为局部类。局部类定义的类型只在定义它的作用域内可见。
和嵌套类不同,局部类的成员受到严格限制。
局部类的所有成员(包括函数在内)都必须完整定义在类的内部。
在实际编程的过程中,因为局部类的成员必须完整定义在类的内部,所以成员函数的复杂性不可能太高。
局部类的成员函数一般只有几行代码,否则就很难读懂它了。
类似的,在局部类中也不允许声明静态数据成员,因为没法定义这样的成员。

Local classes cannot use variables in function scope

局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。
如果局部类定义在某个函数的内部,则该函数的普通局部变量不能被该局部类使用:
int a,val;
void fun(int val)
{
    
    
	static int si;
	enum Loc{
    
    a=1024,b};
	struct Bar
	{
    
    
		Loc locVal;				//正确:使用一个局部类型名
		int barVal;
		void funBar(Loc l = a)	//正确:默认实参时Loc::a
		{
    
    
			barVal = val;		//错误:val是fun的局部变量
			barVal = ::val;		//正确:使用一个全局对象
			barVal = si;		//正确:使用一个静态局部对象
			locVal = b;			//正确:使用一个枚举成员
		}
	};
}

insert image description here

8. Inherent non-portability

8.1. Bit fields

类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。
当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域在内存中的布局是与机器相关的。
位域的类型必须是整型或枚举类型。通常情况下使用无符号类型保存一个位域。
位域的声明形式是成员名字之后紧跟这个一个冒号以及一个常量表达式,常量表达式用于指出成员所占的二进制位数。

typedef unsigned int Bit;
class File
{
    
    
	Bit mode : 2;
	Bit modified : 1;
	Bit prot_owner : 3;
	Bit prot_group : 3;
	Bit prot_world : 3;
public:
	enum modes {
    
     Read = 01, Write = 02, Execute = 03 };
	void write() {
    
     modified = 1; }			//只有一位,直接设置值
	void close() {
    
     if (modified) /**/ }
	File& open(File::modes m)
	{
    
    
		mode |= Read;			//按默认方式设置Read
		if (m & Write)
		{
    
    
			//处理
		}
		return *this;
	}
	bool isRead()const {
    
     return mode & Read; }
	void setWrite() {
    
     mode |= Write; }
};
五个位域可能会存储在同一个unsigned int,能否压缩到一个整数中以及如何压缩是与机器相关的。

8.2. volatile qualifier

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

volatile int a;					//该int值可能发生改变
volatile Task* curr_task;		//curr_task指向一个volatile对象
volatile int arr[maxsize];		//arr的每个元素都是volatile
volatile Screen scr;			//scr的每个成员都是volatile
const volatile int b;

void fun() volatile {
    
    }
只有volatiled的成员函数才能被volatile的对象调用。

volatile int v;
volatile int* vp1;
int *volatile vp2;
volatile int* volatile vp3;

int *p = &v;	//错误:必须使用指向volatile的指针
vp1 = &v;		//正确
vp3 = &v;		//正确
只能将一个volatile对象的地址赋给一个指向volatile的指针。
只有当某个引用时volatile的时,才能使用一个volatile对象初始化该引用。

Synthetic copying is not valid for volatile objects

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

如果一个类希望拷贝、移动或赋值它的volatile对象,则该类必须自定义拷贝或移动操作。
class Test
{
    
    
public:
	Test(const volatile Test&);							//从一个volatile对象进行拷贝
	Test& operator=(volatile const Test&);				//将一个volatile对象赋值给一个非volatile对象
	Test& operator=(volatile const Test&) volatile;		//将一个volatile对象赋值给一个volatile对象
};

虽然可以为volatile对象自定义拷贝和赋值操作,但是一个更深层次的问题是拷贝volatile对象是否有意义呢?

8.3. Link directive: extern "C"
insert image description here
declares a non-C++ function

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

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”等

insert image description here

insert image description here
insert image description here
insert image description here
insert image description here
insert image description here

Guess you like

Origin blog.csdn.net/weixin_41155760/article/details/126142394