这篇文章将帮你解决的问题:
- 什么是编程
- 什么是面向对象编程
- 为什么要使用面向对象思想
- 除了对象+方法,还需要别的吗
本文主要从应用者的角度出发,来一起探讨面向对象思想是如何产生及实现的。本文假定读者有基础的编程语言知识,了解基本的变量,条件和循环。
程序 = 数据结构 + 算法
春天到了,张三想要去春游。那么张三面临的第一个问题就是怎么去,他需要对这次旅行提前做个计划,而为春游做的旅行攻略就是程序,最旅行攻略的过程便是编程了。这不仅仅是一个类比,程序是后可能在类似的使用场景中被遇到的,比如在一个旅游app中,就可能需要自动为游客生成旅游路线或参考行程表。
那这和上面的程序=数据结构+算法有什么关系呢?上面提到旅游攻略便是程序,那么考虑下旅游攻略都会怎么写,大概率会是这样子,3:20 入住酒店 4:00 去第一个景点打卡 6:00 和朋友一起吃晚饭等… 其中入住酒店,景点游玩,吃饭等都是我的行为,或者说是一系列规定的动作,这边是算法。数据结构便是隐藏在旅游攻略中的主体‘我’,为了描述主体‘我’,程序表述可能会是这样,这些基础数据的聚合便成为描述‘我’的数据结构。
struct Man{
int sex;
int height;
bool hungry;
};
Man me;
上面,第一个问题已经解决了。那么什么又是面向对象编程呢?上面提到了对于‘我’的描述中定义了struct Man,然后定义了Man类型的变量 me。在面向对象中变量me被称为对象,用于产生对象的类型叫做类。从这个角度看,你在逗我,不就是换了个叫法,有什么区别呢?是的,它们除了上面称呼上的区别之外,还有以下区别:
- 封装:通过对基础类型的属性组合来描述一类实体,并将这类实体的行为封装起来。
- 抽象:将相同行为抽象出来,以便更好的复用
- 继承:对于有从属关系的类 is a,通过继承来复用已有类
- 多态:对象类型不同,通过复写基类函数,可以产生不同的调用效果
一切都是为了更好的复用
在编程中,为了更好的开发体验和开发效率,总是尽可能多的将相同的代码抽离出来,以变下次使用时能高效开发。面向对象的特点,又也都和复用有关。
首先在使用过程中,以往的经验是将变量和函数分开来写,使用时在组装到一起,来完成软件功能。那么为什么还要使用将函数放到类内呢?
设想这样一种场景,在上面的吃饭场景中,不是用面向对象的方式将产生这样的代码
void eat(Man &m) {
m.hungry = false;
}
eat(me);
这样有什么问题吗?吃饭传入人作为参数,改变人的状态,表示不饿了。功能似乎也实现了,在考虑这样一个场景。有一个人吃饭是否吃饱的标准与其他人不一样,其他人吃一次就饱,他需要3次。这种情况下,我们外部的eat就不能直接更改hungry属性了。
对扩展开放,对修改封闭 – 开放封闭原则
那面向对象怎么解决这种问题呢?
struct Man2 {
int sex;
int height;
bool hungry;
private:
int hungry_stat; // 还需要几份才能吃饱
public:
void eat() {
if (hungry_stat > 0) {
hungry_stat--;
} else {
hungry = false;
}
}
};
Man2 me;
me.eat();
这种编程方式第一个优点便是更符合我们人类的直觉,主体做了什么动作,比如me吃饭。另一个优点便是之前提到的封装,将是否吃饱封装在了类的内部,外部并不需要知道类内对吃饱是如何定义的,这属于类内部的状态,不对外可见,也防止了他人更改对象属性值。封装的原则便是“高内聚,低耦合”
为了让程序有更好的可重用性,除了对数据和方法层面进行封装外,对类间接口的复用属于更高层次的应用。在日常生活中,经常使用一些具有从属关系的概念,生物>动物>人,猫,狗等。将一些类的共有特性抽象到更高层次,作为他们公共的基类,在基类的基础上加入特有的属性和方法,构成新的类,成为派生类,这种方法便是继承。派生类继承自基类,派生类与基类之间的关系是is a的关系。
struct Animal{
virtual void eat();
};
struct Cat {
void eat() {
cout << "cat eat";
}
};
struct Dog {
void eat() {
cout << "dog eat";
}
};
Dog d;
Cat c;
void toEat(Animal *animal) {
animal->eat();
}
一切使用基类的地方,均可以用派生类替换 – 里氏替换原则。
除了继承,有其他实现这种能力的方式吗?组合通过显示将基类作为属性成员的方式进行处理,这其中优劣容后在探讨 // TODO!
struct Animal {
eat();
};
struct Cat {
Animal base;
void eat() {
base.eat();
}
};
struct Dog {
Animal base;
void eat() {
base.eat();
}
};
在上文使用继承方式的代码中,这种toEat中的同一份源码,却能在执行时根据参数的对象使用不同行为的情况称为多态。继承中使用的多态只是多态的一种情况,像函数重载和模板也属于,他们称为编译期多态或静多态
至此,面向对象的概念已经清晰。现在来思考下在C++中,假设你是一个语言设计者,除了一个带方法的对象,和一套继承的机制。在使用的时候,还需要考虑那些东西呢?
- 名字作用域:前面提到了封装的思想,那么有没有一种方式能够对外隐藏一些类的细节,C++使用访问控制符来处理
- 对象的生命周期:变量是具有生命周期的,从被定义的地方开始,到出作用域为止。他们的初始值根据符号位置的不同来确定。那么一个聚合的属性的定义应该如何被初始化呢
- 对象的操作:变量的运算符操作是内建的,对象的运算符操作如何被定义呢
- 继承的对象应该怎样实现,如何在C++弱类型语言中实现is A的关系
- 继承下的多态如何实现
- 若一个派生类继承自多个类,会发生什么情况呢,如何设计更好
问题已经被提出,让我们开始一步步的思考与探索之旅吧