Długo myślałem, że w C wszystkie zmienne trzeba zadeklarować na początku funkcji. Wiem, że w C99 reguły są takie same jak w C ++, ale jakie są zasady umieszczania deklaracji zmiennych dla C89 / ANSI C?
Poniższy kod pomyślnie kompiluje się z gcc -std=c89
i gcc -ansi
:
#include <stdio.h>
int main() {
int i;
for (i = 0; i < 10; i++) {
char c = (i % 95) + 32;
printf("%i: %c\n", i, c);
char *s;
s = "some string";
puts(s);
}
return 0;
}
Czy deklaracje c
i nie powinny s
powodować błędu w trybie C89 / ANSI?
c
declaration
c89
mcjabberz
źródło
źródło
Odpowiedzi:
Kompiluje się pomyślnie, ponieważ GCC zezwala na deklarację
s
rozszerzenia GNU, mimo że nie jest częścią standardu C89 ani ANSI. Jeśli chcesz ściśle przestrzegać tych standardów, musisz przekazać-pedantic
flagę.Deklaracja
c
na początku{ }
bloku jest częścią standardu C89; blok nie musi być funkcją.źródło
s
jest rozszerzeniem (z punktu widzenia C89). Deklaracjac
jest całkowicie legalna w C89, żadne rozszerzenia nie są potrzebne.W przypadku C89 musisz zadeklarować wszystkie swoje zmienne na początku bloku zasięgu .
Tak więc Twoja
char c
deklaracja jest prawidłowa, ponieważ znajduje się na górze bloku zakresu pętli for. Alechar *s
deklaracja powinna być błędna.źródło
gcc
. Oznacza to, że nie ufaj, że program można skompilować w taki sposób, aby był zgodny.Grupowanie deklaracji zmiennych w górnej części bloku jest starsze, prawdopodobnie z powodu ograniczeń starych, prymitywnych kompilatorów języka C. Wszystkie współczesne języki zalecają, a czasem nawet wymuszają deklarację zmiennych lokalnych w ostatnim momencie: w miejscu ich pierwszej inicjalizacji. Ponieważ eliminuje to ryzyko omyłkowego użycia wartości losowej. Oddzielenie deklaracji i inicjalizacji zapobiega również używaniu "const" (lub "final"), kiedy możesz.
C ++ niestety nadal akceptuje stary, najwyższy sposób deklaracji zgodności z poprzednimi wersjami z C (jeden ciąg zgodności C z wielu innych ...) Ale C ++ próbuje odejść od tego:
C99 zaczyna przesuwać C w tym samym kierunku.
Jeśli martwisz się, że nie znajdziesz miejsca, w którym zadeklarowano zmienne lokalne, oznacza to, że masz znacznie większy problem: otaczający blok jest za długi i powinien zostać podzielony.
https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions
źródło
int y; ... if (x) { printf("X was true"); y=23;} return y;
...null
, wszystkie bity-zero są często użyteczną wartością pułapki. Ponadto w językach, które wyraźnie określają, że zmienne mają domyślnie wszystkie bity-zero, poleganie na tej wartości nie jest błędem . Kompilatory nie są jeszcze zbyt zwariowane ze swoimi „optymalizacjami”, ale twórcy kompilatorów próbują być coraz bardziej sprytni. Opcja kompilatora do inicjowania zmiennych z celowymi zmiennymi pseudolosowymi może być przydatna do identyfikowania błędów, ale pozostawienie pamięci przechowującej ostatnią wartość może czasami maskować błędy.Z punktu widzenia możliwości utrzymania, a nie syntaktycznego punktu widzenia, istnieją co najmniej trzy ciągi myśli:
Zadeklaruj wszystkie zmienne na początku funkcji, aby znajdowały się w jednym miejscu i od razu zobaczysz pełną listę.
Zadeklaruj wszystkie zmienne jak najbliżej miejsca, w którym zostały użyte po raz pierwszy, aby wiedzieć, dlaczego każda z nich jest potrzebna.
Zadeklaruj wszystkie zmienne na początku najbardziej wewnętrznego bloku zakresu, aby jak najszybciej wyszły poza zakres i pozwolą kompilatorowi zoptymalizować pamięć i powiedzieć, jeśli przypadkowo użyjesz ich tam, gdzie nie zamierzałeś.
Generalnie wolę pierwszą opcję, ponieważ inne często zmuszają mnie do przeszukiwania kodu w celu znalezienia deklaracji. Zdefiniowanie wszystkich zmiennych z góry ułatwia również inicjalizację i oglądanie ich z debugera.
Czasami deklaruję zmienne w mniejszym bloku zakresu, ale tylko z dobrego powodu, którego mam bardzo niewiele. Przykładem może być po a
fork()
, aby zadeklarować zmienne potrzebne tylko procesowi potomnemu. Dla mnie ten wizualny wskaźnik jest pomocnym przypomnieniem ich celu.źródło
Jak zauważyli inni, GCC jest pod tym względem przyzwalający (i prawdopodobnie inne kompilatory, w zależności od argumentów, z którymi są wywoływane), nawet w trybie C89, chyba że używasz sprawdzania „pedantycznego”. Szczerze mówiąc, nie ma wielu dobrych powodów, aby nie podejmować pedantycznych działań; Wysokiej jakości nowoczesny kod powinien zawsze kompilować się bez ostrzeżeń (lub bardzo niewiele, gdy wiesz, że robisz coś konkretnego, co jest podejrzane dla kompilatora jako możliwy błąd), więc jeśli nie możesz skompilować swojego kodu z pedantyczną konfiguracją, prawdopodobnie wymaga to trochę uwagi.
C89 wymaga, aby zmienne były zadeklarowane przed innymi instrukcjami w każdym zakresie, późniejsze standardy zezwalają na deklarację bliższą użycia (co może być zarówno bardziej intuicyjne, jak i wydajniejsze), zwłaszcza jednoczesną deklarację i inicjalizację zmiennej sterującej pętli w pętlach „for”.
źródło
Jak już wspomniano, istnieją dwie szkoły myślenia na ten temat.
1) Zadeklaruj wszystko u góry funkcji, ponieważ jest rok 1987.
2) Zadeklaruj jak najbliżej pierwszego użycia i w możliwie najmniejszym zakresie.
Moja odpowiedź na to brzmi: ZRÓB OBIEG! Pozwól mi wyjaśnić:
W przypadku długich funkcji 1) sprawia, że refaktoryzacja jest bardzo trudna. Jeśli pracujesz w bazie kodu, w której programiści sprzeciwiają się idei podprogramów, będziesz mieć 50 deklaracji zmiennych na początku funkcji, a niektóre z nich mogą być po prostu „i” dla pętli for, która jest na samym początku dół funkcji.
Dlatego opracowałem na podstawie tego deklarację na szczycie PTSD i próbowałem zrobić opcję 2) religijnie.
Wróciłem do opcji pierwszej z jednego powodu: krótkich funkcji. Jeśli twoje funkcje są wystarczająco krótkie, będziesz mieć kilka zmiennych lokalnych, a ponieważ funkcja jest krótka, jeśli umieścisz je na górze funkcji, nadal będą blisko pierwszego użycia.
Również anty-wzorzec „deklaruj i ustaw na NULL”, gdy chcesz zadeklarować na górze, ale nie wykonałeś pewnych obliczeń niezbędnych do inicjalizacji, został rozwiązany, ponieważ rzeczy, które musisz zainicjować, zostaną prawdopodobnie odebrane jako argumenty.
Więc teraz myślę, że powinieneś zadeklarować na górze funkcji i jak najbliżej pierwszego użycia. Więc obie! A sposobem na to są dobrze podzielone podprogramy.
Ale jeśli pracujesz nad długą funkcją, zastosuj rzeczy najbliższe do pierwszego użycia, ponieważ w ten sposób łatwiej będzie wyodrębnić metody.
Mój przepis jest taki. Dla wszystkich zmiennych lokalnych weź zmienną i przenieś jej deklarację na dół, skompiluj, a następnie przenieś deklarację tuż przed błędem kompilacji. To jest pierwsze użycie. Zrób to dla wszystkich zmiennych lokalnych.
int foo = 0; <code that uses foo> int bar = 1; <code that uses bar> <code that uses foo>
Teraz zdefiniuj blok zakresu, który zaczyna się przed deklaracją i przesuń koniec do momentu kompilacji programu
{ int foo = 0; <code that uses foo> } int bar = 1; <code that uses bar> >>> First compilation error here <code that uses foo>
To się nie kompiluje, ponieważ jest więcej kodu używającego foo. Możemy zauważyć, że kompilator był w stanie przejść przez kod używający bar, ponieważ nie używa foo. W tym momencie są dwie możliwości. Mechaniczny polega na przesunięciu znaku „}” w dół, aż się skompiluje, a drugim wyborem jest sprawdzenie kodu i ustalenie, czy kolejność można zmienić na:
{ int foo = 0; <code that uses foo> } <code that uses foo> int bar = 1; <code that uses bar>
Jeśli kolejność można zmienić, prawdopodobnie tego chcesz, ponieważ skraca to żywotność wartości tymczasowych.
Inną rzeczą wartą uwagi jest to, czy wartość foo musi być zachowana między blokami kodu, które go używają, czy może to być po prostu inne foo w obu. Na przykład
int i; for(i = 0; i < 8; ++i){ ... } <some stuff> for(i = 3; i < 32; ++i){ ... }
Te sytuacje wymagają czegoś więcej niż tylko mojej procedury. Deweloper będzie musiał przeanalizować kod, aby określić, co robić.
Ale pierwszym krokiem jest znalezienie pierwszego zastosowania. Możesz to zrobić wizualnie, ale czasami po prostu łatwiej jest usunąć deklarację, spróbować skompilować i po prostu umieścić ją z powrotem powyżej pierwszego użycia. Jeśli to pierwsze użycie znajduje się wewnątrz instrukcji if, umieść je tam i sprawdź, czy się kompiluje. Kompilator następnie zidentyfikuje inne zastosowania. Spróbuj utworzyć blok zakresu, który obejmuje oba zastosowania.
Po wykonaniu tej części mechanicznej łatwiej jest przeanalizować, gdzie są dane. Jeśli zmienna jest używana w bloku o dużym zasięgu, przeanalizuj sytuację i sprawdź, czy nie używasz tej samej zmiennej do dwóch różnych rzeczy (np. „I”, które jest używane dla dwóch pętli). Jeśli zastosowania są niepowiązane, utwórz nowe zmienne dla każdego z tych niepowiązanych zastosowań.
źródło
Powinieneś zadeklarować wszystkie zmienne u góry lub „lokalnie” w funkcji. Odpowiedź to:
To zależy to od rodzaju używanego systemu:
1 / System wbudowany (szczególnie związany z życiem takim jak samolot lub samochód): pozwala na użycie pamięci dynamicznej (np .: calloc, malloc, new ...). Wyobraź sobie, że pracujesz nad bardzo dużym projektem, w którym uczestniczy 1000 inżynierów. Co się stanie, jeśli przydzielą nową pamięć dynamiczną i zapomną o jej usunięciu (kiedy już nie jest używana)? Jeśli system wbudowany będzie działał przez długi czas, doprowadzi to do przepełnienia stosu i uszkodzenia oprogramowania. Niełatwo jest upewnić się co do jakości (najlepszym sposobem jest zakazanie pamięci dynamicznej).
Jeśli samolot leci w ciągu 30 dni i nie wyłączy się, co się stanie, jeśli oprogramowanie jest uszkodzone (gdy samolot nadal jest w powietrzu)?
2 / Inne systemy, takie jak Internet, PC (mają dużą przestrzeń pamięci):
Powinieneś zadeklarować zmienną „lokalnie”, aby zoptymalizować użycie pamięci. Jeśli te systemy działają przez długi czas i zdarza się przepełnienie stosu (ponieważ ktoś zapomniał usunąć pamięć dynamiczną). Po prostu zrób prostą rzecz, aby zresetować komputer: P Nie ma to wpływu na życie
źródło
malloc()
. Chociaż nigdy nie widziałem urządzenia, które nie jest w stanie tego zrobić, najlepiej jest unikać dynamicznej alokacji w systemach wbudowanych ( patrz tutaj ). Ale to nie ma nic wspólnego z tym, gdzie deklarujesz zmienne w funkcji.sub rsp, 1008
przydziela miejsce dla całej tablicy poza instrukcją if. Dotyczy toclang
igcc
przy każdej wersji i optymalizacji poziomu próbowałem.Zacytuję kilka wypowiedzi z podręcznika do gcc w wersji 4.7.0 dla jasnego wyjaśnienia.
„Kompilator może akceptować kilka podstawowych standardów, takich jak„ c90 ”lub„ c ++ 98 ”, oraz dialekty GNU tych standardów, takie jak„ gnu90 ”lub„ gnu ++ 98 ”. Określając podstawowy standard, kompilator zaakceptuje wszystkie programy zgodne z tym standardem i te, które używają rozszerzeń GNU, które nie są z nim sprzeczne. Na przykład „-std = c90” wyłącza niektóre funkcje GCC, które są niezgodne z ISO C90, takie jak asm i typ słów kluczowych, ale nie inne rozszerzenia GNU, które nie mają znaczenia w ISO C90, takie jak pominięcie środkowego wyrazu?: wyrażenie. "
Myślę, że kluczowym punktem twojego pytania jest to, dlaczego gcc nie jest zgodne z C89, nawet jeśli używana jest opcja "-std = c89". Nie znam wersji twojego gcc, ale myślę, że nie będzie dużej różnicy. Twórca gcc powiedział nam, że opcja „-std = c89” oznacza po prostu, że rozszerzenia sprzeczne z C89 są wyłączone. Nie ma to więc nic wspólnego z niektórymi rozszerzeniami, które nie mają znaczenia w C89. A rozszerzenie, które nie ogranicza umieszczania deklaracji zmiennej, należy do rozszerzeń, które nie są sprzeczne z C89.
Szczerze mówiąc, każdy pomyśli, że na pierwszy rzut oka opcja „-std = c89” powinna być całkowicie zgodna z C89. Ale tak nie jest. Co do problemu, że wszystkie zmienne na początku są lepsze lub gorsze to kwestia przyzwyczajenia.
źródło