进一步走进C++面向对象的世界
Posted 易水南风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进一步走进C++面向对象的世界相关的知识,希望对你有一定的参考价值。
更多博文,请看音视频系统学习的浪漫马车之总目录
上一篇初尝C++的世界 虽然讲的很长,但是如同题目写的“初尝”一般,写的比较蜻蜓点水,简单讲了C++与C语言的一些不同点,这一篇将针对C++类与对象,开始探讨继承、多态相关内容。大家坐稳扶好,咱们继续出发~
继承
关于继承,C++程序员肯定是信手拈来了。 所谓继承,就是一个类从另一个类演化而来,在原来的类基础上添加一些成员和方法,并能访问原来的类中的成员和方法,被继承的类叫做父类或者基类,继承的类叫做子类或者派生类。继承的作用从代码角度来说就是实现了代码的复用,父类的代码可以被多个子类共用,从而节省了多余代码。从宏观的开发思想来说,继承是面向对象的一种升级,对原来对事物进行分类的详细化,更接近人的不仅擅长归类,而且又擅长对同一类事物进行衍生变化(或者更细分)的思维习惯。不仅仅划分为暴龙兽和加鲁鲁,暴龙兽还可以升级为机械暴龙兽和战斗暴龙兽即拥有升级前的战斗技能,又添加了新的技能,然而不管怎样变,它都是暴龙兽,所以这又为后面的多态做了基础。
比如有动物类:
class Animal {
private:
char * name;
protected:
int age;
public:
char *getName();
int getAge();
};
然后创建Dog类继承Animal :
class Dog : public Animal {
private:
char *noseColor;
protected:
char *tailColor;
public:
void eat();
};
这样子,Dog不止可以调用自己的eat方法,还可以调用父类的getName方法。
Dog dog;
//调用父类的方法
dog.getName();
dog.eat();
当然不能说Dog就可以拥有Animal的所有成员的方法了,继承要注意的就是权限问题,拥有了不代表一定能用(对方法来准确来说是拥有其调用权),毕竟父类也是有隐私的,它自己有权利决定哪些可以给子类使用,这也是面向对面重要特点之一:封装。显然,父类的private修饰的成员或者方法就不给子类使用,毕竟是私有的,protected修饰的本身就是专门提供给子类用的,所以子类可以用,public是完全公开的,显然子类也是可以使用的。注意这里只是父类加这些修饰关键字的意图,C++和Java的不同在于子类本身还可以决定自己对父类的成员或者方法的控制力有多大,注意到类声明开始处的“class Dog : public Animal ”,中间多加了个“public”,这个就是继承方式,所以子类最终可以使用父类成员或者方法的权限由父类的修饰符和继承方式共同决定,具体看下方:
-
public继承方式
基类中所有 public 成员在派生类中为 public 属性;
基类中所有 protected 成员在派生类中为 protected 属性;
基类中所有 private 成员在派生类中不能使用。 -
protected继承方式
基类中的所有 public 成员在派生类中为 protected 属性;
基类中的所有 protected 成员在派生类中为 protected 属性;
基类中的所有 private 成员在派生类中不能使用。 -
private继承方式
基类中的所有 public 成员在派生类中均为 private 属性;
基类中的所有 protected 成员在派生类中均为 private 属性;
基类中的所有 private 成员在派生类中不能使用。
咋一看挺复杂,不知如何记忆,其实一言以蔽之:
继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的(比其高的则降为它,低的则保持原来的权限)
是不是就豁然开朗了呢?
比如将Dog的继承方式改为private:
class Dog : private Animal {
private:
char *noseColor;
protected:
char *tailColor;
public:
void eat();
};
则以下语句会报错,因为此时Animal的所有成员和函数都对于Dog来说在父类是private,所以getName函数对于Dog来说是不可调用的。
dog.getName();
继承中的构造函数和析构函数
子类是能够继承父类所有的方法(准确来说是获得其调用权),但是有个特殊方法是不能被继承的,它就是构造函数。构造函数是在创建对象的时候自动调用的,所以继承父类的构造函数是没有意义的。但是子类从父类继承过来的成员在子类创建对象的时候依然是需要初始化的,初始化逻辑依然在父类的构造函数中即使想要在子类中初始化也会可能因为权限问题而访问不到成员,所以为了使得父类的成员能够在子类创建对象的时候得到初始化,这里需要在子类的构造函数中手动调用父类的构造函数。
例如在上面例子中给Animal添加一个构造函数:
Animal(char * name);
再给Dog添加构造函数的时候,类似初尝C++的世界 所描述的初始化列表,这里要求要调用Animal的构造方法,不然编译会报错(clion会报错:“Constructor for ‘Dog’ must explicitly initialize the base class ‘Animal’ which does not have a default constructor”):
Dog::Dog(char *noseColor) : Animal(noseColor) {
}
没错,和Java构造方法第一行要主动调用super.父类构造方法其实一个道理,目的是一样的。
另外要注意的一点就是继承中构造方法和析构函数的调用顺序:
Dog *dog = new Dog("aass");
delete dog;
结果是:
Animal构造函数
Dog构造函数
Dog析构方函数
Animal析构方函数
从这个小例子可以清晰看出,构造函数是先调用父类再调用子类,而析构函数则相反。想想也有道理,子类继承父类,父类就是其依赖,肯定是先初始化被依赖的部分。而析构当然相反,肯定是被依赖的部分后面释放。
多继承
C++多继承看似强大,但因其复杂混乱坑多,所以实则广为程序员所诟病,以至于后世的Java、C#等后辈皆断然弃多继承而去,不过既然作为C++的一种特性,这里还是要有所提及。
多继承,顾名思义,即一个类继承多个类。
假设增加一个Cat类继承Animal:
class Cat : public Animal {
private:
char *noseColor;
protected:
char *tailColor;
public:
void eat();
Cat(char *noseColor);
~Cat();
};
在增加一个Pet类同时继承Dog和Cat:
class Pet : public Dog, Cat{
public:
Pet(char *noseColor1, char *noseColor);
void howOld();
};
是的,一个最简单的多继承类就写完了,因为是多继承,所以根据上面所说,这个类的构造方法应该要同时调用2个父类的构造方法:
Pet::Pet(char *noseColor1, char *noseColor) : Cat(noseColor1), Dog(noseColor) {
}
当然这不是关键,多继承比较麻烦的是二义性,即使用了父类们同名的成员或者方法,以至于编译器不知道究竟是使用哪一个,所以还要专门特定指定是哪个父类的:
假设在Pet实现howOld方法:
void Pet::howOld() {
std::cout << "age" << age << std::endl;
}
这里是编译不过的,因为现在Pet继承Dog和Cat,Dog和Cat又继承Animal,所以形成了一个菱形的继承关系:
因为在C++多继承中,即使是同个父类的成员也会存在多份,即基类的每条路径都会有一份成员数据,所以Pet中使用的age不知道是从Pet–Dog–Animal,还是Pet–Cat–Animal路径继承来的那一份。
解决方案也是so easy的,不知道就指明哪个父类呗~~
void Pet::howOld() {
std::cout << "age" << Dog::age << std::endl;
}
加上类名和域解析符::就可以很清楚解决了。
虚继承
上面谈到多继承会存在二义性的问题,那C++创造者觉得不方便,索性也从语法角度消除了这个问题,那就是虚继承。
首先在Dog和Cat的定义处做一点小小的调整,在继承修饰符前加上“virtual”关键字:
class Dog : **virtual** public Animal ```
```cpp
class Cat : **virtual** public Animal
然后发现Pet即使没有指定age也不报错了:
void Pet::howOld() {
std::cout << "age" << age << std::endl;
}
为啥呢?其实很简单,之前说二义性问题的根源是因为多继承使得基类的每条路径都会有一份成员数据,虚继承就是让不同的继承路径下共享基类的数据,即基类的成员数据保持一份。
可以简单通过以前程序验证:
void Pet::howOld() {
Cat::age = 10;
Dog::age = 20;
std::cout << "age:" << age << std::endl;
}
age:20
即不管是Cat::age还是Dog::age,其实是同一个age。
因为多继承的路径和层次一旦复杂,调试和维护工作会变得很困难,所以被很多高级语言放弃了,所以这里也只是简单介绍,还是进入下一个更加值得我们细细研究的课题吧。
多态
一直觉得多态是面向对面的精华之处,有了多态,可以非常灵活第在运行时对对象的指针进行赋值,可以通过抽象类(Java还可以用接口)实现对类的解耦,使得系统松耦合,扩展性强。
有了多态,我们就可以在被调用的代码块中持有基类指针去调用基类某个方法,但是实际上该基类指针指向的对象是在运行时才确定的,所以决定此时调用的是哪个对象的方法取决于外部怎么给这个基类指针赋值对象,也是典型的依赖注入。
还是上面的例子(Pet继承Dog和Cat,Dog和Cat又继承Animal),给Animal增加方法whoAmI:
char* whoAmI();
实现为:
char *Animal::whoAmI() {
return "Animal";
}
Pet、Dog、Cat分别重写该方法:
Cat:
char *Cat::whoAmI() {
return "Cat";
}
Dog:
char *Dog::whoAmI() {
return "Dog";
}
Pet:
char *Pet::whoAmI() {
return "Pet";
}
假如这里这样写:
Animal *animal = new Cat("aa");
std::cout << animal->whoAmI() << std::endl;
打印结果会怎样呢?
熟悉Java的同学一定脱口而出:Cat
恭喜你,答错了~~
看下结果:
Animal
不是说好的多态么?这样调用的还是父类方法,那还有何意义?
其实调用基类指针调用基类方法,是一种很自然的方式,只是熟悉Java的童鞋思维已经习惯了,所以现在这样反而不习惯。所以C++要真正实现多态,务必使用一个关键字virtual 来指定方法是一个可以使用多态的方法,即虚函数。
为啥Java不用指定virtual 就可以直接使用多态呢,可以参考下这篇文章Virtual Function in Java
其实这要看这一句就够了:
By default, all the instance methods in Java are considered as the Virtual function except final, static, and private methods as these methods can be used to achieve polymorphism.
我们知道Java是C++的精简版,所以Java觉得没有必要脱掉裤子放屁,调用方法的是什么对象就调用这个对象的方法,所以方法直接就默认为虚函数,除了 final, static, private修饰的函数以外,因为这些函数也无法被派生类重写,所以没有成为虚函数的必要。
所以上面例子如何让我们的小喵咪(Cat)的whoAmI方法能够被正确调用呢?很简单,在基类whoAmI方法的声明处加上virtual即可(在Cat的whoAmI方法加也一样,因为如果基类方法是虚函数,那派生类重写的方法也是虚函数):
virtual char* whoAmI();
运行代码:
Cat
这样子就实现了多态,即调用Animal指针的whoAmI方法,真正调用的是Cat对象的whoAmI方法(Pet、Dog亦然)。
再来看下多态具体的好处:
假如有个类People:
class People {
Animal *animal = nullptr;
//注入具体的动物品种
void setAnimal(Animal* animal);
//让动物告诉我它品类
void animalTellMeYourName();
};
实现:
void People::setAnimal(Animal *animal) {
this->animal = animal;
}
void People::animalTellMeYourName() {
this->animal->whoAmI();
std::cout << "I am:" << this->animal->whoAmI() << std::endl;
}
这里我们只知道人 持有一只动物,但不知道是什么动物。这样就为程序提供了灵活性,外部使用People类的时候,可以根据具体场景给他不同的动物,而不需要修改People类的代码。
假如具体场景需要给People一只小猫咪,则:
People *people = new People();
Cat *cat = new Cat("aa");
//给people一只小猫咪
people->setAnimal(cat);
//让people持有的动物告诉我它品类
people->animalTellMeYourName();
运行结果:
I am:Cat
换为给People注入一只Dog,也是一样的,只要保证whoAmI方法是虚函数即可。这样以后外部只要确保给People的是一只Animal即可,具体是一只什么Animal,People不care,所以说这个People是和外部类松耦合的,并没有写固定什么Animal。
关于虚函数实现的原理,会在介绍完C++语法之后的原理博文中详细介绍,敬请期待哈哈。
以上是关于进一步走进C++面向对象的世界的主要内容,如果未能解决你的问题,请参考以下文章