0721-----C++Primer听课笔记----------继承
1. 通过子类对象来调用函数:
a)从父类继承而来的函数,可以正常使用;
b)子类自行添加的函数,可以正常使用;
c)子类编写和父类重名的函数,不管是否改变了参数,调用的都是子类的版本,如果需要调用父类的版本,需要显示指定父类名。
例1:
#include <iostream> #include <string> using namespace std; /* * 派生类对象可以正常的调用从基类继承的函数 */ class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age){} ~Person(){} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } private: int id_; string name_; int age_; }; class Student : public Person{ private: string school_; }; int main(int argc, const char *argv[]) { Student s; s.set(123, "monica", 22); s.print(); return 0; }
例2:
#include <iostream> #include <string> using namespace std; /* * 派生类对象可以正常调用 派生类自己编写的函数 */ class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age){} ~Person(){} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } private: int id_; string name_; int age_; }; class Student : public Person{ public: // 这是派生类自己的函数 void set_school(const string &school){ school_= school; } void print_school()const { cout << school_ << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s; s.set_school("hust"); s.print_school(); return 0; }
例子3:
#include <iostream> #include <string> using namespace std; class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age){} ~Person(){} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } protected: //protected 允许派生类成员访问 int id_; string name_; int age_; }; class Student : public Person{ public: void set_school(const string &school){ school_= school; } void print_school()const { cout << school_ << endl; } void set(int id, const string &name, int age, const string &school){ id_ = id; name_ = name; age_ = age; school_ = school; } void print() const{ cout << id_ << " " << name_ << " " << age_ << " " << school_ << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s; s.set(123, "monica", 22, "hust"); //这里调用的是派生类重写的函数 s.print(); s.Person::print(); //需要显示调用父类的函数 return 0; }
2. 子类重定义父类的函数,不管有没有改变参数, 当使用子类对象去调用这些函数的时候,调用的总是子类自定义的版本, 我们称为子类隐藏了父类的函数。
#include <iostream> #include <string> using namespace std; class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age){} ~Person(){} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } protected: //protected 允许派生类成员访问 int id_; string name_; int age_; }; class Student : public Person{ public: void set_school(const string &school){ school_= school; } void print_school()const { cout << school_ << endl; } void set(int id, const string &name, int age, const string &school){ id_ = id; name_ = name; age_ = age; school_ = school; } void print() const{ cout << id_ << " " << name_ << " " << age_ << " " << school_ << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s; s.set(123, "monica", 22, "hust"); //这里调用的是派生类重写的函数 s.print(); Student s2; s2.Person::set(111, "jobs", 55); //要调用父类的函数必须显式的指出 s2.Person::print(); return 0; }
3. 关于继承的一些要点:
3.1 面向对象的第二个特征是继承(inherit);
3.2 在类的继承体系中,父类扮演的是一种更加泛化、抽象的角色,子类扮演的则是一种更加具体的角色;
3.3 在继承中,父类还有的数据成员,子类全部都有(这里要注意:private 也被子类继承,只是无法直接访问)。
3.4 三种访问权限:
a)private 成员只能在类的内部被访问;
b)protected 成员在家族体系中是可见的;
c)public 成员在任意地方都是可见的。
4. 子类对象的构造
4.1 子类对象在构造的时候,会先生成一个父类的无名对象,所以子类对象内部包含一个父类对象,因此把子类对象赋值给父类对象是合法的,子类赋值的过程中,子类对象多余的部分被丢弃,这个赋值过程称为 对象的切除(这里要注意,C++类的对象, 只含有普通的数据成员,不包括函数)。
int main(int argc, const char *argv[]) { Student s; s.set(123, "monica", 22, "hust"); s.print(); Person p; p = s; // 这里将子类对象赋值给父类对象 p.print(); return 0; }
4.2 类对象的构造顺序:
a) 先构造父类;
b) 构造成员对象;
c) 调用自己的构造函数;
d) 析构顺序与之相反。
5. 派生类对象的拷贝
5.1 对于父类的private 成员,子类的多参数构造函数如何去初始化呢?这里,在构造函数初始化列表中,可以直接调用父类的构造函数去初始化。
#include <iostream> #include <string> using namespace std; /* *派生类构造函数初始化列表可以直接调用 *基类的构造函数 用来初始化基类的私有成员 */ class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age){} ~Person(){} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } private: int id_; string name_; int age_; }; class Student : public Person{ public: Student(int id, const string &name, int age, const string &school) :Person(id, name, age), school_(school){ } void print() const{ Person::print(); cout << school_ << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s(123, "monica", 22, "hust"); s.print(); return 0; }
5.2 派生类的默认构造函数和赋值运算符如何实现的。这里,通过调用基类的拷贝构造函数和赋值运算符来完成派生类对象的复制和赋值工作(要特别注意成员函数隐含了this指针,因此在传递参数时,不用特别声明)。
Student(const Student &other) //派生类的拷贝构造函数 :Person(other), school_(other.school_){} Student &operator=(const Student &other){ if(this != &other){ Person::operator=(other); school_ = other.school_; } return *this; }
5.3 禁止复制类的继承,对于对个禁止复制类,每次都要讲拷贝构造函数和赋值运算符设为private,这就造成了大量的代码重复。这里,采用一个noncopyable类,该类将上述两个还是设为私有,并且只提供声明,任何禁止复制的类只要继承该类即可,注意 这里是私有继承(why?)。 一个派生类对象拥有父类的对象成员和自己的成员,因此子类对象在拷贝的时候也需要拷贝父类的成员,而我们在设计父类的时候已经将父类设置为禁止拷贝,因此子类对象就不能完成拷贝工作。
#include <iostream> using namespace std; class Noncopyable{ public: Noncopyable(){}; ~Noncopyable(){}; private: Noncopyable(const Noncopyable &other); Noncopyable &operator=(const Noncopyable &other); }; // 为什么这里是私有继承 class Test : private Noncopyable{ }; int main(int argc, const char *argv[]) { Test t; Test t2(t); //这里出错 Test t3 = t; return 0; }
6.基类和派生类的相互转化
6.1 基类的指针可以指向派生类的对象。但是使用该指针调用函数时,该指针把自己指向的派生类对象,当做基类对象。这里可以这样理解,父类的指针指向子类对象时,指针的值是子类对象的首地址,但是该指针所能感知的大小空间只是父类对象的大小的空间(在内存空间中,先存储父类的数据成员,再存储子类的其他数据成员),因而父类指针不能感知子类对象中的其他成员的存在。
6.2 用基类指针指向派生类对象,当使用该指针调用函数时:
a) 基类中有,派生类继承过去的函数:调用的是基类版本
b) 基类中没有,派生类自行添加的函数:无法调用。
c) 派生类重写了基类的同名函数,此时通过基类指针调用的仍是基类的版本。
#include <iostream> #include <string> #include <vector> using namespace std; /* * 基类的指针可以指向派生类的对象 * 并且该指针可以调用从父类继承的函数 * 对于派生类重写的函数 该指针调用的是基类的版本 * 该指针无法调用派生类自己的函数 因为它把自己当做基类的指针 */ class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age) {} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } protected: int id_; string name_; int age_; }; class Student:public Person{ public: Student(int id, const string &name, int age, const string school) :Person(id, name, age), school_(school) {} void set(int id, const string &name, int age, const string &school){ id_ = id; name_ = name; age_ = age; school_ = school; } void print()const{ cout << id_ << " " << name_ << " " << age_ << " " << school_ << endl; } void test(){ cout << "base pointer can't call derived own func" << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s(123, "zhangsan", 22, "wangdao"); Person *pt; pt = &s; //该基类指针所感知的空间是父类对象大小的空间 pt->print(); //对于派生类重写的函数。该指针调用的都是基类的版本 //pt->test(); //父类指针无法调用 派生类自己的函数 return 0; } ubuntu%
6.3 子类对象指针转换为父类对象指针时,是自动转换的,此时,指针所指向的内存空间缩 小,但是这片区域是存在的,且是一个父类对象空间,因而是没问题的。父类对象指针转 换为子类对象指针时,在语法上,必须要使用强制转换,此时,指针所指向的内存空间扩 大,但是后面扩大的区域可能不存在,或者是存储的其他数据,这导致不安全问题发生。
#include <iostream> #include <string> #include <vector> using namespace std; class Person{ public: Person(){} Person(int id, const string &name, int age) :id_(id), name_(name), age_(age) {} void set(int id, const string &name, int age){ id_ = id; name_ = name; age_ = age; } void print() const{ cout << id_ << " " << name_ << " " << age_ << endl; } protected: int id_; string name_; int age_; }; class Student:public Person{ public: Student(int id, const string &name, int age, const string school) :Person(id, name, age), school_(school) {} void set(int id, const string &name, int age, const string &school){ id_ = id; name_ = name; age_ = age; school_ = school; } void print()const{ cout << id_ << " " << name_ << " " << age_ << " " << school_ << endl; } void test(){ cout << "base pointer can't call derived own func" << endl; } private: string school_; }; int main(int argc, const char *argv[]) { Student s(123, "zhangsan", 22, "wangdao"); Person *pt; Student *ps; ps = &s; pt = ps; // 派生类指针可以转化为基类指针 Person p1; pt = &p1; ps = (Student *)pt; //将基类的指针转换为派生类的指针 需要类型转换 //但是这虽然在语法上可行,逻辑上是错误的 return 0; }
6.4 继承体系中有这样一个结论:派生类is基类。
a) 从下到上转化(派生类的指针转化为基类指针)没有任何问题;
b) 从上到下,需要程序员手工保证逻辑上的正确性。
6.5 继承体系中:
a) 从上到下是一个具体化的过程,这个过程有无数个选择,人可以转化成学生、工人、教师多种型别。
b) 从下到上是一个抽象、泛化的过程,这里只存在一种选择。
6.6 从上到下的转化称为向下塑形,本质上是不安全的,需要谨慎使用。
7.静态绑定和动态绑定
7.1 以函数重载为例,编译器在编译期间就可以确定调用重载的哪个版本,也就是将函数调用与实际代码实现绑定。这个过程是在编译期间完成的,所以称为静态绑定(static binding),或者早绑定。
#include <iostream> using namespace std; /* * 静态绑定 * */ class Animal{ public: void display(){ cout << "Animal..." << endl; } }; class Cat : public Animal{ public: void display(){ cout << "Cat..." << endl; } }; class Dog : public Animal{ public: void display(){ cout << "Dog ..." << endl; } }; int main(int argc, const char *argv[]) { Cat c; c.display(); // Cat... Dog d; d.display(); // Dog... return 0; }
7.2 满足动态绑定的条件:
a) 用基类的指针或者引用指向派生类对象;
b) 通过该指针或者引用调用的函数必须是virtual函数。
例1 :满足动态绑定的条件a)
#include <iostream> using namespace std; /* * 动态绑定的条件 * 这里的基类指针把所指向的派生类对象当做基类对象 * 因此调用的函数还是基类的版本 没有实现动态绑定 */ class Animal{ public: void display(){ cout << "Animal..." << endl; } }; class Cat : public Animal{ public: void display(){ cout << "Cat..." << endl; } }; class Dog : public Animal{ public: void display(){ cout << "Dog ..." << endl; } }; int main(int argc, const char *argv[]) { Animal *pa; Cat c; pa = &c; pa->display(); //Animal... Dog d; pa = &d; pa->display();//Animal... return 0; }
例2. 满足动态绑定的两个条件,这里,当编译器检测到pa->display();时,发现:
a) pa为Animal类型指针;
b) display为virtual函数。
于是编译器,停止绑定,而是等待到运行期,根据pa所实际指向的对象,来确定调用哪个函数。
#include <iostream> using namespace std; /* * 满足动态绑定的两个条件 */ class Animal{ public: virtual void display(){ cout << "Animal..." << endl; } }; class Cat : public Animal{ public: void display(){ cout << "Cat..." << endl; } }; class Dog : public Animal{ public: void display(){ cout << "Dog ..." << endl; } }; int main(int argc, const char *argv[]) { Animal *pa; Cat c; pa = &c; pa->display(); //Cat... Dog d; pa = &d; pa->display();//Dog... return 0; }
7.3 动态绑定就是:编译器在编译期间并不确定到底调用哪个函数,而是把函数调用和实际代码绑定的过程推迟到运行期间。
8.虚函数
8.1 使用virtual修饰的函数称为虚函数。虚函数具有继承性,只要子类的同名函数参数与基类相同,返回值兼容,那么子类的该函数也是虚函数。
8.2 虚函数与普通函数的区别:
a) 普通函数总是采用静态绑定,根据指针类型来确定调用哪个函数。
b) 虚函数在触发动态绑定的情况下,根据指针所指向的实际对象类型,来决定调用哪个函数。
8.3 下列情形:
a) 基类中有display虚函数,派生类中也有display,参数与display相同,返回值兼容,此时派生类的display也是虚函数,那么我们称派生类的display覆盖了基类的display。
b) 基类中display,而且是virtual函数,但是派生类中的display不满足虚函数继承的条件(参数改变或者返回值不兼容),此时派生类中的display不是虚函数。
c) 基类中的display不是虚函数,派生类中display此时不必关心形参和返回值得问题。
d) 我们把后两者称为派生类函数隐藏了基类的display函数。
8.4 只需要记住动态绑定的情形,也就是函数覆盖的情形,其他所有的均为函数的隐藏。
8.5 函数覆盖:指的是派生类的virtual函数覆盖了基类的virtual函数。通过基类指针调用virtual函数来体现。
函数隐藏:通过派生类对象调用自身改写的与基类同名的函数,此时调用的是自己改写的版本,所以称为隐藏。
8.6 函数重载称为一种编译期的多态,动态绑定称为运行时的多态,但是面向对象所指的多态(polymorphism),只包括动态绑定。
8.7 面向对象的第三个特征是动态绑定(Dynamic Binding)。