每日复习笔记5.29

C++复习笔记

c++ primer笔记

面向对象编程

概述

面向对象的三个特征:继承,封装,多态

其中最关键的就是多态性

  1. 继承

可以通过继承定义这样的类,他们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。派生类能够继承基类定义的成员,可以无需改变的使用哪些与派生类型具体特性不相关的操作;还可以重定义那些与派生类型相关的成员函数,将函数特化;最后还可以继承了基类的成员之后额外定义更多的成员。

在C++中,基类必须指出希望派生类重定义哪些函数,定义为virtual的函数就是基类期待派生类重新定义的,这类函数叫做虚函数

  1. 动态绑定

个人认为多态性就是通过动态绑定体现出来的。
通过动态绑定,我们可以编写程序使用继承层次中任意类型的对象,无需关心对象的具体类型。使用这些类的程序无须区分函数实在基类中定义还是在派生类中。

例如我们可以如下编写print_total函数:

void print_total(ostream &os,
				 const Item_base &item,
				 size_t n)
{
	os << "ISBN: " << item.book()  //调用Item_base::book()
	   << "\t number sold: " << n << "\t total price:"
	   //调用虚函数:具体是那个版本运行时决定
	   << item.net_pirce(n) <<endl;
}

函数比较简单,就是格式化输出一些数据,但是有两点是需要注意的。
a)虽然这个函数的第一个形参是对于Item_base的引用,但是可以将item_base或者Bulk_item对象作为实参进行传递。

b)因为形参是引用而且net_price是虚函数,所以对net_price的调用将在运行时确定。具体调用哪个版本的net_price将依赖于传给print_total的实参。如果实参类型是一个Bulk_item,将运行Bulk_item中定义的应用了折扣的net_price;实参类型是一个Item_base,将运行Item_base中定义的应用了折扣的net_price。

注意:在C++中,通过积累的引用或者指针调用虚函数时,发生动态绑定,引用或者指针既可以指向基类对象,也可以指向派生类对象。这一事实是动态绑定的关键。此时调用的虚函数在运行时决定,被调用的函数是引用或指针所指向对象的实际类型所决定的。

定义基类和派生类
定义基类

一般来说,基类也有定义其接口和实现的数据成员和函数成员。例如下面定义的书店定价程序中,Item_base类定义了book和net_price函数以及存储每本书的ISBN和价格。

class Item_base{
	public:
		Item_base(
				  const string &book = " ",
				  double sales_price = 0.0):
				  isbn(book),price(sales_price)){}
		string book() const{
			return isbn;
		}
		virtual double net_price(size_t n) const{
			return n * price;
		}
		virtual ~Item_base(){}
	private:
		string isbn;
	protected:
		double price;
};

首先这个类定义了一个具有两个默认参数的构造函数,这个构造函数允许使用0个、一个、两个实参进行调用。
然后将析构函数和net_price函数定义为虚函数。

  1. 基类的成员函数

定义虚函数的目的就是启用动态绑定。成员函数默认为非虚函数,对于非虚函数的调用是在编译时就已经确定好了的。而且virtual关键字只能在类内部的成员函数声明中出现,不能用子啊类定义体外部出现的函数定义上。
基类通常会将在派生类中需要重定义的函数定义为虚函数。

  1. 访问控制和继承

在这里,派生类可以访问基类的public成员,但是不能访问private成员。特殊的,对于基类的protected成员,这部分可以被派生类访问,但是不能被该类型的普通用户访问。

protected成员

可以认为protected访问标号是对于public和private的混合:
相比于private成员,protected成员不能被类的用户代码所访问。
相比于public成员,protected成员可以被该类的派生类所访问。
派生类只能通过派生类的对象访问其protected成员,派生类对其基类类型的对象的protected成员没有特殊的访问权限。
(我对于这句话的理解就是,派生类对象可以访问对象内部的基类子对象的protected成员,而不能访问一个独立的新定义的基类对象的protected成员)。

派生类

为了定义派生类,需要使用类派生列表指定基类。形如

class classname: access-label base-class

这里access-label指的是访问控制符private、protected和public三种。

  1. 定义派生类

在之前定义的程序中,我们将从Item_base类派生出Bulk_item类。实际实现如下:

class Bulk_item: public Item_base{
	public:
		//这是声明而不是定义
		double net_prece(size_t n) const;
	private:
		string min_qty;
		double discount;
};

实际上,每个Bulk_item包含四个数据成员,两个是显式定义出的private成员min_qty和discount,另外两个是从基类中继承得来的isbn和price。

  1. 派生类和虚函数

一般来说,派生类会重定义所继承的虚函数,但是并不是必须这么做,如果没有重定义虚函数,则使用基类中定义的版本。

