How do you write a C program to increment a number by keypress and auto-decrement it per second?
Asked Answered
N

8

13

I'm trying to write a program in which a number starts from 0, but when you press any key, it gets incremented by 1. If nothing is pressed, it keeps on decreasing by 1 per second until it reaches 0. Every increment or decrement is displayed on the console window.

Problem with my approach is that nothing happens until I press a key (that is, it checks if anything is pressed with getch()). How do I check that nothing is pressed? And of course, !getch() doesn't work because for that to work, it'll still need to check for keypress which nullifies the purpose itself.

OS: Windows 10 Enterprise, IDE: Code::Blocks

void main()
{
    int i, counter = 0;
    for (i = 0; i < 1000; i++)
    {
        delay(1000);
        // if a key is pressed, increment it
        if (getch())
        {
            counter += 1;
            printf("\n%d", counter);
        }
        while (counter >= 1)
        {
            if (getch())
            {
                break;
            }
            else
            {
                delay(1000);
                counter--;
                printf("\n%d", counter);
            }
        }
    }
}
Nitpicking answered 9/10, 2018 at 7:46 Comment(6)
Why not? If there's some adverse effect I don't know about, what alternative can I use to simulate a delay? Nested for loops are highly CPU consuming so I avoided that. I do not want the decrement to happen immediately because that woudn't give the user enough time to increment it in the first place.Nitpicking
If you want to do multiple things at once (waiting for delay to finish and waiting for a keystroke), you might want to look into multithreadingShaker
How would you handle a keypress while you are blocked in a delay()?Ninepins
For Windows you want #41213146 : call GetNumberOfConsoleInputEvents and if it's above zero call ReadConsoleInput.Campy
If this is for a real-life thing, consider using a third-party event library, like nikhilm.github.io/uvbook/basics.html. You'd have two listeners, a timer and a stream, and each handler would increment/decrement.Harrell
"Nested for loops are highly CPU consuming". That is not a generally true statement.Elery
O
11

The following short program requires neither ncurses or threads. It does, however, require changing the terminal attributes - using tcsetattr(). This will work on Linux and Unix-like systems, but not on Windows - which does not include the termios.h header file. (Perhaps see this post if you are interested in that subject.)

#include <stdio.h>
#include <string.h>
#include <termios.h>

int main(int argc, char *argv[]) {
    struct termios orig_attr, new_attr;
    int c = '\0';
    // or int n = atoi(argv[1]);
    int n = 5;

    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    new_attr.c_cc[VMIN] = 0;
    // Wait up to 10 deciseconds (i.e. 1 second)
    new_attr.c_cc[VTIME] = 10; 
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    printf("Starting with n = %d\n", n);
    do {
        c = getchar();
        if (c != EOF) {
            n++;
            printf("Key pressed!\n");
            printf("n++ => %d\n", n);
        } else {
            n--;
            printf("n-- => %d\n", n);
            if (n == 0) {
                printf("Exiting ...\n");
                break;
            }
            if (feof(stdin)) {
                //puts("\t(clearing terminal error)");
                clearerr(stdin);
            }
        }
    } while (c != 'q');

    tcsetattr(fileno(stdin), TCSANOW, &orig_attr);

    return 0;
}

The vital points are that

new_attr.c_lflag &= ~(ICANON | ECHO);

takes the terminal out of canonical mode (and disables character 'echo'),

new_attr.c_cc[VMIN] = 0;

places it in polling (rather than 'blocking') mode, and

new_attr.c_cc[VTIME] = 10;

instructs the program to wait up till 10 deciseconds for input.

Update (2019-01-13)

  • add clearerr(stdin) to clear EOF on stdin (seems to be necessary on some platforms)
Ollie answered 9/10, 2018 at 8:25 Comment(8)
int c to be able to safely compare with EOF :)Cobby
@pmg: Thanks. I see that ilkkachu has already made the relevant edit.Ollie
An alternative to setting VTIME would be to use select() to see if input becomes available within a particular time period. You'd still need to put the terminal in raw mode to be able to detect anything but full lines, though.Marinemarinelli
Doesn't work for me (FreeBSD 11.2) :( Basically counting up is ok. but the first count down (if I don't hit a key within a second) goes all the way to 0 in a flash.Cobby
@Cobby I witnessed a similar problem on certain Android platforms. Clearing the EOF flag on stdin solved the problem. (I have updated the answer.) I would be curious if that fixes the problem for you on FreeBSD also.Ollie
@DavidCollins: I'll check tomorrow, can't reach my FreeBSD box today.Cobby
@Cobby Cool. I look forward to learning how it goes - if / when you have time.Ollie
David: with your new edit, it works on FreeBSD! I also tested on a Debian 9 (stretch) on which it works with or without the clearerr() call. However, for the debian box, I needed to #define _POSIX_C_SOURCE 200112L to obtain a clean compilation.Cobby
U
4

This could be done with multithreading as already suggested, but there are other possibilities.

ncurses for example has the possibility to wait for input with a timeout.

An example for ncurses (written by Constantin):

initscr();
timeout(1000);
char c = getch();
endwin();
printf("Char: %c\n", c);

I think poll could also be used on stdin to check if input is available.

And to make your program more responsive you could lower your sleep or delay to for example 100ms and only decrement if ten iterations of sleep have passed without input. This will reduce the input lag.

Usurer answered 9/10, 2018 at 8:14 Comment(0)
K
2

Here is a pthread example that works on linux. The concept is ok, but there are probably existing loops/libraries for this.

#include <stdio.h>
#include<pthread.h>


void *timer(void* arg){
    int* counter = (int*)arg;
    while(*counter > 0){
        int a = *counter;
        printf("counter: %d \n", a);
        *counter = a - 1;
        sleep(1);
    }
}

