文章目录
特殊工具与技术
1.控制内存分配
- 应用程序需要重载new和delete运算符以控制内存分配
1.1 重载new和delete
- 使用一个new表达式时,实际执行了三步操作
- new表达式调用一个名为delete new(或者operator new[])的标准库函数
- 编译器运行相应的构造函数初始化特定类型的对象
- 对象被分配了空间并构造完成,返回一个指向该对象的指针
- 使用了delete表达式删除一个动态分配的对象
1.对指针所指的对象或数组中的元素执行对应的析构函数
2.编译器调用名为operator delete(或者operator delete[])释放内存空间
- 当自定义了全局的operator new函数和operator delete函数之后,我们就担负起了控制动态内存分配的职责
- 首先查找是否有自定义版本,若有则调用自定义版本,没有则调用标准库版本
- 可以使用作用域运算符令new、delete忽略类中的函数,直接执行全局版本
::new; //执行全局作用域中的版本
- 类型nothrow_t是定义在new头文件的一个struct,用户可以通过这个对象请求new或delete的非抛出版本
void *operator new(size_t, nothrow_t&) noexcept; //noexcept指定不抛出异常
coid *operator delete(void*, nothrow_t&) noexcept;
- 当将上述运算符定义成类的成员时,它们
隐式静态的
:因为operator new用在对象构造函数之前而operator delete用在对象销毁之后,所以new和delete必须是静态的,而且不能操纵类的任何数据成员 - 自定义operator new函数,则可以为它提供额外的形参;但
下面的函数不能被用户重载
void *operator new(size_t, void*); //不允许重新定义这个版本呢
- 一条new表达式执行过程总是先调用operator new函数以获取内存空间,然后在得到的内存空间中构造对象;delete表达式总是先销毁对象,然后调用operator delete函数释放对象所占的内存空间
- 当自定义了全局的operator new和operator delete函数后,可以使用名为malloc和free执行分配内存和释放内存的操作
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 定位new表达式
- operator new和operator delete函数与allocator的allocate成员和deallocate成员类似,负责分配或释放空间,但不会构造或销毁对象
- 对于operator new分配的内存空间无法使用construct函数构造函数,应该使用new的
定位new
形式构造对象:
new (place_address) type //place_address必须是一个指针
- 当只传入一个指针类型的实参时,定位new表达式构造对象但不分配内存;传给new的指针无须指向operator new分配的内存
- 调用构造函数会销毁对象,但是不会释放内存
2. 运行时类型识别
- 运行时类型识别(RTTI)的功能由两个运算符实现:
1.typeidd运算符,用于返回表达式的类型
2.dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用 - 当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或饮用所绑定对象的
动态类型(动态绑定)
- RTTI适用:想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数
2.1 dynamic_cast运算符
- dynamic_cast运算符使用形式:
dynamic_cast<type*>(e) //type必须是一个类类型,并且通常情况下该类型应该含有虚函数
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
- e的类型必须符合以下三个条件中的任意一个:e的类型是目标type的共有派生类、e的类型是目标type的共有基类或者e的类型就是type的类型
- 一条dynamic_cast语句,指针类型转换失败返回0,引用类型转换失败返回bad_cast异常,该异常定义在
typeinfo
标准库头文件中 - 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针
- 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成
2.2 typeid运算符
- typeid表达式的形式:
typeid(e) //e可以是任意表达式或类型的名字,得到结果为一个常量的引用
- 如果表达式是一个引用,则typeid返回该引用所引对象的类型;如果对数组a执行typeid(a),则所得结果是数组类型而非指针类型
- 当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译类型(即指针类型)
- 如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型(即动态绑定)
2.3使用RTTI
- 解决具有继承关系的类实现相等运算符
- 定义相等运算符,形参是基类的引用,然后使用typeid检查两个运算独对象的类型是否一致。如果类型不一致,则==返回false;类型一致才调用equal函数
class Base {
friend bool operator==(const Base&, const Base&);
public:
//Base的接口成员
protected:
virtual bool equal(const Base&) const;
};
class Derived : public Base {
public:
//Derived的其他接口成员
protected:
bool equal(const Base&) const;
//Derived的数据成员和其他用于实现的成员
};
bool operator==(const Base &lhs, const Base &rhs)
{
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
bool Derived::equal(const Base &rhs) const
{
auto r = dynamic_cast<const Derived&>(rhs);
}
bool Base::equal(const Base &rhs) cosnt
{
//执行比较Base对象的操作
}
2.4 type_info类
- type_info类必须定义在typeinfo头文件中,并至少提供以下的操作:
type_info的操作 | |
---|---|
t1 == t2 | t1和t2表示同一种类型,返回true,否则返回false |
t1 != t2 | t1和t2表示不同类型,返回true,否则返回false |
t.name() | 返回一个C风格字符串,表示类型名字 |
t1.before(t2) | 返回一个bool值,表示t1是否位于t2之前 |
3. 枚举类型
- 枚举类型使我们可以将一组整型常量组织在一起;枚举属于字面值常量类型
- 带有关键字class或struct的枚举类型为限定作用域的枚举类型;省略掉关键字的则为不限定作用域的枚举类型
enum class open_modes {input, output, append}; //限定作用域
enum color {red, yellow, green}; //不限定作用域
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10}; //enum未命名,只能在定义enum时定义它的对象
- 限定作用域的枚举类型,成员名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的;在不限定作用域的枚举类型中,枚举成员的作用域与枚举本身的作用域相同
- 默认情况下,枚举值从0开始,依次加1,也可以指定专门的值
- 枚举成员是const,因此在初始化成员时提供的初始值必须是常量表达式:
enum class intTypes {
charType = 8, shortType = 16, intType = 16,
longType = 32, long_longType = 64
};
constexpr intTypes charbits = intTypes::charType; //必须是常量表达式
//必须使用该类型的一个枚举成员或者该类型的另一个对象初始化enum对象
open_modes om = 2; //错误,2不属于类型open_modes
om = open_modes::input; //正确:input是open_modes的一个枚举成员
- 一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型
- 可以先声明不定义,enum的前置声明必须指定其成员的大小;对于不限定作用域的enum来说,隐式定义为int:
enum intValues : unsigned long long; //不限定作用域,必须指定成员类型
enum class open_modes; //限定作用域,隐式定义成int
- 对于形参匹配,即使某个整型值恰好与枚举成员的值相等,也不能作为函数的enum实参使用:
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main() {
Tokens curTok = INLINE;
ff(128); //精确匹配ff(int)
ff(INLINE); //精确匹配ff(Tokens)
ff(curTok); //精确匹配ff(Tokens)
return 0;
}
4. 类成员指针
- 成员指针:可以指向类的非静态成员的指针
- 类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态的指针与普通指针并无区别
- 成员指针的类型囊括了类的类型以及成员的类型
4.1 数据成员指针
- 成员指针的声明:
1.需指明当前声明的是指针
2.必须在*
之前添加类名
const string Screen::*pdata; //一个指向Screen类的从const string成员的指针
//声明成指向const string成员的指针意味着pdata可以指向任何Screen对象的成员
- 当我们初始化一个成员指针时,需指定它所指的成员:
pdata = &Screen::contents; //将取地址符作用于Screen类的成员而非内存中的一个该类对象
- 当我们初始化一个成员指针或为成员指针赋值时,该指针并未指向任何数据,只有解引用成员指针时才提供对象的信息(两种成员指针访问运算符):
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata; //.*和->*运算符
s = pScreen->*pdata;
- ==数据成员一般情况下是私有的,通常不能直接获得数据成员的指针,最好定义一个函数,令其返回值是指向该成员的指针:
class Screen {
public:
static const std::string Screen::*data()
{ return &Screen::contents; }
};
4.2 成员函数指针
- 定义指向类的成员函数的指针,使用auto自动判断指向成员函数的指针类型:
//pmf是一个指针,可以指向Screen的某个常量成员函数
auto pmf = &Screen::get_cursor;
- 如果成员存在重载的情况,则我们必须显示声明函数类型以明确指出我们想要使用的是哪个函数
- 在成员函数和指向该成员的指针之间不存在自动转换规则:
pmf = &Screen::get; //必须显示地使用取地址运算符
-
因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(parms)和(obj.*p)(args)
-
使用成员指针的类型别名:
using Action = char (Screen*)(Screen::pos, Screen::pos) const;
- 成员函数表,保存自身函数的指针,为private,组成一个数组
4.3 将成员函数用作可调用对象
- 因为成员指针不是可调用对象,所以我们不能直接将一个指向成员函数的指针传递给算法,必须使用.*或->*调用成员指针
- 从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
- 通过标准库功能mem_fn来让编译器负责推断成员类型;使用bind生成一个可调用对象
5. 嵌套类
- 一个类可以定义在另一个类的内部,前者称为嵌套类或者嵌套类型;嵌套类是一个独立的类,与外层类基本没有什么关系,外层类的对象与嵌套类的对象也是相互独立的
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见
- 嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部
- 嵌套类的所有成员定义都需要指明作用域
6. union
- 联合(union)是一种特殊的类,一个union可以有多个数据成员,但在任意时刻只有一个数据成员有值,分配给一个union对象的存储空间至少要容纳它的最大的数据成员
- union不能含有引用类型的成员
- 由于union既不能继承自其他类,也不能作为基类使用,所以在union中不能含有虚函数
- 使用一对花括号内的初始值显示的初始化一个union:
union Token {
char cval;
int ival;
double dval;
};
Token first_token = {'a'}; //初始化cval成员
- 匿名union不能包含保护的成员或私有成员,也不能定义成员函数
- 通常把含有类类型成员的union内嵌在另一个类当中,并将我们的union定义成匿名union,将自身类类型成员的控制权转移给该类
- 定义一个枚举类型(
判别式
)的成员来追踪其union成员的状态
7. 局部类
- 局部类:类定义在某个函数的内部,所有成员都必须完整定义在类的内部,也不允许申请静态数据成员
- 如果类定义在某个函数内部,则该函数的普通局部变量不能被该局部类所使用:
- 外层函数对局部类的私有成员没有任何访问特权
8. 固有的不可移植的特性
- c++从c语言继承而来的另外两种不可移植的特性:
位域
、volatile限定符
8.1 位域
- 当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域
- 位域的声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数:
typedef unsigned int Bit;
class File {
Bit mode : 2; //mode占2位
Bit modified: 1; //modified占1位
Bit prot_owner: 3;
//...
};
- 取地址运算符不能作用域位域,因此任何指针都无法指向类的位域
8.2 volatile限定符
- 当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile,volatile告诉编译器不该对这样的对象进行优化
- 只有volatile的成员函数才能被volatile的对象调用
volatile int v; //v是一个colatile int
int *ip = &v; //错误:必须使用指向volatile的指针
- 可以将形参类型指定为const volatile引用,这样就能利用任意类型的类对象进行拷贝或复值
8.3 链接指示:extern “C”
- c++使用
链接指示
指出任意非C++函数所用的语言 - 声明一个非C++函数:
ertern "C" size_t strlen(const char*);
- 花括号的作用是将适用于该链接指示的多个声明聚合在一起:
ertern "C" {
#include <string.h>
}
- 当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针有效:
ertern "C" void f1(void(*)(int));
- 如果希望传给c++函数传入一个指向C函数的指针,必须使用类型别名:
extern "C" typedef void FC(int);
void f2(FC *);
- C语言不支持重载,因此一个C链接指示只能说明一组重载函数中的一个