Akurat jeśli chodzi o ONP, to jest to ułatwienie, a nie utrudnienie. ONP przetwarza się bardzo łatwo, klasyczną notację infiksową (czyli np. 2+2) -- trudno. Oczywiście, nie możesz zastosować tu po prostu ONP, bo zadanie wyraźnie mówi o tym, że dane zostaną zapisane w postaci infiksowej.
Ale istnieją algorytmy umożliwiające przeprowadzenie konwersji z notacji infiksowej do ONP. Są opisane choćby w artykule na polskiej wikipedii.
Skonwertowanie notacji infiksowej do ONP, a następnie parsowanie i obliczanie wyrażenia może być prostsze od przetwarzania bezpośrednio notacji infiksowej.
Nie zawsze jednak polecam to zastosować. Jeśli program rozumiałby notację infiksową, to można by prawdopodobnie lepiej zaimplementować obsługę błędów.
W przypadku notacji infiksowej można pokusić się o zbudowanie w pamięci drzewa obiektów, które reprezentują poszczególne wyrażenia. Będą to zarówno proste operandy (np. 2 czy 47), jak i operatory wraz z argumentami (przy czym każdy z argumentów jest wyrażeniem, a więc może zawierać operatory).
Przeważnie takie drzewa buduje się dwuetapowo. Najpierw jakiś analizator leksykalny (lekser) dokonuje wstępnego przetwarzania, wczytując kolejne znaki i robiąc z nich listę jakichś bardziej złożonych tokenów. Następnie analizator składniowy (parser) przerabia tę listę na drzewo wyrażeń. W C++ wyrażenia można ładnie zakodować jako polimorficzne obiekty. Tj. każde wyrażenie ma metodę oblicz(). Po klasie Wyrażenie dziedziczą inne klasy, np. Stała (w konstruktorze podaje się jej wartość). Funkcja oblicz() dla stałej 5 zwraca po prostu 5. Inną podklasą Wyrażenia jest np. Dodawanie i jego funkcja oblicz() zwraca prawyOperand->oblicz() + lewyOperand->oblicz(), przy czym prawy- i lewyOperand to także wyrażenia. Po zbudowaniu drzewa składni wystarczy więc wywołać korzeń->oblicz() i wszystko piękne nam się policzy.
Taka obiektowa reprezentacja jest trochę skomplikowana, ale może to być niezłe ćwiczenie w języku obiektowym takim jak C++.
Nie musisz jednak faktycznie stosować polimorficznych obiektów; w ogóle nie musisz stosować obiektowości. Parser może od razu liczyć wartość wyrażenia.
Aby przetwarzać zagnieżdżone nawiasy ważne jest, żeby parser działał rekurencyjnie. Jeśli masz zagwarantowane, że nawiasy zawsze będą użyte, to to też jest sporym ułatwieniem, bo w każdym nawiasie na danym poziomie masz tylko jeden operator.
Funkcja obliczWyrażenie() może działać np. tak (wymyślam to na bieżąco, mam nadzieję że się jakoś głupio nie rypnę):
-Jeśli na początku wejścia jest liczba, to wczytaj ją i zwróć (uwaga: wczytanie oznacza też zwiększenie aktualnej pozycji na wejściu, może to być inkrementacja indeksu lub inkrementacja wskaźnika).
-W przeciwnym wypadku powinien się tam znajdować nawias otwierający. Wykonaj następujące kroki:
--Wczytaj nawias otwierający (jeśli go nie ma -> błąd)
--Wywołaj rekurencyjnie obliczWyrażenie() i zapisz zwróconą wartość w zmiennej lewyOperand (przed tym wywołaniem pozycja na wejściu była ustawiona tuż za nawiasem otwierającym; po wywołaniu będzie za pierwszym argumentem, przy czym pierwszym argumentem może być liczba lub złożone wyrażenie z nawiasem; po wywołaniu powinniśmy być ustawieni na operatorze)
--Wczytaj operator i zapisz w zmiennej operator (może być typu char: '+' albo '-').
--Wywołaj rekurencyjnie obliczWyrażenie() i zapisz zwróconą wartość w zmiennej prawyOperand
--Wczytaj nawias zamykający (jeśli go nie ma -> błąd)
--W zależności od wartości zmiennej operator zwróć lewyOperand + prawyOperand lub lewyOperand - prawyOperand.
I tyle, to raczej prosty algorytm, niewymagający jakiegoś przeszukiwania nawiasów. Dzięki odpowiedniemu ułożeniu wywołań rekurencyjnych oraz właściwego wykonywania obliczeń (najpierw wywołania, na końcu funkcji obliczenia) zapewniamy, że działanie z najbardziej zagnieżdżonego nawiasu zostanie wykonane jako pierwsze.
BTW: w tej wersji funkcja wysypie się, jeśli będziesz miał podwójne nawiasy, czyli np. ((2 + 2)) -- nie wiem, czy funkcja musi to obsługiwać, czy nie. Zresztą można to łatwo zaimplementować. Póki co, gdy wczytujemy nawias, oczekujemy jedynej słusznej sekwencji: nawias otwierający->wyrażenie->operator->wyrażenie->nawias zamykający. Możemy dopuszczać drugą opcję: nawias otwierający->wyrażenie->nawias zamykający. Czyli jeśli zaraz po pierwszym wyrażeniu następuje nie operator, tylko nawias zamykający, to wczytujemy go, nie protestujemy i zwracamy lewyOperand. I obsłużymy przypadek ((takich+nawiasów)).
To zadziała dlatego, że wyrażenia z nawiasu przetwarzasz po prostu wywołaniem rekurencyjnym obliczWyrażenie() i nie obchodzi Cię, czy tam w środku jest jeszcze jakiś nawias, czy nie. Jedno wywołanie obliczWyrażenie() martwi się tylko o jeden poziom nawiasów i jedno działanie.