C++primer薄片系列之特殊工具与技术

new 和 delete

class A
{
public:
    A()
    {
    }
    ~A()
    {
    }
};
A *s =new A();

new 的具体操作过程分三步
1. 调用标准库函数operator new申请足够大的,原始的,未命名的内存空间
2. 编译器使用A的构造函数构造对象,并为其传入初始值
3. 返回构造完成后的对象的指针
delete 的具体操作分两步
1. 执行A的析构函数
2. 调用标准库函数operator delete释放空间

标准库中定义了operator new 和operator delete 函数总共8个版本
其中std::nothrow_t 是定义在new中的一个空struct. new中还定义了一个名为std::nothrow的const对象。用户可以用它请求new的不抛出异常的版本。

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, std::nothrow_t&)noexcept;
void * operator new[](size_t, std::nothrow_t&)noexcept;
void operator delete(void *,std::nothrow_t&)noexcept;
void operator delete[](void *, std::nothrow_t&)noexcept;

用户可以自定义上面版本的任意一个。前提是自定义版本必须位于全局作用域或者类作用域。当将上述运算符定义成类的成员时,它们是隐式静态的。


class A
{
public:
    void * operator new(size_t s)
    {
        std::cout << "2222222 " << s << std::endl;
        void * t;
        return t;//这是个bug,因为我们没有分配内存
    }
    A()
    {
        std::cout << "111111" << std::endl;
        s = new char(5);//这里会报segment fault.因为我这里会在operator new 返回的内存空间里中构造对象。但是我们的operator new返回一个空的void* 
    }

    char *s;
};
int main()
{
    A *s = new A();//这里先调用operator new, operator new  会优先使用A类下面的operator new 成员函数,
}

operator new

用户可以自定义自己的operator new 运算符。

new本身可以被传递额外的参数

我们可以定义自己的new版本,让其包含其他类型的参数,如int(尽管这个int 没啥用)

class A
{
public:
    void * operator new(size_t s, int m)//operator new运算符号需要有额外的参数int 
    {
        std::cout << "2222222 " << s << std::endl;
        void * t;
        return t;
    }
    A()
    {
        std::cout << "111111" << std::endl;
        s = new char(5);
    }

    char *s;
};
int main()
{
    A *s = new(1) A();// 这里我们在new后面增加了一个(1),它意味着我们向operator new运算符号需要有额外的参数int 
}
class A
{
public:
    void * operator new(size_t s, int m,int n)//operator new运算符号需要两个额外的参数int
    {
        std::malloc
        return t;
    }
    A()
    {
        std::cout << "111111" << std::endl;
        s = new char(5);
    }

    char *s;
};
int main()
{
    A *s = new(1,2) A();// 这里我们在new后面增加了一个(1,2),它意味着我们向operator new运算符号需要有两个额外的参数int 
}

两点注意
一、关于operator new

C++ 规定不准用户重载

void *operator new(size_t, void*);

这个只能标准库自己使用。

二、关于operator delete

operator delete或者operator delete[]定义类的成员时候,可以包含另外一个类型为size_t 的参数,此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有个虚析构函数,则传递给operator delete的字节数将因待删除指针所指向的动态类型不同而有所区别。实际运行的operator delete函数版本也由对象类型决定。

malloc 和 free

当定义自己的operator new的时候,必须调用这两个函数来执行内存申请和释放的操作

class A
{
public:
    void * operator new(size_t s, int m,int n)
    {
        if(void* mem = malloc(s))
        {
            return mem;
        }
        else
        {
            throw std::bad_alloc();
        }
    }
    void operator delete(void *s)noexcept
    {
        free(s);
    }
    A()
    {
    }
    ~A()
    {
    }
};
int main()
{
    A *s = new(1,2) A();
}

Placement New(定位new)

placement new 是 operator new 的重载版本。
我们 可以在调用new 的时候,可以给 new 传递一个额外的参数,如上个例子中的int等。但是operator new 不允许我们重载new的带有指针类型的参数。
但是我们可以以直接调用
type A = new(ptr) A()
当我们这样的形式调用new的时候,编译器就会按照特定的规则执行new 操作。也就是placement new.
placement new 的形式如下:
当通过一个地址值调用new时,placement new依靠下面这个特定的new版本”分配内存”

void *operator new(size_t, void* p)
{
    return p;//事实上它只是简单的返回给定的指针
}
class A
{
public:


    A()
    {

    }
    ~A()
    {
    }
    std::string s;
};

int main()
{

   void *s;
   A *a  = new (s)A();//程序将在s指针指向的地址上构造A的对象
   //不过这个程序有bug,因为首先s 是个空指针,然后试图在这个空指针指向的内存中创建对象。
}

