0?wx_fmt=jpeg


内容简介

1、第二部分课:面向对象初探,string的惊天内幕

2、第二部分第二课预告:掀起了"类"的盖头来(一)



面向对象初探,string的惊天内幕


上一课《【C++探索之旅】第一部分第十二课:指针一出,谁与争锋》中,大家辛苦了。


诚然,指针是不容易啃的硬骨头。不过,假以时日,小火慢炖,可以成为一碗上好的骨头汤,对你的C++水平那可是大补。


好了,口水擦一擦,我们正式进入C++探索之旅的第二部分啦,激动不?刚擦完的哈喇子可不要继续流啊。


这一部分的课程称为:C++之面向对象


因为我们要探索"面向对象编程"(OOP: Object-Oriented Programming) 。这是一种不同于以往的编程模式。


这种方法不会立即使你的程序发生革命性的改变,而且刚开始接触的时候你甚至会觉得这种模式没什么用。


目前为止我们的编程模式一直是:面向过程。也就是:我想要实现什么功能,就写什么函数,声明相应变量,等等。比如我要显示一段信息到屏幕上,我就创建一个函数:


void showMessage(string message);


但是,相信我:慢慢地,你会发现面向对象的编程模式比面向过程更符合人类的思维。你将能更好地组织你的代码。


注意:这一课接下来提到的"对象",其实有时是指对象,有时是指类。因为要到下一课我们才学习"类"的概念。暂时先不细分,希望通过这一课让大家对面向对象有大致了解。



对象...有啥用啊?


听到面向对象编程,你最好奇的词应该是"对象"(英语是object)吧。


你会想:这不会是又一种神秘的设计吧?难道是一个宿醉后的疯狂程序员想出来的东西?


其实不然。我们身边充满着对象:你的车是一个对象,你的电脑是一个对象,你的手机是一个对象,等等。


事实上,我们所知道的所有东西都可以被看作对象。面向对象编程,就是在代码中操作这些被称为"对象"的元素。


下面列举几种平时我们在编程中常见的对象:


  • 一个窗体:例如QQ的一个聊天对话框,画图软件的主界面。

  • 一个按钮:比如安装软件时那些"下一步"的按钮。

  • 游戏里的一个人物。

  • 一首歌,一个视频


说了这么多,对象到底是啥呀?是一种新的变量吗,还是一种新的函数类型?


不不不,都不是。对象是一种新的编程元素!


或者说得更具体一些,对象里面混合了变量和函数。


可不要被这句话吓跑了,我们一点点来探索。


想象一下... 一个对象。少年,不是让你想象结婚对象好嘛~


还是来点实际的图片好了,能帮助我们理解。


设想:

有一天,一个程序员决定要写一个图形界面的程序,这个程序可以在屏幕上显示窗体,调整窗体大小,移动窗体,删除窗体,在窗体里可以画各种图形,等等。


要实现这些功能,代码是比较复杂的:需要不少函数,这些函数之间互相调用。而且还需要很多变量,例如窗体位置(x坐标和y坐标),窗体的宽,高,等等。


这个程序员花了不少的时间来写这个程序。程序有点复杂,但是他完成了。最终,他实现的代码包含了很多的函数和变量。


当我们第一次看到他的程序时,就好像看一个化学家的一个实验环境一样,啥也看不懂。如下图:


0?wx_fmt=jpeg


不过,这个程序员对自己的程序还是很满意的,他甚至要把程序发布到网上,让大家可以用他的程序来创建窗体,直接拿来用就可以了,而不需要从零开始写代码。


不过,有一个问题,如果你不是化学专家,你可能得花不少时间搞懂这一堆东西到底是怎么运作的:哪一个函数最先被调用呢?为了改变窗体的大小,哪个变量要被传递到哪个函数里呢?总之,我们很害怕把整个实验环境给弄炸了。


在接到一些用户的建议和抱怨后,这个程序员决定替用户着想。他重新设计了他的代码,从面向过程的模式改为面向对象的模式。


这就好比他把所有和这个化学实验有关的东西都放到一个大方盒子里。这个大方盒子就是我们所说的"对象"。如下图:


0?wx_fmt=jpeg


在上图中,大方盒子的一部分被设为透明,是故意的,使你可以看到里面的景象。是的,我们的化学实验的所有设备都在大方盒子里。但是,实际当中,大方盒子是完全不透明的,用户从外面看不到里面到底有什么。如下图:


0?wx_fmt=jpeg


这个大方盒子里存放着函数和变量(那些化学仪器,试管,烧杯,等等),但这些元素对于用户却是不可见的。


这样,用户看到的就不再是成堆的试管,烧杯,等等让其抓狂的东西了。在大方盒子外面,我们呈现给用户的就只有一些按钮:一个按钮用于"打开窗体",一个按钮用于"改变窗体大小",一个按钮用于"关闭窗体",等等。用户完全不需要理解大方盒子里面的运作原理。


