在面向对象部分提到过,面向对象三大特性(不是只有三个特性,而是指存在感最强的三个特性):封装,继承,多态。
封装:对比C语言,将数据和处理数据的方法放入一个类中,更好的设计管理数据,比如栈就只能通过接口访问顶端数据。
对于什么是封装,继承,多态,这个问题是没有标准答案的。对面向对象特性的理解是要在学习的过程中不断加深理解,自己总结出规律的。
具体来说封装:
1.C语言设计的栈,和c++设计的stack数据结构:相对于C语言,c++的封装设计更好,具体来说是:访问限定符+类的设计
2.迭代器的设计:如果没有迭代器,容器的访问只能暴露底层结构。这会导致两个问题:容器使用复杂,使用成本很高,对使用者的要求也很高。list要头结点,vector要首元素的地址,单链表要第一个节点的指针。不同的结构需要不同的方式访问。迭代器封装了容器的底层结构,且在不暴露底层结构的情况下,提供了统一的容器访问方式。降低了使用成本,简化了使用。
3.stack/queue/priorty_queue,即适配器模式,也是采用封装的思想,封装了具体的容器。而且比较巧妙的是,因为之前的封装, 使容器采用统一的容器访问方式,所以这里不需要很复杂,就能实现容器的封装。
下面介绍继承
继承的概念:
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
比如我们要通过面向对象来设计一个学校管理系统,那么涉及的对象有:学生,老师,餐饮人员,保洁人员,管理人员等等,在构建这些对象的成员变量的时候,会发现一部分成员变量是所有对象都有的,比如姓名,比如性别,比如身份证号,比如电话。这些重复的部分,就是通过类型的复用手段,继承,来完成复用的。前提是要有公共信息,虽然语法上可以随便继承一个类,但没有意义,比如将生物的类继承给死物,除了占用空间没有任何意义。
继承的使用方式:
class person
{
protected:
string _name;
string _sex;
string _identity_card;
string _tele;
};
class student : public person
//语法
{
private:
string _stuID;
};
其中person是父类,也叫基类;student是子类,也叫派生类。
从图中可以看到继承了person的student和没有继承person的teacher在成员变量上是不一样的。但是student只继承了person的成员变量,没有继承person成员变量的处理方法,即成员函数,因为成员函数不在类中。虽然没有继承,但是还是可以调用的,前提是成员函数访问限定符是public修饰的。
继承关系和访问限定符的关系
继承关系有三种,访问限定符也有三种,两两组合的话有九种关系:
这个其实很好记,首先,基类的private成员在子类中都不可见;其余部分在继承方式和访问限定符之间选择权限小的那个(权限:public>protected>private),public继承和protected访问会变成派生类的protected成员。protected继承和private访问会变成派生类的private成员。protected的意义是,让被修饰的成员只能在派生类中被访问。即用到继承的地方,protected和private之间有区别。
那么什么是不可见?
不可见就是虽然基类(成员变量被private修饰)在派生类中,但是语法限制派生类,派生类不能访问基类中的内容(指成员变量)(派生类外更不能),不管采用什么继承方式。当然,可以用基类中public修饰的函数接口,即使函数接口访问了成员变量。
当初这么设计的主要原因是为了考虑各种各样的情况,但实际情况并不会遇到太多情况。
总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
基类和派生类对象的赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去,有的地方也叫向上转换。赋值的是内置类型就浅拷贝,自定义类型就要调用拷贝构造函数。
基类对象不能赋值给派生类对象。相关指针的内容有点复杂,之后再了解。
如这样的代码,都是可以正常编译的
之前说过,不同类型直接的赋值是会产生临时对象的,但是派生类赋值给基类不会产生临时对象,也就是说,派生类赋值给基类是天然支持的。从引用相关的代码也能看出,如果有临时变量,会无法传给rp,因为临时变量具有常属性,给一个没有const修饰的引用会导致权限放大。
派生类赋值给基类,就是将基类的部分依次赋值过去;而基类引用派生类,则是引用派生类中基类那一部分的别名。指针则代表,指针指向派生类,但是当做基类的类型,即解引用的大小按基类处理。当然,基类是不能赋值给派生类的。
通过rp,pp改变s中基类部分的内容,s中的内容也会改变。
当然派生类天然赋值给基类的前提条件是public继承(基类成员变量被public和protected修饰都可以将派生类赋值给基类),如果是protected继承,继承的基类会变成protected成员,在student类类外是不能访问(读取也算访问)继承的基类部分的,就不能将派生类的基类部分赋值给基类。
换一个思路,如果基类的成员变量是public修饰的,然后被protected继承,会如何?答案是派生类的基类部分不能赋值给基类。原因还是派生类的基类部分不可访问。
继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
示例:
如图所示,基类和派生类中存在相同的变量名_num,在没有指定作用域时,在派生类中访问_num时,基类的_num会被隐藏,也就是说,只会访问到派生类的_num。这点和局部变量和全局变量起冲突时,以局部变量优先相似。
关于成员函数只要函数名相同就构成隐藏:
最后一幅图的A::fun()和B::fun()的关系是什么呢?
答案是隐藏。构成函数重载需要在同一个作用域,因为函数的标识符会包含域的信息。也因为A中的fun被隐藏,b调用的是B中的fun,而B中的fun带参数,所以如果没有指明调用的是A中fun,就必须传参,否则会报错。
派生类中的默认成员函数
派生类中同样存在6个默认成员函数,“默认”的意思就是指我们不写,编译器会自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
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:
protected:
int _num;
//学号
};
int main()
{
Student s1;
return 0;
}
运行这段代码后得到结果如下:
由图中可知,创建一个student对象,调用了一次person的构造函数和析构函数。而student类中,什么都没有定义。
这说明派生类的默认构造函数会去调用基类的构造函数取初始化继承自基类的那部分。
派生类构造函数原则:a.继承自基类的成员变量先调用基类的构造函数初始化。b.自己再初始化自己的成员
也就是说,派生类将成员函数分成两个部分,分别处理,而且先处理基类的成员变量。初始化自己成员的规则就和以前的一样了。即内置类型不处理,自定义类型调用其构造函数。
同样,析构,拷贝构造,赋值重载也遵循这样的规则,即先调用基类的析构函数清理基类的部分,再调用自己的析构函数清理自己的部分;先调用基类的拷贝构造函数拷贝构造基类的部分,再调用自己的拷贝构造函数拷贝构造自己的部分……
这就是为什么创建student对象会调用person类的构造函数和析构函数。
下面在student类中添加public修饰的成员函数:
Student(const char* name = " ", int num = 0)
:_name(name)
,_num(num)
{}
按照之前的方式在初始化列表中初始化成员变量可以吗?不可以,编译器会报错
如果选择不在初始化列表初始化,而是在函数体内赋值可以吗?(在初始化列表叫初始化,在函数体内则叫赋值,可以猜测到函数体内的时候已经完成初始化了)
结果是可以,编译通过。运行时得到结果如下:
猜测正确,在初始化列表中派生类已经调用基类的构造函数完成基类部分的初始化了,所以才会出现调用了一次基类构造函数和析构函数的现象。可以将派生类继承自基类的成员变量当作一个自定义成员变量来理解。比如如果不显式调用,派生类只能调用默认构造函数(指不用传参就可以调用的构造函数);如果基类的构造函数没有实现全缺省(存在构造函数,编译器就不会自动生成构造函数),派生类就会找不到合适的构造函数,从而报错。
c++在这里遵循基类一定要调用基类的构造函数初始化原则。
如果没有默认的构造函数,可以这样调用:
Student(const char* name = " ", int num = 0)
:Person(name)
,_num(num)
{}
通过这样的方式,就能决定_name中存储的内容。当然,在函数体内赋值也可以。Person(name)和_num(num)交换位置也不会影响实际执行的顺序,这点和普通类有相似之处。原因在于这里的顺序不是执行的顺序,而是声明的的顺序。可以认为基类成员一定是在所有派生类成员之前初始化的。
拷贝构造
首先,先看默认调用的基类拷贝构造是什么情况:
同样是调用了基类的拷贝构造函数来完成拷贝构造的。
下面要显式传参,拷贝构造同样不支持在初始化列表中通过_name(s.name)的方式初始化。但是要调用Person的拷贝构造就要将派生类中Person的部分传过去,而派生类中又没有基类的成员名。
其实没那么复杂,派生类可以通过切片直接赋值给基类,直接传派生类对象就行了。
这里是只在拷贝构造函数中进行了基类的拷贝,没进行_num的拷贝,所以num还是随机值。
一般来说,除非派生类的拷贝构造涉及深拷贝,否则默认生成的就足够了。
同样赋值重载也遵循这样的原则,基类部分调用基类的赋值重载,自己的成员变量自己解决。
派生类中operator=和基类中operator=函数名相同,构成隐藏。
与拷贝构造不同的是,赋值重载有两次切片,第一次是s1传到基类中,基类形参的切片;第二次是s3的this指针传到基类中,基类赋值重载的this指针对s3 this指针的切片
下面将Student调用成员函数的过程显示出来,观察关系。
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 = 0)
:_num(num)
,Person(name)
{
cout << "Student(const char* name = " ", int num = 0)" << endl;
}
Student(const Student& s)
//要加引用,否则会无限递归调用
/*:_name(s.name)*///同样不支持这样初始化
:Person(s)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
//需要指明调用Person类中的operator=,否则会调用Student类中的operator=
_num = s._num;
cout << "Student& operator=(const Student& s)" << endl;
}
return *this;
}
~Student()
{
~Person();
cout << "~Student()" << endl;
}
protected:
int _num;
//学号
};
int main()
{
Student s1("zhang san",1);
Student s2(s1);
Student s3;
s3 = s1;
return 0;
}
直接这样编译是会报错的,因为析构函数中的~Person(),将这行屏蔽掉,编译就会通过,运行程序:
结果发现屏蔽掉后Person的析构函数反而能正常使用。而且也析构了三次。那为什么不屏蔽Person()就会报错呢?
这是因为父子类(基类-父类,派生类-子类)的析构函数构成隐藏关系。具体来说是为了满足后一章多态的需求,析构函数名会被统一修改为destructor(),如果要调用,就需要指定类域:Person::~Person();
修改后运行:
发现析构函数多调用了三次(没有崩溃是因为这里的析构函数只是做个样子,没有真的释放空间,如果释放空间就会导致对一个空间释放两次,从而崩溃)。
这是因为c++编译器进行了第二个特殊处理:为保证析构顺序,先派生类后基类,派生类析构函数完成后会自动调用基类析构函数,不需要显示调用。所以这行代码是错误存在的:Person::~Person();
保证先派生类后基类的顺序和对象的存储有关系。栈中的存储对象是先定义的先初始化,先定义的后析构。派生类基类总是先初始化,所以析构函数中,基类要后析构(数据的储存是一个栈的结构)。
一个小问题:如何设计一个不能被继承的类?
这里说的不能被继承是指达不到继承的效果,而不是指在继承的时候报错。只需要将基类的构造函数私有就行,但是私有会导致基类也无法使用,而要让派生类无法使用(指一创建对象就报错)就必须让派生类无法调用基类的构造函数。这就需要一个补丁:一个被public修饰的基类成员函数,在函数体内部调用基类的构造函数,这样调用这个函数就能实现基类的构造。但是调用成员函数需要对象,而对象无法创建(构造函数私有)。所以将这个函数修饰为静态(static修饰,被修饰为静态的没有this指针,声明类域就能使用)
友元和继承
一句话:友元不能继承
基类中声明一个函数为基类的友元函数,代表这个函数可以访问基类的私有成员;但是基类的派生类没有声明基类的友元函数同时也是派生类的友元,那基类的友元函数就不能使用派生类的私有成员。
示例:
class Student;
//声明Student类,否则没法说明Person的友元函数中为什么会有
const
Student
&
s(编译器向上查找)
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;如果不注释会报错,Display没有Student私有成员的访问权限
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
继承当中的静态成员
关于静态成员的一个问题?
基类当中有一个静态成员,那么派生类继承基类后是新有一个静态成员,还是和基类共享一个静态成员呢?
答案是共享一个静态成员。
示例:
class Person
{
public:
Person() { ++_count; }
//不管是student还是graduate最终都会从这里调用构造函数
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;
// 研究科目
};
int main()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
Person s5;
cout << " 人数 :" << Person::_count << endl;
cout << " 人数 :" << Student::_count << endl;
cout << " 人数 :" << Graduate::_count << endl;
//一直是公有继承,所以一直是公有,任何类域都能访问到_count
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
return 0;
}