继承其实就是代码的复用,比方说有一个Person类,里面有姓名和学号,接下来我们要实现一个学生类,我们需要在姓名和学号的基础上加上一个学号,此时我们不需要重写,只需要把Person的类继承下去。
class Person
{
public:
void print()
{
cout << "Person()" << endl;
}
protected:
string _name;
int _age;
};
class Student : public Person
{
protected:
int _Pnum;
};
class Teacher : public Person
{
protected:
int _Tnum;
};
int main()
{
Student st;
Teacher te;
st.print();
te.print();
return 0;
}
上面的Person是父类(基类),下面的Student和Teacher是子类(派生类)。
继承关系有三个:public、protected、private
访问限定符也有三个:public、protected、private
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
1️⃣ 基类如果是private,还是会继承下去,只是子类不可见而已,也不能被访问。
2️⃣ 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
3️⃣ 在派生类的权限取权限小的那一个。
4️⃣ 如果不写继承的权限,默认是私有。
子类对象赋值给父类 对象/指针/引用,这里有个形象的说法叫切片
或者切割,寓意把派生类中父类那部分切来赋值过去。
class A
{
public:
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
int main()
{
B b;
A& pa = b;
++pa._a;
cout << b._a << endl;
return 0;
}
结果:
2
用父类指针指向子类对象,也只是指向父类有的那一部分。
注意:
基类对象不能赋值给派生类对象。
class A
{
public:
protected:
int _a = 1;
};
class B : public A
{
public:
void Print()
{
cout << _a << endl;
}
protected:
int _a = 2;
};
int main()
{
B b;
b.Print();
return 0;
}
如果父类和子类有同一个成员变量,在子类中默认访问的是自己的成员变量。
如果我们想要访问父类的呢?
void Print()
{
cout << A::_a << endl;
}
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
class A
{
public:
void Print()
{
cout << "A" << endl;
}
protected:
int _a = 1;
};
class B : public A
{
public:
void Print(int i)
{
cout << "b" << endl;
}
protected:
int _a = 2;
};
注意,这两个成员函数构成隐藏(重定义)。
不是重载是因为他们不在同一作用域。
成员函数满足函数名相同就构成隐藏
class A
{
public:
A(int a = 1)
: _a(a)
{
cout << "A()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
protected:
int _b;
};
int main()
{
B b;
return 0;
}
结果:
A()
可以看出派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用,不然会报错。
当我们想用子类构造函数初始化父类的成员变量:
class B : public A
{
public:
B(int a, int b)
: _a(a)// 错误
, _b(b)
{}
protected:
int _b;
};
不能直接初始化,而是应该调用父类构造函数。
class B : public A
{
public:
B(int a = 1, int b = 2)
: A(1)
, _b(b)
{}
protected:
int _b;
};
派生类的拷贝构造函数必须显示调用基类的拷贝构造完成基类的拷贝初始化,如果不显示调用,那么为了保证基类的变量初始化就会调用基类的构造函数初始化。
class A
{
public:
A(int a = 1)
: _a(a)
{}
A(const A& aa)
: _a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
B(int a = 1, int b = 2)
: A(a)
, _b(b)
{}
B(const B& bb)
: A(bb)// 切片
, _b(bb._b)
{
cout << "B(const B& bb)" << endl;
}
protected:
int _b;
};
int main()
{
B b1(3, 4);
B b2(b1);
return 0;
}
结果:
A(const A& aa)
B(const B& bb)
派生类的operator=必须要调用基类的operator=完成基类的复制。注意要指明作用域
class A
{
public:
A(int a = 1)
: _a(a)
{}
A(const A& aa)
: _a(aa._a)
{}
A& operator=(const A& aa)
{
if (this != &aa)
{
_a = aa._a;
}
cout << "A& operator=(const A& aa)" << endl;
return *this;
}
protected:
int _a;
};
class B : public A
{
public:
B(int a = 1, int b = 2)
: A(a)
, _b(b)
{}
B(const B& bb)
: A(bb)// 切片
, _b(bb._b)
{}
B& operator=(const B& bb)
{
if (this != &bb)
{
A::operator=(bb);// 切片
_b = bb._b;
}
cout << "B& operator=(const B& bb)" << endl;
return *this;
}
protected:
int _b;
};
int main()
{
B b1(3, 4);
B b2(5, 6);
b1 = b2;
return 0;
}
结果
A& operator=(const A& aa)
B& operator=(const B& bb)
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
class A
{
public:
A(int a = 1)
: _a(a)
{}
A(const A& aa)
: _a(aa._a)
{}
A& operator=(const A& aa)
{
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
B(int a = 1, int b = 2)
: A(a)
, _b(b)
{}
B(const B& bb)
: A(bb)// 切片
, _b(bb._b)
{}
B& operator=(const B& bb)
{
if (this != &bb)
{
A::operator=(bb);// 切片
_b = bb._b;
}
return *this;
}
~B()
{
cout << "~B()" << endl;
A::~A();
}
protected:
int _b;
};
结果
~B()
~A()
~A()
注意这里如果想调用父类析构在子类函数中必须指明父类的作用域
因为父类和子类的析构函数构成隐藏关系。
输出结果的原因
因为子类析构完成后会默认调用父类的析构函数
父子析构顺序:先子后父
所以我们不需要显示调用父类析构。
1️⃣ 友元关系不能被继承。
class A
{
public:
A()
{
++cnt;
}
static int cnt;
protected:
int _com;
};
int A::cnt = 0;
class B : public A
{
public:
protected:
};
这里子类对象继承下来的_com在父子之间是两份,父子的_com相互独立
但是父类的静态成员父子用的是同一个。
单继承
多继承
菱形继承
菱形继承的问题:
会引发数据冗余和二义性。
在A对象中的成员变量会继承到B和C中,但是D又会继承B和C当中的A的成员变量,使用的话编译报错,叫做二义性。
二义性的解决方法:
1️⃣ 访问的时候可以指定作用域。
class A
{
public:
int _com;
};
class B : public A
{
public:
};
class C : public A
{
public:
};
class D : public B, public C
{
public:
};
int main()
{
D d;
// 指定作用域
d.B::_com;
d.C::_com;
return 0;
}
2️⃣ 虚继承
virtual:
class A
{
public:
int _com;
};
class B : virtual public A
{
public:
};
class C : virtual public A
{
public:
};
class D : public B, public C
{
public:
};
int main()
{
D d;
d._com;
d.B::_com;
d.C::_com;
return 0;
}
此时三个访问方式访问的是同一个对象。
原理:
如果不加virtual,那么子类对象都会有一个各自的_com对象。
加上virtual后:
虚基类会放在最下面,在子类对象里会存储一份空间(虚基表),这份空间里存的是一个数字,这个数字是距离虚基类(A)的偏移量,目的是为了让B和C找到_com。