C++ 学习笔记(19)new/delete表达式、定位new、typeid、dynamic_cast、type_info、枚举类型、成员函数指针、union、位域、volatile限定符、链接指示
参考书籍:《C++ Primer 5th》
C++ 学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator
19.1 控制内存分配
19.1.1 重载new和delete
- new表达式步骤:
- 调用
operator new
或operator new[]
的标准库函数,分配了一块足够大的未命名内存空间。 - 运行相应的构造函数,并传入初始值。
- 返回一个指向该对象的指针。
- 调用
- delete表达式步骤:
- 运行相应的析构函数。
- 调用
operator delete
或operator delete[]
的标准库函数,释放内存空间。
-
重载带有noexcept说明符运算符时,同样需要加上noexcept。
-
自定义版本必须位于全局作用域或类作用域。
- 作为类的成员时,是隐式静态的,因为new是在对象创建前使用,delete是在对象销毁以后使用,且都不能操纵类的任何数据成员。
-
new的返回类型必须是
void *
,第一个形参类型必须是size_t
,且不能有默认实参(分配对象所需字节数)。可以提供额外形参,除了一个函数用户不能重载:void *operator new(size_t, void*);
-
delete的返回类型必须是
void
,第一个形参必须是void*
。用指向待释放内存的指针,来初始化void*
形参。 -
nothrow_t:空结构体,用来表明函数是否抛出异常。
-
实际上并没有重载new表达式delete表达式,只是改变内存分配方式。因为无法自定义new表达式或delete表达式的行为。
-
malloc函数:
- 参数
size_t
:分配字节数。 - 返回
void*
:分配了空间的指针。失败返回0。
- 参数
-
free函数:
- 参数
void*
:是malloc返回的指针的副本,将其内存释放。
- 参数
19.1.2 定位new表达式
- place_address 必须是一个指针。
- 定位new调用
operator new(size_t, void *)
:函数不分配内存,只是返回指针实参;再由new表达式负责初始化对象。 - 不同于allocator的construct,定位new表达式的指针不需要指向动态内存。
- 和allocate的destroy类似,调用析构函数可以清除对象,但不会释放对象所在空间。
19.2 运行时类型识别
- 运行时类型识别(run-time type identification, RTTI):
- typeid运算符,用于返回表达式的类型。
- dynamic_cast运算符,用于基于类的指针或引用安全地转换成派生类的指针或引用。
- 使用RRTI时,最好定义虚函数而非直接管理类型。
19.2.1 dynamic_cast运算符
- 有三种形式(type为类类型,通常含有虚函数):
dynamic_cast<type*>(e)
:e必须为有效的指针。dynamic_cast<type&>(e)
:e必须为一个左值。dynamic_cast<type&&>(e)
:e不能是左值。
- e的类型必须符合以下三个条件中任意一个:
- 是目标type的公有派生类。
- 是目标type的公有基类。
- 是目标type的类型。
- 转换失败时:
- 目标是指针类型,返回0。
- 目标是引用类型,抛出bad_cast异常。
// 指针类型
// bp指针指向基类Base(至少含有一个虚函数),Derived是Base的公有派生类。
if (Derived *dp = dynamic_cast<Derived*>(bp))
{
// 转换成功。dp指向Derived对象。
}
else
{
// 转换失败。使用dp指向的Base对象
}
// ----------------------------------------------------------------------------- //
// 引用类型
// 因为不存在空引用,对于引用失败,应该用捕获异常的方法。
void f(const Base &b)
{
try {
// 使用b引用的Derived对象。
const Derived *d = dynamic_cast<Derived&>(b);
} catch (bad_cast) {
// 转换失败处理。
}
}
19.2.2 typeid运算符
typeid(e)
:e是任意表达式或类名。返回结果是type_info类型(或派生类)常量对象的引用。- e表达式如果是引用,则typeid返回该对象的类型。
- e表达式如果是数组或函数,不会转成指针,而是原来的类型。
- typeid作用指针时,返回的结果是该指针的静态编译时类型。
- 只有当类型含有虚函数时,编译器才会对表达式求值。
- 不含虚函数时,返回表达式的静态类型。
class A {}; // 非多态
class B : public A {};
class A2 { virtual void foo() {}; }; // 多态
class B2 : public A2 {};
void main()
{
B *b = new B;
A *a = b;
if (typeid(*a) == typeid(*b)) { cout << "test one ok!" << endl; } // 未执行
if (typeid(*a) == typeid(B)) { cout << "test two ok!" << endl; } // 未执行
if (typeid(a) == typeid(B)) { cout << "test three ok!" << endl; } // 未执行
B2 *b2 = new B2;
A2 *a2 = b2;
if (typeid(*a2) == typeid(*b2)) { cout << "test four ok!" << endl; } // 执行,类型相同
if (typeid(*a2) == typeid(B2)) { cout << "test five ok!" << endl; } // 执行,类型相同
if (typeid(a2) == typeid(B2)) { cout << "test six ok!" << endl; } // 未执行,比较的是:A2*和B2类
}
19.2.3 使用RTTI
class Base
{
friend bool operator==(const Base&, const Base&); // 整个类(包括继承)的相等运算符
protected:
virtual bool equal(const Base&) const; // 每个类自己定义的euqal函数
};
class Derived : public Base
{
protected:
bool equal(const Base&) const;
};
// ==运算符的比较
bool operator==(const Base &lhs, const Base &rhs)
{
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs); // 先判断类型是否相同,再调用自定义的相等判断
}
// 基类自定义判断
bool Base::equal(const Base &rhs) const
{
// ...自定义的相等判断
}
// 继承类自定义判断
bool Derived::equal(const Base &rhs) const
{
auto r = dynamic_cast<const Derived&>(rhs); // 继承类转基类,不会抛出异常
if (!Base::equal(rhs)) return false; // 可调用基类判断,先处理基类的成员判断
// ...其他自定义的相等判断
return true;
}
19.2.4 type_info 类
- type_info类没有默认构造函数,也没有拷贝移动等操作。因此无法定义或拷贝或赋值。唯一创建方法是使用typeid运算符。
- 在不同编译器上,type_info类有所区别。
19.3 枚举类型
- 枚举属于字面值常量类型。
- 限定作用域的枚举类型:包含关键字class或struct,枚举类型名,枚举成员列表。
- 不限定作用域的枚举类型:省略关键字class或struct,枚举类型的名字是可选的。
// 不限定作用域的枚举类型
enum color { red, green };
enum stoplight { red, green }; // 错误。重复定义枚举成员
enum class peppers { red, green }; // 正确。限定作用域的枚举类型,隐藏了枚举成员
color eyes = green; // 正确,不限定作用域的值
peppers p = green; // 错误,color枚举类型不能赋给peppers
peppers p2 = peppers::red; // 正确,指定作用域
int i = color::red; // 正确,不限定作用域的枚举可隐式转换成int
int j = peppers::red; // 错误,限定作用域的不会隐式转换
int k = (int)peppers::red; // 可以强制转换
- 默认情况下:
- 限定作用域的enum成员类型是int。
- 不限定作用域的enum成员不存在默认类型。所以在前置声明时必须指定成员大小。
// 不限定作用域的枚举类型,必须指定成员类型
enum intValues : usigened long long;
// 限定作用域,默认int
enum class open_modes;
19.4 类成员指针
- 成员指针:可以指向类的非静态成员的指针。不同于普通指针,成员指针必须包含成员所属的类,即在
*
前添加类名calssname::
。
19.4.2 成员函数指针
class Screen
{
public:
Screen & up();
Screen& down();
// Action是别名,Screen的成员指针,可指向Screen成员的一类函数。
using Action = Screen & (Screen::*)();
enum Directions { UP, DOWN }; // 用来控制输入的枚举类型
Screen& move(Directions); // 提供给外部调用的接口
private:
static Action Menu[]; // 函数列表,通过索引调用对应函数
};
Screen::Action Screen::Menu[] = { &Screen::up, &Screen::down }; // 0:up 1:down
Screen& Screen::move(Directions cm) { return (this->*Menu[cm])(); } // 通过函数表直接调用对应函数
void main
{
Screen s;
s.move(Screen::DOWN); // 等价 s.down();
}
19.4.3 将成员函数用作可调用对象
- mem_fun函数:可以根据成员指针的类型推断可调用对象的类型。
auto fp = &string::empty; // 一般获取的成员函数都是指针类型,不能作为对象
find_if(sv.begin(), sv.end(), fp); // 错误。fp是成员指针,需要调用 ->*
// 使用function转换成对象
function<bool (const string&)> fcn = &string::empty;
find_if(sv.begin(), sv.end(), fcn); // 正确。fcn是函数对象
// 使用mem_fn
find_if(sv.begin(), sv.end(), mem_fn(&string::empty)); // 正确,直接生成可调用对象
auto f = mem_fn(&string::empty); // f可接受对象或对象的指针
f(*svec.begin()); // 正确,传入对象,f调用.*
f(&svec[0]); // 正确,传入指针,f调用->*
// 使用bind,必须将函数指向对象的隐式形参转换成显式的。
auto fb = bind(&string::empty, _1);
fb(*svec.begin()); // 正确
fb(&svec[0]); // 正确
19.6 union:一种节省空间的类
-
union可以有多个数据成员,但在任意时刻只有一个数据成员有值(其他成员变成未定义状态)。分配union对象的存储空间至少能存储其最大的数据成员。
-
不能含有引用类型的成员。
-
默认情况下,成员都是公有的。
-
不能继承其他类或作为基类。
-
匿名union:未命名的union。定义了匿名union,编译器就自动为该union创建一个未命名对象。
- 定义所在作用域内,该union的成员都是可以直接访问的。
- 不能包含proteced或private成员,也不能定义成员函数。
union
{
char cval;
int ival;
};
cval = 'c'; // union对象的成员,此时ival是未定义状态
ival = 2; // 赋值了另一个成员,此时cval是未定义状态
- union只包含内置类型的成员时,编译器依次合成默认构造函数或拷贝控制成员。
- 如果含有类类型成员,且需要修改类类型成员时,就必须运行构造或析构函数。如果定义了自定义默认构造函数或拷贝成员,合成版本的会被声明为删除的。
19.7 局部类
- 如果局部类定义在某个函数内部,局部类无法使用函数内部的局部变量。
19.8 固有的不可移植的特性
- 不可移植(nonportable):因机器而异,通常需要重新编写。
19.8.1 位域
- 类的非静态数据成员可以定义成位域(bit-field)。
- 位域在内存中的布局与机器有关。
- 指针无法指向类的位域。
- 最好将位域设为无符号类型。
struct S
{
unsigned int b : 3; // 三位无符号位域,值的范围在[0,7]
};
void main()
{
S s = { 7 };
cout << s.b << endl; // 输出:7
++s.b; // 无符号上溢(保证回卷)
cout << s.b << endl; // 输出:0
}
19.8.2 volatile限定符
- 确切含义与机器有关,通常用于直接处理硬件的数据,值不直接由程序控制,volatile告诉编译器不应对对象做优化。
- 不能使用合成拷贝移动操作volatile对象。
volatile int v;
int *vloatile vip; // vip:volatile指针,指向int
vloatile int *ivp; // ivp:是一个指针,指向vloatile int
vloatile int *vloatile vivp; // vivp:volatile指针,指向vloatile int
int *ip = &v; // 错误
ivp = &v; // 正确
vivp = &v; // 正确
19.8.3 链接指示:extern “C”
- 链接指示(linkage directive):指出任意非C++函数所用的语言。
// 单语句链接指示:声明使用C语言版本的strlen函数
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函数
}
void (*pf1)(int); // pf1指向一个C++函数
extern "C" void (*pf2)(int); // pf2指向一个C函数
pf1 = pf2; // 错误,C和C++是不同类型
- 链接指示不仅对函数有效,而且对返回类型或形参类型的函数指针也有效。
// 在同时编译C和C++时,做判断以正确编译
#ifdef __cplusplus
extern "C"
#endif
- C语言不支持重载,所以不可以链接指示相同函数名。
extern "C" void print(const char *);
extern "C" void print(int); // 错误,相同函数名
extern "C" double calc(double);
extern int clac(int); // 正确,C++与C函数的重载。