C++特殊类设计及类型转换

这篇具有很好参考价值的文章主要介绍了C++特殊类设计及类型转换。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

目录

一、特殊类的设计

1.不能被拷贝的类

2.只能在堆区构建对象的类

3.只能在栈区构建对象的类

4.不能被继承的类

二、单例模式

1.饿汉模式

2.懒汉模式

3.线程安全

4.单例的释放

三、C++类型转换

1.C语言的类型转换

2.static_cast

3.reinterpret_cast

4.const_cast

5.dynamic_cast

6.总结


一、特殊类的设计

特殊类就是普通类的基础上加上一些限制条件,虽然用处不大,但是有相关需要的时候这些还是能提供思路的。

1.不能被拷贝的类

由于类的拷贝只会发生在拷贝构造函数和赋值运算符重载中,所以不允许调用拷贝构造函数和赋值运算符重载函数即可实现。

C++98中可以使用对拷贝构造函数和赋值运算符重载函数只声明不定义且声明私有的方式实现。编译器都会自动生成默认成员函数,两函数可自动生成,但声明在私有里,定义也就只能在私有里,所以这两个函数在类外面还是调不了。

class copy_ban
{
public:
    copy_ban()
    {}
private:
    copy_ban(const copy_ban& cb);
    copy_ban& operator=(const copy_ban& cb);
};

测试拷贝报错,而构造正常

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

C++11中增加了delete的用法,我们可以在两函数声明后加上=delete让编译器不再生成两函数,从根本上杜绝了拷贝的发生。

class copy_ban
{
public:
    copy_ban()
    {}
private:
    copy_ban(const copy_ban& cb) = delete;
    copy_ban& operator=(const copy_ban& cb) = delete;
};

2.只能在堆区构建对象的类

我们正常定义的局部变量都存储在栈区,要想让变量只占用堆区,我们需要让该类的对象只能通过new创建并使用构造函数初始化。

因为构造函数创建的对象在栈上,构造函数应当私有,而且不能参与资源申请。

既然构造函数不能用于构造对象,那么就需要在类中实现一个函数用于构建对象,而我们知道类内的普通成员函数第一个参数都是this指针,而如果一个类都没有对象,又何来this指针?

而静态成员函数可以直接在类域内调取,所以这个公有的创建对象的成员函数应为静态,而且返回的是new对象。

拷贝构造函数也会将对象构造在栈区,故也需要禁止。

class heap_only
{
public:
    static heap_only* create_ho()
    {
        return new heap_only;
    }
private:
    heap_only()
    {}
};

测试代码正常报错

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

3.只能在栈区构建对象的类

对于C++的内存的管理,只要你屏蔽了new和delete也就不能从堆上申请空间了。

我们使用的new和delete在底层也是两个函数void* operator new(size_t size)和void operator delete(void* p),只要在它们声明后面加上delete就可以了。

class stack_only 
{
public:
    stack_only()
    {}
private:
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
};

你要非要用malloc这种C接口也拦不住你,不过这些也都是可以禁的。

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

4.不能被继承的类

一个派生类的构造是需要调用其基类的构造函数的,所以将一个类的构造函数私有也就不能被继承了。

class no_inherit
{
private:
    no_inherit()
    {}
};

class non : public no_inherit
{};

测试报错

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

不过,这样一个不能构建对象的类也没有价值,所以C++11引入了关键字final,final修饰的类叫最终类,不能被继承。

class no_inherit final
{
public:
    no_inherit()
    {}
private:
    
};

class non : public no_inherit
{};

从语法上拒绝继承也不影响基类

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

二、单例模式

设计模式是编程中被反复使用,大多数人也都知道,最终经过分类整理的代码设计经验总结,也可以认为是代码的设计模板,按照模板编写的程序更加高效稳定。而单例模式就是其中一种设计模式,下面讲到的单例其实也可以归类到特殊类里。

单例模式的类需要保证系统中该类只有一个实例并提供一个用于访问它的全局访问点,而且该实例需要被所有程序模块共享。

