C++ 学习笔记之(19) new、delete表达式、RTTI(运行时类型识别)、枚举、类成员指针、嵌套类、局部类、位域、volatile、extern C
控制内存分配
重载new
和delete
new
表达式原理string *sp = new string("a value"); // 分配并初始化一个 string 对象 string *arr = new string[10]; // 分配 10 个默认初始化的 string 对象
- 调用标准库函数
operator new
或operator new[]
,该函数分配足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组) - 编译器运行相应的构造函数构造对象,并传入初始值
- 对象被分配空间并构造完成,返回一个指向该对象的指针
- 调用标准库函数
delete
表达式原理delete sp; // 销毁 *sp, 然后释放 sp 指向的内存空间 delete [] arr; // 销毁数组中的元素,然后释放对应的内存空间
- 对动态分配的对象或数组中的而元素执行相应的析构函数
- 编译器调用标准库函数
operator delete
(或operator delete[]
)释放内存空间
标准库定义
oeprator new
函数和operator delete
函数的8
个重载版本,前四个可能抛出bad_alloc
异常
- 自定义版本必须位于全局作用域或类作用域中
- 上述运算符函数定义为类的成员时,是隐式静态的。因为
new
用在对象构造之前,delete
用在对象销毁之后,故必须是静态的,且不能操纵类的任何数据成员 new
和new[]
的返回类型必须是void *
, 第一个形参类型必须是size_t
, 且该形参不能含有默认实参,但可提供额外形参。函数形式void *operator new(size_t, void*);
,不可被重载。delete
和delete[]
函数的返回类型必须是void
, 第一个形参类型必须是void*
。用指向待释放内存的指针来初始化void*
形参- 实际上自定义的是
opearot new
函数,并非new
表达式。new
表达式无法改变,自定义operator new
和operator delete
函数的目的在于改变内存分配的方式。
- 实际上自定义的是
定位new
表达式
operator new
函数分配的内存空间应该使用new
的 定位new(placement new)形式构造对象
place_address
为指针,initializers
为初始值列表,用于构造新分配的对象- 当只传入一个指针类型的实参时,定位
new
表达式构造对象但是不分配内存。定位new
表达式使用operator new(size_t, void*)
函数,该函数不分配内存,仅返回指针实参。然后由new
表达式负责在指定的地址初始化对象 - 传给
allocator
的construct
的指针必须指向同一个allocator
对象分配的孔家,但是传给定位new
的指针无需指向operator new
分配的内存,甚至不需要指向动态内存 - 与
allocator
的destroy
类似,调用析构函数会销毁对象,但是不会释放内存
运行时类型识别
运行时类型识别(run-time type identification RTTI)由连个运算符实现
typeid
运算符,用于返回表达式的类型dynamic_cast
运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用- 使用情况:使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。
- 使用
RTTI
时,最好定义虚函数而非直接操作类型管理
dynamic_cast
运算符
- dynamic_cast运算符使用形式(其中
type
必须是类类型, 且通常含有虚函数)
dynamic_cast<type*>(e)
:e
必须是一个有效的指针dynamic_cast<type&>(e)
:e
必须是一个左值dynamic_cast<type&&>(e)
:e
不能是左值
e
的类型必须符合以下三个条件中的任意一个,转换才能成功
e
的类型是目标type
的公有派生类e
的类型是目标type
的公有基类e
的类型就是目标type
的类型
dynamic_cast
转换失败情况
- 若转换目标是指针类型:失败结果为
0
- 若转换目标是引用类型:失败抛出
bad_cast
异常
- 若转换目标是指针类型:失败结果为
// 指针类型的 dynamic_cast
// bp 指针指向基类 Base(至少含有一个虚函数), dp 指针指向公有派生类 Derived
if(Derived *dp = dynamic_cast<Derived*>(bp))
{
// 使用 dp 指向的 Derived 对象
}else { // bp 指向一个 Base 对象
// 使用 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
为任意表达式或类型的名字, 结果为一个常量对象的引用, 该对象类型是type_info或其共有派生类型
- 若表达式是引用,则
typeid
返回该引用所引对象的类型 - 若表达式是数组或函数时,不会转换成指针。即得到的是数组类型,而非指针类型
- 若运算对象不属于类类型或是一个不包含任何虚函数的类时,结果为运算对象的静态类型
- 若运算对象是定义了至少一个虚函数的类的左值时,结果知道运行时才会求得
- 当作用于指针(而非指针所指的对象),结果是该指针的静态编译时类型
- 若表达式是引用,则
使用RTTI
class Base{
friend bool operator==(const Base&, const Base&);
protected:
virtual bool equal(const Base&) const;
};
class Derived: public Base{
protected:
bool equal(const Base&) const override;
};
bool operator==(const Base &lhs, const Base &rhs) {
// 如果 typeid 不相同,返回 false, 否则虚调用 equal
return typeid(lhs) == typeid(rhs) && lhs.equal((rhs));
}
bool Derived::equal(const Base &rhs) const {
// 对象类型相同,故转换过程不会抛出异常
auto r = dynamic_cast<const Derived&>(rhs);
// 执行比较两个 Derived 对象的操作并返回结果
}
bool Base::equal(const Base &rhs) const {
// 执行比较 Base 对象的操作
}
type_info
类
type_info
类没哟默认构造函数, 且拷贝和移动构造函数以及赋值运算符都被定义成删除的。故无法定义或拷贝type_info
类型的对象,也不能为其赋值。创建type_info
对象的唯一途径就是使用typeid
运算符type_info
类在不同的编译器上有所区别
枚举类型
枚举类型(enumeration):属于字面值常量类型,组合一组整型常量
C++包含两种枚举
- 限定作用域的枚举类型
enum class
或enum struct
。作用域外不可访问枚举成员 - 不限定作用域的枚举类型:省略掉关键字
class
或struct
, 名字可选。枚举成员与枚举类型本身的作用域相同
enum color {red, yellow, green}; // 不限定作用域的枚举类型 enum stoplight {red, yellow, green}; // 错误:重复定义枚举成员 enum class peppers {red, yellow, green}; // 正确:枚举成员被隐藏 color eyes = green; // 正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中 // 错误:peppers 的枚举成员不再有效的作用域中。color::green在有效的作用域中,但类型错误 peppers p = green; peppers p2 = peppers::red; // 正确:使用 peppers 的 red
- 限定作用域的枚举类型
枚举成员是常量表达式
限定作用域的枚举类型的对象或枚举成员自动地转换成整形。对于不限定作用域的枚举类型,其枚举成员不存在默认类型
int i = color:::red; // 正确:不限定作用域的枚举类型的枚举成员隐式地转换成 int int j = peppers::red; // 错误:限定作用域的枚举类型不会进行隐式转换
C++11可以提前声明
enum
,enum
的前置声明(无论隐式还是显示)必须指定其成员的大小enum intValues: unsigned long long; // 不限定作用域的,必须指定成员类型 enum class open_modes; // 限定作用域的枚举类型可使用默认成员类型 int
类成员指针
使用7.3.1
节的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;
static const std::string Screen::*data(){
return &Screen::contents;
}
private:
std::string contents;
pos cursor;
pos height, width;
};
数据成员指针
成员指针(pointer to member):可以指向类的非静态成员的指针。成员指针必须包含成员所属的类,故必须在*
之前添加classname::
, 以表示定义的指针可以指向classname
的成员
// pdata 可以指向一个常量(非常量)Screen 对象的 string 成员
const string Screen::*pdata = &Screen::contents;
auto pdata = &Screen::contents; // C++11可使用 auto 或 decltype
成员函数指针
成员函数指针即指向类的成员函数的指针, 最简单的方法是使用auto
来推断类型
// pmf 为指针,指向 Screen 某个常量成员函数, 前提是该函数不接受任何实参,且返回一个char
auto pmf = &Screen::get_cursor;
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get; // 指向含有两个形参的 get
成员函数和指向该成员的指针不存在自动转换规则
pmf = &Screen::get; // 必须显示地使用取地址运算符 pmf = Screen::get; // 错误:成员函数和指针之间不存在自动转换规则
使用
.*
或->*
运算符使用成员函数指针Screen myScreen, *pScreen = &myScreen; char c1 = (pScreen->*pmf)(); char c2 = (myScreen.*pmf2)(0, 0); // 通过对象将实参 0,0 传递给含有两个形参的 get 函数
使用类型别名或
typedef
using Action = char (Screen::*)(Screen::pos, Screen::pos) const; Action get = &Screen::get;
将成员函数用于可调用对象
若想使用成员函数指针进行函数调用,需要利用.*
运算符或->*
运算符将指针绑定到特定对象,故成员指针并不是可调用对象。故不可直接将成员函数指针传递给算法
auto fp = &string::empty; // fp 指向 string 的 empty 函数
find_if(svec.begin(), svec.end(), fp); // 错误:必须使用 .* 或 ->* 调用成员指针
// 使用 function 生成一个可调用对象
function<bool (const string&)> fcn = &string::empty; // empty 接受 string 参数,返回 bool
find_if(svec.begin(), svec.end(), fcn);
// 使用 mem_fn 生成一个可调用对象, mem_fn 可根据成员指针的类型推断可调用对象类型
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
auto f = mem_fn(&string::empty); // f 接受一个 string 或 一个 string*
f(*svec.begin()); // 正确:传入 string 对象,f 使用 .* 调用 empty
f(&svec[0]); // 正确:传入 string 指针,f 使用 ->* 调用 empty
// 使用 bind 生成一个可调用对象, 但必须将函数中用于表示执行对象的隐式形参转换成显示的
// 断则范围中的每个 string, 并将其 bind 到 empty 的第一个隐式实参上
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
f(*svec.begin()); //正确:实参是 string, f 使用 .* 调用 empty
f(&svec[0]); // 正确:实参是 string 指针, f 使用 ->* 调用 empty
嵌套类
嵌套类(nested class)或 嵌套类型(nested type):定义在一个类内部的类
嵌套类对象与外层类对象相互独立,且成员相互独立
嵌套类名字在外层类作用域可见,但在外层类作用域外不可见
嵌套类的访问权限由外层类访问控制符决定,相当于外层类成员
声明和定义嵌套类
class TextQuery{ public: class QueryResult; }; class TextQuery::QueryResult{ /* ... */ };
union
:一种节省空间的类
联合(union)是一种特殊的类,可有多个数据成员,但在任意时刻只有一个数据成员有值(其他成员未定义状态),分配给一个union
对象的存储空间至少要容纳它的最大数据成员
union
不能含有引用类型成员- C++11中,含有构造函数或析构函数的类类型可以作为
union
的成员类型 union
可以为成员指定public
、protected
和private
等保护标记,默认是公有的union
可定义成员函数(包括构造函数和析构函数), 但不能继承其他类,也不能做基类,故不能含有虚函数
union Token{
char cval;
int ival;
};
Token first_token = {'a'}; // 初始化 cval 成员
Token last_token; // 未初始化的 Token 对象
Token *pt = new Token; // 指向一个未初始化的 Token 对象的指针
// 可使用成员访问运算符访问 union 对象的成员
last_token.cval = 'z';
pt->ival = 42;
匿名union(anonymous union) :未命名
union
,编译器自动为该union
创建一个未命名的对象. 在匿名union
的定义所在的作用域内,该union
成员可直接访问union{ char cval; int ival; }; cval = 'c'; //
匿名
union
不能包含受保护的成员或私有成员,也不能定义成员函数如果
union
含有类类型成员, 当将类类型成员的值修改为其他值时,需要分别运行构造或析构函数。
局部类
局部类(local class):定义在函数内部的类,仅可在定义它的作用域内可见,不允许声明静态数据成员
- 局部类仅能访问外层作用域定义的类型名、静态变量及枚举成员。函数的普通局部变量不能使用
int a, val;
void foo(int val)
{
static int si;
enum Loc { a = 1024, b};
struct Bar{ // Bar 是 foo 的局部类
Loc locVal; // 正确:使用一个局部类型名
int barVal;
void fooBar(Loc l = a) // 正确:默认实参是 Loc::a
{
barVal = val; // 错误:val 是 foo 的局部变量
barVal = ::val; // 正确:使用一个全局对象
barVal = si; // 正确:使用一个静态局部对象
locVal = b; // 正确:使用一个枚举成员
}
};
}
固有的不可移植的特性
不可移植(nonportable):即指因机器而异的特性。
位域
类可以将其费静态数据成员定义成 位域(bit-field), 一个位域含有一定数量的二进制位,当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域
- 位域在内存中的布局是与机器相关的
- 位域的类型必须是整形或枚举类型, 通常使用无符号类型保存位域
class File{
unsigned int mode: 2; // mode 占 2 位, 范围为 [0, 3]
unsigned int modified: 1; // modified 占 1 位
}
volatile
限定符
volatile表明编译器不应对这样的对象进行优化
- 不能使用合成的拷贝/移动构造函数及赋值运算符初始化
volatile
对象或从volatile
对象赋值
volatile int dr; // 该 int 值可能发生变化
volatile Task *curr_task; // curr_task 指向一个 volatile Task 对象
volatile int iax[max_size]; // iax 的每个元素都是 volatile
int *volatile vip; // vip 是一个 volatile 指针,指向 int
volatile int *ivp; // ivp 是一个指针, 指向一个 volatile int
volatile it *volatile vivp; // vivp 是一个 volatile 指针,指向一个 volatile int
volatile int v; // v 是一个 volatile int
int *ip = &v; // 错误:必须使用指向 volatile 的指针
链接指示:extern "C"
C++ 使用 链接指示(linkage directive)指出任意非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"{ #include <string.h> // 操作 C 风格字符串的 C 函数 }
指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示
void (*pf1)(int); // 指向一个 C++ 函数 extern "C" void (*pf2)(int); // pf2 指向一个 C 函数, 该函数接受一个 int 返回 void pf1 = pf2; // 错误:pf1 和 pf2 类型不同
链接指示不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。因为链接指示同时作用于声明语句中的所有函数,故若希望给C++函数传入一个指向 C 函数额指针,则必须使用类型别名
extern "C" void f1(void(*)(int)); // f1 是一个 C 函数,形参是一个指向 C 函数的指针 // FC 是一个指向 C 函数的指针 extern "C" typedef void FC(int); // f2 是一个 C++ 函数,该函数的形参是指向 C 函数的指针 void f2(FC *);
通过链接指示对函数进行定义,可令 C++ 函数在其他语言程序中可用
// calc 函数可以被 C 程序调用 extern "C" double cal(double dparm) { /* ... */ }
通过预处理器定义,同时编译
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 函数的重载
结语
- 有的程序需要精确控制内存分配,可通过在类的内部或全局作用中自定义
operator new
和operator delete
实现。自定义后,优先使用自定义版本 - 有的程序需要运行时直接获取对象的动态类型,运行时类型识别
RTTI
提供了语言级别的支持,RTTI
只对定义了虚函数的类有效,对没有定义虚函数的类,只得到其静态类型 - 成员指针类型中包含了该指针所指成员所属类的类型信息,解引用成员指针时,应提供类对象
- C++ 定义了几种聚集类型
- 嵌套类,定义在其他类的作用域中,嵌套类通常作为外层类的实现类
union
,一种特殊类,可定义几个数据成员但在任意时刻只有一个成员有值,union
通常嵌套在其他类内部- 局部类,定义在函数内部,局部类的所有成员都定义在类内。局部类不能含有静态数据成员
- C++支持几种固有的不可移植的特性, 其中 位域和
volatile
使程序更容易让问硬件。 链接指示使得程序更容易访问其他语言编写的代码