C++继承

回忆相关知识点:

初始化参数列表的调用在调用构造函数之前

继承

定义:

继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。

继承方式和访问限定符

我们知道,访问限定符有以下三种:

  1. public访问
  2. protected访问
  3. private访问
    而继承的方式也有类似的三种:
  4. public继承
  5. protected继承
  6. private继承

继承基类成员访问方式的变化

请添加图片描述
我们发现规律是:取最小权限即可。
基类的private在子类中都不可以访问,但是全部继承。

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

派生类对象可以赋值给基类的对象、基类的指针以及基类的引用,因为在这个过程中,会发生基类和派生类对象之间的赋值转换。但是反过来绝对不行代码如下:

//基类
class Person
{
protected:
	string _name; //姓名
	string _sex;  //性别
	int _age;     //年龄
};
//派生类
class Student : public Person
{
protected:
	int _stuid;   //学号
};
==调用==
Student s;
Person p = s;     //派生类对象赋值给基类对象
Person* ptr = &s; //派生类对象赋值给基类指针
Person& ref = s;  //派生类对象赋值给基类引用

以上的做法叫切割或切片,利用子类对象实例化父类、父类指针指向子类、用父类引用去引用子类对象
请添加图片描述

同名隐藏

当子类中,只要函数名字和父类相等,参数可以不一样,就会构成同名隐藏。而还有叫重写,子类重写父类方法,需要完全一样。同名隐藏会有点BUG,如下图,子类调不到父类的f()了。
请添加图片描述
在子类中可以直接调用父类的函数,构成同名隐藏时,更该如此调用。如下:
s.Person::fun(20);
同名隐藏的缺点
如果不同名隐藏,子类可以直接调用到父类的方法,比如b.f(),因为继承可以直接用子拿父。此外,同名隐藏后,子拿不到父的方法了,编译也错。

区分同名隐藏、同名覆盖(重写)

  1. 同名隐藏只有函数名相同即可。
  2. 同名覆盖也就是重写,需要要求同名、参数类型、返回类型、公有继承都一致。

派生类的默认成员构造函数

默认成员函数,即我们不写编译器会自动生成的函数,类当中的默认成员函数有以下六个:
请添加图片描述

//基类
class Person
{
public:
	//构造函数
	Person(const string& 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;
	}
private:
	string _name; //姓名
};

student类默认成员函数基本逻辑如下:

**//派生类
class Student : public Person
{
public:
	//构造函数
	Student(const string& name, int id)
		:Person(name) //调用基类的构造函数初始化基类的那一部分成员
		, _id(id) //初始化派生类的成员
	{
		cout << "Student()" << endl;
	}
	//拷贝构造函数
	Student(const Student& s)
		:Person(s) //调用基类的拷贝构造函数完成基类成员的拷贝构造
		, _id(s._id) //拷贝构造派生类的成员
	{
		cout << "Student(const Student& s)" << endl;
	}
	//赋值运算符重载函数
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s); //调用基类的operator=完成基类成员的赋值
			_id = s._id; //完成派生类成员的赋值
		}
		return *this;
	}
	//析构函数
	~Student()
	{
		cout << "~Student()" << endl;
		//派生类的析构函数会在被调用完成后自动调用基类的析构函数
	}
private:
	int _id; //学号
};
**

派生类与普通类的默认成员函数的不同之处概括为以下几点:

派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员,如果基类当中没有默认的构造函数,则必须在派生类构造函数的初始化列表当中显示调用基类的构造函数。非必须
派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造。** 必须**
派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值。必须,还使用了切片。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。
派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。

在编写派生类的默认成员函数时,需要注意以下几点:

在子类内部:
1 派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
2 由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();。因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
3 在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
在子类定义中拿父类函数,直接 父::fun()

注意:
基类的构造函数、拷贝构造函数、赋值运算符重载函数我们都可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,我们若是自行调用基类的构造函数就会导致基类被析构多次的问题。
我们知道,创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数。

