Istnieją pewne pytania, które mają tendencję do ciągłego zadawania pytań. W tym FAQ spróbujemy odpowiedzieć na te najczęściej spotykane.
P1: Dlaczego nie powinniśmy używać „ using namespace std”?
Oświadczenie using namespace std; jest za pomocą dyrektywy. Dyrektywa using umożliwia dostęp do wszystkich identyfikatorów z danej przestrzeni nazw w zakresie instrukcji using-dyrektywy.
Być może widziałeś coś takiego:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!";
return 0;
}Pozwala nam to używać nazw z std przestrzeni nazw bez konieczności jawnego wpisywania std:: w kółko. W powyższym programie możemy po prostu wpisać cout zamiast std::cout. Brzmi świetnie, prawda?
Jednak gdy kompilator napotka using namespace std, udostępni każdy pojedynczy identyfikator w std przestrzeni nazw z zakresu globalnego (ponieważ tam została umieszczona dyrektywa using). Wiąże się to z 3 kluczowymi wyzwaniami:
- Ryzyko kolizji nazewnictwa pomiędzy wybraną nazwą a nazwą, która już istnieje w
stdprzestrzeni nazw znacznie wzrasta. - Nowe wersje standardowej biblioteki mogą spowodować uszkodzenie aktualnie działającego programu. Te przyszłe wersje mogą wprowadzać nazwy powodujące nowe kolizje nazewnictwa lub, w najgorszym przypadku, zachowanie programu może zmienić się cicho i nieoczekiwanie!
- Brak
std::przedrostków utrudnia czytelnikom zrozumienie, czym jeststdnazwa biblioteki, a czym nazwa zdefiniowana przez użytkownika.
Z tych powodów zalecamy unikanie using namespace std (lub jakakolwiek inna dyrektywa dotycząca użycia) całkowicie. Niewielkie oszczędności na pisaniu nie są warte dodatkowych problemów i przyszłego ryzyka.
Powiązana treść
Zobacz lekcję >7.13 -- Używanie deklaracji i dyrektyw aby uzyskać więcej szczegółów i przykładów.
>Pytanie 2: Dlaczego mogę użyć jakiejś funkcji lub typu bez dołączania nagłówka deklarującego tę funkcję lub typ?
Na przykład wielu czytelników pyta, dlaczego ich program to robi używa std::string_view trzeba #include <string_view> kiedy wydaje się, że działa dobrze bez #include.
Nagłówki mogą #include inne nagłówki. Kiedy #include nagłówek, otrzymasz także wszystkie dodatkowe nagłówki, które zawiera (oraz wszystkie nagłówki, które te nagłówki również zawierają). Wszystkie dodatkowe nagłówki towarzyszące przejazdowi, których nie uwzględniłeś wyraźnie, nazywane są „dołączeniami przechodnimi”.
W Twoim main.cpp pliku prawdopodobnie #include <iostream>. W twoim kompilatorze, jeśli nagłówek <iostream> zawiera nagłówek <string_view> (na jego własny użytek), to gdy #include <iostream> otrzymasz zawartość nagłówka <string_view> (i wszelkich innych nagłówków, które zawiera <iostream>). Oznacza to, że Twój main.cpp będzie mógł używać typu std::string_view bez jawnego dołączania nagłówka <string_view>.
Nawet jeśli może to zostać skompilowane w twoim kompilatorze, nie powinieneś na tym polegać. To, co kompiluje się teraz, może nie zostać skompilowane w innym kompilatorze, ani nawet w przyszłej wersji twojego kompilatora.
Nie ma sposobu, aby ostrzec, kiedy to nastąpi, ani zapobiec temu. Najlepsze, co możesz zrobić, to zadbać o wyraźne dołączenie odpowiednich nagłówków dla wszystkich rzeczy, których używasz. Kompilowanie programu na kilku różnych kompilatorach może pomóc w zidentyfikowaniu nagłówków, które są przechodnio dołączane do innych kompilatorów.
Powiązana treść
Opisane w lekcji 2.11 — Pliki nagłówkowe.
Pytanie 3: Mój kod generujący niezdefiniowane zachowanie wydaje się działać poprawnie. Czy to w porządku?
aka: "Zrobiłem to, czego mi zabroniłeś i zadziałało. Więc w czym problem?"
Niezdefiniowane zachowanie ma miejsce, gdy wykonujesz operację, której zachowanie nie jest zdefiniowane w języku C++. Kod implementujący niezdefiniowane zachowanie może wykazywać dowolnego następujące symptomy:
- Twój program generuje inny wynik przy każdym uruchomieniu.
- Twój program zachowuje się niespójnie (czasami daje pożądany rezultat, czasami nie).
- Twój program konsekwentnie generuje ten sam nieprawidłowy wynik.
- Twój program początkowo wydaje się działać, ale później generuje nieprawidłowe wyniki.
- Twój program ulega awarii natychmiast lub później.
- Twój program działa na niektórych kompilatorach lub platformach, ale nie inne.
- Twój program działa, dopóki nie zmienisz innego pozornie niezwiązanego kodu.
- Twój program i tak wydaje się generować pożądany rezultat.
Jednym z największych problemów związanych z niezdefiniowanym zachowaniem jest to, że zachowanie programu może zmienić się w dowolnym momencie i z dowolnego powodu. Zatem chociaż teraz może wydawać się, że taki kod działa, nie ma gwarancji, że tak będzie po ponownym uruchomieniu później.
Powiązana treść
Niezdefiniowane zachowanie zostało omówione w lekcji 1.6 — Niezainicjowane zmienne i niezdefiniowane zachowanie.
Pyt. 4: Dlaczego mój kod generujący niezdefiniowane zachowanie generuje określony wynik?
Czytelnicy często pytają, co się dzieje, że w ich systemie uzyskuje się określony wynik. W większości przypadków trudno powiedzieć, ponieważ uzyskany wynik może zależeć od bieżącego stanu programu, ustawień kompilatora, sposobu, w jaki kompilator implementuje funkcję, architektury komputera i/lub systemu operacyjnego. Na przykład, jeśli wydrukujesz wartość niezainicjowanej zmiennej, możesz otrzymać śmieci lub zawsze możesz otrzymać określoną wartość. Zależy to od typu zmiennej, od tego, jak kompilator umieści zmienną w pamięci i od tego, co wcześniej znajdowało się w tej pamięci (na co może mieć wpływ system operacyjny lub stan programu sprzed tego momentu).
I chociaż taka odpowiedź może być interesująca pod względem mechanicznym, ogólnie rzecz biorąc, rzadko jest użyteczna (i prawdopodobnie ulegnie zmianie, jeśli i kiedy zmieni się cokolwiek innego). To jakby zapytać: „Kiedy przepinam pas bezpieczeństwa przez kierownicę i podłączam go do pedału przyspieszenia, dlaczego samochód ściąga w lewo, gdy odwracam głowę w deszczowy dzień?” Najlepszą odpowiedzią nie jest fizyczne wyjaśnienie tego, co się dzieje, tylko „nie rób tego”.
Pyt. 5: Dlaczego pojawia się błąd kompilacji, gdy próbuję skompilować przykład, który wydaje się powinien działać?
Najczęstszą przyczyną jest to, że Twój projekt jest kompilowany przy użyciu niewłaściwego standardu językowego.
C++ wprowadza wiele nowych funkcji w każdym nowym standardzie językowym. Jeśli jeden z naszych przykładów wykorzystuje funkcję wprowadzoną w C++ 17, ale Twój program kompiluje się przy użyciu standardu języka C++ 14, prawdopodobnie się nie skompiluje, ponieważ funkcja, której używamy, nie jest obsługiwana przez wybrany przez nas standard języka.
Spróbuj ustawić swój standard językowy na najnowszą wersję obsługiwaną przez Twój kompilator i sprawdź, czy to rozwiąże problem. Możesz także sprawdzić, czy Twój kompilator jest poprawnie skonfigurowany do używania oczekiwanego standardu językowego, uruchamiając program tutaj: 0.13 — Jakiego standardu językowego używa mój kompilator?.
Powiązana treść
Opisane w lekcji 0.12 — Konfigurowanie kompilatora: Wybieranie standardu językowego.
Możliwe jest również, że Twój kompilator albo nie obsługuje jeszcze określonej funkcji, albo zawiera błąd uniemożliwiający użycie w niektórych przypadkach. W takim przypadku spróbuj zaktualizować swój kompilator do najnowszej dostępnej wersji.
Witryna internetowa CPPReference śledzi obsługę kompilatora dla każdej funkcji według standardu językowego. Linki do tych tabel wsparcia znajdziesz na ich stronie głównej, w prawym górnym rogu, w sekcji „Obsługa kompilatora” (według standardu językowego). Na przykład możesz zobaczyć, które funkcje C++23 są obsługiwane tutaj.
Pyt. 6: Dlaczego powinienem #dołączyć „foo.h” z foo.cpp?
Najlepszą praktyką jest, aby plik źródłowy (np. foo.cpp) zawierał sparowany nagłówek (np. foo.h). W wielu przypadkach foo.h będzie zawierał definicje, które foo.cpp będzie musiał poprawnie skompilować.
Jednak nawet jeśli foo.cpp kompiluje plik bez foo.h, łącznie z sparowanym nagłówkiem, pozwala kompilatorowi wykryć pewne typy niespójności pomiędzy dwoma plikami (np. gdy typ zwracany przez funkcję nie jest zgodny z typem zwracanym jej deklaracji forward). Bez włączenia może to spowodować niezdefiniowane zachowanie.
Koszt #include jest znikomy, więc dołączenie nagłówka nie ma żadnych wad.
Powiązana treść
Opisane w lekcji 2.11 — Pliki nagłówkowe.
Pyt.7: Dlaczego mój projekt kompiluje się tylko wtedy, gdy #include „foo.cpp” z „main.cpp”?
Jest to prawie zawsze spowodowane zapomnieniem o dodaniu foo.cpp do projektu i/lub wiersza poleceń kompilacji. Zaktualizuj swój projekt i/lub wiersz poleceń, aby uwzględnić każdy plik źródłowy (.cpp). Kiedy kompilujesz projekt, powinieneś zobaczyć kompilowany każdy plik źródłowy.
Zwykle w projekcie, który ma wiele plików, kompilator kompiluje każdy plik źródłowy (.cpp) indywidualnie. Po skompilowaniu wszystkich plików źródłowych linker łączy je ze sobą i tworzy wynikowy plik wyjściowy (np. plik wykonywalny). Jeśli jednak podzielisz swój kod pomiędzy dwa lub więcej plików (np. main.cpp i foo.cpp), a następnie skompilujesz tylko main.cpp, prawdopodobnie pojawi się błąd kompilacji lub błąd linkera, ponieważ część kodu wymagana dla twojego projektu nie jest kompilowana.
Nowi programiści czasami odkrywają, że mogą sprawić, że ich program będzie działał, dodając #include "foo.cpp" do main.cpp zamiast dodawać foo.cpp do projektu lub wiersza poleceń kompilacji. Po wykonaniu tej czynności, po skompilowaniu pliku main.cpp, preprocesor will utworzy jednostkę tłumaczącą składającą się z kodu z foo.cpp i main.cpp, która następnie zostanie skompilowana i połączona. W mniejszym projekcie może to zadziałać. Dlaczego więc tego nie zrobić?
Jest kilka powodów:
- Może to spowodować kolizje nazw między plikami.
- Unikanie naruszeń ODR może być trudne.
- Każda zmiana w dowolnym pliku .cpp spowoduje ponowną kompilację całego projektu. Może to zająć dużo czasu.
Powiązana treść
Opisane w lekcji 2.11 — Pliki nagłówkowe.
Pytanie 8: Dlaczego muszę return 0 na dole strony głównej?
Nie musisz. Funkcja main() jest wyjątkowa pod tym względem, że domyślnie zwróci 0, jeśli nie podasz instrukcji return.
Jednak każda inna funkcja zwracająca wartość, która dotrze do końca swojej treści bez napotkania instrukcji return, spowoduje niezdefiniowane zachowanie.
Dla zachowania spójności zalecamy jawne zwrócenie 0 z main(). Ale jeśli wolisz pominąć instrukcję return w main() dla zwięzłości, możesz. Tylko nie popełnij błędu, sądząc, że inne funkcje zwracające wartość działają podobnie.
Powiązana treść
Opisane w lekcji 2.2 -- Wartości zwracane przez funkcję (funkcje zwracające wartość).
Pytanie 9: Kiedy kompiluję przykład z tej witryny, pojawia się błąd podobny do „Brak listy argumentów dla szablonu zajęć XXX”. Dlaczego?
Najbardziej prawdopodobnym powodem jest to, że w przykładzie wykorzystano funkcję o nazwie Class Template Argument Deduction (CTAD), która jest funkcją C++17. Wiele kompilatorów domyślnie używa języka C++14, który nie obsługuje tej funkcji.
Jeśli poniższy program nie skompiluje się za Ciebie, oto powód:
#include <utility> // for std::pair
int main()
{
std::pair p2{ 1, 2 }; // CTAD used to deduce std::pair<int, int> from the initializers (C++17)
return 0;
}Powiązana treść
Możesz sprawdzić, dla jakiego standardu językowego skonfigurowany jest Twój kompilator, używając programu na lekcji 0.13 — Jakiego standardu językowego używa mój kompilator?.
Omówimy CTAD na lekcji 13.14 -- Przewodniki dedukcji i dedukcji szablonu klasy (CTAD).
P10: Dlaczego nie stworzymy parametrów funkcji lub typów zwracanych const podczas przekazywania lub zwracania przez wartość?
Zwykle nie const parametrów funkcji według wartości, ponieważ:
- Utworzenie takich parametrów
constnie zapewnia żadnej znaczącej wartości wywołującemu funkcję, ale powoduje bałagan w interfejsie funkcji. - Zazwyczaj nie przejmujemy się tym, czy funkcja modyfikuje te parametry, ponieważ są to kopie, które zostaną zniszczone na końcu funkcji w każdym razie.
Nie const zwraca wartości według wartości, ponieważ:
- Jeśli typ zwracany jest typem zwracanym innym niż klasowy (np. typem podstawowym), const i tak zostanie zignorowany.
- Jeśli typem zwracanym jest typ klasowy, const może uniemożliwiać pewne rodzaje optymalizacji (np. semantykę przenoszenia).
Uwaga: const jest istotne, gdy przekazywanie/zwracanie według adresu lub odniesienia.
Powiązana treść
Opisane w lekcji >5.1 -- Zmienne stałe (zwane stałymi)
P11: Dlaczego powinienem używać constexpr?
Constexpr i inne techniki programowania w czasie kompilacji zapewniają wiele korzyści, w tym:
- Mniejszy i szybszy kod.
- Możemy sprawić, aby kompilator wykrywał określone rodzaje błędów i zatrzymywał kompilację, jeśli wystąpią.
- Niezdefiniowane zachowanie nie jest dozwolone w czasie kompilacji.
- Możliwość używania zmiennych i funkcji w miejscach, które wymagają stałej wyrażenie.
Ostatni punkt jest być może najbardziej nieunikniony, ponieważ niektóre funkcje C++ wymagają wartości znanych w czasie kompilacji.
Powiązana treść
Opisane w lekcji 5.5 -- Wyrażenia stałe
P12: Dlaczego powinienem zdefiniować kwalifikującą się funkcję, skoro wiem, że będzie ona oceniana tylko w czasie wykonywania mojego programu?
Jest kilka powodów, dla których warto to zrobić:
- Jest jest to niewielka wada używania constexpr i może pomóc w optymalizacji kompilatora nawet w kontekstach, w których nie jest on oceniany w czasie kompilacji.
- To, że nie wywołujesz funkcji w kontekście możliwym do oceny w czasie kompilacji, nie oznacza, że nie wywołasz jej w takim kontekście podczas modyfikowania lub rozszerzania programu. A jeśli jeszcze nie skonstruowałeś tej funkcji, możesz nie pomyśleć o tym, kiedy zaczniesz ją wywoływać w takim kontekście, a wtedy stracisz korzyści związane z wydajnością. Możesz też zostać zmuszony do jego późniejszej interpretacji, gdy będziesz musiał użyć zwracanej wartości w kontekście wymagającym gdzieś stałego wyrażenia.
- Powtarzanie pomaga utrwalić najlepsze praktyki.
W przypadku nietrywialnego projektu dobrym pomysłem jest wdrożenie funkcji z myślą, że mogą zostać ponownie wykorzystane (lub rozszerzone) w przyszłości. Za każdym razem, gdy modyfikujesz istniejącą funkcję, ryzykujesz jej uszkodzeniem, a to oznacza, że należy ją ponownie przetestować, co wymaga czasu i energii. Często warto poświęcić dodatkową minutę lub dwie na „zrobienie tego dobrze za pierwszym razem”, aby nie trzeba było powtarzać tego (i ponownie testować) później.
Powiązana treść
Opisane w lekcji F.1 -- Funkcje Constexpr
P13: Dlaczego nie powinienem wywoływać tej samej funkcji wejściowej w wyrażeniu więcej niż raz?
W większości przypadków standard C++ nie określa kolejności, w jakiej oceniane są operandy (w tym argumenty funkcji). Pierwszeństwo i łączność operatorów są używane tylko do określenia sposobu grupowania operandów z operatorami i kolejności obliczania wartości.
Na przykład, biorąc pod uwagę instrukcję std::cout << subtract(getUserInput(), getUserInput()), najpierw można ocenić lewy lub prawy argument w wywołaniu funkcji do subtract() . Załóżmy, że użytkownik wprowadza wartości 5 i 2. Jeśli lewy argument został oceniony jako pierwszy, lewy argument zostanie oceniony jako 5, a prawy argument zostanie oceniony jako 2. 5 - 2 Jest 3. Jeśli prawy argument został oceniony jako pierwszy, prawy argument zostanie oceniony jako 5, a lewy argument zostanie oceniony jako 2. 2 - 5 Jest -3. Zatem ta instrukcja może zostać wydrukowana 3 lub -3.
Można to ujednoznacznić, czyniąc każde wywołanie funkcji getUserInput() oddzielną instrukcją (aby kolejność była deterministyczna) i przechowując zwracaną wartość w zmiennej, dopóki nie będzie potrzebna.
Powiązana treść
Opisane w lekcji 6.1 — Pierwszeństwo operatora i łączność
P14: Nie ma wystarczającej liczby quizów! Gdzie mogę zdobyć więcej praktyki?
Polecamy program https://www.codewars.com/, który zawiera mnóstwo krótkich ćwiczeń, które pomogą Ci udoskonalić ogólne umiejętności rozwiązywania problemów i wdrażania języka C++. To także świetna zabawa!
Gdy już znajdziesz rozwiązanie, możesz porównać je z odpowiedziami innych, aby zobaczyć alternatywne podejścia lub zrozumieć, gdzie można ulepszyć własny kod.
Jednakże, ponieważ jednorazowe ćwiczenia zawierają jednorazowe odpowiedzi, rozwiązywanie takich quizów tak naprawdę nie zachęca do pisania kodu wysokiej jakości ani nie pokazuje, co się stanie, jeśli nie będziesz przestrzegać najlepszych praktyk. W tym celu nie ma nic lepszego niż stworzenie własnego projektu.
Zacznij od czegoś małego - prostej gry lub symulacji. Następnie stopniowo dodawaj kolejne funkcje. W miarę wzrostu złożoności projektu zaczniesz dostrzegać różne braki w kodzie. Pomoże Ci to określić, które obszary Twojego kodu wymagają poprawy jakości.