因此,以面向对象的模式来编程,就是:

程序员编写代码(可能很复杂),然后将这些复杂的代码都装到一个大方盒子(对象)里,用户从外面不能看到里面的实现细节。因此,对于使用这个对象的用户来说,操作起来就容易多了:只需按下对应的按钮,不需要精通化学也可以使用整个实验环境提供的功能了。


当然了,上面的比喻只是一个大致概念。


这一课我们暂时还不学习广义上怎么创建对象。我们反其道而行之,先来使用一个对象。其实这个对象我们之前已经接触过好多次了,就是string。


好几课之前,我们已经说过,string这种类型和普通的int,bool,float,double等不一样。后面的这些是C++的基础数据类型,用于存放简单的数据。


但string却不是这样,事实上它是一个对象。string这个类型背后隐藏了很多细节。



揭开string背后的神秘内幕


别看string宝宝很乖的,白天好像很正经的大公司老板Bruce Wayne(布鲁斯·韦恩),其实他是蝙蝠侠,背后秘密可多了。


多亏了面向对象的编程模式,我们在第一部分课程里就已经开始使用string了,当时我们还不知道string背后的秘密。下面我们就来揭开string神秘的面纱,看看里面的机制大概是什么样。


对于电脑来说,字符其实并不存在


为什么说string内部的机制其实比较复杂呢?


首先,我们之前把string称为"字符串",但其实电脑并不认识string当中的那些字符。


还记得吗?我们说电脑是一台只知道计算的大计算器而已(英语computer就是"计算机"的意思),它只认识数字!


但是,如果电脑只知道操作数字,那么它又是如何在屏幕上显示那么多字符的呢?例如你现在就用屏幕在看小编写的文章。


信息技术的先驱们早就想到了好办法。也许你听说过ASCII(发音是[aski])表。ASCII是American Standard Code for Information Interchange的缩写,表示"美国信息交换标准码"。


ASCII表就是为数字和字符的对应转换服务的一个表格。下面给出ASCII表的一部分:


数字

字符

数字

字符

64

@

96

'

65

A

97

a

66

B

98

b

67

C

99

c

68

D

100

d

69

E

101

e

70

F

102

f

71

G

103

g

72

H

104

h

73

I

105

i

74

J

106

j

75

K

107

k

76

L

108

l

77

M

109

m


如我们所见,大写字母A对应了65,小写字母a对应了97。所有的英文字母都包含在这个表中。


那么,就是说:每次电脑只要看到数字65,就会把它当作大写字母A来处理咯?


不是的。只有我们要求电脑翻译一个数字到字符的时候,它才会照做(果然很呆萌)。实际当中,电脑是根据我们声明的变量的类型来决定如何"看待"每个存储在变量中的数字的。大致规则如下:


  • 例如,我们用int型的变量来储存数字65,那么电脑就把它当成一个数字来看待。

  • 相反地,假如我们用char型的变量来储存数字65,那么电脑就把它当成一个字符来看待。电脑会说:嗯,这是大写字母A。事实上,char是英语character的缩写,表示"字符"。专门用来储存字符的。


因此,char类型的变量储存数字,但是这个数字会被"解释"成字符。不过,一个char型变量一次只能储存一个字符,那么我们怎么储存一个句子呢?我们接着学习。


字符串就类似字符数组


既然char只能储存一个字符(因为char的大小是一个字节,也就是8个bit位,正好是一个英文字符的大小,中文字符占2个字节),因此,程序员们就想到了创建char型数组。


我们已经学过数组了,也就是内存中相邻的相同类型的变量的集合。因此,用字符数组来存放多个字符的集合(例如:一句话)是很棒的选择,我们通常也将字符数组称为"字符串",因为它是一串字符么。


因此,我们只要这样声明一个char型数组:


char text[100];


text这个char型数组里可以储存100个字符。不过这个数组是静态的,大小不可变。如果你要创建大小可以改变的"动态数组",就可以用vector这种类型啦。


例如:


vector<char> text;


理论上,我们可以使用静态char型数组或动态char型数组来存放一串字符。不过这样很不方便,因此C++的设计者决定将有关字符串的操作都封装到一个对象里,那就是string。



创建并使用string对象


经过刚才那一节,我们了解到:操作字符串其实并不简单啊。需要创建一个char型数组,数组中的每一个元素是一个字符。而且,我们需要创建足够大的数组,以便装下我们要存放的字符串。总之,有很多方面要考虑。


这时,面向对象编程就可以派上用场了。还记得我们上面的图片中那个大方盒子吗?string就是这样一个"大方盒子"。


创建一个string对象


