C++基础(一) —— 面向对象(2)

这篇具有很好参考价值的文章主要介绍了C++基础(一) —— 面向对象(2)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


继承和多态

一、继承

子类可以访问基类的public和protected
基类的private只有自己和友元才能访问(友元会破坏类的封装性)

class A
{
public:
	int ma;
protected:
	int mb;
private:
	int mc; // 自己或者友元能访问私有的成员
};
//继承 A 基类/父类   B 派生类/子类
struct B : A
{
public:
	void func() 
	{
		cout << ma << endl;
	}
	int md;
protected:
	int me;
private:
	int mf;
	// int ma;
};
class C : public B
{
	// 在C里面,请问ma的访问限定是什么? 不可见的,但是能继承来
};
int main()
{
	B b;
	//cout << b.mb << endl;

    return 0; 
}

派生类构造过程

除构造函数和析构函数,派生类从基类可以继承来所有的成员(变量和方法)
派生类通过调用基类的构造函数来初始化从基类继承来的成员变量

派生类对象构造和析构的过程:

1.派生类调用基类构造,初始化从基类继承来的成员

2.调用派生类自己的构造,初始化派生类自己特有的成员

派生类对象的作用域到期了

1.调用派生类析构,释放派生类成员可能占用的外部资源(堆内存,文件)

2.调用基类析构,释放派生类内存中,从基类继承来的成员可能占用的外部资源(堆内存,文件)

例1:

class Base
{
public:
	Base(int data) : ma(data) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
protected:
	int ma;
};

class Derive : public Base
{
public:
	// “Base”: 没有合适的默认构造函数可用
	Derive(int data) : Base(data), mb(data)  // ma(data)
	{
		cout << "Base protected member:" << ma << endl;
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
private:
	int mb;
};

int main()
{
	Derive d(20);

	return 0;
}

例2

#ifndef _ROBOT_TCP_H_
#define _ROBOT_TCP_H_
#include <iostream>
class Robot_tcp_cilent
{
public:
	Robot_tcp_cilent(const char* IP, int hont);
	~Robot_tcp_cilent();

private:
};
#endif


class Client: public Robot_tcp_cilent
{
private:
	ros::NodeHandle nh;
	ros::Subscriber subDog_arrived;
	
public:
	Client() : Robot_tcp_cilent("127.0.0.1", 5178)
	{
		subDog_arrived = nh.subscribe<std_msgs::Int8> ("/dog_arrivedsignal", 5, &Client::dogarrivedHandler, this, ros::TransportHints().tcpNoDelay());  
	}
	~Client(){}; 

	void dogarrivedHandler(const std_msgs::Int8::ConstPtr& arrived)
	{
	  	dogarrivedflag = arrived->data;
	  	// 狗到达目标点标志位
	}
}

二、多态

重载、隐藏、覆盖

1.重载
一组函数要重载,必须处在同一个作用域当中;而且函数名字相同,参数列表不同

2.隐藏(作用域的隐藏)
在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了(无论参数是否相同)。如果在派生类中想要访问基类的同名函数,可以使用作用域解析运算符(::)来显式调用。

3.覆盖
覆盖是指在派生类中重新定义一个与基类中同名的成员函数,使其具有相同的函数签名(返回类型、函数名和参数列表)。
通过覆盖,派生类可以改变基类成员函数的实现,但保持相同的函数接口。在使用基类指针或引用调用成员函数时,如果派生类中有与基类同名的函数,将会调用派生类中的函数,而不是基类中的函数。这实现了运行时多态性,允许通过基类指针或引用来调用派生类中特定的函数。

以上是实现多态性和灵活性的重要机制。重载提供了处理不同参数的能力,隐藏允许在派生类中屏蔽基类的同名函数,而覆盖允许在派生类中重新定义和改变基类成员函数的行为。

class Base
{
public:
	Base(int data = 10) :ma(data) {}
	void show() { cout << "Base::show()" << endl; } //#1
	void show(int) { cout << "Base::show(int)" << endl; }//#2
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data = 20) :Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; } //#3
private:
	int mb;
};
int main()
{
	Base b(10);
	Derive d(20);

	// 基类对象b <- 派生类对象d   类型从下到上的转换  Y
	b = d;

	// 派生类对象d <- 基类对象b   类型从上到下的转换  N
	// d = b;

	// 基类指针(引用)<- 派生类对象  类型从下到上的转换 Y
	Base *pb = &d;
	pb->show();
	//((Derive*)pb)->show();
	pb->show(10);

	// 派生类指针(引用)<- 基类对象  类型从上到下的转换  N
	Derive *pd = (Derive*)&b; // 不安全,涉及了内存的非法访问!
	pd->show();

#if 0
	Derive d;
	d.show();
	d.Base::show();
	//“Derive::show”: 函数不接受 1 个参数
	d.Base::show(10);
	d.show(20);//优先找的是派生类自己作用域的show名字成员;没有的话,才去基类里面找
#endif
	return 0;
}

