【问题标题】:Finding first and last capital letter in user input在用户输入中查找第一个和最后一个大写字母
【发布时间】:2019-06-29 17:42:12
【问题描述】:

输入应取自 a-z 或 A-Z,输入以星号 * 结束。

我们需要将输入字符的第一个和最后一个大写字母作为输出。此外,我们应该显示我们每次接受的输入。注:我们逐个字符地输入输入,而不是字符串。

测试用例一:输入:aAbCcP* 输出:AP

测试用例 2:输入:ZabCBc* 输出:ZB

我在下面写了这段代码,满足测试用例1,但不满足测试用例2:

.MODEL
.STACK 100H
.DATA
   STR DB 'Enter letters:$'
.CODE

MAIN PROC

MOV AX, @DATA
MOV DS, AX

LEA DX, STR
MOV AH, 9
INT 21H 

cycle: 

    MOV AH, 1
    INT 21H

    CMP AL, '*'
    JZ output 
    CMP AL, 'Z' 
    JA save


head: 
    CMP BL, 1
    JZ save

    MOV BL, 1
    MOV BH, AL 

clear:
    XOR AL, AL  

save:
    MOV CH, AL

LOOP cycle 

output:
    MOV AH, 2
    MOV DL, BH
    INT 21H 

    MOV AH, 2
    MOV DL, CH
    INT 21H 


MAIN ENDP 
END MAIN 

【问题讨论】:

  • 您的代码打印在案例 2 中的作用是什么?这不是minimal reproducible example。还要评论您的代码和/或用英语描述您的算法supposed 是什么,哪些寄存器应该保存什么。当没有任何东西跳到那里时,我不明白为什么你有一个clear: 标签。您只能从head: 跌落到那里。此外,您似乎在循环开始之前没有将 BL 或 BX 归零就读取了 BL。您的代码似乎依赖于 BL != 1 进入函数。
  • 另外,您的循环会根据 CX 提前结束,否则 loop 指令可能会损坏 CH。我想你想要jmp,因为没有基于计数的循环终止条件。 loop 基本上是 dec cx / jnz 但没有更新标志,所以显然你不希望那样。
  • 无论如何,最明显的问题是,CH 每次循环都总是更新。只有最后一个大写字符也是最后一个整体时,它才能工作。您可能会编写 2 个循环:一个找到第一个大写字母并将其保存在一个寄存器中,然后另一个而不是跟踪最后一次看到的。

标签: assembly emu8086


【解决方案1】:

首先问自己这些问题:

  • 什么是大写字母?
    如果我们不考虑重音字符,那么大写字母就是 ASCII 码从 65 到 90 的字符。

  • 我可以相信用户只输入 a-z 或 A-Z 中的字符吗?
    不,你不能。您无法控制用户在键盘上的操作,这就是为什么您的程序应该采取防御性方法并使用比单个 cmp al, 'Z' 更好的东西来测试大写字母的原因。

  • 如果输入中不包含一个大写字母会怎样?
    您可以选择打印两个空格,或一条描述性消息,或者就像我没有显示任何内容一样。

  • 如果输入只包含一个大写字母会是什么结果?
    你可以选择打印那个大写字母,或者像我一样显示它两次,因为如果你想到的话,那个大写字母同时是大写字母的第一次出现,也是大写字母的最后一次出现。

  • 我将使用哪些输入/输出函数?
    对于单字符输入,您可以在 DOS 函数 01h、06h、07h、08h、0Ch 和 3Fh 之间进行选择。
    对于单字符输出,您可以在 DOS 函数 02h、06h 和 40h 之间进行选择。
    如果您是组装新手,请坚持使用更简单的组装并使用函数 01h 和 02h。在使用任何 DOS 功能之前,请咨询API reference。当然还要检查emu8086是否完全支持该功能!

您需要决定以上所有因素才能完成任务。重要的是,对于您做出的每一个选择,您都可以捍卫自己的选择。


下面是我的这个任务的版本。为简单起见,我使用的是小程序模型。看到上面的ORG 256 指令了吗?这种程序模型的主要好处是让所有段寄存器都平等地指向您的程序(CS = DS = ES = SS)。

