一、封装:
方法:
1、使用类将数据成员和函数方法绑定在一起。
2、使用访问限制符进行限制封装。
作用:信息隐藏的作用,只提供一些必要的接口,而隐藏那些不必要的接口。避免受到外界的干扰和误用,从而确保安全
破坏封装:友元类可访问该类中的所有数据和函数。破化了类的封装性
二、继承
继承:
1、作用:通过子类(派生类)继承父类(基类)的成员,共享不同的东西,实现各自不同的东西,达到代码复用的作用。
继承是类型之间的建模
子类是父类的的一部分,子类可继承父类中的非static成员。
3、继承方式:
访问修饰符:
public——公有(所有权限)
protected—保护(仅在类中可访问,子类继承父类的public成员后,为protected属性,子类可访问)
private—–私有(仅在类中可访问,类外及子类中不可见(继承但不可访问))
成员属性和继承方式最终取权限小的:
即:
成员属性 继承方式 子类继承后的属性
public public–protected—private public–protected—private
protected public–protected—private protected–protected—private
private public–protected—private private—private—private
子类和父类属于不同的作用域,可出现同名的成员,但会构成重定义(隐藏),隐藏父类的成员,屏蔽对父类该成员的直接访问。若要调用父类的成员,则需加类名指定
注:在实际使用中最好不要定义同名的成员,防止出错。
public继承是一种is–a的关系,即子类是父类的一部分,子类可继承使用父类中所有的public成员。
4、赋值兼容规则:
可直接将子类对象赋给父类,即切片(将父类的部分切分给父类),此方法不是一种类型转换。可使用引用的方法接收验证
父类对象不可赋给子类,但可通过强转将父类的指针或引用赋给子类,但使用时还是会出现错误
5、构造函数问题:
注:子类由复合构成,初始化子类中父类的数据成员时,必须显示的调用父类的构造函数,
且当父类无缺省的带参构造函数时,必须在子类构造函的初始化列表中显示调用父类的构造函数,进行初始化父类的成员。
因为:调用子类的构造函数时,会在子类的初始化列表处,调用父类的构造函数,
若此处不显示调用父类的构造函数,则编译器也会寻找父类中无参的构造函数,但父类中已定义带参的构造函数,所以会因找不到匹配的构造函数出错。
缺省的带参函数 = 无参函数+带参函数。
6、拷贝构造问题:
子类初始化父类时,可直接将子类对象传给父类,父类对象会进行切片,将属于父类的部分切分下来进行初始化父类。
7、赋值运算符重载:
注:由于父类的赋值运算符重载函数,与子类的该函数属于同名函数,所以两个函数构成了重定义,子类的该函数隐藏了父类的函数。从而会默认调用子类的赋值函数(就近原则)。
所以在调用父类的赋值函数时,必须加上类域指定调用父类的赋值函数,否则会默认调用子类的赋值函数,从而构成递归。
8、析构函数:
子类和父类的析构函数名虽然表面上看到的相同,但在底层上实际被处理成同名的函数,两者构成了重定义。
所以显示调用父类的析构函数时,必须加上类域指定为父类的,否则会默认的调用子类的析构函数。
调用析构函数时,会先调用子类析构函数,后自动调用父类析构函数。
因为:构造函数时,先调用的是父类构造函数,后构造子类部分,根据栈的先进后出特性,所以不可先析构父类。
protected/private继承是一种has-a关系,类外不可访问,
对于私有继承,子类内不可访问父类成员,
保护继承,子类内可访问父类非私有成员。
列:
AA a;//父类对象
BB b; //子类对象
a = b; //子类对象赋给父类
不是类型转化:
b = a; //父类对象赋给子类,错误
AA & aa = b //可引用,说明未发生类型转换
9、如何定义一个不可继承的类?
解1:将类的构造函数定义成私有的。
原因:1、类的私有成员,外部不可见,子类也不可见。
2、子类是合成的,调用子类的构造函数时,会先调用父类的构造函数,但父类的构造函数不可见,所以无法实例化处对象。所以该类不可继承。
注:若类外要实例化出父类对象,则还需在父类中定义一个static接口函数,类外通过类域指定调用。才能实例化出对象。由于static成员是不继承的,所以子类还是无法调用父类的构造。
解2:将类定义成抽象类,抽象类无法实例化出对象。
分类:
1、单继承:子类只有一个直接父类的继承
2、多继承:子类有多个直接父类的继承方式
3、菱形继承:单继承和多继承的复合。
列:菱形继承
A
/ \
B C
\ /
D
A类中的数据被B,C类继承,B,C再被D继承,从而导致D类中有多个A中的数据成员,从而导致数据二义性和数据沉于问题。
虚继承:
方法:通过在继承方式修饰符前加virtual 关键字进行修饰,即为虚继承。
1、虚继承是为了解决菱形继承产生的二义性和数据沉于问题。
非虚继承时:同名的成员的数据成员相互独立,数值不同,使用时必须指定是哪个父类,否则会造成二义性问题,运行不通过,使用类与指定后可解决二义性问题,但无法处理数据沉于问题(类中有多个该数据成员)。
虚继承:对同名的数据成员操作,实际是对他们公共父类中的该数据成员操作,两父类的该数据成员值相同。从而可以解决数据二义性问题和数据沉于问题,类中有1个该数据成员
2、虚继承会产生虚基表,用于保存到父类的偏移量(地址)。
调用父类(B/C)中继承的成员时,先通过虚基表中的偏移量找到父类(A),然后再找到该成员,而不是直接在父类(B/C)中找到该成员(A类成员)。
当继承的父,子类中存在同名的虚函数时,且参数相同,返回类型相同(协变除外)则就构成了多态。
注:子类可不用加virtual关键字,因为子类会继承父类的虚函数特性
协变—返回类型不相同,但必须是类的指针或引用。
10、代码实例:
1、继承方式:
using namespace std;
class A
{
public:
A()
{}
protected:
void print()
{
cout << "输出" <<endl;
}
private:
int _a;
};
class B : public A
{
public:
B()
{}
void change()
{
//this->_a = 1;//错误父类私用成员,子类内不可见(不可访问)
print();//代码段不属于某一个类,不使用this指针调用,父类的保护成员,子类内部可访问
}
private:
int _b;
};
void test()
{
A a;
B b;
//b._a = 1;错误,父类私用成员不可见(不可访问)
//b.print();//错误,保护成员外部不可见(不可访问)
a = b;
A & aa = b;//支持,说明未发生隐式类型转换
//b = a;//父类对象不可赋给子类
size_t d = 0;
//int & t = d;//引用不支持,发生隐式类型转换
}
2、继承的构造函数问题:
//继承
#include<iostream>
using namespace std;
class A
{
public:
//A(const int& a)
// :_a(new int(a))
//{}
//缺省构造函数 = 无参构造+有参构造
A(const int& a = 0)
:_a(new int(a))
{
cout << "构造A" << endl;
}
~A()
{
cout <<"A"<< endl;
delete _a;
}
protected:
int* _a;
};
class B : public A
{
public:
//A类中无缺省的构造函数
//B(const int& a,const int& b)
// :A(a) //正确
// ,_b(new int(b))
//{}
//B(const int& a,const int& b)
//{
// A(a);//错误,在初始化列表时,会调用A类中的无参构造函数,但A类中无无参的构造函数,所以会报无默认的构造函数出错
// _b = new int(b);
//}
//A类有带缺省的构造函数时
//B(const int& a,const int& b)
//{
// A(a);//错误,在初始化列表时,会调用A类中的参构造函数,无参调用,实例化出a,此处再在调用父类构造,带参调用,又实例化出一个a,导致重定义问题
// _b = new int(b);
//}
//B(const int& a,const int& b)
//{
// _a = new int(a); //错误,在初始化列表时,会调用A类中的参构造函数,无参调用,实例化处_a后,已对_a开辟空间,此处不可再开辟空间
// _b = new int(b);
// cout << "构造B" << endl;
//}
//B(const int& a,const int& b)//正确
//{
// *_a = 1; //正确,在初始化列表时,会调用A类中的参构造函数,无参调用,实例化处_a后,此处进行初始化
// _b = new int(b);
// cout << "构造B" << endl;
//}
B(const int& a,const int& b)//正确
:A(a)
,_b(new int(b))
{
cout << "构造B" << endl;
}
//~B()//自动先析构子类,后析构父类
//{
// ~A();//错误,子类和父类析构函数,在底层上是同名的函数,即构成了重定义,子类隐藏了父类,导致递归出错。
// cout <<"B"<< endl;
// delete _b;
//}
//~B()//自动先析构子类,后析构父类
//{
// A::~A();//错误,显示析构父类后,再析构子类,子类析构后会自动再析构父类,导致出错
// cout <<"B"<< endl;
// delete _b;
//}
~B()//自动先析构子类,后析构父类---正确
{
cout <<"B"<< endl;
delete _b;
}
protected:
int* _b;
};
void test3()
{
B bb(0,0);
}
3、菱形继承
//菱形继承
#include<iostream>
using namespace std;
class A
{
public:
A()
{}
void Funa()
{
cout << "Funa()" << endl;
}
public:
int _a;
};
class B : public A //公有继承A
{
public:
B()
{}
void Funb()
{
cout << "Funb()" << endl;
}
public:
int _b;
};
class C : public A //单继承
{
public:
C()
{}
void Func()
{
cout << "Func()" << endl;
}
public:
int _c;
};
class D : public B,public C //多继承
{
public:
D()
{}
void Fund()
{
cout << "Fund()" << endl;
}
public:
int _d;
};
void test2()
{
A aa;
B bb;
C cc;
D dd;
cout << "类的大小" << endl; //函数(代码段)和静态成员(静态区)不属于某一个对象,是所有对象共有的,所以不算做对象的大小。
cout << "aa = " << sizeof(aa) << endl; //4
cout << "bb = " << sizeof(bb) << endl; //4+4
cout << "cc = " << sizeof(cc) << endl; //4+4
cout << "dd = " << sizeof(dd) << endl; //8+8+4
aa._a = 0;
//dd._a = 10;//错误,D类继承有多个数据_a,存在二义性问题
//dd.A::_a = 1; //错误,A不是D的直接父类,所以A不算是D的父类,不可直接访问A类成员
dd.B::_a = 1; //通过类域,指定访问B类中的_a成员,可解决二义性问题,但D类中有多个_a成员,存在数据冗余问题。
dd.C::_a = 2; //D类中的两个_a成员数据不同
dd._b = 3;
dd._c = 4;
dd._d = 5;
cout << dd.B::_a << endl;
cout << dd.C::_a << endl;
cout << aa._a << endl;
}
4、菱形虚继承
//菱形虚继承
#include<iostream>
using namespace std;
class A
{
public:
A()
{}
void Funa()
{
cout << "Funa()" << endl;
}
public:
int _a;
};
class B : virtual public A //公有继承A
{
public:
B()
{}
void Funb()
{
cout << "Funb()" << endl;
}
public:
int _b;
};
//注意virtual关键字添加的位置,添加在继承公共父类处
class C : virtual public A //单继承
{
public:
C()
{}
void Func()
{
cout << "Func()" << endl;
}
public:
int _c;
};
class D : public B,public C //多继承
{
public:
D()
{}
void Fund()
{
cout << "Fund()" << endl;
}
public:
int _d;
};
void test2()
{
A aa;
B bb;
C cc;
D dd;
//函数(代码段)和静态成员(静态区)不属于某一个对象,是所有对象共有的,所以不算做对象的大小。
cout << "类的大小" << endl;
cout << "aa = " << sizeof(aa) << endl; //4
cout << "bb = " << sizeof(bb) << endl; //4+4+4(虚基表)
cout << "cc = " << sizeof(cc) << endl; //4+4+4(虚基表)
cout << "dd = " << sizeof(dd) << endl; //12+12+4-4(A类大小),
dd.B::_a = 0; //通过类域,指定访问B类中的_a成员,
dd.C::_a = 1; //通过类域,指定访问C类中的_a成员,
dd._a = 2; //但实际都是直接对同一个父类A操作,结果值相同
dd._b = 3;
dd._c = 4;
dd._d = 5;
//注:B,C不直接继承A类,而是通过虚基表表找到A类,所以B,C共用的是一个A类(原A类),而不是各种独有。所以减去多统计的A类大小
dd._a = 10;//正确,D类继承父类B,C,两个父类根据虚基表找到他们的公共父类A,他们都是直接对同一个父类A操作。所以D类实际只有在一个_a成员,从而解决二义性和数据冗余问题
//dd.A::_a = 1; //错误,A不是D的直接父类,所以A不算是D的父类,不可直接访问A类成员
dd.B::_a = 1; //通过类域,指定访问B类中的_a成员,
dd.C::_a = 2; //通过类域,指定访问C类中的_a成员,但实际都是直接对同一个父类A操作,结果值相同
dd._b = 3;
dd._c = 4;
dd._d = 5;
//对公共继承的父类数据操作,即直接对原父类中的数据操作,数据相同
cout << dd.B::_a << endl;
cout << dd.C::_a << endl;
cout << aa._a << endl;//对aa对象操作,属于不同对象,所以数值不同
}
继承图解:
//5、无法继承的类
#include<iostream>
using namespace std;
class A
{
private: //将A的构造函数定义成私有的,使类不可被继承
A()
{
cout << "构造A" << endl;
}
public:
static A* Get()
{
A();
}
protected:
int _a;
};
class B : public A
{
public:
B() //不可调用A的构造函数,无法实例化出对象
{
cout << "构造B" << endl;
}
int add()
{
_a = 10;
_b = 20;
return _a+_b;
}
protected:
int _b;
};
void test4()
{
B bb;
A* a = A::Get(); //通过接口实例化对象
}
三、多态
一、先介绍一下虚函数:
1、实现:在类的成员函数前加关键字virtual,则这个成员函数变为虚函数
2、虚函数的重写:在子类中定义一个与父类完全相同的虚函数(函数名相同,参数相同,返回类型相同,协变除外),则子类会覆盖掉父类中的这个虚函数,称为重写
注:1、父类必须加关键字virtual,子类可不用加virtual关键字,因为子类会继承父类的特性
2、协变—-返回类型是对应类的的指针或引用(同为指针或引用)
3、静态成员函数不能定义的虚函数
4、只有类的成员函数才能定义成虚函数
5、类外定义不可加virtual关键字,类内声明时可加virtual关键字
6、构造函数不可为虚函数,析构函数最好定义成虚函数
原因:构造函数定义成虚函数,调用构造函数时会调用虚表,在虚表中找构造函数,但由于对象还未实例化,所以还未创建续表,导致找不到构造函数出错。
7、析构函数定义虚函数:父类的指针有可能指向的是子类的对象,若子类的析构函数不是多态,则只调用父类的析构,而不会调用子类的析构,导致子类的空间未释放,造成内存泄漏。
列:A * aa = new B; //父类指针指向子类的对象。
delete aa; //析构只调用父类的析构,导致子类内存泄露。
若将析构函数定义成虚函数,由于父类和子类的析构函数名实际是相同的,所以会构成重写。析构时根据对象调用对应的析构函数,从而避免内存泄漏问题。
8、内联函数不可定义成虚函数?
内联函数是在编译时进行展开的,虚函数的调用是在运行时动态绑定的,所以不可定义成虚函数。
9、静态成员函数不可定义成虚函数?
静态成员函数属于类的,不属于某个对象,是所有对象的共有,所以没必要在运行时联编,没必要定义成虚函数。
10、友元函数不可定义成虚函数?
友元函数不可被继承,不可继承的函数不可定义成虚函数。
其它:
1、含有虚函数的类:会创建一个虚表,用于存储虚函数的地址,虚表实际主要就是一个函数指针数组。
2、子类会继承父类的虚函数,会重写所有父类中相同的虚函数,但只将自己的虚函数写在最先继承的父类的虚表中。
3、类中不存虚表,而是存指向这个虚表的的指针。虚表存在代码段中,同类的所有对象共用一个虚表。
二、纯虚函数:
1、方法:在声明时将成员函数的虚函数后写 = 0,则该成员函数即为纯虚函数。
注:写 = 0;的意思是将函数的地址初始化为0,告诉编译器不为该函数编制,从而阻止该类的实例化。
2、含有纯虚函数的类称为抽象类。抽象类不可实例化出对象,纯虚函数在派生类中重新定以后,派生类才能实例化出对象,父类还是不可实例化出对象。
目的:让子类继承并实现他的接口方法
作用:接口与实现分离,不仅把数据成员隐藏,还把实现完全隐藏,只留一些接口给外部调用。从而即使将来实现改变,接口可以保持不变,向后兼容。
这种彻底的封装思想,体现了面向对象的动态绑定技术,主要引用于COM,CORBA等体系结构。
函数都是public的纯虚函数的类称为接口类。
三、多态:
1、多态就是多种形态,C++的多态分为静态多态和动态多态
静态多态:如重载,在编译时进行决议(联编),将函数地址放到函数调用处。
动态多态:通过继承重写基类的虚函数实现的多态,在运行时进行决议(联编)。
多态的虚函数存放在虚表中,虚表实际就是一个函数指针数组,由于数组下标只有在运行时才能确定。所以重写是动态多态。
2、作用:同一接口实现不同的功能,调用结果与对象有关。根据对象调用各自对应的方法,子对象调用子类的,父类对象调用父类的。
不是多态时,调用与类型有关。根据类调用对应的方法。
3、当使用基类的指针或引用调用重写的虚函数时,指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数。
必要条件:
1、调用的必须是子类重写了父类的虚函数
2、必须是父类的指针或引用。(根据所指向的类或引用的对象调用,子类对象调用子类的,父类对象调用父类的。)
继承体系中同名的函数:
一、重载:
1、在同一个作用域
2、函数名相同,参数不同
3、返回值可不同
二、重写(覆盖)
1、在不同的作用域(分别在子类和父类中)
2、函数名相同,参数相同,返回值类型相同(协变除外)
3、访问修饰符可不同
4、基类必须有virtual关键字修饰
三、重定义(隐藏)
1、不在同一作用域(分别在子类和父类中)
2、函数名相同
3、在子类和父类中,不构成重写即为重定义
代码实例:
1、菱形虚继承+虚函数
//菱形虚继承+虚函数
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout <<"A构造"<< endl;
}
~A()
{
cout <<"B析构"<< endl;
}
virtual void Fun1()
{
cout << "A.Fun()" << endl;
}
void Funa()
{
cout << "a.Funa()" << endl;
}
public:
int _a;
};
class B : virtual public A
{
public:
B()
{
cout <<"B构造"<< endl;
}
virtual void Fun2()
{
cout << "B.Fun()" << endl;
}
void Funb()
{
cout << " b.Funb()" << endl;
}
~B()
{
cout <<"B析构"<< endl;
}
public:
int _b;
};
class C : virtual public A
{
public:
C()
{
cout <<"C构造"<< endl;
}
virtual void Fun3()
{
cout << "c.Fun()" << endl;
}
void Funb()
{
cout << " c.Funb()" << endl;
}
~C()
{
cout <<"C析构"<< endl;
}
public:
int _c;
};
class D : public B,public C
{
public:
D()
{
cout <<"D构造"<< endl;
}
virtual void Fun()
{
cout << "D.Fun4()" << endl;
}
void Funb()
{
cout << " d.Funb()" << endl;
}
~D()
{
cout <<"D析构"<< endl;
}
public:
int _d;
};
void test5()
{
A aa;
B bb;
C cc;
D dd;
dd._a = 1;
dd._b = 2;
dd._c = 3;
dd._d = 4;
cout << sizeof(aa) << endl; //8
cout << sizeof(bb) << endl; //20
cout << sizeof(cc) << endl; //20
cout << sizeof(dd) << endl; //36 = 20+20-8+4 ,虚继承B/C占用同一个父类A,-8
}
图解:
2、菱形虚继承+多态
//菱形虚继承+多态
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout <<"A构造"<< endl;
}
~A()
{
cout <<"B析构"<< endl;
}
virtual void Fun()
{
cout << "A.Fun()" << endl;
}
virtual void Fun1()
{
cout << "A.Fun1()" << endl;
}
void Funa()
{
cout << "a.Funa()" << endl;
}
public:
int _a;
};
class B : virtual public A
{
public:
B()
{
cout <<"B构造"<< endl;
}
virtual void Fun()
{
cout << "B.Fun()" << endl;
}
virtual void Fun2()
{
cout << "B.Fun()" << endl;
}
void Funb()
{
cout << " B.Funb()" << endl;
}
~B()
{
cout <<"B析构"<< endl;
}
public:
int _b;
};
class C : virtual public A
{
public:
C()
{
cout <<"C构造"<< endl;
}
virtual void Fun()
{
cout << "C.Fun()" << endl;
}
virtual void Fun3()
{
cout << "C.Fun()" << endl;
}
void Funb()
{
cout << " c.Funb()" << endl;
}
~C()
{
cout <<"C析构"<< endl;
}
public:
int _c;
};
class D : public B,public C
{
public:
D()
{
cout <<"D构造"<< endl;
}
virtual void Fun()
{
cout << "D.Fun()" << endl;
}
virtual void Fun4()
{
cout << "D.Fun4()" << endl;
}
void Funb()
{
cout << " d.Funb()" << endl;
}
~D()
{
cout <<"D析构"<< endl;
}
public:
int _d;
};
void test5()
{
A aa;
B bb;
C cc;
D dd;
dd._a = 1;
dd._b = 2;
dd._c = 3;
dd._d = 4;
cout << sizeof(aa) << endl; //8
cout << sizeof(bb) << endl; //24
cout << sizeof(cc) << endl; //24
cout << sizeof(dd) << endl; //40 = 24+24-8-4+4 //虚继承共用一个父类A,所以-8,父类B,C用同一个占位地址-4。
}
图解:
四、三大特性总结
封装:
1、封装:二中方法(1、类将数据和函数捆绑,2、访问限制符限制)
2、作用:信息隐藏,只提供必要接口,防止外部干扰
继承:
1、方式:public(is-a) protected,private(has-a)
2、作用:通过子类继承父类实现代码复用。
分类:
单继承—只有一个直接父类
多继承—有多个直接父类
菱形继承—单继承和多继承的复合
缺陷:数据二义性,数据冗余
解决:虚继承
虚基表
1、存放到虚表的的偏移量
2、存放到父类的偏移量
多态:
分类:静态多态,动态多态
作用:一个接口实现多种功能,根据对象调用对应的实现函数
必要条件:
1、父,子类中必须有完全相同的虚函数。
2、必须是父类的指针或引用。
虚函数:
使用关键字virtual修饰的函数
虚表
存放虚函数的地址(函数指针数组)
父类有虚表时不创建虚表,有多大个父类时,每个父类都会形成多态,但只将自己的虚函数放在最先继承的父类中
最好为虚函数:析构函数
不可为虚函数:构造,静态,友元,内联。
纯虚函数:在虚函数后写 = 0;函数地址初始为0,告诉编译器不为该函数编址从而使类不可实例化出对象。
含有纯虚函数的类称为抽象类,若继承方式为public的类称为接口类。
如何定义一个不可被继承的类?
解:将类的构造函数定义成私有的。
1、类的私有成员只能在类内可访问,子类不可见。
2、子类是合成的,调用子类的构造函数时会先调用父类的构造函数。