继承
1.继承是什么
2.继承类的大小
3.继承下的访问限定符
4.继承的三种方式
5.继承与转换--赋值兼容规则
6.继承体系中的作用域
7.派生类的默认成员函数
1.继承是什么
在生活中的许多地方都会提及到继承,比如儿子继承父亲的家产,这部分家产是属于父亲被父亲所创造,然后从天而降给了儿子。类中的继承也是这样。子类继承父类的成员。
继承概念:继承是面向对象复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生的类是个新的类,叫派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的关系。
2.继承类的大小
2.1继承类是子类继承了父类的成员,所以计算子类时还要加上父类的大小。代码中的子类如果看着像是一个空类,他所继承的父类不是空类,那么这个子类大小绝对不是1。
2.2那么,计算子类大小的方式和镶嵌类一样么?
3.继承下的访问限定符
对应三种类成员访问限定符,类的继承也有三种关系:public(公有继承),protected(保护继承),private(私有继承)
实现一个简单的继承:
class Person //person:父类/基类名称
{
public :
void show()
{
cout<<_name<<" "<<_age<<endl;
}
string _name;
int _age;
};
class Student : public Person //Student:子类/派生类名称;public:继承关系
{
public:
void PrintName()
{
cout<<_name<<endl;
}
};
int main()
{
Student s;
s._name = "Linmed";
s._age = 18;
s.show();
s.PrintName();
system("pause");
return 0;
}
//熟悉继承方法
4.继承的三种方式
4.1继承方式及权限对应表:
这个表看似很复杂单纯的记忆可能会比较困难,但如果掌握以下俩个规律便可以很容易记住该表:
a.基类的私有成员无论哪种继承方式对子类都是不可见的。
b.继承后的成员类型却决于基类成员类型与集成方式中权限最小的那个。比如在基类中原本是protected成员,以public方式继承,继承后在派生类中就变成了protected成员。(private权限<protected权限<public权限)
4.2总结
4.2.1 基类的私有成员在派生类中是不能被访问的;如果一些基类成员不想被基类直接访问,但需要在派生类中能访问,就定义为保护成员。可以看出保护成员限定符是因继承才出现的。
4.2.2 public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
4.2.3 protected/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a的关系原则,所以非特殊情况下不会使用这俩种继承关系,在绝大多数情境下使用的都是公有继承。(is-a 与has-a参考后续博客)
4.2.4 不管那种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但在子类中不可见(不能访问)。
4.2.5 使用关键字class时默认的继承方式是private,使用关键字struct时默认的继承方式是public,不过最好显示的写出继承方式。
4.2.5.1 当使用关键字class时,可以很明确看出积累中的成员派生类无法访问
4.2.5.2当关键字为struct时,访问成功:
4.2.6 在实际运用中一般都是public继承,极少场景下才会使用protected/private继承。
注:黄色※
5.继承与转换--赋值兼容规则
5.1 子类对象可以赋值给父类对象(切割/切片)->把子类中的父类的部分切割出来赋值给父类
5.2 父类对象不能赋值给子类对象
5.3 父类的指针/引用可以指向子类对象
5.4 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成,但不能调用成员函数,会崩溃)
class Person
{
public:
virtual void Show()
{
cout << _name << " " << _age << endl;
}
//protected:
string _name;
int _age;
};
class Student : public Person
{
public:
void PrintName()
{
cout << _name << endl;
}
//protected:
int _stunum; // 学号
};
int main()
{
Person p;
p._name = "dad";
p._age = 40;
Student s;
s._name = "son";
p._age = 20;
// 切割/切片
// 父 = 子
p = s; // 5.1子类对象可以赋值给父类对象
//s = p; 5.2父类对象不能赋值给子类对象
//5.3父类的引用指针可以指向子类对象
Person* psptr = &s;
Person& ref = s;
//5.4子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student* pster2 = (Student*)psptr;
Student& ref2 = (Student&)psptr;
psptr->_name = "xx";
ref._name = "yy";
6.继承体系中的作用域
6.1在继承体系中基类和派生类都有独立的作用域。用{}确定
6.2子类和父类有同名成员,子类成员将隐藏(屏蔽)父类对成员的直接访问。但是如果想要访问父类的,可以在子类成员函数中使用 ”基类::基类成员“ 访问(指定作用域)。---重定义
6.3注意在实际中继承体系里面最好不要定义同名成员。定义同名成员基本是一个鸡肋操作,但笔试时也非常喜欢考同名----重载和隐藏的区别(主要在于作用域)下面解释
下面实现一个简单的同名程序,重在说明6.1与6.2
class Person //person:父类/基类名称
{
public :
void show()
{
cout<<_name<<" "<<_age<<endl;
}
string _name;
int _age;
};
class Student : public Person //Student:子类/派生类名称;public:继承关系
{
public:
void show()
{
cout<<_name<<endl;
}
};
int main()
{
Student s;
s._name = "Linmed";
s._age = 18;
s.show(); //父类和子类都有这个同名函数
system("pause");
return 0;
}
给上面代码main函数的show();前指定父类作用域
s.Person::show();
6.3.1函数重载---在同一作用域,函数名相同,参数列表不同(参数的个数,类型,次序不同),和返回值是否相同没有关系。
6.3.2函数隐藏---在不同作用域内(父类和子类),函数名相同,参数相不相同无所谓(同名的成员变量和成员函数和类型。是否带参数无关)。派生类对象调用同名称的类成员,派生类优先
调用自己的。
6.3.3一个常见的笔试题:重载就是函数名相同,参数列表不同...(错误)因为没有说明作用域
7.派生类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义这六个成员函数,编译系统则会默认合成(之前学到的是默认生成)这六个默认的成员函数。 其中构造函数,拷贝构造函数,赋值操作符重载必须调用父类的来合成。析构函数是系统自己调用,这样做是为了保证栈后进先出的规则:初始化时先初始化父类的,析构时先析构子类的。
我们来看下面这段代码更深刻的认识派生类的默认成员函数是如何合成的:
class Person
{
public:
void Display()
{
cout<<_name<<endl;
}
Person(const char* name)
:_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;
};
class Student : public Person
{
public:
void Display()
{
cout<<_name<<_num<<endl;
}
Student(const char* name,int num)
:Person(name)
,_num(num)
{
cout<<"Studen()"<<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()
{
cout<<"~Student()"<<endl;
}
private:
int _num;
};
int main()
{
Student s1("Linmed",12);
Student s2(s1);
s2.Display();
Student s3("dada",13);
s1 = s3;
s1.Display();
system("pause");
return 0;
}
子类的析构函数隐藏了父类的析构函数,编译器会使他们的名字一样构成隐藏。
~Student()
{
//Person::~Person();
}