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

news2024/11/25 16:51:52

目录

一、特殊类的设计

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++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()
    {}
};

测试代码正常报错

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接口也拦不住你,不过这些也都是可以禁的。

4.不能被继承的类

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

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

class non : public no_inherit
{};

测试报错

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

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

class non : public no_inherit
{};

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

二、单例模式

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

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

单例模式有以下特征:

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

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

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()进行双检查加锁。

第一版

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

第二版

这一版就是正确的了,有些人可能不理解为什么要判断两次。首先第一次调用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;
}

测试结果:

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

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

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

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

6.总结

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/760481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python补充笔记1-字符串

目录 1.字符串的驻留机制​编辑 2.字符串查找 2.1字符串查询操作方法 3.字符串大小写转换 3.1字符串的大小写转换方法 4.字符串内容对齐 4.1字符串内容对齐操作方法 5.字符串的劈分 5.1字符串劈分操作的方法​编辑 6.字符串判断 6.1判断字符串操作的方法​编辑 6.2字符串替换和…

虚拟化技术及实时虚拟化概述

版权声明&#xff1a;本文为本文为博主原创文章&#xff0c;未经本人同意&#xff0c;禁止转载。如有问题&#xff0c;欢迎指正。博客地址&#xff1a;https://www.cnblogs.com/wsg1100/ 实时虚拟化技术是一种针对实时应用场景的虚拟化技术&#xff0c;它要求在保证虚拟化优势…

STM32 ws2812b 最快点灯cubemx

文章目录 前言一、cubemx配置二、代码1.ws2812b.c/ws2812b.h2.主函数 前言 吐槽 想用stm32控制一下ws2812b的灯珠&#xff0c;结果发下没有一个好用的。 emmm&#xff01;&#xff01;&#xff01; 自己来吧&#xff01;&#xff01;&#xff01;&#xff01; 本篇基本不讲原理…

6、传输层TCP28

TCP协议&#xff1a;传输控制协议 1、协议实现 16位源端端口&16位对端端口&#xff1a;描述通信俩端进程32位序号&#xff1a;告诉接收端&#xff0c;这条数据在整体数据中的排序&#xff0c;接收端根据序号进行排序32位确认序号&#xff1a;向发送端进行回复确定&#xff…

pytest-html报告修改与汉化

目录 前言 生成报告 测试代码 原始报告 修改Environment 修改后的效果 修改Summary 修改后的效果 修改Results 优化Test 解决中文乱码 删除多余部分 修改后的效果 删除Links 修改后的效果 增加失败截图与用例描述 完整的conftest.py代码 汉化报告 修改plugin…

ClickHouse进阶

一、Explain查看执行计划 在 clickhouse 20.6 版本之前要查看 SQL 语句的执行计划需要设置日志级别为 trace 才能可以看到&#xff0c;并且只能真正执行 sql&#xff0c;在执行日志里面查看。 在 20.6 版本引入了原生的执行计划的语法。在 20.6.3 版本成为正式版本的功能。 …

常见的JS内置对象——字符串、数学、日期

二、字符串&#xff08;string&#xff09; 创建 一般使用第一种方式 2&#xff09;字符串的遍历 注意&#xff1a;没有foreach方法 3&#xff09;字符串的常见方法 substr()和substring()&#xff1a; substr()参数是从哪个位置开始&#xff0c;截多长 substring()参数是从…

完美匹配:一种简单的神经网络反事实推理学习表示方法

英文题目&#xff1a;Perfect Match: A Simple Method for Learning Representations For Counterfactual Inference With Neural Networks 翻译&#xff1a;完美匹配&#xff1a;一种简单的神经网络反事实推理学习表示方法 单位&#xff1a; 论文链接&#xff1a;https://a…

【状态估计】基于FOMIAUKF、分数阶模块、模型估计、多新息系数的电池SOC估计研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

C++ 创建共享内存

共享内存用于实现进程间大量的数据传输&#xff0c;共享内存是在内存中单独开辟一段内存空间&#xff0c;这段内存空间有自己特有的数据结构&#xff0c;包括访问权限、大小和最近访问时间等。 1、shmget函数 #include <sys/ipc.h> #include <sys/shm.h> int shm…

