大良网站建设价位,网站改版重新备案,网站案例代码,网站首页弹窗代码文章目录 Ⅰ. 设计一个类#xff0c;不允许被拷贝Ⅱ. 请设计一个类#xff0c;只能在堆上创建对象Ⅲ. 请设计一个类#xff0c;只能在栈上创建对象Ⅳ. 请设计一个类#xff0c;不能被继承Ⅴ. 请设计一个类#xff0c;只能创建一个对象#xff08;单例模式#xff09;不允许被拷贝Ⅱ. 请设计一个类只能在堆上创建对象Ⅲ. 请设计一个类只能在栈上创建对象Ⅳ. 请设计一个类不能被继承Ⅴ. 请设计一个类只能创建一个对象单例模式单例模式1、饿汉模式2、懒汉模式第一种写法第二种写法 Ⅰ. 设计一个类不允许被拷贝
拷贝只会发生在两个场景中拷贝构造函数、赋值运算符重载因此想要让一个类禁止拷贝只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 C98 的方式 设置成私有如果只声明而没有设置成 private用户自己如果在类外定义了还是等于可以拷贝 只声明不定义不定义是因为该函数根本不会调用定义了其实也没有什么意义不写反而还简单而且如果定义了就做不到防止成员函数内部拷贝了。 class CopyBan
{// ......
private:// 设为私有只声明不实现CopyBan(const CopyBan);CopyBan operator(const CopyBan);
};C11 的方式 C11中扩展了 delete 的用法delete 除了释放 new 申请的资源外如果在默认成员函数后跟上 delete表示让编译器删除掉该默认成员函数。 class CopyBan
{// 直接使用delete关键字CopyBan(const CopyBan) delete;CopyBan operator(const CopyBan) delete;// ......
};Ⅱ. 请设计一个类只能在堆上创建对象
实现方式 将类的构造函数私有化并将拷贝构造的声明也私有化防止别人调用拷贝在栈上生成对象。或者用 default、delete 关键字也行 提供一个完成堆对象创建的静态成员函数。
顺便提一下有人采用将析构函数变成私有的方法来使类的默认构造函数、拷贝构造、赋值重载不会自动生成这也是可以的但是这时候就需要我们手动去写一个释放的函数来调用所以一般我们也只用上面的方法而这种 将析构函数私有的方法不常用
class HeapOnly
{
public:// static的好处就是我们不需要对象就可以在类外通过类名::函数名直接访问static HeapOnly* CreateObject(){return new HeapOnly;}
private:// 默认构造函数不能直接封掉因为上面的CreateObject()需要调用// 可以只声明不实现这里直接使用default关键字HeapOnly() default;// 拷贝构造和赋值重载要封掉防止拷贝产生栈空间对象HeapOnly(const HeapOnly) delete;HeapOnly operator(const HeapOnly) delete;
};int main()
{HeapOnly* h1 HeapOnly::CreateObject();// HeapOnly h2 h1; // ❌// static HeapOnly h3; // ❌static HeapOnly* h3 HeapOnly::CreateObject(); // 本质还是一个指向堆空间的对象cout typeid(h3).name() endl;return 0;
}// 运行结果
class HeapOnly * __ptr64Ⅲ. 请设计一个类只能在栈上创建对象 方法一同上面那种情况一样将构造函数私有化然后设计静态方法创建对象返回即可。 class StackOnly
{
public:static StackOnly CreateObject(){return StackOnly();}
private:StackOnly() default;/* 或者只声明不定义StackOnly(){}*/
};int main()
{StackOnly s1 StackOnly::CreateObject();StackOnly* s2 new StackOnly; // ❌static StackOnly s3; // ❌return 0;
}方法二屏蔽 operator new 和 operator delete。因为 new 在底层调用 void* operator new(size_t size) 函数只需将该函数屏蔽掉即可。 注意要防止定位 new。这种方法其实是不太好使的因为就算我们禁用了 operator new 或者 operator delete我们也很难防止其在静态区中产生对象如果使用这种方法那么还是得和方法一一样将构造函数私有化然后使用静态函数返回栈对象那为何不直接使用第一种方法呢❓❓❓ class StackOnly
{
public:StackOnly() {}
private:void* operator new(size_t size);void operator delete(void* p);
};int main()
{StackOnly s1;StackOnly* s2 new StackOnly; // ❌static StackOnly s3; // 仍然可以生成静态区对象return 0;
} 这里要说明一个点就是我们还是 没办法预防产生静态变量如下面代码
class StackOnly
{
public:static StackOnly CreateObject(){return StackOnly();}
private:StackOnly() default;// 不能封掉拷贝构造不然CreateObject无法return// StackOnly(const StackOnly) delete;
};
int main()
{StackOnly s1 StackOnly::CreateObject();// 无法封掉这种情况因为如果封掉拷贝构造的话那么我们就无法在CreateObject中return一个临时栈对象了static StackOnly s2 StackOnly::CreateObject(); cout typeid(s2).name() endl;return 0;
}// 运行结果
class StackOnly 如果我们想避开这种情况唯一的方法就是 不使用栈对象我们只通过 CreateObject() 来调用类中的某些函数但是一般这么做就有点一次性那味。
Ⅳ. 请设计一个类不能被继承 C98 的方式 // C98中构造函数私有化派生类中调不到基类的构造函数。则无法继承
class NonInherit
{public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};C11 的方式 使用 final 修饰类表示该类不能被继承。 class A final
{// ....
};Ⅴ. 请设计一个类只能创建一个对象单例模式
设计模式Design Pattern是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期七国之间经常打仗就发现打仗也是有套路的后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化设计模式是软件工程的基石脉络如同大厦的结构一样。
我们之前在学习 C 的过程中其实早就接触到了设计模式比如迭代器模式、适配器模式等等下面我们就来讲一下单例模式
单例模式
一个类只能创建一个对象即单例模式该模式可以保证系统中该类只有一个实例并提供一个访问它的全局访问点该实例被所有程序模块共享。比如在某个服务器程序中该服务器的配置信息存放在一个文件中这些配置数据由一个单例对象统一读取然后服务进程中的其他对象再通过这个单例对象获取这些配置信息这种方式简化了在复杂环境下的配置管理。
⚜️一般来说单例模式下是不需要考虑资源释放的因为我们这个单例对象是在主程序结束之后会自动释放的如果没有特定需求说要提前释放一般我们都不需要实现资源释放的功能但是如果需要的话比如说我们需要手动释放因为一些资源可能要保存到日志等原因所以我们就得实现释放资源函数比如说 DeleteInstance() 等等下面的懒汉模式中会实现
单例模式有两种实现模式
1、饿汉模式
饿汉模式的宗旨就是说 不管你将来用不用主程序启动时就创建一个唯一的实例对象。这就像一个饿汉一样一旦遇到了食物那么此时都是控制不住想直接去吃的所以这种模式叫做饿汉模式
优点 控制简单因为是在执行主函数之前就生成了对象所以 没有线程安全问题。如果这个单例对象 在多线程高并发环境下频繁使用性能要求较高那么显然使用饿汉模式来避免资源竞争响应速度更好。 缺点 若单例对象的成员数据过多那么会 导致整个程序启动变慢。如果有多个单例类是相互依赖并且有初始化依赖顺序的那么饿汉模式在创建的时候是控制不了这种依赖顺序。可参考 Effective C
实现方式
首先既然是单例模式那么我们肯定要保证全局只能产生一个对象那么我们想到的就是用静态变量所以我们在 Singleton 类中定义一个静态变量 single_object并且用一个静态成员函数 CreateObject() 返回该对象而这个对象就是这个单例对象。
接着为了防止生成一个栈对象、堆对象我们得将拷贝构造和赋值重载封掉并将构造函数私有化而且不实现
// 饿汉模式
// 优点简单
// 缺点可能会导致进程启动慢且如果有多个单例类对象实例启动顺序不确定。
class Singleton
{
public:static Singleton CreateObject(){return single_object;}void Print(){cout 饿汉模式::Print() endl;}
private:// 构造函数私有并且不实现Singleton() {}// 拷贝构造以及赋值重载封掉Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;public:static Singleton single_object; // 声明一个当前类的静态变量
};Singleton Singleton::single_object; // 类外初始化这个静态变量int main()
{// 第一种访问方法通过CreateObject()直接调用相关接口Singleton::CreateObject().Print();// 第二种访问方法可以使用引用接收CreateObject()通过该对象调用相关接口Singleton s Singleton::CreateObject();s.Print();Singleton s1; // ❌无法通过编译Singleton* s2 new Singleton; // ❌无法通过编译static Singleton s3; // ❌无法通过编译return 0;
}// 运行结果
饿汉模式::Print()
饿汉模式::Print() 注意上面我们在实现饿汉模式以及后面的懒汉模式的时候都采用 c11 的 delete 关键字进行防止拷贝发生而不采用 c98 的方式
2、懒汉模式
如果单例对象构造十分耗时或者占用很多资源比如加载插件啊 初始化网络连接啊读取文件啊等等而有可能该对象程序运行时不会用到那么也要在程序一开始就进行初始化就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式延迟加载更好。
但是懒汉模式的问题就是涉及到了多线程安全问题所以实现起来当然会复杂许多。
优点 因为对象在主程序之后才会创建所以 程序启动比饿汉模式要快。可以控制不同的单例类的依赖关系以及控制依赖顺序。 缺点 涉及到多线程安全问题需要加锁实现要复杂许多。
第一种写法
这种写法涉及到了加锁的问题所以会复杂一点。和饿汉模式一样我们需要一个 静态成员函数 CreateObject() 用于获取这个单例对象不同的是懒汉模式中我们不能直接在类内定义一个静态变量因为我们要的效果是当我们调用 CreateObject() 的时候单例对象才会被创建而不是在程序启动之前就被创建了所以我们在 类内定义的成员应该是一个静态指针 single_ptr并且将其初始化为 nullptr这样子当我们去调用 CreateObject() 的时候如果判断 single_ptr 为空则进行资源的初始化否则说明已经被初始化过了则不会再去初始化它达到单例对象的目的
这个时候问题就来了既然出现了判断以及对成员变量的操作那么在多线程环境中就有可能会出现问题所以我们就 需要加锁
我们可以直接在类内定义一把 静态的锁 _mtx然后在单例对象指针判空那个代码块前后分别上锁和解锁。但是这里其实还会涉及一个问题因为我们的 single_ptr 是通过 new 出来的那么 可能 new 还会抛异常导致死锁的问题这个时候其实可以用异常捕获但是我们一般不这么做因为写法比较挫。
一般我们都会用一个 守卫锁在头文件 mutex 中就有一个函数模板 lock_guard 可以直接使用这里为了便于理解我们手动实现一个简易的守卫锁 LockGuard具体看代码其实不难就是 在构造函数中上锁在析构函数中解锁所维护的就是一个锁对象 我们自行实现的守卫锁时候可能会出现一些问题比如我们在定义锁对象的时候不能直接使用它对应的锁类型因为我们在拷贝构造函数中初始化的时候其实是通过拷贝一个锁对象来赋值的但是问题来了锁是不能拷贝的那咋整❓❓❓ 这个时候我们就不能只是简单的定义一个锁对象我们可以定义一个锁对象的引用或者指针这里采用引用的方式 通过锁对象的引用就必须在构造函数中进行初始化C 语法规定这样子的话我们引用的其实就是一个传过来的锁对象的别名就能绕开锁不能赋值的问题了 搞不清楚这里在说什么的可以看看下面代码中的守卫锁部分就懂了 除此之外我们会发现如果我们已经生成了一个单例对象但是如果后续还有线程调用 CreateObject() 的时候每次都会被我们的守卫锁卡住这势必会导致我们程序的效率低下所以这里我们 用两层 if 判空减少对锁的消耗虽说写起来比较冗余但是这大大提高了程序的效率
另外我们在上面提到过单例模式一般来说是不需要释放的但是还是避免不了有时候我们需要保存资源到日志啊等情况那么我们就得对这个资源释放问题做一下处理所以我们下面实现中多写了一个 静态成员函数 DeleteInstance() 用于处理资源释放问题而我们可以在主程序或者其它函数中去手动释放它
但是如果我们想释放又忘记释放了呢❓❓❓所以为了保险我们可以定义一个 自动回收资源类 GC 类garbage control实现并不难我们可以将其定义在单例类里面作为一个 内部类这样子的话就能很方便的取到 Singleton 类中我们写的 DeleteInstance()。
而这个 GC 类实现的思想就是在析构函数中调用上述的 DeleteInstance()要注意的是因为有可能我们提前手动释放了这段空间所以 我们需要判断 single_ptr 是否已经为空是的话说明已经被释放则我们不做任何动作防止二次释放如果不为空我们再去调用 DeleteInstance() 进行释放
这样子 GC 类就达到该目的如果提前手动释放则不会回收如果没有提前手动释放则会在这里自动释放
除此之外还有一个重点就是我们定义的静态指针 single_ptr 得用 volatile 修饰因为由于编译器可能会对代码进行优化导致 重排序等问题使用 volatile 关键字可以防止编译器优化保证线程安全。 重排序的解释 重排序是指在编译器、处理器或者运行时系统中由于优化等原因指令执行的顺序可能会被改变但是最终的执行结果与原本的代码保持一致。 在多线程编程中重排序可能会导致线程安全问题因为线程的执行顺序是不确定的如果在代码中没有正确的同步措施就有可能导致意想不到的结果。 例如在单例模式的实现中如果不加同步措施那么在多线程环境下可能会出现多个实例被创建的情况。这是因为在创建实例的代码中可能会发生指令重排序导致另一个线程在检查实例是否为空之前就已经获取到了一个未完成初始化的实例对象。 为了避免这种情况可以使用 volatile 关键字来修饰单例对象指针。volatile 关键字告诉编译器不要对这个变量进行重排序优化从而保证了单例对象的正确创建。 // 这里单独写一个守卫锁是为了方便理解
templateclass Lock
class LockGuard
{
public:LockGuard(Lock lock):_lock(lock){_lock.lock();}~LockGuard(){_lock.unlock();}
private:// 这里的_lock要用引用接收不然如果只是一个Lock类型的对象那么在构造函数中是不允许拷贝构造的锁不允许拷贝// 当然这里也可以用指针只不过这里用引用更贴切c的方式Lock _lock;
};// 懒汉模式
// 优点第一次使用实例对象时创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
// 缺点复杂
class Singleton
{
public:static Singleton CreateObject(){// 涉及多线程要加锁// 但是有可能new会抛异常导致死锁所以我们可以用一个守卫锁// 除此之外当前对象已经new出来之后为了防止后面来的线程都会被锁住影响效率我们可以用双层判断来防止这种情况if (single_ptr nullptr){// std::lock_guardmutex lock(_mtx); // 当然也可以用库里的lock_guardLockGuardmutex lock(_mtx); // 使用守卫锁if (single_ptr nullptr){single_ptr new Singleton;}}return *single_ptr;}// 自动回收资源的管理类// 如果提前手动释放则不会回收// 如果没有提前手动释放则会在这里自动释放class GC{public:~GC(){// 如果没有被提前手动释放则才会去释放防止不小心多次释放if (single_ptr ! nullptr){// 内部类是外部类的友元可以直接调用DeleteInstance();}}};static GC _gc; // 定义一个静态成员变量程序结束时系统会自动调用它的析构函数从而释放单例对象// 一般我们不需要考虑释放// 但是如果我们想要保存资源的时候就得处理一下static void DeleteInstance(){// 保存文件等操作可自行添加// .......// 删除和保存的时候可能有多线程问题要加锁LockGuardmutex lock(_mtx);if (single_ptr ! nullptr){delete single_ptr;single_ptr nullptr; // 记得置空下一个线程进来的时候判断后就不会进入该代码块}cout 资源处理完成释放成功 endl;}void Print(){cout 懒汉模式::Print() endl;}
private:// 构造函数私有并且不实现Singleton() {}// 拷贝构造以及赋值重载封掉Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;private:volatile static Singleton* single_ptr; // 单例对象指针用volatile修饰防止编译器过度优化static mutex _mtx; // 静态的互斥锁
};volatile Singleton* Singleton::single_ptr nullptr; // 初始化为空
mutex Singleton::_mtx;
Singleton::GC Singleton::_gc;int main()
{// 第一种访问方法通过CreateObject()直接调用相关接口Singleton::CreateObject().Print();// 第二种访问方法可以使用引用接收CreateObject()通过该对象调用相关接口Singleton s Singleton::CreateObject();s.Print();// s.DeleteInstance(); // 如果需要的话就提前手动释放一下//Singleton s1; // ❌//Singleton* s2 new Singleton; // ❌//static Singleton s3; // ❌return 0;
}// 运行结果
懒汉模式::Print()
懒汉模式::Print()
资源处理完成释放成功第二种写法
这种写法相比上面就简单多了其实利用的是 C11 的一个新特性局部静态变量初始化是线程安全的
注意这在 C11 之前都是不保证的所以这种方法不是通用的但是也是很好用的一种
其实就是在 静态成员函数 CreateObject() 中直接创建一个局部静态变量并且返回它的引用我们都知道因为局部静态变量对于这个静态成员函数来说只有一份如果其已经先被初始化了那么后续进来之后是不会有任何初始化工作的并且依靠 C11 更新的这个特性我们 无需上锁
class Singleton
{
public:// 会不会有线程安全问题// c11之前这里是不能保证single_object初始化是线程安全的// c11之后这里是线程安全的// 也就是说c11之后局部静态变量初始化是线程安全的// 所以这种写法不是通用的比较少用但是也是可以用的static Singleton CreateObject(){// 直接在CreateObject()创建一个静态单例类对象直接返回static Singleton single_object;return single_object;}void Print(){cout 懒汉模式::Print() endl;}
private:// 构造函数私有并且不实现Singleton() {}// 拷贝构造以及赋值重载封掉Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;
};int main()
{// 第一种访问方法通过CreateObject()直接调用相关接口Singleton::CreateObject().Print();// 第二种访问方法可以使用引用接收CreateObject()通过该对象调用相关接口Singleton s Singleton::CreateObject();s.Print();//Singleton s1; // ❌//Singleton* s2 new Singleton; // ❌//static Singleton s3; // ❌return 0;
}// 运行结果
懒汉模式::Print()
懒汉模式::Print()