Ma quindi tu non hai bisogno di vedere un'applicazione medio-grossa in C++, ti basta la più semplice delle applicazioni grafiche.
Il problema è che in C++ non c'è un metodo standard per gestire la grafica, ma dipende da sistema operativo a sistema operativo (anche se ci sono dei toolkit multipiattaforma che semplificano molto il lavoro).
Avrei fatto un while(true) e dentro una funzione che controlla ricorsivamente cosa fa l'utente e nel caso venisse cliccato questo bottone mostrerei l'output.
In realtà di base non è molto diverso da come dici. 
Alla base di tutto (sia in Windows che sui sistemi *NIX con X11, non so come faccia Cocoa) c'è il cosiddetto "message loop", che è del codice del tipo:
codice:
while(RecuperaMessaggio(...))
{
ConsegnaMessaggio(...);
}
RecuperaMessaggio non fa altro che chiedere al sistema operativo se ci sono messaggi per l'applicazione (l'utente ha cliccato su un controllo/ha mosso il mouse/premuto un tasto/è necessario ridisegnare la finestra/...); se non c'è niente l'applicazione semplicemente rimane bloccata nella RecuperaMessaggio (in questo modo non spreca CPU), se c'è qualcosa viene fornito il messaggio e l'applicazione lo gestisce.
Tipicamente poi c'è una funzione ConsegnaMessaggio (su Windows è la DispatchMessage) che, in base al destinatario del messaggio, lo "consegna" alla funzione appropriata (su Windows puoi registrare diverse "classi" di finestre, a ognuna delle quali corrisponde una "window procedure" che gestisce i messaggi destinati alle finestre corrispondenti).
Poi i vari toolkit grafici C++ "astraggono" questo concetto e ti consentono di lavorare con classi & co., semplificandoti il lavoro; ad esempio, in un'applicazione Qt tutto il mio main consiste in
codice:
#include <QApplication>
#include <QTime>
#include "solverdialog.hpp"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qsrand(QTime::currentTime().msec());
SolverDialog w;
w.show();
return a.exec();
}
con SolverDialog che eredita da una classe-finestra astratta e si occupa di registrare la finestra, gestire i suoi messaggi & co., e a.exec() di fatto "nasconde" il message loop.
Tipicamente la classe-finestra astratta fornisce tutta una serie di metodi virtuali, che vengono chiamati ogni volta che arriva un messaggio di un certo tipo; nella tua classe derivata poi tu vai a reimplementare questi metodi virtuali in modo da ricevere e gestire i messaggi che ti interessano.
(in realtà con le Qt è un po' diverso, si usa un meccanismo di signal/slot, ma il concetto è simile)