欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析(3)
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用
如上图所示不同继承下的派生类的访问权限各有不同。
但是我们可以注意到,私有继承和保护继承似乎在单继承上的访问权限上没有体现什么太大区别,这个我们不用急,在多次继承中会慢慢体现差别,或者我们想让成员函数在类外不能被访问,在派生类中能被访问,就可以用到保护继承。
对于表格,我们大致能做出以下总结:
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。
。C++ 中的继承分为单继承和多继承两种形式,其格式语法如下:
单继承是指一个派生类只能从一个基类派生出来。其继承格式如下:
class 派生类名 : 访问说明符 基类名{
// 派生类的成员声明
};
其中,访问说明符有三种:public、protected 和 private。这些访问说明符用于控制基类成员在派生类中的访问权限。如果不指定访问说明符,默认情况下是 private 继承。
多继承是指一个派生类可以同时从多个基类派生出来。其继承格式如下:
class 派生类名 : 访问说明符 基类名1, 访问说明符 基类名2, ...{
// 派生类的成员声明
};
其中,每个基类名都需要指定一个访问说明符。多继承的访问说明符也有三种:public、protected 和 private。这些访问说明符用于控制不同基类的成员在派生类中的访问权限。
需要注意的是,当一个派生类同时从多个基类中继承了同名成员时,会出现二义性问题。为了解决这个问题,C++ 提供了虚继承机制。在虚继承中,如果多个基类都包含了同名成员,派生类只继承其中的一个,从而避免了二义性的问题。虚继承的格式如下:
class 派生类名 : 虚继承 访问说明符 基类名{
// 派生类的成员声明
};
(虚继承我们后续再详细说明)
我们知道,在c++赋值操作时,在很多场景下都存在这隐式类型转换,如下代码:
int main() {
string& str = "xxxx";
return 0;
}
这里报了错误,原因就是隐式类型转换产生临时变量,而临时变量具有常性,肯定不能直接赋值给引用str,会导致权限的放大,解决方法是用const 修饰string& str以达到对str权限的缩小。
但是在继承这里,我们传统概念上的认知就有点产生崩塌了,看如下代码:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main() {
Student stu;
Person per = stu;
return 0;
}
竟然没有报错?明明Student类赋值给Person类中间肯定会发生隐式类型转换产生临时变量。
但答案是:这中间不产生临时对象!!
我们需要引入新的概念。
在public继承中
,父类和子类是is a
的关系,子类对象赋值给父类对象/父类的指针/父类的引用,都不产生临时对象,这个叫做父子类赋值兼容规则
这里补充一下,父类对象不能赋值给子类对象,会报错。
总结一下:
RTTI
(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下!)若在子类中有和父类同名的成员变量/函数,在子类中访问这个成员变量,到底是访问哪个呢?
答案是我们遵循就近原则,访问子类本身的成员变量/函数。而如果我们想访问父类的同名成员,可以用访问限定符::,
父类名::成员变量/函数去指明访问。
这里总结一下:
隐藏
,重定义
。(在子类成员函数中,可以使用 基类::基类成员 显示访问)在派生类中,如果没有显示定义诸如构造函数、拷贝构造函数、析构函数等的默认成员函数。
派生类会默认调用父类的默认成员函数。
1.有参构造函数初始化基类成员变量
上述代码中,在我们初始化基类的成员_name的时候出现了报错,我们稍微改正一下:
原因是,在c++规定中,在派生类如果想初始化基类的成员变量,必须显示调用父类的构造函数。
也就是说,子类你自己定义的成员变量,你在你的构造函数中想怎么初始化我不管,但如果要初始化我父类的成员变量,
不好意思抱歉,必须用我自己的构造函数。
2.operator = 运算符重载赋值
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 ; // 姓名
};
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);//这里使用访问限定符是因为父子类的operator=构成重定义(也就是隐藏)
//而我们想要的是调用父类的operator =运算符重载,所以用访问限定符
_num = s ._num;
}
return *this ;
}
protected :
int _num ; //学号
3.析构函数析构基类成员变量
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 ; // 姓名
};
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);//这里使用访问限定符是因为父子类的operator=构成重定义(也就是隐藏)
//而我们想要的是调用父类的operator =运算符重载,所以用访问限定符
_num = s ._num;
}
return *this ;
}
~Student()
{
//Person::~Person();
cout<<"~Student()" <<endl;
}
protected :
int _num ; //学号
这里有两个问题:
————————————————————————————————————————————
对于这个小点,我们做个总结:
1.关于模板的编译说法错误的是( A )
A.模板在.h文件中声明,在.cpp里面实现
B.模板程序一般直接在一个文件里面进行定义与实现
C.不久的将来,编译器有望支持export关键字,实现模板分离编译
D.模板不能分离编译,是因为模板程序在编译过程中需要经过两次编译
解析:
A.模板不支持分离编译,所以不能在.h声明,在.cpp实现
B.由于不支持分离编译,模板程序一般只能放在一个文件里实现
C.不支持分离编译并不是语法错误,而是暂时的编译器不支持,不久将来,或许会被支持
D.模板程序被编译两次,这是不能分离编译的原因所在
2.下面代码输出结果:( D)
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.以上说法都不对
解析:
A.错误
B.错误
C.不能通过编译是正确的,不过原因不是因为成员变量a的问题,而是子类同名隐藏了父类方法的原因
D.很显然以上说法都不对
下面说法正确的是(D )
A.派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化
B.派生类构造函数先初始化子类成员,再初始化基类成员
C.派生类析构函数不会自动析构基类部分成员
D.子类构造函数的定义有时需要参考基类构造函数
解析:
A.如果父类有默认构造函数,此时就不需要
B.顺序相反,先初始化父类,再是子类
C.会调用,并且按照构造的相反顺序进行调用
D.是的,需要看父类构造函数是否需要参数子类的,从而你决定子类构造函数的定义
4.关于基类哪些成员被子类继承说法不正确的是( C)
A.静态成员函数
B.所有成员变量
C.基类的友元函数
D.静态成员变量在整个继承体系中只有一份
解析:
A.静态成员函数也可以被继承
B.成员变量所有的都会被继承,无论公有私有
C.友元函数不能被继承,相当于你爹的朋友不一定是你的朋友
D.静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份