虚函数、静态绑定、动态绑定

本质为覆盖:
基类和派生类的方法,返回值、函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,它们之间成为覆盖关系。

一个类添加了虚函数,对这个类有什么影响?

总结一:
一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区。

总结二:
一个类里面定义了函数数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,它们的额vfptr指向的都是同一张虚函数表。

总结三:
一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的是虚函数表的大小。

虚函数

问题一:哪些函数不能实现成虚函数?

虚函数依赖:
1.虚函数能产生地址,存储在vftable当中
2.对象必须存在(vfptr -> vftable -> 虚函数地址)

所以,以下函数不能为虚

  1. virtual + 构造函数 NO!
    构造函数中(调用的任何函数,都是静态绑定的)调用虚函数,也不会发生静态绑定。
    (派生类对象构造过程 1.先调用的是基类的构造函数 2.才调用派生类的构造函数)
  2. virtual + incline NO!
  3. virtual + static NO!
    静态成员函数属于类本身,而不是类的实例。

问题二:虚析构函数调用的时候,对象是存在的!所以基类析构函数必须实现成虚函数。
原因:如果将基类的析构函数不声明为虚函数,那么在删除派生类对象时,只会调用基类的析构函数,而派生类的析构函数将不会被调用。这将导致派生类的资源无法得到正确释放。

class Base
{
public:
	Base(int data) :ma(data) { cout << "Base()" << endl; }
	// 虚析构函数
	virtual ~Base() { cout << "~Base()" << endl; }
	virtual void show() { cout << "call Base::show()" << endl; }
protected:
	int ma;
}; // &Base::~Base   &Base::show
class Derive : public Base // &Derive::~Derive   &Base::show 
{
public:
	Derive(int data)
		:Base(data), mb(data), ptr(new int(data))
	{
		cout << "Derive()" << endl;
	}
	// 基类的析构函数是virtual虚函数,那么派生类的析构函数自动成为虚函数
	~Derive()
	{
		delete ptr;
		cout << "~Derive()" << endl;
	}
private:
	int mb;
	int *ptr;
};
int main()
{
	Base *pb = new Derive(10);
	pb->show(); //  动态绑定 pb Base*  *pb Derive
	delete pb; // 基类析构为虚,才能正确调用派生类的析构
	/*输出
	~Derive()
	~Base()*/

	/*
	pb->Base  Base::~Base 对于析构函数的调用,就是静态绑定了
	call Base::~Base

	pb->Base Base::~Base 对于析构函数的调用,就是动态绑定了
	pb -> Derive Derive vftable &Derive::~Derive
	*/

	Derive d(10);
	Base *pb = &d;
	pb->show();

	return 0;
}

问题3:纯虚析构函数
纯虚析构函数需要在类的定义外部提供纯虚析构函数的实现,如示例中的Base::~Base()所示。

class Base {
public:
    virtual ~Base() = 0;  // 纯虚析构函数
};

Base::~Base() {
    cout << "~Base()" << endl;
}

