继承
【概念与理解】:继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的东西。
例:实现一个简单的继承关系
显然这里在子类继承父类时提到了继承关系,所以在这里将三种继承关系分析一下:
小小的总结一下:
1. 基类的私有成员在派生类中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中能访问,就定义为保
护成员。可以看出保护成员限定符是因继承才出现的。
2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3. protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a 的关系原则,所以非特殊情
况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能
访问)。
5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
强调:仅对public继承–继承与转换–赋值兼容规则
1. 子类对象可以赋值给父类对象(切割/切片)
2. 父类对象不能赋值给子类对象
3. 父类的指针/引用可以指向子类对象
4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
举实例说明public赋值兼容规则
class Person
{
public:
void Display()
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; // 学号
};
void Test()
{
Person p;
Student s;
// 1.子类对象可以赋值给父类对象(切割/切片)
p = s;
// 2.父类对象不能赋值给子类对象
//s = p;
// 3.父类的指针/引用可以指向子类对象
Person* p1 = &s;
Person&r1 = s;
// 4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)&p;
Student&r2 = (Student&)p;
// 这里会发生什么?
//p2->_num = 10;
//r2._num = 20;
//上面两句代码会使程序挂掉;将父类对象p通过强转成子类,再赋给子类对象时,只能访问到子类对象中父类的成员,而不能够访问子类中的成员,因为父类对象中没有子类的_num,所以这里程序会崩掉,第二句用引用道理与第一句指针强转相同。
}
int main()
{
Test();
system("pause");
return 0;
}
继承体系中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用基类::基类成员访问)–隐藏
–重定义
3. 注意在实际中在继承体系里面最好不要定义同名的成员。
举实例说明:
class Person
{
public:
Person(const char* name = "", int id = 0)
: _name(name)
, _num(id)
{}
protected:
string _name; // 姓名
int _num; // 身份证号
};
class Student : public Person
{
public:
Student(const char* name, int id, int stuNum)
: Person(name, id)
, _num(stuNum)
{}
void DisplayNum()
{
cout << " 身份证号:" << Person::_num << endl;//当这里需要访问父类中的成员时就需要指定作用域
cout << " 学号" << _num << endl;//在自己类中就直接访问
}
protected:
int _num; // 学号
};
void Test()
{
Student s1("paul", 110, 1);
s1.DisplayNum();
};
int main()
{
Test();
system("pause");
return 0;
}
派生类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义这六个成员函数,编译系统则会默认合成这六个默认的成员函数。
1、构造函数
2、拷贝构造函数
3、析构函数
4、赋值操作符重载
5、取地址操作符重载
6、const修饰的取地址操作符重载
class Person
{
public:
Person(const char* name)
: _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;
}
private:
int _num; //学号
};
void Test()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
int main()
{
Test();
system("pause");
return 0;
}
单继承与多继承
1. 单继承–一个子类只有一个直接父类时称这个继承关系为单继承。
2. 多继承–一个子类有两个或以上直接父类时称这个继承关系为多继承。
【单继承】
class A
{
A()
{}
};
class B : public A
{
B()
{}
};
class C : public B
{
C()
{}
};
class D : public C
{
D()
{}
};
【多继承】
class A
{
A()
{}
};
class B
{
B()
{}
};
class C : public A, public B
{
C()
{}
};
简单用图说明逻辑
菱形继承
class A
{
public:
A()
{}
int _a;
};
class B : public A
{
public:
B()
{}
int _b;
};
class C : public A
{
public:
C()
{}
int _c;
};
class D : public B, public C
{
public:
D()
{}
int _d;
};
简单用图说明逻辑
菱形继承对象模型
void Test()
{
D d;
//存在二义性和数据冗余问题
d.B::_a;//显示指定访问父类B中的_a成员;
d.C::_a;//显示指定访问父类C中的_a成员;
}
虚继承--解决菱形继承的二义性和数据冗余的问题
1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体
系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
菱形虚拟继承对象模型
虚函数&多态
虚函数–类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。
虚函数重写–当在子类的定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。
总结:
1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3. 只有类的成员函数才能定义为虚函数。
4. 静态成员函数不能定义为虚函数。
5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
6. 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引
起混淆。
7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
8. 最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但
是构成覆盖,这里是因为编译器做了特殊处理)
继承体系同名成员函数的关系
纯虚函数
在成员函数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚
函数在派生类中重新定义以后,派生类才能实例化出对象。
class A
{
public:
A ()
{}
virtual void Print() = 0;//纯虚函数
int _a;
};
class B : public A
{
public:
B()
{}
virtual void Print()
{
cout << _b << endl;
}
int _b;
};
友元与继承
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
继承与静态成员
基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。