十一、模板进阶
5. 模板的优缺点
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
其实总的来说,模板的利还是大于弊的,该用模板的时候还是要用模板。
十二、继承
1. 继承的概念及定义
继承 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(或叫做子类),被拓展的类叫基类(或叫做父类)。
比如说:我们想设计一个学生管理系统,涉及到的对象有学生和老师,分别封装成两个类,但是老师和学生都含有姓名、年龄、性别等等的信息,要是将这些变量都分别放到各个类里,就会有些冗余。而继承机制就是,设计一个类,这个类里有共有的成员变量或成员函数,派生出的子类继承这个父类,就能拥有父类的这些成员变量和成员函数,减少了冗余。
#include<iostream> using namespace std; // 父类,代表人 class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: // 姓名 string _name = "peter"; // 年龄 int _age = 18; }; // 子类,代表学生,继承自 Person class Student : public Person { protected: // 学号 int _stuid; }; // 子类,代表教师,继承自 Person class Teacher : public Person { protected: // 工号 int _jobid; }; int main() { Student s; Teacher t; s.Print(); t.Print(); return 0; }
我们发现,学生类和教师类里既没有 Print() 函数,也没有 name,age 成员变量,但就是能够使用。这就是继承,从父类获得成员变量和成员函数。
我们知道,类访问限定符有三种,public(共有)、protected(保护)、private(私有) 。同样的,继承方式也有三种:public(共有)、protected(保护)、private(私有) 。那么,这两者有什么关系?从父类继承过来的成员在子类中又是什么访问权限?
什么是共有?共有就是,类本身可以访问,类派生后的子类可以访问,类外也可以访问。什么是保护?保护就是,类本身可以访问,类派生后的子类可以访问,类外不能访问。什么是私有?私有就是,类本身可以访问,类派生后的子类不可以访问,类外不可以访问。
权限大小:public > protected > private 。
实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它父类。的其他成员在子类的访问方式是 成员在父类的访问限定符和继承方式的两者权限较小的 。
继承方式可以不写,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好还是显示的写出继承方式。
2. 基类和派生类对象赋值转换
由于子类是父类的拓展,所以,父类有的,子类都有,子类有的,父类不一定有。当子类赋值给父类时:子类将会把与父类相同的部分进行切割,然后赋值给父类 。
然而,父类并不能赋值给子类 。但是 父类的指针可以通过 强制类型转换 来赋值给子类 。
3. 继承中的作用域
在继承体系中父类和子类都有 独立的作用域 。父类和子类的作用域互不相干,因此,父类和子类允许出现同名变量或者同名函数。
#include<iostream> using namespace std; // 父类,代表人 class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: // 姓名 string _name = "peter"; // 年龄 int _age = 18; // 父类增加一个 _num int _num = 0; }; // 子类,代表学生,继承自 Person class Student : public Person { public: void func() { cout << _num << endl; } protected: // 学号 int _stuid; // 子类增加一个 _num int _num = 1; }; // 子类,代表教师,继承自 Person class Teacher : public Person { protected: // 工号 int _jobid; }; int main() { Student s; s.func(); return 0; }
我们发现,此时子类访问的 _num 是子类本身的成员,因此我们可以知道,当父类和子类中出现同名成员时,会访问到子类成员 。这种情况叫做 隐藏 ,也叫做 重定义 。当然,可以通过作用域限定符来指定访问父类。需要注意的是,只要父类和子类的成员函数函数名相同,参数不同,依然构成 隐藏 ,只要同名就构成 隐藏 ,因为不在同一个域,所以不存在重载情况。
4. 派生类的默认成员函数
类有6个默认成员函数,默认 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
- 对于子类的构造函数,我们需要将父类看作一个整体,看作一个独立的类。子类等价于父类 + 子类独有的成员 。而 父类就相当于自定义类型 ,所以子类的构造函数是需要调用父类的默认构造函数的,如果父类没有默认的构造函数,则子类需要在初始化列表显示定义父类的构造函数。
Person(const char* name = "peter") : _name(name) { // } Student(const char* name, int num) : Person(name) // 匿名对象,显示调用父类的构造函数 , _num(num) { // }
- 拷贝构造函数与构造函数一样。内置类型进行值拷贝,自定义类型调用它的拷贝构造函数。
Person(const Person& p) : _name(p._name) { // } Student(const Student& s) : Person(s) // 切片,子类可以赋值给父类 , _num(s ._num) { // }
- 赋值重载同理,不过需要注意同名函数产生的隐藏。
Person& operator=(const Person& p) { if (this != &p) _name = p ._name; return *this ; } Student& operator = (const Student& s ) { if (this != &s) { // 限定作用域,避免发生隐藏 Person::operator =(s); _num = s ._num; } return *this ; }
- 对于析构函数,构造函数要满足先构造父类后构造子类,析构函数需要满足先析构子类后析构父类,子类的析构函数会在被调用完成后 自动调用父类的析构函数 清理父类成员。
~Person() { // } ~Student() { // } //子类析构完成后会自动调用父类的析构函数