程序运行 2 个循环。 第一个循环一直运行直到收到资金。 (不用说,如果输入包含星号,它会更早停止。)因为该大写同时是大写的第一次出现,也是大写的最后一次出现,所以我将它保存了两次,都在 DLDH.

第二个循环运行直到收到星号。每次出现新的大写字母时,它都会替换DH 中所写的内容。当这个循环最终结束时,DLDH 都会显示在屏幕上,当然也是按照这个顺序显示的。

程序以首选的 DOS 函数 4Ch 退出以终止程序。

我编写了一些基本的 cmets,避免添加多余的 cmets,并为程序中的标签使用描述性名称。请注意漂亮的表格布局。为了可读性,它是关键。

        ORG     256

Loop1:  mov     ah, 01h     ; DOS.GetKeyboardCharacter
        int     21h         ; -> AL
        cmp     al, "*"     ; Found end of input marker ?
        je      Done
        cmp     al, "A"
        jb      Loop1
        cmp     al, "Z"
        ja      Loop1
        mov     dl, al      ; For now it's the first
        mov     dh, al      ; AND the last capital

Loop2:  mov     ah, 01h     ; DOS.GetKeyboardCharacter
        int     21h         ; -> AL
        cmp     al, "*"     ; Found end of input marker ?
        je      Show
        cmp     al, "A"
        jb      Loop2
        cmp     al, "Z"
        ja      Loop2
        mov     dh, al      ; This is the latest capital
        jmp     Loop2

Show:   mov     ah, 02h     ; DOS.DisplayCharacter
        int     21h         ; -> (AL)
        mov     dl, dh
        mov     ah, 02h     ; DOS.DisplayCharacter
        int     21h         ; -> (AL)

Done:   mov     ax, 4C00h   ; DOS.TerminateWithReturnCode
        int     21h

例子:

aZeRTy*

aZeRTy*ZT


如果您采取简单的方法并只是复制/粘贴我的代码,那将是非常令人失望的。我已经尽力解释得很详细,希望你能从中学到很多。

我的解决方案肯定不是这个任务的唯一好解决方案。你可以例如首先输入所有字符并将它们存储在内存中的某个位置,然后您从内存中处理这些字符,类似于我的处理方式。
请尝试编写一个以这种替代方式执行此操作的工作版本。您只会变得更聪明!快乐编程。

【讨论】:

  • OP 已经在使用cmp al, 'Z'。我不确定你的第一个要点是什么。他们被允许假设他们的输入是字母,所以c > 'Z' 表示小写,否则为大写。您的版本通过使用额外的分支来拒绝非字母字符,从而使其复杂化。
  • 但是是的,两个独立的循环似乎是最好的选择,而不是每次找到新首都时都在旗帜上分支。
  • @PeterCordes 我的回答基于 OP 的the previous question。我错误地把它贴在这里。尽管如此,我认为它仍然是一个有效的答案。
  • 是的,这个问题应该被删除或关闭。也许简化第一个要点,因为 OP 已经知道这一点。或者指出要排除非字母字符,以使代码更复杂但能够处理12ABcd7_之类的输入
【解决方案2】:

您的代码已损坏,因为您总是在每次迭代时都会遇到save: MOV CH, AL,因此只有在最后一个大写字母也是整个输入的最后一个字符时它才能工作。

使用调试器单步执行简单的输入,例如ABc*,看看它是如何出错的。

另外,你使用loop,就像dec cx/jnz。这是没有意义的,因为没有基于计数器的终止条件,如果 CL 为零,则可能会破坏 CH。你甚至没有先初始化 CX! loop 指令不是循环的唯一方法;它只是一个代码大小的窥视孔优化,您可以在方便地将 CX 用作循环计数器时使用。否则不要使用它。


这是 Sep 实现的简化版本,利用输入保证是字母的事实,所以我们真的可以像 c <= 'Z' 一样简单地检查大写(在排除 '*' 终止符之后) .我们不必担心12ABcd7_ 或空格或换行符之类的输入,它们的 ASCII 代码也低于大写字母范围。您的 cmp al,'Z' / ja 检查是正确的,只是您分支的代码没有合理的逻辑。