int main(int arg_c, char** args){
    int i = 100;
    pthread_t loop;

    pthread_create(&loop, NULL, timer, &i);

    while(i>0){
        i++;
        getchar();
        printf("inc counter: %d \n", i);
    }
    printf("%d after\n", i);

    pthread_join(loop, NULL);

    return 0;
}

This starts a second thread, which has the countdown on it. That decrements the counter every second. On the main thread it has a loop with getchar. They both modify i.

Kalikalian answered 9/10, 2018 at 8:7 Comment(0)
T
2

You need use thread, and need use __sync_add_and_fetch and __sync_sub_and_fetch to avoid concurrency problem

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <iostream>

static void* thread(void* p) {
    int* counter = (int*)p;
    while (1) {
        if (*counter > 0) {
            __sync_sub_and_fetch(counter, 1);
            printf("sub => %d\n", *counter);
        }  else {
            sleep(1);
        }
    }

    return NULL;
}

int main() {
    int counter = 0;
    char ch;

    struct termios orig_attr, new_attr;
    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    pthread_t pid;
    if (pthread_create(&pid, NULL, thread, &counter)) {
        fprintf(stderr, "Create thread failed");
        exit(1);
    }

    while(1) {
      char c = getchar();
      __sync_add_and_fetch(&counter, 1);
      printf("add: %d\n", counter);
    }

    return 0;
}
Triune answered 9/10, 2018 at 8:31 Comment(0)
C
2

Another example using ncurses and POSIX timers and signals (and global variables).

#include <ncurses.h>
#include <signal.h>
#include <time.h>

int changed, value;

void timer(union sigval t) {
        (void)t; // suppress unused warning
        changed = 1;
        value--;
}

int main(void) {
        int ch;
        timer_t tid;
        struct itimerspec its = {0};
        struct sigevent se = {0};

        se.sigev_notify = SIGEV_THREAD;
        se.sigev_notify_function = timer;
        its.it_value.tv_sec = its.it_interval.tv_sec = 1;
        timer_create(CLOCK_REALTIME, &se, &tid);
        timer_settime(tid, 0, &its, NULL);

        initscr();
        halfdelay(1); // hit Ctrl-C to exit
        noecho();
        curs_set(0);

        for (;;) {
                ch = getch();
                if (ch != ERR) {
                        changed = 1;
                        value++;
                }
                if (changed) {
                        changed = 0;
                        mvprintw(0, 0, "%d ", value);
                        refresh();
                }
        }

        endwin();
}
Cobby answered 9/10, 2018 at 9:8 Comment(0)
S
2

Here is another way that uses select to check if input exists and also to wait. Not a pretty solution but it works. Linux only though.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/select.h>

#define WAIT_TIME 1000 //Delay time in milliseconds

bool inputExists(void)
{
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0, &readfds);

    struct timeval tv;
    tv.tv_sec = tv.tv_usec = 0;

    if(select(1, &readfds, NULL, NULL, &tv))
        return true;
    else
        return false;
}

void wait()
{
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = WAIT_TIME * 1000;
    select(0, NULL, NULL, NULL, &tv);
}

int main(void)
{
    system("stty raw"); /* Switch to terminal raw input mode */

    unsigned int count = 0;
    for(;;)
    {
        if(inputExists())
        {
            char input[256] = {0};
            read(0, input, 255);
            count += strlen(input);

            printf("\rCount is now %d\n", count);
        }
        else if(count > 0)
        {
            count--;
            printf("\rDecremented count to %d\n", count);
        }

        puts("\rWaiting...");
        wait();
    }
}

A better way that avoids system("stty raw") and those \rs would be to use tcgetattr and tcsetattr:

struct termios orig_attr, new_attr;

tcgetattr(STDIN_FILENO, &orig_attr);
new_attr = orig_attr;
new_attr.c_lflag &= ~(ICANON | ECHO); //Disables echoing and canonical mode
tcsetattr(STDIN_FILENO, TCSANOW, &new_attr);

//...

tcsetattr(STDIN_FILENO, TCSANOW, &old_attr);
Sphalerite answered 9/10, 2018 at 13:4 Comment(0)
I
1

If you're not bothered about portability, and you'll always be using Windows, you can use PeekConsoleInput, which tells you what console input events are waiting.

You can't (easily) use ReadConsoleInput, because it blocks until there is at least one pending input event.

Ilowell answered 9/10, 2018 at 14:4 Comment(0)
B
0

Your code has two problems; one serious, one not.

The first problem, as you have found out, is that getch() is a blocking function. In other words, the function call will not return until a key had been pressed.

The second problem, although minor, is that the program only responds to input every second.

I've modified your requirements slightly, starting the initial counter at 5.

#include <windows.h>

int main(void)
{
  int Counter;
  time_t StartTime;
  DWORD EventCount;


  Counter=5;
  do
  {
    StartTime=time(NULL);
    do
    {
      Sleep(10);  /* in ms. Don't hog the CPU(s). */
      GetNumberOfConsoleInputEvents(GetStdHandle(STD_INPUT_HANDLE),&EventCount);
    }
    while( (StartTime==time(NULL)) && (EventCount==0) );  
        /* Wait for a timeout or a key press. */

    if (EventCount!=0)  /* Have a key press. */
    {
      FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));  /* Clear the key press. */
      Counter++;
    }
    else  /* Timed out. */
      Counter--;

    printf("Counter = %d\n",Counter);
  }
  while(Counter>0);

  return(0);
}

Compiled using Microsoft Visual C++ 2015 (command line: "cl main.c").
Tested on Windows 7 and 10.

Bagpipe answered 9/10, 2018 at 19:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.