面向对象编程(C++学习笔记)

面向对象编程(C++学习笔记)

概念

面向对象编程的核心思想(C++ primer)

1.数据抽象:将类的接口与实现分离(封装);2.继承:定义相似类型并对其相似关系进行建模;3.动态绑定:在一定程度上忽略相似类型的区别,而以同一的方式去使用他们的对象。

核心思想也可以说成,面向对象编程(OPP)的三大特征:封装、继承、多态。

封装

封装:就是隐藏对象的具体实现细节、自有属性,对外只提供接口与对象进行交互。

实现方式:

1.设定限定访问符(public、private、protected)(具体我就不写了,之前我写了一部分类的基础知识,不赘述了)

2.类成员的作用域:类成员作用域只在类内部,外部只能通过该类的实例化对象来访问public成员。

继承

继承是类设计层次的复用,通过继承联系在一起的类构成一种层次关系,在层次关系中的根部就是基类,从这个基类间接或直接继承的其它类,称为派生类

class MyClassa//基类
class MyClassb :public MyClassa//派生类

继承有三种方式,主要区别在于,继承后,派生类继承的成员访问权限发生了变化

继承方式基类public成员基类private成员基类protected成员特点
public直接继承(public)不能继承,不可访问直接继承(protected)权限没有变化
private继承(private)不能继承,不可访问继承(private)全部变为私有
protected继承(protected)不能继承,不可访问继承(protected)全部变为protected

补充:

  1. 一般常用就是使用public来继承,这样派生类还可以继续拓展。

  2. 对于基类受保护成员(protected),其不能被实例化的基类对象所调用和访问(这点与private相似),但对于派生类的成员而言是可以进行访问的,但是,访问方法只能通过派生类对象来访问基类的受保护成员,不能直接访问基类对象中受保护成员。

class MyClassb :public MyClassa
{
	void dfs(int a)
	{
		root=4;//public继承之后,此处为派生类的protected成员
	}
	int add(MyClassb bb)
	{
		return bb.root + 5;//能访问MyClassb(派生类)对象的protected成员
	}
	int add(MyClassa aa)
	{
		return aa.root + 5;//不能访问MyClassa(基类)对象的protected成员,会报错
	}
}
  1. 派生类自有的成员与继承的基类成员变量有名字冲突时,派生类自有的成员变量会覆盖基类的。

  2. 基类与派生类之间转换的问题:

例如基类为a,派生类为b

派生类转换为基类:只要通过public继承的派生类,才可以直接使用派生类向基类转换。

转换过程中,并未复制派生类对象(objb),而是将该对象中基类部分复制,其余派生类部分就被切割。

class a{};
class b:public a{};

b objb;
a obja;
a objiaa;
obja=objb;//直接转换
objb=objiaa;//编译报错

基类转换到派生类是无法实现,会报错,原因也很明显,派生类除了有基类的部分,还有自己的成员。基类不存在派生类的自定义成员,故而不好转换。

总结:派生类对象可以是基类对象,基类对象不是派生类对象,因为他不包含派生类成员,故而也不好转换。

  1. C++多重继承、多继承

    结合代码例子, 多重继承 就是一个基类派生一个类,派生类再派生一个类 等等。 代码结果:a----b-----c:构造函数调用顺序从根部基类依次。

    多继承:就是一个类其继承的基类有多个, 代码结果:a----d-----e,基类构造调用顺序为基类继承顺序。

    结果:
    多重继承
    a
    b
    c
    多继承
    a
    d
    e
    多重继承+多继承
    a
    b
    d
    f
    
    class a
    {
    public:
    	a()
    	{
    		cout << "a" << endl;
    	}
    };
    class b :public a
    {
    public:
    	b() :a()
    	{
    		cout << "b" << endl;
    	}
    };
    class c:public b
    {
    public:
    	c():b()
    	{
    		cout << "c" << endl;
    	}
    };
    class d
    {
    public:
    	d()
    	{
    		cout << "d" << endl;
    	}
    };
    class e :public a,public d
    {
    public:
    	e() :a(),d()
    	{
    		cout << "e" << endl;
    	}
    };
    class f :public b, public d
    {
    public:
    	f() :b(), d()
    	{
    		cout << "f" << endl;
    	}
    };
    
    class MyClassc :protected MyClassa
    {
    
    };
    int main()
    {
    	cout << "多重继承" << endl;
    	c cc;
    	cout << "多继承"<<endl;
    	e ee;
    	cout << "多重继承+多继承" << endl;
    	f ff;
    	return 0;
    
    }
    