class Derived : public Base {
public:
    ~Derived() {
        cout << "~Derive()" << endl;
    }
};

int main() {
    // Base* ptr = new Base();  // 错误,无法实例化抽象类
    Base* ptr = new Derived();  // 可以使用派生类指针来指向派生类对象
    delete ptr;  // 调用Derived的析构函数
    return 0;
}

输出:

~Derive()
~Base()

问题4:指针调用方式
Derived* ptr = new Derived();Base* ptr = new Derived();虽然语法上相似,但它们有一些不同的效果。

静态类型和动态类型:
Derived* ptr = new Derived();中,ptr的静态类型和动态类型都是Derived*。这意味着编译器将ptr视为指向Derived类对象的指针,可以直接访问Derived类的成员和函数。
Base* ptr = new Derived();中,ptr的静态类型是Base*,而动态类型是Derived*。这意味着编译器将ptr视为指向Base类对象的指针,但实际上它指向的是派生类Derived的对象。这种情况下,通过ptr只能访问到基类Base中定义的成员和函数,无法直接访问派生类Derived中特有的成员和函数。

多态性:
Derived* ptr = new Derived();中,由于ptr的静态类型和动态类型相同,可以实现动态绑定(dynamic binding)和多态性。当使用虚函数时,可以根据实际对象的类型来调用相应的函数实现。
Base* ptr = new Derived();中,由于ptr的静态类型是Base*,虽然它指向派生类Derived的对象,但在通过ptr调用虚函数时,只能根据静态类型的信息来进行绑定,而不考虑实际对象的类型。这称为静态绑定(static binding),无法实现多态性。
总结起来,Derived* ptr = new Derived();和Base* ptr = new Derived();在语法上相似,但它们的静态类型、动态类型和多态性方面有所不同。使用Derived可以访问派生类特有的成员和函数,并支持动态绑定和多态性。而使用Base只能访问基类的成员和函数,并且静态绑定导致无法实现多态性。选择使用哪种方式取决于具体的需求和设计目标。

动态绑定、静态绑定

复习多态的不同形式
1 静态多态
是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早早绑定的。
静态多态往往通过函数重载(运算符重载)和模版(泛型编程)来实现。

2 动态多态
指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定,这就属于晚绑定,动态多态也往往被叫做动态绑定。 ​
动态绑定
​当使用基类的引用或指针调用虚成员函数时会执行动态绑定。动态绑定直到运行的时候才知道到底调用哪个版本的虚函数,所以必为每一个虚函数都提供定义,而不管它是否被用到,这是因为连编译器都无法确定到底会使用哪个虚函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

问题:那是不是虚函数的调用一定就是动态绑定? 肯定不是的!
1、在类的构造函数当中,调用虚函数,静态绑定(构造函数中调用其它函数(虚),不会发生动态绑定)。
2、不是通过指针或者引用变量来调用虚函数,也是静态绑定。

class Base
{
public:
	Base(int data = 0) :ma(data) {}
	virtual void show() { cout << "Base::show()" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data = 0) :Base(data), mb(data) {}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Base b;
	Derive d;

	// 静态绑定 用对象本身调用虚函数,是静态绑定
	b.show(); // 虚函数 call Base::show 
	d.show(); // 虚函数 call Derive::show

	// 动态绑定(必须由指针调用虚函数)
	Base *pb1 = &b;
	pb1->show();
	Base *pb2 = &d;
	pb2->show();

	// 动态绑定(必须由引用变量调用虚函数)
	Base &rb1 = b;
	rb1.show();
	Base &rb2 = d;
	rb2.show();

	// 动态绑定(虚函数通过指针或者引用变量调用,才发生动态绑定)
	Derive *pd1 = &d;
	pd1->show();
	Derive &rd1 = d;
	rd1.show();

	Derive *pd2 = (Derive*)&b;
	pd2->show(); // 动态绑定 pd2 -> b vfptr -> Base vftable Base::show

	return 0;
}

多态总结

