C++Primer_Chap19_特殊工具与技术_笔记

本文深入探讨C++中的内存管理技术,包括重载new和delete运算符,定位new表达式,以及如何使用标准库函数。此外,还介绍了运行时类型识别(RTTI)的概念,包括typeid和dynamic_cast运算符的使用,以及枚举类型的不同应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 控制内存分配

1.1 重载new和delete

    应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数。当编译器发现一条new表达式或delete表达式后,如果被操作的对象是类类型,则编译器首先在类及其基类的作用域中查找,不存在则在全局作用域中查找匹配的函数。可以使用作用域运算符令new表达式和delete表达式忽略定义在类中的函数:

::new
::delete

operator new接口和operator delete接口

  标准库定义了operator new函数和operator delete函数8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本不抛出异常:

void *operetor new(size_t);
void *operetor new[](size_t);
void *operetor delete(void *) noexcept;
void *operetor delete[](void *) noexcept;

void *operetor new(size_t, nothrow_t&) noexcept;
void *operetor new[](size_t, nothrow_t&) noexcept;
void *operetor delete(void *, nothrow_t&) noexcept;
void *operetor delete[](void *, nothrow_t&) noexcept;

  其中nothrow_t是定义在new头文件中的一个struct,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本。

  调用new表达式的过程如下:

  • 分配一块足够大的、原始的、未命名的内存空间
  • 运行相应的构造函数以构造这些对象,并传入初始值
  • 对象被分配了空间并构造完成,返回指针
#include <new>
int *intp = new (nothrow) int;
  • 我们可以将上述运算符函数定义为类的成员,默认隐式静态
  • delete和析构函数类型,不允许抛出异常

  如果想自定义operator new函数,可以为它提供额外的形参。但用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。但如下形参的函数不能被用户重载:

void *operator new(size_t, void*);

malloc函数和free函数

  继承自C语言,在C++中定义在cstdlib头文件中。

1.2定位new表达式

  与allocator不同,对于operator new分配的内存空间来说我们无法使用construct函数构造对象。相反,我们应该还使用new的定位new(placement new)形式构造对象。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表达式构造对象但不分配内存。

  调用析构函数会销毁对象,但不会释放内存。

2 运行时类型识别

  运行时类型识别(run-time type identification,RTTI)的功能由两个运算符实现:

  • typeid运算符,用于返回表达式的类型
  • dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用

  当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

  这两个运算符特别适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。

2.1 dynamic_cast运算符

dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
dynamic_cast<type&&>(e);

  type必须是一个类类型,且通常情况下该类型应该含有虚函数。e分别必须是:一个有效指针、左值、非左值。且e的类型必须符合以下三个条件中的任意一个:

  • e的类型是目标type的公有派生类
  • e的类型是目标type的公有基类
  • e的类型是目标type的类型

符合上述三个条件,则类型转换可以成功。否则转换失败,返回0.如果转换目标是引用类型且失败,抛出bad_cast异常。

指针类型的dynamic_cast

"
Base类至少含有一个虚函数,Derived是Base的公有派生类
"

if (Derived *dp = dynamic_cast<Derived *>(bp))
{
    //使用dp指向的Derived对象
}
else
{
    //bp指向一个Base对象
}

引用类型的dynamic_cast

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

2.2 typeid运算符

  typeid运算符(typeid operator)操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或type_info公有派生类型,定义在typeinfo头文件中。

  • 顶层const被忽略
  • 运算表达式是引用时,返回该引用已用对象的类型
  • 运算对象数组或函数,不会执行向指针的标准类型转换
  • 运算对象不属于类类型或一个不包含任何虚函数的类时,指示的为运算对象的静态类型
  • 运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才可得。
Derived *dp = new Derived;
Base *bp = dp;

if( typeid(*bp) == typeid(*dp) )
{
    "bp和dp指向同一类型对象"
}

if( typeid(*bp) == typeid(Derived) )
{
    "bp实际指向的类型对象"
}

 "检查永远失败,bp的类型是指向Base的指针"
if( typeid(bp) == typeid(Derived) )
{
   
}

2.3 使用RTTI

class Base {
    friend bool operator==(const Base&, const Base&);
public:
    //
protected:
    virtual bool equal(const Base&) const;
};

bool operator==(const Base &lhs, const Base &rhs)
{
    return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
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);
}

2.4type_info类

#include <typeinfo>
type_info的操作
t1 == t2 
t1 != t2 
t.name()返回一个C风格字符串,表示类型名字的可打印形式。类型名字的生成方式因系统而异
t1.before(t2)返回一个bool值,表示t1是否位于t2之前。顺序关系依赖编译器

  type_info类没有默认构造函数,不能定义、拷贝和赋值(相关函数和运算符定位为delete)。创建type_info对象的唯一途径是使用typeid运算符。name的返回值不一定是程序中使用的名字,对于name返回值的唯一要求是:类型不同返回的字符串必须有所区别。

3 枚举类型

  枚举属于字面值常量类型。C++包含两种枚举:限定作用域(关键字enum class或者enum struct)和不限定作用域(关键字enum)。

enum color {red, yellow, green};
enum stoplight {red, yellow, green};    //错误:重复定义了枚举成员
enum class peppers {red, yellow, green};    //正确

color eyes = green;    //正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green;    //错误:peppers的枚举成员不在有效的作用域中
                        //color::green在有效作用域中,但类型错误

color hair = color::red;
peppers p2 = peppers::red;

   只要enum有名字,就能定义并初始化该类型的成员。必须使用该类型的一个枚举成员或该类型的另一个对象:

open_modes om = 2;         "错误:2不属于类型open_modes"
om = open_modes::input;  

  一个不限定作用域的枚举类型的对象或枚举成员自动转换成整型,而限定作用域的不会进行隐式转换:

int i = color::red;    //正确
int j = peppers::red;    //错误

  在C++11标准中,可以加冒号和想使用的类型来指定成员类型:

enum intValues : unsigned long long {
    charTyp = 255, shortTyp = 65525, intTyp = 65535,
    longTyp = 4294967295UL,
    longlongTyp = balabala ULL
};

  如果没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int。对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型

枚举类型的前置声明

  在C++11标准中,可以提取声明enum。enum的前置声明(无论隐式韩式显式地)必须指定其成员大小(限定作用域的枚举类型可以使用默认类型参数int):

enum intValues : unsigned long long;
enum class open_modes;

形参匹配和枚举类型

  要初始化一个enum对象,必须使用该enum类型的另一个对象或者它的一个枚举成员。因此,即使某个整型值恰好与枚举成员的值相等,也不能作为enum实参使用。

enum Tokens { INLINE = 128, VIRTUAL = 129 };

void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;

newf(VIRTUAL);    //newf(int)
newf(uc);        //newf(unsigned char)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值