《C++ Primer》学习笔记 (第十五章)——面向对象程序设计

面向对象程序设计

(本章的知识点比较多,同时也很重要,但是书上讲解顺序感觉有点乱,所以我自己尽量对本章的内容进行归纳总结,并没有按照书上的顺序进行复习。)

面向对象程序设计的核心思想是:数据抽象、继承、动态绑定,相应的面向程序设计的特征就是:封装、继承、多态。

一、基类
1、首先,基类中有两种不同类型的成员函数,一种是与派生类类型无关的,派生类直接继承不需要改变的成员函数,另一种成员函数基类希望派生类能够对其进行覆盖,不同的派生类类型调用该种函数的结果不同,这种函数基类将其定义为虚函数,当我们使用基类的指针或者引用调用虚函数时,根据指针或引用所绑定的对象类型不同,执行相应的版本的函数,这就是动态绑定。注意:只有当使用基类的指针或者引用调用虚函数时,才会发生动态绑定引用或指针的静态类型和动态类型不同这一特点正是c++多态性的根本所在
举个栗子:

class Animal {  //定义基类Animal
public:
	Animal() = default;   //默认构造函数
	Animal(string s, int i) :name(s), age(i) {};  //含有两个参数的构造函数
	void show(){  //普通成员函数
	cout<<name<<" "<<age;
	}
	virtual void Speak() {   //成员函数,由于我们希望不同派生类对象(不同动物)叫声不一样,因此定义为virtual
		cout << "动物说话" << endl; //基类版本的Speak函数
	}
	virtual ~Animal() {};  //虚析构函数,后面再见解为什么要声明为虚析构函数
protected:  //保护成员,外界不能访问,但是派生类的成员可以访问
	string name="";
	int age=0;
};

class Dog: public Animal {  //派生类Dog公有继承Animal类
public:
	Dog(string s, int i) :Animal(s, i) {};  //构造函数,调用基类的构造函数执行基类数据成员的初始化
	void Speak() {  //Dog类版本的Speak函数,覆盖了基类版本
		cout << "汪汪" << endl;
	}
};

class Cat : public Animal {  //派生类Cat公有继承Animal类
public:
	Cat(string s, int i) :Animal(s, i) {};   /构造函数,调用基类的构造函数
	void Speak() {  //Cat类版本的Speak函数,覆盖了基类版本
		cout << "喵喵" << endl;   
	}
};

int main()
{
	Animal animal;  //定义一个Animal类对象,调用默认构造函数
	Dog dog("小狗", 2);  //定义Dog类对象
	Cat cat("小猫", 2);  ..定义一个Cat类对象
	Animal* A;  //声明一个指向Animal类的指针
	A = &animal;   //指针指向Animal类对象
	A->Speak();  //由于指针绑定的是Animal类对象,因此调用基类Animal版本的speak函数
	A = &dog;  //将指针指向Dog类对象
	A->Speak();  //由于指针绑定的是Dog类对象,因此调用派生类Dog版本的speak函数
	A = &cat;  //将指针指向Cat类对象
	A->Speak();  //由于指针绑定的是Cat类对象,因此调用派生类Cat版本的speak函数
    return 0;
}

上述例子就涉及到本章的许多内容,如:虚函数,动态绑定、虚析构函数、派生类的构造函数、派生列表中的访问说明符以及派生类向基类的类型转换。接下来一一进行见解。

2、首先观察上述代码,其实Animal类里面的speak虚函数实现是没有意义的,或者说如果我们声明ygAnimal类的对象没有任何意义,因为"动物"是一个抽象的概念,我们可以实例化一个“狗”或者"猫",但是实例化一个“动物”仿佛有些奇怪。那么我们可以把Animal中的虚函数speak声明为纯虚函数,声明形式为在虚函数后加上“=0”,=0只能出现在类内部虚函数声明语句处。如:

void speak()=0;  //纯虚函数

一个纯虚函数无须定义,因为声明纯虚函数就是声明它是无实际意义的。含有(或未经覆盖直接继承纯虚函数)的类为抽象基类,我们不能创建抽象基类的对象