静态(编译时期)的多态: 函数重载、模板(函数模板和类模板)

动态(运行时期)的多态: Base Derive
在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为动态多态。

多态底层是通过动态绑定来实现的,pbase-》访问谁的vfptr=》继续访问谁的vftable =》当然调用的是对应的派生类对象的方法了

继承、多态的好处是什么?
1.可以做代码的复用
2.在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了

实例

抽象类Animal

// 动物的基类
class Animal
{
public:
	Animal(string name) :_name(name) {}
	virtual void bark() {}
protected:
	string _name;
};
// 以下是动物实体类
class Cat : public Animal
{
public:
	Cat(string name) :Animal(name) {}
	void bark() { cout << _name << " bark: miao miao!" << endl; }
};
class Dog : public Animal
{
public:
	Dog(string name) :Animal(name) {}
	void bark() { cout << _name << " bark: wang wang!" << endl; }
};
class Pig : public Animal
{
public:
	Pig(string name) :Animal(name) {}
	void bark() { cout << _name << " bark: heng heng!" << endl; }
};
/*
下面的一组bark API接口无法做到我们软件涉及要求的“开-闭“原则
软件设计由六大原则   “开-闭“原则  对修改关闭,对扩展开放
*/
void bark(Animal *p)
{
	p->bark(); // Animal::bark虚函数,动态绑定了
	/*
	p->cat Cat vftable &Cat::bark
	p->dog Dog vftable &Dog::bark
	p->pig Pig vftable &Pig::bark
	*/
}
int main()
{
	Cat cat("猫咪");
	Dog dog("二哈");
	Pig pig("佩奇");

	bark(&cat);
	bark(&dog);
	bark(&pig);

	return 0;
}

抽象类Car文章来源地址https://www.toymoban.com/news/detail-470568.html

// 汽车的基类
class Car // 抽象类
{
public:
	Car(string name, double oil) :_name(name), _oil(oil) {}
	// 获取汽车剩余油量还能跑的公里数
	double getLeftMiles()
	{
		// 1L 10  *  oil
		return _oil * this->getMilesPerGallon(); // 发生动态绑定了
	}
	string getName()const { return _name; }
protected:
	string _name;
	double _oil;
	virtual double getMilesPerGallon() = 0; // 纯虚函数
};
class Bnze : public Car
{
public:
	Bnze(string name, double oil) :Car(name, oil) {}
	double getMilesPerGallon() { return 20.0; }
};
class Audi : public Car
{
public:
	Audi(string name, double oil) :Car(name, oil) {}
	double getMilesPerGallon() { return 18.0; }
};
class BMW : public Car
{
public:
	BMW(string name, double oil) :Car(name, oil) {}
	double getMilesPerGallon() { return 19.0; }
};
//给外部提供一个同一的获取汽车剩余路程数的API
void showCarLeftMiles(Car &car)
{
	cout<<car.getName() << " left miles:" 
		<< car.getLeftMiles() << "公里" <<endl;
	 // 静态绑定 call Car::getLeftMiles()
}
int main()
{
	Bnze b1("奔驰", 20.0);
	Audi a("奥迪", 20.0);
	BMW b2("宝马", 20.0);
	showCarLeftMiles(b1);
	showCarLeftMiles(a);
	showCarLeftMiles(b2);
	return 0;
}