继承和友元

类的public成员变量可以在类外被自由访问。
友元关系不能继承,爹的友元只能玩爹。

继承与静态成员

基类中的static静态成员变量只有一份,无论派生多少个子类都只有一个static成员实例,总之就是共用同一个。

菱形继承

多继承的一种,子类中存两个父类中的两个爷类,因为子类一定会存父类中全部,而两个父类有一个爷类,所以爷类就重复了。多用了内存,此外,访问成员会有二义性,孙子里面存在两个爷爷,孙子不知道用哪个爹中的爷爷类。所以需要指明。菱形继承关系如下:
请添加图片描述

菱形继承的二义性

如下代码,直接通过子类,再也无法直接拿到基类的成员。

#include <iostream>
#include <string>
using namespace std;
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()
{
	Assistant a;
	a._name = "peter"; //二义性:无法明确知道要访问哪一个_name
	return 0;
}

解决办法是指明要获取哪个父类的_name:

//显示指定访问哪个父类的成员
a.Student::_name = "老刘同学";
a.Teacher::_name = "老刘老师";

进一步了解菱形继承:

观察如下代码的内存分布情况:

#include <iostream>
using namespace std;
class A
{
public:
	int _a;
};
class B : public A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

通过内存窗口,我们可以看到D类对象当中各个成员在内存当中的分布情况如下:
请添加图片描述
内存中的分布情况:
请添加图片描述

解决方案:菱形虚拟继承

#include <iostream>
#include <string>
using namespace std;
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; //主修课程
};
int main()
{
	Assistant a;
	a._name = "peter"; //无二义性
	return 0;
}

观察内存如下:
请添加图片描述

内存分布如下:

请添加图片描述
解释:
原来菱形继承中的每个父类都存一份A,而现在A的位置存了两个指针,叫虚基表指针,而且我们发现在对象D中存的A只有一份且放在最后,两个指针指向的两个虚基表中存的是距离对象D中共同父亲A(共同基类)的成员的偏移量。也就是,经过偏移量计算,可以得到成员_a。
且此后的切片,都会按照虚基表中的第二个数据找到公共A的成员,得到切片后该B类对象在内存中仍然保持这种分布。

继承总结

继承是复用的一种手段,但是多继承可能出现菱形继承,要避免菱形继承。

继承和组合

继承关系是:is-a,狗和动物,而组合是:has-a,轮子和车的关系。

习题1:

  1. 下面关于继承说法不正确的是( )
    A.继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展
    B.继承体系中子类必须要体现出与基类的不同
    C.子类对象一定比基类对象大
    D.继承呈现了面相对象程序设计的层次结构,体现了有简单到复杂的认知过程
    答:
    B:好像有点绝对,但是肯定的,不然你定义子类干啥。
    C:不一定,如果子类只改写父类方法,没有增加自身数据成员,则大小一样。

  2. 关于继承说法正确的是( )
    A.所有的类都可以被继承
    B.Car(汽车)类和Tire(轮胎)类可以使用继承方式体现
    C.继承是实现代码复用的唯一手段
    D.狗是一种动物,可以体现出继承的思想
    答:
    A:final标记类不能被继承。
    B:has-a关系需要组合,不是继承,继承是is-a
    C:模板也是代码复用的手段
    D:狗继承动物。

  3. 下面关于访问权限与继承权限说法不正确的是:
    A.访问权限和继承权限是不同的概念
    B.访问权限和继承权限关键字上是一样的,但是出现位置不一样
    C.如果是protected继承方式,基类public的成员变量能通过基类对象在类外直接访问
    D.基类私有的成员变量在子类中都不能直接访问,因为没有被子类继承了
    答:
    A:肯定不一样
    B:访问权限在类内,继承权限在类外(从父类角度来说)由子决定。
    C:protected访问权限是为了类外不能访问,而子类可以访问,而public以protected继承,则子类继续访问,类外public也自由访问。
    D:父类的全部都会被继承,但是private不能访问。

  4. 关于同名隐藏正确的是:
    A.同一个类中,不能存在相同名称的成员函数
    B.在基类和子类中,可以存在相同名称但参数列表不同的函数,他们形成重载
    C.在基类和子类中,不能存在函数原型完全相同的函数,因为编译时会报错
    D.成员函数可以同名,只要参数类型不同即可,成员变量不能同名,即使类型不同
    答:
    A:可以,重载
    B:父类子类是两个不同的空间,不会重载,会成同名隐藏。
    C:不报错,是同名隐藏且合法。
    D:成员函数同名且不同参直接可以重载,但是成员变量不能存在同名

  5. 下面代码输出结果:( )

