目录
[方法二:C++11 关键字两用 delete 删除](#方法二:C++11 关键字两用 delete 删除)
[2. 只能在堆上创建对象的类设计](#2. 只能在堆上创建对象的类设计)
[3.1 new 和 delete 运算符的作用介绍](#3.1 new 和 delete 运算符的作用介绍)
🌼🌼前言:编程中的世界像一个巨大的乐园,而类则是这个乐园中的小小建筑,它们有时如流水般灵活,有时却如堡垒般坚固。当我们设计一个类时,大多关注的是功能实现和性能优化,但某些特殊场景下,类的设计变成了一门艺术,比如禁止拷贝 、实现单例模式 、或者打造多态框架 。今天我们从**"特殊类** "的角度聊聊如何设计一个既"特别"又"优雅"的类。
1**、禁止拷贝:让类不可被复制**
有时候,我们需要设计一个类,不允许用户拷贝它的实例。例如,管理某些稀有资源(如文件句柄、硬件资源等)时,拷贝可能导致资源冲突甚至灾难性后果。
😊😊 怎么做呢?
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载 ,因此想要让一个类禁止拷贝 ,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
方法一 :只声明不定义
将拷贝构造函数与赋值运算符重载只声明不定义 ,并且将其访问权限设置为私有即可:
cpp
#include <iostream>
class NonCopyable {
public:
NonCopyable() {} // 默认构造函数
~NonCopyable() {} // 默认析构函数
private:
// 只声明,不定义
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};
int main() {
NonCopyable obj1;
// NonCopyable obj2 = obj1; // 编译时链接错误,尝试调用未定义的拷贝构造函数
// NonCopyable obj3;
// obj3 = obj1; // 编译时链接错误,尝试调用未定义的拷贝赋值运算符
return 0;
}
必须要把拷贝构造函数与赋值运算符重载的声明放在private:中。 因为外部可以定义类中的私有(private)和共有(public)成员函数。
如果只声明拷贝构造函数或拷贝赋值运算符而不设置为**
private
或delete
** ,编译器不会主动阻止用户在类外对这些函数进行定义。用户可以在类外自行定义这些函数,从而绕过了我们试图禁止拷贝的设计。这是因为C++的访问控制只在类的声明中生效,未设置访问控制的成员默认是public
,允许类外定义和调用。
例如:
cpp
#include <iostream>
class NonCopyable {
public:
NonCopyable() {}
~NonCopyable() {}
// 只声明,未设置访问权限
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};
// 用户在类外定义这些函数
NonCopyable::NonCopyable(const NonCopyable&) {
std::cout << "Copy constructor called!" << std::endl;
}
NonCopyable& NonCopyable::operator=(const NonCopyable&) {
std::cout << "Copy assignment operator called!" << std::endl;
return *this;
}
int main() {
NonCopyable obj1;
NonCopyable obj2 = obj1; // 调用用户定义的拷贝构造函数
obj1 = obj2; // 调用用户定义的拷贝赋值运算符
return 0;
}
这样原来用户在外部定义了**拷贝构造函数或拷贝赋值运算符,依然实现了拷贝构造。不符合我们的要求。将 **拷贝构造函数或拷贝赋值运算符设置为私有,在类外就无法调用他们了,从而实现了不可复制的效果。
方法二:C++11 关键字两用 delete 删除
delete :作为销毁 资源的关键字,和删除的关键字。
在C++中,可以通过删除拷贝构造函数和拷贝赋值运算符 ,来防止类的实例被拷贝。这种方式非常直接而且清晰。以下是一个示例代码:
cpp
#include <iostream>
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 删除拷贝构造函数
NonCopyable(const NonCopyable&) = delete;
// 删除拷贝赋值运算符
NonCopyable& operator=(const NonCopyable&) = delete;
void show() const {
std::cout << "This object cannot be copied!" << std::endl;
}
};
int main() {
NonCopyable obj1;
obj1.show();
// NonCopyable obj2 = obj1; // 编译错误,调用了删除的拷贝构造函数
// NonCopyable obj3;
// obj3 = obj1; // 编译错误,调用了删除的拷贝赋值运算符
return 0;
}
2. 只能在堆上创建对象的类设计
通过将构造函数私有化 并提供静态成员函数进行对象创建 ,可以确保类的对象只能通过静态成员函数动态分配在堆上 ,而不能直接在栈上创建。
**构造函数私有化会导致外部无法直接创建对象,因为无法调用构造函数。**然而内部可以调用构造函数。
以下是实现一个只能在堆上创建对象的类的完整代码。
cpp
#include <iostream>
class OnlyHeap {
public:
// 提供静态方法创建对象
static OnlyHeap* createInstance() {
return new OnlyHeap();
}
// 可选:提供释放堆对象的静态方法
static void deleteInstance(OnlyHeap* instance) {
delete instance;
}
// 防止拷贝和赋值操作
OnlyHeap(const OnlyHeap&) = delete;
OnlyHeap& operator=(const OnlyHeap&) = delete;
// 提供公开方法供对象使用
void display() {
std::cout << "This object can only be created on the heap!" << std::endl;
}
private:
// 私有构造函数,禁止直接调用
OnlyHeap() {
std::cout << "OnlyHeap constructor called." << std::endl;
}
// 私有析构函数,禁止直接删除栈对象(适用于不使用 deleteInstance 的设计)
~OnlyHeap() {
std::cout << "OnlyHeap destructor called." << std::endl;
}
};
int main() {
// 在堆上创建对象
OnlyHeap* obj = OnlyHeap::createInstance();
obj->display();
// 释放对象
OnlyHeap::deleteInstance(obj);
// 禁止以下操作,编译器会报错:
// OnlyHeap stackObj; // 构造函数为私有,禁止栈上创建
// OnlyHeap stackObjCopy = *obj; // 拷贝构造函数被删除,禁止拷贝
// OnlyHeap* stackObjAssign = obj; // 赋值运算符被删除,禁止赋值
return 0;
}
**注意:****如果 createInstance()函数不是静态的,**将会出现调用歧义。因为没有创建出OnlyHeap的对象怎么能使用类中的函数?而静态函数不同,它没有this指针(
this
指针的地址等同于对象的起始地址 ),属于OnlyHeap类实例化的所有对象中,所有对象只有一份存在。
防止拷贝和赋值 通过将拷贝构造函数和拷贝赋值运算符声明为 **delete
,**可以避免拷贝和赋值行为:
cpp
OnlyHeap objCopy = *obj; // 编译错误
OnlyHeap* objAssign = obj; // 编译错误
私有析构函数 析构函数也可以(也可以不设置) 设为私有,禁止用户直接释放栈上对象(例如:delete obj
),但静态方法可以控制对象的释放。
3.只能在栈上创建对象
3.1 new
和 delete
运算符的作用介绍
在此之前我们要先介绍一下:new
和 delete
运算符的作用
-
new
运算符:它做了两件事:- 调用合适的内存分配函数**(通常是
operator new
)**来分配足够的内存。 - 在分配的内存上调用对象的构造函数。
- 调用合适的内存分配函数**(通常是
-
delete
运算符:它做了两件事:- 调用对象的析构函数。
- 释放内存**(通过
operator delete
)。**
重载 new
和 delete
的作用
你可以重载 new
和 delete
运算符来定制对象的内存管理行为。比如,你可以自定义内存池、跟踪内存使用情况,或实现其他定制化的内存分配策略。
cpp
void* operator new(size_t size) {
std::cout << "Allocating " << size << " bytes using custom new." << std::endl;
return malloc(size); // 使用自定义的内存分配方式
}
void operator delete(void* pointer) {
std::cout << "Releasing memory using custom delete." << std::endl;
free(pointer); // 使用自定义的内存释放方式
}
方法一:直接禁止堆上面创建
要设计一个只能在栈上创建对象的类,我们需要限制对象不能在堆上分配内存 (即不能通过 new
操作符创建对象)。这可以通过将类的 new
和 delete
运算符重载为私有或禁用来实现。
实现原理
-
禁止使用
new
运算符 : 将类的operator new
和operator new[]
声明为私有或删除,这样在类外部无法通过new
创建对象。 -
禁止使用
delete
运算符 : 同样,将**operator delete
和operator delete[]
** 声明为私有或删除,防止在堆上创建对象时误用delete
。(可以这样设置😊) -
允许在栈上创建对象: 默认构造函数和析构函数保留为公有权限,允许在栈上正常创建和销毁对象。
下面是实现方式的详细说明和代码示例:
cpp
#include <iostream>
class StackOnly {
public:
StackOnly() {
std::cout << "Object created on the stack" << std::endl;
}
~StackOnly() {
std::cout << "Object destroyed" << std::endl;
}
private:
// 禁止通过 new 运算符创建对象
void* operator new(size_t) = delete;
void* operator new[](size_t) = delete;
// 禁止通过 delete 运算符销毁对象
void operator delete(void*) = delete;
void operator delete[](void*) = delete;
};
int main() {
// 在栈上创建对象
StackOnly obj;
// 以下代码会导致编译错误,禁止在堆上创建对象
// StackOnly* ptr = new StackOnly(); // 错误:new 运算符被禁用
// delete ptr; // 错误:delete 运算符被禁用
return 0;
}
方法二:返回值创建
私有构造函数,通过外部调用类中的静态函数 的返回值拷贝生成一个栈上面的对象。同时也要禁止operator new
和 operator delete
来达到目的。
- 禁用
new
和delete
运算符:这将禁止在堆上创建对象。 - 通过静态方法创建栈上的对象 :
CreateObj
方法返回一个栈对象,但它的返回值是通过值传递(会调用拷贝构造函数)来保证它不能通过new
来分配内存。
cpp
#include <iostream>
class StackOnly {
public:
// 静态工厂函数,返回栈上的对象
static StackOnly CreateObj() {
return StackOnly(); // 创建栈上的对象
}
// 禁用 new 和 delete 运算符,防止在堆上分配内存
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
// 私有构造函数,禁止外部直接创建对象
StackOnly() : _a(0) {
std::cout << "StackOnly object created!" << std::endl;
}
// 私有成员变量
int _a;
};
int main() {
// 通过静态方法创建栈上的对象
StackOnly obj = StackOnly::CreateObj(); // 静态方法返回对象,并且它是在栈上创建的
// 以下代码会导致编译错误,禁止在堆上创建对象
// StackOnly* ptr = new StackOnly(); // 错误:new 被禁用
// delete ptr; // 错误:delete 被禁用
return 0;
}
4.请设计一个类,不能被继承
在 C++ 中,如果你想设计一个类,使得它不能被继承,可以通过将该类的构造函数、析构函数或其他成员函数 声明为 final
,或者将整个类声明为 final
。这样可以阻止其他类继承该类。
final有点最终类,最终函数的意思在😊
-
final
类声明:- 在类声明时使用了
final
关键字,表示该类不能被继承。 - 如果尝试从该类派生一个新类,编译器会抛出错误。
- 在类声明时使用了
-
final
关键字的作用:final
关键字禁止类继承。当一个类被标记为final
时**,任何尝试从它派生的操作都会导致编译错误。**- 同样,
final
也可以用于虚函数中,表示该函数不能被重写。
cpp
class NonInheritable final { // 使用 final 阻止继承
public:
NonInheritable() {
// 构造函数
}
void show() {
// 示例成员函数
std::cout << "This is a non-inheritable class." << std::endl;
}
private:
int _data;
};
// 下面的代码将导致编译错误,因为 NonInheritable 被声明为 final,不能被继承。
// class Derived : public NonInheritable {
// // 错误:无法从 'NonInheritable' 继承
// };
通过将基类的构造函数声明为私有 ,阻止了其他类从基类继承 ,因为派生类必须调用基类的构造函数 ,而私有构造函数无法被派生类访问。
cpp
class NonInherit {
public:
static NonInherit GetInstance() {
return NonInherit();
}
private:
NonInherit() {} // 私有构造函数,禁止外部直接创建对象
// 禁止拷贝构造和赋值操作
NonInherit(const NonInherit&) = delete;
NonInherit& operator=(const NonInherit&) = delete;
};
5.设计一个类,只能创建一个对象(单例模式)
什么是单例模式:
一个类只能创建一个对象,即单例模式 ,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
在单例模式(Singleton Pattern)中,饿汉模式 和懒汉模式是两种常见的实现方式,它们在实例化单例对象的时机上有所不同。
饿汉模式:
饿汉模式是在程序启动时就创建单例对象,这意味着一开始就会实例化单例对象。无论你是否使用该单例对象,都会在程序启动时创建它。
实现代码如下:
cpp
#include <iostream>
class Singleton {
private:
// 构造函数私有化,防止外部实例化
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 禁用拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态成员变量,保存单例对象
static Singleton instance;
public:
// 提供静态成员函数访问单例对象
static Singleton& getInstance() {
return instance;
}
void showMessage() {
std::cout << "This is the Singleton instance." << std::endl;
}
};
// 静态成员变量初始化,饿汉式,在类加载时就创建实例
Singleton Singleton::instance;
int main() {
// 访问单例对象
Singleton& singleton1 = Singleton::getInstance();
singleton1.showMessage();
// 再次访问,返回同一个实例
Singleton& singleton2 = Singleton::getInstance();
singleton2.showMessage();
std::cout << "Are both instances the same? " << (singleton1 == singleton2 ? "Yes" : "No") << std::endl;
return 0;
}
原理:
1.将构造函数私有化,使得类外面不能创造
2.禁用拷贝构造函数和赋值操作符,防止复制。
3.静态类成员变量私有,防止再次创建另一个。
4.提供一个共有静态成员函数 ,引用返回唯一对象,在类外利用。
static Singleton instance;
在类中创建静态对象 , Singleton(类型) Singleton(域名)::instance; 在类外面初始化的意思。😊
优点
简单实现 :实现比较简单,只需在类加载时就初始化静态成员变量,无需考虑延迟加载的问题。
线程安全:由于对象的初始化是在类加载时完成的,而静态变量的初始化是线程安全的,因此不需要额外的线程同步机制。
保证单例:通过静态成员变量和私有化构造函数,保证了单例模式的实现,确保了对象的唯一性。
缺点:
浪费内存 :无论是否使用该单例对象,它都会在程序启动时就被创建。如果在程序运行过程中并没有使用到该单例对象,则会浪费内存。
不适合懒加载:因为在程序启动时就会创建对象,不能根据实际需求延迟创建实例。
懒汉模式
与饿汉模式不同,懒汉模式不在程序启动时就创建单例对象,而是通过某种方式(通常是懒加载)在需要时才创建。
懒汉模式通常通过在静态成员函数中判断单例对象是否已经创建,若没有创建则进行实例化,若已经创建则直接返回现有对象。
cpp
#include <iostream>
#include <mutex>
class Singleton {
private:
// 构造函数私有化,防止外部实例化
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 禁用拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态指针,保存单例对象
static Singleton* instance;
static std::mutex mtx; // 用于保证线程安全
public:
// 提供静态成员函数访问单例对象
static Singleton* getInstance() {
if (instance == nullptr) { // 如果实例还没有创建
std::lock_guard<std::mutex> lock(mtx); // 确保线程安全
if (instance == nullptr) { // 双重检查锁定
instance = new Singleton();
}
}
return instance;
}
void showMessage() {
std::cout << "This is the Singleton instance." << std::endl;
}
// 单例销毁
static void destroy() {
delete instance;
instance = nullptr;
}
};
// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
int main() {
// 访问单例对象
Singleton* singleton1 = Singleton::getInstance();
singleton1->showMessage();
// 再次访问,返回同一个实例
Singleton* singleton2 = Singleton::getInstance();
singleton2->showMessage();
std::cout << "Are both instances the same? " << (singleton1 == singleton2 ? "Yes" : "No") << std::endl;
// 销毁单例对象
Singleton::destroy();
return 0;
}
设计在堆上的原因:(😊)
在堆上创建对象的主要原因是控制对象的生命周期 。堆上的对象可以在函数调用结束后继续存在,而栈上的对象在函数返回时会被自动销毁 。**堆上的对象允许跨多个函数或类的作用域使用,适用于需要动态分配内存和控制生命周期的场景,**如需要跨多个函数或类持久化的数据。
优点
- 延迟加载:只有在第一次访问时才会创建对象,这对于资源消耗较大的单例对象非常有利,可以避免不必要的开销。
- 适合懒加载:在一些场景中,只有在特定条件下需要使用单例对象,懒汉模式可以很好地处理这种情况,避免提前创建对象带来的资源浪费。
缺点
- 线程安全问题:如果不小心处理多线程访问,可能会导致并发创建多个实例的问题。为了保证线程安全,通常需要使用互斥锁,然而这也会增加一定的性能开销。
- 实现复杂:相比于饿汉模式,懒汉模式的实现相对复杂,需要考虑线程安全、内存管理等问题。
结语😊🌼:在本文中,我们探讨了如何设计一些特殊类,确保它们只能在特定条件下创建或使用,例如单例模式、只能在堆上创建的对象以及禁止继承的类。这些设计模式不仅帮助我们更好地控制对象的生命周期和行为,还能有效地避免程序中的潜在问题,如内存泄漏或错误的对象访问。通过深入理解这些设计原则和实现方法,我们能够构建更加健壮、可维护的系统,确保代码的质量和稳定性。
希望本文的内容能为你在实际开发中提供一些思路和启发,帮助你应对更多复杂的编程挑战。如果你有任何问题或想法,欢迎留言讨论。