一、静态成员
1、什么是静态成员
被static修饰的成员变量和成员函数就叫静态成员
2、普通成员的特点
成员变量:每个类对象中都有一份属于自己的成员变量,相互之间没有关联、独立的
成员函数:隐藏着一个this指针,接收调用者地址用于区分调用者
3、静态成员的特点
静态成员变量:
①存储在data或者bss内存段中,一个类中的所有静态成员变量只有唯一一份,被所有类对象共享
②一旦成员变量被声明为静态成员变量后,通过sizeof计算类字节数时,就不统计该变量的字节数
③静态成员变量必须在类内加static声明,需要在类外单独定义、初始化
④静态成员变量的生命周期不再依赖于某个对象,伴随整个程序生命周期
⑤因为静态成员变量的存在不依赖于任何对象,所以可以无需实例化类对象,直接访问静态成员变量
对象名.成员变量/静态成员变量
对象名->成员变量/静态成员变量
类名::静态成员变量 (无需实例化对象,但是一般成员变量都是私有的)
静态成员函数:
①没有隐藏的this指针,所以在静态成员函数中无法直接访问普通成员变量、普通成员函数,但是手动传递对象的地址间接访问,毕竟静态成员函数还是属于类的
②但是可以直接访问静态成员变量、静态成员函数
③调用方式
对象名.成员函数()/静态成员函数()
对象名->成员函数()/静态成员函数()
类名::静态成员函数() (无需实例化对象)
4、静态成员的作用
①静态成员相当于多了一层类作用域的全局变量、全局函数
②静态成员变量适合存储所有类对象的公共属性,从而节约资源(税率,利率等)
③静态成员函数可以当做访问私有的静态成员变量的公开接口,一方面不破坏类的封装性,另一方面可以无需实例化对象也能调用类的静态成员,让类本身具有管理自己成员的能力
二、单例模式
1、什么是单例模式
只能实例化一个类对象
2、什么场景下使用单例模式
进程管理器、日志管理器
网站的访问计数器、应用配置程序
线程池、服务器的连接管理器
3、实现单例模式的原理
①禁止在类外随意实例化对象,把构造函数\拷贝构造都私有化
②确保类对象只有一份,在类中定义一个静态的类对象成员变量
③提供一个获取静态类对象成员变量的公开的接口,设计静态成员函数用于获取那唯一的一个静态类对象
饿汉模式的单例
程序运行开始时就立即实例化单例类对象,不管后期是否用得到都会完成实例化
优点:不可能被多个线程同时运创建多份
缺点:如果后期使用不到,就浪费时间、资源
#include <iostream>
using namespace std;
class Single
{
static Single obj;
Single(void)
{
cout << "我是构造函数" << endl;
}
Single(const Single& that)
{
cout << "我是拷贝构造" << endl;
}
public:
static Single& get_obj(void)
{
return obj;
}
void show(void)
{
cout << &obj << endl;
}
};
Single Single::obj;
int main(int argc,const char* argv[])
{
// Single::obj;
Single& s = Single::get_obj();
s.show();
Single& s1 = Single::get_obj();
s1.show();
}
懒汉模式的单例
什么时候使用,什么时候才去实例化
优点:使用时才会创建,节约时间、资源
缺点:此时可能会被多个线程同时实例化,有可能会创建出多个单例类对象(线程不安全)
#include <iostream>
#include <pthread.h>
using namespace std;
class Single
{
static Single* obj;
Single(void)
{
cout << "我是构造函数" << endl;
}
Single(const Single& that)
{
cout << "我是拷贝构造" << endl;
}
public:
static Single& get_obj(void)
{
if(NULL == obj)
{
obj = new Single;
}
return *obj;
}
void show(void)
{
cout << obj << endl;
}
};
Single* Single::obj;
void* run(void* arg)
{
Single& s = Single::get_obj();
}
int main(int argc,const char* argv[])
{
pthread_t tid[20];
for(int i=0; i<20; i++)
{
pthread_create(&tid[i],NULL,run,NULL);
}
for(int i=0; i<20; i++)
{
pthread_join(tid[i],NULL);
}
}
三、运算符函数
在C++中会把运算符当做函数处理,一个表达式 ,其实可能调用了很多运算符函数来完成计算,这种特性对内建类型没有用,但是对于自建类型而言,通过设计运算符函数能够进行个性化运算,以此提高代码的可读性、易用性,例如string类
string str;
str += "hehe"; // strcat(str, "hehe");
str == str1; // strcmp(str, str1);
运算符函数的格式:#表示运算符 O表示运算符对象
1、单目运算符: #O O#
成员函数:
[] O::operator#(void)
{
}
返回值不确定,唯一的参数就是调用者本身
全局函数:
[] operator#(O& o)
{
}
某种运算符成员函数、全局函数只能同时实现一个,不能一起实现
2、双目运算符: a # b
注意:左操作数是运算符函数的发起者
成员函数:
[] A::operator#(B& b)
{
}
全局函数:
[] operator#(A& a, B& b)
{
}
设计一个坐标类
Point
{
int x;
int y;
}
Point p1(1,1),p2(2,2);
p1+p2;
p1-p2;
四、运算类的双目运算符 O是类名
成员函数:a + b
const O O::operator+(const O& b)const
{
return O(x+b.x, y+b.y);
}
全局函数:
const O operator+(const O& a, const O& b)
{
}
友元:
在实现类的全局运算符函数时,可能会使用到类内的私有成员,此时全局函数是没有访问权限,如果改变私有为公开会破坏类的封装性,如果提供公开的访问函数又非常麻烦,最好的方式是给该全局函数给予独家授权让其能够访问类内私有成员,这种行为称把该全局函数设置为友元函数
方式:在类内对全局函数声明,并在声明前加 friend
friend const O operator+(const O& a, const O& b);
五、输入输出运算符
在C++中 << >> 运算符不光是按位左移、按位右移,同时还是cout该类的输出运算符 cin该类的输入运算符
1、输出运算符
cout << 10 << endl;
Test t;
cout << t << endl;
由于 << 运算符的调用者是cout对象,我们是无法在该对象的类中去设计一个输出运算符的成员函数,所以只能实现 << 运算的全局函数
ostream& operator<<(ostream& os, const O& t)
{
return os << t.x << t.y;
}
2、输入运算符
cin >> num;
cin >> t1 >> t2;
istream& operator>>(istream& is, O& t)
{
return is >> t.x >> t.y;
}
注意:
①由于输出、输入是可以连续进行的,所以返回值还应该是ostream、istream引用
②因为无法在ostream、istream中重载运算符成员函数,所以<< >>只能重载成全局函数
③如果在重载全局函数中使用到自己类中私有的成员变量,声明为友元函数
④输出运算符中,第二个参数一定要加const,而输入运算符函数中不能加
六、运算类的单目运算符
单目:++/-- !~ - * & sizeof
成员函数:~ !-
const int num;
~0; ~num; // true
O O::operator~(void)const
{
return O(~x, ~y);
}
注意:运算对象可以具备常属性,因此需要是常函数,运算结果只是一个临时值,并且是右值,所以返回值也需要加const
全局函数:
O operator~(const O& a)
{
return O(~a.x, ~a.y);
}
七、自变运算符函数
C++的前后自变左右值问题:
能位于赋值运算符= 左边的就是左值,反之为右值
有名称、可以获取到存储地址的称为左值,反之为右值
C++前自变:++num = 10; // 成功 num=10
直接修改原对象,在原对象的基础上实现自变,然后将原对象的引用返回,所以操作和返回的一直是原对象,是左值
C++后自变:num++ = 10; // 报错
先将原对象的数据存储到临时变量中,接着在原对象基础上自变,然后把临时变量以只读方式返回,并且该临时变量执行语句结束后立即销毁了,无法访问,因此结果是右值
++num++ // 后自变优先级更高,报错
(++num)++ // 先前自变为左值,成功
注意:C语言中,无论前后自变,结果都是右值
前自变运算符: ++a/–a
成员函数:
O& O::operator++(void)
{
x++,y++;
return *this;
}
全局函数
O& operator++(O& a)
{
a.x++,a.y++;
return a;
}
后自变运算符:a++/a–
哑元:在参数列表末尾增加一个不使用且无形参名的int哑元类型,唯一目的就是用于区分是前自变还是后自变
成员函数:
O O::operator++(int)
{
return O(x++,y++);
}
全局函数:
O operator++(O& a, int)
{
return O(a.x++,a.y++);
}
注意:笔试题中出现过写前后自变运算符重载函数
八、特殊的运算符重载函数
*& -> () [] new delete
1、[]下标运算符
想让一个类对象当成数组一样使用,可以考虑重载下标运算符,例如:vector v[10]
可以考虑在下标重载函数中做非法下标的判断,让下标的使用更加安全
2、()下标运算符
重载此运算符可以让一个类对象当做函数一样使用
注意:()/[] 均不能实现为全局运算符函数,只能实现成员函数(C++全局中已经有类似的函数实现,所以不让实现)
= 赋值运算符函数也不能实现为全局函数,因为类内本身一定有一个=赋值运算符的成员
3、解引用* 和访问成员运算符->
重载这两个运算符可以让类对象像指针一样使用,智能指针就是通过重载这俩运算符从而像使用指针一样的类
4、new/delete的重载
void* operator new(size_t size)
C++语法要求重载new运算符的参数必须为size_t,编译器会帮助计算出要申请的字节数并传递,返回值必须为void*,编译器会帮助转换成对应的类型指针返回
void* operator delete(void* ptr)
C++语法要求重载delete的参数必须为void*,编译器帮助转换成void *传递
注意:如果显式地实现析构函数,则会多申请4字节存储要析构的次数;如果不显式实现析构,则不会多申请
注意:new、delete的成员函数、全局函数格式一样
如果只是针对某个类想要重载它的new/delete时,则写为成员函数
如果想要所有类型都执行重载版本,则实现为全局函数
为什么要重载new/delete函数
①可以在重载函数中记录每次分配、释放内存的地址、代码情况、次数情况等到日志中,从而方便检查是否出现内存泄露,以及泄露位置
②对于字节少、且频繁申请、释放的对象,可以在重载函数中给他多分配点内存从而减少产生碎片的可能
九、重载运算符的限制
1、不能重载的运算符
:: 域限定符
. 直接访问成员的运算符
?: 三目运算符
sizeof 计算字节数
typeid 获取类型信息的运算符
2、只能重载为全局函数的运算符
<< 输出运算符
>> 输入运算符
3、只能重载为成员函数的运算符
[]
()
=
->
注意:运算符重载可以自定义运算符执行的过程,但是无法改变运算符的优先级
注意:运算符的操作数量也不能改变
注意:不能发明新的运算符
4、建议
①重载运算符要遵循一致性原则,不要随意改变运算符本身的含义
②不要忘记实现运算符重载函数的初衷,为了提高可读性,不要随意炫技