鱼C论坛

 找回密码
 立即注册
查看: 2289|回复: 32

[已解决]如何让程序一次读一个字符且不需要按下回车就能执行

[复制链接]
发表于 2022-8-14 00:21:13 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
        大家好,linux中有个 less 函数用来分页,分页的时候,按下键盘 j 键就自动执行向下滚动一行的操作,我也想实现一个 “一次只读一个字符且不需要按下回车就可以执行” 的机制,但试了一些 system call 函数比如 read(), write() 还是实现不了这个机制(初学,可能没有理解这些函数的正确用法)
       
        所以,我的问题是:
                1. 有没有现成的库函数或系统调用,能实现这个机制?
                2. read() 的机制是怎样的?即使我指定 read(0, buf, 1) 一次只读一个字符(? 是一个字符吗),它还是需要按下回车才读,而且好像,就算我输入“asdf”,read(0, buf, 1) 读到 buf 里后,调用 write(1, buf, 1) 却输出了 “asdf” 而不是我期望的 'a'。
                3. 当我在终端输入“asdf”且没按回车时,这4个字符显示在了终端上,是不是说明 stdin 已经接收到了?那此时,这些字符存到哪里了?有没有可能直接访问到存储这些字符的地方,并取一个字符出来(取'a')?
                4. 题外话,less 源码看不懂。。。less 里这个机制是怎么实现的呢?
       
        谢谢大家!
最佳答案
2022-8-14 01:25:42
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>

int main(void) {
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag &= ~ICANON;
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    char ch;
    scanf("%c", &ch);
    printf("'%c'\n", ch);
    printf("'%c'\n", getchar());
    read(STDIN_FILENO, &ch, 1);
    printf("'%c'\n", ch);
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag |= ICANON;
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    return 0;
}
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2022-8-14 01:25:42 | 显示全部楼层    本楼为最佳答案   
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>

int main(void) {
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag &= ~ICANON;
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    char ch;
    scanf("%c", &ch);
    printf("'%c'\n", ch);
    printf("'%c'\n", getchar());
    read(STDIN_FILENO, &ch, 1);
    printf("'%c'\n", ch);
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag |= ICANON;
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    return 0;
}
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 07:10:21 | 显示全部楼层
另一个比较有意思的选项是 ECHO,复位这个标志可以让你在输入的时候屏幕上什么也不显示
passwd程序可以让你在输入密码的时候,屏幕上面什么也不显示,passwd程序就是这么整的
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>

int main(void) {
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag &= ~(ICANON | ECHO);
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    char ch;
    scanf("%c", &ch);
    printf("'%c'\n", ch);
    printf("'%c'\n", getchar());
    read(STDIN_FILENO, &ch, 1);
    printf("'%c'\n", ch);
    {
        struct termios termios;
        ioctl(STDIN_FILENO, TCGETS, &termios);
        termios.c_lflag |= ICANON | ECHO;
        ioctl(STDIN_FILENO, TCSETS, &termios);
    }
    return 0;
}
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 07:19:31 | 显示全部楼层
"就算我输入“asdf”,read(0, buf, 1) 读到 buf 里后,调用 write(1, buf, 1) 却输出了 “asdf” 而不是我期望的 'a'。"
看一下你的代码
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 07:21:01 | 显示全部楼层
less 使用 ncurses 实现的,ncurses 了解一下
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 07:27:01 | 显示全部楼层
"当我在终端输入“asdf”且没按回车时,这4个字符显示在了终端上"
这是因为设置了 ECHO 标志的原因,没按回车之前 stdin 应该是没有收到字符,这些字符应该是存在内核缓冲区中的(应该是这样)
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 07:31:05 | 显示全部楼层
"有没有可能直接访问到存储这些字符的地方,并取一个字符出来(取'a')?"
当你的程序调用 scanf、getchar、read这类获取输入的函数以后,你的程序就阻塞了,不再继续运行了
如果没有复位 ICANON 标志,在按下回车之前,你的程序都一直阻塞着,得不到运行的
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 08:21:15 | 显示全部楼层
(gdb) print *stdin
$1 = {_flags = -72539512, _IO_read_ptr = 0x4052a2 "cd1234\n", _IO_read_end = 0x4052a9 "",
  _IO_read_base = 0x4052a0 "abcd1234\n", _IO_write_base = 0x4052a0 "abcd1234\n",
  _IO_write_ptr = 0x4052a0 "abcd1234\n", _IO_write_end = 0x4052a0 "abcd1234\n", _IO_buf_base = 0x4052a0 "abcd1234\n",
  _IO_buf_end = 0x4056a0 "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0,
  _chain = 0x0, _fileno = 0, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000',
  _shortbuf = "", _lock = 0x7ffff7e4e750 <_IO_stdfile_0_lock>, _offset = -1, _codecvt = 0x0,
  _wide_data = 0x7ffff7e4b7e0 <_IO_wide_data_0>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1,
  _unused2 = '\000' <repeats 19 times>}
#include <stdio.h>