创建一个对象和我们之前创建一个普通的变量(比如int型变量)是类似的。例如:


#include <iostream>
#include <string> // 必须引入string头文件,因为string的原型就定义在里面

using namespace std;

int main()
{    
    string mString; //创建一个string类型的对象mString   
      
    return 0;
}


上面的程序不复杂,我们主要关注那句创建string对象的指令:


string mString;


故此,创建对象的方法和创建一个变量是一样的咯?


并不尽然,创建一个对象有几种方式,我们方才演示的只是其中最简单的一种。不过,这种方式确实和创建一个普通的变量没什么大区别。


这样的话,我们如何区分对象和普通变量呢?


为了区分变量和对象,可以从命名上来看,我们有规则:


  • 变量的类型以小写字母开始,例如普通的变量类型int

  • 对象的类型以大写字母开始,例如:Car


我知道你肯定会说:你看string不就是以小写字母开始的吗?它也是对象的类型啊。


我承认,这是一个例外,上面的规则并不是强加给我们的。很显然,实现string的程序员并没有遵守这个规则。


不过,大部分的情况下,对象的类型是以大写字母开始的。


当声明字符串时对其初始化


为了在创建对象时就对其初始化,有多种方式,我们来看看最简单的一种:


int main()
{    
    string mString("Hello !");    
    //创建一个string类型的对象mString,并初始化它的值 
    
    return 0;
}


这种方式的初始化,和C++的基础变量类型,例如int,double等,是一样的。


除了上面的这种方式,我们也可以用以下方式来初始化我们的string对象:


string mString = "Hello !";


我们既然创建了一个string对象,并为其赋值"Hello !",我们可以来打印这个对象的内容。


int main()
{    
    string mString("Hello !");
    
    cout << mString << endl;    
    //显示string的内容,就好像它是一个字符串     
    
    return 0;
}


运行,显示:


Hello !


在字符串初始化后再改变其值


现在我们已经创建了我们的字符串,并且对其赋了初值。我们还可以再改变其值。


int main()
{
    string mString("Hello !");
    cout << mString << endl;
    
    mString = "How are you ?";
    cout << mString << endl;
 
    return 0;
}


为了改变string变量的值,我们须要用=号(赋值符号)。


上面演示的方法,其实和之前我们使用普通变量类型时并没太大区别。我在这里只是要向你展示面向对象的神奇之处。


你,也就是用户,刚才就好像按下了一个按钮,这个按钮的作用是发出一个命令:我要将字符串的内容从"Hello !"改写为"How are you ?"。


虽然你的指令很简短,但string对象内部的那些函数接到指令后却开始忙乎起来了(让我想到了小黄人),它们依次做了以下几件事:


  1. 首先检测当前存放"Hello !"的char型数组是否足够容纳"How are you ?"这个字符串。

  2. 答案是否定的。因此它们创建一个新的char型数组,大小足以容下"How are you ?"这个新的字符串。

  3. 一旦新的char型数组创建完毕,就把旧的数组销毁,毕竟没用了。

  4. 然后把"How are you ?"这个字符串的内容拷贝到新的char型数组里。


看到吧,这就是面向对象编程的一个强大之处:我们完全不知道string对象里面原来发生了这么多事。


字符串的串联


假如我们想要将两个字符串的内容前后相接,合并为一个字符串。理论上说来,这是不容易实现的。但是string对象中早就设计好这样的机制啦:


int main()
{
    string mString1("Hello !");
    string mString2("How are you ?");
    string mString3;
 
    mString3 = mString1 + " " + mString2;
    cout << mString3 << endl;
 
    return 0;
}


很简单不是吗?只要用加号将两个字符串连起来就好了,至于string对象内部做了多少复杂的操作,我们"VIP用户"根本不在乎,有权就是这么"任性"~


字符串的比较


还要继续学吗?很不错,就要这种精神。


我们可以用==和!=符号来比较字符串,分别表示相等和不相等。


int main()
{
    string mString1("Hello !");
    string mString2("How are you ?");
 
    if (mString1 == mString2) // 显然两个字符串不相等,括号中条件语句为假
    {
        cout << "两个字符串相等." << endl;
    }
    else
    {
        cout << "两个字符串不相等." << endl;
    }
 
    return 0;
}


事实上,在string对象内部,这个比较的过程是两个字符串的一个字符接一个字符来比较的(借助一个循环)。但是我们不需要在意这些细节,就是这么潇洒。



string的一些方法


string对象的功能可不至于此。其中还有很多有用的方法,我们可以来见识几个常用的。


属性和方法


我们之前说过,一个对象中包含了变量和函数。不过,我们既然要学习面向对象编程了,就要用更专业的术语。


在面向对象的领域,对象中的变量被称为"属性",而其中的函数被称为"方法"。只是称呼不同而已。


