Umieszczanie zmiennej deklaracji w C


132

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=c89i 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 ci nie powinny spowodować błędu w trybie C89 / ANSI?


55
Uwaga: zmienne w ansi C nie muszą być deklarowane na początku funkcji, ale raczej na początku bloku. Tak więc znak c = ... na górze twojej pętli for jest całkowicie legalny w ansi C. Jednak znaki * nie byłyby.
Jason Coco

Odpowiedzi:


153

Kompiluje się pomyślnie, ponieważ GCC zezwala na deklarację srozszerzenia GNU, mimo że nie jest częścią standardu C89 ani ANSI. Jeśli chcesz ściśle przestrzegać tych standardów, musisz przekazać -pedanticflagę.

Deklaracja cna początku { }bloku jest częścią standardu C89; blok nie musi być funkcją.


42
Warto chyba zauważyć, że tylko deklaracja sjest rozszerzeniem (z punktu widzenia C89). Deklaracja cjest całkowicie legalna w C89, żadne rozszerzenia nie są potrzebne.
AnT

8
@AndreyT: Tak, w C deklaracje zmiennych powinny być @ początkiem bloku, a nie funkcją samą w sobie; ale ludzie mylą blok z funkcją, ponieważ jest to podstawowy przykład bloku.
legends2k

1
Przeniosłem komentarz z +39 głosami do odpowiedzi.
MarcH

78

W przypadku C89 musisz zadeklarować wszystkie swoje zmienne na początku bloku zasięgu .

Tak więc Twoja char cdeklaracja jest prawidłowa, ponieważ znajduje się na górze bloku zakresu pętli for. Ale char *sdeklaracja powinna być błędna.


2
Całkiem poprawnie. Możesz zadeklarować zmienne na początku dowolnego {...}.
Artelius

5
@Artelius Niezupełnie poprawne. Tylko wtedy, gdy curlies są częścią bloku (nie, jeśli są częścią deklaracji struktury lub unii lub inicjalizatora stężonego).
Jens

Żeby być pedantycznym, błędną deklarację należy zgłosić przynajmniej zgodnie ze standardem C. Więc powinien to być błąd lub ostrzeżenie w pliku gcc. Oznacza to, że nie ufaj, że program można skompilować w taki sposób, aby był zgodny.
jinawee

@Jens Jak deklarujesz nowe zmienne wewnątrz inicjalizatora struktury, unii lub stężenia? „Blok” oczywiście oznacza tutaj „blok kodu”.
MarcH

@MarcH Nie tak powiedział Artelius. Powiedział „na początku każdego {...}” bez zastrzeżeń.
Jens

39

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:

  • Projekt odwołań w C ++ nie pozwala nawet na taki szczyt grupowania bloków.
  • Jeśli oddzielisz deklarację i inicjalizację obiektu lokalnego w C ++, to nic nie zapłacisz za dodatkowy konstruktor. Jeśli konstruktor bez argumentu nie istnieje, to znowu nie możesz nawet rozdzielić obu!

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



Zobacz też, jak wymuszanie deklaracji zmiennych na górze bloku może tworzyć luki w zabezpieczeniach: lwn.net/Articles/443037
MarcH

„C ++ niestety nadal akceptuje stary, najwyższy sposób deklaracji zgodności wstecznej z C”: IMHO, to po prostu czysty sposób na zrobienie tego. Inny język „rozwiązuje” ten problem zawsze inicjalizując z 0. Bzzt, które maskuje tylko błędy logiczne, jeśli o mnie chodzi. Istnieje kilka przypadków, w których POTRZEBUJESZ deklaracji bez inicjalizacji, ponieważ istnieje wiele możliwych lokalizacji inicjalizacji. I właśnie dlatego RAII w C ++ jest naprawdę ogromnym problemem - teraz musisz dołączyć „prawidłowy” stan niezainicjowany do każdego obiektu, aby uwzględnić takie przypadki.
Jo So

1
@JoSo: Jestem zdezorientowany, dlaczego myślisz, że odczyty niezainicjowanych zmiennych przyniosą dowolne efekty, dzięki czemu błędy w programowaniu będą łatwiejsze do wykrycia, niż sprawienie, by dały one stałą wartość lub błąd deterministyczny? Zwróć uwagę, że nie ma gwarancji, że odczyt niezainicjalizowanej pamięci będzie zachowywał się w sposób zgodny z dowolnym wzorem bitowym, jaki mogła posiadać zmienna, ani nawet, że taki program będzie zachowywał się w sposób zgodny ze zwykłymi prawami czasu i przyczynowości. Biorąc pod uwagę coś takiego jak int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat

1
@JoSo: W przypadku wskaźników, szczególnie w implementacjach, które przechwytują operacje 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.
supercat

22

Z punktu widzenia możliwości utrzymania, a nie syntaktycznego punktu widzenia, istnieją co najmniej trzy ciągi myśli:

  1. Zadeklaruj wszystkie zmienne na początku funkcji, aby znajdowały się w jednym miejscu i od razu zobaczysz pełną listę.

  2. 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.

  3. 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.


