【C++11】特殊类设计

文章目录

  • [1. 设计一个类,不能被拷贝](#1. 设计一个类,不能被拷贝)
  • [2. 设计一个类,只能在堆上创建对象](#2. 设计一个类,只能在堆上创建对象)
  • [3. 设计一个类,只能在栈上创建对象](#3. 设计一个类,只能在栈上创建对象)
  • [4. 设计一个类,不能被继承](#4. 设计一个类,不能被继承)
    • [4.1 方法一](#4.1 方法一)
    • [4.2 方法二](#4.2 方法二)
    • [4.3 额外补充](#4.3 额外补充)
  • [5. 设计模式](#5. 设计模式)
  • [6. 设计一个类,只能创建一个对象(单例模式)](#6. 设计一个类,只能创建一个对象(单例模式))
    • [6.1 饿汉模式](#6.1 饿汉模式)
    • [6.2 懒汉模式](#6.2 懒汉模式)
    • [6.3 懒汉模式的线程安全问题](#6.3 懒汉模式的线程安全问题)
  • [7. 最简单的懒汉模式实现](#7. 最简单的懒汉模式实现)

1. 设计一个类,不能被拷贝

拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

C++98:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。

cpp 复制代码
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

  • 设置成私有:如果只声明没有设置成 private,用户自己如果在类外定义了,就可以不能禁止拷贝了。
  • 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

C++11 :扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。

cpp 复制代码
class CopyBan
{
    CopyBan(const CopyBan&) = delete;
    CopyBan& operator=(const CopyBan&) = delete;
    // ...
};

2. 设计一个类,只能在堆上创建对象

正常来说,创建对象的方式如下所示:

cpp 复制代码
class HeapOnly
{
public:
};

int main()
{
	HeapOnly hp1;			// 栈上申请
	static HeapOnly hp2;	// 静态区
	HeapOnly* hp3 = new HeapOnly;	// new出来的

	return 0;
}

但是我如果只想在堆上申请呢?

实现方式:

  • 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  • 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。

C++98 写法代码如下所示:

cpp 复制代码
class HeapOnly
{
public:
	static HeapOnly* CreateObject()
	{
		cout << "static HeapOnly* CreateObject()" << endl;
		return new HeapOnly;
	}

private:
	// 构造函数私有
	HeapOnly() {}

	// C++98
	// 1.只声明, 不实现。因为实现可能会很麻烦,而你本身不需要。
	// 2.声明成私有
	HeapOnly(const HeapOnly&); // 拷贝构造私有
};

int main()
{
	HeapOnly* hp3 = HeapOnly::CreateObject();

	return 0;
}

C++11 写法代码如下所示:

cpp 复制代码
class HeapOnly
{
public:
	static HeapOnly* CreateObject()
	{
		cout << "static HeapOnly* CreateObject()" << endl;
		return new HeapOnly;
	}

private:
	// 构造函数私有
	HeapOnly() {}

	// C++11
	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
	HeapOnly* hp3 = HeapOnly::CreateObject();

	return 0;
}

这里比较推荐 C++11 这种写法。

3. 设计一个类,只能在栈上创建对象

构造函数私有化,然后设计静态方法创建对象返回即可。

cpp 复制代码
class StackOnly
{
public:
    static StackOnly CreateObj()
    {
        // 这里和堆不同的是:需要返回一个对象
        StackOnly st;
        return st;

        // 写法等价于下面
        //return StackOnly();
    }

    // 对一个类实现专属的【operator new】
    // 禁掉【operator new】以后,可以把下面用 【new + 调用拷贝构造】申请对象给禁掉
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;

private:
    // 构造函数私有
    StackOnly()
        :_a(0)
    {}

private:
    int _a;
};

int main()
{
    StackOnly st1 = StackOnly::CreateObj();
    StackOnly copy(st1);

    // new = operator new + 构造
    StackOnly* st2 = new StackOnly(st1); // 这里会编译报错

    return 0;
}

但这里有一个关键问题需要点破:你的类并没有真正 "完全禁止堆上创建对象"。

问题出在哪?虽然写了:

cpp 复制代码
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;

这确实可以禁止:

cpp 复制代码
new StackOnly;

但是在 main 函数里写的这一句:

cpp 复制代码
StackOnly* st2 = new StackOnly(st1);

这行代码仍然是非法的(编译报错),因为:new StackOnly(st1) 本质还是调用了:

复制代码
  operator new + 构造函数

而你已经把 operator new 禁掉了,所以这里 根本不会绕过限制,而是直接编译失败。

运行结果如下所示:

4. 设计一个类,不能被继承

4.1 方法一

C++98 中构造函数私有化,那么派生类中就调不到基类的构造函数,进而无法继承。

代码如下所示:

cpp 复制代码
class NonInherit
{
public:
    static NonInherit GetInstance()
    {
        return NonInherit();
    }
private:
    NonInherit()
    {}
};

原理是:子类构造时,必须调用父类的构造函数,但是父类的 NonInherit() 是 private 的,故子类访问不到。

运行结果如下所示:

4.2 方法二

第二种:在 C++11 中有一个 final 关键字,被 final 修饰的类,表示该类不能被继承。

cpp 复制代码
class A final
{
    // ....
};

原理:final 是语言级限制,编译器直接禁止。

运行结果如下:

4.3 额外补充

下面这段代码本质上是一个 静态工厂函数(static factory function),作用是在类内部创建对象,并把对象返回出去。

cpp 复制代码
static NonInherit GetInstance()
{
    return NonInherit();
}

static 表示这是一个 类函数(静态成员函数),特点是:

  • 不需要对象就能调用
  • 用类名直接访问

访问方式如下:

cpp 复制代码
NonInherit obj = NonInherit::GetInstance();

那么 NonInherit GetInstance() 是什么意思?其实说的是:返回值是 NonInherit 对象(按值返回)

也就是说:

cpp 复制代码
return NonInherit();

会创建一个临时对象,然后返回给调用者

为什么要这样写?因为把构造函数设成了 private 私有:

cpp 复制代码
private:
    NonInherit() {}

那么在外部就不能像下面这样写:

cpp 复制代码
NonInherit obj; // ❌ 不允许

解决办法就是让类 自己创建对象,然后交给你使用:

cpp 复制代码
NonInherit obj = NonInherit::GetInstance(); // ✅

5. 设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化,它是软件工程的基石脉络,如同大厦的结构一样。

6. 设计一个类,只能创建一个对象(单例模式)

一个类只能创建一个对象,即单例模式。该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:

  • 饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
  • 懒汉模式:就是说只有在第一次使用时,才创建这个唯一的实例对象(按需创建)。

6.1 饿汉模式

比如,我现在有一份代码,它可以创建多个对象

cpp 复制代码
class Singleton
{
private:
	map<string, string> dict;
};

int main()
{
	Singleton s1;
	Singleton s2;

	return 0;
}

运行结果如下:可以看到 s1 和 s2 的地址是不一样的

但是我现在只想让它创建一个对象

cpp 复制代码
class Singleton
{
public:
	// 第三步:提供一个获取单例的接口
	static Singleton* GetInstance()
	{
		return &m_instance;
	}

private:
	// 第一步:构造函数私有
	Singleton() {};

	// 第二步:C++98写法:防拷贝
	//Singleton(Singleton const&);
	//Singleton& operator=(Singleton const&);

	// or

	// 第二步:C++11写法:防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

private:
	map<string, string> _dict;

	// 1.1 必须在类里面进行声明
	static Singleton m_instance;
};

// 1.2 然后在类外面定义
Singleton Singleton::m_instance;  // 在程序入口之前就完成单例对象的初始化

int main()
{
	Singleton* s1 = Singleton::GetInstance();
	Singleton* s2 = Singleton::GetInstance();

	cout << "s1: " << &s1 << endl;
	cout << "s2: " << &s2 << endl;

	return 0;
}

运行结果如下:可以看到 s1 和 s2 的地址是相同的

饿汉模式是在程序启动阶段就已经创建好了唯一的实例对象,也就是说,在进入 main 函数之前,m_instance 就已经初始化完成了。

现在我们还可以添加其他成员函数

cpp 复制代码
// 饿汉模式
class Singleton
{
public:
	// 第三步:提供一个获取单例的接口
	static Singleton* GetInstance()
	{
		return &m_instance;
	}

public:
	void Add(const pair<string, string>& kv)
	{
		_dict[kv.first] = kv.second;
	}

	void Print()
	{
		for (const auto& e : _dict)
		{
			cout << e.first << ":" << e.second << endl;
		}
		cout << endl;
	}

private:
	// 第一步:构造函数私有
	Singleton() {};

	// 第二步:C++11写法:防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

private:
	map<string, string> _dict;

	// 1.1 必须在类里面进行声明
	static Singleton m_instance;
};

// 1.2 然后在类外面定义
Singleton Singleton::m_instance;  // 在程序入口之前就完成单例对象的初始化

然后在主函数里面定义调用即可

cpp 复制代码
int main()
{
	// 定义
	Singleton::GetInstance()->Add({ "xxx", "111" });
	Singleton::GetInstance()->Add({ "yyy", "222" });
	Singleton::GetInstance()->Add({ "zzz", "333" });
	Singleton::GetInstance()->Add({ "abc", "123" });

	// 打印
	Singleton::GetInstance()->Print();

	return 0;
}

运行结果如下:

饿汉模式的优缺点如下:

  • 优点:实现简单,而且天然线程安全,因为初始化在程序启动阶段完成。
  • 缺点:首先如果单例对象初始化内容很多,那么会影响启动速度。其次,如果有两个单例类,并且互相有依赖关系,那么实例启动的顺序就不确定。(比如 A、B 两个单例类,要求 A 先创建,B 再创建,B 的初始化创建会依赖 A)

那么饿汉模式适用于,如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

6.2 懒汉模式

如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。

懒汉模式就是说不在程序启动时创建对象,而是在第一次使用时才创建。

cpp 复制代码
// 懒汉模式
class Singleton
{
public:
	// 提供获取单例的接口
	static Singleton* GetInstance()
	{
		if (m_instance == nullptr)
		{
			m_instance = new Singleton;
		}
		return m_instance;
	}

private:
	// 构造函数私有
	Singleton() {};

	// 防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

private:
	map<string, string> _dict;

	// 指针形式的单例
	static Singleton* m_instance;
};

// 类外初始化
Singleton* Singleton::m_instance = nullptr;

懒汉模式是在第一次调用 GetInstance 时才创建对象,如果一直不调用,就不会创建对象。它的优点是按需创建,不会浪费资源;缺点是线程不安全,在多线程环境下可能会创建多个对象,需要加锁处理。

另外,一般单例不需要释放,但是还是会有一些特殊场景,比如:

  • 1、需要显示释放。
  • 2、程序结束时,需要做一些特殊动作(持久化)

如下所示:

cpp 复制代码
public:
	// 释放接口
	static void DelInstance()
	{
		if (m_instance)
		{
			delete m_instance;
			m_instance = nullptr;
		}
	}
	
private:
	// 析构函数私有
	~Singleton()
	{
		cout << "~Singleton()" << endl;
	}

假设我现在单例模式创建出来的对象已经有值了,但是我想要手动的去销毁,然后重新创建。

cpp 复制代码
int main()
{
	// 定义
	Singleton::GetInstance()->Add({ "xxx", "111" });
	Singleton::GetInstance()->Add({ "yyy", "222" });
	Singleton::GetInstance()->Add({ "zzz", "333" });
	Singleton::GetInstance()->Add({ "abc", "123" });

	// 打印
	Singleton::GetInstance()->Print();

	// 手动销毁
	Singleton::DelInstance();

	// 重新定义
	Singleton::GetInstance()->Add({ "apple", "苹果" });
	Singleton::GetInstance()->Print();

	return 0;
}

运行结果如下:

除此之外,我们还可以实现一个内嵌垃圾回收类(它是一个内部类),如下所示:

cpp 复制代码
// 懒汉模式
class Singleton
{
public:
	// 实现一个内嵌垃圾回收类 
	class GC
	{
	public:
		~GC()
		{
			Singleton::DelInstance();
		}
	};
	static GC _gc;
};

// 类外初始化
Singleton::GC Singleton::_gc;

那么我们可以把原始数据先保存到文件中去,然后再添加新的文件

cpp 复制代码
int main()
{
	// 定义
	Singleton::GetInstance()->Add({ "xxx", "111" });
	Singleton::GetInstance()->Add({ "yyy", "222" });
	Singleton::GetInstance()->Add({ "zzz", "333" });
	Singleton::GetInstance()->Add({ "abc", "123" });

	// 打印
	Singleton::GetInstance()->Print();

	// 重新定义
	Singleton::GetInstance()->Add({ "www", "000" });
	Singleton::GetInstance()->Print();

	return 0;
}

运行结果如下所示:

此时我们打开文件可以看到,之前定义的内容已经被写进去了,做了持久化处理

6.3 懒汉模式的线程安全问题

饿汉模式:程序一启动就创建好单例对象(main 函数之前),无需考虑多线程并发,线程安全天然保证。

而懒汉模式:第一次调用 GetInstance() 时才创建对象,如果两个线程同时调用,那么 GetInstance() 函数里面的【判断是否存在 + 创建对象】不是原子操作,可能同时创建多个实例,破坏单例唯一性,从而存在线程安全问题。

代码如下所示:

cpp 复制代码
#include <iostream>
#include <map>
#include <thread>

using namespace std;

// 懒汉模式单例
class Singleton
{
public:
    // 提供获取单例的接口(线程不安全版)
    static Singleton* GetInstance()
    {
        if (m_instance == nullptr) // 判断+创建不是原子操作
        {
            m_instance = new Singleton;
        }
        return m_instance;
    }

    void PrintAddress()
    {
        cout << "Singleton instance address: " << this << endl;
    }

private:
    Singleton() {}  // 构造函数私有
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    map<string, string> _dict;  // 示例资源
    static Singleton* m_instance;
};

// 类外初始化
Singleton* Singleton::m_instance = nullptr;

int main()
{
    // 创建两个线程,同时调用 GetInstance()
    thread t1([]() {
        Singleton* s1 = Singleton::GetInstance();
        s1->PrintAddress();
    });

    thread t2([]() {
        Singleton* s2 = Singleton::GetInstance();
        s2->PrintAddress();
    });

    t1.join();
    t2.join();

    return 0;
}

其中,if (m_instance == nullptr)new Singleton 不是原子操作,那么两个线程可能同时通过判断,导致创建两个对象。

运行程序多次,会发现打印的地址可能不同,说明出现了线程安全问题。

那么我们可以通过加锁来解决这个问题,代码如下所示:

cpp 复制代码
// 懒汉模式单例(双检锁)
class Singleton
{
public:
    // 提供获取单例的接口(线程安全版)
    static Singleton* GetInstance()
    {
        // 双检查
        if (m_instance == nullptr)
        {
            unique_lock<mutex> lock(_mtx);
            if (m_instance == nullptr)
            {
                m_instance = new Singleton;
            }
        }
        return m_instance; // 修正:保证始终返回实例
    }

    void PrintAddress()
    {
        cout << "Singleton instance address: " << this << endl;
    }

private:
    Singleton() {}  // 构造函数私有
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    map<string, string> _dict;  // 示例资源
    static Singleton* m_instance;
    static mutex _mtx;  // 锁对象
};

// 类外初始化
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::_mtx;

int main()
{
    // 启动多个线程同时获取单例
    thread t1([]() { Singleton::GetInstance()->PrintAddress(); });
    thread t2([]() { Singleton::GetInstance()->PrintAddress(); });
    thread t3([]() { Singleton::GetInstance()->PrintAddress(); });

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

运行结果如下:

这里使用了双检锁:

  • 外层 if 避免每次都加锁,提高效率。
  • 内层加锁保证第一次创建对象时只有一个线程能执行。

总结一句话

懒汉模式线程安全问题的本质是:判断和创建不是原子操作,多个线程可能同时创建实例。

7. 最简单的懒汉模式实现

下面是一个最简单的懒汉模式实现。

cpp 复制代码
// 懒汉模式单例
class Singleton
{
public:
    // 提供获取单例的接口
    static Singleton& GetInstance()
    {
        // 局部的静态对象会在第一次调用时初始化
        static Singleton inst;
        return inst;
    }

private:
    Singleton() {}  // 构造函数私有
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

那么这份代码是不是线程安全的呢?

在 C++11 之前它不是安全的,但是,在 C++11 之后它可以保证局部静态对象的初始化是线程安全的,即只初始化一次。

  • 以前:局部静态变量初始化 不是线程安全的,需要手动加锁。
  • 以后:局部静态变量初始化 线程安全,编译器保证只初始化一次。
相关推荐
沐知全栈开发2 小时前
MySQL 索引
开发语言
代码改善世界2 小时前
【C++初阶】vector 核心接口和模拟实现
开发语言·c++
Lyyaoo.2 小时前
【设计模式】工厂模式
java·开发语言·设计模式
今晚打老虎2 小时前
限时回归了
c++
老四啊laosi2 小时前
[C++进阶] 22. unordered_set && unordered_map使用
c++·unordered_map·unordered_set
宵时待雨2 小时前
C++笔记归纳20:智能指针
开发语言·c++·笔记
jinanwuhuaguo2 小时前
OpenClaw 2026.4.5 深度解读
android·开发语言·人工智能·kotlin·openclaw
小小马喽_Thendras2 小时前
ScheduledExecutorService 和Timer的区别
java·开发语言
报错小能手2 小时前
ios开发方向——swift内存基础
开发语言·ios·swift