一旦函数在基类中被声明为了虚函数,它就一直为虚函数,派生类无法改变该函数是虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这么做。

  1. 派生类对象包含了基类对象作为子对象

  2. 派生类中的函数可以使用基类的成员

  3. 用作基类的类必须是已经定义过的

  4. 派生类的声明
    如果只需要声明一个派生类而不实现,则声明包含类名但是不包含派生列表。例如:

// error
class Bulk_item : public Item_base;
// right
class Bulk_item;
class Item_base;
virtual与其他成员函数

C++中的函数调用默认不使用动态绑定(因为函数默认定义为非虚函数)。要触发动态绑定需要两个条件:
·只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,不进行动态绑定。
·必须是通过基类类型的引用或者指针进行函数调用。

  1. 从派生类到基类的转换

因为每个派生类对象都包含基类的部分,所以可以将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。

double print_total(const Item_base&, size_t);
Item_base item;
//将Item_base的引用绑定到Item_base的对象上
print_total(item,10);
Item_base* p = &item;
Bulk_item bulk;
//将Item_base的引用绑定到Bulk_item的对象上
print_total(bulk,10);
//可以用Item_base类型的指针指向Bulk_item对象的Item—base部分
p = &bulk;

同一基类的指针既可以指向积累类型的对象,也可以指向派生类类型的对象。
因为可以使用基类类型的指针和引用来使用派生类对象,所以在使用基类的引用或者指针时,不知道指针指向或者引用绑定的具体类型。不过无论实际对象是哪一种类型,将其当作基类类型的对象总是安全的。

基类类型引用和指针的关键点在于静态类型(指针或者引用的类型,编译时可知的类型)和动态类型(指针或引用所绑定对象的类型,运行时可知的类型)可能不同。

  1. 可以在运行时确定virtual函数的调用

将基类类型的引用或指针绑定到派生类对象对原对象是没有影响的,对象本身不会改变,还是派生类对象。即对象的实际类型可能不同于该对象引用或者指针的静态类型。这就是C++中动态绑定的关键。也是多态性的体现。

  1. 编译时确定非virtual的调用

不论传给print_total的实参类型是什么,对于book的调用在编译时就确定喂Item_base::book。
即使Bulk_item定义了自己的book函数版本,这个低矮用也只会调用基类中的版本。

  1. 覆盖虚函数机制

某些情况下,希望屏蔽虚函数机制并强制函数使用虚函数的特定版本。这时可以使用作用域操作符:

Item_base *base_p = &derived;
double d = base_p->Item_base::net_price(42);

这里就强制将net_price的调用确定为Item_base中定义的版本。

派生类虚函数调用积累版本时,必须显式地的使用作用域操作符。如果不这样做,函数调用会在运行时确定这是一个自身调用,从而陷入无限的递归中。

公有、私有和受保护的继承

每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松所继承的成员的访问。

主要有以下三种继承方式:
a.公有继承public,基类成员保持自己的访问级别:基类的public成员石派生类的public成员,基类的protected成员为派生类的protected成员。
b.受保护继承,基类的public和protected成员在派生类中为protected成员。
c.私有继承,基类中的所有成员在派生类中为private成员。

  1. 接口继承与实现继承

(这里其实是不理解的,希望有懂的人看见帮我解答一下)

public派生类继承了基类的接口,它具有与基类相同的接口。通常被称为接口继承
private和protected派生的类不继承基类的接口,相反,这些派生通常被称为实现派生

派生类基本存在三种需求:
a. 只是从基类中继承了接口(函数声明)。纯虚函数
b. 从基类继承了接口和实现,而且可以重写这些函数。虚函数
c. 从基类继承了接口和实现,但是不能修改这些函数。非虚函数

  1. 默认继承保护级别

使用class保留字定义的派生类默认具有private继承;而用struct保留字定义的类默认具有public继承。

小tip:尽管私有继承在使用class保留字时是默认情况,但是实际情况下相对罕见,通常显式指定private比依赖于默认更合理,这样可以清楚知道是想要私有继承,而不是一时疏忽使用的默认情况。

友元关系与继承

友元关系是不能继承的。基类的友元对于派生类的成员没有特殊的访问权限。如果基类被赋予友元关系,则只有基类具有特殊访问权限,而该基类的派生类是不能访问授予友元关系的类。

转换与继承

每个派生类都包含一个基类部分,这意味着可以像使用基类对象一样在派生类上进行操作。因为派生类对象也是基类对象,所以存在从派生类型引用倒基类类型引用的自动转换。指针亦是如此。
但是反过来一个基类对象可能是一个独立对象,也可能是派生类对象的一部分,所以不存在从基类引用(或指针)到派生类引用(或指针)的自动转换

派生类到基类的转换
  1. 引用转换不同于转换对象