单例模式有以下特征:

  • 因为全局只允许存在单个对象,所以单例对象常放在静态区或者堆区(只创建一次)。
  • 为了防止在其他位置创建该对象,构造函数必须私有。
  • 为了防止拷贝,需要禁用拷贝构造和赋值运算符重载函数。

单例模式有两种实现方式:饿汉模式和懒汉模式。

1.饿汉模式

在单例类内直接定义一个静态的类对象,静态成员变量在类域外进行初始化。在没有定义变量时静态成员就已经初始化完毕,也就是main函数开始执行语句前单例就已经建立完成。静态区也不可能再建立第二个变量,保证了单例的唯一性。

获取单例指针可以用静态函数get_objection()返回单例对象的指针,通过指针可以实现内部成员函数的调用。

class one_obj
{
public:
    static one_obj* get_objection()//获取单例对象的接口
    {
        return &_obj;
    }
private:
    one_obj()
    {}
    one_obj(const one_obj& o) = delete;//不允许拷贝构造
    one_obj& operator=(const one_obj& o) = delete;//不允许赋值

    static one_obj _obj;//单例对象,设为静态保证独一份
};
one_obj one_obj::_obj;//根据one_obj初始化

不管以后会不会使用这个单例对象,只要程序一启动,程序就会先创建一个唯一的实例对象然后再执行main中的代码。就很像一个饿汉看到吃的就直接扑上去吃,这个上去就吃的动作就相当于创建单例对象。

饿汉模式也有缺点:

  • 可能会减慢程序的启动。比如实例对象很复杂,而饿汉模式又必须优先创建单例对象,启动就会花费很多时间。
  • 实例顺序不确定。如果有多个单例对象且各对象之间存在互相依赖关系,由于单例的实例化是再main函数之前完成的,所以对象的实例顺序是由编译器决定的。如果单例初始化的顺序不合适,就会发生错误。

2.懒汉模式

懒汉模式的构造函数也是私有,拷贝构造和赋值运算符重载函数也禁止调用,但是把静态成员变量改成了静态单例对象的指针。在类外实例化静态指针变量的时候,将其初始化为空。

我们将单例对象的构造放在了获取单例指针的静态函数get_objection()中,在第一次调用这个函数时,内部就会构造出单例对象并返回指针,由于是单例对象,以后也不需要再构造直接返回指针即可。

class one_obj
{
public:
    static one_obj* get_objection()
    {
        if (_ptr == nullptr)
        {
            _ptr = new one_obj;
        }
        return _ptr;
    }
private:
    one_obj()
    {}
    one_obj(const one_obj& o) = delete;
    one_obj& operator=(const one_obj& o) = delete;

    static one_obj* _ptr;
};
one_obj* one_obj::_ptr = nullptr;

正因为单例只有在第一次被使用到时才被建立,所以懒汉模式又叫延时加载模式,就像一个懒汉一样,什么事都拖到截止日才干。

如果单例构造耗时或者占用资源多就有可能导致程序启动时非常的缓慢,而这种情况下懒汉模式就更合适。

懒汉模式的优点:

  • 第一次使用单例对象时才创建对象,进程启动过程无负载。
  • 多个互相依赖的单例可以通过代码控制构造顺序。

3.线程安全

C++11解决了饿汉模式的线程安全问题,因为单例对象是在main函数之前就实例化的,而多线程都是在main函数里面启动的。

但是懒汉模式是存在线程安全问题的,当多个线程第一次使用单例对象时,get_objection()获取对象因为调度问题就可能会出现误判,导致构造多个单例对象。

所以我们就需要对get_objection()进行双检查加锁。

第一版

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

很多人都会写成这个错误的版本,我们加锁是为了不允许两个线程同时进入第二层的判断语句。这样的加锁只保证了多个线程不会同时new出多个单例对象。所以加锁必须在第二层判断语句的外部。

第二版

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

