【C++ 第二十一章】特殊类的设计(学习思路)




在这里插入图片描述



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

设计思路

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

C++98 的做法

将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。(不定义:则拷贝操作无法实际的实现;设置成私有:避免公有被调用出来实现)

class A
{
private:
	A(const A&);
	A& operator=(const A&);
};

原因:

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



C++11 的做法

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

class A
{
	A(const A&) = delete;
	A& operator=(const A&) = delete;
};



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


方法一:构造函数私有化

  1. 构造、拷贝和赋值私有化:将类的构造函数私有,拷贝构造和赋值声明成私有。防止别人调用拷贝在栈上生成对象。或者将构造私有化,将拷贝和赋值 delete 禁用
  2. 同一提供对外接口:提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

这个有点封装的味道了,将一些功能封装起来,自己提供对外接口,就可以控制外界可以使用的功能(控制权限)

为什么要设置成静态函数:

关于为什么对外功能接口要设置成静态函数?

思路:

1、首先,我们将构造函数私有化,拷贝构造和赋值 delete 禁用掉,外界就不可以调用这几个函数在栈上构造一个对象
2、其次,该对外功能接口函数 也是成员函数,调用一个成员函数需要一个对象来调用,但是我们这里都没有创建对象,正等着该功能函数来创建对象呢?何来一个对象?
3、这里就产生:先有鸡,还是先有蛋的问题
4、因此就需要避免使用通过对象调用的方式
可以设置成 静态成员,在外部通过类域指定调用(这时静态成员的特性,就无需通过对象调用)

class HeapOnly
{
public:
	// 设置成静态函数:
	static HeapOnly* CreateObj() {
		return new HeapOnly;
	}

	// 将拷贝与赋值重载都使用 delete 禁用掉
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly& operator=(const HeapOnly&) = delete;
private:
	// 将构造函数设置成私有:避免外部调用,在栈上构造对象
	HeapOnly() {};
};

int main() {
	HeapOnly* p = HeapOnly::CreateObj();  // 通过类域调用类的静态成员

	return 0;
}



方法二:析构函数私有化

注释都解释清楚了

// 析构函数私有化
class HeapOnly
{
public:
	void Destroy() {
		delete this;
	}
private:
	// 将析构函数设置成私有
	~HeapOnly() {};
};

int main() {
	HeapOnly obj;  // 报错:创建一个类对象,销毁时会自动调用析构,但是这里调用不了(因为析构函数被"禁用"了),因此也不允许这个对象被创建出来
	HeapOnly* p = new HeapOnly();  // 不报错:new 出来的对象,不会自动调用析构,需要手动 delete
	delete p;  //报错:调用不了析构 
	p->Destroy(); // 通过类中的功能接口销毁对象
	return 0;
}



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

这个比限制只能在堆上,还要麻烦一些些

实现第一步:构造函数私有化

class StackOnly
{
public:
	// 对外功能接口:该函数可以调用私有成员,构造函数,创建对象,返回匿名对象
	static StackOnly CreateObj() {
		return StackOnly();
	}

private:
	// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象
	StackOnly() {};
};




int main() {
	StackOnly obj1 = StackOnly::CreateObj();
	StackOnly* p_obj2 = new StackOnly(obj1); 
	
	return 0;
}

禁掉了 构造函数,还可以走拷贝构造的路:通过CreateObj() 函数,先创建一个对象出来,再 拷贝+new 生成一个新对象


因此,还要完善


实现第二步:拷贝与赋值用 delete 禁用掉

class StackOnly
{
public:
	// 对外功能接口:该函数可以调用私有成员,构造函数,创建对象,返回匿名对象
	static StackOnly CreateObj() {
		return StackOnly();
	}

	// 将拷贝与赋值重载都使用 delete 禁用掉
	StackOnly(const StackOnly&) = delete;
private:
	// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象
	StackOnly() {};
};




int main() {
	StackOnly obj1 = StackOnly::CreateObj();
	//StackOnly* p_obj2 = new StackOnly(obj1); 
	
	return 0;
}




但是!

将 拷贝构造禁用掉:会导致 CreateObj 函数失效,因为该成员函数返回的是局部对象,需要拷贝生成临时对象,而拷贝构造失效,导致生成失败

因此,还要完善


实现第三步:operator new 用 delete 禁用掉

可以尝试从 new 的本质入手:new = 全局函数 operator new(malloc+抛异常)+ 构造

我们若自己显式实现 operator new ,则 new 优先使用我们自己的

因此可以在这里将 new 的 operator new 禁掉,使得 new 无法调用


// 只能在栈上创建对象
class StackOnly
{
public:
	// 该函数可以调用私有成员:构造函数,创建对象,返回匿名对象
	static StackOnly CreateObj() {
		return StackOnly();
	}
	
