首页 > 编程语言 >C++学习笔记2

C++学习笔记2

时间:2023-03-15 14:34:08浏览次数:35  
标签:函数 int 子类 笔记 学习 继承 C++ 父类 public

@

目录

15.const和static在类中的应用

const回顾:
只读变量,定义的时候就要初始化,之后不允许修改

int * const p = 0;  //p不能修改
int const *p = 0;   //*p不能修改
const int *p = 0;  //*p不能修改
const  int * const p = 0  //p,*p都不能修改

const在类中的应用(1):在类当中定义一个常量成员

如果在类当中定义一个常量成员呢?在定义类的时候成员属性不允许初始化,但是加const的变量又必须在定义的时候初始化,那怎样在类里面定义一个常量呢?

继续往下看↓

初始化列表
在构造函数中有一个初始化列表,在构造函数执行的时候,先按照初始化列表进行变量初始化,再执行构造函数内容

注意:

  • 在初始化列表里,初始化多个变量之间用逗号隔开,初始化的顺序是变量定义顺序
  • 类里面如果要定义一个其他类型的 对象,这个对象要执行一个带参的构造函数,需要在初始化列表中
class CPerson
{
	public:
		const int age;
	public:
		CPerson():age(20)  //初始化列表,用来给成员属性初始化
		{
			 //构造函数内容
		}
};

所以我们在类里面用const修饰的成员属性,必须在初始化列表中进行初始化

const在类中的应用(2):在类当中定义一个常函数

常函数:不能修改类中的普通成员属性
在函数名之后加const即定义了一个常函数

例如:

class CPerson
{
	public:
		int high;
	public:
		void show() const  //定义一个常函数
		{
			 //high =  100;  //这是不允许的
			 cout << high << endl;  //使用普通变量是允许的
		}
};

为什么常函数不能修改类中普通成员属性?

void show() const
实际上告诉它给this指针加const,即
void show(const CPerson* this)

所以this对应的那块空间里的变量就只能是只读

const在类中的应用(3):常量类

例如:

class CPerson
{
	public:
		int high;
	public:
		void show() const  //定义一个常函数
		{
			 //high =  100;  //这是不允许的
			 cout << high << endl;  //使用普通变量是允许的
		}

		void shoe()  //定义一个普通函数
		{
			 high =  100;  //这是允许的
			 cout << high << endl;  //使用普通变量也是允许的
		}
};

int main()
{
	const CPerson ps;   //定义一个常量类
	ps.show();   //不报错
	ps.shoe();   //报错
	return 0;
}

如果你定义了一个常量的类,那么这个类只能使用常函数

为什么呢?
看了上面的,就会知道,普通成员函数和常函数的区别是:

void show(const CPerson* this)
void shoe(CPerson* this)

如果定义的是一个常量对象

const CPerson ps;

在调用shoe函数的时候,传给this指针的就是一个const CPerson类型的指针变量,参数变量类型不符是一方面,重要的是此时ps的安全性降低了,安全性是允许提高的,但是不允许降低,所以普通的CPerson类型的对象是可以调用常函数的,但是常量CPerson类型的对象能调用普通函数。

static在类中的应用(1):静态成员属性

空类的大小是1,有成员属性的类的大小是成员属性所占的字节大小
如果成员属性用static修饰呢?我们能够发现,这时类的大小是1

class CPerson
{
	public:
		static int a;
};

int main()
{
	cout << sizeof(CPerson) << endl;
	return 0;
}

输出结果是1

我们知道sizeof(类型)是表示:如果用这个类型去定义变量,会分配多大空间。static修饰了a,所以在编译期,a就存在了,在定义的时候不需要再给它分配空间,所以这里结果是1

注意:

  • 类中定义了静态变量,就必须在类外对静态变量进行初始化
    初始化方法:int CPerson::a = 100;
  • 静态变量是编译器存在,是类中的一部分,类中只有一份,所有对象共享

既然静态变量a是在编译期就存在的,那就是说不用定义类,就能够使用变量a了,使用方法:CPerson::a
例如:

class CPerson
{
	public:
		static int a;	
};
int CPerson::a = 100;
int main()
{
	cout << CPerson::a << endl;
	return 0;
}

static在类中的应用(2):静态成员函数

注意:

  • 静态函数也可以直接通过类名和作用域调用
  • 静态函数只能用静态成员

