目录
友元关系不能继承。也就是说基类友元不能访问子类私有和保护成员
继承的概念
继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类的复用。
class Person
{
public:
void Print1()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "未成功先生"; // 姓名
int _age = 18; // 年龄
};
// student类继承了Person类
class student : public Person
{
private:
int _stuid; // 学号
};
int main()
{
Person p;
student s;
s.Print1();
return 0;
}
继承定义
要让一个子类继承父类,需要在子类的类的类名后加上冒号,并跟上继承方式和父类类名即可
继承方式和访问限定符
继承方式没写时(最好是写上),用class定义派生类,默认的继承方式是private,用struct定义派生类,默认的继承方式为public。
继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可 |
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
基类的 private 成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就需要定义为 protected,所以protected 限定符是因继承才出现的
1、public继承:当子类通过public继承,那么,父类中的public成员就是子类中的public成员,父类中的protected成员就是子类中的protected成员。父类的private私有成员可以被继承,但是看不见.
2、protected继承:当子类通过protected继承,那么父类中的public成员和protected成员在子类中都是子类的protected成员。父类的private私有成员可以被继承,但是看不见。
3、private继承:当子类通过private继承,那么父类中的public成员和protected成员在子类中都是子类的private成员。父类的private私有成员可以被继承,但是看不见。
不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
总结
根据继承方式把基类(父类)的成员变成派生类(子类)“对应”(通过下面的方式)访问限定符内。
基类的成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private。
常用的是public。
基类与派生类对象的赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
class Person
{
public:
void Print1()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "未成功先生"; // 姓名
int _age = 18; // 年龄
};
class student : public Person
{
private:
int _stuid; // 学号
};
int main()
{
Person p;
student s;
// 基类和派生类对象赋值转换 ,中间不产生临时变量
p = s;
Person* ptrp = &s;
Person& rp = s;//不用加上const
return 0;
}
可以理解为切片
注意:基类对象不能赋值给派生类对象
但可以用强转的方式
Student s;
Person p;
Person* pp;
pp = &p;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
继承类中的作用域
显式访问
在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义,想要访问基类的同名成员就需要指明作用域, 基类::基类成员 显式访问
//B中的fun和A中的fun不是构成重载,因为不是在同一作用域
//B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun(); // 调用父类的
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(1);
//func()
//func(int i)->1
};
int main()
{
Test();
return 0;
};
隐藏函数
//B中的func和A中的func不是构成重载,因为不是在同一作用域
//B中的func和A中的func构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int i)
{
A::func(); // 调用父类的
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.func(1);
//func()
//func(int i)->1
b.A::func();// 显式调用基类的成员
return 0;
};
注意:只需要名相同就构成隐藏,所以在实际中在继承中里面最好不要定义同名的成员
派生类的默认成员函数
派生类当中的默认成员函数,与普通类的默认成员函数的不同之处:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
- 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
- 派生类对象初始化先调用基类构造再调派生类构造
- 派生类对象析构清理先调用派生类析构再调基类的析构
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
构造函数
class Person
{
public:
Person(const char* name = "Peter")
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
// Student 继承 Person
class Student : public Person
{
public:
Student(const char* name, int num = 18)//构造函数
:Person(name)
, _num(num)
{
//Person(name); // 不能这样写,运行时会报 name 的重定义
//_num = num;
cout << "Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student a("张三");
return 0;
}
拷贝构造函数
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 = 18)//构造函数
:Person(name)
, _num(num)
{
//Person(name); // 不能这样写,运行时会报 name 的重定义
//_num = num;
cout << "Student()" << endl;
}
Student(const Student& s)//拷贝构造
:Person(s)
, _num(s._num)
{
//Person(s);
//Person::operator=(s);//注意是Person,这样也可以
//_num = s._num;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
//Person(s); // 不能用这个,会报Person没有_num这个成员的错误,只能用下面这句
Person::operator=(s);//注意是Person,
_num = s._num;
}
return *this;
}
void print()
{
cout << _name<<endl;
}
protected:
int _num; //学号
};
int main()
{
Student a("张三");
Student a1(a);
a1.print();
return 0;
}
析构函数
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 = 18)//构造函数
:Person(name)
, _num(num)
{
//Person(name); // 不能这样写,运行时会报 name 的重定义
//_num = num;
cout << "Student()" << endl;
}
Student(const Student& s)//拷贝构造
:Person(s)
, _num(s._num)
{
//Person(s);
//Person::operator=(s);//注意是Person,这样也可以
//_num = s._num;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
//Person(s); // 不能用这个,会报Person没有_num这个成员的错误,只能用下面这句
Person::operator=(s);//注意是Person
_num = s._num;
}
return *this;
}
~Student()//析构 // 派生类的析构也会与基类构成隐藏
{
//Person::~Person();//不需要这样调用,会自动调用,先自动调用派生类(子类)之后调用基类(父类)的
cout << "~Student()" << endl;
}
void print()
{
cout << _name<<endl;
}
protected:
int _num; //学号
};
int main()
{
Student a("张三");
Student a1(a);
a1.print();
return 0;
}
子类析构函数和父类析构函数构成隐藏关系。(由于多态需求关系,所有析构函数都会特殊处理成destructor
的函数名,这样由于函数名相同就会发生隐藏)
不必在派生类的析构函数中调用基类的析构,而是会自动调用。
顺序:
构造: 基类先构造,派生类后构造。
析构: 派生类先析构,基类后析构。
友元关系不能继承。也就是说基类友元不能访问子类私有和保护成员
继承与静态成员
class Person
{
public:
Person() { ++_count; }
void Print()
{
cout << "Print" << endl;
}
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()
{
/*Person p;
Student s;
Graduate g;
p._name = "张三";
s._name = "李四";
p._count++;
s._count++;
cout << p._count << endl;
cout << s._count << endl;
cout << &p._count << endl;
cout << &s._count << endl;
cout << Person::_count << endl;
cout << Student::_count << endl;
cout << Graduate::_count << endl;*/
//空指针也可以调用静态成员
Person* ptr1 = nullptr;
Graduate* ptr2 = nullptr;
ptr1->_count++;
//cout << ptr->_name << endl;//no
ptr1->Print();//yes
cout << ptr1->_count << endl;//yes
cout << ptr2->_count << endl;//yes
(*ptr1).Print();
cout << (*ptr1)._count << endl;
return 0;
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
棱形继承与虚拟继承
单继承
多继承
Java没有多继承
棱形继承
菱形继承的继承方式存在数据冗余和二义性的问题。
// 多继承的影响
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher // 棱形继承
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
//a._name; // 会报不明确的错
//指定作用域就行了
a.Student::_name = "张三";
a.Teacher::_name = "张老师";
cout << &a.Student::_name << endl; // 地址不一样
cout << &a.Teacher::_name << endl;
return 0;
}
但是数据冗余没有解决
虚拟继承
虚拟继承在继承方式前加 virtual 关键字即可
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
// virtual 关键字
// 虚继承
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
a._name = "小张"; // 编译通过
a.Student::_name = "张三";
a.Teacher::_name = "张老师"; // 改一个其他的也跟着变
cout << &a._name << endl;
cout << &a.Student::_name << endl;
cout << &a.Teacher::_name << endl; // 同一个地址
cout << a._name << endl;
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
return 0;
}
虚拟继承的原理
没加 virtual 的情况
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl; // 20
// 调试,用内存监控看 &d
d.B::_a = 4;
d.C::_a = 5;
d._b = 1;
d._c = 2;
d._d = 3;
B b;
return 0;
}
调试:
相当于
加 virtual 的情况,虚拟继承
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl; // 24
// 调试,用内存监控看 &d
d.B::_a = 4;
d.C::_a = 5;
d._b = 1;
d._c = 2;
d._d = 3;
d._a = 6;
return 0;
}
调试:
其中D类对象当中的 _a 成员被放到了最后,而在原来存放两个 _a 成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。虚基表中存的偏移量,通过偏移量可以找到下面的A
虚基表中包含两个数据,第一个数据(全为0的)是为多态的虚表预留的存偏移量的位置(暂时不理会),第二个数据就是当前类对象位置距离公共虚基类的偏移量(由于VS是小端,地址需要成对的倒着读,比如 14 00 00 00 ,读的话就是0x00 00 00 14,十进制就是20)
总结
一般不建议设计出多继承,一定不要设计出菱形继承,否则在复杂度及性能上都有问题。
继承与组合
public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象
组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象
优先使用对象组合,而不是类继承
//继承
class X
{
int _x;
};
class Y :public X
{
int _y;
};
//组合
class M
{
int _m;
};
class N
{
M _mm;//组合
int _n;
};
什么是菱形继承?菱形继承的问题是什么?
什么是菱形虚拟继承?如何解决数据冗余和二义性的
继承和组合的区别?什么时候用继承?什么时候用组合?
1、多继承中的一种特殊继承,即一个类可能被另一个类以不同的作用域继承多次。菱形继承会导致代码的二义性及空间浪费。
2、通过virtual变成虚继承。解决冗余与二义性则通过偏移量的访问方式。
3、继承与组合就像白箱与黑箱,对于非public,一个一览无余,一个恰好相反。因此,在需要派生类和基类间的依赖关系强时,就用继承。相反就用组合。如果都可以,就优先使用组合。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。