欢迎光临散文网 会员登陆 & 注册

如何用轻小说的方式学C++(五) 斐波那契与他的小兔子们

2019-09-22 01:49 作者:汐留楓Channel  | 我要投稿

刹那悄悄地瞥了一眼活动室内。

黑灯瞎火,空无一人。

刹那松了一口气,潜进了活动室,正要摸开关,发现灯突然就被两位挚友给点亮了。


“刹那啊你发给我的那个程序什么意思?以为自己写的复杂点我看不懂就会以为你是对的吗?”

凛音跳了出来,一手叉腰一手直指刹那,仿佛名侦探推理完毕指着嫌疑人的那酷酷的动作。

“说过了要写成特定的形式计算机才能读得懂,你这乱写肯定会出错的!”

刹那羞愧地低下了头。

因为回去以后把循环的语法忘得一干二净,结果写了很久都没写出来,最后不得不拜托隔壁班某位樱姓同学的姐姐帮忙。传说这位姐姐姓樱名宁宁,也是半年前才开始自学C艹与游戏编程,现在却已经在Eagle Jump里面担当游戏开发。

大概过了十分钟,这位姐姐把程序发了过来,刹那看也没看就转发给了凛音。

然后就有了刚才的一幕。


久远走上前看了一下程序,然后暴击凛音:

“虽说肯定不是刹那写的,但是这代码毫无疑问是可以运行且正确的啊!”

“诶?!”

凛音一脸懵逼。这和说好的不一样啊?

面对一脸懵逼的凛音,久远无奈地再重锤了凛音一拳:

“之前说了你想教刹那至少自己先把《C++ Primer》过一遍啊!这些都是常规操作啊!”

“阿诺,很抱歉...我重写就是了,不要责怪凛音酱了...”

刹那似乎是以为自己的无能牵扯了为师的凛音,这让久远更是无奈了,只好对刹那说程序是对的不过下次要自己写。


“那么,今天的主题是——斐波那契!”

“肥婆纳妾?”

“是斐波那契!”

“我知道!是那个有一堆小兔子的吧!”

“知道就好讲话了。那么今天的课题——求出斐波那契的前n项!”

“诶有n个数的话怎么存嘛?”

“这里就要请出——数组!”

“数组?”

“字面意思!数组!”


int a; //一个整形变量

int b[100]; //一个整形变量的数组

b[0]=100; //如此这般访问。要注意,从0开始算,所以b[1]是第二个元素


“所以并没有什么难的,那么让我们开始吧!上次的循环没忘吧?”

“...要是没忘就交上次作业了。”

“呃...那么看这段吧。”


#include<iostream>

int main() {

    int a[1024],n;

    a[0]=1;

    a[1]=1;

    std::cin >> n;

    for(int j=2;j<n;++j) {

        a[j]=a[j-1]+a[j-2];

     }

    for(int j=0;j<n;++j) {

        std::cout<<j<<" "<<a[j]<<std::endl;

    }

    return 0;

}


“嗯,看——不懂。”

凛音掀起了桌子。

“就是一个循环啊!不停地把后两项加到前面一项去啊!”

(似懂非懂)

“另外,字符串也可以看成一个数组哟。”

(点头)

“那么我们进入到高精度加法吧。”

“...等等?高精度?”

“先说一句,int的取值范围是-2147483648~2147483647内”

“诶!第一次听说啊!还有这魔法一样的数字是什么鬼!”

“[-2^31]~[2^31-1],合计2^32个数,就是这么魔法。因为现在的电脑上int通常占用4个字节,每个字节8个bit,也就是32个bit。”

“唔...晕了!”

“也没要你记。现在你写一个能够进行100位数以内的加法的程序。”

“诶?!100位?!真的能算吗?!我可以手算啊!”

“前提是你手算的能有电脑快。”

刹那茫然地看着编辑器一闪一闪的光标。


“补充一些关于std::string的东西吧。”

