1.控制内存分配
某些程序对于内存分配有特殊的需求,需要重载new运算符和delete运算符。
1.1、重载new和delete
string* sp = new string("value");
delete sp;
new表达式调用一个名为operator new的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象。
编译器调用名为operator delete的标准库函数来释放内存空间。
当我们定义自己的operator new或operator delete函数,编译器将使用我们自定义的版本来代替标准库定义的版本。
应用程序可以在全局作用域中定义operator new函数和operator delete函数,也允许将它们定义为成员函数。
可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接使用全局作用域中的版本。
标准库定义了operator new函数和operator delete函数的八个重载版本:
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 t,nothrow_t&) noexcept; //以下版本不会抛出异常
void* operator new[](size_t,nothrow_t&) noexcept;
void* operator delete(size)t,nothrow_t&) noexcept;
void* operator delete[](void*,nothrow_t&) noexcept;
当我们将上述运算符函数定义成类的成员时,它们都是隐式静态的,并且它们不能操作类的任何数据成员。
对于operator new函数或operator new[]函数来说,它的返回类型必须是void*,第一个形参必须是size_t且该形参不能含有默认实参。
在一般情况下,我们可以自定义具有任何形参的operator new,但下面的函数却不能被用户重载。
void* operator new(size_t,void*); //不允许重新定义这个版本
对于operator delete函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。
使用malloc和free的函数来编写operator new和operator delete函数。
void* operator new(size_t size)
{
if(void* mem = malloc(size)) //分配size大小的空间
return mem;
else
throw bad_alloc(); //抛出异常
}
void operator delete(void* mem) noexcept { free(mem); } //释放mem
1.2、定位new表达式
对于operator new分配的内存空间来说,我们是无法使用construct函数来构造对象。
我们使用new的定位new形式来构造对象。
当仅通过一个地址值调用时,定位new使用operator new(size_t,void*)来分配所需的内存空间。
当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。
调用析构函数只会销毁对象,但是不会释放内存。
2.运行时类型识别
运行时类型识别(RTTI)的功能由两个运算符实现:
- typeid运算符,用于返回表达式的类型。
- dynamic_cast运算符,用于将基类的指针或引用安全地转换为派生类的指针或引用。
当操作被定义为虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。
使用RTTI运算符蕴含着许多的潜在危险,使用者应清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。
在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。
2.1、dynamic_cast运算符
dynamic_cast运算符的使用形式:
dynamic_cast<type*>(e); //e必须是一个有效的指针
dynamic_cast<type&>(e); //e必须是一个左值,才能被引用
dynamic_cast<type&&>(e); //e不能是右值
//type必须是一个类类型,并且通常情况下该类型应该含有虚函数
在上述形式中,e的类型必须符合以下条件中的任意一个:
- e的类型是目标type的公有派生类
- e的类型是目标type的公有基类
- e的类型是目标type的类型
转换失败时,dynamic_cast语句的转换目标是指针类型,则结果为0,若目标是引用类型,则抛出一个bad_cast异常。
在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
//指针bp指向基类base,而derived是base的公有派生类
if(derived* dp = dynamic_cast<derived*>(bp)) //将bp转换成指向derived的指针
我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
当我们使用引用类型的dynamic_cast时,应将其放入try语句块中。
try{
const derived& d = dynamic_cast<const derived&>(b);
}catch(bad_cast){
//处理措施
}
2.2、typeid运算符
typeid表达式的形式:
typeid(e); //e可以是任意表达式或类型的名字
typeid运算符可以作用于任意类型的表达式,与往常一样,顶层const会被忽略掉。
当typeid作用于数组或函数时,并不会执行向指针的标准类型转换。
当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。
通常情况下,我们使用typeid运算符来比较两条表达式的类型是否相同:
derived* dp = new derived;
base* bp = dp;
if(typeid(*bp) == typeid(*bp)) //bp和dp都指向同一类型的对象
当typeid作用于指针时,返回的结果是该指针的静态编译时的类型。
2.3、使用RTTI
当我们想为具有继承关系的类实现相等运算符时,RTTI将非常有用。
在类的继承体系中,每个派生类都有属于自己的数据成员和来自基类的成员。
我们可以通过定义一套虚函数,令其在继承体系中的各个层次上分别执行相等性判断,但是这个方法具有缺陷。
虚函数的基类版本和派生类版本必须具有相同的形参类型,这样就没法比较派生类独有的成员。
若想实现真正有效的相等比较操作,需要认识到参与比较的两个对象类型不同,则比较结果为false
实现思路:先使用typeid来检查运算对象的类型是否相同,若相同,则将工作委托给虚函数equal来进行比较,若运算对象是base时,调用base::equal,若是derived的对象时,则调用derived::equal
bool operator==(const base& lhs,const base& rhs)
{
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
//若typeid不相同,则返回false,或者调用equal
}
继承体系中每个类必须定义自己的equal函数。
bool derived::equal(const base& rhs) const
{
auto r = dynamic_cast<const derived&>(rhs); //每个派生类都要将实参的类型转换为派生类类型
//执行比较两个derived对象的操作
}
2.4、type_info类
C++标准规定type_info类必须定义在typeinfo头文件中。
t1 == t2 | 若type_info对象t1和t2表示同一种类型,返回true |
t1 != t2 | 若type_info对象t1和t2表示不同的类型,返回true |
t.name() | 返回一个C风格字符串,表示类型名字的可打印形式 |
t1.before(t2) | 返回一个bool值,表示t1是否位于t2之前 |
type_info类没有默认构造函数,而且它的拷贝和移动构造函数、赋值运算符都被定义成删除的。
创建type_info对象的唯一途径是使用typeid运算符。
cout << typeid(42).name() << endl;
3.枚举类型
枚举类型可以将一组整型常量组织在一起,枚举属于字面值常量类型。
C++包含两种枚举:限定作用域和不限定作用域的。
使用关键字enum class来定义限定作用域:
enum class open_modes{input,output,append};
定义不限定作用域的枚举类型时省略掉关键字class,枚举类型的名字是可选的。
enum color{red,yellow,green}; //不限定作用域的枚举类型
若enum是未命名的,则我们只能在定义该enum时定义它的对象。
enum{floatpre = 6,doublepre = 10}; //未命名的,不限定作用域的枚举类型
限定作用域的枚举成员在枚举类型的作用域外是不可访问的。
不限定作用域的枚举成员的作用域与枚举类型本身的作用域相同。
enum color {red,yellow,green}; //不限定作用域的枚举类型
enum class peppers {red,yellow,green}; //限定作用域的枚举类型
color eye = red; //正确,可直接访问不限定作用域的枚举成员
peppers p = green; //错误,枚举成员不在有效的作用域中
peppers b = peppers::green; //正确
默认情况下,枚举值从0开始,依次加1。
若我们没有显式地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。
每个枚举成员本身就是一条常量表达式,因此初始值必须是常量表达式。
要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。
open_modes om = 2; //错误
om = open_modes::input; //正确,input是open_modes的一个枚举成员
一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。
int i = color::red; //正确,不限定作用域的枚举类型的枚举成员隐式地转换为int
int j = peppers::red; //错误,限定作用域的枚举类型不会进行隐式转换
C++新标准中,我们可以在enum的名字后定义我们想在enum中使用的类型。
enum intvalue : unsigned long long{
inttyp = 65535,longtyp = 223232323;
};
若我们没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型为int。
在C++新标准中,我们可以提前声明neum。
enum的前置声明(隐式或显式)必须指定其成员的大小。
enum intvalue :unsigned long long; //不限定作用域必须指定成员类型
enum class intvalue; //限定作用域的枚举类型可以使用默认类型int
由于不限定作用域的enum未指定成员的默认大小,因此每个声明必须指定成员的大小。
和其他声明语句一致,enum的声明和定义必须匹配。
enum class intvalue;
enum intvalue; //错误,intvalue已经被声明为限定作用域的enum
尽管我们不能直接将整型值传给enum形参,但是可以将一个不限定作用域的枚举类型的对象传给整型形参。此时,enum的值提升成int或更大的整型。
4.类成员指针
成员指针是指可以指向类的非静态成员的指针,但成员指针指向的是类的成员,而非类的对象。
成员指针的类型包括了类的类型以及成员的类型。
当初始化一个成员指针时,令其指向类的某个成员,但是不指定该成员所属的对象。
4.1、数据成员指针
与普通指针不同的是,成员指针必须包含成员所属的类。
const string screen::* padata; //padata指向一个常量screen对象的string的成员
当我们初始化一个成员指针(或向它赋值)时,需指定它所指的成员。
padata = &screem::contents;
//padata指向某个非特定screen对象的contents成员
当我们初始化一个成员指针或为成员指针赋值时,该指针并未指向任何数据。
成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时才提供对象的信息。
成员指针访问运算符: .* 和 ->* 帮助我们解引用指针并获取该对象的成员。
screen myscreen;
screen* p = &myscreen;
auto s = myscreen.*padata;
s = p->*padata;
这些运算符执行解引用成员指针以得到所需的成员,然后通过访问运算符来获取成员。
常规的访问控制规则对成员指针同样有效。
由于数据成员一般情况下都是私有的,所以我们通常不能直接获取数据成员的指针。
最好是在类的内部定义一个函数,令其返回值是指向该成员的指针。
class screen{
public:
static const std::string screen::* data()
{ return &screen::contents;} //返回数据成员的地址
};
由于数据成员指针复杂的返回类型形式,一般从右向左阅读该函数的返回类型。
4.2、成员函数指针
我们可以定义一个指向类的成员函数的指针,使用auto来推断它的返回类型是最简单的方法。
指向成员函数的指针也需要指定目标函数的返回类型和形参列表。
若成员存在重载的问题,我们在使用时必须显式地声明函数类型以明确使用的是哪个函数。
char (screen::*pmf2)(screen::pos,screen::pos) const;
//由于优先级的不同,screen::两端的括号必不可少
和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
pmf = &screen::get; //正确,必须显式地使用取地址符
pmf = screen::get; //错误,在成员函数和指针之间不存在自动转换规则
我们使用.*和->*运算符作用于指向成员函数的指针,以调用类的成员函数。
screen myscreen;
screen *p = &myscreen;
char c1 = (p->*pmf)(); //通过p所指的对象调用pmf所指的函数
char c2 = (myscreen.*pmf)(0,0); //通过myscreen对象将实参0,0传给含有两个形参的get函数
由于调用运算符的优先级要高于指针指向成员运算符的优先级,因此(myscreen.*pmf)的括号必不可少。
使用类型别名或typedef可以让成员指针更容易理解。
using action = char(screen::*)(screen::pos,screen::pos) const;
我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型。
screen& action(screen&,action = &screen::get);
//指向成员的指针形参可以拥有默认实参
对于普通函数指针和指向成员函数的指针来说,一种常见的用法就是将其存入一个函数表当中。
4.3、将成员函数用作可调用对象
成员指针不是一个可调用对象,这样的指针并支持函数调用运算符。
我们不能直接将一个指向成员函数的指针传递给算法。
从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板funciton。
function<bool(const string&)> fcn = &string::empty;
find_if(svec.begin(),svec.end(),fcn);
若想使用function模板,我们必须提供成员的调用形式。
使用标准库mem_fn来让编译器负责推断成员的类型。
mem_fn可以根据成员指针的类型推断可调用对象的类型,而无须用户显式地指定。
find_if(svec.begin(),svec.end(),mem_fn(&string::empty));
mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用。
auto f = mem_fn(&string::empty);
f(*svec.begin()); //传入一个string对象,f使用.*调用empty
f(&svec[0]); //传入一个string的指针,f使用->*调用empty
我们也可以使用bind从成员函数生成一个可调用对象。
auto it = find_if(svec.begin(),svec.end(),bind(&string::empty, _1));
5.嵌套类
一个类可以定义在另一个类的内部,前者被成为嵌套类或嵌套类型。
嵌套类是一个独立的类,与外层类基本没什么关系,外层类的对象和嵌套类的对象是相互独立的。
嵌套类的名字在外层类作用域中是可见的,在外层作用域之外不可见。
嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部。
当我们在外层类之外定义一个嵌套类时,必须以外层类的名字限定嵌套类的名字:
class textquery::queryresult{...};
在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全的类型。
嵌套类本身是一个嵌套作用域,所以还必须查找嵌套类的外层作用域。
嵌套类是其外层类的一个类型成员,因此外层类的成员可以任意使用嵌套类的名字。
尽管嵌套类定义在其外层类的作用域中,但外层类的对象和嵌套类的对象没有任何关系。
6.union:一种节省空间的类
联合(union)类可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。
当我们给union的某个成员赋值之后,该union的其他成员就变成未定义的状态。
union不能含有引用类型的成员。
在C++新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。
默认情况下,union的成员都是公有的,可以为其成员指定public、private等保护标记。
由于union既不能继承自其他类,也不能作为基类来使用,因此在union中不能含有虚函数。
union提供了一种有效的途径来帮助我们表示一组类型不同的互斥值。
union token{
char cval;
int ival;
double dval;
};
union的名字是一个类型名,默认情况下,union是未初始化的。
token first = {'a'}; //初始化cval成员
token last; //未初始化的token对象
token* p = new token; //指向一个未初始化的token对象的指针
匿名union是一个未命名的union,编译器会自动地为该union创建一个未命名的对象。
在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。
union{
char cval;
int ival;
double dval;
}; //定义一个未命名的对象,我们可以直接访问它的成员
cval = 'c';
ival = 42;
当union包含的是内置类型的成员时,我们可以使用普通的赋值语句改变union保存的值。
当union想将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员。
对于union来说,构造或析构类类型的成员必须执行复杂的操作,因此通常将含有类类型的成员的union内嵌到一个类当中。
为了追踪union中存储了什么类型的值,通常会定义一个独立的对象,该对象被称为union的判别式。
可以定义一个枚举类型的成员追踪其union成员的状态。
7.局部类
类可以定义在某个函数的内部,我们称这种类为局部类。
局部类的所有成员(包括函数)都必须完整地定义在类的内部。
局部类中不允许声明静态数据成员。
局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。
若局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。
外层函数对局部类的私有成员没有任何访问特权。
局部类中的名字查找次序与其他类相似,都是从内向外依次查找各个作用域。
允许在局部类的内部再嵌套一个类,嵌套类的定义可以出现在局部类之外。
当我们在类的外部定义成员时,必须指明该成员所属的作用域。
8.固有的不可移植的特性
为了支持底层编程,C++定义了一些固有的不可移植的特性。
8.1、位域
类可以将其非静态数据成员定义成位域,在一个位域中含有一定数量的二进制位。
位域在内存中的布局是与机器相关的,通常在给设备传递二进制机制时用到。
位域的类型必须是整型或枚举类型,通常使用无符号类型来保存一个位域。
位域表达式用于指定成员所占的二进制位数。
class file{
unsigned int mode : 2; //mode占2位
};
取地址运算符不能作用于位域,因此任何指针都无法指向类的位域。
通常情况下最好将位域设为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定。
通常使用内置的位运算符操作位域。
8.2、volatile限定符
关键字volatile能限制编译器去对运算对象进行优化。
volatile int display; //该int值可能发生改变
volatile task* current; //current指向一个volatile对象
volatile int iax[max_size]; //iax的每个元素都是volatile
只有volatile的成员函数才能被volatile的对象调用。
我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或对volatile对象赋值。
若一个类希望拷贝、移动或赋值它的volatile对象,则该类必须自定义拷贝或移动操作。
可以将形参类型指定为const volatile引用,这样我们就能利用任意类型的对象。
8.3、链接指示:extern"C"
C++程序有时需要调用其他语言编写的函数,使用链接指示来指出任意非C++函数所用的函数。
链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明都出现。
extern "C" size_t strlen(const char*); //单语句链接指示
extern "C" { int strcmp(const char*,const char*);
char* strcat(char*, const char*);
} //复合语句链接指示
其中的字面值常量指出了编写函数所用的语言。
当一个头文件被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。
extern "C"{
#include<string.h> //操作C风格字符串的C函数
.....
}
指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示。
extern "C" void (*pf)(int); //pf指向了一个C函数,该函数接受一个int返回void
当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。
extern "C" void f1(void(*)(int));
//f1是一个C函数,它的形参是一个指向C函数的指针
链接指示与重载函数的相互作用依赖于目标语言。