写在前面:
大家脑海里是否还能浮现以下的场景:
刘海柱:我,从小父母双亡,家徒四壁,除了成功,别无选择;
你呢?
你回了老家能干啥,除了继承你家那个养猪场,你爸的几套房子几辆车,和五十亩地之外,你说你还有啥,你还是个啥!!!!
今那么今天咱就讲讲继承的那些事
目录:
- 继承的概念与定义
- 继承方式
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 继承中的static成员变量
- 继承的形式(单继承,多继承,菱形继承,虚拟继承)
一、继承的概念与定义
先抛开继承在语言里的概念,看过这么多年的电视的我们应该也熟悉“继承”这个词,
大多是,子承父业,继承遗产之类的,那么在继承之后,儿子理所应当的拥有了父亲的所有东西,而儿子还应该还拥有一些自己的东西。
这是与继承在编程语言中的含义是大致相同的:
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
名词解释:
定义:
示例:
class A
{
};
class B:public A//这就是类的继承的定义 class B继承了 A 拥有了A的东西
{
};
原来的类被称为基类或者父类
继承基类或者父类的类被称为子类或者派生类
那么具体有什么特性呢?
看示例的代码:
#include<iostream>
using namespace std;
class Father//基类 或者派生类
{
public:
Father()//构造函数
{
cout << "Father()" << endl;
}
~Father()//析构函数
{
cout << "~Father()" << endl;
}
void Print()//类的成员函数
{
cout << "money:" << money << "car:" << car << endl;
}
protected://成员变量
int money=100;
int car=2;
};
class Son:public Father//子类或者派生类
{
public:
Son()//构造函数
{
cout << "Son()" << endl;
}
~Son()//析构函数
{
cout << "~Son()" << endl;
}
protected://成员变量
int age;
};
int main()
{
Father f;
Son s;
s.Print();
return 0;
}
从下面的打印结果,可以看到我们实例化了一个 类Son 的对象 s; s 中有默认的构造和析构函数
但是通过打印结果我们发现,先调用了基类(父类) 的构造函数,再调用了子类的构造函数,而子类也可以调用父类的成员函数,说明了成员函数的复用,与此同时我们还发现,析构的时候是先调用子类的析构函数再调用父类的析构函数。
而父类的成员变量也理所应当的是可以被子类复用的
例如:我们在上述的代码中父类拥有两个成员变量 money=100和car=2 而在子类中能否访问呢?
当然是可以的!
大家可能会注意到一个问题:
上面的成员变量是 protected的
继承的方式是 : class Son : public Father
那么这些继承的方式又有什么异同呢?
二、继承方式
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结起来就是:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是 被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
例如:
无论什么继承方式,基类的私有成员是不可见的
但是子类可以通过调用基类的成员函数,来对基类的私有成员进行访问
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式就是比较访问范围小的那个,例如基类的public程序员被protected继承时,就成为了子类的protected成员,
访问限定符的范围 public > protected > private。- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的 写出继承方式。
例如:
class Student : Person 这里默认是private继承,将基类的public、private成员都继承为私有的- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中 扩展维护性不强。
三、基类和派生类对象赋值转换
定义两个类:
class Father
{
};
class Son:public Father
{
};
在子类对象与父类对象的赋值过程中有如下需要注意的事项
int main()
{
Son s;
Father f;
f = s;//子类对象可以赋值给父类对象
Father* p = &s;//也可以赋值给父类的指针
Father& p1 = s;//也可以赋值给父类的引用
//s = f;//但是父类不能赋值给子类对象
return 0;
}
注意:
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。 基类对象不能赋值给派生类对象 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才 是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来 进行识别后进行安全转换。
ps:之所以这里的子类可以给父类对象赋值,而父类不能赋值给子类对象是因为,一般子类的成员变量除了继承父类的还有他自己的,而赋值这一操作类似于切片的操作,是一个多向少的过程,就像是我可以给你你拥有的,但是我不能给你我没有拥有的这个意思。
四、继承中的作用域
这里有这样一个问题,我在子类中如何调用父类的成员函数或者成员变量呢?
其实,即使子类继承了父类的成员函数和成员变量,他们之间还是会有明确而独立的作用域的。
例如:
class Father
{
public:
void Print()
{
cout <<"Father:"<< "money:" << money << "car:" << car << endl;
}
int money=10;
int car=1;
};
class Son: public Father
{
public:
void PrintBaseElement()
{
Father::Print();
}
void Print()
{
cout << "Father: money:" << Father::money << "car:" << Father::car << endl;
cout <<"Son:"<<"money:"<< money << "car: " << car << endl;
}
protected:
int age;
int money = 100;//
int car = 2;
};
int main()
{
Son s;
s.Print();
s.PrintBaseElement();
return 0;
}
打印结果:
我们这里会发现 父类 Father 的成员变量有 : money 和 car 这两个变量了,当子类中再定义相同的变量名会如何呢?
这时当我们在子类中访问 money 和 car 这变量时访问的就会是子类自己定义的 money 和car ,同名成员变量将会将父类的成员变量隐藏起来,也可以算作是子类重写了父类的成员变量,而父类的成员变量还是保持原来的值,但是不建议这样定义,容易造成混淆,要想访问父类的变量需要显示访问 如:
void Print1()
{
cout << "Father: money:" << Father::money << "car:" << Father::car << endl;
//作用域展开,进行显示的访问
cout <<"Son:"<<"money:"<< money << "car: " << car << endl;
//同名的成员变量对父类的成员变量造成隐藏
}
同理:当子类定义了与父类同名的成员函数,也同样构成函数隐藏,同样也可以对其进行显示的调用
class Son: public Father
{
public:
void PrintBaseElement()
{
Father::Print();//显示的调用父类的成员函数
}
};
以上的定义以及显示访问都是被允许的操作,但是在继承类里面还是不建议这样定义,容易发生混乱
五、派生类的默认成员函数
类有六个默认成员函数分别是:
1.构造函数
2.拷贝构造函数
3.赋值函数
4.析构函数
5.取地址
6.重载
那么在子类的默认成员函数会如何调用呢?
先看一段代码:
#include<iostream>
using namespace std;
class Father
{
public:
Father(const int _money = 10, const int _car = 2)
:money(_money)
,car(_car)
{
cout << "Father() 构造函数" << endl;
}
Father(const Father& f)
:money(f.money)
,car(f.car)
{
cout << "Father(const Father& f) 拷贝构造函数" << endl;
}
Father& operator=(const Father&f)
{
cout << "Father& operator=(const Father&f) " << endl;;
if (this != &f)
{
money = f.money;
car = f.car;
}
return *this;
}
~Father()
{
cout << "~Father()析构函数" << endl;
}
protected:
int money;
int car;
};
class Son :public Father
{
public:
Son(const int _money,const int _car,int _book)
:Father(_money,_car)
,book(_book)
{
cout << "Son() 构造函数" << endl;
}
Son(const Son&f)
:Father(f)
,book(f.book)
{
cout << "Son(const Son&f) 拷贝构造函数" << endl;
}
Son& operator=(const Son&s)
{
if (this != &s)//防止自己给自己赋值
{
Father::operator=(s);
book = s.book;
}
return *this;
}
~Son()
{
cout << "~Son 析构函数" << endl;
}
protected:
int book;
};
int main()
{
Son s1(100, 20, 15);
Son s2(s1);
Son s3(10, 35, 5);
s1 = s3;
return 0;
}
打印结果:
这里我们可以总结出如下的规律:
1.子类会先调用父类的构造函数再调用子类自己的构造函数,如果父类没有的话,必须在子类的构造函数初始化列表显示调用
当子类的构造函数不显示调用时会自动调用父类的构造函数,否则应该显示调用
2.子类必须调用父类的拷贝构造函数完成父类的拷贝构造初始化,并完成自己的拷贝构造初始化
3.子类调用operator =函数的时候必须调用父类的operator =
4.子类先调用自己的析构函数,再调用父类的析构函数
构造结果测试:
根据上面的知识这里举一个,类不能被继承的情况
示例:
当父类的构造函数为私有的时候,即使子类怎么继承都无法调用父类的构造函数这样就无法继承该类了
class Father
{
private:
Father(){}
};
class Son :public Father
{
public:
Son(){}//报错 即使这里不写下面实例化对象也会报错
protected:
int age;
};
int main()
{
Son s;//不写子类的构造函数这里还是会报错:显示无法引用Son的构造函数,因为它时已经被删除的函数
return 0;
}
六、继承与友元
父类的友元函数是不能被继承的,因此父类的友元函数无法访问子类的私有成员和保护成员,但是可以访问公有成员例如:
class A
{
public:
friend void Print(const A& a, const B&b);
public://protected 和private 不可访问
int car = 10;
};
class B : public A
{
public://protected 和private 不可访问
int book = 5;
};
void Print(const A& a,const B& b)
{
cout << a.car << " " << b.book;
}
int main()
{
A a;
B b;
Print(a, b);
return 0;
}
七、继承与静态成员
关于静态成员 被 static 修饰的成员在继承中是唯一的实例,也就是说:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一 个static成员实例 。
关于 关键字 static 这里不再过多的阐述,作为存储在静态区的变量,即使这个变量出了函数的作用域,依然会保持它的值,这样的特质也使其在类的继承中也因此只有这一个static 成员的实例。
例如:
class A 中有一个 static 变量,在类外进行第一次初始化,A 里面的构造函数是对 这个变量++,
class B、C分别继承了A 那么会出现什么结果:
class A
{
public:
static int count;
A() { ++count; }
};
int A::count = 0;
class B :public A {};
class C : public A {};
int main()
{
A a;
B b;
C c;
cout << c.count << endl;
return 0;
}
而当我们不使用 static 对其修饰会怎么样:
八、继承的多种形式
1.单继承:
像我们上面介绍的大多都是单继承,也就是:
class A{};
class B:public A{};
像是这样就是单继承
2.多继承:
就是一个类继承了多个类(两个或者两个以上)如:
class A{};
class B{];
class C:public A,public B{};//这是C++支持的方法,C就同时继承了A和B的成员变量和成员函数
3.菱形继承
像是这样,因为他们之间的继承关系像一个菱形因而得名,这使多继承的一种比较特殊的情况,这时我们可以发现,这样的继承:
当 class B和class C没有自定义自己的成员变量和成员函数的时候还不如之间 使 class D :public A ,这样的菱形会造成数据的冗余,多继承了一份A中的成员变量和成员函数,而且这样的继承还容易造成二义性
上图表示数据的二义性
这样就会有两份A类的数据了,而继承的数据string 也存在着二义性,到底是B类的还是C类的,这个问题使可以通过显示访问解决的,但是数据的冗余却无法解决,
因此在设计类的继承的时候要避免设计出这样的菱形继承
那么这样的如何去解决呢?
这里给出了虚拟继承的概念:
虚拟继承
通过 virtual 关键字,使继承称为虚拟继承 如:
class A{};
class B:virtual public A{];
class C:virtual public B{};
class D:public B,public C{};
而虚拟继承的原理就是:
这里是通过了B和C的两个指针,指向的一张表。这两个指 针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A
这就是虚拟继承的原理,但是值得注意的是,当需要解决菱形继承的数据冗余和二义性的时候可以使用虚拟继承,但是不要在其他的地使用虚拟继承;