你可以把对象提供的每一个方法想象成我们之前那个大方盒子外面对用户可见的那些按钮。


属性和方法又被称为"成员变量"和"成员函数"。


为了调用一个对象中的方法,我们用一种你已经见识过的方式:


object.method()


我们在对象和其成员函数之间用一个点来连接。这意味着:对于此对象,我调用它的这个方法。


理论上,我们也可以用同样的方式来访问成员变量(属性)。然而,在面向对象编程中,我们尽量避免用户直接访问我们的私人成员变量。关于这个,有很多知识点,我们以后的课程会讲到。


调用成员函数和成员变量的方式也是对象独有的,普通的变量类型并不能这样做。这也是除了名字之外区别对象和变量的好方法。


我们来看几个string提供的成员函数吧。


size方法


size在英语中是"大小,尺寸"的意思。因此,size方法用于获取string对象的大小,也就是里面包含的字符数目。


size方法的用法很简单,没有参数:


int main()
{
    string mString("Hello !");
    cout << "字符串的长度是 " << mString.size();
 
    return 0;
}


运行,显示:


字符串的长度是 7


erase方法


erase方法用于删去字符串的内容,因为erase在英语中是"删除,清除"的意思。


int main()
{    
    string mString("Hello !");
    
    cout << "字符串的内容是 : " << mString << endl;
    
    mString.erase();
    
    cout << "调用erase方法后, 字符串的内容是 : " << mString << endl;     
    return 0;
}


运行,显示:


字符串的内容是 : Hello !

调用erase方法后, 字符串的内容是 :


正如我们预期的,调用erase方法后,字符串的内容被清空了。


其实,erase方法的效果和


mString = "";


是一样的。


substr方法


substr是sub和string的缩合,sub表示"子的,副的",string就是"字符串"啦,因此,顾名思义,substr就是取得一个字符串的一部分。


substr方法的原型如下:


string substr( size_type index, size_type num = npos );


可以看到,substr的返回值也是string类型。它接收两个参数,一个是index,表示从字符串的第几个字符开始截取,第二个参数是num,表示截取多少个字符。


事实上,更确切地说,substr方法接收两个参数,第一个参数是必须的,第二个参数是非必须的。就是说,第一个参数必须要提供,第二个参数假如没有提供,那么num就会取默认值,默认值是npos。


看到上面的原型中有 num = npos了吗?暂时还不需要深究,这个其实是C++的一种特性,术语称为"默认参数"。就是说,假如函数被调用时,这个参数没有被赋予值,那么这个参数会取等号后面的默认值(类似"备胎"的概念)。


npos这个值表示:取余下的所有字符,直到最后一个。


举个例子吧:


int main()
{
    string mString("Hello !");
    cout << mString.substr(3) << endl;
 
    return 0;
}


运行,显示:


lo !


因为我们只给了substr一个参数,就是3,所以substr的第一个参数index就被赋值为3,而第二个参数num没有被赋值,就会取默认值。


因此,表示从mString的第四个字符开始截取,一直截取到最后。


再来试试给第二个参数num赋值的情况。


int main()
{    
    string mString("Surprise !");    
    cout << mString.substr(2, 4) << endl;     
    
    return 0;
}

运行,显示:


rpri


也很好理解,我们给index和num分别赋值2和4,那么就是从mString的第3个字符开始截取,截取4个字符。


我们之前在数组的那一课已经学习过了,我们可以对string类型的对象用类似数组的方式来访问其中某一个字符,使用中括号[]。例如:


string mString("Surprise!");
cout << mString[3] << endl;  //显示第4个字符,就是 'p'


好了,这一课就结束了。


这一课的主要目的是不想让你对面向对象编程感到恐惧。经过此课的学习,你是否已经对面向对象有点概念了呢?事实上,在这之前,你就已经用过对象了,string和vector其实都是。


我希望你已经做好准备来创建属于你自己的对象了。都说程序员没有对象,那么我们自己new一个呗~


这是下一课开始的目标。



总结


  1. 面向对象编程是设计代码的一种方法。操作的是被称为"对象"的元素。

  2. 对象里面的实现细节可以很复杂,但是使用对象的人却并不需要关心这些细节,只需要知道如何使用就可以了。这是面向对象编程的一大优势。

  3. 一个对象是由一些属性和方法组成的,也就是变量和函数。

  4. 我们可以调用对象的方法来实现各种需求。

  5. 在内存中,字符串的操作其实是很复杂的。为了替我们这些C++的使用者着想,C++的设计者为我们设计了精巧的对象:string。我们只需要用string来创建字符串实例,操作这些字符串,而并不需要关心内存中到底发生了什么。



第二部分第二课预告


今天的课就到这里,一起加油吧!

下一课我们学习:掀起了"类"的盖头来(一)