我们知道C++是一个面向对象的语言,继承是面向对象程序设计使代码可以复用的最重要的手段,使在逻辑上是继承关系的对象在用类来描述对象语言层面实现了代码复用,这样产生的新的类叫做派生类
例如用学生系统来举例
图中的学生管理系统建立了四个类用来描述 人、学生、老师、助教,但是我们发现学生、助教、老师这三个类都有人这个类的属性,在这个属性上才会多出来一些不同的特征,例如学生可能会有学号、班级…,于是学生从人继承部分属性就是类中的继承,人这个类叫做 父类 或者 基类 ,学生这个类别叫做 子类 或派生类。
class person
{
public:
void print()
{
cout << name << endl;
cout << _age << endl;
}
protected:
string name = "peter";
int _age = 18;
};
class student:public person //student类继承了基类person 中的所有属性
{
private:
int stuid; //student中新增的属性
};
student叫做 子类 或 派生类,person叫做 父类 或 基类,public叫做继承方式,不同的继承方式会导致继承的属性在子类中的访问权限不同
属性访问权限 和 继承方式
以前在学习类的时候 不同的访问限定符 : public
、private
、protected
,成员变量或函数声明在不同的访问限定符内会导致访问权限大不相同,在继承的时候也会出现这个问题, 不同访问限定符的属性(成员函数或变量)和 继承方式 会导致父类中这些属性在子类中的访问权限不同
public继承 | protected继承 | private继承 | |
---|---|---|---|
父类属性在prublic访问限定符内 | 继承到子类prublic访问限定符内 | 继承到子类protected访问限定符内 | 继承到子类private访问限定符内 |
父类属性在protected访问限定符内 | 继承子类protected访问限定符内 | 继承到子类protected访问限定符内 | 继承到子类private访问限定符内 |
父类属性在private访问限定符内 | 无法被子类继承 | 无法被子类继承 | 无法被子类继承 |
注意
private
和 protected
之间的用法上的区别,如果基类的成员不想被类外直接访问,而能被派生类访问就可以使用protected关键字来定义成员。protected是因为继承才出现的public
继承方式,很少取用private
和protetcted
由于派生类和基类中都含有共同的成员变量和函数,他们之间满足一定的单向赋值关系!
派生类对象 可以赋值给 基类的对象、基类的指针、基类的引用。这里如上图所示就是将派生类除继承类以外的所有成员赋值给基类成员,形象的说法就是叫做切片。但是基类不能赋值给派生类!
class person
{
public:
void print()
{
cout << _name << endl;
cout << _age << endl;
}
person(string name="peter", int age=10)
:_name(name),_age(age)
{
}
person(person &s)
{
cout << "拷贝构造" << endl;
}
protected:
string _name ;
int _age ;
};
class student :public person
{
public:
student(int stuid=10, string name = "peter", int age = 10)
:_stuid(stuid),person(name,age)
{
}
private:
int _stuid;
};
int main()
{
student s;
person p1 = s; //基类的对象
person* p2 = &s; //基类的指针
person& p3 = s; //基类的引用
}
我们这里就可以发现 基类指针 和 引用的赋值是发生了切片,而基类的对象赋值发生了赋值拷贝,这点很重要!
在继承的过程中另一个问题就会出现,如果从基类继承的成员变量名或者成员函数名与派生类的成员变量名或函数名重名的情况。这时候就要讨论一下 基类 和 派生类的作用域的问题
变量重名
class person
{
protected:
string name = "peter";
int _age = 18;
int num = 10;
};
class student:public person
{
public:
void print()
{
cout << num << endl; //打印的是student类中的num
cout << person::num << endl; //打印的是person类中的num ,注意要加限定符::
}
private:
int stuid;
int num = 100;
};
int main()
{
student s;
s.print();
}
函数名重名
class person
{
public:
void fun()
{
cout << "this is person" << endl;
}
protected:
string name = "peter";
int _age = 18;
};
class student:public person
{
public:
void fun()
{
cout << "this is student" << endl;
}
private:
int stuid;
};
int main()
{
student s;
s.fun();
s.person::fun(); //同名函数想要访问必须显示访问
}
构造函数 | 子类中从父类继承的成员会被看成一个整体,在构造的时候会整体调用基类的构造函数进行初始化,而且基类的构造函数会在派生类的构造函数之前调用。如果派生类继承于多个基类,基类函数的构造函数调用顺序是继承声明时的顺序 ,而不是构造函数参数列表初始化的顺序! |
---|---|
拷贝构造函数 | 拷贝构造函数由于本质上是一种特殊的构造函数,所以调用规则和上面一样 |
赋值运算符函数 | 派生类的 赋值运算符函数 同理要显示的调用 父类的 赋值构造函数,特别注意 这里要显示调用基类的函数(在函数名前加上类的作用域,否则会构成函数隐藏) |
析构函数 | 析构函数虽然函数名看似不相同,但是编译器处理所有类的析构函数时都会认为函数名相同,所以这里继承的时候就会构成隐藏,必须显示调用。还有要注意一下调用顺序:构造函数时先 基类 后 派生类 ,根据FILO的原则:析构函数的顺序就是:先派生类 后 基类 |
class person //基类
{
public:
person();
person(string s)
:name(s)
{
cout << "person" << endl;
};
person(person& p)
:name(p.name)
{};
person& operator=(person& p)
{
name = p.name;
return *this;
}
~person()
{
cout << "~person" << endl;
}
protected:
string name;
};
class student :public person //派生类,如果这里声明了多个基类,那么构造函数的调用顺序就是这里基类的声明顺序
{
public:
student(string s, int n)
:person(s)
, stuid(n)
{
cout << "student" << endl;
};
student(student& s)
:person(s)
{
stuid = s.stuid;
}
student& operator=(student& s)
{
person::operator=(s);
stuid = s.stuid;
return *this;
}
~student()
{
//person::~person();
cout << "~student" << endl;
}
private:
int stuid;
};
int main()
{
student s("sht",191);
}
继承中中的静态成员 基类 和 派生类 共享同一个静态成员
代码证明:
class person
{
public:
person();
person(string s)
:name(s)
{
count++;
};
static int count;
protected:
string name;
};
int person::count = 0;
class student :public person
{
public:
student(string s, int n)
:person(s)
, stuid(n)
{};
private:
int stuid;
};
int main()
{
person s1("jack");
student s2("tony", 10);
student s3("henry", 20);
cout << s3.count << endl;
}
友元关系不能继承,所以友元函数不能访问子类的private和protected所有内容
菱形继承代码:
class A
{
public:
int a;
};
class B :virtual public A
{
public:
int b;
};
class C : virtual public A
{
public:
int c;
};
class D : public B,public C
{
public:
int d;
};
int main()
{
D d;
d.a = 1;
d.b = 2;
d.c = 3;
d.d = 4;
}
如果我们没有使用虚继承的时候,打开内存监视面板,查看d的内存应该是如下:
int main()
{
D d;
d.B::a = 1;
d.C::a = 10;
d.b = 2;
d.c = 3;
d.d = 4;
}
如果加上虚拟继承的话,d的内存结构就会发生改变,B::a
和C::a
的存储位置就不会存储a的值了,而是存储了一个地址,这地址指向一块内存这块内存我们把它叫做虚基表,这个表上第一行存储的是多态的虚表,后面学习多态的时候会提到。第二个存储的值是虚基表相对于虚基类存储位置的偏移量。
注意
虚继承不仅会改变D类的存储结构,同时还会改变B类和C类的存储结构
学完了继承之后,一下两个类之间的关系要重点辨析
class B :public A // 关系一:is-a关系
{};
class B //关系二:has-a关系
{
A a;
};
继承是一种 is-a关系,逻辑上如果B继承了A那么B应该包含A,A中的所有成员和都会暴露给B使用,B可以直接对A中的 成员变量直接进行修改,所以这里就会造成一定的风险。本质上是一种白箱操作,两个类如果是继承关系,那么关联度就会很高,耦合度就会很高
关系二是一种has-a关系,B只能调用A中想要暴露给外界的接口,成员变量是无法访问的,是一种黑箱操作,因为只是调用接口,所以两个类之间的关联度很低,耦合度也很低
所以我们在编写程序的时候尽量用高内聚,低耦合的组织方法,尽量使用黑箱操作少暴露类里面的成员,使用对外暴露的接口