对象的使用
①、对于特定类型的全体对象而言,有时候可能需要访问一个全局的变量。比如说统计某种类型对象已创建的数量。
②、如果我们用全局变量会破坏数据的封装,一般的用户代码都可以修改这个全局变量,这时我们可以用类的静态成员来解决这个问题。
③、非static数据成员存在于类类型的每个对象中,static数据成员独立该类的任意对象存在,它是与类关联的对象,不与类对象关联。
下面用代码来使用static成员实现统计对象已创建的数量:
接着定一个静态变量:
编译运行:
这是为啥呢?
而对于静态成员还需要有定义性声明,它需要在文件作用域上进行声明,如下:
编译运行:
为啥还是出错呢?这是由于静态定义性说明就不用static关键字来修饰了:
再编译运行:
由于没对count_静态成员变量初始化,现在想对它进行初始化100,那修改代码如下:
编译运行:
从错误提示可以知道,变量的初始化应该在文件作用域中进行,将上面的代码先还原,修改代码如下:
编译运行:
而回到这个例子,由于是统计对象个数,则初始化还是还原成0,然后在对象创建的时候+1,销毁对象时候-1,如下:
接下来测试一下代码:
编译运行:
④、static成员优点
- static成员的名字是在类的作用域中,因此可以避免与其它类成员或全局对象名字冲突。
- 可以实施封装,static成员可以是私有的,而全局对象不可以。
将它改为私有,修改代码如下:
编译运行:
这时则需要向外暴露一个可以访问它的方法,如下:
其输出结果一样。 - 阅读程序容易看出static成员与某个类相关联,这种可见性可以清晰地反映程序员的意图。
⑤、static成员的定义:static成员需要在类定义体外进行初始化与定义
⑥、特殊的整型static const成员:整型static const成员可以在类定义体中初始化,该成员可以不在类体外进行定义
编译运行:
当然也可以在类外部进行声明:
编译运行:
类体中和类外中只能对其初始化一次,不能同时都初始化,所以应该这样改:
编译运行:
如果换成double呢?
编译运行:
【注意】:这种语法在VC6中是不允许的。
①、static成员函数没有this指针。
②、非静态成员函数可以访问静态成员。
③、静态成员函数不可以访问非静态成员。
用代码来说明下:
编译:
而出现这个错误的原因就是没有隐含的this指针指向某个对象,而y_是属于某个对象的,所以就无法访问。
编译运行:
【面试时可能会问到它】
①、类大小计算遵循前面学过的结构体对齐原则。
②、类的大小与数据成员有关与成员函数无关。
③、类的大小与静态数据成员无关。
④、虚函数对类的大小的影响。【关于虚函数后面会学到,会占4个字节,增加一个虚表指针,先做个了解】
⑤、虚继承对类的大小的影响。
下面用代码来试下:
编译运行:
关于对象的作用域与生存期实际上在之前已经学过了,这里总结复习一下。
隐含调用构造函数(程序中没有显示调用)。
代码如下:
①、 用于函数内部修饰变量,即函数内的静态变量。这种变量的生存期长于该函数,使得函数具有一定的“状态”。使用静态变量的函数一般是不可重入的,也不是线程安全的,比如strtok(3)。
如代码:
如果函数执行完了,这时n的值还是100,第二次再进这个函数时,还是100,该函数被称为不可重录函数,也不是线程安全的函数,一般这种函数不会用在信号处理中用到,这个在Linux编程中有介绍过。
②、 用在文件级别(函数体之外),修饰变量或函数,表示该变量或函数只在本文件可见,其他文件看不到也访问不到该变量或函数。专业的说法叫“具有internal linkage”(简言之:不暴露给别的translation unit)。
所以要定义全局变量,就应该在.c文件中定义,千万不能在.h头文件中定义:
C语言的这两种用法很明确,一般也不容易混淆。
③、由于C++引入了类,在保持与C语言兼容的同时,static关键字又有了两种新用法:
1.用于修饰类的数据成员,即所谓“静态成员”。这种数据成员的生存期大于class的对象(实例/instance)。静态数据成员是每个class有一份,普通数据成员是每个instance 有一份。
2、用于修饰class的成员函数,即所谓“静态成员函数”。这种成员函数只能访问静态成员和其他静态程员函数,不能访问非静态成员和非静态成员函数。
关于什么是单例模式这里就不多介绍了,在平常工作中会大量用到,比如你如果用过java,这里简单列一下它的定义:
- 保证一个类只有一个实例,并提供一个全局访问点。
-
禁止拷贝
编译运行:
上面就实现了一个单例模式,但是还是有一些缺陷的,下面来改一下代码:
编译运行:
这是第一个缺陷,紧接着还存在一个问题,如下:
编译:
那不让其拷贝很简单,如下:
这时再编译:
接着就要解决析构不能被调用的问题,进一步改造单例模式:
#include <iostream> using namespace std; class Singleton { public: static Singleton* getInstance() { if(instance_ == NULL)//做一层判断,保证调用此方法永远返回同一个实例 instance_ = new Singleton(); return instance_; } ~Singleton() { cout<<"~Singleton ..."<<endl; } static void free() { if(instance_ != NULL) delete instance_; } private: Singleton(const Singleton& other); Singleton& operator=(const Singleton& other); Singleton(){//将构造函数声明成私有的,防止外部进行实例化 cout<<"Singleton ..."<<endl; } static Singleton* instance_; }; Singleton* Singleton::instance_; int main(void) { Singleton* s1 = Singleton::getInstance(); Singleton* s2 = Singleton::getInstance(); //Singleton s3(*s1); //调用拷贝构造函数,而会生成一个s1对象,目前这种写法是可以允许的,也就不是单例了 //Singleton s3 = *s2; //不允许调用=号运算符来赋值 Singleton::free(); //在需要的时候手动调用释放代码 return 0; }
编译运行:
成功解决,但是:如果一个程序有很多地方都使用了单例的对象,到底哪里进行手动释放我们也不太好确定,所以这种方法不是很可取,我们应该让它自动释放,在这个单例对象生命周期限结束之后自动释放,解决方案是:使用内嵌类来解决,具体代码如下:
#include <iostream> using namespace std; class Singleton { public: static Singleton* getInstance() { if(instance_ == NULL)//做一层判断,保证调用此方法永远返回同一个实例 instance_ = new Singleton(); return instance_; } ~Singleton() { cout<<"~Singleton ..."<<endl; } static void free() { if(instance_ != NULL) delete instance_; } class Garbo { public: ~Garbo() { if(Singleton::instance_ != NULL) delete instance_; } }; private: Singleton(const Singleton& other); Singleton& operator=(const Singleton& other); Singleton(){//将构造函数声明成私有的,防止外部进行实例化 cout<<"Singleton ..."<<endl; } static Singleton* instance_; static Garbo garbo_; //利用对象的确认性析构,当对象的生命周期结束之后会主动调用析构来达到目的 }; Singleton* Singleton::instance_; Singleton::Garbo Singleton::garbo_; int main(void) { Singleton* s1 = Singleton::getInstance(); Singleton* s2 = Singleton::getInstance(); //Singleton s3(*s1); //调用拷贝构造函数,而会生成一个s1对象,目前这种写法是可以允许的,也就不是单例了 //Singleton s3 = *s2; //不允许调用=号运算符来赋值 //Singleton::free(); return 0; }
编译运行:
可以看到利用这种确认性析构自动调用的析构方法,其实单例还有另外一种实现方式,如下:
利用静态对象的特征来实现单例模式,这是最简单的一种实现方式,编译运行:
再次编译运行:
但是这是线程不安全的单例模式,关于怎么实现线程安全的单例模式,在之后会学习到。
①、const成员函数不会修改对象的状态。
②、const成员函数只能访问数据成员的值,而不能修改它。
如果在这个函数中对x_成员变量进行修改,则会报错:
①、如果把一个对象指定为const,就是告诉编译器不要修改它。
②、const对象的定义:const 类名 对象名(参数表);
③、const对象不能调用非const成员函数
编译:
编译运行:
用mutable修饰的数据成员即使在const对象或在const成员函数中都可以被修改。
如下面这个场景:
#include <iostream> using namespace std; class Test { public: Test(int x):x_(x), outputTimes_(0) { } int getX() const{ //x_ = 100; ERROR,const成员函数不能修改成员 cout<<"const getX..."<<endl; return x_; } int getX(){ cout<<"getX..."<<endl; return x_; } void output() const {//在对象输出时,要对次数成员变量进行+1,以便统计对象输出的次数 cout<<"x="<<x_<<endl; outputTimes_++; } int getOutPutTimes() const{//打印总输出次数 return outputTimes_; } private: int x_; int outputTimes_;//统计对象被输出的次数 }; int main(void) { const Test t(10); t.getX(); Test t2(20); t2.getX(); return 0; }
编译运行:
那怎么解决这个冲突呢?当然就是用mutable关键字来解决喽,如下:
再次编译:
可见冲突解决了,下面编写测试代码:
运行:
最后再对const进行一个总结,已经学习过了很多相关的用法:
第一种用法:定义常量:
比如:const int n = 100;
const Test t(10);
第二种用法:const引用
比如:const int& ref = n,但是不能这样写:int& ref = n;
第三种用法:const与指针
放在*号左边,如下:
const int* p;//const出现在*号左边,表示*p是常量,*p=200是错的。
放在*号右边,如下:
int * const p2;//const 出现在*号右边,表示p2是常量,p2=&n2则是错的。
const int* const p3 = &n3;//*p3既是常量,p3也是常量。
另外在类中,如果有const成员,const成员的初始化只能在构造函数初始化列表中进行。
第四种用法:const成员函数【本节所学】
表示成员函数不能修改对象状态,也就是只能访问数据成员,但是不能修改数据成员。