	// 可以使用
	/*void* operator new(size_t size) {
		return malloc(size*sizeof(StackOnly));
	}*/
	void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
private:
	// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象
	StackOnly() {};
};




int main() {
	StackOnly obj1 = StackOnly::CreateObj();
	StackOnly* p_obj2 = new StackOnly(obj1);  // 报错
	
	return 0;
}




但是问题又回来了:

只要没有将 拷贝构造禁用掉,还是可以通过拷贝构造创建一个在静态区的对象

StackOnly obj1 = StackOnly::CreateObj();

static StackOnly obj3(obj1); // 不报错



同时,还可以通过 移动构造 创建静态区的对象

StackOnly obj1 = StackOnly::CreateObj();

static StackOnly obj4(move(obj1));

这里为什么创建静态区的对象? 仅仅是将栈区对象区别开


实现最终大法:直接使用返回对象进行操作

既然我们的目的是设计一个只能在栈上对象的类,
我们直接从这里思考,我们先将拷贝、赋值、移动构造私有化 或 delete 禁用。
既然直接将 CreateObj() 函数返回的匿名对象拷贝给新对象会触发拷贝或移动构造

StackOnly obj1 = StackOnly::CreateObj(); // 这里会触发拷贝或移动构造

干脆别拷贝给新对象,而是直接使用这个 匿名对象进行操作
(其实这个方法有点取巧,但是不也是达到了题目要求吗?😎)

int main() {
	StackOnly::CreateObj().Print();  // 直接使用该返回对象进行操作

	//StackOnly obj1 = StackOnly::CreateObj();  // 会触发拷贝或移动构造
	//StackOnly* p_obj2 = new StackOnly(obj1);  // 报错:operator new 和 拷贝构造 不能用了
	//StackOnly obj3(obj1); // 报错:拷贝构造 不能用了
	//static StackOnly obj4(move(*obj1)); // 报错:拷贝构造 和 移动构造 不能用了
	//static StackOnly obj2(obj1);
	return 0;
}



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

C++98 方式

C++98 中构造函数私有化,派生类中调不到基类的构造函数,则无法继承

class A
{
private:
	A() {};
};



C++11 方式

使用 final 关键字
final 关键字,final修饰类,表示该类不能被继承。

class A final
{
private:
	A() {};
};



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

5.1 设计模式:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

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

我们之前其实已经接触过一些设计模式了,比如迭代器模式、适配器/配接器模式
下面我们要学习的是设计模式中的 单例模式

5.2 单例模式:

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:饿汉模式 和 懒汉模式

5.3 饿汉模式

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。(即程序一开始就实例化一个该类对象给你了)

为什么叫做饿汉?:饿汉就好比你放学饿着肚子回家,在你回家前妈妈就已经准备好饭菜给你了

设计思路:
1、构造函数私有化
2、将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象。
3、创建一个自己这个类的静态成员对象:一个类的静态成员只能创建一个,而且static数据会在程序启动时创建好(刚好符合 饿汉模式 的理念)

// 单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
class InfoMgr
{
public:
	// 只有这个函数可以向外提供唯一一个实例化对象
	static InfoMgr& GetInstance() {
		return _ins;
	}
	void Print() {
		cout << _ip << '\n';
		cout << _port << '\n';
		cout << _buffSize << '\n';
	}


	// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr(InfoMgr&&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;
private:
	// 将构造函数私有化:外部无法直接构造该类对象
	InfoMgr() {
		cout << "InfoMgr()" << '\n';
	}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;

	// 这不能说是在类中创建一个自己,否则就套娃乱透了
	// 静态成员不存储在一个类对象里面
	static InfoMgr _ins;
};
InfoMgr InfoMgr::_ins;

int main() {
	// 调试程序可以发现:调试还没有开始走就已经打印 "InfoMgr()" ,说明 在main函数程序执行前,对象就已经构造好了(这是因为该对象是 static,全局域)
	InfoMgr::GetInstance().Print();
	return 0;
}

饿汉模式的缺陷

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

由于饿汉模式的对象在 main 函数前就被创建,所以它不存在线程安全问题,但是它也存在一些缺点:

1、多个饿汉模式的单例,某个对象初始化内容较多(读文件),会导致程序启动慢


2、A 和 B 两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证其初始化顺序


5.4 懒汉模式

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


饿汉模式的特点就是先创建好对象,这也容易引发一些问题

懒汉模式 可以解决这个问题:不先创建对象,而是需要时再创建对象

这样就可以按需创建,即你要吃的东西,我不提前给你准备好,只会在你需要吃时再做,这就是懒汉

懒汉模式写法一:定义类对象指针

在 main 函数中,程序一般都会按顺序执行(不像懒汉模式中全局变量执行顺序不定),而且按需调用即可,这样也可以解决 懒汉模式中的 依赖关系的先后问题

