当前课程知识点:基于Linux的C++ > 第九讲 类与对象 > 9.7 类与对象的成员(二) > LinuxCPP0907
静态的数据成员和静态的成员函数
非常非常有用 在我们的编程中
频繁地需要设计一个很特殊的程序功能
就是什么呢
存在着某一个类的单一的共享对象
这样的一个共享对象 它是全局的
在整个程序中 它只能存在一个
不会有很多个
并被整个程序中的所有的模块所共享
这样的一个东西 我们称它为是一个单子
英语的名字叫singleton
那么我们的编程呢
就必须能够保证这样的一个单子:
(1)全局就存在一个
(2)在全局的任何一个地方都能够访问到它
也就是说 我们一定要有一个
全局的访问策略
保证在程序代码的任何地方
只要需要就能够访问到它
操纵的那唯一一个数据对象
这个数据对象就叫单子
单子模式呢 有很多种实现策略
我们来看一个实现策略
这个程序代码虽然代码量很短
但实际上相当复杂
我们定义类“classSingleton{};”
我里面定义一些公开的成员函数、
私有的成员函数和私有的数据
我们定义的这个单子模式里面
因为要求这样的数据对象
全局只能构造一个唯一的副本
这就意味着它的构造函数不能公开
你一公开构造函数
就意味着程序中任何一个地方
都可以构造它
你就没有办法保证这个单子对象的唯一性
所以我要将这个单子的构造函数
全部定义为私有的
缺省构造函数定义成私有的
拷贝构造函数要弄成私有的
赋值的这个重载的构造函数
也要弄成私有的 然后析构函数
我们也要把它定义成私有的
我也不允许你析构我们这样的一个对象
私有的构造函数和私有的析构函数
这就意味着我们没有办法
在这个类的外部构造和销毁这个对象
在这个类的实现里面
有两个地方比较特殊的就是
我们在Singleton的拷贝构造函数里面
还有Singleton的赋值操作符重载的构造里面
后面都带着分号 我们声明了它
但是我们在源代码里边并没有实现它
只声明而不实现
这就意味着这样的拷贝函数
不仅外界是不可以调用的
就是我们自己也是不会调用的
因为没有它的实现代码
你一调用编译器就报错
这实际上意味着我们通过这种手段
保证我们这个单子的模式
它是一个不可拷贝对象
这个语义是非常重要的
因为如果你允许这个对象可以被拷贝
那就意味着这个单子就不再单了
只定义而不实现
它的拷贝构造函数和赋值构造函数
就能够让这个单子对象不可拷贝
从某种程度上来讲
也保证我们的单子模式的唯一性
接下来 我们要保证
怎么真正地构造这个对象
你没有公开的构造函数
外界就不能够被调用
那我们这个对象什么时候被构造出来呢
因为单子嘛 它又不是无子 对不对
它总归还要有一个子
那么就提供一个公开的Get函数
用这个Get函数来构造这个对象
因为这个Get函数是我们类的一个成员函数
当我们调用这个公开的Get函数的时候
那么它就会执行它的内部代码
而这个内部代码又在这个类的内部
所以它可以调用这个类的私有的
构造函数去替我们构造它
那么我们在这里面
就可以用一个new操作符调用私有的构造函数
去构造我们的这个对象
然后我们返回它构造出来的这个对象
构造出来的这个对象保存在什么地方
这是一个需要特别注意的一个问题
因为我们时刻要记录构造的对象
分配的内存地址在哪里
这个指针必须要保存
所以我们要在这个类Singleton里面
定义一个私有的数据字段
是一个指向Singleton的指针
来保存我们构造出来的这个类的
单子对象到底存在哪里
因为这个类中保存的单子就一份
所以这个指针_s应该只有一份
前面要加上static关键字
保证_s是指向本类唯一的对象的
那一个唯一的指针
我们还写了一个私有的数据成员inta
来作为我们的验证数据
同时我们写了一个函数intGetData()
然后return++a
来返回我们的验证数据看看对不对
这个就是我们class Singleton的
类的全部的定义
在这里面我仅仅是
写了一个析构函数的声明
所以我没有销毁在构造函数中分配的
那个指针所指向的目标数据对象的内存
当程序结束的时候
绝大多数的操作系统
都会自动地释放动态分配的内存
我们可以不实现它 大部分时候都没问题
但是偶尔 真地没有析构函数
它确实是有可能导致问题的
因为我们这里面毕竟有一个指针
指向一个动态分配的内存
因为这个静态成员
是从属于一个特定的类的
而不从属于那个类的某一个对象
所以要想解析这个静态数据成员
那么前面必须加上类名 Singleton::_s
我要把它初始化成NULL
那么我们怎么使用它呢
我们将以Singleton::Get这个函数调用
来获取我们的那个单子对象
获取过来的这个对象
就是Get替我们构造的
当然了 它是在需要的时候才构造
回过头来我们来看Get这个成员函数的实现
这两条语句说明什么呢
我写在内联函数里面了
说明当我们调用这个Get成员函数
来获取这个单子的时候
如果这个单子还不存在
那么Get就会给我们构造一个单子出来
然后返回它
而如果这个单子已经存在了
那么就直接返回那个单子
就给你 就OK了
所以我们在程序中访问这个单子的唯一方式
就是Singleton::Get
因为它是静态的成员函数
所以解析的时候仍然是用类名加“::”
我们得到这个指针之后
就可以引领这个指针
所指向的那个目标对象
就是我们的Singleton的一个对象
然后调用它的GetData
返回它对应的实际的那个验证数据的值
当然是递增以后返回
返回递增以后的那个验证数据的值
这个就是我们的单子模式的第一版
没有析构
没有析构 当然是有可能导致问题的
那么我们就给它弄上一个析构的函数
看看我们这样的一个实现
还是classSingleton的定义
然后是一个静态的
返回本类一个指针的Get函数
当这个单子对象不存在的时候
我就构造它 如果存在就返回
GetData也一样 Singleton的构造也一样
无参数的构造函数被我们实现了
拷贝构造和赋值构造我们都只声明不实现
然后我们给它实现一个析构的函数
我就按照这个方式去析构:if(Singleton::_s)
如果存在这样一个对象
那么我们就销毁它 我们delete
然后Singleton::_s赋值为NULL
你说我就按照这个方式在我的Singleton
这个析构函数里面实现它 行不行呢
跟同学们说 这个实现是错的
首先一点 你把析构函数实现为private
在外部是不能够调用的
第二个 即便将这个析构函数
改成public访问控制的 它也是不对的
delete这个操作符本身
是需要调用这个析构函数的
你不是delete Singleton::_s吗
Singleton::_s是什么呀
它是指向Singleton的一个指针哪
要销毁这个_s所指向的那个目标对象
目标对象就是Singleton
所以你就必须调用这个Singleton析构函数
才能够去销毁它嘛
所以你在这个类的析构函数里边
调用这个类的析构函数本身是不可以的
另外 非静态的函数
不能够释放我们的静态的数据成员的
否则在某些Linux系统下面
它会导致系统崩溃
所以这个实现肯定是不对的
那我们又需要这个析构
那这刚才那个实现 它又不对
那我们怎么办呢 那我们就有一个方案
我希望这个析构函数它能够正确地被调用
我不是说我不实现它
但是我需要它在某一个
恰当的时机能够被调用
那么什么样一个时机被调用呢
就是程序完全结束之前
所有的动态分配的静态量
都应该销毁它们指向的目标数据对象
那么我们就想了一个很巧妙的一个技巧
使用到了嵌套类的概念
如果我在一个类的内部还定义了另外一个类
那么这样的一个类就从属于这个类
这个叫嵌套类
你看我们在Singleton里面的private字段里面
定义了一个新的类 叫Destroyer(毁灭者)
我定义了一个毁灭者类
那么这个类就是从属于Singleton这个类的
你要解析它的时候只能Singleton::Destroyer
如果再解析Destroyer的成员函数或数据成员
就只能Singleton::Destroyer
“::”它的成员 比如说~Destroyer
你可以按照这样一个方式来解析它
这就叫嵌套类 我们这个嵌套类里面
就实现了一个函数——析构函数
if(Singleton::_s )
{ delete Singleton::_s, Singleton::_s = NULL; }
就销毁它 这个析构函数就干这一件事情
所以类 Destroyer本身
它的唯一目的就是为了释放单子
我在这个classSingleton单子类里面
添加一个新的静态成员staticDestroyer_d
这样的一个静态成员
它将在程序结束的时候
由操作系统负责替我们销毁
在销毁它的时候
操作系统就会调用这个_d
这个静态成员的析构函数
而这个析构函数
就会替我们销毁_s所指向的
那个静态的Singleton那个单子对象
这个过程是自动的
可以按照像这样的一个模式
完成我们单子的析构
但是这个析构 它也是有一些问题的
一个 这个销毁只能在程序结束的时候进行
因为它是自动进行的
什么时候_d需要被销毁了 它才能够销毁
所以它在程序结束的时候
才会销毁我们的单子
销毁的时机我们无从选择
想提前销毁不可能
第二个 有些编译器在程序结束的时候
因为优化的目的
所有的资源都会直接返还给操作系统
也就是说 这样对象
你不销毁其实问题也不大
所以它可能压根就不销毁_d
它不销毁_d
那么就不会销毁_s所指向的singleton
如果你的单子模式中
它的析构函数里面写了一些特殊的代码
那么这个类可能会有问题
假设我们这个classSingleton
它是一个实际应用的类
它会有自己其它附加的信息
专门做某些特定的事情的
那么这样一个析构函数
因为要被外界调用
所以我需要把它变成public 同时呢
你比如讲我这个类里面有一些数据
需要在这个单子对象被销毁的时候
完成它的数据持久化
那么如果我把那段代码
写在了Singleton的析构函数里
要确保这个Singleton的这个析构函数
能够被调用 刚才不讲了吗
有些编译器 它最后程序都结束了
你所有动态分配的量不管三七二十一
全返还给操作系统算了
根本就不用管 所以这个析构函数
它压根就不调用 没关系
操作系统一股脑全收回了
根本不管这个单一的一个对象
要不要销毁 要不要释放的问题
它不释放_s
Singleton这个析构函数就不会被调用
它不会被调用
你的数据持久化代码如果写在析构函数里
它就不会被执行
你的程序语义就会有问题
所以这个析构的方式是正确的
但是在某些特殊的情况下边
我们的程序可能不像
我们所期望的那么工作
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口