int main(void) {
    // getchar函数把输入的一行字符存入stdin中的缓冲区
    // 然后返回第0个字符
    int ch = getchar();
    // 上面的getchar获取了第0个字符,这里读指针减1之后
    // 读指针指向的位置就是上面getchar返回的那个字符
    --stdin->_IO_read_ptr;
    // 这里验证一下上面说的,看看读指针的位置是不是和变量ch中的字符一样
    if(ch == stdin->_IO_read_ptr[0]) {
        for(char *p = stdin->_IO_read_ptr; p != stdin->_IO_read_end; ++p) {
            printf("'%c' ", *p);
        }
        puts("");
    }
    // 让读指针指向当前输入行的后面
    // 让之后使用stdin的函数去获取新的一行字符
    stdin->_IO_read_ptr = stdin->_IO_read_end;
    scanf("%c", (char *)&ch);
    // 再来一次
    --stdin->_IO_read_ptr;
    if(ch == stdin->_IO_read_ptr[0]) {
        for(char *p = stdin->_IO_read_ptr; p != stdin->_IO_read_end; ++p) {
            printf("'%c' ", *p);
        }
        puts("");
    }
    return 0;
}
$ gcc-debug -o main main.c
$ ./main
asdf1234
'a' 's' 'd' 'f' '1' '2' '3' '4' '
'
zxcv09876
'z' 'x' 'c' 'v' '0' '9' '8' '7' '6' '
'
$

注意,stdin里面的这些变量是下划线开头的,下划线开头的名字是保留的,你不应该使用这些名字
上面的程序很有可能无法在你那边运行,因为这个程序假设stdin里面有_IO_read_ptr这些名字
但是stdin完全可以不是这个样子,stdin是什么样子,这取决于实现

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 09:35:14 | 显示全部楼层
#include <stdio.h>
#include <conio.h>

int main(void)
{
        char c , s[2048]                      ;
        int n                                 ;
        printf("请输入密码 : ")               ;        
        for(n = 0 ; (c = getch()) != 3 && c != 13 && c != 27 ;) {
                if(c >= ' ' && c <= 127) {
                        printf("*")           ;
                        s[n ++] = c           ;
                } else if(c == 8 && n) {
                        printf("\b \b")       ;
                        n --                  ; 
                }       
        }
        printf("\n")                          ;
        s[n] = 0                              ;
        if(n) printf("你的密码是 : %s\n" , s) ;
}
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 09:38:31 | 显示全部楼层
本帖最后由 人造人 于 2022-8-14 09:39 编辑


conio.h,不是C标准库中的头文件,在C standard library,ISO C 和POSIX标准中均没有定义。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 09:42:05 | 显示全部楼层
人造人 发表于 2022-8-14 09:38
conio.h,不是C标准库中的头文件,在C standard library,ISO C 和POSIX标准中均没有定义。

         有何不妥吗,是 VC 不支持还是 GNU C 编译器不支持?
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 09:44:51 | 显示全部楼层
jackz007 发表于 2022-8-14 09:42
有何不妥吗,是 VC 不支持还是 GNU C 编译器不支持?

linux 下新版本的gcc也没有这个头文件
$ gcc --version
gcc (GCC) 12.1.1 20220730
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -g -Wall -o main main.c
main.c:2:10: fatal error: conio.h: No such file or directory
    2 | #include <conio.h>
      |          ^~~~~~~~~
compilation terminated.
$
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 09:56:31 | 显示全部楼层
人造人 发表于 2022-8-14 09:44
linux 下新版本的gcc也没有这个头文件
D:\[00.Exerciese.2022]\C>gcc --version
gcc (GCC) 12.1.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

D:\[00.Exerciese.2022]\C>g++ -o x x.c

D:\[00.Exerciese.2022]\C>
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 10:00:07 | 显示全部楼层

看起来只有windows有这个头文件
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 10:26:46 | 显示全部楼层
本帖最后由 jackz007 于 2022-8-14 10:36 编辑
人造人 发表于 2022-8-14 10:00
看起来只有windows有这个头文件


       看来所谓的 "非标准" 就是指的这个了。
       以前,我曾经在 Unix 系统上使用过 ncurses 库,同样可以使用 getch(),只不过相应的头文件是
#include <curses.h>
       编译的时候要特别指定 ncurses 库
g++ -lcurses -o x x.c
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 10:41:01 | 显示全部楼层
jackz007 发表于 2022-8-14 10:26
看来所谓的 "非标准" 就是指的这个了。
       以前,我曾经在 Unix 系统上使用过 ncurses 库 ...

嗯,我也了解过ncurses
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 11:10:38 | 显示全部楼层
人造人 发表于 2022-8-14 08:21
注意,stdin里面的这些变量是下划线开头的,下划线开头的名字是保留的,你不应该使用这些名字
...

好厉害啊。

因为很多知识点太少用了,我很多都忘记了。程序语言真的需要时常写。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2022-8-14 11:25:50 | 显示全部楼层
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2022-8-14 11:57:34 | 显示全部楼层
人造人 发表于 2022-8-14 08:21
注意,stdin里面的这些变量是下划线开头的,下划线开头的名字是保留的,你不应该使用这些名字
...

谢谢这么详细的回答!!!因为只能选一个最佳答案,所以就选了第一个回答。

学到了 ICANON 和 ECHO 两个标志,确实有意思
刚才好奇为什么要复位 ICANON 和 ECHO, 就把那段删了,然后运行一遍之后,我的命令行输入就“隐身”了。。。吓死,赶紧还原了。没想到是直接影响了当前控制台了。

还想问一下,代码中,把 termios 那 4 行单独用 { } 括起来,只是为了代码的可读性呢,还是说有什么别的用途?

最后,再次感谢回答!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2022-8-14 12:00:12 | 显示全部楼层

谢谢回答!!

我在 阿里云的服务器上试了一下,也报错说没有 conio.h(gcc version 10.2.1)

但还是谢谢你的回答!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-11-23 19:05

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表