1.继承的概念和定义
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
class paper
{
public:
void Print()
{std::cout<<"thickness:"<<thickness_<<
"long:"<<long_<<
"wide:"<<wide_<<std::endl;}
protected:
int thickness_;
int long_;
int wide_;
};
//这里book类通过继承paper来对Print、thickness_、long_、wide_复用
//可以通过调试窗口来查看函数和变量的复用
class book : public paper
{
public:
int number_;
int author_;
};
int main()
{
paper s;
book t;
s.Print();
t.Print();
return 0;
}
这里是通过调试得到的截图
1.2 继承的定义
1.2.1 定义的格式
以1.1来说 paper为父类也叫基类 book是子类也叫派生类
paper前的public为继承方式
class book : public paper
{
public:
int number_;
int author_;
};
1.2.2 继承后基类成员访问方式的变化
一句话概况就是那个范围小访问方式就是那个,比如以public方式继承protected成员或方法那么继承下来的成员或方法的访问方式就是protected
其中范围关系为public>protected>private。private成员或者继承后访问方式为private的成员,不论在类内还是类外都访问不了
struct默认的继承方式是public,class默认的继承方式是private
2.基类和派生类对象赋值转化
派生类对象可以赋值给基类的对象、基类的指针、基类的引用。基类的对象不能赋值给派生类对象。
这里有一个很形象的说法叫切片或者叫分割,简单来说就是在派生类中将父类的成员和方法分割出来给子类,而为什么不支持基类对象赋值给派生类对象(通过一些办法可以,本文不讲)也就很好理解了,子类的一些成员和方法是父类没有的,所以自然也就不支持了
class paper
{
public:
void Print()
{
std::cout << "thickness:" << thickness_ <<
"long:" << long_ <<
"wide:" << wide_ << std::endl;
}
protected:
int thickness_;
int long_;
int wide_;
};
class book : public paper
{
public:
int number_;
int author_;
};
int main()
{
// 1.子类对象可以赋值给父类对象/指针/引用
book sobj;
paper pobj = sobj;
paper* p = &sobj;
paper& pp = sobj;
//这可能越界访问
p = &pobj;
book* ps1 = (book*)p;
//这没有问题
p = &sobj;
book* ps2 = (book*)p;
return 0;
}
这里再讲一下为什么ps1不行ps2可以,这里仅从个人角度来将。不论是&pobj还是&sobj本质上还是指针,赋值给p以后指针并未改变,通过(book*)强转后原来是book*的自然没问题,但是原来是paper*的如果访问book内的成员就会越界。
3.继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域
2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义,在子类成员函数内可以通过基类::基类成员 显示访问
3.成员函数的隐藏,只需要函数名相同就构成隐藏
class paper
{
public:
void Print()
{
std::cout << "thickness:" << thickness_ <<
"long:" << long_ <<
"wide:" << wide_ << std::endl;
}
protected:
int thickness_ = 100;
int long_;
int wide_;
};
class book : public paper
{
public:
void Print()
{
std::cout << "thickness:" << thickness_ << std::endl;
std::cout <<"paper thickness:" << paper::thickness_ << std::endl;
}
public:
int number_;
int author_;
int thickness_ = 200;
};
int main()
{
book s;
s.Print();
return 0;
}
运行截图
你们可以在派生类book的Print方法里在这样paper::Print()调用一下父类的方法
4.派生类的默认成员函数
默认的意思是我们不写,编译器会自动帮我们生成一个
1.派生类的构造函数必须调用基类的构造函数初始化基类的那部分成员,如果基类没有默认的构造函数,则必须在派生类的构造函数的初始化列表阶段显示调用
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
上述7点可以简要记为我的成员必须我来初始化、拷贝、赋值,先有父再有子,子先析父再析
对于第6点这里讲解一下,子类中的某些成员需要通过父类的成员来初始化,如果先析构父类成员,可能对子类成员有影响,所以先析构子类
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
std::cout << "Person()" << std::endl;
}
Person(const Person& p)
: _name(p._name)
{
std::cout << "Person(const Person& p)" << std::endl;
}
Person& operator=(const Person& p)
{
std::cout << "Person operator=(const Person& p)" << std::endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
protected:
std::string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
std::cout << "Student()" << std::endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
std::cout << "Student(const Student& s)" << std::endl;
}
Student& operator = (const Student& s)
{
std::cout << "Student& operator= (const Student& s)" << std::endl;
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
std::cout << "~Student()" << std::endl;
}
protected:
int _num; //学号
};
void Test()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
这里Student的里的Person(s)能成功的原因是单参数构造函数支持隐式类型转换
5.继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
通俗的理解就是父亲的朋友不是我的朋友
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
std::string name_ = "zkj";
};
class Student : public Person
{
protected:
int stuNum_ = 2;
};
void Display(const Person& p, const Student& s)
{
std::cout << p.name_ << std::endl;
//std::cout << s.stuNum_ << std::endl;//无法访问
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
读者可以将我注释的代码放开来看是否跑的通,从而验证友元不能继承
6.继承和静态函数
基类定义了一个static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
class Person
{
public:
Person() { ++_count; }
protected:
std::string _name;
public:
static int _count;
};
int Person::_count = 0; //静态成员函数在类外初始化
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
std::string _seminarCourse;
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
std::cout << " 人数 :" << Person::_count << std::endl;
Student::_count = 0;
std::cout << " 人数 :" << Person::_count << std::endl;
}
int main()
{
TestPerson();
return 0;
}
这个代码的意思是通过_count的值来证明整个继承体系同一个静态成员只有一个,代码逻辑是在对类Student实例化是调用父类的构造函数,从而是_count加1如果实例化的每个对象,都有自己的_count那么打印出来的第一个_count应该是0,你们还可以cp代码然后打印一下s1、s2、s3里的_count。
7.复杂的菱形继承以及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
class Person {};
class Student :public Person {};
class PostGraduate :public Student {};
这就是一个简单的单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
class Person {};
class Student :public Person {};
class Teacher :public Person {};
这是一个简单的多继承
菱形继承:菱形继承时多继承的一种特殊情况
菱形继承的问题:数据冗余和二义性的问题
class Person {
public:
Person(std::string& name, int ID, std::string& gen)
:_name(name), ID_(ID), gender(gen)
{}
public:
std::string _name;
int ID_;
std::string gender;
};
std::string a = "zkj";
std::string b = "hyt";
class Student :public Person {
public:
Student(int ID)
:Person::Person(a, 1314, b), Student_ID(ID)
{}
public:
int Student_ID;
};
class Teacher :public Person {
public:
Teacher(int ID)
:Person::Person(a, 1314, b), Teacher_ID(ID)
{}
public:
int Teacher_ID;
};
class Assistant :public Student, public Teacher
{
public:
Assistant()
:Student::Student(123), Teacher::Teacher(456)
{}
};
int main()
{
Assistant c;
//std::cout << c.:_name << std::endl;
std::cout<<c.Student::_name << std::endl;
std::cout << c.Teacher::_name << std::endl;
return 0;
}
通过这份代码进行调试可以看到数据冗余和二义性的问题,我们可以通过指定域来明确访问变量但是这样也只是解决了数据二义性的问题,数据冗余的问题还是没有解决。
为了解决数据冗余的问题引入了虚拟继承这个东西
class Person {
public:
std::string _name;
int ID_;
std::string gender;
};
class Student :virtual public Person {
public:
int Student_ID;
};
class Teacher :virtual public Person {
public:
int Teacher_ID;
};
class Assistant :public Student, public Teacher
{};
int main()
{
Assistant c;
//std::cout << c.:_name << std::endl;
c._name = "zkj";
std::cout << c.Student::_name << std::endl;
std::cout << c.Teacher::_name << std::endl;
return 0;
}
本质其实是引入了虚基表和虚基表指针,虚基表中存的偏移量,通过偏移量来找到_name。