这一版就是正确的了,有些人可能不理解为什么要判断两次。首先第一次调用get_objection()时,单例没有构造_ptr为空,进入第二层,加锁保证不会有第二个线程进入判断。第二层也通过了就直接构造单例对象即可,最后返回_ptr。

而如果有线程同时进入第一层,此时_ptr就不为空了,当然也不会构造单例。

如果没有最外层的判断,那么每一个线程进来一次都需要加锁解锁一次,这样会增大无故的开销。所以这层判断只要不符就证明单例构建好了,直接返回指针就好了,不要再加锁了。

4.单例的释放

new出的单例对象一般情况我们都不释放,因为全局只有一个单例对象,而且在程序运行时也会一直被使用。当程序结束的时,操作系统会回收该进程的所有资源,包括堆区上的资源,所以也没必要释放。

如果非得主动释放,基本也是在释放的同时将一些信息保存到磁盘,比如错误日志什么的。而单例的回收可能较为复杂,内部类又是外部类的友元,所以我们可以使用内部类析构。

最终代码如下:

#include<mutex>
class one_obj
{
public:
    static one_obj* get_objection()
    {
        if (_ptr == nullptr)
        {
            _mtx.lock();
            if (_ptr == nullptr)
            {
                _ptr = new one_obj;
            }
            _mtx.unlock();
        }
        return _ptr;
    }
    class recycle
    {
    public:
        ~recycle()
        {
            if (_ptr)
            {
                delete _ptr;
                _ptr = nullptr;

                //保存数据到磁盘,这里是读写磁盘的代码
            }
        }
    };
private:
    one_obj()
    {}
    one_obj(const one_obj& o) = delete;
    one_obj& operator=(const one_obj& o) = delete;

    static one_obj* _ptr;
    static std::mutex _mtx;
    static recycle _rec;
};
one_obj* one_obj::_ptr = nullptr;
std::mutex one_obj::_mtx;
one_obj::recycle one_obj::_rec;

两种模式各有其优点和缺点:懒汉模式中需要双检查加锁,考虑线程安全,相比于饿汉模式复杂,所以饿汉模式的优点就是简单明了,而懒汉模式的缺点就是复杂细节多。两种模式的使用要要根据具体应用场景决定。

懒汉模式还有一种实现方式,这次是在get_objection()被调用时创建静态对象,静态对象不会被第二次创建,保证了单例的唯一性。

因为禁止了拷贝构造,所以调用成员函数时不能使用one_obj obj = one_obj::get_objection()的方式,而是直接使用get_objection().print()才能正常使用。

#include<iostream>
class one_obj
{
public:
    static one_obj& get_objection()
    {
        static one_obj o;
        return o;
    }
    void print()
    {
        std::cout << "one_obj" << std::endl;
    }
private:
    one_obj()
    {}
    one_obj(const one_obj& s) = delete;//禁止拷贝
    one_obj& operator=(const one_obj& s) = delete;//禁止赋值
};

int main()
{
    one_obj::get_objection().print();
    return 0;
}

三、C++类型转换

1.C语言的类型转换

C语言中,如果赋值运算符(=)两边的变量类型不同,函数形参实参类型不匹配,还有返回值类型和接收变量类型不一致,都需要进行类型转换。

C语言有两种类型转换:隐式类型转换和显式类型转换。

隐式类型转换是编译器在编译阶段自动完成的,类型能转换就转换,不能就编译失败。

显式类型转换是用户自己处理的转换,主要是对两种没有任何关系的类型进行转换,比如将指针类型转换成整型等。

#include<stdio.h>
int main()
{
    int a = 1;
    double b = a;//隐式类型转换
    printf("%d %f\n", a, b);

    int* p = &a;
    int num = (int)p;//显式类型转换
    printf("0x%p %d\n", p, num);

    return 0;
}

C语言的类型转换也存在一定缺陷:

隐式类型转换有时会发生数据精度丢失,比如整形提升等。

显式类型转换允许很多底层结构相似的类型相互转化,所有情况混合在一起,代码不够清晰。

