继承的概念及定义
继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承的定义
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
继承基类成员访问方式的变化
类成员/继承方式 public继承 protected继承 private继承 基类的public成员 派生类的public 成员 派生类的protected 成员 派生类的private 成员 基类的protected 成员 派生类的protected 成员 派生类的protected 成员 派生类的private 成员 基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可
总结:
- 对于基类成员的继承的理解,可以把基类的成员分为私有成员和其他成员
- 对于基类的私有成员,无论任何继承方式,到子类中都是不可见的(不可见:无论类域内还是在类域外都无法访问,但它确实存在派生类的成员中)
- 对于基类的公有成员和保护成员的继承规则,是取成员和继承方式的较小值,(比如:公有成员保护继承,在派生类中这个成员就是保护成员)(权限:public > protected < private)
- protected成员在父类中与private成员没有区别,但是protected成员继承到子类中,可以对它进行访问,而private成员继承到子类中,是不可见的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中尽量使用公有继承
基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象,基类的指针,基类的引用。(我们可以把这个过程讲作切割,就是把子类中父类那部分切来赋值过去)
- 基类对象不能赋给派生类对象
- 把派生类对象赋值给基类对象的过程,是语法天然支持的,没有产生中间变量
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
/ 1.子类对象可以赋值给父类对象/指针/引用(赋值兼容的转换)
Person pobj = sobj; /把子类中父类那部分赋值给子类
Person* pp = &sobj; /pp是子类中父类那一部分的地址
Person& rp = sobj; /rp是对子类中父类那一部分的别名
/2.基类对象不能赋值给派生类对象
sobj = pobj;
}
继承中的作用域
- 在继承体系中基类和派生类都有自己独立的作用域,在子类中的会有从父类继承来的成员,这部分成员属于父类的作用域,其余部分成员属于子类的作用域
- 子类和父类都有独立的作用域,所以在子类中会出现同名成员,这时,子类会把父类的同名成员隐藏起来,直接访问的是子类的同名成员,但是可以使用基类 ::基类成员 显示访问
- 如果是成员函数的隐藏,只需要函数名相同就构成重载(符号表中的函数名的修饰规则有域名)
- 注意在实际中在继承体系里面最好不要定义同名的成员
/ Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
/ 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(10);
};
派生类的默认成员函数
子类构造函数原则:
调用父类构造函数初始化继承自父类那部分成员变量
再初始化子类自己的成员变量
如果父类有默认构造函数,那么子类的构造函数中就不需要显示写出,初始化列表会自动调用
如果父类没有默认构造函数,那么就需要在子类的构造函数的初始化列表显示调用
析构、拷贝构造、复制重载也类似
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(因为在栈区内,先构造父类,再创建子类,析构时,需要先析构子类再析构父类)
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 父类和子类的析构函数会构成隐藏,因为编译器会对析构函数处理成
destrutor()
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)
: Person(name )
, _num(num )
{
cout<<"Student()" <<endl;
}
Student(const Student& s)
: Person(s)
, _num(s ,_num)
{
cout<<"Student(const Student& s)" <<endl ;
}
Student& operator = (const Student& s )
{
cout<<"Student& operator= (const Student& s)"<< endl;
if (this != &s)
{
Person::operator =(s);
_num = s ._num;
}
return *this ;
}
~Student()
{
cout<<"~Student()" <<endl;
}
protected :
int _num ; //学号
};
如何设计一个不能被继承得类
父类A的构造函数私有化后,b就无法构造函数(但是同时A类也无法调用构造函数创建对象)
解决方法:
设置一个静态函数,在类内调用构造函数
class A { public: static A CreateObj() { return A(); } private: A() {} }; / 父类A的构造函数私有化以后,B就无法构造对象 class B : public A { }; int main() { B b; A a = A::CreateObj(); return 0; }
友元不能继承
静态成员共用
菱形继承
多继承会导致菱形继承,出现二义性和数据冗余的问题
为了解决这个问题,采用虚继承(在菱形继承腰部使用虚继承)
没有采用虚继承
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;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
采用虚继承
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;
d._a = 0;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
采用虚继承,A为虚基类,在B类中,_a的位置存放一个虚基表指针,这个指针指向一个空间,叫做虚基表,存放一个偏移量,偏移量就是原先存放 _a的位置距离真正存放 _a的位置的距离。C类与B类类似
使用D类对象访问可以直接访问,而B类和C类对象需要通过虚基表指针,获取偏移量,再访问。因为_a在D类空间内,在B类C类空间外。
继承和组合的区别
继承的封装性没有组合好,组合是只能使用它的公有成员,不能访问私有,保护成员,而(公有)继承是可以访问它的所有(私有,保护,公有)成员。
相比之下,组合的封装性更好,建议多使用组合
//组合 class A { }; class B { private: A a; };
//继承 class A { }; class B:public A { };