二、 动态绑定
动态绑定指当使用基类的指针或引用调用一个虚函数时,会根据指针或引用所绑定的对象类型调用该类型版本的虚函数。因此,动态绑定有两个不可缺少的条件:①基类的指针或引用;②虚函数。如上述代码所示:我们声明了一个基类Animal的指针类型,当该指针绑定到Animal类时,就调用Animal类的speak函数;当指针绑定到Dog类对象时,就执行Dog类的speak函数;当指针绑定到Cat类对象时,就执行Cat类的speak函数。这就是动态绑定,调用哪个版本的虚函数与指针类型无关,而与指针所绑定的类型有关。由此可以得出另一个结论:普通的成员函数在编译是被解析,而虚函数的调用在运行时才被解析

三、虚函数
1、前面也提到过,基类有两种成员函数,一种是“固定”的成员函数,这种成员函数与派生类的类型无关,无论派生类是什么类型,执行效果相同,如上述代码中的show函数,无论派生类是狗还是猫,都打印名字和年龄。另外一种成员函数基类希望不同的派生类调用的结果不同,这类成员函数可以声明为虚函数。如上述代码中的speak函数,因为不同派生类狗和猫的叫声不同,因此不同派生类调用该函数的执行效果不同,所以speak函数被声明为虚函数。

2、任何除构造函数以及静态函数(静态函数在编译时确定,而虚函数只有在运行时确定)以外的成员函数都可以被声明为虚函数,即在函数前加上关键字virtual。virtual只能出现在类内部的声明语句之前,不能用于类外部的定义

3、如果基类声明了一个虚函数,那么该函数在派生类中也是隐式的虚函数,此时可以在派生类内部加上virtual也可以不加。

4、派生类通常会覆盖从基类中继承的虚函数,因为虚函数的目的就是让派生类定义自己版本的函数,但是派生类也可以不用覆盖继承的虚函数,这种情况下,派生类会直接继承基类中的版本。如上述代码中,类Dog内如果没有speak的定义,即没有覆盖基类中的虚函数speak,那么即使指针绑定到Dog类对象,执行的是基类Animal版本的speak。另外派生类可以在相应的成员函数最后(如果是const函数,那么在const后面)加上关键字override来显式注明覆盖了某一个虚函数,好让编译器进行检查。

5、如果派生类想要覆盖基类中的虚函数,那么派生类中的函数的返回类型和形参列表必须和基类中的虚函数完全一致,否则编译器就会认为派生类定义了一个新的函数,并且该函数与基类中的虚函数无任何关系。(注意:这种情况并不构造重载,重载必须在同一作用域类),举个栗子:

class Fruit{
public:
virtual void f();
}

class Apple: public Fruit{
public:
     ①、void f(); //正确,覆盖基类中的虚函数f()
     ②、void f() override;//正确,覆盖基类中的f()
     ③、void f(int) override; //错误,override会进行检查,该函数与基类中的虚函数形参列表不同
     ④、void f(int); //正确,声明了新的函数f(int),同时该类中还存在从基类中继承得到的虚函数f().不过该虚函数被隐藏了,如果需要调用必须显示调用(Fruin::f())
}

如上述代码所示:其中①②正确,是对基类虚函数的覆盖,而③声明为了override表示这是一个覆盖基类虚函数的函数,而编译器通过检查发现基类没有这样一个虚函数,因此发生错误。④形参列表与基类中的虚函数不同且没有显式声明为覆盖函数,因此编译器为认为该函数为一个新函数,二基类中的虚函数f()在派生类中被隐藏了。

6、注意覆盖隐藏的区别,个人理解为:覆盖一般是对虚函数进行覆盖,而隐藏是对包含虚函数在内的所有所有成员函数进行隐藏,派生类成员将会隐藏基类中的同名成员。在派生类中被隐藏的函数只能显式调用或者通过基类调用隐藏的虚函数,如:

class A {
public:
	 void f()
	{
		cout << "A" << endl;
	}
};
class B : public A {
public:
	void f(int i) {
		cout << "B" << endl;
	}
};

B b;
b.f();  //错误,B类中没有无形参的f函数,基类中的f()被隐藏了
b.A::f();// 正确,通过作用域限定符能够显示调用被隐藏的同名函数

注意:上述代码中基类中的函数f()和派生类中的f(int)并不会构成函数重载

7、如果不想基类中的某个函数被派生类给覆盖掉,可以在该函数后面加上关键字final,将函数定义为final后,之后任何尝试覆盖该函数的操作都将引发错误。另外,为了防止覆盖,除了覆盖继承而来的虚函数以外,最好不要在派生类中定义在基类中的名字

8、如果不希望某个类被继承,同样可以在该类名字后面加上关键字final,这样编译器就不允许其他类继承该类:

