C++函数进阶功能之类继承


以下未标注的都是C++特有的功能

1. 成员初始化列表的作用

成员初始化列表是在创建对象时进行初始化。它的应用场景如下:

class Test
{
private:
const int num;

public:
Test(int Input);
}
// not allowed
Test::Test(int Input)
{
num = Input;
}

如果用上面的构造函数进行初始化,是不允许的,因为私有数据是const变量,创建好对象后就不能对其进行修改。想要对私有数据中的const变量进行初始化,只能用成员初始化列表,而且这个方法只能用于构造函数,不能用于其他的成员函数。
成员初始化列表:

class Test
{
private:
const int num;
public:
Test(int Input);
}
// not allowed
Test::Test(int Input): num(Input)
{

}

2. 公有派生

如果想要定义一个类,继承上面的Test类,可以这样定义:

Class TestPlus1: public Test
{
private:
int numplus;
...
};

上面的public说明了类Test是一个公有基类,这种派生称为公有派生。它具有以下特点:

  1. 基类的公有成员(函数)成为派生类的公有成员(函数)
  2. 基类的私有部分只能通过基类的公有方法保护方法访问。
  3. 派生类TestPlus1需要有自己的构造函数,而且根据任务需要,可以额外地定义一些数据成员和成员函数。

2.1 派生类的构造函数

由于需要先创建基类,才能创建派生类,所以派生类的构造函数需要使用成员初始化列表+基类的构造函数,且非构造函数不可以使用初始化成员列表。此外,派生类的构造函数还需要初始化新增的私有数据,这个既可以用成员初始化列表,也可以用在派生构造函数内初始化。

TestPlus1:: TestPlus1(const int &a1, const int &a2):Test(a1)
{
numplus = a2;
};

或者

TestPlus1:: TestPlus1(const int &a1, const int &a2):Test(a1), numplus(a2)
{
};

如果省略上面的Test(a1),那么程序会调用默认基类构造函数。

2.2 派生类的析构函数

和构造过程相反,析构函数的调用是,先调用派生类的析构函数,再调用基类的析构函数。

2.3 派生类和基类之间的特殊关系

  1. 派生类可以用基类的方法,只要基类的方法不是私有部分即可。
  2. 对于指针和引用而言,一个基类对象可以指向派生类对象,然后调用派生类中的基类方法。但是反过来就不行,因为基类对象是数据少的那个,申请用派生类对象指向基类对象会因为数据的缺失而报错。
  3. 因为第二点的存在,所以如果在一个函数的形参中使用基类对象的引用(或者指针),那么传给这个函数的参数,既可以是基类对象,也可以是派生类对象。

2.4 公有继承的本质与缺陷

公有继承是一种is-a的关系。比如有一个基类Fruit,那么可以创建一个公有继承的派送类Banana,因为Banana是Fruit的一种。
但是对于其它的has-a, is-like-a, is-implemented-as-a, uses-a关系来说,公有继承并不是很好的方式。

2.5 多态公有继承

多态公有继承可以让同一成员函数,对于派生类和对于基类来说,行为是不一样的。
以下两种机制一起使用,就能实现多态公有继承:

  1. 在派生类中重新定义基类的方法
  2. 使用虚方法

虚方法介绍:

  1. 虚方法的使用需要在函数声明前加上关键词virtual,注意在函数定义中不需要加关键词virtual,但需要加作用域解析运算符(::)。如果基类派生类相同的两个函数,但是没有使用关键词virtual,那么程序会根据引用类型或者指针类型来决定使用哪个函数,又由于派生类的指针和引用不能指向基类,所以此时通常是调用基类的成员函数
  2. 如果基类派生类相同的两个函数,且都使用关键词virtual,那么会根据引用或者指针指向的对象类型来决定调用基类的函数还是派生类的函数。
  3. 综合前面两点来看,如果要在派生类中重新定义基类的方法,那么应该在基类中将方法声明为虚方法(即加上关键词virtual),这样派生类的同一方法将自动变成虚方法,但是在派生类中加一个virtual额外地声明一下,也可以,起强调和提醒的作用。
  4. 在基类中需要声明一个虚析构函数,这样在对基类指针数组(一个数组里全是指向基类的指针)进行内存释放时,可以合理地选择基类析构函数 or 派生类析构函数。

关于虚函数(方法)的几点注意事项

  1. 构造函数不能是虚函数,因为派生类和基类的构造函数不是一个函数。
  2. 析构函数一定要是虚函数,除非这个类不用做基类,只要这个类用作基类,那么就一定要在它的析构函数前面加一个virtual关键词。即使不是基类,也可以加,只是效率上的问题,不加执行效率更高。
  3. 只有成员函数才能是虚函数,因此友元不能是虚函数。
  4. 派生类中没有重新定义的函数,将使用基类的版本。
  5. 如果派生类中定义了基类中已经有的函数,只要两者函数名相同,那么就不会进行重载,派生类的对象只能调用派生类的函数,基类的同名函数被覆盖。如果在基类中,就定义了多个同名函数进行重载,那么在派生类中只要定义了一个这样的同名函数,基类所有的同名函数全被覆盖,要想使用基类的同名函数,必须在派生类中重新定义所有的基类版本的同名函数。

3. 静态联编和动态联编

函数名联编:在函数中调用其它函数,把其他函数解释为特定的代码块的过程。
静态联编:在编译的过程中完成联编
动态联编:在程序运行时才能完成联编过程。在使用虚方法的时候,就需要动态联编

