1.继承
1.1继承概念:
继承是面向对象重要的复用手段,就是一个类继承另一个类的属性和方法,这个新类包含上一个类的属性和方法,被称为子类或者派生类,被继承的类叫做父类或者基类。
1.2继承的方式及访问属性
1.2.1共有继承(public):除过基类是私有成员无法继承外,其它成员都可以按照基类方式继承,基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
1.2.2保护继承(protected):除过基类是私有成员外,其它成员继承后都变为保护继承,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
1.2.3私有继承(private):私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
1.3三种继承关系
继承方式 | 基类是public | 基类是protected | 基类是private | 继承引起的访问关系变化 |
---|---|---|---|---|
public | 仍为public成员 | 仍为protected | 不可见 | 基类的非私有成员在子类的访问属性都不变 |
protected | 变为protected | 仍为protected | 不可见 | 基类的非私有成员在子类中都变为保护成员 |
private | 变为private | 变为privated | 不可见 | 基类中的私有成员都变为子类的私有成员 |
eg:
class father//父类
{
public:
char* _fname;//父亲的名字
protected:
int _fIdCard;//银行卡账号是需要保护的,子类就是儿子可以知道
private:
int _fIdPassword;//银行卡密码只能父类知道,子类不能知道,怕你乱取钱花
}
class son:public father//子类
{
public:
void func(void)
{
char* _sname = _fname;//可以继承,基类的公有成员在派生类中变为公有成员。
int _sIdCard = _fIdCard;//可以继承,基类的保护成员在派生类中变为保护成员
int _sIdPassword = _fIdPassword;//不能继承,基类的私有成员在派生类中是不可见的
}
}
知识点总结:
1.基类中的私有成员不想让基类对象直接访问基类成员,但派生类却可以访问,就定义为保护成员,可见保护成员是为继承而出现的
2.public继承保持is-a原则,每个父类可用的成员在子类中也可用。当然不包括私有成员。
3.protetced/private继承,基类的部分成员并未完全成为子类接口的一部分,是 has-a 的关系原则,所以一般情况下不会使用
这两种继承关系,在绝大多数的场景下使用的都是公有继承
5.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能
访问)
6.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
附:
is-a原则: 全称为is-a-kind-of,显然这里is-a原则指的是子类是父类的一种;例如:人是基类,男人是子类,子类和基类构成继承关系;满足is-a 原则;男人是人的一类
has-a 原则:protected/private继承是实现继承,父类的成员有一部分子类是无法继承的 一个子类中有一个父类,即部分父类的成员是不可用的(has-a),has-a多用于组合关系。
例如:学生是一个类,姓名,学号,电话号都是一个类,这三个类组合成为学生类。
2.继承与转换
2.1赋值兼容规则–public继承
2.1.1 :子类对象可以赋值给父类对象(因为子类是从父类继承下来的,因此父类肯定会包含部分子类成员,在子类进行赋值时,父类成员只要获取自己的部分成员即可,这就叫做切片处理)
2.1.2:父类对象不能赋值给子类对象(因为父类私有成员是不可见的,所以父类给子类赋私有成员时,子类并不知道需要多少空间来接收父类私有成员)
2.1.3 :父类的指针/引用可以指向子类对象(但是只能访问子类从父类继承过来的成员,访问子类其它成员函数或者变量会出错)
2.1.4 :子类的指针/引用不能指向父类对象(因为当你用指针访问父类中子类特有的成员函数或者变量时,父类中没有就会非法访问,但是可以通过强制类型转换完成)
eg:
class base
{
public:
char* name;
int number;
}
class derived: public Base
{
public:
int IDcard;
}
base b;//基类
derived d;//派生类
b = d;//基类对象不能赋给派生类对象
d = b;//派生类对象可以赋给基类对象
//基类的指针或引用可以指向派生类对象
base *b1 = &d;
base &b2 = d;
//派生类的指针或引用不能指向基类对象(但可以通过强制转换)
derived *b3 = (derived*)&d;
derived &b4 = (derived&)d;
3.成员函数的重载、 覆盖和隐藏区别
3.1.、重载:
在同一个作用域里,函数名字相同,参数类型不同或者参数个数不同或者返回值不同,会构成重载。
c语言中函数名字相同是不能实现的,但是在C++中却可以,因为 c++ 中底层汇编语言中会将重载函数名映射为返回类型+函数名+参数列表。这样c++有同名函数时,链接时只要参数类型或者顺序不一样就可以调用
深入了解重载点击这里
3.2、重写(覆盖):
1. 不在同一个作用域(即一个父类一个子类)
2. 函数名相同/参数完全相同(包括类型和顺序)/返回值相同(协变除外)
3. 基类函数前必须有virtual。(即基类函数为虚函数)
3.3、重定义(隐藏):
1.在不同作用域里(即父类和子类)
2.子类和父类函数名相同,参数不同,在子类中父类的成员函数被隐藏
3.子类和父类函数名相同,参数完全相同,但父类函数没有virtual(即不是虚函数),父类成员函数被隐藏,切记不是重写
举个栗子:
class B
{
public:
void fun()
{
cout << "B::fun()" << endl;
}
void fun(int a)
{
cout << "B::fun(a)" << endl;
}
};
class D : public B
{
public:
void fun()
{
cout << "D::fun()" << endl;
}
};
class B
{
public:
void fun()
{
cout << "B::fun()" << endl;
}
void fun(int a)
{
cout << "B::fun(a)" << endl;
}
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
};
class D: public B
{
public:
using B::fun;//让class B类中所有fun的所有函数在D类中都可见,并且是public
void fun()
{
cout << "D::fun()" << endl;
}
virtual void fun1()
{
cout << "D::fun1()"<<endl;
}
};
class B
{
public:
void fun()
{
cout << "B::fun()" << endl;
}
void fun(int a)
{
cout << "B::fun(a)" << endl;
}
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
};
class D : private B
{
public:
void fun(int a)//转交函数
{
B::fun(2);//偷偷成为inline内联函数
}
void fun1()
{
cout << "D::fun1()" << endl;
}
}
总结:
1.子类继承父类时,它们有重名函数,子类成员函数将屏蔽父类对子类成员函数直接访问。
2.子类对象想要访问同名父类函数时可以通过显示调用,using表达式声和转交函数
3.但在实际使用时子父类最好不要用同名函数,以免出错。
4.派生类成员函数
子类继承父类,但是并不是父类所以成员函数和变量子类都可以继承,那么那些不能继承呢?
子类不能从父类继承的有:
1、构造函数
原因:子类创建对象时,需要调用父类的构造函数,如果你继承了父类的构造函数,就不能让子类的构造函数去初始化属于父类的那部分成员变量了,也就是说,子类里属于父类部分的成员函数必须由父类的构造函数亲自初始化,所以不能继承
2.、析构函数
原因:析构函数继承会构成重写。
3、赋值操作符=重载函数
原因:因为赋值操作符重载函数的作用是自己拷贝自己的类,如果继承下来,拷贝类型就对不上号了,就会出错。子类的构造函数应该在其初始化列表里显式的调用父类构造函数(除非父类构造函数不能访问)
- 如果父类是多态类,那么必须把父类析构函数定义为虚函数,因为这样就可以像其他虚函数一样实现动态绑定了,否则就会产生内存泄漏。
详细原因戳这里 - 在写子类的赋值函数时,注意不要忘记父类的数据成员重新赋值,这可以通过调用父类的赋值函数来实现
那么举个栗子吧
class Base
{
public:
Base(const char* name = "")
:_name(name)
{
cout << "Base()" << endl;
}
Base(const Base &b)
:_name(b._name)
{
cout << "Base(const Base &b)" << endl;
}
Base& operator = (const Base &b)
{
cout << "Base& operator = (const Base &b)" << endl;
if (this != &b)
{
_name = b._name;
}
return *this;
}
~Base()
{
cout << "~Base()" << endl;
}
string _name;
};
class Derived : public Base
{
public:
Derived(const char* name = "", int number = 0)
:Base(name)//显示调用构造函数
,_number(number)
{
cout << "Derived(const char* name="", int number=0)" << endl;
}
Derived(const Derived& d)
:Base(d)//显示调用拷贝构造
,_number(d._number)
{
cout << "Derived(const Derived& d)" << endl;
}
Derived& operator = (const Derived& d)
{
if (this != &d)
{
Base::operator=(d);//对基类的成员重新赋值
_number = d._number;
cout << "Derived& operator = (const Derived&d)" << endl;
}
return *this;
}
~Derived()
{
cout << "~Derived()" << endl;
}
int _number;
};
可以看出在派生类构造对象时,先构造基类成员函数再构造派生类成员函数,但析构时,缺失却是先析构派生类再析构父类。
5.菱形继承和虚继承
5.1、菱形继承
:两个子类继承父类,又一个子类同时继承上面两个子类
看图:
菱形继承存在问题:
5.1.1:数据冗余
5.1.2:二义性
eg:
class person
{
public:
int _name;//为了后续更好验证,把所有成员变量定义为int
};
class student :public person
{
public:
int _number;//学号
};
class teacher :public person
{
public:
int id;//学工号
};
class assistant :public student,public teacher
{
public:
int telephone;
};
解决办法:
1.指定作用域
void Test()
{
assistant a;
a.teacher::_name;
a.student::_name;
}
2.采用虚继承
class person
{
public:
int _name;
};
class student : virtual public person
{
public:
int _number;//学号
};
class teacher : virtual public person
{
public:
int _id;//学工号
};
class assistant :public student,public teacher
{
public:
int _telephone;
};
1.虚继承写时要注意: virtual public person这样写,如果写成virtual person,那就默认虚继承的私有继承了,你就不能访问父类成员变量
2.虚继承解决了菱形继承中的数据冗余造成的浪费空间问题
接下来分析虚继承怎么解决掉这些问题的
先看看虚继承后assistant a对象大小的改变
普通继承中,assistant继承teacher的8字节和student8字节,再加上自己的4字节,所以大小时20字节,那虚继承多出来的这四个字节是什么?我们来看看内存中的情况
可以看出每一次虚继承后,子类都会产生一个指针,指针指向一个虚基表,虚基表里的内容是一个偏移量,是子类对象实例化后通过自身地址加上这个偏移量找到存放继承自父类对象的地址,这样就可以找到里面的内容了。