静态函数如果要使用普通类成员,要如何实现呢?
可以给他写一个pthis指针,例如:

class CPerson
{
	public: 
		static int a;
		int b;
	public:
		CPerson():b(10)
		{
		
		}
	public:
		static void show(CPerson* pthis)
		{
			cout << pthis->b << endl;
		}
};

int main()
{
	CPerson ps;
	CPerson::show(&ps);
	return 0;
}

16.类之间的关系

1.横向关系

它们的强弱关系是:依赖 < 关联 < 聚合 < 组合

(1)依赖(Dependency)

  1. UML表示法:虚线 + 箭头
    在这里插入图片描述
  2. 关系:" ... uses a ..." 人需要空气
  3. 此关系最为简单,也最好理解,所谓依赖就是某个对象的功能依赖于另外的某个对象,而被依赖的对象只是作为一种工具在使用,而并不持有对它的引用。 比如:
class Human
{
    public void breath()
    {
        Air freshAir = new Air();
        freshAir.releasePower();
    }
    public static void main()
    {
        Human me = new Human();
        while(true)
        {
            me.breath();
        }
    }
};

class Air
{
    public void releasePower()
    {
        //do sth.
    }
};

在这里插入图片描述

  1. 释义:一个人自创生就需要不停的呼吸,而人的呼吸功能之所以能维持生命就在于吸进来的气体发挥了作用,所以说空气只不过是人类的一个工具,而人并不持有对它的引用。
  2. 释义2:被依赖类(空气)可以通过构造函数方法参数方法返回值方法内局部变量的形式存在于依赖类(人)中。

(2)关联(Association)

  1. UML表示法:实线 + 箭头
    在这里插入图片描述
  2. 关系:" ... has a ..."
  3. 所谓关联就是某个对象会长期的持有另一个对象的引用,而二者的关联往往也是相互的。关联的两个对象彼此间没有任何强制性的约束,只要二者同意,可以随时解除关系或是进行关联,它们在生命期问题上没有任何约定。被关联的对象还可以再
    被别的对象关联,所以关联是可以共享的。 比如:
class Human
{
    ArrayList friends = new ArrayList();
    public void makeFriend(Human human)
    {
        friends.add(human);
    }
    public static void main()
    {
        Human me = new Human();
        while(true)
        {
            me.makeFriend(mySchool.getStudent());
        }
    }
} ;

在这里插入图片描述
3. 释义:人从生至死都在不断的交朋友,然而没有理由认为朋友的生死与我的生死有必然的联系,故他们的生命期没有关联,我的朋友又可以是别人的朋友,所以朋友可以共享。
4.释义2:关联表示一个类A和另一个类B之间的联系,它使类A知道类B的属性和方法。通常类B会以私有成员变量的形式存在于类A中。可以通过构造函数赋值。关联形式有一对一(员工->工牌),一对多(部门->员工),多对多(商店->商品)。

(3)聚合(Aggregation)

  1. UML表示法:空心菱形 + 实线 + 箭头
    在这里插入图片描述
  2. 关系:" ... owns a ..."
  3. 聚合是强版本的关联。它暗含着一种所属关系以及生命期关系。被聚合的对象还可以再被别的对象关联,所以被聚合对象是可以共享的。虽然是共享的,聚合代表的是一种更亲密的关系。 比如:
class Human
{
    Home myHome;
    public void goHome()
    {
        //在回家的路上
        myHome.openDoor();
        //看电视
    }
    public static void main()
    {
        Human me = new Human();
        while(true)
        {
            //上学
            //吃饭
            me.goHome();
        }
    }
};

在这里插入图片描述
4. 释义:我的家和我之间具有着一种强烈的所属关系,我的家是可以分享的,而这里的分享又可以有两种。其一是聚合间的分享,这正如你和你妻子都对这个家有着同样的强烈关联;其二是聚合与关联的分享,如果你的朋友来家里吃个便饭,估计你不会给他配一把钥匙。
5.释义2:聚合是关联关系的一种,是强的关联关系。聚合关系是整体和个体的关系。一般关联关系的 两个类处于同一个层次上,聚合关系中的两个类处于不同的层次,一个是整体,一个是部分。比如员工聚合于一家公司,员工类也是以私有成员变量的形式存在于公司类中。