27
Używam opcji 2 lub 3, więc łatwiej jest znaleźć zmienne - ponieważ funkcje nie powinny być tak duże, że nie widać deklaracji zmiennych.
Jonathan Leffler

8
Opcja 3 nie stanowi problemu, chyba że używasz kompilatora z lat 70.
edgar.holleis

15
Gdybyś używał przyzwoitego IDE, nie musiałbyś szukać kodu, ponieważ powinno istnieć polecenie IDE, aby znaleźć deklarację dla ciebie. (F3 w Eclipse)
edgar.holleis

4
Nie rozumiem, w jaki sposób możesz zapewnić inicjalizację w opcji 1, może czasami możesz uzyskać wartość początkową tylko później w bloku, wywołując inną funkcję lub wykonując obliczenia.
Plumenator

4
@Plumenator: opcja 1 nie zapewnia inicjalizacji; Zdecydowałem się zainicjować je po zadeklarowaniu, albo na ich „poprawne” wartości, albo na coś, co zagwarantuje, że następny kod się zepsuje, jeśli nie zostaną odpowiednio ustawione. Mówię „wybrałem”, ponieważ moje preferencje zmieniły się na drugie, odkąd to napisałem, być może dlatego, że teraz używam Javy częściej niż C i dlatego, że mam lepsze narzędzia programistyczne.
Adam Liss

6

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”.


0

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ń.


0

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


Nie jestem pewien, czy to prawda. Myślę, że twierdzisz, że łatwiej jest przeprowadzić audyt pod kątem wycieków pamięci, jeśli zadeklarujesz wszystkie lokalne zmienne w jednym miejscu? Że może być prawdą, ale nie jestem tak pewny, że go kupić. Co do punktu (2), mówisz, że zadeklarowanie zmiennej lokalnie „zoptymalizowałoby użycie pamięci”? Jest to teoretycznie możliwe. Kompilator mógłby zdecydować się na zmianę rozmiaru ramki stosu w trakcie wykonywania funkcji, aby zminimalizować zużycie pamięci, ale nie znam żadnego, który to robi. W rzeczywistości kompilator po prostu skonwertuje wszystkie deklaracje „lokalne” na „uruchomienie funkcji za kulisami”.
QuinnFreedman

1 / System wbudowany czasami nie pozwala na pamięć dynamiczną, więc jeśli deklarujesz wszystkie zmienne na początku funkcji. Kiedy kod źródłowy jest budowany, może obliczyć liczbę bajtów potrzebnych do uruchomienia programu. Ale z pamięcią dynamiczną kompilator nie może zrobić tego samego.
Dang_Ho

2 / Jeśli deklarujesz zmienną lokalnie, ta zmienna istnieje tylko w nawiasach otwierających / zamykających "{}". Kompilator może więc zwolnić przestrzeń zmiennej, jeśli ta zmienna jest „poza zakresem”. To może być lepsze niż deklarowanie wszystkiego na początku funkcji.
Dang_Ho

Myślę, że jesteś zdezorientowany co do pamięci statycznej i dynamicznej. Na stosie przydzielana jest pamięć statyczna. Wszystkie zmienne zadeklarowane w funkcji, bez względu na to, gdzie są zadeklarowane, są przydzielane statycznie. Pamięć dynamiczna jest przydzielana na stercie z czymś w rodzaju 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.
QuinnFreedman

1
Chociaż zgadzam się, że byłby to rozsądny sposób działania, nie dzieje się tak w praktyce. Oto rzeczywisty montaż czegoś bardzo podobnego do twojego przykładu: godbolt.org/z/mLhE9a . Jak widać, w linii 11. sub rsp, 1008przydziela miejsce dla całej tablicy poza instrukcją if. Dotyczy to clangi gccprzy każdej wersji i optymalizacji poziomu próbowałem.
QuinnFreedman

-1

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.


zgodność nie oznacza nieakceptowania rozszerzeń: tak długo, jak kompilator kompiluje prawidłowe programy i tworzy wszelkie wymagane informacje diagnostyczne dla innych, jest zgodny.
Pamiętaj o Monice

@Marc Lehmann, tak, masz rację, jeśli słowo „zgodność” jest używane do rozróżnienia kompilatorów. Ale kiedy do opisania niektórych zastosowań używa się słowa „zgodność”, można powiedzieć „zastosowanie nie jest zgodne ze standardem”. A wszyscy początkujący mają opinię, że zastosowania niezgodne ze standardem powinny powodować błąd.
junwanghe

@Marc Lehmann, nawiasem mówiąc, nie ma diagnostyki, gdy gcc widzi użycie niezgodne ze standardem C89.
junwanghe

Twoja odpowiedź jest nadal błędna, ponieważ twierdzenie, że „gcc nie jest zgodny”, nie jest tym samym, co „jakiś program użytkownika nie jest zgodny”. Twoje użycie zgodności jest po prostu nieprawidłowe. Poza tym, kiedy byłem początkującym, nie byłem tego zdania, więc to też jest błędne. Wreszcie, nie ma wymogu, aby zgodny kompilator zdiagnozował niezgodny kod, aw rzeczywistości jest to niemożliwe do zaimplementowania.
Pamiętaj o Monice
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.