标准库有个allocator 类,它的成员函数allocate/deallocate和operator new/delete功能类似,都是分配空间,但是不构造对象. allocator类通过调用construct构造对象,这一点和 placement new相似,两者都是构造对象的,但是转给construct 的指针必须指向同一个allocator 分配的空间。传给placement new 的指针则没有限制,只要是个地址就可以了。

我们可以利用placement new 实现对一段地址的反复利用

class A
{
public:
    A()
    {
        std::cout << "1111" << std::endl;
    }
    void test()
    {
        std::cout << "@@@@@@@ " << a << std::endl;
    }
    void set()
    {
        a = 10;
    }
    ~A()
    {
        std::cout << "22222" << std::endl;
    }
private:
    int a;
};
int main()
{
    char a[sizeof(A)];//方式1:栈上分配
    //char *a = new char(sizeof A);//方式2: 堆上分配
    //int *ptr= new int(4);
    //void *a =  reinterpret_cast<void *>(ptr);//方式3:重新解释 
    A * m = new (a) A();
    m->set();
    m->test();
    m->~A();//这里只会调用析构函数体,却不会对内存进行释放,因为释放是 调用operator delete操作符来实现的
    A *n = new (m) A();
    n->test();
}

类型转换

static_cast的用法

1.基本类型的转换

double f =3.0;
int a = static_cast<int>(f);

2.任意类型的表达式转成void*类型
3. void 指针转换为目标类型

    void  * s = static_cast<void*>(new B());
    B * m = static_cast<B*>(s);

4.进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的

class A
{

};
class B : public  A
{
public:
    int *m;
};
int main()
{
    A* s = new A();
    B* m = static_cast<B*>(s);//下行转换,这里事实上是错误的。但是编译器不会报错。运行时也不会报错,但是如果用了这个指针去访问B的成员,会报错,也就是不是安全的
    std::cout << *m->m << std::endl;//这里访问了一个不存在的变量,程序崩溃

}

dynamic_cast的用法

dynamic 运算符的使用形式如下:

dynamic_cast < type-id > ( expression )

type-id 必须是个类的指针或者引用,或者void*
dynamic 可以用于类的上行或者下行转换。
上行转换
派生类向基类转换

class A
{
};
class B : public  A
{
public:
    int *m;

};
int main()
{
    B* s = new B();
    A*n = dynamic_cast<A*>(s);//这里用static_cast替换掉dynamic_cast,也是同样的
}

下行转换
基类向派生类转换。
这里要求基类类型含有虚函数。

class A
{
public:
    virtual void test()//必须至少有一个成员函数是虚函数
    {}
};
class B : public  A
{
public:
    int *m;

};
int main()
{
    A* s = new A();
    B* m = dynamic_cast<B*>(s);//这里转换时会进行运行时的检测,如果转换失败,会返回空指针
    if(m)
        std::cout << *m->m << std::endl;//成功转换的时候,我们才能安全的利用该指针访问其成员函数
    else
        std::cout << "1111111" << std::endl;
    void *kk = dynamic_cast<void*>(s);//转换为void *的用法和 static_cast类似 
}

更安全的写法

    A* s = new A();
    if(B* m = dynamic_cast<B*>(s))//这样写的好处保证我们在if语句外不会访问到m变量
        std::cout << *m->m << std::endl;
    else
        std::cout << "1111111" << std::endl;

dynamic_cast 可以转换引用,但是不存在所谓的空引用 ,因此引用类型无法使用与指针类型完全相同的错误报告策略。判断引用转换成功的方式是捕获std::bad_cast异常

    A* s = new A();
    try
    {
        const B& t = dynamic_cast<const B&>(*s);
    }
    catch (std::bad_cast e)
    {
        std::cout << e.what() << std::endl;
    }

typeid

使用方式 typeid(e), e 可以使表达式或者类型的名字
返回的结果是常量对象的引用,该对象标准库类型是type_info或者type_info 的共有派生类型
当e不属于类类型或者是个 不包含任何虚函数的类时,typeid指示的是运算对象的静态类型

    A* s = new A();
    B *s2 = new B();
    std::cout << (typeid(s) == typeid(A)) << std::endl;//False, s是个指针,因此typeid的结果是个s的静态类型,s的静态类型是A*
    std::cout << (typeid(s) == typeid(A*)) << std::endl;//True

typeid 对一个空的指针进行解引用求值时,会抛出std::bad_typeid的错误

A* ret()
{
    return nullptr;
}
int main()
{
    try
    {
        typeid(*ret());
    }
    catch (std::bad_typeid e)
    {
        std::cout << e.what() << std::endl;
    }
}