(4)组合/复合(Composition)

  1. UML表示法:实心菱形 + 实线 + 箭头
    在这里插入图片描述
  2. 关系:" ... is a part of ..."
  3. 组合是关系当中的最强版本,它直接要求包含对象对被包含对象的拥有以及包含对象与被包含对象生命期的关系。被包含的对象还可以再被别的对象关联,所以被包含对象是可以共享的,然而绝不存在两个包含对象对同一个被包含对象的共享。比如:
class Human
{
    Heart myHeart = new Heart();
    public static void main()
    {
        Human me = new Human();
        while(true)
        {
            myHeart.beat();
        }
    }
};

在这里插入图片描述

  1. 释义:组合关系就是整体与部分的关系,部分属于整体,整体不存在,部分一定不存在,然而部分不存在整体是可以存在的,说的更明确一些就是部分必须创生于整体创生之后,而销毁于整体销毁之前。部分在这个生命期内可以被其它对象关联甚至聚合,但有一点必须注意,一旦部分所属于的整体销毁了,那么与之关联的对象中的引用就会成为空引用,这一点可以利用程序来保障。心脏的生命期与人的生命期是一致的,如果换个部分就不那么一定,比如阑尾,很多人在创生后的某个时间对其厌倦便提前销毁了它,可它和人类的关系不可辩驳的属于组合。
    5.释义2:是关联关系的一种,是比聚合更强的关系,要求普通的聚合关系中代表整体的对象负责代表个体的对象的生命周期。当删除整体对象时也要级联删除个体对象。

图解实例:
在这里插入图片描述

2.纵向关系

继承

  1. UML表示法:实现 + 空心三角
    在这里插入图片描述
  2. 所谓继承就是一个类的属性和方法是另一个类属性和方法的子集,也就是说这个类它本身不进行任何的操作,就具有和另一个类的所有属性和方法,子类中还可以添加自己特有的属性和方法。
  3. 语法:class B : 继承方式 Aclass B为class A的子类
  4. 特点:
    (1)在继承的时不写继承权限时默认private权限继承
    (2)继承以后,以继承时的权限和本身的权限保密等级高的为主(后面详细讨论)
    (3)先构造父类,再构造子类,也就是说在子类构造之前就要先构造父类
    (4)析构,先调用子类析构函数,在子类析构的最后调用父类析构

看下面例子:

class CPerson
{
	public:
	void say()
	{
		cout << "hello world!" << endl;
	}
}

class CSuperman : public CPerson   //继承CPerson类
{
	
}

以上的代码中CSuperman就继承了CPerson,CSuperman是子类,CPerson是父类。

如果CSuperma和CPerson两个类都有say的功能,那么就没有必要每个类都写一遍,CSuperman继承CPerson之后,CSuperman也将具有CPerson的成员属性和成员方法,当然,CSuperman也可以具有自己特有的成员属性和成员方法。

关于继承的一些问题:

>1 父类和子类中具有同名成员属性的情况:

看下面例子1,预测以下输出结果:

class CFather
{
	public:
		int m_nMoney;
	public:
		CFather()
		{
			m_nMoney = 100;
		}
};

class CSon : public CFather
{
	public:
		int m_nMoney;
	public:
		CSon()
		{
			m_nMoney = 1;
		}
};

int main()
{
	CSon son;
	cout << son.m_nMoney << endl;
	return 0;
}

以上代码输出结果为1,显然它输出的是CSon中的m_nMoney的值。
可以在main函数中再写一句:

cout << sizeof(CSon) << endl;

输出结果为两个int的字节大小,就是说,CSon中实际上包含了两个int变量,一个来自于继承,一个是自己的,只不过他们的名字都是m_nMoney,那么既然两个变量都存在,我们如何使用来自继承的m_nMoney呢?

在main函数中加入:

cout << son.CFather::m_nMoney << endl;
cout << son.CSon::m_nMoney << endl;

输出结果为:100,1

所以说,对于父类和子类具有同名成员变量的情况,可以加类名作用域来区分。

>2 能否通过子类去修改父类的变量呢?:

答案是:不能

看下面例子:

class CFather
{
	public:
		int m_nMoney;
	public:
		CFather()
		{
			m_nMoney = 100;
		}
};
class CSon : public CFather
{
	
};
int main()
{
	CFather fa;
	CSon so;
	so.CFather::m_nMoney = 1;
	cout << fa.m_nMoney << endl;
	return 0;
}

以上代码输出结果还是100。