c++——多态(补充)

优先查看&#xff1a;c——多态_Hiland.的博客-CSDN博客 目录 菱形虚拟继承子类的重写问题 菱形虚拟继承中的偏移量补充 逆向思维——汇编查看多态中被重写的虚函数 菱形虚拟继承子类的重写问题 继承环节时&#xff0c;菱形虚拟继承解决了菱形继承的数据冗余和二义性问题。…

C# Modbus通信从入门到精通(11)——Modbus RTU(调试软件Modbus Slave和Modbus Poll的使用)

前言 我们在开发Modbus程序的时候,会需要测试以下我们写的Modbus程序有没有问题,这时候就需要使用到Modbus Slave和Modbus Poll这两个软件,Modbus Slave是模拟Modbus从站,Modbus Poll是模拟Modbus从站主站的, 1、Modbus Slave 一般情况下我们开发的嗾使Modbus主站程序,…

性能测试(Jemeter)

1.性能指标 响应时间&#xff1a;一次请求的往返时间tps&#xff1a;每秒系统能够处理的事务数&#xff0c;比如订单中的下单操作&#xff0c;下单后续有很多操作&#xff0c;比如创建订单&#xff0c;扣除库存&#xff0c;清算库存等&#xff0c;这个完整操作就是一个完整的事…

【数据分享】1929-2022年全球站点的逐日最大持续风速数据(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 对于具体到监测站点的气象数据&#xff0c;之前我们分享过1929-2022年全球气象…

Qt添加第三方字体

最近开发项目时&#xff0c;据说不能用系统自带的微软雅黑字体&#xff0c;于是找一个开源的字体&#xff0c;思源黑体&#xff0c;这个是google和Adobe公司合力开发的可以免费使用。本篇记录一下Qt使用第三方字体的方式。字体从下载之家下载http://www.downza.cn/soft/266042.…

Pytest参数化——那些你不知道的使用技巧

目录 前言 装饰测试类 输出 说明 装饰测试函数 单个数据 输出 说明 一组数据 输出 说明 图解对应关系 组合数据 输出 说明 标记用例 输出 说明 嵌套字典 输出 增加可读性 使用ids参数 输出 说明 自定义id做标识 输出 说明 总结 总结&#xff1a; 前…

给你二叉树的根节点 root ,返回它节点值的中序遍历

题目&#xff1a;给你二叉树的根节点 root &#xff0c;返回它节点值的中序遍历。 要求&#xff1a;非递归实现。 1/ \2 3/ \ / \4 5 6 7中序遍历结果为&#xff1a; 4 2 5 1 6 3 7这里考察中序遍历思想&#xff0c;使用Stack的后进先出特性输出结果。 TreeNode树状结…

spring项目的创建和使用(详细教程 手把手)方法一

今天我们来讲使用maven方式创建一个sping项目。 1、创建一个普通的maven项目。 2、添加spring框架(引入依赖)支持。添加到pom.xml文件中。 添加的框架有 spring-context&#xff1a;spring 上下⽂&#xff0c;还有 spring-beans&#xff1a;管理对象的模块。 <dependenc…

python将.h5文件转换成csv

五、在jupyter中找到results文件夹&#xff0c;然后可以把跑的.h5结果转换为csv文件 pip install tables import h5py import numpy as np import pandas as pd filename Mnist_FEDL_0.003_0_10u_20b_5_avg.h5 f h5py.File(filename, r) # List all groups print("K…

SpringMVC【文件上传(原生方式上传、上传多文件、异步上传、跨服务器上传 ) 】(五)-全面详解(学习总结---从入门到深化)

目录 SpringMVC文件上传_原生方式上传 SpringMVC文件上传_SpringMVC方式上传 SpringMVC文件上传_上传多文件 SpringMVC文件上传_异步上传 SpringMVC文件上传_跨服务器上传 SpringMVC文件上传_原生方式上传 上传是Web工程中很常见的功能&#xff0c;SpringMVC框架简化了文…