// 懒汉模式
class InfoMgr
{
public:
	// 若对象指针为 nullptr,就给你 new 一个对象
	// 若不为空,就返回该对象给你
	static InfoMgr& GetInstance() {
		if (_pIns == nullptr) {
			_pIns = new InfoMgr();
		}
		return *_pIns;
	}
	void Print() {
		cout << _ip << '\n';
		cout << _port << '\n';
		cout << _buffSize << '\n';
	}


	// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr(InfoMgr&&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;
private:
	// 将构造函数私有化:外部无法直接构造该类对象
	InfoMgr() {
		cout << "InfoMgr()" << '\n';
	}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;

	// 这不能说是在类中创建一个自己,否则就套娃乱透了
	// 静态成员不存储在一个类对象里面
	static InfoMgr* _pIns;
};
InfoMgr* InfoMgr::_pIns;


int main() {
	// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的
	InfoMgr::GetInstance().Print();
	return 0;
}




有没有发现 懒汉模式存在一个问题:该模式中的对象是 new 出来的,就需要手动 delete 释放

而我们上面的类中,默认的析构只会将 _pIns 这个指针置空,而不会 delete 指向的资源,相当于 ”浅析构“,会造成内存泄漏


实际上,只有单例对象内存泄漏问题并没有这么严重

如果想要delete,这里有个很好的方法:定义内部类对象,当本项目程序结束后,该对象销毁会调用自己的析构函数,我们就可以在析构函数里面设置 delete 相关程序

这其实是一种解决问题的 思想:自己类无法做到的事,可以定义内部类,利用类的特性间接完成一些功能


// 定义一个内部类:用于析构单例对象
class DestroyIns
{
    public:
    ~DestroyIns() {
        if (InfoMgr::_pIns != nullptr) {
            delete InfoMgr::_pIns;
            cout << "delete InfoMgr::_pIns;" << '\n';
        }
    }
};


InfoMgr::DestroyIns desIns;  // 全局对象:程序结束后会销毁,自动调用析构函数,则会执行析构函数里面 delete 的程序

应用进去

// 懒汉模式
class InfoMgr
{
public:
	// 若对象指针为 nullptr,就给你 new 一个对象
	// 若不为空,就返回该对象给你
	static InfoMgr& GetInstance() {
		if (_pIns == nullptr) {
			_pIns = new InfoMgr();
		}
		return *_pIns;
	}
	void Print() {
		cout << _ip << '\n';
		cout << _port << '\n';
		cout << _buffSize << '\n';
	}


	// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr(InfoMgr&&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;


	// 定义一个内部类:用于析构单例对象
	class DestroyIns
	{
	public:
		~DestroyIns() {
			if (InfoMgr::_pIns != nullptr) {
				delete InfoMgr::_pIns;
				cout << "delete InfoMgr::_pIns;" << '\n';
			}
		}
	};

private:
	// 将构造函数私有化:外部无法直接构造该类对象
	InfoMgr() {
		cout << "InfoMgr()" << '\n';
	}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;

	// 这不能说是在类中创建一个自己,否则就套娃乱透了
	// 静态成员不存储在一个类对象里面
	static InfoMgr* _pIns;
};
InfoMgr* InfoMgr::_pIns;
InfoMgr::DestroyIns desIns;

int main() {
	// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的
	InfoMgr::GetInstance().Print();
	return 0;
}



懒汉模式二:利用 局部静态变量(推荐写这个)


局部静态变量

函数内的静态变量也称为局部静态变量,其作用域只限于函数内部,别的函数不能访问。

局部静态变量存储在全局数据区,只允许初始化一次,但它的生命周期和全局变量一样,自它们被定义时就一直存在,直到程序结束时才会被销毁。不会随着函数的结束而被销毁,会一直存在


特性:只允许初始化一次

作用域:在函数内部

存储区:全局静态区

生命周期:全局,不会随着函数的结束而被销毁,程序结束时才会被销毁



由于局部静态变量的特性,也可以达到 第一次调用 GetInstance() 函数,就定义一个类对象,其他时候调用不会重新定义,只允许定义一次 的目的

同时,程序结束时会该对象也会自动销毁,不用再定义内部类对齐处理了!!



这个写法简单明了,相比前一种写法更加巧妙

// 懒汉模式二:利用 局部静态变量
class InfoMgr
{
public:
	// 若对象指针为 nullptr,就给你 new 一个对象
	// 若不为空,就返回该对象给你
	static InfoMgr& GetInstance() {
		static InfoMgr pIns;
		return pIns;
	}
	void Print() {
		cout << _ip << '\n';
		cout << _port << '\n';
		cout << _buffSize << '\n';
	}


	// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr(InfoMgr&&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;


private:
	// 将构造函数私有化:外部无法直接构造该类对象
	InfoMgr() {
		cout << "InfoMgr()" << '\n';
	}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;
};


int main() {
	// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的
	InfoMgr::GetInstance().Print();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值