类之间的关系是继承关系,但是用这两个类所定义的对象之间是没有关系的,他们具有独立的内存空间。

>3 继承的方式:

我们注意到在开始的例子1当中,CSon继承CFather的时候是这样写的:

class CSon : public CFather

既然有public,那是不是就可以改成 protected / private,其实这里的public就是类的继承方式,下面详细说明:

我们知道访问修饰符有三种:
public : 公共的,谁都可以使用
protected :保护的,自己类和派生类中可以使用
private : 私有的, 只有自己类中才可以使用

那么, 如果是继承的话, 变量的访问修饰符是会发生 变化的, 我们来看一下, 将发生什么样的变化:

(1) 继承方式为public时

  • 父类中的public不变
  • 父类中的protected不变
  • 父类中的private变为不可访问
    在这里插入图片描述

(2) 继承方式为protected时

  • 父类中的public变为protected
  • 父类中的protected不变
  • 父类中的private变为不可访问

(3) 继承方式为private时

  • 父类中的public变为private
  • 父类中的protected变为private
  • 父类中的private变为不可访问

需要注意的是: 不可访问是指不可以直接使用, 如果类中提供接口, 我们就可以通过接口来对这个不可访问的变量进行相应的操作

>4 继承中的函数重载与函数重写:

  • 函数重载: (两个函数名相同, 参数列表不同)

在继承中的函数重载叫做隐藏,或者叫做纵向重载, 是说当一个类继承了另一个类之后, 并不能对父类中的存在函数进行重载, 当两个函数名相同的时候, 默认会调用子类中的函数, 如果要使用父类中的函数, 需要加类名作用域

在这里插入图片描述

  • 函数重写:(函数名相同, 参数列表也相同, 返回值可以不同)

是指对于父类中存在的函数, 在子类中重新定义, 两个函数名相同, 参数列表相同, 但是执行的功能不同, 这种情况下, 如果在主函数中调用子类的该函数, 不写作用域时默认调用的是子类中的函数, 要调用父类中的该函数, 就要使用类名作用域

在继承关系中, 可以使用父类的指针通过new得到一个子类的对象

Cfather* p = new Cson;
  • 这样做的优缺点:
    (1) 优点: 提高代码的复用性, 比如在某一个链表中的节点类型是某三个类(假如是Cson1, Cson2, Cson3)那么, 我们只需要在结构体中定义一个父类类型的指针, 就不需要定义三种类型了
    (2)缺点: 这样的指针, 他不能取到子类中特有的东西, 只能取到子类中继承了父类的东西

多继承

在前面的例子中,派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。

多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。

多重继承的语法:

class D: public A, private B, protected C{
    //类D新增加的成员
}

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

多继承下的构造函数(了解即可)

  • 多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
    //其他操作
}
  • 基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:
D(形参列表): B(实参列表), C(实参列表), A(实参列表){
    //其他操作
}

那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。

#include <iostream>
using namespace std;

//基类
class BaseA{
public:
    BaseA(int a, int b): m_a(a), m_b(b)
    {
    	cout<<"BaseA constructor"<<endl;
	}
    ~BaseA()
    {
    	cout<<"BaseA destructor"<<endl;
	}
protected:
    int m_a;
    int m_b;
};

//基类
class BaseB{
public:
    BaseB(int c, int d): m_c(c), m_d(d)
    {
   		cout<<"BaseB constructor"<<endl;
	}
    ~BaseB()
    {
    	cout<<"BaseB destructor"<<endl;
	}
protected:
    int m_c;
    int m_d;
};

//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e)
    {
    	cout<<"Derived constructor"<<endl;
	}
	~Derived()
	{
    	cout<<"Derived destructor"<<endl;
	}
public:
    void Derived::show()
    {
    	cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
	}
private:
    int m_e;
};

int main(){
    Derived obj(1, 2, 3, 4, 5);
    obj.show();
    return 0;
}

运行结果:
在这里插入图片描述

从运行结果中还可以发现,多继承形式下析构函数的执行顺序和构造函数的执行顺序相反。

命名冲突

  • 当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

17.多态

1.虚函数

什么是虚函数,虚函数是指在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数,即被virtual关键字修饰的成员函数;

(1)虚函数的作用:

前面在继承中有说, 可以使用父类指针new子类对象, 但是缺点是不能使用子类中的元素, 那么虚函数就可以弥补这个缺陷, 虚函数的作用就是, 可以通过父类的指针去用子类的函数,是的父类具有多种形态,即实现多态。