6.对于继承基类没有默认构造函数时,派生类的构造函数的初始化列表里面需要添加对基类的构造函数
7.对于多重继承导致的二异性问题,
参考以及详细: http://t.csdn.cn/rWq2V

//多重继承时,当一个类继承的多个基类中存在同名的成员,就会产生同名二义性;
//当一个派生类的多个基类是从同一个基类(祖父基类)派生,此时派生类对象访问祖父基类的成员时,
//就会产生路径二义性。
解决方法:
1.在使用派生类的对象时,调用存在二义性的成员时,可以指定哪父类的成员;
格式: 派生类对象.父类::成员;
例子 child.faher1::money();

2.派生类重新定义存在二义性的成员,进行覆盖

3.针对路径二义性,采用虚拟继承的方式来解决菱形继承的二义性和数据冗余的问题
语法:
class 派生类名:virtual 继承方式 基类名
虚拟继承的特点:
1.虚基类并不是在声明基类时声明的,##而是在声明派生类时,指定继承方式时声明的##。
因为一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。
2.底层方面:每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)。当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
例子
3.基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化 (最派生类会先去调用虚基类的构造函数)
4.在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的对象。
// B、C类继承类A,采用虚拟继承的方式,此时A就是虚拟基类
class A;
//B、C继承A时,只会拷贝虚基类一次(特点4),其它都是只又一个虚基类指针、虚基类表(特点2)
class B :public virtual A{};
class C :public virtual A{};
//在D继承B,C时,也继承了其中的虚基类(A)的指针,并对虚基类进行初始化
class D :public B,public C{};

多态

多态是什么?emmm,看了书以及很多博客,最后就引用一下百度百科的:在编程语言中,多态指为不同数据类型的实体提供同一的接口。

额,这定义很专业,刚刚看还是云里雾里,先往下学,看看是否最后能理解这句话。

在C++中多态主要分为静态多态、动态多态

静态多态就是函数重载、泛型编程,其在程序编译期间确定程序行为,也称为静态绑定。

动态多态(就是虚函数),是在程序运行时,根据基类引用的对象(或指向对象的指针),来确定调用具体哪个类的虚函数,又称为多态绑定。

以下所讲多态,都是指动态多态。

多态的实现条件:

1.基类中必须有虚函数,并且派生类中一定对基类的虚函数进行重写

2.通过基类对象指针或引用来调用虚函数。

虚函数

C++中的虚函数主要就是帮助实现多态。可以说多态的最核心的就是虚函数的重写。

啥是虚函数?

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。

虚函数表特点:

  1. C++编译器为每个有虚函数的类都创建一个虚函数表(即使是拥有继承关系的类,虚函数表不继承)

  2. 虚函数表被一个类所有对象拥有。(即同一类的实例化对象共用一个虚函数表)

  3. 虚函数表就是存储一个类的虚函数地址的表,一个类对象的首地址指向虚函数表地址(4个字节);

  4. 虚函数表的指针类型为 void *(原因:在于一个类的所有虚函数都会存储到这个表中,所以对于虚函数表指针类型无确定,只有在调用时,编译器才会知道该虚函数表指针的类型。)虚函数指针在linux里面占用8个字节,在vs里面占用4个字节。

  5. 虚函数表是一个常量表,在编译时自动生成,所以他是存放在静态区(全局数据区),但虚函数指针是属于实例对象的,所以虚函数指针存放取决于对象定义的方式,如果是采用new的定义对象是存放在堆区,对象如果是采用直接声明,则是是存放在栈区的。

  6. 虚函数表的首地址就是虚函数表的第一个虚函数地址。其每行存储一个虚函数地址(4字节)其存储方式如下