class Base final  {//};//Base类不能被继承

四、函数调用的解析过程
写了这么多,感觉越写越乱啊,什么覆盖啊,隐藏啊,什么时候可以调用什么时候不可以调用感觉很难去记忆。但是了解了函数调用的解析过程后,我感觉会容易理解一点:
当我们使用指针、引用调用函数时(p->f()),或者使用对象调用函数时(obj.f())函数调用的解析过程分一下四个步骤:
①、首先确定p或者obj的静态类型;
②、在该静态类型中进行查找相应的名字为f的函数,如果没有找到,则继续在该类的基类中进行查找,一直到继承链的顶端,如果还是没有找到,那么编译器就会报错。
③、如果找到了名字为f的函数,那么就进行类型检查(名字查找先于类型检查,一旦名字找到,那么编译器将不再进行查找而是进行类型检查),以便确认此次调用是否合法;
④、在调用合法后,如果该函数为虚函数并且我们是通过指针或者引用进行的调用,那么就进行动态绑定,根据指针或引用绑定的类型,调用相应版本的虚函数。如果该函数不是虚函数或者我们是通过对象进行调用,那么就进行常规的函数调用。

五、派生类
1、struct默认的继承保护级别为:public公有继承,class的默认继承保护级别为:private私有继承。

2、友元关系不能继承,基类的友元在访问派生类时不具有特殊性,派生类的友元也不能随意访问基类中的成员。

5.1、派生类成员以及派生类对象的访问权限
关于访问权限,首先需要搞清楚是成员的访问权限,还是类对象的访问权限。
①、首先对于只有一个类来讲,类的成员可以访问类中任意成员(public,protected,private),对于该类的对象来讲,只能访问公有成员(public)

②、对于具有继承关系的类来讲,基类的情况与①一样。对于派生类来讲,派生类的成员可以访问基类中公有成员和保护成员(public/protected)不能访问私有成员(provate),派生类的对象的访问权限与继承保护级别有关。对于public继承,基类中的public和protected成员在派生类中仍然是public和protected,派生类对象只能访问基类中的public成员;对于protected继承,基类中的public和protected成员在派生类中仍然是public和protected成员在派生类中都是protected,派生类对象不能访问基类中如何成员;对于private继承,基类中的public和protected成员在派生类中都是private,派生类对象同样不能访问基类中的任何成员。

总结:派生类中成员只能访问基类中public和protected成员,继承保护级别与派生类的成员访问权限无关。继承保护级别只影响派生类对象对基类中成员的访问权限。一定要分清是类成员的访问权限还是类对象的访问权限

六、继承中的构造函数和拷贝控制

6.1、虚析构函数
如在开头提到过,我们一般会为基类声明一个虚析构函数,这与动态绑定有关,比如我们声明一个Animal类型的指针,但是指向Dog类型的对象,当执行析构时,由于析构函数是虚函数,所以编译器就会执行Dog版本的析构函数,从而正确析构Dog对象。如果基类的析构函数不是虚函数,那么编译器就会通过判断指针类型而直接调用基类的析构函数,而delete应该指向派生类对象的基类指针可能产生错误。

6.2 派生类的析构函数只负责销毁有派生类直接分配的资源,而对象的基类部分被隐式销毁。而当派生类定义了拷贝或者移动操作时,该操作负责拷贝或者移动包括基类部分成员在内的整个对象。对象的销毁顺序与创建顺序相反,派生类析构函数首先执行,然后是基类的析构函数,以此类推。

6.3 继承的构造函数
一个类只初始化它的直接基类,因此一个类只继承其直接基类的构造函数。但是派生类并不直接初始化基类中的成员,派生类必须使用基类的构造函数初始化它基类部分的成员,每个类只控制它自己成员的初始化。首先初始化基类部分,然后按声明顺序依次初始化派生类之间的成员。
举个栗子:

class A {
public:
	A(int i, string s) :num(i), name(s) {};  //基类的构造函数
	virtual~A(){};
private:
	int num;
	string name;
};
class B : public A {
public:
	B(int i, string s, double d) :A(i, s), height(d) {};  //B类的构造函数,在初始化部分使用了基类自己的构造函数初始化基类自己的成员
private:
	double height;
};

本章内容到此结束,本章的知识点很多而且很重要,还有很多细节的内容没有记录下来,有时间还是多看看书。暴晒了很长时间的北京,今天终于下雨了,然而我没有带伞~~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值