之前提到,可以将派生类对象传递给希望接受基类引用的函数。实际上,将对象传递给希望接受引用的函数时,引用就直接绑定到该对象,看起来传递的是对象,实际上实参是该对象的引用,对象本身没有被复制,这种引用转换是不改变派生类类型的对象。
但是,如果将派生类对象传递给希望接受基类对象(而不是引用)的函数时,情况就发生了变化。形参类型时固定的——编译和运行时都是基类类型对象。如果将派生类对象作为实参传递给这样的函数,则该派生类的基类部分会被复制到形参。

  1. 用派生类对象给基类对象进行初始化或赋值

用派生类对象对基类对象进行初始化或赋值时,一般有两种可能。
第一种可能性是,积累显示的定义了将派生类类型的对象复制或赋值给基类对象的定义,一般是通过定义适当的构造函数和赋值操作符实现的:

class Derivedclass Base {
	public:
		Base (const Derived&); //构造函数
		Base &operator = (const Derived&);  //赋值操作符
}

但是一般来说都不会发生第一种情况。更多的情况是,积累对定义自己的复制构造函数和赋值操作符,这些成员接受一个基类类型的引用。当使用派生类对象调用这些函数时,就会发生之前提到的派生类的引用向基类引用的转换,这样就可以使用派生类对象对基类对象进行初始化和赋值。

Item_base item;
Bulk_item bulk;
//初始化
Item_base item(bulk);
//赋值
item = bulk;

上面这段代码发生了以下的过程:
a. 将bulk对象转换为Item_base引用,是将一个Item_base引用绑定到了Bulk_item对象上。
b. 将该引用作为实参传递给复制构造函数或者赋值操作符。
c. 这些操作使用Bulk_item的Item_base部分分别调用构造函数或赋值操作符对Item_base对象的成员进行初始化或赋值。
d. 操作完成,对象即为Item_base。内容是Bulk_item中Item_base部分的副本,其余部分则被忽略。

  1. 派生类到基类转换的可访问性

要确定到基类的转换是否可访问,可以考虑基类的public成员是否可访问,如果可访问,则转换可访问;否则。转换是不可访问的

如果类是public继承,则用户代码和后代类都可以使用派生类到基类的转换。如果类是private或protected继承,则用户代码不能将派生类对象转换为基类对象。如果是private继承,则从private继承派生的类不能转换为基类对象。如果是protected继承,则后续派生的类可以转换为基类对象。

基类到派生类的转换

从基类到派生类的自动转换是不存在的。需要派生类对象时也不能使用基类对象。

构造函数和复制控制

构造函数和复制控制成员是不能继承的,每个类需要定义自己的构造函数和复制控制成员。

基类构造函数和复制控制

继承对于基类构造函数的唯一影响是,在确定提供哪些构造函数时,必须考虑一类新用户。可其他成员一样,构造函数可以是protected或private(虽然一般是public的),某些类需要只希望派生类使用特殊的构造函数,这样的构造函数应定义为protected。

派生类构造函数
  1. 合成的派生类默认构造函数

派生类的合成默认构造函数和一般的非派生的构造函数有一点不同:除了初始化派生类的数据成员之外,还会调用基类的默认构造函数初始化派生类对象的基类部分。

  1. 定义默认构造函数

一般来讲,如果派生类中有内置类型的成员,就应该定义自己的默认构造函数。

  1. 向基类构造函数传递实参

派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承来的成员。通常派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。

  1. 只能初始化直接基类

一个只能能初始化自己的直接基类。直接基类就是在派生列表中指定的类。

虚析构函数

删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。
如果删除基类指针,则需要运行基类的析构函数并清除基类的成员,如果对象时实际是派生类类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数就必须是虚函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因为指针所指向的对象类型的不同而不同。

在复制控制成员(复制构造函数、赋值操作符和析构函数)中,只有析构函数应该被定义为虚函数,构造函数不能定义为虚函数。构造函数事在对象完全构造之前运行的,在构造函数运行时,对象的动态类型还没有完全确定。

将赋值操作符设置为虚函数会令人混淆,因为虚函数必须在基类或派生类中具有相同的形参。基类赋值操作符有一个形参是对自身类类型的引用,如果该操作符是虚函数,则每个类都将获得一个虚函数成员,该成员定义了一个参数为基类成员的=。但是对于派生类而言,这个操作符和赋值操作符是不同的。

构造函数和析构函数中的虚函数

如果构造函数或者析构函数中调用了虚函数,则运行的是构造函数或析构函数自身类型定义的版本。

纯虚函数

纯虚函数的的定义形式:

class Disc_item : public Item_base{
	publicdouble net_price(size_t n) const = 0;
}

此时net_price函数就是一个纯虚函数,包含纯虚函数的类被称为抽象类。将函数定义为纯虚函数意味着,该函数为后来类型提供可可以覆盖的接口,但是这个类中的版本永远不会被调用(因为在这个类中的仅仅是定义,没有实现)。重要的是,用户也不能创建抽象类的对象。
抽象类只是作为基类派生出其他类,但是不能被实例化为对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值