写上#include<string>以后,你就可以用这个东西了。”


一些用法:

std::string a;

std::cin>>a; //输入一个字符串。

a.length(); //能够得到字符串的长度,类型是size_t,一般是unsigned int的重定义,即无符号整数,能表示0~4294967295之间的数。通常也不会有这么长的字符串就是。

a[100]; //字符串第101个元素的值。(要从0号开始算,所以是第101个)


ざわざわ~

ざわざわ~

“动手!能写多少写多少!”凛音咆哮。


#include<iostream>

#include<string>

int main() {

     std::string a,b;

     std::cin>>a>>b;

     size_t aLen = a.length();

     size_t bLen = b.length();

     size_t maxLen = aLen > bLen ? aLen : bLen;

    .......


“写不下去了!怎么做嘛...”

“提示,先倒序,这样进位可以往后面进。”


     ......

     int ia[101],ib[101];

     for(int i=0;i<aLen;++i) {

         ia[i] = a[aLen-i-1];

     }

     for(int i=0;i<bLen;++i) {

         ib[i] = b[bLen-i-1];

     }

“停——这里要注意,字符的'0'的值并不是0哟。”

“什么意思?”

“有个东西叫ASCII码,把常见的一些字符与具体的二进制数值做了一个映射。比如字符0的ASCII是48号,A是65,a是97等。所以要改成这样:”


     ......

     int ia[101],ib[101];

     for(int i=0;i<aLen;++i) {

         ia[i] = a[aLen-i-1] - '0';

     }

     for(int i=0;i<bLen;++i) {

         ib[i] = b[bLen-i-1] - '0';

     }

“没必要去背那个表,写成 '0' 机器自动就会认出来并且当成常量处理的。”

继续:

     for(int i=0;i<maxLen;++i) {

         ia[i]=ia[i]+ib[i];

         if(ia[i]>9) {

             ia[i]=ia[i]-10;

             ++ia[i+1];

         } //进位

     }

“至此运算就全部做完了,不过还需要输出。综上所述:”


#include<iostream>

#include<string>

int main() {

     std::string a,b;

     std::cin>>a>>b;

     size_t aLen = a.length();

     size_t bLen = b.length();

     size_t maxLen = aLen > bLen ? aLen : bLen;

     int ia[101]={0},ib[101]={0}; //数组元素全部清零

     for(int i=0;i<aLen;++i) {

         ia[i] = a[aLen-i-1] - '0';

     }

     for(int i=0;i<bLen;++i) {

         ib[i] = b[bLen-i-1] - '0';

     } //倒序

     for(int i=0;i<maxLen;++i) {

         ia[i]=ia[i]+ib[i];

         if(ia[i]>9) {

             ia[i]=ia[i]-10;

             ++ia[i+1];

         } //进位

     }

     int i;

     for(i=maxLen-1;i>0;--i) {

         if(ia[i]!=0) break; //break的意思是跳出这一层循环

     }

     for(;i>=0;--i) {

         std::cout<<ia[i];

     }

     return 0;

}


“作为今天的第一个作业,思考一下输出的时候为什么要这两个循环吧~”

“诶?!”

“那么进入今天的第二个部分——递归!”

“递归?好像数学课里面有听到过,唔...”

“递归就是说一个函数自己调用自己~”

“诶那不会一直调用下去吗?!”

“所以有个东西叫递归出口啊~”

“诶?”

“比如斐波那契数列求第n项可以这么写:”


int fib(int n) {

     if(n==1 || n==2) return 1;

     else return fib(n-1)+fib(n-2);

}


“可以看到,fib这里自己调用了自己两次,然后把值加起来返回。正可谓数学里面的A[n]=A[n-1]+A[n-2]”

(似懂非懂)

“那个...凛音酱啊,这里这么多n...不会互相覆盖吗?”

“你还记得我之前说的实际参数与形式参数吗?”

“忘了。”

“就是说,这些东西里面互相不干扰,会进行替换的。这里的参数n只是一个标记,当你运行fib(n-1)的时候,相当于新开了一个副本,又有一个新的n,我们姑且记作n'吧,它的值是n-1。然后在n'的里面,又有fib(n'-1),记作n",以此类推。”

“好厉害啊!那就是说会有很多个n喽?”

“的确。”

“好我们来算一下fib(10000)吧!”


Segmentation fault (core dumped)


“诶?!出错了?!”

“这也引出另一个话题。正是因为计算机要给这些n存位置,包括你新调用一次fib就会多一些数据,当这些数据太大了以后就会炸掉。所以——尽量避免递归。”

(虽然不懂但还是点点头吧)

“但是递归的确是一个解决的思路。并且——递归与循环等价。”

“等价?”

“那么今天的第二个作业~禁止使用for do-for while以及goto,实现1到100的求和。”

“诶?!”


【久远的小课堂】



图片来自某三次元傻屌同学。

从命名上可以看出有多严重的心理问题

[注意: 不是所有的编译器都支持宽字符标识符的,写代码请还是好好用字母]


这一次我就对刹那不知从哪里搞来的代码进行剖析。

听不懂也没事,听个响也行。


先从main过程入手,可以看到main里面仅仅是对另一个过程的调用。那么让我们来看一下这个“有多严重的心理问题()”

这个过程里面只有一个循环,for(auto 问题 : 心理问题() );

刹那你应该还记得之前凛音说到的那个带冒号的范围for吧,这里就出现了。

冒号的意思表示,前面的这个变量取变后面这个范围的所有的值,并且进行一些操作。这里直接写分号不进行任何操作,所以仅仅是给“问题”赋上全部的值以后结束。

然后,自然而然地我们会看到心理问题,即上面的struct。

final的意思表示不允许被继承,关于继承我们会放到之后的类来讲。

可以看到,心理问题里面有三个部分:类“草”的定义,begin方法及end方法。


通常而言,我们进行for操作的,都是一个集合,比如数组啊容器啊之类的,但是也不是一定的,只要一个数据结构满足一定的条件,我们就可以for,即begin、end还有operator++。

begin返回一个泛迭代器,你可以理解为一个名片,名片上面印着第一个元素的姓名、地址等很多信息。end则是最后一个元素。operator++则代表你可以获得下一个名片。

在这个例子里面,“草”就是这个名片的种类。你会不停地拿到像“草”一样的名片。当执行operator++时,草里面的i会自增。显然,循环是要停下来的,那么就是在operator!=为假(即now==end)时停止。if(i==100) goto end;可以看到end后面的return false;

即循环100次以后会结束循环。

这里有个template,以及后面的一些type_traits内的模板,这是一个典型的错误用法,不过鉴于不会对这里造成什么影响我就不去说了。


“草”是个名片,但是你肯定要根据名片去找具体的元素。每找一次,就调用一次operator*,可以看到operator*会返回一个デデドン类型的元素。循环100次,则会生成100个デデドン。

在类与对象里,对象死亡后有个东西叫“析构函数”,格式就是一个波浪号~加上类的名字デデドン。后面的noexcept(false)表示允许抛出异常。通常而言析构函数是不允许抛出异常的,因为会造成异常语句之后的语句没法正确执行,这里显然不知道为什么就给允许了。再后面看似有个while,实际如果顺利的话只会执行一次——

std::cout<<"まわれ";

执行完毕后,会返回std::cout也是说过的。那么,把std::cout转换为bool的时候,会根据这个流是否依然有效来转换。如果顺利输出了,那么就是true,取反感叹号后变成false,结束循环;但是一旦没顺利输出,则会一直死循环在这,直到引发异常。

生成100个对象,那么这100个对象死的时候也会输出100个まわれ。


综上,这个程序顺利的话能正常运行完毕。

如何用轻小说的方式学C++(五) 斐波那契与他的小兔子们的评论 (共 条)

分享到微博请遵守国家法律