为了解决这些不足,C++也建立了自己的类型转换结构,C++作为C语言的超集,C语言的转换依旧可以使用。

2.static_cast

C语言的隐式类型转换在C++中统一使用static_cast转换,它不能用于两个不相关的类型进行转换。

#include<iostream>
int main()
{
    //C语言
    int a = 1;
    double b = a;
    printf("%d %f\n", a, b);

    //C++
    int c = 1;
    double d = static_cast<double>(c);//隐式类型转换
    printf("%d %f\n", c, d);

    return 0;
}

3.reinterpret_cast

C语言的显式类型转换在C++中统一使用reinterpret_cast,如果使用static_cast会报错,它专用于将一种类型转换为另一种无关的类型。

#include<iostream>
int main()
{
    //C语言
    int a = 1;
    int* p1 = &a;
    int num1 = (int)p1;
    printf("0x%p %d\n", p1, num1);

    //C++
    int b = 1;
    int* p2 = &b;
    int num2 = reinterpret_cast<int>(p2);
    printf("0x%p %d\n", p2, num2);

    return 0;
}

4.const_cast

常变量本身的常属性是在其定义时就规定好的,以后也都不可改变。而C++中使用const_cast可以去除常变量的常属性,此时变量就可以修改了。

要注意,const_cast只能将常变量的指针作为参数并返回一个普通指针,变量可以通过普通指针进行修改。

#include<iostream>
int main()
{
    const int a = 1;
    const int* p1 = &a;//指针为const int*,不能改变a值
    int* p2 = const_cast<int*>(p1);//p1转为int*并用p2接收
    *p2 = 3;
    printf("%d", a);
    return 0;
}

5.dynamic_cast

dynamic_cast可以将父类对象的指针或者引用转为子类对象的指针或引用。

对于继承的指针与引用转化分为向上和向下转换。

向上转换是子类对象的指针或引用转为父类对象的指针或引用。C++语法直接支持,不会发生类型转换。

向下转换是父类对象的指针或引用转为子类对象的指针或引用。语法不支持,但dynamic_cast可以支持。

dynamic_cast的转化有以下特性:

  • dynamic_cast只能用于父类含有虚函数的类。
  • dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回nullptr。
  • dynamic_cast是父子指针的安全转换方式,C语言的转换方式可能出现越界访问。
#include<iostream>
//父类
class A
{
public:
    virtual void f()
    {}

    int _a = 1;
};
//子类
class B : public A
{
public:
    int _b = 2;
};

int main()
{
    A a;
    B b;

    A* p1 = &a;//父类指针指向父类对象
    B* p2 = &b;//子类指针指向子类对象
    A* p3 = &b;//父类指针指向子类对象,不需要类型转换
    B* p4 = dynamic_cast<B*>(&a);
    //使用dynamic_cast转化指针,指向父类对象的父类指针转为子类指针
    B* p5 = dynamic_cast<B*>(p3);
    //使用dynamic_cast转化指针,指向子类对象的父类指针转为子类指针

    std::cout << p1 << " " << p2 << " " << p3 << " " << p4 << " " << p5 << std::endl;
    return 0;
}

测试结果:

C++特殊类设计及类型转换,C++,c++,开发语言,c语言

对于上述常见指针,在p3、p4、p5之中我们发现只有p4不能正常转化。

这是因为指向父类对象的父类指针如果转为了子类指针,它所指向的对象没有子类的部分,这就可能会造成越界访问,当然不会成功。

而将指向子类对象的父类指针转为子类指针可使该指针维护的内容覆盖整个子类数据,因为父类指针不能赋值给子类指针,所以就需要dynamic_cast转化。

C++中的类型转换,尤其是前两种static_cast和reinterpret_cast是建议用法,可以采用也可以不采用。const_cast是一种新用法,但是存在风险,dynamic_cast是一种安全的类型转换。

6.总结

C++推出了一套自己的变量类型转换方式,除了dynamic_cast是在多态转换中必须使用外,其他三种方式也仅是建议使用,可以增加代码的规范性。

 文章来源地址https://www.toymoban.com/news/detail-580225.html

