定义格式
看个代码会更好理解
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int a = -1 % 5;
cout << a << endl;
return 0;
}
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
这里Teacher类和Student类是继承自Person。Teacher和Student是派生类(子类);Person是基类(父类)两者中间的public是继承方式。
继承方式和访问限定符
继承方式有三种:public、protected、private;访问限定符也有三种:public、protected、private
继承基类成员访问方式的变化
基类private成员 在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中 ,但是在语法上限制了派生类对象不管在类中还是类外都不能去访问它。
基类的private成员在派生类中不可访问。如果不想在类外被访问 ,**但是想在派生类中被访问就定义为protected;**因此可以看出来protected是在继承之后出现的。
基类成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式) ,public > protected > private。
关键字class 的默认继承方式是private ,struct 的默认继承方式是public。
基类和派生类之间的赋值兼容规则
1、子类对象可以赋值给父类对象/指针/引用 (天然支持,不是隐式类型转换)
父类 = 子类 (指针是只能看见子类中父类的那那一部分 )(引用也是一样) 就是在子类中把父类的那一部分切出来给父类,这个过程称为切片/切割
2、父类对象不可以赋值给子类对象
**3、父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。**但是必须是 父类的指针是指向子类对象时才是安全的
继承中的作用域
(这里的成员既是成员函数 又是成员变量 )当父类的成员和子类的成员重名 ,使用就近原则 ,在子类访问这个成员,会先在子类中去找,再去父类中寻找(当父类和子类同时有同名成员 时,子类的成员隐藏 了父类的成员)(称为重定义/隐藏) (与重载区分开,重载必须在同一作用域)。 若在这种情况下就是想访问父类的成员,可以通过显式访问 ,指定作用域 ,(父类名::)
派生类的默认成员函数
1、派生类继承 自基类的那一部分成员 ,必须调用基类的构造函数初始化 。如果基类没有默认的构造函数 ,则必须在派生类的构造函数的初始化列表阶段中显式调用 。(默认的构造函数:无参构造函数、全缺省构造函数、没写编译器默认生成的构造函数)
2、派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3、派生类的operator=必须调用基类的operator=完成基类的赋值
4、派生类的析构函数会在被调用完之后自动调用基类的析构函数清理基类成员,这样才能保证派生类对象先清理派生类的成员再清理基类成员的顺序
5、派生类对象初始化先调用基类的构造再调用派生类的构造
cpp
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person(const char* name = "perter") // 假如这里不是默认的构造函数,那么必须在子类的构造函数中初始化列表阶段中显式调用
: _name(name) // 比如去掉"perter",这样就不是默认的构造函数了。
{ // 但是,不是显式调用意义不大,因为在创建子类对象时,如果不把name传过来
cout << "Person()" << endl; // 那子类对象的name就会初始化为缺省值。
}
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) // 假如这里没有调用基类的拷贝构造,那么在拷贝构造s2的时候,s2中基类的那一部分成员会调用基类的构造初始化
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{ //operator =(s) 会调用子类的赋值,会栈溢出
Person::operator =(s); // 这里将子类对象传给父类的赋值运算符重载函数,也是一个切片
_num = s._num;
}
return *this;
}
~Student()
{
//Person::~Person(); 析构函数不需要显式调用,会自动调用
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
总的来说:
1、子类继承自父类的成员变量,会调用父类的构造和析构。(子类的成员调子类的,父类的成员调父类的) 不可以在子类中去构造和析构父类的成员变量,但是可以在子类中显式调用父类的构造
2、无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。 当父类有默认构造函数时,可以不在子类中显式调用,但是假如没有默认构造函数时,那么就调不到构造了。这时就需要显式调用**( 父类名(实参))** (在显式调用中的传参,假如是子类对象或者是子类的指针、引用,都涉及到切片)
3、子类的析构函数和父类的析构函数构成隐藏,因为他们的名字会被编译器统一处理成destructor(多态)
4、析构函数不需要显式调用,会自动调用 ,(先构造父类,在构造子类,所以先析构子类,再析构父类)因为需要保证先析构子类再析构父类。在子类析构结束后,会自动调用父类的析构。
5、父类和子类自己调用自己的构造、拷贝、赋值、析构。区别是:前三个需要在子类中显式调用,析构会自动调用,因为需要保证先析构子类再析构父类的顺序
面试题:如何设计一个不能被继承的类?
答:将父类的构造函数私有化。这样父类的构造函数在子类中不可见;并且父类的成员必须调用父类的构造,那么就构造不了了。
继承与友元
友元关系不能继承,也就是说基类的友元函数不能访问派生类私有和保护成员
继承与静态成员
基类定义static静态成员,那么在整个继承体系里面都只有一个这样的成员,无论派生出多少子类,都只有一个static成员实例
菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或以上直接父类
菱形继承:
这样的话Assistant对象会有两份Person成员,并且这两份是重名的,并不知道哪个是哪个类的,这样就会有数据冗余和二义性问题
虚拟继承解决数据冗余和二义性原理
class A
{
public:
int _a;
};
//class B : public A // 普通继承
class B : virtual public A // 虚拟继承
{
public:
int _b;
};
//class C : public A
class C : virtual 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;
cout << sizeof(d) << endl;
return 0;
}
普通继承
d对象实例化占20个字节,d对象中有一个属于B的_a,还有一个属于C的_a。
虚拟继承
这里占24个字节,d对象的_b中会存一个地址,这个地址存偏移量,可以找到_a,_c也一样。这样就可以解决二义性,这里的父类B和父类C只有少量的成员变量,如果存的成员变量很多,那就可以体现解决数据冗余。
继承的总结
继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象
组合是一种has-a的关系。假设B组合了A,每个B对象都有一个A对象。
继承是一种白箱复用,父类对于子类基本是透明的,但是它一定程度破坏父类封装
组合是一种黑箱复用,A对B是不透明的,A保持着它的封装
组合的类耦合度更低,继承的类是一种高耦合
面试题:使用继承还是组合呢?
答:不能一概而论,根据使用场景,符合is-a就使用继承,符合has-a就使用组合,都符合就优先使用组合