总体目录
概述:通过继承,我们可以用原有类型来定义一个新类型,定义的新类型既包含了原有类型的成员,也能自己添加新的成员,而不用将原有类的内容重新书写一遍。原有类型称为“基类”或“父类”,在它的基础上建立的类称为“派生类”或“子类
1、继承的定义
1)当一个派生类继承一个基类时,需要在派生类的类派生列表中明确的指出它是从哪个基类继承而来的。类派生列表的形式是在类名之后,大括号之前用冒号分隔,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问修饰限定符,其形式如下:
2)派生类的生成过程包含3个步骤:
- 吸收基类的成员
- 改造基类的成员
- 添加自己新的成员
3)代码
->>子类为Point3D,继承父类Point的public
//子类为Point3D,继承父类Point的public
class Point3D
: public Point
{
public:
Point3D(int x, int y, int z)
: Point(x, y)
, _z(z)
{
cout << "Point3D(int,int,int)" << endl;
}
void display() const
{
print();
cout << _z << endl;
}
private:
int _z;
};
2、继承的局限
不论何种继承方式,下面这些基类的特征是不能从基类继承下来的:
构造函数
析构函数
用户重载的operator new/delete运算符
用户重载的operator=运算符
友元关系
3、派生方式对基类成员的访问权限
1)派生类继承了基类的全部成员变量和成员方法(除了构造和析构之外的成员方法),但是这些成员的访问属性,在派生过程中是可以调整的
2)派生(继承)方式有3种,分别是
public(公有)继承
protected(保护型)继承
private(私有)继承
3)通过继承,除了基类私有成员以外的其它所有数据成员和成员函数,派生类中可以直接访问。
①private成员是私有成员,只能被本类的成员函数所访问,派生类和类外都不能访问。
②public成员是公有成员,在本类、派生类和外部都可访问。
③protected成员是保护成员,只能在本类和派生类中访问,是一种区分血缘关系内外有别的成员。
4)总结:派生类的访问权限规则如下:
1.不管以什么继承方式,派生类内部都不能访问基类的私有成员。
2.不管以什么继承方式,派生类内部除了基类的私有成员不可以访问外,其他的都可以访问。
3.不管以什么继承方式,派生类对象除了公有继承基类中的公有成员可以访问外,其他的一律不能访问
4、派生类对象的构造
1)我们知道,构造函数和析构函数是不能继承的,为了对数据成员进行初始化,派生类必须重新定义构造函数和析构函数
2)由于派生类对象通过继承而包含了基类数据成员,因此,创建派生类对象时:
①系统首先通过派生类的构造函数来调用基类的构造函数,完成基类成员的初始化,
②而后对派生类中新增的成员进行初始化。
③派生类构造函数的一般格式为
3)对于派生类对象的构造,我们分下面4种情况进行讨论:
总结:
a)派有基无 √
b)派无基有 ×
c)派有基均 若想调用基类有参得再初始化列表
d)派有基有 必在初始化列表调基类有参
(1) 如果派生类有显式定义构造函数,而基类没有显示定义构造函数,则创建派生类的对象时,派生类相应的构造函数会被自动调用,此时都自动调用了基类缺省的无参构造函数。
class Base
{
pulbic:
Base()
{
cout << "Base()" << endl;
}
};
class Derived
: public Base
{
public:
Derived(long derived)
: _derived(derived)
{
cout << "Derived(long)" << endl;
}
long _derived;
};
void test()
{
Derived d(1);
}
(2) 如果派生类没有显式定义构造函数而基类有显示定义构造函数,则基类必须拥有默认构造函数。
class Base
{
pulbic:
Base(long base)
{
cout << "Base(long)" << endl;
}
private:
long _base;
};
class Derived
: public Base
{
public:
//没有显示定义那就是编译器合成
};
void test()
{
Derived d;//error
}
(3) 如果派生类有构造函数,基类有默认构造函数,则创建派生类的对象时,基类的默认构造函数会自动调用,如果你想调用基类的有参构造函数,必须要在派生类构造函数的初始化列表中显示调用基类的有参构造函数。
(4) 如果派生类和基类都有构造函数,但基类没有默认的无参构造函数,即基类的构造函数均带有参数,则派生类的每一个构造函数必须在其初始化列表中显示的去调用基类的某个带参的构造函数。如果派生类的初始化列表中没有显示调用则会出错,因为基类中没有默认的构造函数
class Base
{
pulbic:
Base(long base)
{
cout << "Base(long)" << endl;
}
private:
long _base;
};
class Derived
: public Base
{
public:
Derived(long base, long derived)
: Base(base)
, _derived(derived)
{
cout << "Derived(long, long)" << endl;
}
long _derived;
};
void test()
{
Derived d(1, 2);
}
4)虽然上面细分了四种情况进行讨论,但不管如何,谨记一条: 必须将基类构造函数放在派生类构造函数的初试化列表中,以调用基类构造函数完成基类数据成员的初始化。派生类构造函数实现的功能,或者说调用顺序为:
a)完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成。
b) 调用基类的构造函数完成基类成员的初始化。(基类先派生类完成构造函数和其基类成员的初始化)
c) 若派生类中含对象成员、const成员或引用成员,则必须在初始化表中完成其初始化。
d) 派生类构造函数体执行。(最后才有派生类构造函数)
5、派生类对象的销毁
1)当派生类对象被删除时,派生类的析构函数被执行。析构函数同样不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序是
①先执行派生类的析构函数,
②再执行基类的析构函数,这和执行构造函数时的顺序正好相反。
2)当考虑对象成员时,继承机制下析构函数的调用顺序:
- 先调用派生类的析构函数
- 再调用派生类中成员对象的析构函数
- 最后调用普通基类的析构函数
6、多基继承(多基派生)
1)C++除了支持单根继承外,还支持多重继承。那为什么要引入多重继承呢?
身兼数职
2)多重继承的定义形式如下:
下面我们举一个例子:
class Fairy
{
public:
void fly()
{
cout << " can fly.\n";
}
};
class Monstor
{
public:
void attack()
{
cout << " take an attack.\n";
}
};
class Monkey
: public Fairy
, public Monstor
{
public:
Monkey(const string & name)
: _name(name)
{
}
void print() const
{
cout << _name << " ";
}
private:
string _name;
};
void test()
{
Monkey sunWukong("Sun Wukong");
sunWukong.print();
sunWukong.fly();
sunWukong.attack();
}
6.1、多基继承的派生类对象的构造和销毁
1)多基派生时,派生类的构造函数格式如(假设有N个基类):
2)首先要执行所有基类的构造函数,再执行派生类构造函数中初始化表达式的其他内容和构造函数体。各基类构造函数的执行顺序与其在初始化表中的顺序无关,而是由定义派生类时指定的基类顺序决定的(执行顺序和基类名1、2、3等的顺序有关)
6.2、成员名冲突的二义性
1)在派生类中对基类成员的访问应当具有唯一性,但在多基继承时,如果多个基类中存在同名
成员的情况,造成编译器无从判断具体要访问的哪个基类中的成员,则称为对基类成员访问的二义性问题。
2)如下面的例子,我们先定义3个不同的类A、B、C,这3个类中都有一个同名成员函数print,然后让类D继承自A、B、C,则当创建D的对象d,用d调用成员函数print时,
class A
{
public:
void print()
{
cout << "A::print()" << endl;
}
};
class B
{
public:
void print()
{
cout << "B::print()" << endl;
}
};
class C
{
public:
void print()
{
cout << "C::print()" << endl;
}
};
class D
: public A
, public B
, public C
{
};
void test()
{
D d;
d.print();//error
d.A::print();//ok
d.B::print();//ok
d.C::print();//ok
}
6.3、菱形继承的二义性问题
1)而另外一个就是菱形继承的问题了。多基派生中,如果在多条继承路径上有一个共同的基类,如下图所示,不难看出,在D类对象中,会有来自两条不同路径的共同基类(类A)的双重拷贝。
2)出现这种问题时,我们的解决方案是采用虚拟继承。中间的类B、C虚拟继承自A,就可以解决了。至于背后到底发生了什么,待我们学了多态的知识后一起做讲解。
class A
{
public:
void setNumber(long number)
{
_number = number;
}
private:
long _number;
};
class B
: virtual public A
{
};
class C
: virtual public A
{
};
class D
: public B
, public C
{
};
int main(void)
{
D d;
d.setNumber(10);
return 0;
}
7、基类与派生类间的相互转换
1)“类型适应”是指两种类型之间的关系,说A类适应B类是指A类的对象能直接用于B类对象所能应用的场合,从这种意义上讲,派生类适应于基类,派生类的对象适应于基类对象,派生类对象的指针和引用也适应于基类对象的指针和引用
2)也就是说如果函数的形参是基类对象或者基类对象的引用或者基类对象的指针类型,在进行函数调用时,相应的实参可以是派生类对象:
①可以把派生类的对象赋值给基类的对象
②可以把基类的引用绑定到派生类的对象
③可以声明基类的指针指向派生类的对象 (向上转型)
class Base
{
public:
Base(long base)
{
cout << "Base(long)" << endl;
}
private:
long _base;
};
class Derived
: public Base
{
public:
Derived(long base, long derived)
: Base(base)
, _derived(derived)
{
cout << "Derived(long,long)" << endl;
}
private:
long _derived;
};
void test()
{
//定义基类对象
Base base(1);
//定义派生类
Derived derived(10, 11);
//可以把派生类的对象赋值给基类的对象
base = derived;//ok
//可以把基类的引用绑定到派生类的对象
Base &refBase = derived;//ok
//可以声明基类的指针指向派生类的对象 (向上转型)
Base *pBase = &derived;//ok
//derived = base;//error
//Derived &refDerived = base;//error
//Derived *pDerived = &base;//error
cout << endl << endl;
Base base2(10);
Derived derived2(20, 30);
//不安全的向下转型
Derived *pderived2 = static_cast<Derived *>(&base2);
pderived2->print();
cout << endl;
//安全的向下转型
Base *pbase3 = &derived2;
Derived *pderived3 = static_cast<Derived *>(pbase3);
pderived3->print();
}
8、派生类对象间的复制控制
1)基类的拷贝构造函数和operator=运算符函数不能被派生类继承,那么在执行派生类对象间的复制操作时,就需要注意以下几种情况:
- 如果用户定义了基类的拷贝构造函数,而没有定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。
- 如果用户重载了基类的赋值运算符函数,而没有重载派生类的赋值运算符函数,那么在用一个派生类对象给另一个已经存在的派生类对象赋值时,两对象间的派生类部分执行缺省的赋值行为,而两对象间的基类部分执行用户定义的重载赋值函数。
- 如果用户定义了派生类的拷贝构造函数或者重载了派生类的对象赋值运算符=,则在用已有派生类对象初始化新的派生类对象时,或者在派生类对象间赋值时,将会执行用户定义的派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,(这时,通常需要用户在派生类的拷贝构造函数或者派生类的赋值函数中显式调用基类的拷贝构造或赋值运算符函数。)
总结:(若自己派生类定义了赋值运算符函数,那就使用派生类的,否则使用基类的用户定义的重载赋值函数),或者是显示调用基类的赋值运算符函数,在=中用Base::operator=(rhs);//显式调用赋值运算符函数
2)代码
class Base
{
public:
Base(const char *data)
: _data(new char[strlen(data) + 1]())
{
cout << "Base(const char *)" << endl;
strcpy(_data, data);
}
Base(const Base &rhs)
: _data(new char[strlen(rhs._data) + 1]())
{
cout << "Base(const Base &)" << endl;
strcpy(_data, rhs._data);
}
Base &operator=(const Base &rhs)
{
cout << "Base &operator=(const Base &)" << endl;
if(this != &rhs)
{
delete [] _data;
_data = new char[strlen(rhs._data) + 1]();
strcpy(_data, rhs._data);
}
return *this;
}
~Base()
{
cout << "~Base()" << endl;
delete [] _data;
}
const char *data() const
{
return _data;
}
private:
char *_data;
};
class Derived
: public Base
{
public:
Derived(const char *data, const char *data2)
: Base(data)
, _data2(new char[strlen(data2) + 1]())
{
cout << "Derived(const char *, const char *)" << endl;
strcpy(_data2, data2);
}
Derived(const Derived &rhs)
: Base(rhs)
, _data2(new char[strlen(rhs._data2) + 1]())
{
cout << "Derived(const Derived &)" << endl;
strcpy(_data2, rhs._data2);
}
Derived &operator=(const Derived &rhs)
{
cout << "Derived & operator=(const Derived &)" << endl;
if(this != &rhs)
{
Base::operator=(rhs);//显式调用赋值运算符函数
delete [] _data2;
_data2 = new char[strlen(rhs._data2) + 1]();
strcpy(_data2, rhs._data2);
}
return *this;
}
~Derived()
{
cout << "~Derived()" << endl;
}
friend std::ostream &operator<<(std::ostream &os, const Derived &rhs);
private:
char *_data2;
};
std::ostream &operator<<(std::ostream &os, const Derived &rhs)
{
os << rhs.data() << "," << rhs._data2;
return os;
}
int main(void)
{
//定义Derived的对象d1
//两对象间的基类部分执行用户定义的基类拷贝构造函数,符合情况1
Derived d1("hello", "world");
cout << "d1 = " << d1 << endl;
//赋值函数
Derived d2 = d1;
cout << "d1 = " << d1 << endl;
cout << "d2 = " << d2 << endl;
//
Derived d3("guangdong", "shenzhen");
cout << "d3 = " << d3 << endl;
//符合情况2
//派生类的赋值构造函数缺省,基类会执行用户定义的赋值运算符函数
//有了<<后就变成情况3了
d3 = d1;
cout << "d1 = " << d1 << endl;
cout << "d3 = " << d3 << endl;
return 0;
}