Nie ma żadnej przewagi, o której mogę myśleć (ale patrz uwaga dla JasonS na dole), owijanie jednego wiersza kodu jako funkcji lub podprogramu. Może z wyjątkiem tego, że można nazwać funkcję „czytelną”. Ale równie dobrze możesz skomentować linię. A ponieważ zawijanie wiersza kodu w funkcji kosztuje pamięć kodu, przestrzeń stosu i czas wykonywania, wydaje mi się, że jest to w większości przeciwne do zamierzonego. W sytuacji dydaktycznej? To może mieć sens. Ale to zależy od klasy uczniów, ich wcześniejszego przygotowania, programu nauczania i nauczyciela. W większości uważam, że to nie jest dobry pomysł. Ale to moja opinia.
Co prowadzi nas do dolnej linii. Pana obszerny obszar pytań jest od dziesięcioleci przedmiotem pewnej debaty i do dziś pozostaje przedmiotem debaty. Tak więc, przynajmniej gdy czytam twoje pytanie, wydaje mi się, że jest to pytanie oparte na opiniach (tak jak je zadałeś).
Można by odejść od tego, by opierać się na opiniach, gdybyś był bardziej szczegółowy na temat sytuacji i dokładnie opisał cele, które uważałeś za najważniejsze. Im lepiej zdefiniujesz narzędzia pomiarowe, tym bardziej obiektywne mogą być odpowiedzi.
Ogólnie rzecz biorąc, chcesz wykonać następujące czynności dla każdego kodowania. (Poniżej założyłem, że porównujemy różne podejścia, z których wszystkie osiągają cele. Oczywiście, każdy kod, który nie wykonuje wymaganych zadań, jest gorszy niż kod, który się powiedzie, niezależnie od tego, jak jest napisany.)
- Bądź konsekwentny w swoim podejściu, aby kolejny odczyt twojego kodu mógł zrozumieć, w jaki sposób podchodzisz do procesu kodowania. Niespójność jest prawdopodobnie najgorszym możliwym przestępstwem. To nie tylko utrudnia innym, ale utrudnia ci powrót do kodu po latach.
- W miarę możliwości spróbuj tak ułożyć rzeczy, aby inicjowanie różnych sekcji funkcjonalnych było możliwe bez względu na kolejność. Jeżeli wymagane jest zamówienie, jeśli wynika to z bliskiego powiązania dwóch ściśle powiązanych podfunkcji, rozważ jedną inicjalizację dla obu, aby można było zmienić kolejność bez powodowania szkody. Jeśli nie jest to możliwe, udokumentuj wymaganie dotyczące kolejności inicjalizacji.
- Hermetyzacji wiedza dokładnie w jednym miejscu, jeśli to możliwe. Stałe nie powinny być powielane w całym miejscu w kodzie. Równania rozwiązujące niektóre zmienne powinny istnieć w jednym i tylko jednym miejscu. I tak dalej. Jeśli zauważysz, że kopiujesz i wklejasz zestaw wierszy, które wykonują pewne wymagane zachowanie w różnych lokalizacjach, zastanów się, jak uchwycić tę wiedzę w jednym miejscu i wykorzystać ją w razie potrzeby. Na przykład, jeśli masz strukturę drzewa, którą trzeba kroczyć w określony sposób, nie rób tegoreplikuj kod chodzenia po drzewach w każdym miejscu, w którym musisz zapętlić węzły drzewa. Zamiast tego, złap metodę chodzenia po drzewie w jednym miejscu i użyj jej. W ten sposób, jeśli zmieni się drzewo i zmieni się metoda chodzenia, masz tylko jedno miejsce do zmartwienia, a cała reszta kodu „po prostu działa poprawnie”.
- Jeśli rozłożysz wszystkie swoje procedury na ogromny, płaski arkusz papieru, ze strzałkami łączącymi je, jak nazywają je inne procedury, zobaczysz w dowolnej aplikacji, że będą „klastry” procedur, które mają wiele strzałek między sobą, ale tylko kilka strzałek poza grupą. Będą więc naturalne granice ściśle powiązanych procedur i luźno powiązane połączenia między innymi grupami ściśle powiązanych procedur. Skorzystaj z tego faktu, aby uporządkować kod w moduły. To znacznie ograniczy pozorną złożoność kodu.
Powyższe jest ogólnie prawdziwe w odniesieniu do całego kodowania. Nie dyskutowałem o użyciu parametrów, lokalnych lub statycznych zmiennych globalnych itp. Powodem jest to, że w przypadku programowania wbudowanego przestrzeń aplikacji często nakłada ekstremalne i bardzo znaczące nowe ograniczenia i nie jest możliwe omówienie ich wszystkich bez omawiania każdej wbudowanej aplikacji. W każdym razie tak się nie dzieje.
Ograniczeniami tymi mogą być dowolne (i więcej) z tych:
- Poważne ograniczenia kosztów wymagające niezwykle prymitywnych MCU z niewielką pamięcią RAM i prawie zerową liczbą pinów we / wy. Do nich mają zastosowanie zupełnie nowe zestawy zasad. Na przykład może być konieczne wpisanie kodu asemblera, ponieważ nie ma dużo miejsca na kod. Może być konieczne użycie TYLKO zmiennych statycznych, ponieważ użycie zmiennych lokalnych jest zbyt kosztowne i czasochłonne. Być może będziesz musiał unikać nadmiernego korzystania z podprogramów, ponieważ (na przykład niektóre części Microchip PIC) są tylko 4 rejestry sprzętowe, w których można przechowywać adresy zwrotne podprogramów. Może być konieczne radykalne „spłaszczenie” kodu. Itp.
- Poważne ograniczenia mocy wymagające starannie spreparowanego kodu do uruchamiania i zamykania większości MCU oraz nakładania poważnych ograniczeń na czas wykonywania kodu przy pełnej prędkości. Ponownie może to czasami wymagać pewnego kodowania zestawu.
- Surowe wymagania dotyczące czasu. Na przykład zdarzają się sytuacje, w których musiałem się upewnić, że transmisja otwartego odpływu 0 musiała zająć DOKŁADNIE taką samą liczbę cykli jak transmisja 1. I że próbkowanie tej samej linii również musiało zostać wykonane z dokładną fazą względną w stosunku do tego czasu. Oznaczało to, że C NIE może być tutaj użyte. Jedynym możliwym sposobem uzyskania tej gwarancji jest staranne wykonanie kodu montażowego. (I nawet wtedy nie zawsze we wszystkich projektach ALU.)
I tak dalej. (Kod okablowania dla krytycznych dla życia instrumentów medycznych ma również swój własny świat.)
Konsekwencją tego jest to, że osadzone kodowanie często nie jest darmowym rozwiązaniem dla wszystkich, w którym można kodować tak, jak na stacji roboczej. Często występują poważne, konkurencyjne powody dla wielu różnych bardzo trudnych ograniczeń. A te mogą zdecydowanie argumentować przeciwko bardziej tradycyjnym i podstawowym odpowiedziom.
Jeśli chodzi o czytelność, uważam, że kod jest czytelny, jeśli jest napisany w spójny sposób, którego mogę się nauczyć podczas czytania. A tam, gdzie nie ma celowej próby zaciemnienia kodu. Naprawdę nie jest więcej wymagane.
Czytelny kod może być dość wydajny i może spełniać wszystkie powyższe wymagania, o których już wspomniałem. Najważniejsze jest to, że w pełni rozumiesz, co każdy wiersz, który piszesz, tworzy na poziomie zespołu lub maszyny, gdy go kodujesz. C ++ stanowi tutaj poważne obciążenie dla programisty, ponieważ istnieje wiele sytuacji, w których identyczne fragmenty kodu C ++ faktycznie generują różne fragmenty kodu maszynowego, które mają znacznie różną wydajność. Ale ogólnie C jest w większości językiem „to, co widzisz, dostajesz”. W związku z tym jest to bezpieczniejsze.
EDYCJA według JasonS:
Używam C od 1978 r. I C ++ od około 1987 r. Mam duże doświadczenie w korzystaniu zarówno z komputerów mainframe, minikomputerów, jak i (głównie) aplikacji osadzonych.
Jason komentuje użycie „inline” jako modyfikatora. (Z mojej perspektywy jest to stosunkowo „nowa” funkcja, ponieważ po prostu nie istniała przez około pół życia lub więcej przy użyciu C i C ++.) Korzystanie z funkcji wbudowanych może w rzeczywistości wykonywać takie wywołania (nawet dla jednej linii kod) całkiem praktyczny. I tam, gdzie to możliwe, jest znacznie lepsze niż używanie makra ze względu na typowanie, które może zastosować kompilator.
Ale są też ograniczenia. Po pierwsze, nie można polegać na kompilatorze, który „bierze podpowiedź”. Może, ale nie musi. I istnieją dobre powody, aby nie brać podpowiedzi. (Dla oczywistego przykładu, jeśli zostanie wzięty adres funkcji, wymaga to utworzenia instancji funkcji, a użycie adresu do wykonania połączenia będzie ... wymagało połączenia. Nie można wtedy wstawić kodu.) inne powody również. Kompilatory mogą mieć wiele różnych kryteriów, według których oceniają sposób obsługi podpowiedzi. A jako programista oznacza to, że musiszpoświęć trochę czasu na naukę tego aspektu kompilatora, w przeciwnym razie prawdopodobnie podejmiesz decyzje na podstawie wadliwych pomysłów. Jest to więc obciążenie zarówno dla autora kodu, jak i dla każdego czytnika, a także dla każdego, kto planuje przenieść kod na jakiś inny kompilator.
Ponadto kompilatory C i C ++ obsługują osobną kompilację. Oznacza to, że mogą skompilować jeden fragment kodu C lub C ++ bez kompilowania innego pokrewnego kodu dla projektu. Aby wstawić kod, przy założeniu, że kompilator mógłby to zrobić inaczej, nie tylko musi mieć deklarację „w zakresie”, ale także musi mieć definicję. Zwykle programiści będą pracować, aby upewnić się, że tak jest, jeśli używają „wbudowanego”. Ale łatwo jest wkraść się w błędy.
Zasadniczo, chociaż używam również inline tam, gdzie uważam to za właściwe, zwykle zakładam, że nie mogę na tym polegać. Jeśli wydajność jest istotnym wymogiem i myślę, że OP już wyraźnie napisał, że nastąpił znaczący spadek wydajności, gdy wybrali bardziej „funkcjonalną” trasę, to z pewnością wolałbym unikać polegania na inline jako praktyce kodowania i zamiast tego podążyłby za nieco innym, ale całkowicie spójnym wzorcem pisania kodu.
Ostatnia uwaga na temat „wstawiania” i definicji jest „w zakresie” dla oddzielnego kroku kompilacji. Możliwe jest (nie zawsze niezawodne) wykonanie pracy na etapie łączenia. Może się to zdarzyć tylko wtedy, gdy kompilator C / C ++ zakopuje w plikach obiektowych wystarczająco dużo szczegółów, aby linker mógł działać na żądanie „wbudowane”. Osobiście nie spotkałem systemu linkera (poza Microsoft), który obsługuje tę funkcję. Ale może się zdarzyć. Ponownie, to, czy należy na nim polegać, zależy od okoliczności. Ale zwykle zakładam, że nie został on przerzucony na linker, chyba że wiem inaczej na podstawie dobrych dowodów. A jeśli na tym polegam, zostanie to udokumentowane w widocznym miejscu.
C ++
Dla zainteresowanych, oto przykład, dlaczego zachowuję ostrożność wobec C ++ podczas kodowania aplikacji osadzonych, pomimo jego gotowej dostępności już dziś. Wyrzucę kilka terminów, które moim zdaniem wszyscy programiści C ++ powinni znać na zimno :
- częściowa specjalizacja szablonów
- tabele
- wirtualny obiekt podstawowy
- ramka aktywacyjna
- ramka aktywacyjna rozwija się
- wykorzystanie inteligentnych wskaźników w konstruktorach i dlaczego
- optymalizacja wartości zwracanej
To tylko krótka lista. Jeśli jeszcze nie wiesz wszystkiego o tych terminach i dlaczego je wymieniłem (i wielu innych nie wymieniłem tutaj), odradzam używanie C ++ do pracy osadzonej, chyba że nie jest to opcja dla projektu .
Rzućmy okiem na semantykę wyjątków C ++, aby uzyskać tylko smak.
ZAb
ZA
.
.
foo ();
String s;
foo ();
.
.
ZA
b
Kompilator C ++ widzi pierwsze wywołanie foo () i może po prostu pozwolić na normalne odwrócenie ramki aktywacji, jeśli foo () zgłosi wyjątek. Innymi słowy, kompilator C ++ wie, że w tym momencie nie jest potrzebny żaden dodatkowy kod do obsługi procesu odwijania ramki związanego z obsługą wyjątków.
Ale po utworzeniu String s kompilator C ++ wie, że musi zostać odpowiednio zniszczony, zanim będzie można zezwolić na odwijanie ramki, jeśli później wystąpi wyjątek. Drugie wywołanie foo () jest semantycznie różne od pierwszego. Jeśli drugie wywołanie funkcji foo () zgłasza wyjątek (który może, ale nie musi), kompilator musi umieścić kod zaprojektowany do obsługi zniszczenia String s przed zezwoleniem na zwykłe odwijanie ramki. Różni się to od kodu wymaganego do pierwszego wywołania foo ().
(Możliwe jest dodanie dodatkowych dekoracji w C ++, aby ograniczyć ten problem. Ale faktem jest, że programiści używający C ++ po prostu muszą być znacznie bardziej świadomi implikacji każdego wiersza kodu, który piszą.)
W przeciwieństwie do malloc C, nowe C ++ używa wyjątków do sygnalizowania, kiedy nie może wykonać surowej alokacji pamięci. Podobnie będzie z „dynamic_cast”. (Zobacz trzecie wydanie Stroustrup, The C ++ Programming Language, strony 384 i 385 dla standardowych wyjątków w C ++.) Kompilatory mogą pozwolić na wyłączenie tego zachowania. Ale generalnie poniesiesz trochę narzutu z powodu prawidłowo utworzonych prologów i epilogów obsługi wyjątków w wygenerowanym kodzie, nawet jeśli wyjątki faktycznie nie mają miejsca, a nawet gdy kompilowana funkcja nie ma żadnych bloków obsługi wyjątków. (Stroustrup publicznie narzekał).
Bez częściowej specjalizacji szablonów (nie wszystkie kompilatory C ++ ją obsługują), użycie szablonów może oznaczać katastrofę dla programowania wbudowanego. Bez tego rozkwit kodu jest poważnym ryzykiem, które może zabić flashowany projekt osadzony w małej pamięci.
Gdy funkcja C ++ zwraca obiekt, tymczasowy kompilator bez nazwy jest tworzony i niszczony. Niektóre kompilatory C ++ mogą zapewnić efektywny kod, jeśli w instrukcji return użyty jest konstruktor obiektów zamiast obiektu lokalnego, co zmniejsza potrzebę budowy i zniszczenia o jeden obiekt. Ale nie każdy kompilator to robi, a wielu programistów C ++ nawet nie zdaje sobie sprawy z tej „optymalizacji wartości zwracanej”.
Zapewnienie konstruktorowi obiektu jednego typu parametru może pozwolić kompilatorowi C ++ znaleźć ścieżkę konwersji między dwoma typami w całkowicie nieoczekiwany sposób dla programisty. Tego rodzaju „inteligentne” zachowanie nie jest częścią C.
Klauzula catch określająca typ podstawowy „wycina” rzucony obiekt pochodny, ponieważ rzucony obiekt jest kopiowany przy użyciu „typu statycznego” klauzuli catch, a nie „typu dynamicznego obiektu”. Nierzadkie źródło nieszczęścia wyjątków (gdy czujesz, że możesz sobie pozwolić na wyjątki we wbudowanym kodzie).
Kompilatory C ++ mogą automatycznie generować dla Ciebie konstruktory, destruktory, konstruktory kopiujące i operatory przypisania, z niezamierzonymi rezultatami. Zdobycie łatwości ze szczegółami tego zajmuje.
Przekazywanie tablic obiektów pochodnych do funkcji akceptującej tablice obiektów podstawowych rzadko generuje ostrzeżenia kompilatora, ale prawie zawsze powoduje nieprawidłowe zachowanie.
Ponieważ C ++ nie wywołuje destruktora częściowo skonstruowanych obiektów, gdy wystąpi wyjątek w konstruktorze obiektów, obsługa wyjątków w konstruktorach zwykle nakazuje „inteligentne wskaźniki” w celu zagwarantowania, że skonstruowane fragmenty w konstruktorze zostaną odpowiednio zniszczone, jeśli wystąpi tam wyjątek . (Patrz Stroustrup, strony 367 i 368.) Jest to częsty problem podczas pisania dobrych klas w C ++, ale oczywiście unika się go w C, ponieważ C nie ma wbudowanej semantyki konstrukcji i zniszczenia. Pisanie odpowiedniego kodu do obsługi konstrukcji podobiektów w obiekcie oznacza pisanie kodu, który musi poradzić sobie z tym unikalnym problemem semantycznym w C ++; innymi słowy „pisanie wokół” zachowań semantycznych C ++.
C ++ może kopiować obiekty przekazywane do parametrów obiektu. Na przykład w następujących fragmentach wywołanie „rA (x);” może spowodować, że kompilator C ++ wywoła konstruktor dla parametru p, aby następnie wywołać konstruktor kopiujący w celu przesłania obiektu x do parametru p, a następnie innego konstruktora dla obiektu zwracanego (nienazwanego tymczasowego) funkcji rA, co oczywiście jest skopiowane z parametru p. Co gorsza, jeśli klasa A ma własne obiekty, które wymagają konstrukcji, może to spowodować katastrofalny teleskop. (Programator AC uniknąłby większości tego śmiecia, optymalizując ręcznie, ponieważ programiści C nie mają tak przydatnej składni i muszą wyrażać wszystkie szczegóły pojedynczo).
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Na koniec krótka uwaga dla programistów C. longjmp () nie ma przenośnego zachowania w C ++. (Niektórzy programiści C używają tego jako swoistego mechanizmu „wyjątku”). Niektóre kompilatory C ++ będą faktycznie próbowały ustawić rzeczy do wyczyszczenia, gdy pobierany jest longjmp, ale takie zachowanie nie jest przenośne w C ++. Jeśli kompilator czyści skonstruowane obiekty, nie jest przenośny. Jeśli kompilator nie wyczyści ich, obiekty nie zostaną zniszczone, jeśli kod opuści zakres skonstruowanych obiektów w wyniku longjmp, a zachowanie jest nieprawidłowe. (Jeśli użycie longjmp w foo () nie pozostawia zasięgu, zachowanie może być w porządku). Nie jest to zbyt często używane przez programistów osadzonych w języku C, ale powinni oni zapoznać się z tymi problemami przed ich użyciem.