mg src="C:\Users\29504\AppData\Roaming\Typora\typora-user-images\image-20220319201824237.png" alt="image-20220319201824237" style="zoom:75%;" />

代码验证目标

查看虚函数在虚函数表内存储方式;

检验在一般继承后,派生类虚函数表情况

顺便展示一下多态的使用,

class vrclassbase
{
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};
class vrclasschild:public vrclassbase
{
	virtual void cf()
	{
		cout << "cf1()" << endl;
	}
};
class vrclasschild2 :public vrclassbase
{
	virtual void cf()
	{
		cout << "cf2()" << endl;
	}
	virtual void ch()
	{
		cout << "ch2()" << endl;
	}
	void f()
	{
		cout << "child2f()" << endl;
	}
	
};


//虚函数验证;
	vrclassbase base;
	vrclasschild child;
	vrclasschild2 child2;
	cout << "base的首地址:"<<(int*)&base<<endl;//强制转换base地址为int*类型,
	//此时输出的base的首地址
	cout << "虚函数f地址(虚函数表):" << (int*)*(int*)(&base) << endl;//输出虚函数表的地址,也就是虚函数表存的第一个虚函数。
	cout << "虚函数g地址:" << (int*)*(int*)(&base) + 1 << endl;//g
	cout << "虚函数h地址:" << (int*)*(int*)(&base) + 2 << endl;//h
	//Fun fun=NULL;
	
	//fun=(Fun)*((int*)*(int*)(&base));
	void(*fun1)();
	fun1 = (void(*)())*((int*)*(int*)(&base));
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&base) + 1);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&base) + 2);
	fun1();

	//没有虚函数覆盖
	cout << "child地址:" << (int*)&child << endl;
	cout << "child虚函数f地址(虚函数表):" << (int*)*(int*)(&child) << endl;
	cout << "child虚函数g地址:" << (int*)*(int*)(&child)+1 << endl;
	cout << "child虚函数h地址:" << (int*)*(int*)(&child)+2 << endl;
	cout << "child虚函数cf地址:" << (int*)*(int*)(&child)+3 << endl;
	fun1 = (void(*)())*((int*)*(int*)(&child));
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child)+1);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child) + 2);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child) + 3);
	fun1();
	//有虚函数覆盖
	cout << "child2(已覆盖)地址:" << (int*)&child2 << endl;
	cout << "child2(已覆盖)虚函数地址(虚函数表):" << (int*)*(int*)(&child2) << endl;
	cout << "child2(已覆盖)虚函数地址:" << (int*)*(int*)(&child2)+1 << endl;
	cout << "child2(已覆盖)虚函数地址:" << (int*)*(int*)(&child2)+2 << endl;
	cout << "child2(已覆盖)虚函数地址:" << (int*)*(int*)(&child2)+3 << endl;
	cout << "child2(已覆盖)虚函数地址:" << (int*)*(int*)(&child2) + 4 << endl;
	cout << "地址对应的函数为:" << endl;
	fun1 = (void(*)())*((int*)*(int*)(&child2));
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child2)+1);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child2) + 2);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child2) + 3);
	fun1();
	fun1 = (void(*)())*((int*)*(int*)(&child2) + 4);
	fun1();

结果