(2)什么是多态?

关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

(3)为什么虚函数可以实现多态(虚函数的原理)

  1. 说说虚函数的原理:

先来看一段简单的继承的代码:

#include <iostream>
#include <stdlib.h>
using namespace std;

class CFather
{
public:
	int a;
public:
	CFather(int x)
	{
		a = x;
	}
public:
	virtual void AA()
	{
		cout << "CFather:: AA" << endl;
	}

	virtual void BB()
	{
		cout << "CFather:: BB" << endl;
	}

};

class CSonA : public CFather
{
public:
	void AA()
	{
		cout << "CSonA:: AA" << endl;
	}

	void BB()
	{
		cout << "CSonA:: BB" << endl;
	}
};

class CSonB : public CFather
{
public:
	void AA()
	{
		cout << "CSonB:: AA" << endl;
	}

	void BB()
	{
		cout << "CSonB:: BB" << endl;
	}
};

int main()
{
	CFather* p = new CSonA;
	p->AA();
	system("pause");

}

这是一种有虚函数覆盖的一般继承方式,他所对应的图大致是这样:
在这里插入图片描述

说明:子类的虚函数列表是要先继承父函数列表的,但是此处是“有虚函数覆盖且全覆盖”所以就没有画出。如果暂时无法理解也没有关系,看完后面的内容再回头看这段说明就明白啦。

父类(CFather)中存在虚函数时,就会有一个名为_vfptr的指针(如果没有虚函数,就没有喽,当然那样也跟多态没啥关系啦)
在这里插入图片描述

另外补充一点东西:
既然说_vfptr是指针,所以他就占四个字节啦
所以
(1)如果你的类是这样的:

class A{
 public: int a;
 };

那么你sizeof(A)得到的结果会是4
(2)如果你的类是这样的:

 class A{
 };

那么你sizeof(A)得到的结果会是1,1个字节用来占位
(3)如果你的类是这样的:

class A{
public: int a;
public: virtual void show(){};
 };

那么你sizeof(A)得到的结果会是8
也是间接证明了当有虚函数存在的时候,指针_vfptr的存在

它的作用是指向一个虚函数表(v_table),虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是有一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其能真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针(_vfptr)存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
在这里插入图片描述
将上面主函数换成下面的代码,观察现象:

int main()
{
	typedef void(*Fun)(void);
	CFather* p = new CFather(5);
	Fun pFun;

	pFun = (Fun) *((int*)*((int*)p)+1);
	int a = ((int*)p)[1];
	pFun();
	cout << a << endl;

	system("pause");
}

运行结果为:

CFather::BB
5

仔细推敲一下,你应该能够得知其中的道理。
在这里插入图片描述

解释如下:

  • p是CFather类型的指针它指向我们new出来的CFther类型的类
  • p = 0x10
  • *((int*)p)是对p进行强制类型转换,并间接引用得到虚函数表的首地址,即ptr1的地址
  • *((int*)p) = 0x40
  • 往后偏移一位得到ptr2的地址
  • *((int*)p) + 1= 0x41
  • 再强转并间接引用,得到CFather::BB()的函数地址
  • *((int*)*((int*)p)+1) = 0x20
  • 再强转为函数指针类型赋值给pFun
  • 此时pFun就是一个指向CFather::BB()的函数指针,可以通过它调用函数CFather::BB()
    你也可以自己推一下为什么输出的变量a的值是5

说明:
关于这里为什么要强转,说明一下
间接引用取到的内容的大小,取决于指针是什么类型的

例如:int *p间接引用之后能够取到4个字节的内容
char *p间接引用之后只能取到1个字节的内容

写到这里我联想到二维数组的偏移,不妨来回忆一下:
有一个数组这样定义:int arr[2][3];
如果int *p = arr,则p+=1之后指针会相对偏移 3*4=12个字节
如果int *p = &arr,则p+=1之后指针会相对偏移 2*3*4=24个字节
仅供类比

言归正传,这里为什么要强转,因为p是CFather类型的指针,它占8个字节,我们的目的是取到_vfptr的内容,只需要前4个字节(指针类型占4个字节并且_vfptr位于最前面),所以我们强转为int*类型,间接引用来拿到_vfptr中的内容。

(4)一般继承(无虚函数覆盖)