即使您确实想严格检查c >= 'A' && c <= 'Z',也可以使用sub al,'A' 对一个分支进行范围检查; cmp al,'Z'-'A' ; ja non_upper 而不是一对 cmp/jcc 分支。 (这会修改原始文件,但如果您将其保存为 SI 或其他内容,您可以稍后使用 lea ax, [si+'A'] 恢复它)

您还可以在两个循环的循环底部放置一个条件分支,而不是底部的 jmp 和内部的 if() break。 Sep 的代码在第一个循环中已经这样做了。

我同意 Sep 的观点,即有 2 个循环比每次找到一个大写字母时检查一个标志更容易(看看它是否是第一个大写字母)。

        ORG     100h        ; DOS .com is loaded with IP=100h, with CS=DS=ES=SS
                            ; we don't actually do any absolute addressing so no real effect.

        mov     ah, 01h     ; DOS.GetKeyboardCharacter
                            ; AH=01 / int 21h doesn't modify AH so we only need this once
find_first_cap:  
        int     21h         ; stdin -> AL
        cmp     al, '*'     ; Found end of input marker ?
        je      Done        ;  if (c=='*') return;  without print anything, we haven't found a capital yet

        cmp     al, 'Z'
        ja      find_first_cap
    ; fall through: AL <= 'Z' and we can assume it's a capital letter, not a digit or something.

        mov     dl, al      ; For now it's the first
        ;mov     dh, al      ; AND the last capital

        ;mov     ah, 01h     ; DOS.GetKeyboardCharacter   AH still = 01
        ;jmp     loop2_entry      ; we can let the first iteration set DH
Loop2:                      ; do {
        cmp     al, 'Z'       ; assume all c <= 'Z' is a capital alphabetic character
        ja      loop2_entry
        mov     dh, al        ; This is the latest capital

loop2_entry:
        int     21h         ; stdin -> AL
        cmp     al, '*'
        jne     Loop2       ; }while(c != '*');


Show:   mov     ah, 02h     ; DOS.DisplayCharacter
        int     21h         ; AL -> stdout
        mov     dl, dh
        ; mov     ah, 02h     ; DOS.DisplayCharacter
        int     21h         ; AL -> stdout

Done:   mov     ax, 4C00h   ; DOS.TerminateWithReturnCode
        int     21h

在这一点上,它可以说不是更简单,而是更优化,尤其是针对代码大小。当我写任何东西时,这往往会发生,因为那是有趣的部分。 :P

对于非大写情况,在循环内使用分支可能会更糟。 (在 P6 兼容 CPU 的现代代码中,您可能会使用 cmovbe esi, eax 而不是条件分支,因为条件移动正是您想要的。)

int 21h 之前省略mov ah, XX,因为它仍然设置不会使您的程序更易于阅读,但是如果您仔细检查the docs 以确保它们没有调用,这是安全的'不返回 AH 中的任何内容。

【讨论】:

  • 如果你正在优化代码大小,你也会删除 Done: mov ax, 4C00h ; DOS.TerminateWithReturnCode int 21h 以支持 Done: ret 。 COM 程序可以通过简单的ret 终止。 DOS 将在将控制权转移到 CS:100h 之前将 0000h 压入堆栈顶部。 CS:0000h 是 PSP 的开始,包含指令 int 20h,该指令依次退出到 DOS。
  • @MichaelPetch:嘿,我没有考虑优化算法本身之外的东西,但我猜在.com 文件中没有任何其他开销如此确定。 int 20h(是否通过 ret)是否设置退出状态 = 0?我在谷歌上找到的所有东西都称之为“没有”退出状态,除非 DOS 退出状态类似于标志+值或可以编码非状态而不是数字的东西,否则这是没有意义的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-01
  • 2012-02-04
  • 1970-01-01
  • 2012-08-16
  • 2017-05-18
  • 1970-01-01
  • 2018-07-08
相关资源
最近更新 更多