到了这里,关于C++基础(一) —— 面向对象(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • c、c++、java、python、js对比【面向对象、过程;解释、编译语言;封装、继承、多态】

    目录 内存管理、适用 区别 C 手动内存管理:C语言没有内置的安全检查机制,容易出现内存泄漏、缓冲区溢出等安全问题。 适用于系统级编程 C++ 手动内存管理:C++需要程序员手动管理内存,包括分配和释放内存,这可能导致内存泄漏和指针错误。 适用于游戏引擎和系统级编

    2024年02月08日
    浏览(76)
  • 面向对象详解,面向对象的三大特征:封装、继承、多态

    一、面向对象与面向过程 面向对象编程 (Object-Oriented Programming,简称OOP)和 面向过程编程 (Procedural Programming,简称PP)是两种不同的 编程范式 。 面向对象编程强调把问题分解成对象,通过封装、继承和多态等机制,来处理对象之间的关系 。每个对象都可以独立地处理自

    2024年02月21日
    浏览(53)
  • 面向对象(类/继承/封装/多态)详解

    面向对象编程(Object-Oriented Programming,OOP)是一种广泛应用于软件开发的编程范式。它基于一系列核心概念,包括类、继承、封装和多态。在这篇详细的解释中,我们将探讨这些概念,并说明它们如何在PHP等编程语言中实现。 类是OOP的基础。它是一种用于创建对象的蓝图或模

    2024年02月08日
    浏览(63)
  • Java面向对象 - 封装、继承和多态

    目录 第1关:什么是封装,如何使用封装 第2关:什么是继承,怎样使用继承 第3关:super的使用 第4关:方法的重写与重载 第5关:抽象类 第6关:final的理解与使用 第7关:接口 第8关:什么是多态,怎么使用多态 Java_Educoder

    2024年02月07日
    浏览(79)
  • 第十一单元 面向对象三:继承与多态

    假设老师类设计如下: 学生类设计如下: 我们秉承着,让最简洁的代码,实现最最强大的功能原则,能否让上述案例中的重复代码进行优化呢?我们能否将学生类与老师类再进行抽象,得到一个人类?这章节学习继承与多态。 继承是面向对象程序设计中最重要的概念之一。

    2024年02月06日
    浏览(58)
  • 【C++】 为什么多继承子类重写的父类的虚函数地址不同?『 多态调用汇编剖析』

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》 《数据结构》 《蓝桥杯试题》 《LeetCode刷题笔记》 《实训项目》 《C++》 《Linux》《算法》 🌝 每一个不曾起舞的日子,都是对生命的辜负 本篇文章主要是为了解答有关多态的那篇文章那块的一个奇怪现象,大家还记得这张

    2024年02月19日
    浏览(38)
  • C++ 基础知识 五 ( 来看来看 面向对象的继承 上篇 )

    C++ 继承是指派生类(子类)从基类(父类)继承属性和行为的过程。我们可以创建一个新的类,该类可以继承另一个类的数据属性和方法。 在上述代码中,我们定义了一个父类 Person 与一个子类 Student。Student 类继承了 Person 类的属性和方法,包括 name、age、gender 和 eat() 函数

    2024年02月03日
    浏览(96)
  • 什么是面向对象,它的三个基本特征:封装、继承、多态

    什么是面向对象思想?已经学完了java确不知道如何跟别人解释面向对象是什么意思这很常见。让我们一起来回顾下这个奇思妙想~ 现在越来越多的高级语言流行起来了,如大家耳熟能详的c++,python,java等,这些都是基于 面向对象 的语言 而最最基础的,学校必学的语言----c语

    2024年02月02日
    浏览(53)
  • Python-面向对象:面向对象、成员方法 、类和对象、构造方法、魔术方法、封装、继承、类型注解、多态(抽象类(接口))

    当前版本号[20230806]。 版本 修改说明 20230806 初版 生活中数据的组织 学校开学,要求学生填写自己的基础信息,一人发一张白纸,让学生自己填, 易出现内容混乱 但当改为登记表,打印出来让学生自行填写, 就会整洁明了 程序中数据的组织 在程序中简单使用变量来记录学

    2024年02月14日
    浏览(51)
  • Go后端开发 -- 面向对象特征:结构体 && 继承 && 多态 && interface

    type 用来声明数据类型 使用 type 定义结构体 对于结构体对象: 可以先定义后初始化; 也可以直接在{}中初始化 值传参 传递的是结构体的拷贝,原结构体不会发生改变 引用传递 传递的是结构体的指针,原结构体的值会改变 GetName 这个函数前面的 (this Hero) 表明这个函数是绑定

    2024年01月17日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包