继承是代码复用的一种体现。在已有类的基础上进行扩展。
那么,继承的形式是什么样的呢?
class son:public father
{}
son叫做派生类/子类,father叫做基类/父类;public是继承方式。
继承方式有3种,公有继承public,保护继承protected,私有继承private。
当没有写是什么方式继承的时候为私有继承。
我们知道类也有三种访问限定:公有
public,继承protected,继承private。
那么基类不同的限定访问,在不同方式的继承之后,派生类会出现怎么样的访问限定。
结论:
- 基类的私有成员,无论是哪一种形式的继承,继承之后在派生类中也不能访问。
- 基类任意一种访问限定符限定的成员,当为私有继承的时候,在派生类外面都不能进行访问。
- 我们之前在讲类的时候说,类的
protected和private的访问限定是一样的,在类外面都不能访问。而继承就体现了它用法,当为protected继承的时候,在派生类的里面是可以访问到基类的成员的。- 大多数的情况下都是公共继承的,因为继承之后在派生类的外面是可以访问的。
| 基类 / 派生类 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public成员 | 派生类public成员 | 派生类protected成员 | 派生类private私有成员 |
| protected成员 | 派生类protected成员 | 派生类protected成员 | 派生类private私有成员 |
| private成员 | 不可访问 | 不可访问 | 不可访问 |
- 派生类是可以给基类赋值的,可以是赋值给基类的指针,基类的引用,基类的对象。外面把这种操作叫做切片操作。
- 基类不可以给派生类继承。
下面来解释一下赋值的底层:
class father
{
public:
int _a;
};
class son :public father
{
public:
int _b;
};
int main()
{
son x;
x._a = 1;
x._b = 2;
father y = x;
y._a = 0;
return 0;
}

- 赋值给指针
int main()
{
son x;
x._a = 1;
x._b = 2;
father* y = &x;
y->_a = 0;//此时派生类中继承的基类的成员的数据会改变。
return 0;
}
- 赋值给引用
int main()
{
son x;
x._a = 1;
x._b = 2;
father y = &x;
y._a = 0;
return 0;
}

不管是基类还是派生类它都有独立的作用域,都在该类域里面。
- 当基类中的成员和派生类中的成员名相同的时候,此时基类中的该成员隐藏。要显示基类的类域才可以访问,没有显示类域默认访问派生类中的。
- 注意:只要名字一样就构成隐藏。
成员变量构成的隐藏
class A
{
public:
int _a=10;
};
class B :public A
{
public:
int _a=20;
};
int main()
{
B x;
cout <<"B中:" <<x._a << endl;
cout << "A中:" << x.A::_a << endl;//访问A中的_a要指定类域
return 0;
}

成员函数构成的隐藏
class A
{
public:
void f()
{
cout << "A" << endl;
}
public:
int _a=10;
};
class B :public A
{
public:
void f()
{
cout << "B" << endl;
}
public:
int _a=20;
};
int main()
{
B x;
x.f();
x.A::f();
return 0;
}

在基类中,如果没有默认成员函数,我们必须在派生类中显示的写出。如果基类中有默认成员函数,当派生类中不显示调用的时候,会自动调用。
- 对于构造函数,都会在初始化列表的时候自动调用基类的构造函数。
- 对于析构函数,在对象销毁的时候,会自动调用基类的析构函数,在调用自身的析构函数,所以,不需要自己在析构函数里面显示的调用基类的析构函数。这样才能保证先析构派生类,再析构基类
- 1.当基类有默认的构造函数的时候,可以只初始化派生类新的成员变量,也可以自己调用基类的默认构造,看自己的心情。
- 2.当基类没有默认的构造函数的时候,必须自己要写构造函数调用基类的
class A
{
int _a;
public:
A()
{
_a = 10;
}
};
class B :public A
{
int _b;
public:
B()
{
_b = 20;
}
};
int main()
{
B x;
return 0;
}
class A
{
int _a;
public:
A(int a)
{
_a = 10;
}
};
//把A的构造函数改成不是默认构造的时候,那么上面的代码就会报错

此时B就应该在初始化列表显示的调用A中的构造函数。
class B :public A
{
int _b;
public:
B()
:A(0)
{
_b = 20;
}
};
必须调用基类的。派生类自己的成员还是和类的拷贝构造,赋值运算符重载一样。
赋值运算符要指定一下域
class A
{
int _a;
public:
A(int a=0)
{
_a = a;
}
A(const A& x)
{
_a = x._a;
}
A& operator=(const A& x)
{
_a = x._a;
return *this;
}
};
class B :public A
{
int _b;
public:
B()
:A(0)
{
_b = 20;
}
B(const B& x)
:A(x)//把参数X传给A的拷贝构造,派生类可以传给基类,上面讲了。也可以传其他值哦。
{
_b = x._b;
}
B& operator=(const B& x)
{
A::operator=(x);//指明域
_b = x._b;
return *this;
}
};
int main()
{
B x;
B y(x);
return 0;
}
派生类的析构函数在对象销毁的时候,会自动调用基类的析构函数,所有不需要自己再手动调用基类的析构函数。
class A
{
public:
~A()
{
cout << "A析构" << endl;
}
};
class B :public A
{
public:
~B()
{
cout << "B析构" << endl;
}
};
int main()
{
B x;
return 0;
}

如果在B的析构函数里面手动析构A,
A::~A(),会把A析构两次,如果A中的成员是动态开辟的,将会产生野指针。
友元是不能继承的——基类友元不能访问派生类的私有和保护成员。
class B;
class A
{
friend void print(const A& x, const B& y)
{
cout << x._a << y._b << endl;
}
protected:
int _a;
};
class B :public A
{
protected:
int _b;
};
int main()
{
print(A(), B());
return 0;
}

对于基类的静态成员,无论它派生出多个派生类,所有继承体只要这么应该静态成员。
如下图:B继承了A,C也继承了A,D既继承了B也继承了C。这种继承关系就是菱形继承

C++支持多继承
二义性和数据冗余
- 二义性
如上图:对于D来说,含有2个A,当对他们访问的时候,不知道是哪一个
- 数据冗余
2个A都需要占用内存
class father
{
public:
int _f;
};
class son1:public father
{
public:
int _s1;
};
class son2 :public father
{
public:
int _s2;
};
class kunkun :public son1, public son2
{
public:
int _kk;
};
int main()
{
kunkun x;
x._f = 1;
return 0;
}
我们会发现它就会报错,因为存在二义性

看它的内存分布:从它的内存分布上可以看出,
father有两份,所有会冗余,也是当kunkun木有指定类域的时候不知道访问的哪一个。
关键子
virtual,可以解决二义性和冗余的问题。只需要son1``son2虚拟继承father即可。
class father
{
public:
int _f;
};
class son1:virtual public father
{
public:
int _s1;
};
class son2 :virtual public father
{
public:
int _s2;
};
class kunkun :public son1, public son2
{
public:
int _kk;
};
int main()
{
kunkun x;
x._s1 = 1;
x._s2 = 2;
x._f = 0;
x._kk = 3;
return 0;
}
看它的内存分布:

我们通过观察他们在内存中的存储,可以看出
father的成员_f只有一份,放在了如图所示的地方。
son1,son2他们怎么找到_f在哪里的?通过观察上图,会发现
son1,son2里面存了两个地址,0x005f7bdc,0x005f7be4
我们看看地址指向哪里:
16进制的14为20,这里的20是偏移量,相对_s1的偏移量,加上20,此时就是指向_f,下面那个含义也是一样的。
继承是is-a的关系——派生类是基类
组合是has-a的关系——A类中有B类