从机器码到高级语言
所谓对象,有一句程序员喜欢说的经典的一句话”一切皆对象“,换句话说,生活中处理的几乎一切都是对”对象“来处理的。对于程序来说,这样一个”对象“包含了唯一的身份、状态和行为,在代码中的具体表现就是class
类
一个对象的状态表现为数据域以及当前值,一个对象的行为由一组函数定义
一个程序本身,其实都是调用一个个对象和封装一个个对象的结果。可以肯定的是,对象从不同的视角来看是不一样的。
只要长了眼睛就知道Mk2是由两个风扇和一个操纵杆组成,不过第一次建造时,需要一定的手法、
但如果使用究极手的话,Mk2在林克眼里就是9块左纳尼乌姆
所以说同样一个对象,在不同视角下是不一样的;用更专业的术语来说,那就是一个对象就是类的一个实例。进一步讲,我们可以创建一个类的多个实例,创建一个实例称为实例化,这样说可能有点抽象,那么我们继续拿Mk2来举例子,class定义Mk2是这么一个东西,我们可以用究极手的蓝图功能将它复刻,而且我们可以用很多个左纳尼乌姆来量产很多台Mk2,这就是对class的实例化。
不过这样解释还是片面了点,更深入地说,如果class将Mk2定义为“必须要用操纵杆”(这里可以将这个设定比喻成构造函数,不过这一点我们后面再讲),那么理论上如果讲两个小风扇换成大螺旋桨,这个东西和纯粹用究极手复制出来的Mk2不太一样,但也可以说是Mk2的一个“实例”。
同样的,在原Mk2的基础上,如果我们能加装一个灯,我们就可以用它来探索地底;如果我们能加装几门炮,那么Mk2瞬间变为战斗飞艇。这里可以引申出面向对象的另一个特点,那就是多态,牵扯到父类与子类等,不过这些具体的我们也放在后面讲。
我们现在聊聊面向对象的语言层面上的内容。
前面提到了对象在不同视角上表现是不同的,那么一个程序对于机器与对于程序员来说也不太一样。
图片出自知乎@herb.dr
汇编语言的机器码是机器认得的语言,所有程序在机器眼里就是这么一串0101代码。直接将机器码初步翻译,比如翻译成mov
这个基础转移关键字,这是至少程序员能看懂的语言
但是汇编语言对于程序员来说还是太麻烦了,一个hello world就能让操作寄存器
DATA SEGMENT
BUF DB 'HELLO WORLD! THIS IS MY FIRST ASM FILE! $'
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE,DS:DATA
START:
MOV AX,DATA
MOV DS,AX
LEA DX,BUF
MOV AH,09H
INT 21H
MOV AH,4CH
INT 21H
CODE ENDS
END START
作为一个面向机器的语言,很难从其代码上理解程序设计意图,设计出来的程序不易被移植,所以理所当然的不被广泛使用。当然,作为最“底层”的代码,汇编语言通常在驱动程序、嵌入式操作等有很大应用
所以,C语言的发明是里程碑式的
#include <stdio.h>
int main()
{
printf("Hello World!");
return 0;
}
简化版的helloworld十分符合人的口味儿,不仅编写简单,程序员需要处理的对象也变为了数据与算法。C语言可以看成直接对机器指令的各种调度,比如我们如果要写一个围棋程序,那么我们第一个要做的事情就是首先建立棋盘,可能要先调度图形库画出一个十九路棋盘,然后让玩家1下子,下子后要判断胜负,所以在程序中的这个阶段要引用判断胜负的函数,之后轮到玩家2下子,然后继续判断胜负,由此循环,可能还要建立for循环之类,最终在能判断出一场对局的胜负后结束程序,这就是一个很典型的面向过程的编程方式
我们需要发明出更符合人口味的编程方式以及编程语言,毕竟很多程序没上面说的这么简单。比如我们要正经写一个围棋游戏,那我们可能得编写账户登录程序,这儿好歹还能继续用面向过程强行编写。和水平相差太大的玩家下没意思,我们可能还需要增加排位机制和匹配机制;高水平的对局很吸引人,所以我们可能需要增加直播机制和录像机制;围棋下累了想娱乐一下,那么我们可能需要增加五子棋模式……这样程序立马就变得十分复杂了,继续使用面向过程方式,程序就会产生各种冲突和bug,屎山代码会越积越多。
C++与C相比一个最大的特点就是实现面向对象编程,C++可以同时支持面向对象和面向过程编程,而更偏应用化的C#则只支持面向对象编程。
class user{
public:
std::string name;
int grade
void introduction(){
std::cout<<"Hello,Nice to play chess with you"<<std::endl;
}
};
这是一个简单的账号类,包括名字与段位分数以及“签名”这个简单函数,这个在人的视角中最容易理解的程序,继续拿围棋程序举例,如果使用面向对象编程,我们可以实例化这个类在建立一个个账户,也可以派生在增加账户内功能;同样,我们也可以建立棋盘类,甚至棋子类,规则类等等等等,这样的程序在人眼中是很清晰的。
可以说面向对象编程就是将数据和算法结合的
一张图,简明的表达出面向对象的历史渊源
图片来源b站@---杨同学---
UML类图
设定类和对象的关系的时候,我们可以用UML类图将其标准化
类图主要作用是用来显示系统中的类、接口以及它们之间的静态结构和关系的一种静态模型。
数据域表示如下:
dataFieldName:dataFieldType
构造函数表示如下:
ClassName(parameterName:parameterType)
函数表示如下:
functionName(parameterName:parameterType):returnType
- “+”表示
public
- “-”表示
private
- “#”表示
protected
- 不带符号表示
default
若表示抽象类,则名称改为斜体
//接口UML图、包UML图、关系UML图is coming。。。//
构造函数
通过调用构造函数来创建对象
- 构造函数的名字必须与类名相同
- 构造函数没有返回类型——即使返回void也不可以
- 在创建对象时,构造函数被调用,它的作用就是初始化对象
《C++程序设计》中的原话定义
值得一提,构造函数如果给予void等返回值,编译器会报错,或者把它当成一个普通函数。笔者在初学C++面向编程时,经常会因为没想起”构造函数“这个概念而对没有返回值的同名函数感到疑惑。
我们在创造对象时,一般需要对对象赋初始值,比如写围棋程序时假设需要为双方分别给100个棋子,这是每局比赛必要的条件,在创建对象之后再重新赋值显得十分繁琐,就好像选手上场后,先每人给一个小碟子,然后再每人在小碟子里倒50个棋子,还不如直接每人给一个带有50个棋子的小碟子来地简单。
这就是构造函数的主要目的,从另一个角度上看,构造函数也是一种对类的宣言。构造函数一旦被定义,意味着所有对象肯定要按照这个条件来,就好像建国需要先立宪法,国家运作要按照宪法来,当然宪法可以后天更改,就好像构造函数作用过后也可以单独为对象单独调用另外的函数来调整数据域。
作为类成员,直接在声明里初始化是错误的!尽管作为一个函数声明的同时初始化变量看起来是多么正常!
默认构造函数/缺省构造函数(default constructor)
构造函数是必要的,你甚至可以直接声明一个构造函数但不做任何事!
Circle(){}
哪怕不声明构造函数,或者说忘记声明构造函数了,编译器也会自动提供默认构造函数
在《C++程序设计》这本书中,对默认构造函数的描述是:只有当程序员没有在类中显式地声明构造函数时
根据这个原则,下面2种构造函数都是默认构造函数:
class Sample {
public:
// 默认构造函数。
Sample() {
// do something
}
};
class Sample {
public:
// 默认构造函数。虽然有形参,但有默认值,调用的时候可以不显示的传入实参。
Sample(int m = 10) {
// do something
}
};
书中提到了另一个赋初值的方法:初始化列表法适用于对象的数据域没有无参构造函数时
ClassName(parameterList)
:datafield1(value1),datafield2(value2)
{}
//对象的具体使用方法略,这点十分基础简单,实在不行翻翻文档
不过可以说说一些小细节
- circle2=circle1;实现对象间内容复制,1的数据域内容被复制到2中
- 一个对象的大小不看其函数,因为函数存储在类中,对象共享
- 如果一个对象只使用一次,则可以用
ClassName()
语法创建一个匿名对象,带括号的匿名对象满足大部分一般对象的功能(可能我一般用不到,但看别人代码的时候忽视这一点又会很浪费时间)
类定义和类实现的分离
和C程序写函数文件一样,类也会将其实现与定义分开,就像函数的声明的定义,最傻瓜的区分方式就是声明文件直接接;
二元作用域解析运算符::
用于指明类的作用范围
例如下为例类实现
Circle::Circle()
{
radius=1;
}
Circle::Circle(double newRadius)
{
radius=newRadius;
}
double Circle::getArea()
{
return radius*radius*3.14;
}
- 这种方式可以方便保护软件工作者的知识产权,比如只提供头文件和类,隐藏类的实现就相当于“祖传秘方”
- 类定义用于提供给客户程序,如果客户有需求更改条件,则只需更改实现,客户程序不需要更改
这种将类定义和实现分开的方式,其实就是类抽象
抽象后的类的具体实现是隐藏的,这就是类封装
数据域的封装
一个非常常用的功能,类数据域的内容通常由类本身的函数来改变,有时候也会由其他类函数改变,也甚至可能直接被客户程序改变circle1.radius=5
。
问题来了,如果该变量非常重要,关乎程序整体生态的,比如说在一个已经写好的程序中,需要增加一个对某变量÷的操作,那么这个变量肯定不能为0了。这个变量的赋值都是由类函数进行,假设理论上不会出现0的情况,那么这个操作相对来说是安全的。这个时候谁家小孩直接在自己的操作界面加了句circle1.radius=0
,那么恭喜程序直接崩溃。
直接让类以外的操作对类本身数据域进行操作是危险的,这个时候我们需要用到private
关键字,对数据域进行保护,这样类以外的操作就不会对所属数据发生改变,甚至连访问都是不被允许的
相对的,允许改变的使用public
关键字
引言:如果需要对private成分进行访问或者修改,则可以使用get与set函数
//略变量作用域
到这里为止,就能对面向对象编程有一个基本的认识了,不过要对面向编程有更深入的了解还是要具体实操,接下来会继续针对面向对象思想深入了解string类等进阶操作