应用
判断两个对象是否相等的方法是对象的类型和成员取值是否一致
当派生类对象需要判断相等的方法时,普通做法是在基类定义一个虚函数,同时让派生类各自实现各自的相等的方法。
这个方法通常难以奏效,因为虚函数要求派生类和基类的形参是一致的

class A
{
public:
    virtual void equal(A&){
    }
};
class B:public A
{
public:
    void equal(A&)override{
   
   //B的test方法必须和A的完全一致,才能实现覆盖
    }
}

这样继承类的equal方法只能比较基类成员,而不能比较派生类独有的成员。
这时可以采用 运行时类型识别的方法来解决

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

    }
};
class B : public A
{
protected:
    virtual bool equal(const A&s) const  override
    {
        auto r = dynamic_cast<const B&>(s);//我们确定了两个类型是一致的,所以不会抛出异常
        //继续比较派生类成员
    }
};
bool operator==(const A&s1 , const A&s2)
{
    return typeid(s1) == typeid(s2) && s1.equal(s2);//s1和s2类型一致,则进入虚函数equal,不一致则直接返回false
}

枚举

C++11引入了限定作用域的枚举类型

enum A{ m,n,o};//不限定作用域的枚举
enum struct B{m,n,o};//限定作用域的枚举
enum class C{m,n,o}//限定作用域的枚举
A a = m;//正确
B m2 = m;//错误,不限定作用域的枚举值不能初始化限定作用域的枚举类型
B m3 = B::m;//正确
C m2 = B::m;//错误,二者类型不一致
std::cout << m << std::endl;//可以直接输出,输出0
std::cout << B::m << std::endl;//编译错误
std::cout << int(B::m) << std::endl;//正确,限定作用域枚举类型必须显示转化,输出0

默认情况下,限定作用域的enum成员类型是int,可以将其指定为其它整型类型

enum A: unsigned long long
{
    m = 18446744073709551615ULL
}

类成员指针

成员指针是类的非静态成员的指针。
数据成员指针

class A
{
public:
    A() :data("sds") {}
    const std::string data;
    static const std::string A::* data()//返回成员指针
    {
        return &Screen::data;//成员指针指向A类的成员,而非实际数据。使用成员指针,必须把它绑定到A类类型的对象上
    }
};
int main()
{
    const std::string A::*pdata;//指向A类的const string 成员的指针
    pdata = &A::data;//该指针没有指向任何数据
    std::cout << pdata << std::endl;//!!!输出1
    A s1, *s2 = &s1;
    std::cout << s2->*pdata << std::endl;//pdata的使用方式,首先解引用成员指针,获得所需的成员,再用成员访问运算符获取成员
}

成员函数指针

class A
{
public:
    A() :data("sds") {}
    const std::string data;
    void test(int tt)
    {
        std::cout << data << std::endl;
    }
};
int main()
{
    A s1, *s2 = &s1;
    void (A::*func)(int);//声明成员函数指针
    func = &A::test;
    std::cout << func << std::endl; //输出1 
    (s1.*func)(0);//类似数据成员指针,必须绑定对象使用
    using MM = void (A::*)(int);//可以使用别名
    MM func2 = &A::test;
    (s1.*func2)(1);
}

将成员函数用作可调用对象

int main()
{
    std::vector<std::string> s;
    auto fp = &std::string::empty;//使用string的成员函数empty的指针
    std::find_if(s.begin(), s.end(), fp);//错误,我们没有传入对象,成员指针只能使用.* 或者->*来调用
}

使用function 生成一个可调用对象

#include<functional>
int main()
{
    std::vector<std::string> s;
    std::function<bool(const std::string&)> fcn = &std::string::empty;
    std::find_if(s.begin(), s.end(), fcn);
}

使用mem_fn生成一个可调用对象

find_if(s.begin(),s.end(), mem_fn(&string::empty));
//std::mem_fn在头文件functional中,std::mem_fn 可以根据成员函数指针的类型推断可调用对象的类型,而无需用户显示指定      

使用bind生成一个可调用对象

find_if(s.begin(), s.end(), std::bind(&std::string::empty, std::placeholders::_1));

union

union 不能包含引用类型的成员。一个union任何时候只有一个数据成员有值,分配给union的存储空间至少容纳它的最大数据成员。默认情况下,union是public的。也可以指定某些成员为private, protected。
匿名union

union{
    char a;
    int m;
};
a = 'c';//为union对象赋一个新值
m = 10;//该对象当前为10