class A
{
public:
 void f(){ cout<<"A::f()"<<endl; }
 int a;   
};
class B : public A
{
public:
 void f(int a){cout<<"B::f()"<<endl;}
 int a;
};
int main()
{
 B b;
 b.f();
 return 0;
}

请添加图片描述
A.打印A::f()
B.打印B::f()
C.不能通过编译,因为基类和派生类中a的类型以及名称完全相同
D.以上说法都不对
答:
B把A的f()覆盖了,如果调用的话,编译都出错错在调用**。

区分同名隐藏、同名覆盖(重写)

  1. 同名隐藏只有函数名相同即可。
  2. 同名覆盖也就是重写,需要要求同名、参数类型、返回类型、公有继承都一致。
  1. 下面关于继承权限说法正确的是( )
    A.派生类在继承基类时,必须明确指定继承方式
    B.Class定义的类,默认的访问权限是protected
    C.struct定义的类,默认访问权限是public
    D.子类没有继承基类私有的成员
    答:
    A:不指定时候,以private,可以不指定。
    B:class默认private
    C:对
    D:继承了,但不能访问。

  2. 以下程序运行结果正确的是( )

template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
template <class T1>
class Data<T1, int>
{
public:

Data() { cout << "Data<T1, int>" << endl; }

private:

T1 _d1;

int _d2;

};

template <typename T1, typename T2>

class Data <T1*,T2*>

{

public:

Data() { cout << "Data<T1*, T2*>" << endl; }

private:

T1 _d1;

T2 _d2;

};

template <typename T1, typename T2>

class Data <T1&, T2&>

{

public:

Data(const T1& d1, const T2& d2)

: _d1(d1)

, _d2(d2)

{

cout << "Data<T1&, T2&>" << endl;

}

private:

const T1 & _d1;

const T2 & _d2;

};

int main()

{

Data<double, int> d1;

Data<int, double> d2;

Data<int *, int*> d3;

Data<int&, int&> d4(1, 2);

return 0;

}

A.Data<T1, T2> Data<T1, int> Data<T1*, T2*> Data<T1&, T2&>
B.Data<T1, int> Data<T1, T2> Data<T1&, T2&> Data<T1*, T2*>
C.Data<T1, int> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>
D.Data<T1, T2> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>

答:
Data<double, int> d1;看代码下半段,跟指针引用相关。所以这肯定向上找偏特化或全特化,显然第二个更匹配一点。从BC中挑,看第三个两个指针类型,肯定找指针的偏特化,所以肯定选是C。