假设有如下所示的一个继承关系:

在这里插入图片描述
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

对于实例:Derive d; 的虚函数表如下:
在这里插入图片描述
我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

(5)一般继承(有虚函数覆盖)

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
在这里插入图片描述
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
在这里插入图片描述

我们从表中可以看到下面几点,

1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();
 b->f();

注意:

  • 有虚函数覆盖的时候,子类中覆盖(重写)父类的函数写不写virtual都是可以的,因为写不写他都是虚函数,但是父类中该函数一定要写。

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

(6)多重继承(无虚函数覆盖)

假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
在这里插入图片描述
对于子类实例中的虚函数表,是下面这个样子:
在这里插入图片描述
我们可以看到:

1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

(7)多重继承(有虚函数覆盖)

下图中,我们在子类中覆盖了父类的f()函数。
在这里插入图片描述
下面是对于子类实例中的虚函数表的图:
在这里插入图片描述
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d; <br>
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()<br>
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

(2)多态总结:

在这里插入图片描述

补充-友元:

1.什么是友元

  • 类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
  • 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

2.友元能做什么

  • 私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。

  • C++ 是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。

  • C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。

3.友元怎么使用

友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。

  • 将全局函数声明为友元的写法如下:
friend  返回值类型  函数名(参数表);
  • 将其他类的成员函数声明为友元的写法如下:
friend  返回值类型  其他类的类名::成员函数名(参数表);

但是,不能把其他类的私有成员函数声明为友元。

友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:

friend  class  类名;

标签:函数,int,子类,笔记,学习,继承,C++,父类,public
From: https://www.cnblogs.com/fau152/p/17218407.html

相关文章

  • 人月神话阅读笔记01
    阅读笔记01什么是人月神话?人是程序员,月是时间,当1个人干10个月等同于10个人干1个月,那就成了“神话”!这也就是人月神话名称的由来。其中,焦油坑示例,让我印象颇深,所谓焦油......
  • Markdown语法笔记
    Markdown语法笔记#代表标题,标题级别大小随#数量增加而变小,最多到6级标题+分割线二级标题自带分割线图片![file](图片url)超链接超视网膜屏幕[文字](跳转url)//必......
  • 思考(C++)
    为什么C++类中成员访问修饰符是private、protected、public三种而不是别的?面向对象的三大特征是:封装,继承和多态封装是指隐藏对象的属性和实现细节,仅对外公开接口使得使用......
  • python+playwright 学习-32 启动Google Chrome 或 Microsoft Edge浏览器
    前言playwright默认会下载chromium,firefox和webkit三个浏览器,目前支持通过命令下载的浏览器有:chromium、chrome、chrome-beta、msedge、msedge-beta、msedge-dev、f......
  • opencv读取摄像头并显示的C++代码
    #include<opencv2/opencv.hpp>#include<iostream>usingnamespacecv;usingnamespacestd;intmain(){//创建VideoCapture对象,参数为0表示打开默认摄像头......
  • 《动手学深度学习》安装mxnet出现问题
    在看《动手学深度学习》时,安装mxnet(CPU)版时安装失败。首先是下载时使用国内镜像,可参考Python安装库太慢?配置好这个速度飞起-知乎(zhihu.com)下载时出现的问题:Buildin......
  • 数据类型学习
    JAVAWriteOnceRunanywhereJDK:java开发者工具JRE:java运行时环境JVM:java虚拟机HelloworldpublicclassHelloWorld{publicstaticvoidmain(String[]arg......
  • 快速莫比乌斯/沃尔什变换 (FMT/FWT) 学习笔记
    引入考虑一个基本问题:给定序列\(a_n,b_n\),求出序列\(c_n\),满足\(c_i=\sum_{j\oplusk=i}a_jb_k\),其中\(\oplus\)是一种二元运算符,形如上式的问题一般被称为卷积。......
  • 笔记即思维
    很多人会纠结于哪个笔记软件比较好,对比各个软件的功能。其实并没有必要,他们只是代表了各个设计者不同的思维。比如:印象笔记,是按照传统的笔记本,多个笔记本形成组,就像我们......
  • python入门学习-3.多线程、多进程、网络通信
    进程和线程多任务线程是最小的执行单元,而进程由至少一个线程组成。多进程Linux操作系统提供了一个fork()系统调用,子进程返回0,父进程返回子进程的ID。调用getpid()可以......