类中成员变量叫做属性,类中成员函数叫做方法。
在C++中,通过定义一个类来定义数据结构。一个类定义了一个类型,以及与其关联的一组操作。
对象的概念类似于C语言中的结构体变量,而类类似于结构体。
定义类
定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。
对象
定义C++对象
类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。
一般格式
类名 对象名;
//对象都有他们各自的数据成员
面向对象三大特点
封装、继承、多态
封装和访问控制
struct
当单一变量无法完成描述需求的时候,结构体类型解决了这一问题。可以将多个类型打包成一体,形成新的类型。C语言中封装的概念
对C语言中结构体的操作,都是通过外部函数来实现的。
封装的访问属性
struct 中所有行为和属性都是public的(默认)。C++中的 class 可以指定行为和属性的访问方式。
封装,可以达到,对内开放数据,对外屏蔽数据,对外提供接口。达到了信息隐蔽的功能。
比如我们用 struct 封装的类,即知其接口,又可以直接访问其内部数据,这样却没有达到信息隐蔽的功效。而 class 则提供了这样的功能,屏蔽内部数据,对外开放接口。
封装的2层含义:把属性和方法进行封装,对属性和方法进行访问控制
struct和class关键字的区别
在用 struct 定义类时,所有成员默认属性为 public
在用 class 定义类时,所有成员的默认属性为 private
对象数组
数组有几个元素就调用几次构造函数,调用的构造函数可以是不同的构造函数【构造函数函数重载】。
定义
类名 数组名[n];
类名 数组名[n]={类名(传参),类名(传参),……};
//采用无名对象的方法
调用
数组名[i].成员变量;
数组名[i].成员函数;
访问数据成员
类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问。
类成员的访问权限
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是类外面的代码访问该类中成员权限。
在类的内部,即类的成员函数中,无论成员被声明为 public、protected 还是private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),通过对象只能访问 public 的成员,不能访问 private、protected 属性的成员。
private 后面的成员都是私有的,直到有 public 出现才会变成共有的;public 之后再无其他限定符,所以 public 后面的成员都是共有的。
private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。
声明为 private 的成员和声明为 public 的成员的次序任意,既可以先出现 private 部分,也可以先出现 public 部分。如果既不写 private 也不写 public,就默认为 private。
在一个类体中,private 和 public 可以分别出现多次。每个部分的有效范围到出现另一个访问限定符或类体结束时(最后一个右花括号)为止。
可能会说,将成员变量全部设置为 public 省事,确实,这样做 99.9%的情况下都不是一种错误,我也不认为这样做有什么不妥;但是,将成员变量设置为 private 是一种软件设计规范,尤其是在大中型项目中,还是尽量遵守这一原则。
成员和类的默认访问修饰符是 private。
公有成员 public
公有成员在程序中类的外部是可访问的。可以不使用任何成员函数来设置和获取公有变量的值。
私有成员 private
私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。
默认情况下,类的所有成员都是私有的。
实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。
受保护成员 protected
受保护成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。
继承中的特点
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
- 1.public 继承:基类 public 成员,protected 成员的访问属性在派生类中分别变成:public, protected,
- 2.protected 继承:基类 public 成员,protected 成员的访问属性在派生类中分别变成:protected, protected
- 3.private 继承:public以及protected成员会变为private成员 private成员不可继承
- private 成员在继承中,无论继承的权限是什么都无法继承给派生类
但无论哪种继承方式,上面两点都没有改变:
- 1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
- 2.protected 成员可以被派生类访问。
成员变量
命名
成员变量大都以m_开头,这是约定成俗的写法,不是语法规定的内容。以m_开头既可以一眼看出这是成员变量,又可以和成员函数中的参数名字区分开。
成员函数EnBuffer的函数体如下:
// 启、禁用缓冲区
void CFile::EnBuffer(bool bEnBuffer)
{
m_bEnBuffer=bEnBuffer;
}
const 常成员变量
值不能修改
初始化
声明时直接初始化
相当于给所有的对象都赋同一个值
构造函数使用初始化列表
只能使用初始化列表对其初始化,而不能在函数体内初始化
全局函数与成员函数
1、把全局函数转化成成员函数,通过this指针隐藏左操作数Test add(Test &t1, Test &t2)===》Test add(Test &t2)
2、把成员函数转换成全局函数,多了一个参数void printAB()===》void printAB(Test *pthis)
3、函数返回元素和返回引用
#include<iostream>
using namespace std;
class Test {
public:
Test(int a, int b) {
this->a = a;
this->b = b;
}
void printT() {
cout << "a=" << this->a << ",b=" << this->b << endl;
}
int getA() {
return this->a;
}
int getB() {
return this->b;
}
/*BBB 成员函数 相加
Test TestAdd(Test& another) {
Test temp(this->a + another.a, this->b + another.b);
return temp;
}
BBB*/
/*CCC 成员函数 +=
void TestAdd2(Test& another) {
this->a += another.a;
this->b += another.b;
}
CCC*/
//连 +=
Test& TestAdd3(Test& another) {
this->a += another.a;
this->b += another.b;
return *this; //如果想返回一个对象本身,在成员函数中,用 *this 返回
}
private:
int a;
int b;
};
/*AAA 全局函数
Test TestAdd(Test& t1, Test& t2) {
Test temp(t1.getA() + t2.getA(), t1.getB() + t2.getB());
return temp;
}
AAA*/
int main() {
Test t1(10, 20);
Test t2(100, 200);
/*AAA
Test t3 = TestAdd(t1, t2);
t3.printT();
AAA*/
/*BBB
Test t3 = t1.TestAdd(t2);
t3.printT();
BBB*/
/*CCC
t1.TestAdd2(t2);
t1.printT();
CCC*/
t1.TestAdd3(t2).TestAdd3(t2);//t1+=t2+=t2
//如果想对一个对象连续调用成员函数,每次都会改变对象本身,成员函数需要返回引用
t1.printT();
return 0;
}
成员函数
实现
一、所有内容放入类中
二、函数声明放入类中,函数整个放在类外
常成员函数
函数体内不能修改变量的值,只能读取数据
只能访问常函数
语法
返回值类型 函数名(参数列表)const{函数体;}
构造函数
class 类名{
类名(形参){
构造体
}
}
例如:
class A{
A(形参){
......
}
}
声明一个构造函数时,默认构造函数消失,默认拷贝构造函数存在;
声明一个拷贝构造函数时,默认构造函数和默认拷贝构造函数均消失。
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
在CFile类的声明中,有一些特殊的成员函数CFile(),它就是构造函数(constructor)。
CFile();
// 类的构造函数
CFile(bool bEnBuffer);
// 类的构造函数
构造函数的名字和类名相同,没有返回值,不能被显式的调用,而是在创建对象时自动执行。
功能
1.创建对象时,对对象分配内存
2.为数据成员进行初始化
调用——构造函数是对象初始化的时候调用
自动调用
一般情况下C++编译器会自动调用构造函数
手动调用
在一些情况下则需要手工调用构造函数
规则
1.在对象创建时自动调用,完成初始化相关工作
2.无返回值,与类同名,默认参数,可以重载,可默认参数
3.一经实现,默认不复存在
特点:
1)构造函数必须是 public 属性。
2)构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,不管是声明还是定义,函数名前面都不能出现返回值类型,即使是void 也不允许。
3)构造函数可以有参数,允许重载。一个类可以有多个重载的构造函数,创建对象时根据传递的参数来判断调用哪一个构造函数。
4)构造函数在实际开发中会大量使用,它往往用来做一些初始化工作,对成员变量进行初始化等,注意,不能用memset对整个类进行初始化。
CFile::CFile()
// 类的构造函数
{
m_fp=0;
m_bEnBuffer=true;
}
CFile::CFile(bool bEnBuffer)
// 类的构造函数
{
m_fp=0;
m_bEnBuffer=bEnBuffer;
}
带参数的构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
构造函数初始化列表
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数。
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
初始化列表中的初始化顺序,与声明顺序有关,与前后赋值顺序无关。
构造对象成员的顺序跟初始化列表的顺序无关,而是和成员对象的定义顺序有关。
第一种:在构造函数内部初始化
类对象=形参;
第二种:定义构造函数函数时初始化,初始化列表
:类对象(形参)
//调用拷贝构造函数
谁先初始化,谁后析构【即先初始化的后析构】
分类
class Test
{
public:
//无参数构造函数
Test()
{
;
}
//带参数的构造函数
Test(int a,int b)
{
;
}
//赋值构造函数
Test(const Test &obj)
{
;
}
private:
int a;
int b;
}
无参构造函数
有参数构造函数
拷贝构造函数——由已存在的对象,创建新的对象[用对象初始化对象]
浅拷贝和深拷贝——主要区别是针对指针来说的
浅拷贝——默认构造函数
拷贝数据——需要一个拷贝构造函数和默认构造函数,在释放时会出错,因为会释放两次,第一次的时候已经释放了,第二次就没有需要释放的内容了
指针:拷贝它的数据,相当于两个指针指向同一个空间,所指向的内容相同
其它:相当于进行了赋值操作
深拷贝——自定义构造函数
指针:申请一个大小相同的空间,然后再拷贝数据,相当于有两个相同大小的空间,并且空间中的数据相同
需要显示的提供一个拷贝构造函数,来完成深拷贝动作。
手写深拷贝:1.申请一个大小相同的空间 2.拷贝数据
由已存在的对象,创建新的对象。也就是说新对象,不由构造器来构造,而是由拷贝构造器来完成。拷贝构造器的格式是固定的。
class 类名
{
类名(const 类名 & another)
{
拷贝构造体
}
}
例如:
class A
{
A(const A & another)
{}
}
使用拷贝构造函数的场合
- 用对象1初始化对象2
Test t2=t1;
Testt2(t1);
(不可写成:Test t2;t2=t1;
,是“=”操作符,不是拷贝构造函数的调用) - 函数的参数是一个对象,并且是值传递方式
- 函数的返回值是一个对象,并且是值传递方式
Test t2(t1);
等价于Test t2=t1;
不可以进行如下操作·
Test t2;
t2 = t1;
//这不是拷贝构造,拷贝构造函数是在变量初始化的时候调用,这是“=”操作符
void operator=(const Test &another_obj){
cout<<"="<<endl;
m_a = another_obj.m_a;
}
默认构造函数
默认无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空。
当没有任何显示的构造函数(显示的无参构造函数、显示的有参构造函数、显示的拷贝构造函数)的时候,默认无参构造函数就会出现。
默认拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制。
当没有显示的拷贝构造函数的时候,默认拷贝构造函数就会出现。
规则
1.系统提供默认的拷贝构造器。一经实现,不复存在。
2.系统提供的时等位拷贝,也就是所谓的浅浅的拷贝【浅拷贝】。
3.要实现深拷贝,必须要自定义。
析构函数
class 类名
{
~类名(){
析构体
}
}
例如:
class A
{
~A()
{}
}
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
在 CFile 类的声明中,还有一个特殊的成员函数~CFile()
,它就是析构函数(destructor)。
~CFile();
// 类的析构函数
析构函数的名字在类的名字前加~
,没有返回值,但可以被显式的调用,在对象销毁时自动执行,用于进行清理工作,例如释放分配的内存、关闭打开的文件等,这个用途非常重要,可以防止程序员犯错。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
规则
1.对象销毁时,自动调用。完成销毁的善后工作。
2.无返回值,与类同名。无参。不可以重载。
作用
并不是删除对象,而在对象销毁前完成的一些清理工作。
特点:
1)构造函数必须是 public 属性的。
2)构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许。
3)析构函数不允许重载的。一个类只能有一个析构函数。
CFile::~CFile()
// 类的析构函数
{
Close();
// 调用Close释放资源
}
默认析构函数
当没有显示的析构函数的时候,默认析构函数会出现。
虚析构和纯虚析构
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象 。
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。【解决方法:将父类中的析构函数改为虚析构或纯虚析构。】
虚析构和纯虚析构共性
- 可以解决父类指针释放子类对象
- 都需要有具体的函数表现
虚析构和纯虚析构区别
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
语法
虚析构
virtual ~类名(){}
纯虚析构
virtual ~类名() = 0;
//类内
类名::~类名(){}
//类外
作用
如果父类的析构函数不加virtual关键字
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete 掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。
如果父类的析构函数加virtual关键字
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete 掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。
总结
- 虚析构或纯虚析构是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
哥哥笔记
文档:第三天.note
链接:有道云笔记
类与类之间的关系
has-A,uses-A和is-A
has-A 包含关系
用以描述一个类由多个“部件类”构成。实现has-A关系用类成员表示,即一个类中的数据成员是另一种已经定义的类。
类本身和成员对象的构造函数的执行顺序:先执行成员对象的构造函数,在执行该类的构造函数。
当有多个成员对象时,构造函数的执行顺序与定义对象的顺序一致,与初始化列表中的顺序无关。
初始化类的成员对象:只能在构造函数的初始化列表进行,不能在构造函数内进行,因为在函数内属于赋值操作而非定义时的初始化,不能直接调用成员对象的构造函数,此时不是实例化类,若对()重载,可以实现在构造函数中初始化成员对象。
uses-A 一个类部分地使用另一个类
通过类之间成员函数的相互联系,定义友员或对象参数传递实现。
is-A 继承
关系具有传递性,不具有对称性。
继承
定义
类的继承,是新的类从已有类那里得到已有的特性。或从已有类产生新类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。【父类的构造函数不能被子类继承】
创建一个子类的同时会创建一个父类,并且是先创建其父类再创建子类。
继承的好处:可以减少重复的代码。
派生与继承,是同一种意义两种称谓。 isA 的关系。
派生类的组成
派生类中的成员,包含两大部分,一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。
几点说明:
1,全盘接收,除了构造器与析构器。基类有可能会造成派生类的成员冗余,所以说基类是需设计的。
2,派生类有了自己的个性,使派生类有了意义。
方式
语法
class 派生类名:继承方式 基类名{
派生类成员声明;
};
一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为单继承。
protected 访问控制
protected对于外界访问属性来说,等同于私有,但可以派生类中可见。
派生类成员的标识和访问
继承方式 | 共有成员 public | 保护成员 protected | 私有成员 private | ||||||
访问属性 | 内部访问 | 对象访问 | 访问属性 | 内部访问 | 对象访问 | 访问属性 | 内部访问 | 对象访问 | |
public 公有继承 | public | YES | YES | protected | YES | NO | private | NO | NO |
protected 保护继承 | protected | YES | NO | protected | YES | NO | private | NO | NO |
private 私有继承 | private | YES | NO | private | YES | NO | private | NO | NO |
通过多次私有继承后,对于基类的成员都会成为不可访问。
private 成员在子类中依然存在,但是却无法访问。不论何种方式继承基类,派生类都不能直接使用基类的私有成员。
如何恰当的使用 public、protected、private 为成员声明访问级别?
1.需要被外界访问的成员直接设置为public
2.只能在当前类中访问的成员设置为private
3.只能在当前类和子类中访问的成员设置为protected,protected成员的访问权限介于public和private之间。
类型兼容性原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
类型兼容规则中所指的替代:
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。
子类就是特殊的父类
赋值兼容规则
派生类的对象可以赋值给基类对象
A a1;
// 基类A对象 a1
B b1;
// 类A的公有派生类B的对象 b1
a1 = b1;
//用派生类B对象 b1 对基类对象 a1 赋值
在赋值时舍弃派生类自己的成员,只进行数据成员的赋值。赋值只是对数据成员赋值,对成员函数不存在赋值的问题,内存中数据成员和成员函数是分开的。
赋值后不能通过基类对象 a1 去访问派生类对象 b1 的成员,因为 b1 的成员与 a1 的成员是不同的,派生类中的一些成员变量是基类没有的。
#include<iostream>
using namespace std;
class A {
public:A(int aa):a(aa){}void Aoutput() {cout << a << endl;}int a;
};
class B :public A {
public:B(int aa, int bb) :A(aa) {b = bb;}void Boutput() {Aoutput();cout << b << endl;}int b;
};
int main() {A a1(2);a1.Aoutput();B b1(3, 4);b1.Boutput();a1 = b1;a1.Aoutput();cout << "----------------" << endl;cout << a1.a << endl;//cout << a1.b << endl; 因为 a1 中不存在成员变量 bcout << b1.a << endl;cout << b1.b << endl;return 0;
}
只能用子类对象对其基类对象赋值,不能用基类对象对其子类对象赋值,因为两种对象的大小是不同的,基类对象不包含派生类的成员变量无法对派生类的成员变量赋值。同理,同一基类的不同派生类之间也不能赋值。
派生类的对象可以初始化基类的引用
A a1;
// 基类A对象 a1
B b1;
//派生类B对象 b1
A &r = a1;
此时 r 和 a1 共享同一段内容单元
A &r = b1;
此时 r 不是 b1 的别名,也不与 b1 共享同一段内存单元,只是 b1 中基类部分的别名。
r 是A类的引用,其有效范围只有A类那么大,r 与 b1 中基类部分共享同一段存储单元,r 与 b1 具有相同的 起始地址。
如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。
派生类对象的地址可以赋给指向基类的指针,即指向基类对象的指针可以指向派生类对象
通过指向基类对象的指针,只能访问到派生类中的基类成员变量,不能访问派生类增加的成员变量,即使派生类和基类中存在同名变量,访问的也是基类的成员变量而非派生类的成员变量。
继承中的对象模型
子类是由父类成员叠加子类新成员得到的。
父类中所有非静态成员属性都会被子类继承,父类中私有成员属性被编译器隐藏,因而访问不到,但是是被继承了。
利用开发人员命令提示工具查看对象模型:跳转盘符——>跳转文件路径【cd 具体路径(复制粘贴路径)】——>查看命名【cl /d1 reportSingleClassLayout类名 文件名】
继承中的构造和析构
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化。
在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理。
派生类的构造函数的初始化列表中可以包含:基类的构造函数、派生类中成员对象的初始化、派生类中一般成员变量的初始化
继承中构造析构调用原则
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反
继承和组合并存,构造和析构原则
构造顺序:虚基类—>非虚基类—>成员对象—>自己
先构造父类,再构造成员对象,最后构造自己
先析构自己,再析构成员对象,最后析构父类
【一个类,如果不是派生类,那么就先构造成员变量,在构造自己,先析构自己,再析构成员变量;一个类,如果是派生类,那么就先构造基类,再构造成员变量,最后构造自己,先析构自己,再析构成员变量,最后析构基类】
#include<iostream>
using namespace std;
class Object {
public:
Object(const char* s) {
cout << "Object()" << " " << s << endl;
}
~Object() {
cout << "~Object()" << endl;
}
};
class Parent :public Object {
public:
Parent(const char* s) :Object(s) {
cout << "Parent()" << " " << s << endl;
}
~Parent() {
cout << "~Parent()" << endl;
}
};
class Child :public Parent {
public:
Child() :o2("o2"), o1("o1"), Parent("Parameter from Child!") {
cout << "Child()" << endl;
}
~Child() {
cout << "~Child()" << endl;
}
private:
Object o1;
Object o2;
};
void run() {
Child child;
}
int main(int argc, char* argv[]) {
run();
return 0;
}
继承中同名成员的处理方法
同名成员变量和成员函数通过作用域分辨符进行区分
同名成员变量
当子类成员变量与父类成员变量同名时,子类依然从父类继承同名成员,但直接访问到的是子类中的同名成员,在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符),同名成员存储在内存中的不同位置 。
同名成员函数
如果子类中出现和父类同名的成员函数,子类的同名函数会隐藏/覆盖掉父类中所有同名成员函数(包括重载函数)。
访问父类中隐藏的同名成员函数:
- 定义基类指针,让基类指针指向派生类对象,调用到的是基类中的函数
- 加作用域
派生类中的static关键字
基类定义的静态成员,将被所有派生类共享。根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质 (遵守派生类的访问控制),派生类中访问静态成员,用以下形式显式说明:类名 :: 成员
或通过对象访问对象名.成员
static函数也遵守3个访问原则
static易犯错误(不但要初始化,更重要的显示的告诉编译器分配内存)
同名静态成员
成员变量
- 通过对象访问
默认访问子类本身,添加作用域访问父类
子类数据:对象.变量
父类数据:对象.父类::变量
- 通过类名访问
子类数据:子类::变量
父类数据:子类::父类::变量
第一个::
代表通过类名方式访问,第二个::
代表访问父类作用域下
成员函数
子类出现和父类同名的静态成员函数,会隐藏父类中所有同名成员函数。如果想要访问父类中被隐藏的同名成员函数,需要加作用域。
- 通过对象访问
子类函数:对象.函数名()
父类函数:对象.父类::函数名()
- 通过类名访问
子类函数:子类::函数名()
父类函数:子类::父类::函数名()
多继承
父类出现同名成员,需要加作用域区分。
菱形继承/钻石继承
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性 。
多个父类拥有相同数据,需要加以作用域区分。但是会导致数据有多份,导致资源浪费。
虚继承 virtual
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性 ;如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象,要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类。
虚继承声明使用关键字 virtual 。
在继承方式之前加上关键字 virtual,变为虚继承。使其基类变为虚基类。
底层实现:vbptr(v—virtual,b—base,ptr—pointer):虚基类指针,指向 vbtable,即虚基类表。
多态
分类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
区别
静态多态的函数地址早绑定—编译阶段确定函数地址
动态多态的函数地址晚绑定—运行阶段确定函数地址
多态包括两种多态性:编译时的和运行时的。前者是通过函数和运算符实现的,而后者是通过类继承关系和虚函数实现的
意义
如果有几个相似而不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。
C++中所谓的多态是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统升级、维护、调试的工作量和复杂度。
封装
突破了C语言函数的概念
继承
代码复用,复用原来写好的代码
多态
多态可以使用未来,80年代写了一个框架,90人写的代码
多态是软件行业追寻的一个目标
优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
成立条件
- 有继承关系
- 子类重写父类中的虚函数【父类中的函数使用关键字 virtual,使之成为虚函数】(重写:函数返回值类型、函数名、参数列表完全相同)
- 有父类指针(父类引用)指向子类对象(使用多态)
多态是设计模式的基础,多态是框架的基础
实现前提——赋值兼容
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
赋值兼容是一种默认行为,不需要任何的显示的转化步骤。
赋值兼容规则中所指的替代包括以下的情况:
派生类的对象可以赋值给基类对象。
派生类的对象可以初始化基类的引用。
派生类对象的地址可以赋给指向基类的指针。
替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
面向对象新需求
编译器的做法不是我们期望的;
根据实际的对象类型来判断重写函数的调用;
如果父类指针指向的是父类对象则调用父类中定义的函数;
如果父类指针指向的是子类对象则调用子类中定义的重写函数。
C++中通过virtual关键字对多态进行支持
使用virtual声明的函数被重写后即可展现多态性
实现原理
vftable (虚函数表)和 vptr (即vfptr,即虚函数表指针)
当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类成员函数指针的数据结构,是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针) 。
类中存在一个 vfptr ,指向 vftable,表内记录一个虚函数地址【&作用域::函数名
】。当子类重写父类中的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址。当父类的指针或引用指向子类对象时,发生多态。
说明:
1. 通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
2.出于效率考虑,没有必要将所有成员函数都声明为虚函数。
3.C++编译器,执行run函数,不需要区分是子类对象还是父类对象,而是直接通过p的VPTR指针所指向的对象函数执行即可。
证明vptr指针的存在
sizeof A1 = 4
是因为存在一个 vfptr 指针,指向虚函数表
sizeof A2 = 1
A2 类似于空类,空类占一个字节
构造函数中能否调用虚函数,实现多态?
对象在创建的时,由编译器对VPTR指针进行初始化
只有当对象的构造完全结束后VPTR的指向才最终确定
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表
父类指针和子类指针的步长
静态联编和动态联编
1.联编是指一个程序模块、代码之间互相关联的过程。
2.静态联编是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。编译时就确定函数的调用与被调用的关系。
3.动态联编是指程序联编推迟到运行时进行,使用又称为晚期联编(迟绑定)。switch语句和if语句是动态联编的例子。运行时确定函数的调用与被调用的关系。
C++实现运行时运行时多态,必须使用基类指针调用虚函数。
重载、重写、重定义
纯虚函数和抽象类
基本概念
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本。纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。一个具有纯虚函数的基类称为抽象类。
语法
virtual 类型 函数名(参数表)=0;