1 类与对象(可访问性针对的是对象,而不是类)
封装是面向对象程序设计最基本的特性,把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。
类是一种数据类型,定义时系统不为类分配存储空间,所以不能对类的数据成员初始化。类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型。
定义类本身的目的就是要实现一个封装性,对外是封闭的,对内是开放的
成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可调用函数成员。
函数定义通常在类的说明之后进行,其格式如下:
返回值类型 类名::函数名(参数表)
{……}//函数体
const 只能作用于能被对象调动的成员函数 使成员函数成为常方法,不能改变对象的值
类中的成员,除了成员数据和成员函数外,还有成员对象,用其他类的对象作为类的成员。成员对象是实体,系统不仅为它分配内存,而且要进行构造(必须调用该成员对象的构造函数来实现。并且是首先依次自动调用各成员对象的构造函数,再使用类自己的构造函数。)
CGoods Car3; //定义时调用不带参数的构造函数
//CGoods Car4();//定义对象时不能加括号。
//其含义是Car4()是不带参数的函数,它的返回值是类CGoods的对象。
其中运算符“::”称为作用域解析运算符(scope resolution operator),它指出该函数是属于哪一个类的成员函数。
对象是类的实例。声明一种数据类型只是告诉编译系统该数据类型的构造,并没有预定内存。类只是一个样板,以此样板可以在内存中开辟出同样结构的实例——对象。
对象的定义:
1.直接定义类的实例——对象
对象只在定义它的域中有效。
2.采用动态创建类的对象的方法
在程序运行时才建立对象
对象的生存期与程序的运行过程有关。
一个类默认有6个成员函数:
- 赋值语句的重载
- 构造函数
- 析构函数
- 拷贝构造函数
- 取地址运算符重载
- 取地址运算符重载 const
Object(int x = 0) : value(x) //初始化
{
cout<<"Create 0bject "<<this<<endl ;
}
Object (const Object &obj) : value(obj.value)//Object (Object obj) : value(obj.value) 是不行的 &使得不会产生一个临时量
{
cout<<"Copy Create 0bject : "<<this<<endl ;
}//Object c(b) 如果不使用&,b传入就要调用拷贝构造函数以b来构造一个临时量,传入b..... 陷入死循环,一直调动拷贝构造函数
~0bject()
{
cout<<"Destroy 0bject"<<this<<endl ;
}
Object * operator&(){ return this};
const Object * operator&() const {return this};
常方法增加了通用性
ps:常对象只能调动常方法,不能调动普通成员函数,若不将一个不会改变的函数设置成常方法,则常对象无法对其访问。
若类的成员函数不去改变数据,尽量做成常方法(普通对象、常对象都可以方法),具有通用性,不然可能出现常对象无法访问普通成员函数的情况(常对象只能调动常方法)。
类的访问设置
访问属性是在编译时即确定的,可以根据其访问属性设置一个对象的可使用的方法,如将构造函数放在私有或保护,使得外部无法构建该对象,如将重载new放在私有或保护,此时其外部对象不能使用其来动态开辟内存生成对象!!!
同样可以使用保护属性,使得其子类可以构建对象,能父类不能构建对象等
另外可以使用C11关键字final声明该类为最后一个类,其不能被再作为父类。此外final作为最为标记不允许虚函数覆盖该函数。
此外当构造变为私有,其不能被继承,也不能被构建对象,可以使用单例模式的方法对其进行构建对象
2 this指针(thiscall调用)
对象的储存:
一般情况下是将每个对象的属性分配一个数据区,而成员函数(代码区)为各对象所共用。
那么,成员函数是共用的,那么函数怎么知道是哪个对象在调用它呢?
this指针。当调用成员函数时才产生(具有const特性),对象不具有this指针,(节省了空间) * const this
成员调用编译器操作
当对象调用成员函数时,编译器在编译时会进行如下操作:
- 先扫描数据成员的属性 (与处于类的位置无关)
- 然后扫描成员函数的声明(不管函数定义),记录每一个成员函数(返回类型,函数名,参数列表)和访问属性和是否为virtual
- 扫描成员函数的定义与声明是否对应,然后对每一个成员函数进行改写,参数增加了 (类名 * const this指针),然后开始扫描函数体,如果存在私有数据,进行改写,最后在main里对对象调动成员函数语句进行改写。
void Cgoods::Reg(char name[],int amount);
//编译时增加了void Cgoods::Reg(CGoods * const this,char name[],int amount);
{
strcpy(Name,name);//字符拷贝函数 改写strcpy(this->Name,name);
Amount=amount;//this->Amount=amount;
}
main
CGoods cl,c2;
c1.Reg("C++",10,20);// RegisterGoods(&c1,"C++",10,20);调用时将C1的地址给了this指针
c2.Reg("java", 20,12);// RegisterGoods(&c2,"java" ,20,12);调用时将C2的地址给了this指针
return 0:
this指针没有写入参数的原因:
成员函数的里参数(数据成员),是通过函数的形参与实参的结合,即栈的调动形式压入,而this指针是靠ecx寄存器来传递
静态成员函数没有使用this指针。
this指针用法:
class Int
{
int value;
public:
Int(int x=0):value(x){}
Int(const Int &it):value(it.value){}
~Int(){}
int GetValue(){return value;}
void Setvalue(int x){value=x};
Int Add(const Int &b) const//const 使函数成为一个常方法,即调动该函数的对象不能被修改
{//Int Add(Int * const this,const Int &b) const
//Int c;
//int s=this->Getvalue()+b.Getvalue();
//return c(s); //对象c被直接构造在临时量空间内,只构建了一次对象
this->value+=b.value();//可能会改变了this的对象和b对象,所以要加const
return *this;//已值的形式传递,产生了临时量
}
};
int main()
{
Int a(10),b(20),c;
c=a.Add(b);
}
thiscall调用,专用于类成员函数的调用,其特点随编译器的不同而不同,在VC里this指针存放于ecx寄存器,将this看作是函数的第一个参数。
3 构造函数 *(获取资源)
构造函数是特殊的公有成员函数(也可以是私有),其特征如下:
1.函数名与类名相同。
2.构造函数无函数返回类型说明。注意是没有而不是void,即什么也不写,也不可写void!实际上构造函数有返回值,返回的就是构造函数所创建的对象
3.在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被系统调用,在该对象生存期中也只能被调用这一次。
4.构造函数可以重载。严格地讲,说明中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。
5.如果类说明中没有给出构造函数,则C++编译器自动给出一个缺省的构造函数:
类名(void) {};
而只要我们定义了一个构造函数,系统就不会自动生成缺省的构造函数。C11关键字default可以使得编译器生成默认构造
6.只要构造函数是无参的或者只要各参数均有缺省值的,C++编译器都认为是缺省的构造函数,并且缺省的构造函数只能有一个 。
7. 如果对象的数据成员全为公有的,也可以在对象名后加“=”加“{}”,在花括号中顺序填入全体数据成员的初始值.(此时就是结构体)
8. 其赋值顺序是根据类设计成员数据顺序赋值,与结构体一样,而不是根据列表方案设计,且列表方案对内置类型是一样的,而对于对象列表写法是拷贝构造,而赋值写法时,还是会进行列表进行构造函数,而赋值拷贝又会进行构造函数(强转),创建临时对象,来给它赋值,即调动了两次构造函数,所以一般对象的构造都写在列表内,只需要进行构造一次。
9. 拷贝构造函数也是构造函数的一种
缺省构造函数(当我们定义了一个构造函数时,编译器不会生成缺省构造函数)
class Object
{
public:
Object(int x) { cout << " O" << this << endl; }
Object(const Object &obj)
{
cout << "COPY O" << this << endl;
value = obj.value;
}
Object &operator=(const Object& obj)
{
if (this != &obj)
{
value = obj.value;
}
cout << "= O" << this << endl;
return *this;
}
~Object() { cout << "Destory O" << this<<endl; }
private:
int value;
};
int main()
{
Object a;//错误,由于其没有缺省构造函数
}
ps:
- 所以一般在写构造函数时,都会给其参数添加缺省值,避免没有缺省构造的情况。
- 在继承中,若基类没有缺省构造函数,可能将无法编译通过,因为派生类的列表语句里若没有明确调动构造函数或拷贝构造函数,编译器将会自动生成一个基类的缺省构造函数来完成派生类从父类继承的隐藏对象成员。而在实例化子类对象时,需要将基类对象成员初始化,已达到实例化的目的(获取资源),这也是C++本身的优点。
ps:
3. 构造函数不是系统调用的吗,为什么会有this指针。虽然是系统调用,但其传入了对象的地址,然后才进行对象的构造,不然构造函数怎么知道构造的是哪个对象呢?而接受对象的地址的还是this指针,即编译器对构造函数进行了改写,且构造函数有返回类型,只是返回类型为对象,返回的是构造的对象。
#include<iostream>
using namespace std;
class Object
{
private:
int value;//其赋值顺序是根据类设计成员数据顺序赋值,与结构体一样,而不是根据列表方案设计
int num;
public:
Object(int x = 0) :value(x)//初始化 :称为列表方案,不是根据列表方案设计赋值顺序
{
//value = x;
cout << "Create Object : " << this << " : " << value << endl;//?为什么有this指针
}
~Object()
{
cout << "Destroy 0bject : " << this << " : " << value << endl;
}
};
Object x(1);
void fun()
{
Object b(4);
}
int main()
{
Object a(2);
int b(10);//C++下的伪构造形式(面向对象)内置类型从语义上有构造函数,实际上没有构造函数,所以是伪构造,当编译时,会进行赋值
int c=int(10);//C++下的伪构造形式(面向对象)
fun();
return 0;
}
Object y(3);
输出结果是:13244231
分析:在运行程序时,在进入main()函数之前,外部对象就已经被创建(全局变量和全局对象就被加载到.data区),即系统分配了空间(.data区)和调动了构造函数,当main()结束,程序运行结束,系统回收.data区,所以先进行析构对象
对象的生存期与程序的运行过程有关,由系统控制
ps:无论是内置类型还是类类型
- Type() 调动构造函数 对于内置类型C++是伪构造,也有构造函数
- ~Type()调动析构函数
内置类型从语义上有构造函数,实际上没有构造函数,所以是伪构造,当编译时,会进行赋值
int b(10);//C++下的伪构造形式(面向对象)
int c=int(10);//C++下的伪构造形式(面向对象)
//内置类型从语义上有构造函数,实际上没有构造函数,所以是伪构造,当编译时,会进行赋值
构造函数的三个功能(类的成员,尽可能用列表方式进行构造初始化)
- 创建对象
- 初始化对象
- 类型转换 当单参(多参但有一个没有默认值,其他参数都有默认值)或没有进行explicit时会进行隐式转换,而当这两个条件不满足时需要进行强转。 类型转换只能转换一个变量
#include<iostream>
#include<stdlib.h>
using namespace std;
class Object
{
public:
Object(int x = 0) :num(x), value(num)//赋值顺序与列表无关,而与设计时的成员顺序有关
{
}
~Object()
{
}
void Print() const
{
cout << "value:" << value << endl;//随机值
cout << "num" << num << endl;//10;
}
operator int()const//有返回值,但是返回值不显示,防止有人造成二义性,返回类型就是需要强转的类型
{
return value;
}
private:
int value;
int num;
};
int main()
{
Object obj(10);//构建对象,初始化对象
int x=100;
obj=100;//与 obj=(Object)x 一样,进行了类型转换 先构造,在赋值
Object obj1=x;//由于obj1对象还不存在,此时隐式转换为 obj1(x) 进行构造对象
//当不能进行隐式转换时,若存在两个参数且带有explicit时
//obj=(Object)(x,y) //是错的,强转只能强转一个,这种写法是逗号运算符,将y传递给了第一个参数,另外的参数有默认值的情况下。
obj=Object(x,y)//正确,但是这是构造一个无名对象,而不是强转
x=(int)obj;//需要重载强转obj
obj1==obj2; //当没有重载==时,编译器会寻找强转类型,将两个对象都进行强转(打通对象与对象之间的比较) 但是有多个重载强转类型时,其会失效,需要进行==重载
obj.Print();
return 0;
}
拷贝构造函数(当一个对象需要被另一个对象创建时)
系统会自动提供,称为缺省的按成员语义支持的拷贝构造函数,每个类成员被依次拷贝,亦称为缺省的按成员初始化。按成员作拷贝是通过依次拷贝每个数据成员实现的,而不是对整个类对象按位拷贝。赋值运算符“=”称缺省的按成员拷贝赋值操作符,同类对象之间可以用“=”直接拷贝 。
1.当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。(虽然切断了实参对象与形参对象之间的联系,但拷贝出一个对象太费时间和空间了)
2.当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。
因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
Object (const Object &obj) : value(obj.value)//Object (Object obj) : value(obj.value) 是不行的 &使得不会产生一个临时量
{
cout<<"Copy Create 0bject : "<<this<<endl ;//编译器为了一致性,将临时量都写成常量形式,即当对象的临时量在等号右边,它就是一个常量。即需要const &,但其又可以当做左值。
}//Object c(b) 如果不使用&,b传入就要调用拷贝构造函数以b来构造一个临时量,传入b..... 陷入死循环,一直调动拷贝构造函数
int main()
{
Object x(10);
Object a=x;//不调动赋值(由于a还没进行构造,=相当于调动拷贝构造函数)
Object b(x) ;//x为实参,调动拷贝构造函数
return 0;
}
ps:
- 对象里有指针,需要进行深拷贝。(默认浅拷贝)
- 编译器为了一致性,将临时量都写成常量形式,即当对象的临时量在等号右边,它就是一个常量。
- 拷贝构造是一个对象创建另一个对象,而赋值一般是两个对象已经存在,进行赋值,若左值对象不存在会进行隐式转换为拷贝构造,而若右值不是对象,会进行构造类型,再进行赋值
4 析构函数(释放资源)
当一个对象定义时,C++自动调用构造函数建立该对象并进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数注销该对象并进行善后工作,这个特殊的成员函数即析构函数:
- 构函数名与类名相同,但在前面加上字符‘~’,如
~CGoods()。 - 析构函数无函数返回类型,与构造函数在这方面是一样的。但析构函数不带任何参数。
- 一个类有一个也只有一个析构函数,这与构造函数不同。析构函数可以缺省。
- 对象注销时,系统自动调用析构函数。
- 对象可以控制析构
int main()
{
Object x(10);//系统分配空间调动构造函数
x.Object(10);//错误,对象不能控制构造
x.~Object(); //对象可以控制析构,但系统在程序运行结束时还会调用一次,即这样写调动了两次析构
return 0;
}
对于不同作用域的对象类型,构造函数和析构函数的调用如下:
1. 对全局定义的对象,当程序进入入口函数main之前对象就已经定义,这时要调用构造函数。整个程序结束时调用析构函数。
2. 对于局部定义的对象,每当程序控制流到达该对象定义处时,调用构造函数。当程序控制走出该局部域时,则调用析构函数。
3. 对于静态局部定义的对象,在程序控制首次到达该对象定义处时,调用构造函数。当整个程序结束时调用析构函数。
ps:构造函数和析构还是只管理对象的生命期,而空间是由OS管理
5 new delete
1.面向对象,封装
2.C有空间就可以直接进行操作,而C++系统先分配了空间,在调动构造函数构造对象,此时对象才实例化,才有对象,所以C++里对象与空间是分离的。
所以如果有对象存在,必有空间占有。
Object *p=NULL;//指向Object类的指针
p=new Object(10);//new 返回了所创对象的地址
C++的全局对象在进入main()函数之前就被实例化(系统在.data区分配空间,然后调动构造函数)
new malloc delete free的区别
new
- 系统先分配空间
- 系统调动构造函数构造对象 (那进行变量分配空间时?同样的,只不过是初始化)
new是一个关键字(可进行运算符重载)
申请失败 抛出异常 throw bad_alloc
new的特殊用法:
Object *p=new Object(10);//关键字,operator
Object *s=(Object *)::operator new (sizeof(Object)); //不调用构造函数,相当于malloc,唯一区别申请失败时抛出异常 运算符重载 需要 ::operator delete(os)来释放;
::operator delete(os);
new(s) Object(100);//定位new,以s所指向的空间(不开辟空间)调动构造函数构造对象
int main()
{
Object x;//系统分配空间调动构造函数
new(&x) Object(100);//系统在x的空间上再次调动构造函数
x->~Object();
return 0;
}
malloc
仅进行分配空间
malloc是一个函数
申请空间时,必须指定空间大小
申请空间后必须进行强转
申请失败,返回NULL值
delete
1.调动析构函数摧毁对象
2.释放空间
delete [] p //此时会去读取new分配时的头部信息,释放一块连续的堆空间。
new [10] 会多4个字节分配,来保存头部信息
free
1.仅进行空间释放
如果有对象存在,必有空间占有。
内存泄露:当指针指向的一块空间没有得到释放而指向了另外的空间,则发生了内存泄露,与释放释放该内存无关。
class Empty
{
public:
Empty() {}
~Empty() {}
};
int main()
{
Empty a;
sizeof(a);//1字节,有对象必然有空间 占位符
return 0;
}
6 引用
C++函数中参数的传递方式是传值。在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中。
如果要求改变实参的值,怎么办呢?如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢?在C++中有一种新的导出型数据类型—引用(别名),将指针进行了封装
引用不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是C++系统不为引用类型变量分配内存空间。引用主要用于函数之间的数据传递。
ps:
1.不能空引用
如 int &c; //int * const c
是错的 必须初始化,这也是它的优点,所以不用判断是否为空,而指针需要,所以引用比指针安全。对于c指针不能指向即是一个垃圾指针
2.int a=10; int &c=a; c一旦绑定了a,则与a不求同生但求共死,指向同一块内存,且c不能去绑定其他变量
3.别名c其实开辟了空间,但对别名c的操作(底层对c进行解引用,即对c的操作都是作用于a),都无法访问到c开辟的空间
4.一般引用不作为函数的返回值,而如果要利用引用作为返回值时,返回值只能用不受生存期影响的变量、对象(全局、堆、局部的static、*this)(一个函数返回值时(返回值占有空间比较大),一般要生成一个临时变量作为返回值的拷贝,引用作为返回值不会产生值的拷贝)
Object & fun ()
{
Object x(10) ;
return x;
//x生存期只在函数里有效,即要被绑定的对象内存在函数结束时释放了,而执行main函数里的a = fun ();时fun得到的别名是相当于一个空引用,即引用作为返回值时,返回值只能用不受生存期影响的变量、对象
}
int main()
{
0bject a;
a = fun ();//a无法得到x的值
return 0;
}
编译后转换为指针
Object * const fun ()
{
Object x(10);
//int a=0; int &b=a; ----> int * const b=&a; 所以对于复杂的引用其实是可以将右边的取地址覆盖左边的 * const
return &x;//x生存期只在函数里有效,返回的是&x(x空间的地址),而当函数结束时,x的空间被释放
}
int main()
{
Object a;
a = *fun ();//这里相当于*(&x) 但x的空间在fun()函数结束时已经被释放,所以*(&x)相当于越界访问
return 0;
}
5.引用作为函数的形参(改变形参可以改变传入实参值,不会产生拷贝)
void Swap_Int (int *ipl, int *ip2)
{
//还需要判断指针是否为空
int tmp = *ip1 ;
*ip1 = *ip2;//对所指内存的内容进行修改,但指针还是指向原空间
*ip2 = tmp;
}
void Swap(int &a,int &b)
{
int tmp = a ;
a = b;//其实也是对所指内存的内容进行修改,但指针还是指向原空间
//对a操作时,进行了 解引用,即操作实参的空间,而变量对空间的操作就是对其值的操作
b = tmp;
}
6.对数组只能引用数组元素,不能引用数组(数组名本身为常量地址)
7.不能定义引用的引用(引用也是地址),所以当函数的参数为引用时,引用不能作实参。
指针和引用的区别:
指针指向的是地址,指针必须通过解引用才能访问到其内容,引用只是某个变量的别名,通过改变引用就能改变变量(但引用只是将指针封装了(底层来看引用就是常性指针))。
void fun(int &x)
{
int *p =&x ;
x= 100;
}
int main(){
int a = 10;
int b = 20;
int &c = a;//引用不需初始化,不存在空引用
int *p = &c;
fun(a);
fun(c) ;
return 0;
}
其编译后会变成
void fun(int * const x)
{
int *p =&x ;
*x= 100;
}
int main(){
int a = 10;
int b = 20;
int * const c = a;
int *p = c;
fun(&a);
fun(c) ;
return 0;
}
别名x也分为x与*x x指向自己开辟的空间 *x指向a的空间
对别名x的操作,底层对c进行解引用,即对c的操作都是作用于a的空间,而无法访问的x的空间,使得看上去x与a是指向同一块内存空间
const 引用
引用在内部存放的是被引用对象的地址,不可寻址的值是不能引用的;当引用作为形参时,实参也不能使用不可寻址的值,更不可能进行类型转换(如:实数转换为整数)。
当const引用作为形参时,实参也能使用不可寻址的值,并能进行类型转换
右值引用
传统的拷贝构造和赋值 左值引用 只是简单的对象与对象之间的赋值
&&引用可以对临时量进行修改,右值引用只能作用于右值,当需要将左值赋值给右值引用时,需要利用std::move(),进行转换。
int &a=10;//error
const int &a=10;//正确
int &&b=10;//ture
b=100;
//&&引用可以对临时量进行修改
当返回对象时会产生临时量,此时需要将该对象的所有权转移给临时量,然后再通过临时量将权限转移给等待接收的对象。若不使用右值引用,传统拷贝,赋值,返回时进行了拷贝构造,可以将权限移交给临时量,而赋值时,你无法对临时量进行权限转移,因为你无法修改临时量的值。此时临时量死亡,堆区资源也将被释放。且由于unique,只能有一个对象拥有该资源,也不可能编译通过,因为临时量无法将自己的权限转移,而通过右值引用,即可以修改临时量的值,即能实现转移。而不用担心临时量的死亡导致资源的死亡。 而两个unique需要进行move操作的原因是,当需要转移时,需要将另一个对象变为右值,即使其不能再访问该资源,unique只能有一个对象访问该资源(独占)。若不进行move操作,其还是左值,其仍能操作该资源就与unique设计冲突,另外,因为使用move使其变为右值,所以需要移动赋值来进行操作,修改它
Object(Object &&obj):value(obj.value) 移动拷贝
7 名字空间域和类域
在C++中支持三种域:局部域、名字空间域和类域。
名字空间域:
名字空间域是随标准C++而引入的名字空间,主要是为了解决全局名字空间污染(global namespace pollution)问题,即防止程序中的全局实体名与C++各种库中声明的全局实体名冲突。
它相当于一个更加灵活的文件域(全局域)。名字空间域不能定义在函数声明、函数定义或类定义的内部。
可以用花括号把文件的一部分括起来,并以关键字namespace开头给它起一个名字:
namespace ns1
{
float a,b,c;
fun1(){……}
…
}
//访问a,fun();
ns1::a,ns1::fun1();
在域外使用域内的成员时,需加上名字空间名作为前缀,后面加上域操作符“::” 。这里添加了名字空间名称的成员名被称为限定修饰名。最外层的名字空间域称为全局名字空间域(global namespace scope),即文件域。
名字空间域可分层嵌套,同样有分层屏蔽作用。
使用using声明可只写一次限定修饰名。using声明以关键字using开头,后面是被限定修饰的(qualified)名字空间成员名.以后在程序中使用被修饰名时,就可以直接使用成员名,而不必使用限定修饰名。
namespace cplusplus_primer
{
namespace Matrixlib
{ //名字空间嵌套
class matrix{……} //名字空间类成员matrix
…...
}
}
//访问matrix
cplusplus_primer::Matrixlib::matrix;
using cplusplus_primer::Matrixlib::matrix;//名字空间类成员matrix的using声明,以后在程序中使用matrix时,就可以直接使用成员名,而不必使用限定修饰名。
使用using指示符可以一次性地使名字空间中所有成员都可以直接被使用,比using声明方便。using指示符以关键字using开头,后面是关键字namespace,然后是名字空间名。
标准C++库中的所有组件都是在一个被称为std的名字空间中声明和定义的。在采用标准C++的平台上使用标准C++库中的组件。
类域:类体也定义了一个域称为类域。在类域中说明的标识符仅在该类的类域内有效。必须加上“类名::”作限定修饰。
在类域中类成员在类体中被声明的顺序同样很重要,后声明的成员不能被先声明的成员引用。
类中编译器对名字(标识符)的解析分两步对于函数中的默认参数,编译时已确定,对于类成员的可访问性编译时也确定了。
第一步查找在声明中用到的名字,包括数据成员和函数成员声明中用到的参数类型,第二步才是函数成员体内的名字。
class string
{
public:
typedef int index_type;//为易读易懂用下标型命名
char GetstringElement(index_type elem)
{
return Astring[elem];
} // Astring先说明
private:
char Astring[30]; //Astring后说明
int a;
};
Astring名字的解析是在第一步,而内联函数使用它是在第二步
8 运算符的重载(针对对象)
当存在多个运算符重载时,如*、++, 如++this,则this作为++操作符的参数使用,而不会发生函数重载
运算符的重载实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该重载的运算符时调用此函数,重载运算符作用在该表达式。运算符是其实是函数,有返回值,且如重载类型运算符,当两个对象比较时会默认调动。
定义运算符重载函数的一般格式:
返回值类型 类名::operator 重载的运算符(参数表)
operator是关键字,它与重载的运算符一起构成函数名
1.运算符重载函数的函数名必须为关键字operator加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。
2. 当用类的成员函数实现运算符的重载时,运算符重载函数的参数(当为双目运算符时)为一个或(当为单目运算符时)没有。运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数。
3. 单目运算符“++”和“–”存在前置与后置问题。
4. 重载运算符可以重载强转类型运算符,但其没有显示返回值,是返回其被强转的类型
前置“++”格式为:
返回类型 类名::operator++(){……}
Int & operator++()//使用&,直接改变this
{
this->value+=1;
return *this;
}//;++c c.operator++() operator++(&c);
而后置“++”格式为:
Int operator++(int)//不带引用,返回的是一个局部对象,不能使用引用,不然无法产生拷贝,返回别名绑定的对象被析构
{
int val=this->value;
this->value+=1;
return Int(val);
}//c++; c.operator++(0) operator(&c,0); 多了一个参数
后置“++”中的参数int仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名。
重载强转类型运算符(打通了内置类型和对象之间的阻碍,甚至打通对象与对象之间的强转!)
//重载强转类型运算符
operator int()const//有返回值,但是返回值不显示,防止有人造成二义性,返回类型就是需要强转的类型
{
return value;
}
当没有重载==时,编译器会寻找强转类型,将两个对象都进行强转(打通对象与对象之间的比较) 但是有多个重载强转类型时,其会失效,需要进行==重载,所以在一般情况下由于类型隐式转换,在进行运算时,其就进行了类型转换,其可以不重载比较运算符!!
operator bool() const//对象可以直接使用if判断
{
if(this->ptr!=NULL)
return true;
return false;
}
重载的->,当调动进行重载时,被回退了,然后再继续调动-> ,即->被使用了两次
ps:
1.重载的运算符遵循了原运算符的表达方式(一致)
不能改变运算符原有的优先级、结合性和语法结构,不能改变运算符操作数的个数
重载运算符含义必须清楚,不能有二义性
2.sizeof、::、.和(成员操作符)、? : 、注释符都不允许重载
class Int
{
int value;
public:
Int(int x=0):value(x){}
Int(const Int &it):value(it.value){}
~Int(){}
int GetValue(){return value;}
void Setvalue(int x){value=x};
Int operator+(const Int &b) const
{
this->value+=b.value();
return *this;
}
Int operator+(int x) const
{
this->value+=x;
return *this;
}
};
Int operator+(int x,const Int &a) // 全局函数,不会产生this指针
{
return a+x;
}
int main()
{
Int a(10),b(20),c
int x=100;
c=a+b;
//编译器会将其编译成 c=a.operator+(b);然后转化为c=operator+(&a,b)
c=a.operator+(b); //可通过
c=a+x;
c=x+a;//c=operater(x,a); 需全局函数,不产生this
}
ps:在C语言中也存在运算符重载,即对不同类型的数据进行运算,其实也进行了运算符的重载
赋值语句重载
Object & operator=(const Object &x)
{
if(this!=&x)//不让自己=自己
{
this->value=x.value;
//return *this; 不符合内置类型的赋值法则,不能连续赋值
return *this;
}
}
int main()
{
Object a(10);
Object b(a);
Object c(0);
//a=a;
c=a;
c.operator=(a);
//operator=(&c,a);
int x=10,y=20,z=0;
z=x;
z=x=y;//可以连续赋值
c=a=b;
c=a.operator=(b)//c=operator=(&a,b) 成为了函数调动 如不写 return *this;此时返回的是无类型给了c,即达不到连续赋值
}
9 空间与对象
int main()
{
Object a(10);
Object b(100);
Object z;
z=x;
Object *op=(Object *)malloc(sizeof(Object));//空间
*op=x;//op无对象,对于虚函数崩溃,对象构建到空间 对象无法给空间赋值
new(op)Object(s);//定位new,以s所指向的空间(不开辟空间)调动构造函数构造对象
}
10 对象直接的关系(包含,继承,关联)
对象与对象之间的关系
- 包含:当对象A是对象B的属性时,称对象B包含对象A. has a
- 继承:当对象A是对象B的特例时,称对象A继承对象B. is a
- 关联:当对象A的引用是对象B的属性时,称对象A和对象B之间是关联﹑关系.所谓对象的引用是指对象的名。(指针,引用)
ps:继承不是包含 最大的区别就是保护属性,包含的对象成员无法访问其保护数据,继承可以访问保护属性(方法) 保护成员对于对象是私有的,对于继承,可以访问继承(隐藏成员)而来的保护成员,但无论是包含还是继承,都需要有缺省的构造函数。
成员对象与继承对象的差异: 子类实例化时继承的基类也会产生一个隐藏对象。
。
ps:
- Object::oa(相当于一个无名对象访问),所以继承来的对象可以访问保护
- 想访问包含的保护私有,因使用友元技术
1. 包含
//包含关系
#include<stdlib.h>
using namespace std;
class Text
{
public:
Text(int x=10);
~Text();
//当调用拷贝构造函数时,由于Text内有对象,需要先构造对象,而如果进行列表方式
Text(const Text &it) :ix(it.ix), obj(it.obj)//此时只会进行一次拷贝构造函数
{
// ix = it.ix;
//obj = it.obj; //会调动缺省的构造函数obj,在进行对象和对象直接的赋值,不调动拷贝构造函数,由于obj无法进行隐式转换
}
private:
int ix;
Object obj;
};
Text::Text(int x=10):ix(x),obj(x+10) //尽量将对象的构造写在列表
{
ix = x;
//obj = x + 10;
cout << " Create Text:" << this << endl;
}
Text::~Text()
{
cout << " Destroy Text:" << this << endl;
}
class Object
{
public:
Object(int x = 0,int y=0) :num(x), value(x)//当具有包含关系时,需要有缺省构造函数
{
cout << " Create object:" << this << endl;
}
Object(const Object &x) :num(x.num), value(x.value)//只有拷贝构造和构造才能使用列表
{
cout << "COPY Create object:" << this << endl;
}
~Object()
{
cout << " Destroy object:" << this << endl;
}
const Object &operator=(const Object &obj)
{
if (this != &obj)
{
value = obj.value;
num = obj.num;
}
cout << this << "=" << &obj << endl;
return *this;
}
void Print() const
{
cout << "value:" << value << endl;//随机值
cout << "num" << num << endl;//10;
}
operator int()const
{
return value;
}
private:
int value;
int num;
};
int main()
{
Text t1;//包含的obj需要有缺省构造函数,由于先对成员进行构造,才完成构造自身。
Text t2(t1);//此时t2内的obj还需要进行构造,在赋值,即先调用缺省的构造函数,而不是调用拷贝构造函数
}
ps:
- 先进行包含对象的构造,在进行自身的构造,需要注意的是,被包含的对象需要有缺省构造函数,不然无法进行构造。
- 当包含的对象不是单参,或有explicit时,构造和拷贝构造最好写成列表方式
包含关系的嵌套
class Text
{
public:
Text(int x = 10);
~Text();
//当调用拷贝构造函数时,由于Text内有对象,需要先构造对象,而如果进行列表方式
Text(const Text &it) :ix(it.ix), obj(it.obj)//此时只会进行一次拷贝构造函数
{
// ix = it.ix;
//obj = it.obj; //会调动缺省的构造函数obj,在进行对象和对象直接的赋值,不调动拷贝构造函数,由于obj无法进行隐式转换
}
private:
int ix;
Object & obj;//构造时先构造obj,但其是引用,类内是设计的,当有Text t1时,obj需要初始化进行构造构造可以,但其引用扩宽了obj的作用域,使得函数之间的的不安全了,其他函数可以修改obj的内容。
//private:
// int ix;
// Object * obj; //该写法是obj是一个Object类型的指针,对其构造可以指向一个Object类。
//Text t;
//private://无法构建成功
// int ix;
// Text * next;//当进行构造时,先进行构造 next,而next是指向Text类型的指针,是不同的类型,不会陷入无穷构造,类似结构体。
// Text &test; //当进行构造时,test会先进行构造,相当于调动自身构造函数构造自己,但构造自己时,test里有包含了Text &类型对象,无穷构造。不能嵌入自己,所以是错误的
// Text t1;
};
Text::Text(int x = 10) :ix(x), obj(x + 10) //尽量将对象的构造写在列表
{
ix = x;
//obj = x + 10;
cout << " Create Text:" << this << endl;
}
Text::~Text()
{
cout << " Destroy Text:" << this << endl;
}
class Object
{
public:
Object(int x = 0, int y = 0) :num(x), value(x)//当具有包含关系时,需要有缺省构造函数
{
cout << " Create object:" << this << endl;
}
Object(const Object &x) :num(x.num), value(x.value)//只有拷贝构造和构造才能使用列表
{
cout << "COPY Create object:" << this << endl;
}
~Object()
{
cout << " Destroy object:" << this << endl;
}
const Object &operator=(const Object &obj)
{
if (this != &obj)
{
value = obj.value;
num = obj.num;
}
cout << this << "=" << &obj << endl;
return *this;
}
void Print() const
{
cout << "value:" << value << endl;//随机值
cout << "num" << num << endl;//10;
}
operator int()const
{
return value;
}
private:
int value;
int num;
};
2. 继承(派生类实例化对象里包含了基类的隐藏对象成员)
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认知过程。
层次概念是计算机的重要概念。通过继承(inheritance)的机制可对类(class)分层,提供类型/子类型的关系。
C++通过类派生(class derivation)的机制来支持继承。被继承的类称为基类( base class)或超类(superclass),新生的类为派生类(derived class)或子类(subclass) 。
基类和派生类的集合称作类继承层次结构( hierarchy) 。
如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型(subtype) 。
派生反映了事物之间的联系,事物的共性与个性之间的关系。派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。
如果一个派生类可以同时有多个基类,称为多重继承(最好少用)(multiple-inheritance),这时的派生类同时得到了多个已有类的特征。一个派生类只有一个直接基类的情况称为单—继承(single-inheritance)。
tip:同名隐藏与同名覆盖是不同的。同名覆盖针对的是虚函数。
上面的步骤就是继承与派生编程的规范化步骤。
第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。 错的
第三步中,独有的新成员才是继承与派生的核心特征。
第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用一律重写可免出错。
访问控制,亦称为继承方式,是对基类成员进一步的限制。
访问控制也是三种:
公有(public)方式,亦称公有继承 is(是一个的关系) 学生是人 ,人不一定是学生 具有兼容
保护(protected)方式,亦称保护继承
私有( private)方式,亦称私有继承。 组成关系(由什么组成)
ps:
- 当继承派生类类进行实例化时,其会调动父类进行构造从父类继承下来的属性,创建一个隐藏基类对象!
- 从父类继承的对象被父类构造后,派生类的成员包括自身的几个数据成员和父类构造出来的一个数据成员(隐藏的对象成员)! 如下图:base1具有4个成员。
- 派生类不能访问继承基类的私有成员。
当进行public继承时,该派生类实例对象的方法可以访问自身的数据成员(公有,保护,私有),可以访问基类的公有,保护,但不能访问基类的私有成员。基类的成员是公有继承,该一级派生类的二级派生方法能访问基类的公有,保护。
当进行private继承时,该派生类实例对象的方法可以访问自身的数据成员(公有,保护,私有),可以访问基类的公有,保护(相当于访问自身私有的公有和保护,但不能访问私有中的私有),但不能访问基类的私有成员,但相对的基类的成员被私有化,该一级派生类的二级派生方法不能访问基类的公有,保护,因为此时一级派生私有继承(成为了其私有数据成员)。
ps:对于保护成员,对于对象而已和私有一样,封装,但对于方法,其和公有一样,可以访问
t1的方法可以访问自身的数据,可以访问继承base里的公有和保护,可以访问继承obj里的公有和保护。
但当是私有继承时,t1的方法可以访问自身的数据,可以访问继承base里的公有和保护,但不能访问base的私有对象(Obj继承的base的私有)
派生类应用的讨论(在任何需要基类对象的地方都可以用公有派生类的对象来代替)注意其是public 相当于继承里有默认转换(可以包含设计的类类型和指针类类型可以隐式转换,而内置类型指针不能隐式转换,继承的特征:赋值兼容)
在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称赋值兼容规则。它包括以下情况:
1.**派生类的对象可以赋值给基类的对象,这时是把派生类对象中从对应基类中继承来的隐藏对象赋值给基类对象。**反过来不行,因为派生类的新成员无值可赋。(对象对对象)
2.**可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的隐藏对象,不能访问派生类中的新成员。**同样也不能反过来做。(对象地址对指针)
3.派生类对象可以初始化基类的引用。引用是别名,但这个别名只包含派生类对象中的由基类继承来的隐藏对象。(对象对对象引用)
#include<iostream>
#include<stdlib.h>
using namespace std;
class Person
{
private:
int value;
public:
Person(int x = 0):value(x){}
};
class Student:public Person //公有继承 is 是一个
{
private:
int num;
public:
Student(int x = 0) :num(x), Person(x + 10){}//先构造基类对象,再构造num,构建顺序与列表方式无关
};
void dance(const Person &s)//人能跳舞,学生也能跳舞
{
}
void Study(const Student &s)//只针对学生,人不能学习,即传入限制
{
}
int main()
{
Person se(23);
Student st(10);
Person &ps = st;//引用与指针类似
Person *ps = &st;//类型决定了指针解析能力
se = st;//赋值兼容,其派生类内具有基类的隐藏对象,相当于截断但这里一般称为切片,以区分寄存器
//ps:不能反过来
dance(se);
dance(st);//赋值兼容
//Study(se);//错误,人不一定是学生
Study(st);
return 0;
}
继承里子类实例化时,先调用父类的缺省构造,然后进行子类的构造函数构造。若列表没有明确调动基类构造函数时,其将会调动缺省构造,然后在进行子类构造。
在调用拷贝构造函数时,若列表没有明确说明调动基类拷贝构造函数时,其会调用基类缺省构造,然后进行子类拷贝构造。
在进行两个对象赋值时,在子类重载赋值函数里没有明确说明调动基类重载赋值时,其将不进行重载赋值,直接进行子类重载赋值。
ps:
- 所以一般在子类的构造和拷贝构造函数在列表里都明确写上基类构造和拷贝构造。(其不若不在列表写而在子类函数里写。系统将默认产生一个缺省的构造在列表里。)
- 对于重载赋值,由于是两个已经实例化的对象,系统不会为其生成构造函数,若子类的函数里重载赋值没有明确写上基类的,将只进行子类的赋值。
Base &operator=(const Base& obj)
{
if (this != &obj)
{
Object::operator=(obj); //表示以继承的基类无名对象进行调用。
((Object*)this)->operator=(obj);//对this进行赋值兼容。
num = obj.num;
}
cout << "= B" << this << endl;
return *this;
}
都可以调动基类的赋值语句。
同名隐藏(数据成员和方法)
#include<iostream>
using namespace std;
class Object
{
public:
Object(int x = 0) { cout << " O" << this << endl; }
void Print() const
{
cout << value << endl;
}
public://可以是公有和保护
int num;
private:
int value;
};
class Base : public Object
{
private:
int num;
public:
Base(int x=0) :Object(x)
{
cout << " B" << this << endl;
num = x;
}
void set()
{
num = 100;//子类的
//相当于
this->num=10;
this->Object::num = 10;//同名隐藏 还可以利用隐藏对象来调动
//相当于
((Object*)this)->num = 10;
}
void Print() const
{
cout << num << endl;
}
};
int main()
{
Base a(10);
a.Print();//调动子类的print
a.Object::Print();//调动父类的print
return 0;
}
隐藏:隐藏的方法和数据成员还存在,只是没有显示调动,也是由于该机制会将继承的同名的函数隐藏(只需要同名),所以继承下来不会发生函数重载机制。
在任何需要基类对象的地方都可以用公有派生类的对象来代替
此外,若没有发生同名覆盖,子类里有父类无名对象
多态性(polymorphism)
多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(早期绑定)。以虚基类(晚绑定,只有在运行时,才知道对象与哪个函数有关系)为基础的运行时的多态性是面向对象程序设计的标志性特征。体现了类推和比喻的思想方法。
虚函数(需要public的赋值兼容—针对this指针)
没有this指针,即不是类方法的函数都不能作为虚函数。如:
1.静态函数,被同一类所有对象共有
2.友元函数
3.内联函数
4.外部函数
5.构造函数 构造函数和析构函数就不会被继承,不会被继承的函数都不能加虚
6.析构函数
ps:
-
一般在声明处加virtual,不在定义处加virtual,与缺省参数在声明处加是一个道理,防止二义性,声明和定义只有一处能加virtual。
-
当编译时发现有虚函数,编译时会产生一个虚函数表(.存放在.data区,其元素是该类虚函数的入口地址),且还会在基类里产生一个指向这个虚函数表的指针vptr。且对于继承体系里,每一个层次里编译器都会产生一个虚函数表,且该派生类的虚函数表先对基类虚函数表拷贝,再在派生类虚函数内容覆盖。
-
当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。(老鼠的儿子会打洞)
-
当在派生类中重新定义虚函数(overriding avirtual function,亦译作超载或覆盖)时,不必加关键字virtual。但重新定义时不仅要同名,而且它的参数表和返回类型全部与基类中的虚函数一样.否则联编时出错。
-
派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型(三同)。否则被认为是隐藏,而不是虚函数。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是个例外。
-
—个类对象的静态和动态类型是相同的,实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。(告诉编译器,此时运行时识别,运行时确定调用关系)
tip:重载是静态的多态,在编译时,编译器就根据调用函数参数的类型进行了绑定,运行时速度快。(古时,指腹为婚)
运行时多态:当输入不同时,调动的类方法不同,实现运行时多态。
ps:
- 当基类的虚函数是常方法时,其派生类的虚函数也必须是常方法(同参数列表)
- 覆盖:虚函数的覆盖指的是将虚函数表里的函数地址覆盖
当编译时发现有虚函数,编译时会产生一个虚函数表(.存放在.data区,其元素是该类虚函数的入口地址),且还会在基类里产生一个指向这个虚函数表的指针vptr。且对于继承体系里,每一个层次里编译器都会产生一个虚函数表,且该派生类的虚函数表先对基类虚函数表拷贝,再在派生类虚函数内容覆盖。
每个类都有一个虚表指针,而通过Base * ba=& a 取得了a的虚表指针,其指针指向a的虚表,且该虚表的每一个元素是一个函数 ba>add(),不是像普通函数那样直接跳转到函数的代码处。
首先是取出vptr的值(此时vptr值为a的vptr值),这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数a::add是第一个虚函数,所以取出vtbl中第一个Slot的值即为第一个虚函数的地址,即a::add(),
即a::add是(Fun)*((int*)*(int*)(&a)),
当ba->fun是ba->(Fun)*((int*)*(int*)(&a)+1); 即通过取子类对象的指针得到子类的虚表指针,如这里的a。
而此时ba能通过获取子类a的虚函数指针来找到其函数,并且可能ba内没有虚函数表里的内容,当ba还去试图寻找其没有的内容时,此时就会编译失败,因为对于ba来说,其方法只有类内定义函数的部分。即ba的虚函数指针只能访问自身的方法。(也相当于赋值兼容)。 当基类指针访问这个函数时相当于访问的是自己虚函数表里的函数。而不是简单的跳到该函数。
name mangling(名称粉碎技术)
名字粉碎技术为什么不包含返回类型?
返回类型针对的是临时量,而与其return返回的值无关。即返回的值一般是存放在eax里的,而eax存放的是值,不关心你的值是什么类型,而在函数调用之后会将eax赋值给一个变量,如a=fun();所以在这里eax就是那个临时量,这也是为什么临时量具有常性的原因,而它的类型,其实在编译过程就确定了,因为如char int 比较 先写入寄存器,而写入寄存器之前,其类型就被转换了,即这里发生了隐式转换。而这种转换是在编译阶段,编译器器就会检测出来,然后进行改写,相当于添加了强制转换了,然后在运行时,才进行写入寄存器,使得能正常的比较。所以返回值类型是在编译时就会确定下来的,即eax只管传值,而编译器早就每个地方都写入了类型。
11 静态成员 设计成类成员的原因是限制其作用域,不能被其他修改
其不在对象(图纸)内,其在.data区
在c++类中声明成员时可以加上static关键字,这样声明的成员就叫做静态成员(包括数据成员和成员函数)。
与函数中的静态变量有明显差异。类的静态成员为其所有对象共享,不管有多少对象,静态成员只有一份存于公用内存中。
1.静态数据成员 其处于.data区,计算类大小时,不会计算其大小,.data区在初始化阶段就处理了
该类产生的所有对象共享系统为静态成员分配的一个存储空间**,而这个存储空间是在编译时分配的,在定义对象时不再为静态成员分配空间。**
ps:静态成员不能使用初始化列表进行初始化,其不是被各个对象私有的,是共享的,若每次进行初始化,将破坏了该静态性,共享性,另外初始化是对象的创建,对象的构建不包含该静态变量
如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化
静态数据是该类所有对象所共有的,可提供同一类的所有对象之间信息交换的捷径(统计构建对象的个数)
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
2.静态成员函数(没有this指针,不面向对象,不能定义为常方法)
普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}
友元函数 使用友元使得,它的朋友能来类家里玩。
友元函数只是让该函数成为了类的友元,但其还是外部函数,其需要传入类对象才能进行访问。
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend
C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。
友元的三个特性:
1.没有自反性,是单向的,我是你的友元,但你不是我的友元。我能访问你,但你不能访问我。
2.没有传递性 。 我是你的友元,你是他的友元,但他不是我的友元。
3.不具有继承性。
成员函数友元
class B;
class A
{
private:
int sum;
public:
void Print(B &a);
};
class B
{
private:
int num;
public:
//当将一个类作为类型传入另一个类的方法时
friend void A::Print(B &a); //使得A类的方法能访问B的成员,此时需要A在B之前
};
//该方法需要在B类之后
void A::Print(B &a) //即传入B类对象,操作B类的数据
{
cout << a.num;
}
int main()
{
A a;
B b;
//传入B类对象
a.Print(b);
return 0;
}
12 模板
template<class Type> //typedef
const Type & Max(const Type & a, const Type & b)
{
return a > b ? a : b;
}
int main()
{
//int x = Max(12, 28.3); 出错
int x = Max<int>(12, 28.3);//明确告诉编译器使用int,若不告诉则编译器自己推演
//int x = Max(12, 28);//编译器将template<class Type> 转换为typedef int Type ,而不是替换?为什么
double dx = Max(12.23, 54.23);//编译器将template<class Type> 转换为typedef double Type
char ch = Max('a', 'b');//编译器将template<class Type> 转换为typedef char Type
}
模板的重载
与函数的重载类似,参数个数和参数类型(名字粉碎技术)。
模板函数推演(编译器)根据实参类型推演形参类型(typedef)
//这是错误的
#include<iostream>
using namespace std;
template<class T>
void fun(T a)//当进行模板转换时发生了冲突
{
}
template<class U>
void fun(U a)
{
}
int main()
{
fun(12);
}
#include<iostream>
using namespace std;
template<class T>
void fun(T a)//当进行模板转换时发生了冲突
{
T x,y;//不是替换而是typedef T x ,T y
}
int main()
{
int a=10;
int *p=&a;
fun(a);
fun(&a);
}
函数模板的特化:
#include<iostream>
using namespace std;
template<class T>
void fun(T * a)//*与变量名结合,即形参此时已经是指针
//模板函数推演(编译器)根据实参类型推演形参类型
{
T x,y;
}
int main()
{
int a=10;
int *p=&a;
fun(a);//错误,int 无法转int*,加一个模板fun(T a)则正确
fun(&a);//正确,但编译器根据实参和形参变换 T为int
}
#include<iostream>
using namespace std;
template<class T>
void fun(T a)//一般模板函数,只要是类型就行
{
T x,y;
}
template<class T>//部分特化模板函数
void fun(T * a)//只接受指针类型
{
T x,y;
}
template<class T>
void fun(char * a)//完全特化模板函数,只接受char *类型
{
}
template<class T=int,class N=3>
void fun(T a)
{
T array[N];
}
//fun(3) 其产生了一个 int array[3]
模板的使用注意
template <class _T1, class _T2>//重载模板函数
inline void construct(_T1* p, const _T2& value)//为什么要这么写,如果两个参数都是T1,开辟的是int * 类型,而你传入的另一个参数是double,就会造成模板错乱,这样写虽然使得多了一个类型,但是必不可少的
{
new (p) _T1(value);//为什么是T1而不是T2呢,因为是T1类型的空间,它需要的类型是T1的数据,那为什么还要传入T2呢?
}
13 STL
1. vector(算头不算尾)
从第一个元素到最后一个元素或,到end()-1个元素
vector简介:
- vector是表示可以改变大小的数组的序列容器。
- 与数组一样,vector对元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问它们的元素。
- vector与数组不同,它们的大小可以动态变化,容器会自动处理它们的存储。
- vector使用一个动态分配的数组(堆)来存储它们的元素。在插入新元素时存储空间可能需要重新分配,以便增大大小,这意味着分配一个新数组并将所有元素移动到其中。就处理时间而言,这是一项相对昂贵的任务,因此,向量不会在每次向容器添加元素时重新分配。
- 相反,vector容器可以分配一些额外的存储空间以适应可能的增长,因此容器的实际容量可能大于严格需要的存储容量(即容器的大小)。
- 对于不同的插入位置,可以在不同的时间间隔内实现不同的插入策略,但只能在不同的位置上实现内存大小的平衡。(与其他动态序列容器(deques、list和forward_list)相比,vectors 可以非常高效地访问其元素(就像数组一样),并相对高效地从其末尾添加或删除元素。对于在结尾以外的位置插入或删除元素的操作,其性能较差。)
- vector与array相比,向量消耗更多的内存,以换取管理存储和以高效方式动态增长的能力。
构造函数
#include<vector>
using namespace std;
class Object
{
private:
int value;
public:
Object(int x = 0) :value(x) {}
~Object(){}
};
int main()
{
int ar[] = { 12,23,34,45,56,67,87,90,100 };
int n = sizeof(ar) / sizeof(ar[0]);
vector<int> veca;//构造函数,没有空间
vector<int> vecb(10);//构造函数,开辟10个空间,每个空间值为0
vector<int> vecc(10, 23);//构造函数,开辟10个空间,每个空间值为23
vector<int> vecd( ar, ar + n);
int br[]{ 1,2,3 };//只能在VS2017以上版本 C++标准
vector<int> vecf{ 1,2,3 };
vector<int> vecg={ 1,2,3 };
vector<Object> vech{Object(10),Object(20)};//容器----模板类
}
}
1. 迭代函数
iterator begin()
//返回指向起始的迭代器
iterator endo()
//返回指向末尾的迭代器
reverse_iterator rbegin()
//返回指向起始的逆向迭代器(最后一个元素)
reverse_iterator rend()
//返回指向末尾的逆向迭代器(第一个元素,但其++)
void ForwardPrint(const vector<int> &vec)
{
vector<int>::const_iterator it = vec.begin();//约束了*it 常性迭代器
for (; it != vec.end(); it++)//for(auto &x:vec) {cout <<x;} //只能正向访问
{
cout << *it;// 类似指针迭代器每一次都会检测是否越界,指针不会检测
}
}
int main()
{
vector<int> vecg = { 1,2,3 };
vector<int>::iterator it = vecg.begin();//普通迭代器
ForwardPrint(vecg);
//for (; it != vecg.end(); it++)
//{
// cout << *it;// 类似指针迭代器每一次都会检测是否越界,指针不会检测
// }
return 0;
}
逆向迭代器
int main()
{
vector<int> vecg = { 1,2,3 };
vector<int>::reverse_iterator rit = vecg.rbegin();//逆向迭代器 一般用rit 常性逆向迭代器const_reverse_iterator rit
for (; it != vecg.rend(); rit++) // 依然是++ 逆向打印
{
cout << *rit;// 类似指针迭代器每一次都会检测是否越界,指针不会检测
}
return 0;
}
2.容量函数
ps:
- 容器的capacity(容量) != size(元素个数)
- max_size() 最大元素个数,只是一种理论数,可当计算当前内存大小(4G )
- reserve 预留100个容量(空间),但其空间内没有对象(通过指针来实现),迭代器检测中止。
- vector只有shrink_to_fit才有缩减容量的能力,但会产出内存碎片。
3 .插入函数
ps:
- clear只是清除了对象,而空间还存在。
- 当进行了插入和删除元素时,都需要重新对迭代器进行重置
#include<vector>
#include<iostream>
using namespace std;
template<typename T>
void ForwardPrint(const vector<T> &vec)
{
cout << "size:" << vec.size() << "\t" << "capacity" << vec.capacity()<<endl;
vector<int >::const_iterator it = vec.begin();
for (; it < vec.end(); it++)
{
cout << *it<<" ";
}
cout << endl;
}
int main()
{
int ar[]{ 2,3,4,5,6 };
vector<int> vec{ 12,23,34,56,78 };
int n = vec.size();
vector<int>::iterator it = vec.begin();
vec.insert(it, 10);//如不是对末尾操作,其进行了移位操作
//cout<<*it;//错误,迭代器失效 无论是否进行了增容,还是没有增容,用户不知道其增容没,所以都将进行迭代器的重置
it = vec.begin();//当进行插入和删除时迭代器失效,需要进行it 的重置,才能再次进行插入
cout<<*it;//错误,迭代器失效
//vec.insert(it,10,23)//插入10个23
//vec.insert(it,ar,ar+n);
//vec.insert(it,{1,2,3,4,5,6});//插入列表元素
vec.push_back(100);//尾部加入元素100 O(1)
vec.pop_back();//尾部删除
ForwardPrint(vec);
return 0;
}
4 .元素访问
int *p=vec.data();//指针p指向vec的首地址 _Myfirst
it=vec.begin();//返回指向起始的迭代器
5.vector中的resize, reserve , assign 的区别 *
void reserve (size_type n);
reserver函数用来给vector预分配存储区大小,即capacity的值 ,但是没有给这段内存进行初始化。reserve 的参数n是推荐预分配内存的大小,实际分配的可能等于或大于这个值,注意是推荐,如果小于capacity该方法不做任何事。。
当调用函数时,n的值如果大于capacity的值,就会重新分配内存 ,使得capacity的值会大于n 。这样,当调用push_back函数使得size 超过原来的默认分配的capacity值时避免了内存重分配开销。
需要注意的是:reserve 函数分配出来的内存空间未初始化对象,只是表示vector可以利用这部分内存空间,但vector不能有效地访问这些内存空间,如访问的时候就会出现越界现象,导致程序崩溃。
void resize(const size_type n);
void resize(const size_type n, const T& x)
resize函数重新分配大小,改变容器的大小,并且创建对象。
当n小于当前size()值时候,vector首先会减少size()值保存前n个元素,然后将超出n的元素删除(remove and destroy)
当n大于当前size()值时候,vector会插入相应数量的元素使得size()值达到n,并对这些元素进行初始化0,如果调用上面的第二个resize函数,指定val,vector会用val来初始化这些新插入的元素。
当n大于capacity()值的时候,会自动分配重新分配内存存储空间。
void assign( size_type n, const T& x );
void assign( InputIt first, InputIt last );
将n个值为x的元素赋值到vector容器中,或者将区间[first,last)的元素赋值到当前的vector容器中,这个容器会清除掉vector容器中以前的内容。 如果n大于当前容器,重新开辟内存,然后就将当前容器的所有元素进行重写
ps:末尾迭代器指向的是该元素的下一个元素,指针指向元素,所以直接传入指针就有问题,所谓算头不算尾
ps: 由于容器无法设计释放指针指向的空间,因为指针指向的空间可能是堆,也可能是栈,即当摧毁该容器时,需要在申请了资源时,自己释放该对象,然后在析构。
vector当使用auto_ptr时,当对该容器元素赋值时,元素指向了NULL,但元素个数不变。unique_ptr时,当对该容器元素赋值时,元素指向了NULL,但元素个数-1.。
哈希
ps:数组和链表的组合体 —哈希
双端队列 deque
deque系由一块一块的固定大小的连续空间构成(块与块之间是不连续)。一旦有必要在deque的前端或尾端增加新的空间,便配置一块固定大小的连续空间,串接在整个deque的头端或尾端。
deque的最大任务,便是在这些分块的固定大小连续空间上,维护其整体连续的假象,并提供随机存取的接口(随机迭代器),代价则是迭代器架构较为复杂。
deque 采用一块所谓的_M_map (注意,不是STL的map容器)作为主控。这里所谓_M_map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。
迭代器
主体
分为对头队尾迭代器 注意:当队头M_cur与_M_frist相同时不开辟内存,因为此时M_cur指向头
多态性与虚函数
多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。
早期绑定,在编译时即确定了函用函数的地址。名字修饰技术。
晚绑定,虚函数,编译时为继承层次里的每一个层次都构建了一个虚函数表(该表在.rodata)也相当于一个跳表(存放了各个虚函数的地址),而一般函数在.txt ,即此时编译时无法确定调用函数的地址,该函数虽然仍在.txt区,但其地址可以通过跳表,在运行时基类指针确定指向哪个表来调动不同的函数(类方法的地址不仅仅是与该对象之间的关系了,且这种关系是指向关系,是在运行时才能确定这种关系,才知道指向哪个表)!
纯虚函数
纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。 如水果,人,食物
定义纯虚函数的一般格式为:
virtual返回类型函数名(参数表) =0;
“=0”表明程序员将不定义该函数,函数声明是为派生类保留一个位置。“=0”本质上是将指向函数体的指定为NULL.
抽象类
含有纯虚函数的基类是不能用来定义对象的,但可以定义指向对象的指针和引用。 (如void也不能定义对象,其也是抽象的)
纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象类。
带有纯虚函数的类称为抽象类。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
ps:只要子类没有完全实现基类纯虚函数的实现,其就是一个抽象类,所以需要利用桥模式
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()//纯虚函数的实现
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()//纯虚函数的实现
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
抽象类的规定
( 1)抽象类只能用作其他类的基类,不能建立抽象类对象。
(2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
(3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
虚函数和纯虚函数有以下所示方面的区别。
类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现,这就像Java的接口一样通常把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。抽象类和大家口头常说的虚继承基类还是有区别的,在C#中用abstract定义抽象类,而在C++中有抽象类的概念,但是没有这个关键字。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
虚基类:“virtual”继承的类,也就是说任何类都可以成为虚基类。
抽象类:至少包含一个纯虚函数的类,其不能被实例化,哪怕该纯虚函数在该类中被定义。二者没有任何联系。
虚基类∶就是解决多重多级继承造成的二义性问题。
动态联编
联编是指计算机程序自身彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把一条消息和一个对象的操作相结合的过程
如果使用基类指针或引用指明派生类对象并使用该指针调用虚函数(成员选择符用箭头号“->”),则程序动态地(运行时)选择该派生类的虚函数,称为动态联编。动态联编(dynamic binding)跌称滞后联编(late binding)
如果使用对象名和点成员选择运算符“."引用特定的一个对象来调用虚函数,则被调用的虚函数是在编译时确定的(称为静态联编)
//编译时确定类成员的可访问性和各个方法的缺省参数
#include<iostream>
using namespace std;
class Object
{
public:
virtual void print(int a = 10)
{
cout << "Object::printf a:" << a << endl;
}
};
class Base :public Object
{
private:
virtual void print(int b = 100)
{
cout << "Object::printf b:" << b << endl;
}
};
int main()
{
Base base;
Object *p = &base;
//编译时当函数在被调用时对其进行分配地址,而此时在编译时根据p的类型对函数进行了解析,即确定了该函数的可访问性,编译时函数使用了缺省参数,此时该参数会被编译器记录在代码区(常量),而当类型对象运行时调用函数,该常量被直接压入栈中使用,而当运行到访问p->print();时 虽然其是private的,但是此时是运行阶段,编译器是在运行之前对程序进行检测,所以无法检测出来
p->print();
return 0;
}
ps: 因为p是Object类型,p指向子类,公有继承赋值兼容可以,而其print是公有的,可以访问,能编译通过,而在运行时,对print进行了动态联编查找虚表此时不进行检测其函数的可访问性和参数
程序编译链接成功才能进行执行,而执行过程中不会进行编译链接检查
RAlI与智能指针
1.什么是 RAlI
- RAlI(Resource Acquisition /s lnitialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字,互斥量,文件句柄够等(需要进行获取和释放),局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。*
裸指针:即原始指针,需要自己分配内存,然后显示释放的指针。
仿函数:可以使用类重载()运算符,使得对象()调动该仿函数完成特定的功能
auto_ptr可以由shared_ptr或unique_ptr替换,具体取决于具体情况
shared_ptr:如果您担心释放资源/内存,并且如果您有多个函数可以使用该对象AT-DIFFERENT次,那么请使用shared_ptr。
unique_ptr:如果您只关心释放内存,并且对对象的访问是SEQUENTIAL,那么请转到unique_ptr。
- RAll的原理
资源的使用一般经历三个步骤:a.获取资源(创建对象)
b.使用资源
c.销毁资源(析构对象)
但是资源的销毁往往是程序员经常忘记的一个环节,所以程序界就想如何在程序员中让资源自动销毁呢?解决问题的方案是:RAII,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。给一个简单的例子来看下局部对象的自动销毁的特性:
#include<iostream>
using namespace std;
class Object
{
public:
Object()
{
cout << "Create Object:" << this << endl;
}
~Object()
{
cout << "Destory Object:" << this << endl;
}
};
void fun()
{
Object obj;//将对象变为局部对象,即可以由OS来管理其的生命期进行析构和构造
return;
}
int main()
{
Object ob;//必须为局部对象
return 0;
}
智能指针
C–里面的四个智能指针 auto_ptr、 unique_ptr、 shared_ptr, weak_ptr其中后三个是C++11支持,并且第一个已经被C++11弃用。
智能指针对普通类型没用,作用是当需要元素指向一个需要分配和释放的资源时才有用,如果容器里存的是一个类对象,而该对象使用了资源,当不用该容器时等,会将该容器的对象析构,即不用担心发生内存泄露。
C++的auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
auto_ptr 使用场景不明
所以也不要让智能指针对象指向一个堆,不然又需要自己释放该堆资源。
//一般情况下
void fun()
{
int *p=new int(10);
*p=100;
cout<<*p<<endl;//可能会忘记了释放,即需要程序员进行自动释放
}
tip:利用局部对象类的构造和析构是由OS管理的,可以实现对堆的自动分配和释放
#include<iostream>
using namespace std;
namespace sqh
{
template<class T>
//当对象是局部对象时能自动申请资源和释放资源,且该资源当需要给其他人使用时也可以进行转移,
//只有拥有该权限的对象才能释放资源,其他对象死亡也不能释放资源。即可以达到当不会使用该资源时才会释放
//但是各个对象写数据都是写在这个资源里
class auto_ptr//该智能指针是我改进的
{
private:
bool Owns;//拥有资源是唯一的 需要保证该变量在设计之前
T * ptr;
public:
typedef T element_type;
public:
auto_ptr(T * p=nullptr) :ptr(p),Owns(p!=nullptr) {}//即没有申请空间时没拥有该资源
//auto_ptr<int>p 此时为空 auto_ptr<int> p(new int(10)) 此时拥有
~auto_ptr()
{
if(Owns)//需要拥有着才能释放该资源,防止没在使用该资源的对象释放了该资源
//即就算使用该资源的对象生命期结束,也不会释放该资源,相当于转移了
delete ptr;
ptr = NULL;
}//当没人拥有该资源时,释放该资源
T *Getptr()const
{
return ptr;
}
T & operator*()
{
return *Getptr();
}
T * operator->()
{
return Getptr();//直接返回地址,是指针嘛->前面,是指向该对象的指针
}
T * release() const//将资源转移了 const *const this 去常性 *const this
{
_Ty *tmp = NULL;
if(_Owns)
{
((auto_ptr<_Ty> *) this)->_Owns = false;
tmp = _Ptr;
((auto_ptr<_Ty> *) this)->_Ptr = NULL;
}
return tmp;
}
void reset(_Ty *p = NULL)//重载拥有的资源
{
if(_Owns) // _Ptr ->Object;
{
delete _Ptr;
}
_Ptr = p;
}
auto_ptr(const auto_ptr &aip):Owns(aip.Owns), ptr(aip.release()){}//需要确保Owns设计在前 构造函数可以使用列表方案
auto_ptr &operator=( auto_ptr<T> &aip)//方法内可以使用类内的成员数据,封装将实现在方法内实现,而不能在用户处实现,;类内不存在什么可访问性,但也不能定义一个该类的对象在该类里
{
if (&aip != this)
{
if (aip.ptr != ptr&&aip.Owns==true)//此时其两个对象的资源不是同一块,且aip拥有该资源
//此时需要delete a的资源?那怎么确定a资源是否还有其他对象在使用呢,还需要一个计数器来技术的吧,不用因为此时只有a能使用资源,当其他对象需要这个资源时需要赋值来转移
{
delete ptr;
ptr =aip.release();//实现了指向同一块heap,而不担心其他对象死亡时释放该空间
Owns = true;//此时a获得资源
}
else if (aip.ptr == ptr && aip.Owns == true)//指向同一个资源,此时将该资源给另外一个就可以了
{
aip.Owns = false;
Owns = true;
}
else if (aip.Owns != true&&aip.ptr!=nullptr)//最后释放的时候是拥有该资源的对象进行释放,那么其他对象都可以修改这个资源里的值啊,还需要进行保护多线程的情况下
{
ptr = aip.ptr;
Owns = false;
}
else
{
cout << "其赋值对象是null,不进行转移" << endl;
}
}
return *this;//确保可以连=
}
};
//template<class T>//模板需要在最近的一个域内
//T * operator*()
//{
// return *ptr;
//}
}
class Object
{
private:
int value;
public:
Object(int x = 0) :value(x){}
~Object() {}
int &Value() { return value; }
const int &Value()const { return value; }
};
void fun()
{
//sqh::auto_ptr ip(new int(10));
sqh::auto_ptr<int > ip(new int(10));//new 放在用户端,根据用户具体如何使用new 函数会先计算实参表达式再进行传参
int a = *ip;
//int * p=new int(20);
//delete p;
sqh::auto_ptr<double> dp(new double(12.23));//由于堆内存需要获取资源和释放资源
double b = *dp;
*dp = 1;
sqh::auto_ptr<Object> cp(new Object(2));//管理一个对象,需要获取资源和释放资源 为该对象申请一个空间
Object * c = new Object(2);
a = (*cp).Value();
a = cp->Value();//此时->被调动了两次 一次重载,cp.operator->() 当返回为地址时再调用->Value ,编译器不允许写两次->
//cp. 可以访问其所在的类的方法 .相当于偏移
//cp-> 可以访问指向对象其所在的类的方法 因为->的本意就是让指针访问其指向的数据
//重载的->被回退用了两次
//cp->->Value();error 语法错误,这与->本身出现了矛盾,在编译时语法上错误的
//cp.operator->()->Value;//虽然编译后会使其改写成为这样 即系统自动添加了-> 而->->在语法上是错的
//cp.operator->()Value;
(cp.Getptr())->Value();
c->Value();
//资源的转移,虽然还是共享该资源,但是其他对象死亡时,不会影响该资源
sqh::auto_ptr<int> iap(new int(10));//此时iap拥有了资源
sqh::auto_ptr<int> ibp(iap);//此时ibp不拥有资源 拷贝构造是对需要构造的对象进行构造
//ibp(iap); error 此时系统以为是调用函数,即拷贝构造函数需要对未构造的对象进行构造,其也是一种构造,不能对已经拥有对象再次进行构造
ibp = iap;//因为资源的拥有是唯一的,所以此时ibp获得资源而iap失去资源,但资源没释放,相当于转移了
sqh::auto_ptr<int> icp;
iap = icp;
}
int main()
{
fun();
return 0;
}
1 构造函数与析构函数
**auto_ptr在构造时获取对某个对象的所有权(ownership),在析构时释放该对象。**我们可以这样auto_ptr 来提高代码安全性:
int *p=new int(0);
auto ptr<int> ap(p);
从此我们不必关心应该何时释放p,也不用担心发生异常会有内存泄漏。
auto_ptr的注意事项(所以C11将其舍弃了)
1)因为auto_ptr析构的时候肯定会删除他所拥有的那个对象,所以我们就要注意了,一个萝坑,**两个auto_ptr 不能同时拥有同一个资源(对象,堆内存,互斥量)。**像这样:
int *p=new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);//此时申请的堆区资源有了两个拥有者
因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p,两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr。想再次使用该申请的资源只能通过赋值和拷贝构造,不能使用构造
2)
string *sar = new String[10];
auto_ptr<String> ap(sar);
因为 auto_ptr的析构函数中删除指针用的是delete,而不是delete[].所以我们不应该用auto_ptr来管理—个数组指针。
3 构造函数没有explicit 关键词,不能有效地阻止从一个"裸"指针隐式转换成auto_ptr类型。
拷贝构造与赋值
**与引用计数型智能指针不同的, auto_ptr 要求其对裸"指针的完全占有性。也就是说一个裸"指针不能同时被两个以上的auto_ptr 所拥有。**那么,在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。auto_ptr做法是"所有权转移",即拷贝或赋值的源对象将失去对"裸"指针的所有权,所以,与一般拷贝构造函数,赋值函数不同,auto_ptr 的拷贝构造函数,赋值函数的参数为引用而不是常引用(constreference).当然,一个auto_ptr 也不能同时拥有两个以上的裸"指针,所以,拷贝或赋值的目标对象将先释放其原来所拥有的对象。
这里的注意点是:
- 因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个 auto_ptr 的提领(dereference)操作是不安全的。如下:
int *p=new int(0);
int *p2=new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2=ap1;//这会进行拷贝构造函数 ap1=p2; 会进行构造函数,然后在进行赋值拷贝
cout<<"*ap1";//是错误的,此时ap1已经指向null
2)还有一个比较隐蔽的问题 当auto_ptr 的对象作为参数传递时,若不使用地址传递,则会发生提前释放资源!!
int *p=new int(0);
auto_ptr<int> ap1(p);
fun(ap1);//当其作为函数的实参进行传参时,进行了拷贝构造,相当于其控制权已经转移
cout<<"*ap1";//错误,此时ap1已失去p指针的拥有权,这很隐蔽,且当形参结束时就会将该资源释放!!!
3) 继承里会发生隐式转换
那么下列代码就可以通过,实现从auto_ptr到auto_ptr的隐式转换,因为Base可以转换为Object类型
class Object{};
class Base:public Object{};
auto_prt<Object> apobj = auto_ptr<Base> (new Base) ; //此时父对象拥有了子对象的拥有权,而不是父对象的拥有权。而子对象有些资源可能不能被释放
4 )因为auto_ptr 不具有值语义(value semantics),所以auto_ptr不能被用在stl标准容器中。
vector<int *>vec;
vec.push_back(new int(10));
vec.push_back(new int(20));//当销毁时需要对vec里的内容动态销毁,但vector不具有将其元素内容即指针所指之物销毁释放的能力
vector<auto_ptr<int> >vec;//错误,虽然想法是好的,即将vec里元素申请的资源可以自动申请释放,但其不支持
什么是值语义?简单的说,所有的内置类型(primitive variables)都具有value semantics。有人也称它为POD (plain old data),也就是旧时的老数据〈有和OOP的新型抽象数据对比之意)。
对一个具有值语义的变量赋值可以转换成内存的 bit-wise-copy(按位拷贝)。 ???
什么样的类没有值语义呢﹖我们不妨称这种型为none-value-semantics type (INVST).
借鉴了boots库
unique_ptr.
unique_ptr(唯一)是一种定义在中的智能指针(smart pointer)。不能进行复制操作只能进行移动操作。
unique是独特的、唯一的意思,故名思议,unique_ptr 可以"独占"地拥有它所指向的对象,它提供一种严格意义上的所有权。
unique_ptr和 shared_ptr类型指针有很大的不同: shared_ptr 允许多个指针指向同一对象(多个shared_ptr 指向同一个资源),而 unique_ptr在某一时刻只能有一个指针指向该资源(两个unique_ptr 不能指向同一个资源)。unique_ptr保存指向某个对象的指针,当它本身被删除或者离开其作用域时会自动释放其指向对象所占用的资源。
1、如何创建unique_ptr
unique_ptr不像 shared_ptr一样拥有标准库函数 make_shared来创建一个shared_ptr实例。要想创建一个unique_ptr,我们需要将一个new 操作符返回的指针传递给unique_ptr 的构造函数。
int main()
{
unique_ptr<int> pInt(new int(5));//与auto_ptr一样
cout<<*pInt;
}
2、unique_ptr无法进行拷贝构造和赋值操作
unique_ptr没有copy构造函数,不支持普通的拷贝构造和赋值操作。 确保了其唯一性。
int main()
{
unique_ptr<int> pInt(new int(5));//与auto_ptr一样
unique_ptr<int> pInt2(pInt);//报错
unique_ptr<int> pInt3 = pInt;//报错 确保了其唯一性
}
如何实现将拷贝构造和赋值操作禁止
//在C11以前将拷贝构造和赋值放在私有,对象无法访问,以防止系统产生默认拷贝构造和赋值
private:
unique_ptr(const auto_ptr &);//有声明没实现,此时告诉编译器不要默认拷贝构造。即对象无法调动该函数。
unique_ptr & operator=(const auto_ptr &));
//在C11之后
unique_ptr(const auto_ptr &)=delete;
unique_ptr & operator=(const auto_ptr &))=delete; 即明确系统删除了该函数
std::move
在C++11中,标准库在中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
unique_ptr仿写 unique意义明确,只要赋值了,其就会消失,一个对象独占一个资源。
#include<iostream>
#include<vector>
using namespace std;
#pragma warning (disable:4996)
namespace sqh
{
//为什么要加模板,因为可以针对多种类型,其是多种类型的删除器
template<class Tp>
struct get_delete//类型加括号,构造函数,构造一个删除器对象,在调用仿函数来调动实现特定的函数功能
{
void operator() (Tp *ptr)//删除时,会根据传入的参数为删除器,然后构造一个无名删除器执行仿函数,进行删除资源
{
delete ptr;//实现删除一个堆区
ptr = nullptr;
}
};
template<class Tp>
struct get_delete<Tp[]>//类型加括号,构造函数,构造一个删除器对象,在调用仿函数来调动实现特定的函数功能
{
void operator() (Tp *ptr)//new Tp[10] 模板特化,即在类型处说明其特化,在编译器解释时选择其进行重载
{
delete [] ptr;//实现删除一块连续的堆区
}
};
//template<>//错误想法
//struct get_delete<FILE>//类型加括号,构造函数,构造一个删除器对象,在调用仿函数来调动实现特定的函数功能
//{
// void operator()(FILE *ptr)
// {
// fclose(ptr);//实现删除一个文件
// ptr = nullptr;
// }
//};
//且其是删除器的一种
struct FILE_delete//文件类型只有这一种啊,为什么你要做模板呢。。
{
void operator()(FILE * Ptr)
{
fclose(Ptr);
Ptr = nullptr;
}
};
template<typename Tp,typename Dp= get_delete<Tp>>//类型加括号构造,在使用构造的对象,对象调动仿函数完成特定功能。
class unique {
public:
typedef Tp element_type;
typedef Tp * pointer;
typedef Dp delete_type;
explicit unique(Tp *p=nullptr):Ptr(p) {}
unique(unique &&p):Ptr(p.relase()){}
unique &operator=(unique &&p)
{
if (this != &p)
{
reset(p.relase());
}
return *this;
}
~unique()
{
if (Ptr != nullptr)
delete_type()(Ptr);
// get_delete()();
}
Tp* reset(Tp* p) //释放当前资源,重新获取一个资源
{
get_delete()(Ptr);
Ptr = p;
return Ptr;
}
Tp* relase()//该对象资源放弃,将资源转移给另一个对象
{
Tp* tmp = Ptr;
// get_delete()(Ptr);
((unique *)this)->Ptr = nullptr;
return tmp;
}
void swap(unique &q)
{
std::swap(Ptr, q.Ptr);//auto_ptr也可以使用
}
Tp* get()const
{
return Ptr;
}
Tp &operator *()const
{
return *get();
}
Tp* operator ->()const
{
return get();
}
operator bool()const
{
return Ptr != nullptr;
}
private:
Tp *Ptr;
unique(const unique &p) = delete;
unique & operator=(const unique &p) = delete;
delete_type get_delete() { return delete_type(); }
};
template<typename Tp, typename Dp>//dp 删除器类型
class unique<Tp[],Dp> {
public:
typedef Tp element_type;
typedef Tp * pointer;
typedef Dp delete_type;
explicit unique(Tp *p = nullptr) :Ptr(p) {}
unique(unique &&p) :Ptr(p.relase()) {}
unique &operator=(unique &&p)
{
if (this != &p)
{
reset(p.relase());
}
return *this;
}
~unique()
{
if (Ptr != nullptr)
delete_type()(Ptr);
// get_delete()();
}
Tp* reset(Tp* p) //释放当前资源,重新获取一个资源
{
get_delete()(Ptr);
Ptr = p;
return Ptr;
}
Tp* relase()//该对象资源放弃,将资源转移给另一个对象
{
Tp* tmp = Ptr;
// get_delete()(Ptr);
((unique *)this)->Ptr = nullptr;
return tmp;
}
void swap(unique &q)
{
std::swap(Ptr, q.Ptr);//auto_ptr也可以使用
}
Tp* get()const
{
return Ptr;
}
Tp &operator *()const
{
return *get();
}
Tp* operator ->()const
{
return get();
}
operator bool()const
{
return Ptr != nullptr;
}
private:
Tp * Ptr;
unique(const unique &p) = delete;
unique & operator=(const unique &p) = delete;
delete_type get_delete() { return delete_type(); }
};
}
void fun()
{
sqh::unique<int> a(new int(30));
sqh::unique<int> b(new int(20));
sqh::unique<int> c(std::move(a));
sqh::unique<int[], sqh::get_delete<int []>>d(new (int[10]));//堆内没有初始化是随机值
FILE *e = fopen("sqh.text", "w");
sqh::unique<FILE, sqh::FILE_delete> f(e);
//即unique的两个参数,一个是需要的类型资源,一个是资源类型的删除器
}
int main()
{
fun();
return 0;
}
ps:即unique的两个参数,一个是需要的类型资源,一个是资源类型的删除器
对于独占unique 的理解
vector<unique_ptr<int>> a;
vector<unique_ptr<int>> b;
a.push_back(unique_ptr<int> (new int(20)));
a.push_back(unique_ptr<int> (new int(30)));
//此时a的容量还是为2,但是我们知道第二个对象已经释放了资源
unique_ptr<int> c = std::move(a.back());
// b = a; //error,不能这样写,其是独占的,要转移需要变为move
//此时a的容器已经没有任何资源
b = std::move(a);
unique的缺陷
shared_ptr(共享指针)
(1) (shared_ptr)的引用计数本身是线程安全(引用计数是原子操作)。
原子性:原子性的程序不允许打断,要么完成,要么失败。
std::atmoic<int> mCnt; //mCnt 计数是原子性的
mCnt++;//原子性,应该是重载了++ --运算符
mCnt--;
mCnt.load();//获得该技术的瞬间值
mCnt=mCnt+1; //这不是原子性的
(2) 多个线程同时读同一个shared_ptr 对象是线程安全的。
(3) 如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
(4) 多线程读写shared ptr所指向的同一个对象,不管是相同的 shared ptr对象,还是不同的 shared ptr对象,也需要加锁保护。
其对象有两个数据,一个指向资源(对象),另一个是计数,计数也分为两个数据,一个也指向资源(对象),另一个表示计数。
即share_ptr 包含了一个对象mpRefcnt
shared_ptr仿写
#include<iostream>
#include<vector>
#include<atomic>
#include<memory>
using namespace std;
#pragma warning (disable:4996)
namespace sqh
{
template<class Tp>
class RefCnt
{
private:
Tp* Ptr;
std::atomic<int> mCnt;
public:
RefCnt(Tp *p=nullptr):Ptr(p)
{
if (Ptr != nullptr)
mCnt = 1;
}
int load()
{
return mCnt.load();
}
int addRef()
{
return mCnt++;//原子性的封装
}
int delRef()
{
return mCnt--;
}
~RefCnt() {}
};
template<class Tp>
struct share_detele
{
void operator()(Tp* p)
{
delete p;
p = nullptr;
}
};
template<class Tp,class Dp=share_detele<Tp>>
class share_ptr
{
public:
typedef Dp delete_type;
typedef Tp * pointer;
public:
share_ptr(Tp *p=nullptr):Ptr(p)
{
//还需要去指向该计数
mpRefCnt = new RefCnt<Tp>(Ptr);//其指向这个计数对象
}
//对于对象资源不能使用浅拷贝,但对于共享指针可以指向同一个资源
share_ptr(const share_ptr &p):Ptr(p.Ptr), mpRefCnt(p.mpRefCnt)
{
if(Ptr!=nullptr)
p.mpRefCnt->addRef();
//mpRefCnt = p.mpRefCnt;
}
share_ptr & operator=(const share_ptr &p)
{
if (this != &p)
{
if (p.Ptr!=Ptr)
{
reset(p.Ptr);
}
//Ptr = p.Ptr;
p.mpRefCnt->addRef();
mpRefCnt = p.mpRefCnt;
}
}
share_ptr(share_ptr &&p) :Ptr(p.Ptr)
{
p.mpRefCnt->addRef();
mpRefCnt = p.mpRefCnt;
p.release();
}
share_ptr &operator=(share_ptr &&p)
{
if (this != &p)
{
if (p.Ptr != Ptr)
{
reset(p.Ptr);
}
//Ptr = p.Ptr;
p.mpRefCnt->addRef();
mpRefCnt = p.mpRefCnt;
p.release();
}
}
void reset(Tp * p)
{
get_delete()(Ptr);
Ptr = p;
mpRefCnt->delRef();
}
Tp &operator *() const
{
return *get();
}
pointer operator->() const
{
return get();
}
int use_count() const
{
return mpRefCnt->load();
}
operator bool()
{
return get() != nullptr;
}
bool unique()//判断是否独享该资源对象
{
if (Ptr != nullptr&&use_count() == 1)
return true;
return false;
}
~share_ptr()
{
release();
}
private:
//需要是*,指针,使得每一个智能指针共享(指向)该计数
RefCnt<Tp> * mpRefCnt;//若不使用指针,则会出现每个智能指针的计数是独立的
Tp * Ptr;//当编译时,对象就有两个空间,分别存放 Ptr和 mpRefCnt
delete_type get_delete() { return delete_type(); }
void release()
{
if (Ptr!=nullptr&&mpRefCnt->delRef() == 0)
{
get_delete()(Ptr);
//Ptr = nullptr;
//当没有使用该资源时还需要释放堆路开辟的计数
delete mpRefCnt;
//mpRefCnt = nullptr;
}
Ptr = nullptr;
mpRefCnt = nullptr;
}
pointer get()
{
return Ptr;
}
};
}
void fun()
{
sqh::share_ptr<int> a(new int(10));
{
sqh::share_ptr<int> b(a);//对于资源不能使用浅拷贝,不然会释放两次
}
}
int main()
{
fun();
return 0;
}
移动和拷贝对于shared_ptr的影响
shared_ptr<int>a(new int(10));
shared_ptr<int>b(a);//此时进行的是一般拷贝,会增加计数
//此时a为空
shared_ptr<int>c(move(a));//此时进行了移动拷贝,此时a对象资源被转移,被置空
int x = c.use_count();//2
shared_ptr<int>d(new int(20));
shared_ptr<int>e;
e = d;//此时进行的是一般赋值,会增加计数
shared_ptr<int>f;
//此时d为空
f= move(d);//此时进行了移动复制,此时a对象资源被转移,被置空
int z = f.use_count();//2
//此时操作a,d会出错
循环指向 引入了弱智能指针
class Child;//需要声明
class Parent
{
public:
shared_ptr<Child> child;
~Parent()
{
cout << "bye Parent" << endl;
}
void hi() const { cout << "Hello" << endl; }
};
class Child
{
public:
~Child()
{
cout << "bye Child" << endl;
}
shared_ptr<Parent> parent;
};
int main()
{
shared_ptr<Parent> par( new Parent());
shared_ptr<Child> pch(new Child());
}
此时当par生存期到达时,由于pch还存在,只是将其的计数-1,但不会影响pch的计数,且该资源不会释放。
而当pch生存期到达时,由于par的包含对象还指向该资源,只是将其的计数-1,将得到两个计数为1的死循环引用。即内存泄露。
解决循环引用的方法,使用弱智能指针
class Child;//需要声明
class Parent
{
public:
weak_ptr<Child> child;
~Parent()
{
cout << "bye Parent" << endl;
}
void hi() const { cout << "Hello" << endl; }
};
class Child
{
public:
~Child()
{
cout << "bye Child" << endl;
}
weak_ptr<Parent> parent;
};
3如何使用weak_ptr
weak_ ptr并没有重载operator->和operator*操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock 函数来获得shared_ptr示例,进而访问原始对象。
弱智能指针需要通过lock 函数才能访问其指向的对象
既然 weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr 指向的对象被释放掉这种情况。这时,我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr 指向对象是否存在呢?
C++中提供了lock函数来实现该功能。
如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr
关联性容器
当非关联容器里存储的只是该地址时,即需要通过其他结构体才能满足调用关系,此时可以使用关联性容器,map。
#include<unordered_map> //关键码非顺序map 哈希结构,将key值转换为数值0,1,2,3
#include<forward_list> //单链表 非关联
#include<map> //顺序map
#include<iostream>
#include<unordered_map>
#include<map>
using namespace std;
int main()
{
map<string, int> simap;
simap.insert(std::pair<string, int>("sqh", 23));
simap.insert(std::pair<string, int>("sqd", 26));
simap.insert(std::pair<string, int>("yhp", 16));
cout << simap["yhp"] << endl;
}
ps:在容器里禁止使用new来开辟空间,若容器是类对象则会调动构造函数(若构造函数时即使用网络、文件那就造成了资源浪费)
此时,对应哈希里的对象,不能使用唯一性指针指针,不然当在容器里使用时,当有对象访问容器,该对象将被转移。下次访问哈希无法访问到该对象。因为唯一性指针指针资源的转移需要进行右值转移,即需要move,此时容器内的资源已经被转移了。所以只能使用共享,共享不存在转移问题。
异常
当异常发生时,异常打乱了程序的运行过程,抛出异常时,抛出异常的该函数将不再进行,且会回收栈帧(调动析构函数析构对象)回到可以捕获异常,处理异常的函数,若最后主函数都没有能处理该异常,最后会将其抛给OS,杀死该进程。所以会造成连锁反应,因为抛出异常说明该函数出现问题。
class NotPayment
{
public:
NotPayment(const string &str)
{
str_ = str;
}
void What() const
{
cout << "Not Create Payment:" << str_.c_str() << endl;
}
private:
string str_;
};
Payment * Payment::Factory(const string &type)
{
if (type == "Ali")
return new AliPay();
else if (type == "Wechat")
{
return new Wechat();
}
else if (type == "Diret")
{
return new Diret();
}
else
{
throw NotPayment(type);
}
}
int main()
{
//此时需要特定的参数来生成特定的产品,且此时工厂功能(生产的产品一样)比较单一,若是工厂职责复杂需工厂模式
//对应不同产品不同的工厂,来使得职责单一化
unique_ptr<Factory> factory(new Factory());
shared_ptr<Payment> s;
try {
s = factory->GetPay("Ali");
Payment *s = Payment::Factory("Al");
}
catch(NotPayment &e)
{
e.What();
return 0;
}
s->pay(10);
return 0;
设计模式
简单工厂
当构建(参数和各个子类的方法等)的是统一的,而没有其他变化时,可以使用简单工厂,此时只用修改配置文件就能达到类的复用