4. 保护成员:protected

  1. protected部分是一种介于private部分和public部分的状态。对于基类来说,protected和private是一样的,外部的函数都是无法直接访问其中的数据,只能通过public间接访问。对于派生类来说,protected中的数据是可以直接访问的,但是private的数据是不能访问的,此时protected的行为又像public。
  2. 对于数据成员来说,最好放在私有部分;对于成员函数来说,则可以根据需要,放在保护部分。

5. 抽象基类

  1. 有的基类方法并不完全地贴合派生类的性质,那么为了进一步地提炼基类和派生类的共同性质,可以用抽象基类来实现。
  2. 所谓抽象基类,就是在成员函数中至少加上纯虚函数(就是没有实现,只有定义的函数),这样的函数不仅需要在函数声明前加关键词virtual,还需要在函数声明的最后加上一个“=0”,例如:
class Test2
{
public:
virtual void Show() = 0;
}
  1. 当然纯虚函数也可以有具体的实现,这样在继承抽象基类的时候,可以调用纯虚函数。

6. 继承和动态内存分配

6.1 动态内存分配

  1. 当基类有动态分配内存(就是出现new和delete),而派生类 没有使用 new和delete,那么不需要额外地定义析构函数,复制构造函数,赋值运算符,直接用程序默认的即可。
  2. 当基类有动态分配内存(就是出现new和delete),而派生类 使用了 new和delete,那么需要额外地定义析构函数,复制构造函数,赋值运算符,进行深度复制。
  3. 具体来说派生类的析构函数只需要delete新增的new,而基类的动态内存,程序会自动调用基类的析构函数。
  4. 派生类的复制构造函数则需要用成员初始化列表的方法,来调用基类的复制构造函数,然后再在派生类的复制构造函数内定义深度的复制。
TestPlus::TestPlus(const TestPlus &t) : Test(t)
{
numplus = t.numplus;
strplus = new char[strlen[t.strplus]+1];
strcpy(strplus, t.strplus);
}
  1. 派生类的赋值运算符则是在函数内使用基类的赋值构造函数(即基类名 + 运算符:: + 函数名)来实现基类部分的深度复制,至于派生类的其他部分,需要另外书写。
TestPlus& TestPlus::operator=(const TestPlus &t)
{
if(this == &t)
	return *this;
Test::operator=(t); // 这条语句的意思等价于: *this = t; 就是把基类部分都深复制地复制到派生类的基类私有参数中
......
return *this;
}

6.2 友元的继承

在友元的继承过程中,如果要调用基类的友元函数,可以直接调用,但是需要强制类型转换,将派生类转换成基类,从而让程序知道去调用基类的友元函数。因为友元函数不是成员函数,所以不能使用作用域解析运算符。
例如:

//因为它是友元函数,所以不需要baseDMA::
std::ostream& operator<< (std::ostream &os, const baseDMA &bd)
{
...
return os;
}

std::ostream& operator<< (std::ostream &os, const lackDMA &ld)
{
os << (const baseDMA &)ld;
os << ....... << std::endl;
return os;
}


7. 类设计回顾

7.1 默认构造函数

作用:给只声明,没赋值的类对象赋初值。如:

Test a; //Test是一个类

原则:如果程序没有提供任何构造函数,编译器会提供一个默认构造函数,数据的初值都是随机的。一旦程序提供了构造函数,那么编译器不会提供默认构造函数,必须程序提供一个显示的默认构造函数,上面的代码才不会报错

7.2 复制构造函数

作用:将一个已有的对象的值赋给刚创建的对象。
这对应的场景有很多,如:

Test a = b; //将新对象初始化为一个同类对象
t1.show(t2); // show的原型是 void Test::show(Test t),这种按值传递对象的时候,也会调用复制构造函数,因为相当与初始化Test t = t2。
t3 = t4.edit(); // edit的原型是 Test Test::edit(),这种按值返回对象的时候需要创建一个临时对象,不妨记为tmp,假设EditRetrun是返回对象,那么就相当于Test tmp = EditRetrun; t3 = tmp;
//第四种情况是编译器生成临时对象。

原则:如果程序没有提供复制构造函数,编译器会提供一个复制构造函数,但是这是浅复制,最好程序定义一个深复制的复制构造函数。

7.3 赋值运算符

作用:将一个已有的对象的值赋给已有的对象。
原则:如果程序没有提供赋值运算符,编译器会提供一个赋值运算符,但是这是浅复制,最好程序定义一个深复制的赋值运算符。

7.4 构造函数

作用:创造对象,在创造的时候就给初值。(而默认构造函数是代码中不给初值,是函数给它赋初值)
原则:派生类需要用成员初始化列表来使用基类的构造函数。因为在没有基类对象之前,是不能有派生类对象的。

7.5 析构函数

注意delete构造函数中的new数据

7.6 转换函数

单值可以转换为类对象,类对象也可以转换为基本类型,这个都是需要程序定义的。

7.7 公有继承考虑的因素

  1. 构造函数、析构函数、赋值运算符,这三者都是不能被继承的。因为创建和消除对象是需要调用构造和析构函数的,对象不同,不能继承,而且在基类的析构函数中,还需要设置为虚方法。赋值运算符因为对象不同,所以最好要重新定义,不过因为基类指针或者引用可以指向派生类,所以基类可以赋值给派生类,而派生类要赋值给基类则需要另外定义函数。
  2. 友元函数因为不是成员函数,所以也不能被继承。可以通过强制类型转换的方式,使得派生类也能使用基类的友元函数。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值