C++中的继承

C++中的继承


一、继承的概念以及定义

1.继承的概念

继承是面向对象编程的三大特征之一,继承机制是一种面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

cpp 复制代码
class Person
{
public:
 void Print()
 {
 cout << "name:" << _name << endl;
 cout << "age:" << _age << endl;
 }
protected:
 string _name = "peter"; // 姓名
 int _age = 18;  // 年龄
};

class Student : public Person
{
protected:
 int _stuid; // 学号
};

class Teacher : public Person
{
protected:
 int _jobid; // 工号
};

2.继承的定义

Person是父类,也称作基类。Student是子类,也称作派生类。

cpp 复制代码
//    (子类)              (父类)
//     派生类   继承方式   基类
class Student : public   Person
{
private:
	int _major;
	...

继承方式有三种:public继承、private继承、protected继承

访问限定符有三种:public访问、private访问、protected访问
对于继承基类成员访问方式的变化

对于表格的概括:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
  3. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),其中 public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。

二、基类和派生类对象赋值转换

  1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这个操作叫切片 或者切割 。寓意把派生类中父类那部分切来赋值过去。
    **注意:**这里的切片或切割不同于C语言中的截断,截断会产生临时变量(其本质是类型转换),而切片切割不会。
cpp 复制代码
void Test()
{
	//C语言中的截断与提升,本质是类型转换
	int i = 1234;
	//截断
	char ch = i;
	cout << (int)ch << endl;
	//提升
	i = ch;
	cout << i << endl;
	//与上面机制不一样
	//特殊的语法规则,只限制公有继承:不是类型转换,中间没有产生临时变量
	Student s;
	Person p;
	p = s;
	Person* ptr = &s;//指针指向子类中切割出的父类那一部分
	Person& ref = s;//引用的是子类中切割出的父类那一部分
	ptr->_name += 'x';
	ref._name += 'y';
	p._name += 'z';
	cout << p._name << endl;
	cout << s._name << endl;
}
int main()
{
	Test();
	return 0;
}
  1. .基类对象不能赋值给派生类对象。
  2. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
cpp 复制代码
class Person
{
protected :
 string _name; // 姓名
    string _sex;  // 性别
    int _age; // 年龄
};
class Student : public Person
{
public :
 int _No ; // 学号
};
void Test ()
{
 Student sobj ;
 // 1.子类对象可以赋值给父类对象/指针/引用
 Person pobj = sobj ;
 Person* pp = &sobj; 
 Person& rp = sobj;  //引用的是子类中父类那一部分
    
 //2.基类对象不能赋值给派生类对象
    sobj = pobj;
    
    // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj
    Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;
    
    pp = &pobj;
 		Student* ps2 = (Student*)pp; /*这种情况转换时虽然可以,但是会存在越界访问的问
题*/
    ps2->_No = 10;
}

三、继承中的作用域

  1. 在继承体系中基类派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏 ,也叫重定义 。(在子类成员函数中,可以使用 基类::基类成员 显示访问),注意:如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
cpp 复制代码
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A
{
public:
 void fun(int i)
 {
 A::fun();
 cout << "func(int i)->" <<i<<endl;
 }
};
void Test()
{
 B b;
 b.fun(10);
};

四、派生类的默认成员函数

  1. 构造函数:派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 拷贝构造函数:派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 赋值拷贝函数:派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 注意:派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。
  6. 后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
cpp 复制代码
class Person
	 {
public:
	 Person(const char* name = "peter")
		: _name(name)
	 {
			cout << "Person()" << endl;
	 }
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
public:
	static int count;   
};
int Person::count = 0;
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)  
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
			
		{
			// 构成隐藏,所以需要显⽰调⽤
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		//由于多态,析构函数的名字会被统一处理成destructor()
		//析构函数会构成隐藏关系,所以这里要指定类域
		//构造时是先父后子(因为继承时父类变量声明在子类变量声明上面)
		// ,析构时应先子后父!,自己显式写析构时无法保证,所以交由系统自动调用父的析构
		//Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};

五、继承与友元

友元关系不能被继承,也就是说基类友元不能访问子类私有和保护成员。

cpp 复制代码
class Student;
class Person
{
public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 cout << s._stuNum << endl;  //错误
}
void main()
{
 Person p;
 Student s;
 Display(p, s);
}

六、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

cpp 复制代码
class Person
{
public :
 Person () {++ _count ;}
protected :
 string _name ; // 姓名
public :
 static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
 int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
 string _seminarCourse ; // 研究科目
};
void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl;
}

七、菱形继承及菱形虚拟继承

单继承

:一个子类只有一个直接父类时称这个继承关系为单继承

多继承

:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承

:菱形继承是多继承的一种特殊情况。

注意菱形继承是一个巨坑 ,会存在数据冗余二义性的问题。

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职⼯编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 编译报错:对"_name"的访问不明确
	Assistant a;
	a._name = "peter";
	// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,
	// 但是数据冗余问题(同一个对象中存储多份信息)⽆法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

菱形继承的解决方式

cpp 复制代码
class Person
{
public :
 string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
 int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 Assistant a ;
 a._name = "peter";
}

八、继承的总结

  1. 以一般不建议设计出多继承,一定不要设计出菱形继承。
  2. 对于继承和组合 :public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。优先使用对象组合,而不是类继承 。
  3. 实际尽量去用组合。组合的耦合度低,代码维护性好。有些关系就适合继承那就用继承,要实现多态,必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
cpp 复制代码
// Car和BMW Car和Benz构成is-a的关系
   class Car{
   protected:
   string _colour = "白色"; // 颜色
   string _num = "陕ABIT00"; // 车牌号
   };
   class BMW : public Car{
   public:
   void Drive() {cout << "好开-操控" << endl;}
   };
   class Benz : public Car{
   public:
   void Drive() {cout << "好坐-舒适" << endl;}
   };
   
   // Tire和Car构成has-a的关系
   class Tire{
   protected:
       string _brand = "Michelin";  // 品牌
       size_t _size = 17;         // 尺寸
   };
   class Car{
   protected:
   string _colour = "白色"; // 颜色
   string _num = "陕ABIT00"; // 车牌号
    Tire _t; // 轮胎
   };  
相关推荐
迈巴赫车主2 小时前
蓝桥杯 20531黑客java
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
有点。2 小时前
C++ ⼀级 2025 年09 ⽉
开发语言·c++
2501_941982052 小时前
复杂消息格式自动化:图片、视频和自定义卡片的消息体构造
开发语言·php
星辰烈龙2 小时前
黑马程序员Java基础8
java·开发语言
蒙奇D索大2 小时前
【数据结构】考研408 | 红黑树收官与B树启航:删除策略与多路平衡解析
数据结构·笔记·b树·考研·改行学it
sin_hielo2 小时前
leetcode 3531
数据结构·算法·leetcode
a程序小傲2 小时前
华为Java面试被问:SQL执行顺序
java·后端·sql·华为·面试
change_topic2 小时前
c语言实现顺序表和链表(利用了c++的引用)
c语言·开发语言·链表
zmzb01032 小时前
C++课后习题训练记录Day48
数据结构·c++·算法