本篇博客整理了一些常见面试题,通过诠释有部分限制条件的特殊类,更深入地探究C++中类和对象的运用。
目录
一、无法被拷贝的类
类和对象的拷贝主要跟类提供的拷贝构造和赋值重载有关,因此禁止拷贝构造函数和赋值重载就可以禁止类和对象的拷贝了。这在C++中主要有两种方案实现:
C++98提供了,将拷贝构造和赋值重载设置为私有且只声明不定义。
class CopyBan
{
public:
CopyBan()
{}
private:
//将以下两个默认成员函数设置为私有且只声明不定义
CopyBan(const CopyBan& cb); // 拷贝构造
CopyBan& operator=(const CopyBan& cb); // 赋值重载
//1.将它们设置成私有,哪怕用户在类外定义了它们,也可以禁止类外的拷贝
//2.只声明不定义,就可以让它们被调用的时候,
// 由于没有定义而产生链接错误,在编译阶段就报错,禁止类内的拷贝
};
C++11提供了扩展的关键字delete,可以让编译器禁用类中的默认成员函数。
class CopyBan
{
public:
CopyBan()
{}
private:
//关键字delete可以让编译器禁止生成拷贝构造和赋值重载
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
二、只能在堆上创建的类
正常创建对象时,一般是在栈上创建的,其间会调用相应的构造函数来初始化对象。
已知new申请的资源是从堆上申请的,那么要设计一个只能在堆上创建的类,只需为对象定制一个构造的方案,让对象只能通过new来创建。方案如下:
1、将析构函数私有化,为对象定制析构的方案,并通过new+默认构造来创建对象:
class HeapOnly
{
public:
void Destroy()
{
delete this;
}
private:
~HeapOnly()
{
//...
}
};
int main()
{
HeapOnly* hp = new HeapOnly;
hp->Destroy();
return 0;
}
2、将构造函数私有化,通过静态成员函数定制对象构造的方案,且禁止拷贝和赋值:
class HeapOnly
{
public:
static HeapOnly* CreateObj()//构造不能被直接调用,但可以被间接调用
{
return new HeapOnly;
}
//非静态成员函数都有一个默认参数this指针,它本质是一个HeapOnly对象
//但构造函数设置为私有,无法创建这样一个对象,调用函数会陷入“先有鸡还是先有蛋”的问题(非静态成员函数调用需要传HeapOnly对象,但HeapOnly对象又只能调用非静态成员函数创建)
//因为静态成员函数没有默认参数this指针,就无须先创建一个HeapOnly对象,避免了以上问题
//所以此处选用静态成员函数来定制构造方案
//调用时通过“类名::静态成员函数名”即可
private:
//1. 将类的构造函数私有,拷贝构造和赋值重载声明成私有并禁用,防止别人调用拷贝在栈上生成对象。
//2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
HeapOnly()
{
//...
}
HeapOnly(const HeapOnly& hp) = delete;
HeapOnly& operator=(const HeapOnly& hp) = delete;
};
int main()
{
HeapOnly* hp = HeapOnly::CreateObj();//HeapOnly*类型的指针hp接收new的返回值
//HeapOnly copy(*hp3); //不可拷贝
delete hp;//hp不使用后,手动delete释放(这里会调用HeapOnly的默认析构)
return 0;
}
三、只能在栈上创建的类
一般创建对象时,本就是在栈上创建的,唯有通过new才能在堆上创建对象。所以,要设计只能在栈上创建的类,只需在类中防止new创建对象。
将构造函数私有化,通过静态成员函数定制对象构造的方案,且禁用operator new:
class StackOnly
{
public:
//将构造函数私有化,通过静态成员函数定制对象构造的方案
//防止别人用new在堆上创建StackOnly对象
static StackOnly CreateObj()
{
StackOnly st;
return st;
}
private:
StackOnly()
{
//...
}
// 对一个类实现专属operator new,然后禁用它
// 防止内类new创建对象
void* operator new(size_t size) = delete;
};
int main()
{
StackOnly hp1 = StackOnly::CreateObj();
StackOnly copy(hp1);//允许通过默认拷贝构造进行拷贝
// StackOnly* hp2 = new StackOnly(hp1);//但不允许new来创建对象
// new的时候可以调构造也可以调拷贝构造
// new => operator new(默认调用全局的) + 构造
return 0;
}
四、无法被继承的类
C++中有两种方案来实现一个类无法被继承。
C++98提供了,将构造函数私有化。
//构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit //基类
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class N : public NonInherit // 生类
{};
int main()
{
//N n; //一旦实例化就会报错
return 0;
}
C++11提供了关键字final。
//final修饰类,表示该类不能被继承
class A final
{
// ....
};
class B : public A //编译时就会报错
{
// ....
};
五、只能创建一个对象的类
要设计只能创建一个对象的类,可以通过单例模式来实现。
单例模式是一种设计模式。设计模式是指,一套被反复使用、大多数人知晓的、经过分类整理的、代码设计的经验总结,可以提高代码可重用性,让代码更容易被他人理解,保证代码可靠性。设计模式可以使代码编写真正工程化,是软件工程的经络。
单例模式的作用是,可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,使该实例被所有程序模块共享。例如在某个服务器程序中,服务器的配置数据存放在一个文件,而这些配置数据由一个单例对象统一读取,服务进程中的其他对象可以通过这个单例对象来获取这些配置信息,这样就简化了在复杂环境下的配置管理。
而它又有饿汉模式和懒汉模式——两种实现模式。
【Tips】单例模式的实现要点:
- 因为全局只能有一个对象,所以需要将构造函数私有化;
- 用一个static静态指针(类的成员变量之一,在类外初始化)管理实例化的单例对象,并且提供一个静态成员函数,以获取这个static静态指针;
- 禁止拷贝,保证全局只有一个单例对象;
- 可以使用互斥锁来保证数据读取时的线程安全(本篇博客在此不涉及)。
1.饿汉模式
饿汉模式是指,在程序启动时(即main函数开始前)就实例化出单例对象。
// 饿汉模式:一开始(main函数运行之前)就创建单例对象
//优点:实现起来更简单
//缺点:
// 1、如果单例对象初始化内容很多,影响启动速度
// 2、如果两个单例类互相有依赖关系,就会发生错误(假设有A B两个单例类,要求A先创建,B再创建,B的初始化创建依赖A)
namespace hungry
{
class Singleton
{
public:
// step2、提供获取单例对象的接口函数
static Singleton& GetInstance()
{
return _sinst;
}
private:
// step1、构造函数私有
Singleton()
{
// ...
}
// step3、防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
//成员变量
map<string, string> _dict;//管理配置数据
static Singleton _sinst; //指向单例对象的指针
public:
//单例类中可以添加各种函数
void func();
void Add(const pair<string, string>& kv)
{
_dict[kv.first] = kv.second;
}
void Print()
{
for (auto& e : _dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
};
//类外初始化
Singleton Singleton::_sinst = new Singleton();;
void Singleton::func()
{
_dict["xxx"] = "1111";
}
}
2.懒汉模式
懒汉模式是指,单例对象在第一次被需要使用时才实例化。如果单例对象的构造十分耗时,或者会占用很多资源(例如加载插件、初始化网络连接、读取文件等),为了不影响程序的正常启动,可以使用懒汉模式(或称延迟加载)。
//懒汉模式
//跟饿汉模式几乎相同,唯独不能在main函数运行前创建对象
//优点:
// 1、第一次使用单例对象时才创建对象,进程启动过程中无负载
// 2、多个互相依赖的单例可以控制启动顺序(通过代码顺序)
//缺点:
// 1、实现起来更复杂
// 2、存在线程安全问题
namespace lazy
{
class Singleton
{
public:
// step2、提供获取单例对象的接口函数
static Singleton& GetInstance()
{
if (_psinst == nullptr)
{
// 第一次调用GetInstance的时候创建单例对象
_psinst = new Singleton;
}
return *_psinst;
}
//【补】单例对象释放问题:
// 一般不用释放,唯独特殊场景:
// 1、中途需要显示释放
// 2、程序结束时,需要做一些特殊动作(如持久化)
// 写法如下:
static void DelInstance()
{
if (_psinst)
{
delete _psinst;
_psinst = nullptr;
}
}
//方式1:嵌在单例类内部来析构大量对象
class GC
{
public:
~GC()
{
lazy::Singleton::DelInstance();
}
};
private:
// step1、构造函数私有
Singleton()
{
// ...
}
// step3、防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
//成员变量
map<string, string> _dict;
static Singleton* _psinst;//指向单例对象的指针(因为懒汉模式下的单例对象是new出来的)
static GC _gc; //内部类用以析构单例对象
};
//类外初始化
Singleton* Singleton::_psinst = nullptr; //指针使对象不被调用就不会被创建
Singleton::GC Singleton::_gc;
}
//方式2:写在单例类外部来析构大量对象
class GC
{
public:
~GC()
{
lazy::Singleton::DelInstance();
}
};
GC gc;