习题2

  1. 下面说法正确的是( )
    A.派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化
    B.派生类构造函数先初始化子类成员,再初始化基类成员
    C.派生类析构函数不会自动析构基类部分成员
    D.子类构造函数的定义有时需要参考基类构造函数
    答:
    A:必须太绝对了,当基类的构造函数是默认构造函数则不需要显示调用,编译器会自动调用。
    默认构造函数特指无参空构造函数。
    B:派生类和父类的初始化顺序:构造先父再子,析构先子再父,符合FILO、构造和析构顺序相反。
    C:会自动调用,**注意不用专门在子类析构中显示写出来调用父类析构,**因为编译器会自动调用,且先调用子类析构,再调用父类析构。
    D:需要,比如父类构造函数有参数,则子类的构造函数中需要显示调用。父类的构造函数直接影响到子类的构造函数。

  2. 关于派生类构造函数与析构函数说法正确的是( )
    A.在派生类对象构造时,先调用基类构造函数,后调用子类构造函数
    B.在派生构造函数初始化列表的位置必须显式调用基类构造函数
    C.在派生类对象销毁时,先调用基类析构函数,后调用子类析构函数
    D.派生类的析构函数只需析构派生类的资源即可
    答:
    A:A,构造先父再子,析构先子再父
    B:父类有默认构造就不需要
    C:反了
    D:派生类的析构还要连通父类的析构一起调用,这个D感觉说的不好,因为你不用写出来,我们写的话只写派生类的析构就行了,因为父类析构会自动调用。人家说的我觉得也行

  3. 关于基类与派生类对象模型说法正确的是()
    A.基类对象中包含了所有基类的成员变量
    B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量
    C.子类对象中没有包含基类的私有成员
    D.基类的静态成员可以不包含在子类对象中
    E.以上说法都不对
    答:
    A:静态变量在静态区。
    B:基类成员变量有静态变量就说错了
    C:子类对象中会有包含,因为全都继承,
    D:文字游戏,一定不被包含,但是人家说的TM也对啊,可不包含,可是什么意思呢?B题。可理解为是,D就对啊。
    E:答案是这个勾八。

  4. 关于基类与子类对象之间赋值说法不正确的是( )
    A.基类指针可以直接指向子类对象
    B.基类对象可以直接赋值给子类对象
    C.子类对象的引用不能引用基类的对象
    D.子类对象可以直接赋值给基类对象
    赋值兼容规则
    指针可以从子类给父类。
    子类对象可以直接赋值给基类对象。
    ========
    B:父类不能给子类,因为父类对子类来说不完全。
    C:父类也不能给子类的引用。BC是一样的。
    选B。

  5. 正确的是:

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 
{ public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}

A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3

多继承时候,两个父类B1B2在Derive中所在位置不一样,所以切片位置也不一样,所以p1!=p2。
而Base1是第一个继承的父类,父类对象存在子类对象特有变量的前面,所以Base1的起始位置就是对象d的起始位置。p1==p3。

  1. 下列代码中f函数执行结束后输出( )
class A
{
public:
  A() { cout<<"A::A()"<<endl; }
  ~A() { cout<<"A::~A()"<<endl; }
  int a;
};
class B : public A
{
public:
  B() { cout<<"B::B()"<<endl; }
  ~B() {cout<<"B::~B()"<<endl; }
  int b;
};
void f()
{
  B b;
}

A.B::B() B::~B()
B.B::B() A::A() A::~A() B::B()
C.A::A() B::B() B::~B() A::~A()
D.以上都不对
按构造先父后子,析构先子后父即可。

  1. 关于以下菱形继承说法不正确的是( )
class B {public: int b;};
class C1: public B {public: int c1;};
class C2: public B {public: int c2;};
class D : public C1, public C2 {public: int d;};

A.D总共占了20个字节
B.B中的内容总共在D对象中存储了两份
C.D对象可以直接访问从基类继承的b成员
D.菱形继承存在二义性问题,尽量避免设计菱形继承

A:
类B:对齐数是4,大小4
类C1:对齐数4,大小8
类C2:对齐数4,大小8
类D:对齐数4,它继承了C1、C2,没有直接继承B,所以大小是自己的4+C1和C2= 20,且对齐了4。
B:B中内容在D对象存了两份。从类C1和类C2中来。对的
C:不能直接访问,因为菱形继承有二义性,需要加类名::B,告诉编译器访问从C1还是C2来的B。
D:菱形继承存在二义性,要避免。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值