C++11 允许union包含类类型的成员
如果类类型成员定义了自己的构造函数,析构函数,则union必须也定义构造函数和析构函数。如果类类型成员没有 定义的话,则union也不需要定义

class A
{
public:
    A()
    {
        std::cout << "A construct" << std::endl;
    }
    ~A()
    {
        std::cout << "A destruct" << std::endl;
    }
};
union K{
    K()
    {}//A定义了构造函数,所以K 必须定义
    ~K()
    {}//A定义了析构函数,所以K 必须定义
    char a;
    int m;
    A h;
};
int main()
{
    K q;
    q.a = 'c';//为union对象赋一个新值
    q.m = 10;//该对象当前为10
    q.h = A();

}

通常可以将union以匿名的方式放在类里面作为类成员,由类负责构造或者析构

class A
{

public:
    A() :tag(INT),a(0)
    {
    }
    ~A()
    {
        if (tag == STR)
        {
            c.std::string::~string();//需要 显示释放std::string 成员,union只会析构基本类型,std::string内置的char指针需要单独释放
        }
    }
    A&operator=(A& s)
    {
        if (tag == STR && s.tag != STR) c.std::string::~string();
        if (tag == STR && s.tag == STR)
            c = s.c;
        else
            copyToUnion(s);
        tag = s.tag;
        return *this;
    }
    A &operator=(const std::string &g)
    {
        if (tag == STR)
        {
            c = g;
        }
        else
            new(&c) std::string(g);//!!!原位赋值
        tag = STR;
        return *this;
    }
    A &operator=(const int &g)
    {
        if (tag == STR)
        {
            c.std::string::~string();
        }
        tag = INT;
        a = g;
        return *this;
    }
    A &operator=(const char &g)
    {
        if (tag == STR)
        {
            c.std::string::~string();
        }
        tag = CHAR;
        b = g;
        return *this;
    }
    void test()
    {
        std::cout << &a << std::endl;//返回首地址
        std::cout << &b << std::endl;//返回首地址
        std::cout << &c << std::endl;
    }
private:
    enum { INT, CHAR, STR } tag;//用来指明union中存在的成员类型
    union
    {
        int a;
        char b;
        std::string c;
    };

    void copyToUnion(const A&s)
    {
        switch (s.tag)
        {
        case INT: a = s.a; break;
        case CHAR:b = s.b; break;
        case STR:
            new(&c) std::string(s.c); break;
        default:
            break;
        }
    }
};

局部类

定义在函数的内部的类,被称为局部类
局部类不允许声明静态数据成员
局部类只能访问外层作用域定义的类型名,静态变量和枚举成员

int val = 10;
void foo(int val)
{
    static int ai = 5;
    enum Loc{ a =1024, b};
    struct Bar
    {
        Loc l;// 使用一个局部类型名
        int barvar;
        void fooBar(Loc l = a)
        {
            barvar = val;//错误,不能访问函数作用域的普通变量
            barvar = b;//使用枚举成员
            barvar = ::val;//使用全局变量
            l = ai;//使用静态局部对象
        }
    };
}

位域

取地址运算符无法作用于位域

typedef unsigned int Bit;
class K
{
    Bit mod : 2;//mod占2个二进制位
    Bit modified : 1;
    Bit prot_owner : 3;
public:
    enum modes
    {
        READ = 01,
        WRITE = 02,
        EXE = 03
    };
    K &write()
    {
        modified = 1;
    }
    A& open(modes m)
    {
        mod != READ;
        if (m & WRITE)
        {
            //...
        }
    }
    inline bool isRead()const { return mod &READ; }
    void test()
    {
        auto n = &mod;//错误
    }
};

volatile限定符

当对象的值可能在程序的控制或者检测之外被改变时,volatile告诉编译器不应该对这样的对象进行优化。例如程序可能包含一个由系统时钟定时更新的变量。
不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile 对象或者从volatile赋值

链接指示extern “C”

链接指示不能出现在类或者函数定义的内部。链接指示必须在函数的每个声明都出现
除了extern “C”外,还可以有其他语言指示,如 extern “FORTRAN”, extern “Ada” 等

extern "C"
{
#include<string.h>//将string.h的所有函数链接起来。头文件的所有普通函数声明都被认为是由链接指示的语言编写的
}

链接指示还可以让C++函数在其他语言编写的程序中使用

extern "C" double calc(double param);

有时候需要C 和C++ 编译同一个源文件,在编译C++版本的程序时,可以使用预处理器定义__cplusplus

#ifdef __cplusplus
extern "C"
#endif
int strcmp(const char*,const char*);

猜你喜欢

转载自blog.csdn.net/jxhaha/article/details/78626782