到了这里,关于C++特殊类设计及类型转换的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • c++学习之特殊类设计与类型转换

    方法:c++98,通过私有且只申明不实现拷贝构造与赋值函数,从而实现该类不能被拷贝。c++11引入delete后,可以使构造构造与赋值函数等于delete。效果也是无法被拷贝。 方法一,析构私有化 方法二,构造私有化 方法一: 还是构造私有化,但是注意拷贝构造,我们拷贝

    2024年01月20日
    浏览(44)
  • C++特殊类的设计与类型转换

    通过new创建的类就是堆上的。 方法一: 这里主要以封禁构造函数为主,让外部只能通过调用func函数方式去创建对象,func函数的内部是通过new创建的,这里要注意的就是拷贝构造的问题。 赋值重载不用删除,因为需要现有一个对象才能赋值给另一个对象,上面的代码只会创

    2024年02月08日
    浏览(37)
  • C++:特殊类的设计和类型转换

    1.设计一个类,不能被拷贝 拷贝只会放生在两个场景中: 拷贝构造函数以及赋值运算符重载 ,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 2.设计一个类,只能在堆上创建对象 两种实现方式: 将类的 构造函数私有 , 拷贝构造声

    2024年01月24日
    浏览(52)
  • 【C++】特殊类设计+单例模式+类型转换

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。   目录 一、设计一个类,不能被拷贝 1、C++98 2、C++11 二、设计一个类,只能在堆上创建对象 1、将构造设为私有 2、将析构设为

    2024年02月06日
    浏览(52)
  • 【C++】异常+智能指针+特殊类和类型转换

    上天可能觉得我太孤独,派你来和我一起对抗虚无。 1. C语言传统处理错误的方式无非就是返回错误码或者直接是终止运行的程序。例如通过assert来断言,但assert会直接终止程序,用户对于这样的处理方式是难以接受的,比如用户误操作了一下,那app直接就终止退出了吗?这

    2024年02月08日
    浏览(50)
  • 【高级程序设计语言C++】特殊类设计

    拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 用C++11的话,可以使用特殊的语法来实现一个不能被拷贝的类。在C++11中,可以使用删除函数(deleted function)来禁用拷

    2024年02月10日
    浏览(48)
  • UG NX二次开发(C++)-Tag的含义、Tag类型与其他的转换

    在UG NX中,每个对象对应一个tag号,C++中,其类型是tag_t,一般是5位或者6位的int数字,打开模型后,tag号是唯一的。采用UFun函数时,其很多的函数都是依赖于tag_t的,不管其是输入参数还是输出参数。本文介绍一下tag号的含义,以及其与其他类型的转换,比如int型、double型、

    2024年02月15日
    浏览(48)
  • 【C++】特殊类设计

    欢迎来到Cefler的博客😁 🕌博客主页:折纸花满衣 🏠个人专栏:题目解析 🌎推荐文章:【LeetCode】winter vacation training 拷贝只会放生在两个场景中: 拷贝构造函数 以及 赋值运算符重载 ,因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即

    2024年01月17日
    浏览(41)
  • C++ 特殊类设计

    如果只能在堆上创建对象,就是限制在栈上和静态区中创建对象,步骤如下: 方法一:将构造函数私有化. 将构造函数设置为私有,以防用户在栈上或者静态区直接调用构造函数创建对象. 在公有区域提供一个静态的成员函数,在该静态成员函数中使用new创建对象,此时因为new在成员函

    2024年02月11日
    浏览(32)
  • 【C++】特殊类的设计

    💕 C++98方式: 在C++11之前,想要一个一个类不被拷贝,只有将 拷贝构造函数 定义为私有,这样在类外就不能调用拷贝构造函数来构造对象了。但是在类内还是可以调用拷贝构造函数来构造对象。 所以正确的做法是 将拷贝构造函数定义为私有,同时拷贝构造函数只声明,不

    2024年02月07日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包