《C++ Primer Plus》学习笔记 — 类继承知识补充
一、继承中的函数
1、派生类和基类的方法隐藏
如果我们在派生类中声明了与基类成员函数同名而函数签名不同的函数,那么基类的函数将会被隐藏。隐藏是指无法通过派生类对象获指针访问基类同名接口。
#include <iostream>
using namespace std;
class CLS_Base
{
public:
virtual~CLS_Base()
{
}
virtual void testVirtual()
{
cout << "testVirtual" << endl;
}
void testNonVirtual()
{
}
};
class CLS_Derived : public CLS_Base
{
public:
virtual void testVirtual(int a)
{
}
void testNonVirtual(int a)
{
}
};
int main()
{
CLS_Derived* p1 = new CLS_Derived();
p1->testVirtual(); // invalid
p1->testNonVirtual(); // invalid
}
对于虚函数,我们把它们保存在虚函数表中进行多态访问。从这个角度讲,派生类重写父类的虚函数实际是拷贝父类的虚函数表并覆盖函数签名相同的虚函数指针。
2、方法重定义的规则
(1)返回类型协变(covariance of return type)
派生类函数的返回值类型应该和基类函数相同。特别的,当返回值为引用或指针时,派生类函数所返回的指针和引用类型可以为基类函数返回值类型的派生类。
(2)如果基类函数声明被重载了,应该在派生类中重新定义所有的基类版本
3、继承中的友元函数
前面我们学习友元函数时讨论过产生这种机制的原因:对于私有变量的访问。那么如果是派生类中的友元函数想要访问基类的私有成员怎么办?需要基类也实现相同功能的友元函数。
class CLS_Base
{
private:
int m_iBasePrivate;
public:
friend const CLS_Base operator+(const CLS_Base& lhs, int _iPara)
{
CLS_Base ret;
ret.m_iBasePrivate = lhs.m_iBasePrivate + _iPara;
return ret;
}
};
class CLS_Derived : public CLS_Base
{
private:
int m_iDerivdePrivate;
public:
CLS_Derived() = default;
CLS_Derived( const CLS_Base& base) :
CLS_Base(base),
m_iDerivdePrivate(1)
{
}
friend const CLS_Derived operator+(const CLS_Derived &lhs, int _iPara)
{
CLS_Derived ret((const CLS_Base&)lhs + _iPara);
ret.m_iDerivdePrivate = lhs.m_iDerivdePrivate + _iPara;
return ret;
}
};
首先,想要实现友元函数的功能,我们不难看出最好的办法是:一个继承链中的类(或者至少到某个层次以上)都实现该友元函数。 因为派生类友元函数的实现非常依赖基类友元函数。当然,这个依赖源于友元函数所提供的对私有变量的访问特性。其次,对于上面的操作符友元函数,我们还做了一个特殊处理 — 为派生类添加一个以基类对象为参数的构造函数。
4、构造\析构函数的特殊性
(1)构造\析构函数和普通函数的区别
在C++中,我们一般不会强调构造函数和普通函数的区别;然而在JAVA中,构造器和普通方法是两个概念。那么反观C++中的构造函数,它和普通函数一样吗?答案显然是否定的。
区别一 — 在构造函数中,编译器需要分情况插入需要的代码。这包括基类构造函数的调用和class member object的构造函数调用等;在析构函数中,也需要插入对相应的析构操作。对于普通函数,编译器不需要做特殊处理。
区别二 — 构造函数中提供了初始化列表来初始化成员变量和基类成分。就像我们前面讨论过的,这是初始化,而不是赋值。
区别三 — 构造函数和析构函数是不能被继承的,因为其名字的特殊性。普通函数可以被其派生类继承。
(2)C++11中的构造函数继承
其实就是使用了using声明:
#include <iostream>
using namespace std;
class CLS_Member
{
public:
CLS_Member()
{
cout << "CLS_Member" << endl;
}
};
class CLS_Base
{
private:
int m_iPrivate;
public:
CLS_Base(int _iPara)
{
m_iPrivate = _iPara;
cout << "CLS_Base" << endl;
}
};
class CLS_Derived : public CLS_Base
{
private:
CLS_Member m_mem;
public:
using CLS_Base::CLS_Base;
};
int main()
{
CLS_Derived derived1(1);
}
这里有两点需要注意:
using声明并不意味着我们直接调用基类的构造函数。其构造过程如我们以前所讨论,只是在编译时会为我们生成一个和基类构造函数同参数的派生类构造函数。
如果使用的是基类的构造函数是无参版本,那么这样的得到的派生类构造函数会被其他显式声明的派生类带参构造函数所隐藏。如下:
class CLS_Base
{
public:
CLS_Base()
{
}
};
class CLS_Derived : public CLS_Base
{
public:
using CLS_Base::CLS_Base;
CLS_Derived(int)
{
}
};
int main()
{
CLS_Derived derived1; // invalid — no default constructor
}
二、非虚继承多态中类的构建析构过程和RTTI
派生类想要初始化基类的私有成员,只能通过直接调用基类的构造函数或者set接口实现。我们一般都会在初始化列表中直接调用我们需要的基类构造函数。那么我们可以了解下编译器在类的构造函数的行为,包括this指针到底是什么东西等等。以下我们需要借助RTTI实现类型的读取。
#include <iostream>
#include <rttidata.h>
using namespace std;
class CLS_Base;
void test(CLS_Base* _pThis);
typedef void (*vfunction)();
class CLS_Base
{
public:
string m_str;
public:
CLS_Base(const char* _pc):
m_str(_pc)
{
test(this);
}
virtual void testVirtual1()
{
cout << "testVirtual1" << endl;
}
virtual void testVirtual2()
{
cout << "testVirtual2" << endl;
}
};
class CLS_Derived : public CLS_Base
{
public:
CLS_Derived() :
CLS_Base(typeid(this).name())
{
test(this);
}
};
void test(CLS_Base* _pThis)
{
cout << typeid(_pThis).name() << " this = " << _pThis << " m_str = " << _pThis->m_str << endl;
cout << typeid(_pThis).name() << " typeid(this).raw_name() = " << typeid(_pThis