继承的本质就是类层次的复用。
父类就是被继承的类。
子类就是继承的类。
子类复用了父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "tongtong"; // 姓名
int _age = 15; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student s;
//如果共有子类可以用父类的函数
//私有就不可以
s.Print();
return 0;
}
用上面的定义来说,
person就是父类,student就是子类,子类会复用父类
class Student : public Person
,
在定义student
的时候其中pubilc
就是继承方式。
继承关系和访问限定符都是有三种,public,protected,private
所以继承关系和访问限定符有9种组合方式,具体如下图。
类成员/继承方式 | pubilc 继承 |
protected 继承 |
private 继承 |
---|---|---|---|
父类的pubilc 成员 |
子类的pubilc 成员 |
子类的protected 成员 |
子类的private 成员 |
父类的protected 成员 |
子类的protected 成员 |
子类的protected 成员 |
子类的private 成员 |
父类的private 成员 |
在子类中不可见 | 在子类中不可见 | 子类中不可见 |
当在继承的时候,可以不写继承关系。
但是class默认的继承方式是private,struct的继承方式是public。
protected
和 private
的区别
protected
在子类中可见,不可用。private
在子类中不可见,不可用。
将子类转换为父类,不需要发生类型转换,不会产生临时变量,天然支持的。
可以这样理解,父类是特殊的子类。
int main()
{
//发生隐式类型类型转换
double d = 1.5;
int i = d;
//必须用const,临时变量具有常性
const int& i2 = d;
//天然支持的,不存在类型转换发生
Student s;
Person p = s;
//没有产生临时对象,所以不用加const
Preson p2& = s;
Preson* ptr = &s;
return 0;
}
基类::基类成员
显示访问)做下面这个练习题
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;
}
};
//A:两个fun构成函数重载
//B:两个fun构成隐藏
//C:编译报错
//D:以上说法都不对
B,
在不同的作用域,不可能构成函数重载,这两个类也没有问题,他们构成隐藏。
有人会想,他们同名,参数不同还构成隐藏吗?
如果是成员函数的隐藏,只需要保证函数名相同就可以。不用考虑参数,和返回值。
如果想用A的fun怎么用?
B().A::fun();
加上域作用限定符就可以,我用的是匿名对象,大家也可以不用。
先考虑默认成员函数
先给大家一个父类,写一个
student
子类
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; // 姓名
};
父类的成员必须调用父类的构造进行初始化。如果在子类中不写构造和析构函数,调用父类的构造函数和析构函数。
父类的成员调用父类的构造函数初始化,子类的自己的成员自己初始化,
用到方法就是在初始化列表中,按照这个方式初始化person(name)
,调用父类的构造,用我们自己传的参数构造。
如果不写person(name)
,也会调用父类的构造函数,用缺省参数构造。
我们可以这样理解,将父类当成子类的成员。
class Student : public Person
{
Student(const char* name,int num)
:Person(name)//规定这么写,调用父类的构造函数初始化
//如果不写person(name)也可以调用父类的构造函数。
,_num(num)
{}
protected:
int _num;
};
int main()
{
Student s("tongtong",20);
return 0;
}
如果不写子类的拷贝构造,会调用父类的默认的拷贝构造。
class Student : public Person
{
public:
Student(const Student& s)
{}
protected:
int _num;
};
int main()
{
Student s("tongtong",20);
Student s2(s);
return 0;
}
对于上面这种写了拷贝构造但不做处理,编译器不会调用父类的拷贝构造。只进行了一次构造,还是因为传参的时候调用。
初始化父类,就传父类的拷贝构造
class Student : public Person
{
public:
Student(const Student& s)
//直接将子类切片。调用父类的拷贝构造
:Person(s)
,_num(s._num)
{}
protected:
int _num;
};
Student& operator =(const Student& s)
{
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
//析构函数会被处理为Destructor,构成隐藏所以用作用域
~Student()
{
Person::~Person();
}
int main()
{
Student s1("tongtong", 20);
}
如果这样我们的析构会调用三回,本来只需要调用两回,多调用了一会的原因是
Person::~Person();
这个多调用了一会。
析构函数不需要我们自己显示调用,他自己调用父类的析构和子类的析构。
所以为了保证析构的顺序,先析构子再析构父。
如果我们自己操作不能保证这个顺序,
所以编译器帮我们做,子类析构函数完成时,会自动调用父类析构函数,保证先析构子再析构父。
因为要保证顺序,
构造的顺序肯定是先构造父类在构造子类。
四个子类的默认函数调用,除了析构函数,顺序都是先父后子。
友元关系不能继承。
下面代码有错误,因为友元函数不能继承,所以我们再有友元函数中不能访问子类的成员,没有继承了父类的友元。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
//错误的代码下一句,不可访问
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
但是如果想要访问,就在子类加上友元函数的声明就可以了
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
//加上友元就可以使用了。
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
使用虚继承解决数据冗余和二义性
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;
}
使用虚继承
内存窗口
将公共继承的A放到一块空间,
B通过第一个地址找到偏移量20,向下走5个地址,从而找到A,就可以改变A。
C通过第一个地址找到偏移量12,向下走3个地址,从而找到A,就可以改变A。
这样就只有一块A,解决数据冗余和二义性