C++进阶指北-面向对象编程

这篇文章将帮你解决的问题:

  1. 什么是编程
  2. 什么是面向对象编程
  3. 为什么要使用面向对象思想
  4. 除了对象+方法,还需要别的吗

本文主要从应用者的角度出发,来一起探讨面向对象思想是如何产生及实现的。本文假定读者有基础的编程语言知识,了解基本的变量,条件和循环。

程序 = 数据结构 + 算法

春天到了,张三想要去春游。那么张三面临的第一个问题就是怎么去,他需要对这次旅行提前做个计划,而为春游做的旅行攻略就是程序,最旅行攻略的过程便是编程了。这不仅仅是一个类比,程序是后可能在类似的使用场景中被遇到的,比如在一个旅游app中,就可能需要自动为游客生成旅游路线或参考行程表。
那这和上面的程序=数据结构+算法有什么关系呢?上面提到旅游攻略便是程序,那么考虑下旅游攻略都会怎么写,大概率会是这样子,3:20 入住酒店 4:00 去第一个景点打卡 6:00 和朋友一起吃晚饭等… 其中入住酒店,景点游玩,吃饭等都是我的行为,或者说是一系列规定的动作,这边是算法。数据结构便是隐藏在旅游攻略中的主体‘我’,为了描述主体‘我’,程序表述可能会是这样,这些基础数据的聚合便成为描述‘我’的数据结构。

struct Man{
    int sex;
    int height;
	bool hungry; 
};
Man me;

上面,第一个问题已经解决了。那么什么又是面向对象编程呢?上面提到了对于‘我’的描述中定义了struct Man,然后定义了Man类型的变量 me。在面向对象中变量me被称为对象,用于产生对象的类型叫做类。从这个角度看,你在逗我,不就是换了个叫法,有什么区别呢?是的,它们除了上面称呼上的区别之外,还有以下区别:

  1. 封装:通过对基础类型的属性组合来描述一类实体,并将这类实体的行为封装起来。
  2. 抽象:将相同行为抽象出来,以便更好的复用
  3. 继承:对于有从属关系的类 is a,通过继承来复用已有类
  4. 多态:对象类型不同,通过复写基类函数,可以产生不同的调用效果

一切都是为了更好的复用

在编程中,为了更好的开发体验和开发效率,总是尽可能多的将相同的代码抽离出来,以变下次使用时能高效开发。面向对象的特点,又也都和复用有关。

首先在使用过程中,以往的经验是将变量和函数分开来写,使用时在组装到一起,来完成软件功能。那么为什么还要使用将函数放到类内呢?
设想这样一种场景,在上面的吃饭场景中,不是用面向对象的方式将产生这样的代码

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++中,假设你是一个语言设计者,除了一个带方法的对象,和一套继承的机制。在使用的时候,还需要考虑那些东西呢?

  1. 名字作用域:前面提到了封装的思想,那么有没有一种方式能够对外隐藏一些类的细节,C++使用访问控制符来处理
  2. 对象的生命周期:变量是具有生命周期的,从被定义的地方开始,到出作用域为止。他们的初始值根据符号位置的不同来确定。那么一个聚合的属性的定义应该如何被初始化呢
  3. 对象的操作:变量的运算符操作是内建的,对象的运算符操作如何被定义呢
  4. 继承的对象应该怎样实现,如何在C++弱类型语言中实现is A的关系
  5. 继承下的多态如何实现
  6. 若一个派生类继承自多个类,会发生什么情况呢,如何设计更好

问题已经被提出,让我们开始一步步的思考与探索之旅吧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值