base的首地址:00CFFBF8
虚函数f地址(虚函数表):00D89B34
虚函数g地址:00D89B38
虚函数h地址:00D89B3C
f()
g()
h()
child地址:00CFFBEC
child虚函数f地址(虚函数表):00D89B54
child虚函数g地址:00D89B58
child虚函数h地址:00D89B5C
child虚函数cf地址:00D89B60
f()
g()
h()
cf()
child2(已覆盖)地址:00CFFBE0
child2(已覆盖)虚函数地址(虚函数表):00D89B70
child2(已覆盖)虚函数地址:00D89B74
child2(已覆盖)虚函数地址:00D89B78
child2(已覆盖)虚函数地址:00D89B7C
child2(已覆盖)虚函数地址:00D89B80
地址对应的函数为:
child2f()
g()
h()
cf2()
ch2()

通过代码结果,可以看到一般继承下,

结果中,基类与派生类虚函数表地址不同,说明派生类有自己的虚函数表。

结果中,虚函数表的虚函数地址差值为4个字节,说明其在虚函数表里是按序排列的;

如果派生类没有对继承父类的虚函数重写(虚函数覆盖):根据结果中的地址分布可以看到,派生类的虚函数将接着基类虚函数存储后面位置存储;

child地址:00CFFBEC
child虚函数f地址(虚函数表):00D89B54
child虚函数g地址:00D89B58
child虚函数h地址:00D89B5C
child虚函数cf地址:00D89B60
f()
g()
h()
cf()

如果有虚函数覆盖(重写)情况,根据显示结果,child2重定义了基类的f(),虽然定义顺序在cf2()、ch2()之后,重定义后的虚函数覆盖了原本父类虚函数的位置,故最后地址中重定义的虚函数在原来f()位置(也就是第一位)。

则派生类覆盖的虚函数将会出现在对应父类的那个被覆盖虚函数的位置上。

child2(已覆盖)地址:00CFFBE0
child2(已覆盖)虚函数地址(虚函数表):00D89B70
child2(已覆盖)虚函数地址:00D89B74
child2(已覆盖)虚函数地址:00D89B78
child2(已覆盖)虚函数地址:00D89B7C
child2(已覆盖)虚函数地址:00D89B80
地址对应的函数为:
child2f()
g()
h()
cf2()
ch2()

对于派生类,编译器处理虚函数步骤:

1.拷贝基类的虚函数表,如果多继承,就拷贝所有基类的;

2.查看派生类是否存在重写基类的虚函数,有就替换;

3.查看派生类中是否有自身的虚函数,有就将自己的虚函数添加到自身的虚函数表中。

虚函数与析构函数、构造函数

构造函数一般不定义为虚函数,因为虚函数调用时,只需要部分接口,不需要确定对象具体类型,但在创建一个对象(使用构造函数)时,要确定对象的类型,虚函数无法做到。

从编译角度看。 虚函数的调用是通过实例化的对象(看我的代码例子)来找虚函数表,再进行调用的。如果构造函数为虚函数,虚函数表的指针就不存在,构造函数为虚函数,也无法调用,这陷入死循环了,无法使用!这也违反了虚函数先实例化再调用的准则

析构函数一般需要写成虚函数,主要原因是,如果基类析构函数不是虚函数,在其派生类对象销毁时,会调用所继承的基类析构函数并不会执行派生类的析构函数,这容易造成内存泄漏(因为派生类还有自己的部分)。基类析构函数加virtul,成为虚函数后,派生类对象销毁会调用自己的析构函数。

多态缺陷:

1.存在空间浪费;2.由于多态需要找虚函数表的地址,故降低了程序的运行效率。

抽象基类

抽象基类就是包含纯虚函数的类。其只定义接口,但不涉及实现,所以抽象基类是不能实例化,创建对象的。其主要就是作为其它派生类的基类。而继承了抽象基类的子类必须重写纯虚函数,否则该子类也是给抽象基类,无法实例化

纯虚函数:在虚函数声明后加“ =0 ”

virtual int func()=0;//纯虚函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值