中山网站建设文化信息,营销技巧第一季,中科建声公司简介,网站设计计划书的要求目录
1. 请设计一个类#xff0c;只能在堆上创建对象
2. 请设计一个类#xff0c;只能在栈上创建对象
3.请设计一个类#xff0c;不能被拷贝
C98
C11
4. 请设计一个类#xff0c;不能被继承
C98
C11
5. 请设计一个类#xff0c;只能创建一个对象(单例模式)
设计…目录
1. 请设计一个类只能在堆上创建对象
2. 请设计一个类只能在栈上创建对象
3.请设计一个类不能被拷贝
C98
C11
4. 请设计一个类不能被继承
C98
C11
5. 请设计一个类只能创建一个对象(单例模式)
设计模式
单例模式
单例模式的两种实现方式
饿汉模式
懒汉模式
单例对象的释放
懒汉模式与饿汉模式的优缺点
现代懒汉模式的写法
1. 请设计一个类只能在堆上创建对象
正常情况下一个类的对象既可以在堆上又可以在栈上 实现方式1. 将类的构造函数私有拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。2. 提供一个静态的成员函数在该静态成员函数中完成堆对象的创建class HeapOnly
{
public:static HeapOnly* CreateObj(){return new HeapOnly;}HeapOnly(const HeapOnly ho) delete; //防止通过拷贝构造开在栈上
private:HeapOnly():_a(0){}private:int _a;
}; 2. 请设计一个类只能在栈上创建对象
同上将构造函数私有化然后设计静态方法创建对象返回即可。目前看似杜绝在堆上创建出对象但是我们却可以通过这样的方式在堆上创建出对象 。 我这里的new虽然没有调用构造函数但是调用了拷贝构造又因为获取对象的函数是传值返回所以我们不能删除拷贝构造但是我们却可以屏蔽new因为new在底层调用void* operator new(size_t size)函数只需将该函数屏蔽掉即可。 一个类可以重载它专属的operator new没有重载之前调用的是全局的new但是重载专属的operator new以后调用的就是专属的operator new。
class StackOnly
{
public:static StackOnly CreateObj(){return StackOnly(); //不能删除拷贝构造因为这里是传值返回}void* operator new(size_t size)delete;void operator delete(void* p) delete;
private:StackOnly():_a(0){}private:int _a;
}; 3.请设计一个类不能被拷贝
拷贝只会放生在两个场景中拷贝构造函数以及赋值运算符重载因此想要让一个类禁止拷贝只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。C98
将拷贝构造函数与赋值运算符重载只声明不定义并且将其访问权限设置为私有即可。class CopyBan
{// ...private:CopyBan(const CopyBan);CopyBan operator(const CopyBan);//...
}; 原因 1. 设置成私有如果只声明没有设置成private用户自己如果在类外定义了就可以不能禁止拷贝了2. 只声明不定义不定义是因为该函数根本不会调用定义了其实也没有什么意义不写反而还简单而且如果定义了就不会防止成员函数内部拷贝了。C11
C11扩展delete的用法delete除了释放new申请的资源外如果在默认成员函数后跟上delete表示让编译器删除掉该默认成员函数。
class CopyBan
{// ...CopyBan(const CopyBan)delete;CopyBan operator(const CopyBan)delete;//...
}; 4. 请设计一个类不能被继承 C98 // C98中构造函数私有化派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
}; C11 final关键字final修饰类表示该类不能被继承 class A NonInherit
{// ....
}; 5. 请设计一个类只能创建一个对象(单例模式) 设计模式 设计模式Design Pattern是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期七国之间经常打仗就发现打仗也是有套路的后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化设计模式是软件工程的基石脉络如同大厦的结构一样。
迭代器就是一种设计模式被广泛的用于数据结构中。这个数据结构可能是个数组图哈希表二叉树等等如果要去访问它的话要把数据结构的底层全部都暴露出来但是C是不希望暴露结构的暴露的缺点在于1.访问不方便(需要熟悉底层结构) 2.暴露底层结构别人直接访问修改数据不方便管理。所以就出现了迭代器模式好处在于1.统一方式封装访问结构。底层结构不暴露。2.可以使用统一的方式轻松访问容器不关心底层是树还是链表等。
单例模式 一个类只能创建一个对象即单例模式该模式可以保证系统中该类只有一个实例并提供一个访问它的全局访问点该实例被所有程序模块共享。比如在某个服务器程序中该服务器的配置信息存放在一个文件中这些配置数据由一个单例对象统一读取然后服务进程中的其他对象再通过这个单例对象获取这些配置信息这种方式简化了在复杂环境下的配置管理。 我们直接从实例入手对于下面的这段代码我们是采取分成三个部分写的快排的定义放在一个文件里实现放在一个文件里最终的调用也放在一个文件里我要进行统计快速排序递归调用次数。我们一般的思路就是定义一个全局变量。每次递归这个全局变量就最终拿到这个全局变量就是一共递归调用的次数。
Singleton.h
#pragma once#includeiostream
#includetime.h
using namespace std;int callCount 0; //统计次数的全局变量
// 统计快速排序递归调用次数
void QuickSort(int* a, int left, int right);
Singleton.cpp
#includeSingleton.h
void QuickSort(int* a, int left, int right)
{callCount;if (left right){return;}int keyi left;int prev left;int cur prev 1;while (cur right){if (a[cur] a[keyi] prev ! cur){swap(a[cur], a[prev]);}cur;}swap(a[prev], a[keyi]);keyi prev;// [left, keyi-1]keyi[keyi1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi 1, right);
}
test.cpp
#includeSingleton.hvoid TestOP()
{srand(time(0));const int N 100000;int* a1 (int*)malloc(sizeof(int) * N);for (int i 0; i N; i){a1[i] rand();}int begin5 clock();QuickSort(a1, 0, N - 1);int end5 clock();printf(QuickSort:%d\n, end5 - begin5);printf(QuickSort Call Time:%d\n, callCount);free(a1);}
int main()
{TestOP();return 0;
}
但是对这段代码进行编译的时候会有报错 为什么会报一个全局变量callCount重定义的错误呢 Singleton.h中有一个callCount的定义Singleton.h被Singleton.cpp和test.cpp都所包含。.h文件在.cpp文件中展开.cpp会生成两个.objlinux下叫做.o然后这两个.obj中都有一个叫做callCount的变量。编译器链接的时候是都要包这些东西合在一起的Singleton.h有callCount的定义Singleton.cpp有一个callCount的变量test.cpp中也有一个叫callCount的变量这就会导致合并在一起的时候就冲突了同一个域中不能有同名的变量所以就会有重定义的错误。总之就是因为全局变量在多个文件中都可见就导致了这个问题。 static关键字 static这个关键字就可以解决这个问题普通的全局变量在多个文件都可见(在Singleton.obj和test.obj的符号表里面都有符号表里面除了有函数名还有变量。当别人要用这个变量的时候不知道是用你Singleton.obj的还是用test.obj的)。而静态全局变量只在当前文件可见static除了会改变生命周期还会影响变量和函数的链接属性让他只在当前文件可见(意思就是Singleton.cpp和test.cpp中都有一个callCount的变量但是Singleton.cpp中的callCount只在Singleton.cpp可见test.cpp中的callCount只在test.cpp可见)这样链接的时候就不会看见他们冲突因为他们只在内部可见。 但是也并没有真正的解决掉整个问题我们发现最终的callCount是0。这是因为只在当前文件可见导致 Singleton.cpp和test.cpp中的callCount并不是同一个。 我们通过打印地址发现并不是同一个 extern关键字
用extern关键字我们在Singleton.h是定义要做到把定义和声明分离就要有extern关键字。我们把声明放在一个.h中定义也只放在一个.cpp中 这个时候只有Singleton.cpp生成的Singleton.obj中有这个变量test.obj中是没有这个变量的test.cpp包着Singleton.h只有callCount的声明只有声明的话编译器是可以编过的只是我不知道这个callCount的地址我在链接的时候就可以访问到这个变量了。extern就告诉test.cpp说这个callCount变量是有的是全局的但是它在其他的.obj中定义的你链接的时候自己去找吧所以链接的时候除了要找这个QuickSort函数(因为他也是只有声明)的地址还要找这个callCount的地址这个时候就可以把callCount这个变量访问到。
这个时候就可以统计出递归调用的次数了。
ps:我们可以通过小区间优化优化一下递归调用的次数当排序的数据量变小用插排
Singleton.cpp
#includeSingleton.hint callCount 0; //然后我们在这个cpp里面放上定义void InsertSort(int* a, int n)
{for (int i 0; i n - 1; i){// x[0, end]int end i;int x a[end 1];while (end 0){if (a[end] x){a[end 1] a[end];--end;}else{break;}}a[end 1] x;}
}void QuickSort(int* a, int left, int right)
{if (callCount 0){cout Singleton.cpp中的callCount的地址 callCount endl;}callCount;if (left right){return;}if (right - left 1 10){int keyi left;int prev left;int cur prev 1;while (cur right){if (a[cur] a[keyi] prev ! cur){swap(a[cur], a[prev]);}cur;}swap(a[prev], a[keyi]);keyi prev;// [left, keyi-1]keyi[keyi1, right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi 1, right);}else{InsertSort(a left, right - left 1);}}
递归次数明显减少
单例模式的两种实现方式 假设我不仅要把全局变量保存下来我还要有一个vector要求把递归调用的区间也存储下来。你就又需要向之前一样一边声明一边定义。这个时候就可以用面向对象的思想来解决这个问题。 我们需要全局有一个变量或者对象这个对象里面有一些信息这个信息是全局的并且全局只有唯一一份且提供一个访问它的全局访问点 。我们就可以设计一个单例模式。 把需要统计的放在一个类里面这个时候就不需要加extern因为类里面的变量天生就是声明类对象才是定义。我们同时不期望别人随便创建对象我们就可以把构造函数私有化并且我们还要提供一个让你获取对象的方式并且这个对象是唯一的一个对象。这个时候我们就可以采用懒汉和饿汉两种方式获取。 饿汉模式
就是说我已经把饭提前准备好了我随时可以吃在main函数之前就创建好了单例对象程序随时可以访问这个单例对象如果这个单例对象在多线程高并发环境下频繁使用性能要求较高那么显然使用饿汉模式来避免资源竞争提高响应速度更好。这个_inst对象目前只有声明它的定义可以放在Singleton.h但是这样的话就又有之前全局变量的问题Singleton.cpp和test.cpp就各有一份了所以我们最好在Singleton.cpp中定义。 这样的话每次调用GetInstance都是调用的_inst这个对象并且都是同一个对象。 接下来统计递归调用的次数 因为这里涉及到类成员变量的一种方式就是把成员变量设置成public一种就是通过提供成员函数间接。博主这里选择增加成员函数进行 test.cpp则用成员函数获取总次数 运行结果 这个地方用类去封装用一个单例的类是非常的好用的避免我们统计的信息只有唯一一份。为了防止恶意拷贝我们就可以把拷贝构造封死
//单例模式--任意一个类都可以设计成单例模式
//饿汉模式
class CallInfo
{
public:static CallInfo GetInstance(){return _inst;}int GetCallCount(){return _callCount;}void AddCallCount(int n){_callCount n;}void Push(const pairint, int kv){_v.push_back(kv);}CallInfo(const CallInfo cl) delete;
private:CallInfo():_callCount(0){}
private:int _callCount; //统计次数vectorpairint, int _v; //记录区间信息static CallInfo _inst; //类里面定义的变量都是声明//构造函数虽然是私有的但是静态的变量是可以调用构造函数初始化的因为它的作用域是类里面//它也是类的成员只不过是在类外定义的而已。
};
PS在类里面定义对象如果是普通的对象是不可以定义的因为这就成了递归但如果是静态的就没问题因为静态的就相当于是全局的就是你这个类定义好了然后我又定义了一个全局的这个对象属于类整体相当于你就是定义了一个全局的变量只是它的作用域被限定在这个类域里面且在类里面只是一个声明和你定义一个全局的变量没有任何区别。
官方来讲因为声明的静态数据成员不占用对象的内存。但是普通的数据成员在初始化呢的时候首先要为对象分配内存然后里面又有一个类一直持续下去就不行会无限迭代的分配内存。当然引用和指针是可以的。引用和指针的大小是固定的。 //例如class A
{private:
int i;static A a;
}//A 的大小其实只是 i 的大小。
懒汉模式
事先没有准备好只有第一次访问的时候才创建单例对象如果单例对象构造十分耗时或者占用很多资源比如加载插件啊 初始化网络连接啊读取文件啊等等而有可能该对象程序运行时不会用到那么也要在程序一开始就进行初始化就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式延迟加载更好。
//懒汉模式
class CallInfo
{
public:static CallInfo GetInstance(){if (_pInst nullptr){_pInst new CallInfo;}return *_pInst;}int GetCallCount(){return _callCount;}void AddCallCount(int n){_callCount n;}void Push(const pairint, int kv){_v.push_back(kv);}CallInfo(const CallInfo cl) delete;
private:CallInfo():_callCount(0){}
private:int _callCount; //统计次数vectorpairint, int _v; //记录区间信息static CallInfo* _pInst; //类里面定义的变量都是声明}; 这就是懒汉模式懒汉就是创建一个指针静态指针执行的这个对象保证全局唯一保证每次进来获取的都是那个唯一的对象。 但是当前写的懒汉模式的GetInstance是存在大问题的是存在线程安全问题的。 如果你有一个t1和一个t2t1和t2都调用GetInstance导致统计的次数不准确。假设t1和t2都走到62行t1和t2走到这里都是空指针t1走到这时间片到了t2先newnew完后返回这个对象然后去调用把次数加到1。然后t2的时间片到了t1开始运行虽然此时的指针已经不为空了但是t1不会判断(因为一个线程回来的时候是从它切出去的那一行开始运行的)然后t1在new一个返回。t2在来的时候发现不为空但是获取到的是t1刚刚new的所以次数又从0次开始t2之前统计的次数就丢失了。而且后面new出来的还会把前面new出来的覆盖掉会存在内存泄露的问题。 eg: 我们可以通过加锁解决 PS用静态的锁一方面是要保证两个线程使用同一把锁另外一方面就是静态成员函数只能访问静态成员变量不能访问普通成员变量 为什么不能在静态成员函数中使用非静态变量 所谓静态就是程序编译好以后就已经给它分配了内存区域它一直在那里所谓动态就是运行时候临时分配内存的变量。 程序最终都将在内存中执行变量只有在内存中占有一席之地时才能被访问。因为静态是针对类的而成员变量为对象所有。 静态成员函数不属于任何一个类对象没有this指针而非静态成员必须随类对象的产生而产生所以静态成员函数”看不见”非静态成员自然也就不能访问了 类的静态成员(变量和方法)属于类本身在类加载的时候就会分配内存可以通过类名直接去访问非静态成员变量和方法属于类的对象所以只有在类的对象产生创建类的实例时才会分配内存然后通过类的对象实例去访问。 在一个类的静态成员中去访问其非静态成员之所以会出错是因为在类的非静态成员不存在的时候类的静态成员就已经存在了访问一个内存中不存在的东西当然会出错。 C会区分两种类型的成员函数静态成员函数和非静态成员函数。这两者之间的一个重大区别是静态成员函数不接受隐含的this自变量。所以它就无法访问自己类的非静态成员。 当前就可以保证不会发生线程安全的问题了。但是我们还可以进一步去优化加锁是为了保护第一次获取对象。只要对象创建出来以后就没有线程安全的问题现在这种写法后面每次获取对象都要进行加锁就会影响效率。 所以我们可以采用双检查加锁 这个时候单例对象创建好了以后获取单例对象的时候就不用每次进行加锁解锁。 单例对象的释放 一般情况下单例对象是不需要释放的不用担心它内存泄露的问题因为它是进程堆上的资源通常也不大main函数结束以后进程就销毁了进程结束后的资源都会还给系统。但是你非要释放的话也是可以的、
1.直接主动的去提供一个释放的接口但是这样做并不常见 2.提供一个内部类进行回收
这个gc对象出了作用域就会去调用它的析构函数这个析构函数就会把new的对象带走。
一般懒汉的单例对象不需要回收因为进程正常结束资源都会还给系统这个对象只有一个系统自动回收也没什么问题但是如果你在单例对象释放析构时有一些要完成的动作比如要记录日志等等。那么可以考虑搞一个类似下面的回收类帮助去完成这个事情。
懒汉模式与饿汉模式的优缺点
饿汉没有线程安全的问题因为它在main函数之前就准备好了main函数之前是没有多线程竞争的问题的。
饿汉优点简单。缺点无法控制单例创建初始化顺序假设两个单例类AB要求A单例先创建B单例后创建B的创建依赖A。饿汉是无法实现这样的需求的如果单例对象初始化很费时间会导致程序启动慢就像卡死一样。
懒汉优点对应饿汉的两个缺点。缺点1.相对复杂尤其是还要控制线程安全的问题
现代懒汉模式的写法
class CallInfo
{
public:static CallInfo GetInstance(){static CallInfo sInst;return sInst;}int GetCallCount(){return _callCount;}void AddCallCount(int n){_callCount n;}void Push(const pairint, int kv){_v.push_back(kv);}CallInfo(const CallInfo cl) delete;
private:CallInfo():_callCount(0){cout CallInfo() endl;}
private:int _callCount; //统计次数vectorpairint, int _v; //记录区间信息}; 对于这样的写法我在GetInstance中先创建了一个局部的静态对象对于局部的静态对象只有第一个调用它的人会进行初始化后面的人都不会初始化直接返回对象并且每次都返回同一个局部的静态对象生命周期属于全局但是它的作用域只在GetInstance中。 但是这样的写法在C98中多线程调用的时候静态的对象的构造初始化并不能保证线程安全C11优化了这个问题C11中静态对象的构造初始化是线程安全的。 所以双加锁的懒汉模式是在任何环境下都安全的其次这种双加锁的好处是如果new出来的对象很大new出来的对象在堆上堆就很大如果把这个对象放在数据段上相对而言没那么好。 设计模式的扩展工厂模式观察者模式