Zwracanie ciągu znaków w języku C z funkcji

109

Próbuję zwrócić ciąg C z funkcji, ale to nie działa. Oto mój kod.

char myFunction()
{
    return "My String";
}

W mainnazywam to tak:

int main()
{
  printf("%s", myFunction());
}

Próbowałem też innych sposobów myFunction, ale one nie działają. Na przykład:

char myFunction()
{
  char array[] = "my string";
  return array;
}

Uwaga: nie wolno mi używać wskaźników!

Małe tło w tym problemie:

Jest funkcja, która sprawdza, który to miesiąc. Na przykład, jeśli jest 1, zwraca styczeń itd.

Więc kiedy to się dzieje, aby wydrukować, to robi to tak: printf("Month: %s",calculateMonth(month));. Teraz problem polega na tym, jak zwrócić ten ciąg z calculateMonthfunkcji.

itsaboutcode
źródło
10
Niestety w tym przypadku potrzebne są wskazówki.
Nick Bedford,
1
@Hayato Cóż, uważam, że jesteśmy tu dorośli i wiem, że powinno zwrócić 0, to tylko po to, by dać przykład lox ..
itsaboutcode
3
return 0jest domyślna tylko w C99 (i C ++), ale nie w C90.
hrnt
1
Wtedy nie będziesz w stanie tego zrobić, poza głupimi sztuczkami, które i tak są naprawdę zepsutymi manipulacjami wskaźnikiem. Wskaźniki istnieją z jakiegoś powodu ...: |
GManNickG,

Odpowiedzi:

222

Twój podpis funkcji musi być:

const char * myFunction()
{
    return "My String";
}

Tło:

Jest to tak fundamentalne dla C i C ++, ale trochę więcej dyskusji powinno być w porządku.

W C (i C ++ jeśli o to chodzi), łańcuch jest po prostu tablicą bajtów zakończoną bajtem zerowym - stąd termin „łańcuch zero” jest używany do reprezentowania tego szczególnego rodzaju ciągu. Istnieją inne rodzaje ciągów, ale w C (i C ++) ten smak jest z natury zrozumiały przez sam język. Inne języki (Java, Pascal itp.) Używają różnych metodologii do zrozumienia „mojego ciągu”.

Jeśli kiedykolwiek użyjesz Windows API (które jest w C ++), dość regularnie zobaczysz parametry funkcji, takie jak: "LPCSTR lpszName". Część „sz” reprezentuje pojęcie „string-zero”: tablica bajtów z zakończeniem zerowym (/ zero).

Wyjaśnienie:

Na potrzeby tego „wstępu” używam zamiennie słowa „bajty” i „znaki”, ponieważ w ten sposób łatwiej jest się nauczyć. Należy pamiętać, że istnieją inne metody (znaki szerokie i systemy znaków wielobajtowych ( mbcs )), które są używane do obsługi znaków międzynarodowych. UTF-8 to przykład pliku mbcs. Ze względu na intro po cichu „pomijam” to wszystko.

Pamięć:

Oznacza to, że ciąg taki jak „mój ciąg” w rzeczywistości wykorzystuje 9 + 1 (= 10!) Bajtów. Jest to ważne, aby wiedzieć, kiedy w końcu zajmiesz się dynamicznym przydzielaniem ciągów.

Tak więc bez tego „kończącego zera” nie masz łańcucha. Masz tablicę znaków (zwaną również buforem) wiszącą w pamięci.

Trwałość danych:

Korzystanie z funkcji w ten sposób:

const char * myFunction()
{
    return "My String";
}

int main()
{
    const char* szSomeString = myFunction(); // Fraught with problems
    printf("%s", szSomeString);
}

... generalnie wyląduje z przypadkowymi nieobsługiwanymi wyjątkami / usterkami segmentów i tym podobnymi, zwłaszcza „w dół drogi”.

Krótko mówiąc, chociaż moja odpowiedź jest prawidłowa - 9 razy na 10 skończysz z programem, który ulega awarii, jeśli używasz go w ten sposób, zwłaszcza jeśli uważasz, że jest to „dobra praktyka”, aby robić to w ten sposób. Krótko mówiąc: generalnie nie.

Na przykład, wyobraź sobie kiedyś w przyszłości, że struną trzeba teraz w jakiś sposób manipulować. Ogólnie rzecz biorąc, programista `` pójdzie prostą ścieżką '' i (spróbuje) napisać kod w następujący sposób:

const char * myFunction(const char* name)
{
    char szBuffer[255];
    snprintf(szBuffer, sizeof(szBuffer), "Hi %s", name);
    return szBuffer;
}

Oznacza to, że program będzie awarię ponieważ kompilator (może / nie może) wydali pamięć używaną przez szBufferprzez czas printf()w main()nazywa. (Twój kompilator powinien również wcześniej ostrzec Cię o takich problemach).

Istnieją dwa sposoby zwracania łańcuchów, które nie będą tak łatwo barfować.

  1. zwracające bufory (statyczne lub przydzielane dynamicznie), które istnieją przez jakiś czas. W C ++ użyj `` klas pomocniczych '' (na przykład std::string) do obsługi długowieczności danych (co wymaga zmiany wartości zwracanej przez funkcję) lub
  2. przekazują bufor do funkcji, która jest wypełniana informacjami.

Zauważ, że nie można używać łańcuchów bez użycia wskaźników w C. Jak pokazałem, są one synonimami. Nawet w C ++ z klasami szablonów zawsze w tle używane są bufory (czyli wskaźniki).

Tak więc, aby lepiej odpowiedzieć na (teraz zmodyfikowane pytanie). (Z pewnością istnieje wiele „innych odpowiedzi”, których można udzielić).

Bezpieczniejsze odpowiedzi:

Przykład 1, używając statycznie przydzielonych ciągów:

const char* calculateMonth(int month)
{
    static char* months[] = {"Jan", "Feb", "Mar" .... };
    static char badFood[] = "Unknown";
    if (month<1 || month>12)
        return badFood; // Choose whatever is appropriate for bad input. Crashing is never appropriate however.
    else
        return months[month-1];
}

int main()
{
    printf("%s", calculateMonth(2)); // Prints "Feb"
}

To, co robi tutaj „statyczny” (wielu programistów nie lubi tego typu „alokacji”), polega na tym, że ciągi znaków są umieszczane w segmencie danych programu. Oznacza to, że jest przydzielony na stałe.

Jeśli przejdziesz na C ++, zastosujesz podobne strategie:

class Foo
{
    char _someData[12];
public:
    const char* someFunction() const
    { // The final 'const' is to let the compiler know that nothing is changed in the class when this function is called.
        return _someData;
    }
}

... ale prawdopodobnie łatwiej jest korzystać z klas pomocniczych, na przykład std::string, jeśli piszesz kod na własny użytek (a nie część biblioteki, którą można udostępniać innym).

Przykład 2, przy użyciu buforów zdefiniowanych przez wywołującego:

Jest to bardziej „niezawodny” sposób przekazywania sznurków. Zwrócone dane nie podlegają manipulacji ze strony dzwoniącego. Oznacza to, że przykład 1 może być łatwo nadużywany przez stronę dzwoniącą i narażać Cię na błędy aplikacji. W ten sposób jest dużo bezpieczniejszy (choć używa więcej linii kodu):

void calculateMonth(int month, char* pszMonth, int buffersize)
{
    const char* months[] = {"Jan", "Feb", "Mar" .... }; // Allocated dynamically during the function call. (Can be inefficient with a bad compiler)
    if (!pszMonth || buffersize<1)
        return; // Bad input. Let junk deal with junk data.
    if (month<1 || month>12)
    {
        *pszMonth = '\0'; // Return an 'empty' string
        // OR: strncpy(pszMonth, "Bad Month", buffersize-1);
    }
    else
    {
        strncpy(pszMonth, months[month-1], buffersize-1);
    }
    pszMonth[buffersize-1] = '\0'; // Ensure a valid terminating zero! Many people forget this!
}

int main()
{
    char month[16]; // 16 bytes allocated here on the stack.
    calculateMonth(3, month, sizeof(month));
    printf("%s", month); // Prints "Mar"
}

Istnieje wiele powodów, dla których ta druga metoda jest lepsza, szczególnie jeśli piszesz bibliotekę do użytku przez innych (nie musisz blokować określonego schematu alokacji / zwalniania, osoby trzecie nie mogą złamać twojego kodu, i nie musisz łączyć się z konkretną biblioteką zarządzania pamięcią), ale jak w przypadku każdego kodu, to od Ciebie zależy, co lubisz najbardziej. Z tego powodu większość ludzi wybiera na przykład 1, dopóki nie zostały spalone tyle razy, że nie chcą już pisać tego w ten sposób;)

Zrzeczenie się:

Kilka lat temu przeszedłem na emeryturę i moje C jest teraz trochę zardzewiałe. Ten kod demonstracyjny powinien w całości skompilować się poprawnie w C (jest jednak w porządku dla każdego kompilatora C ++).

cmroanirgo
źródło
2
W rzeczywistości funkcja musi zwrócić a char *, ponieważ literały łańcuchowe w C są typu char[]. Nie wolno ich jednak w żaden sposób modyfikować, dlatego const char*preferowane jest zwracanie (patrz securecoding.cert.org/confluence/x/mwAV ). Zwrot char *może być potrzebny, jeśli ciąg zostanie użyty w starszej lub zewnętrznej funkcji bibliotecznej, która (niestety) oczekuje char*argumentu jako argumentu, nawet jeśli będzie on tylko odczytywał z niego. Z drugiej strony C ++ ma literały łańcuchowe const char[]typu (a od C ++ 11 można również mieć std::stringliterały).
TManhente
17
@cmroanirgo prefiks my deklaruje czytelnikowi, że funkcja została utworzona przez użytkownika. Uważam, że użycie w takim kontekście jest całkowicie uzasadnione.
ilość
4
zgodnie z tutaj: stackoverflow.com/questions/9970295/… , możesz zwrócić literał ciągu
giorgim
6
Kod zaznaczony fraught with problemsw sekcji „Długowieczność danych” jest właściwie całkowicie poprawny. Literały ciągów mają statyczne okresy istnienia w C / C ++. Zobacz link, o którym wspomina Giorgi powyżej.
chengiz
1
@cmroanirgo Zwracanie literałów ciągu to dobra praktyka i dobry styl. Nie jest „najeżony problemami” i nie ulegnie awarii 9 na 10 razy: nigdy się nie zawiesi. Nawet kompilatory z lat 80. (przynajmniej te, których używałem) poprawnie obsługują nieograniczoną żywotność literałów ciągów. Uwaga: nie jestem pewien, co miałeś na myśli, mówiąc o edytowaniu odpowiedzi: nadal widzę, że jest ona podatna na awarie.
cessy
12

Łańcuch AC jest zdefiniowany jako wskaźnik do tablicy znaków.

Jeśli nie możesz mieć wskaźników, z definicji nie możesz mieć łańcuchów.

Crashworks
źródło
Można przekazać w tablicy do funkcji, a następnie działać na tej tablicy: void foo( char array[], int length). Oczywiście arrayjest to wskaźnik pod maską, ale nie jest to wskaźnik „jawnie”, dlatego może być bardziej intuicyjny dla kogoś, kto uczy się tablic, ale nie do końca nauczył się wskaźników.
jvriesem
12

Zwróć uwagę na tę nową funkcję:

const char* myFunction()
{
    static char array[] = "my string";
    return array;
}

Zdefiniowałem „tablicę” jako statyczną. W przeciwnym razie po zakończeniu funkcji zmienna (i zwracany wskaźnik) wyjdzie poza zakres. Ponieważ ta pamięć jest przydzielona na stosie i zostanie uszkodzona. Wadą tej implementacji jest to, że kod nie jest ponownie wprowadzany i nie jest bezpieczny dla wątków.

Inną alternatywą byłoby użycie malloc do przydzielenia ciągu w stercie, a następnie zwolnienie we właściwych lokalizacjach kodu. Ten kod będzie ponownie wprowadzany i bezpieczny dla wątków.

Jak zauważono w komentarzu, jest to bardzo zła praktyka, ponieważ atakujący może następnie wstrzyknąć kod do Twojej aplikacji (musi otworzyć kod za pomocą GDB, następnie utworzyć punkt przerwania i zmodyfikować wartość zwracanej zmiennej, aby przepełnić i zabawa dopiero się zaczyna).

O wiele bardziej zalecane jest, aby wywołujący zajmował się alokacjami pamięci. Zobacz ten nowy przykład:

char* myFunction(char* output_str, size_t max_len)
{
   const char *str = "my string";
   size_t l = strlen(str);
   if (l+1 > max_len) {
      return NULL;
   }
   strcpy(str, str, l);
   return input;
}

Należy pamiętać, że jedyną treścią, którą można zmodyfikować, jest treść użytkownika. Kolejny efekt uboczny - ten kod jest teraz bezpieczny wątkowo, przynajmniej z punktu widzenia biblioteki. Programista wywołujący tę metodę powinien sprawdzić, czy używana sekcja pamięci jest wątkowo bezpieczna.

elcuco
źródło
2
Na ogół jest to zły sposób postępowania. Znak * może być manipulowany przez otaczający kod. Oznacza to, że możesz robić takie rzeczy: strcpy (myFunction (), "Naprawdę długi ciąg"); a program ulegnie awarii z powodu naruszenia zasad dostępu.
cmroanirgo
Brakuje czegoś w pobliżu „tego, którego użytkownik” .
Peter Mortensen
8

Twój problem dotyczy zwracanego typu funkcji - musi to być:

char *myFunction()

... a wtedy Twoja oryginalna receptura zadziała.

Zauważ, że nie możesz mieć łańcuchów C bez wskaźników, które znajdują się gdzieś wzdłuż linii.

Ponadto: Podkręć ostrzeżenia kompilatora. Powinien był cię ostrzec o tej linii powrotnej konwertującej a char *na charbez jawnego rzutowania.

kawiarnia
źródło
1
Myślę, że podpis powinien const char *, ponieważ ciąg jest literałem, ale jeśli się nie mylę, kompilator to zaakceptuje.
Luke
5

Opierając się na nowo dodanej historii z pytaniem, dlaczego po prostu nie zwrócić liczby całkowitej od 1 do 12 dla miesiąca i pozwolić funkcji main () na użycie instrukcji switch lub drabiny if-else do podjęcia decyzji, co wydrukować? Z pewnością nie jest to najlepsza droga - byłby to char * - ale w kontekście takiej klasy jak ta wyobrażam sobie, że jest prawdopodobnie najbardziej elegancka.

Twisol
źródło
3

Możesz utworzyć tablicę w funkcji wywołującej, która jest główną funkcją, i przekazać ją do obiektu wywoływanego, którym jest twoja funkcja myFunction (). W ten sposób myFunction może wypełnić ciąg znaków w tablicy. Musisz jednak zadeklarować myFunction () jako

char* myFunction(char * buf, int buf_len){
  strncpy(buf, "my string", buf_len);
  return buf;
}

A w głównej funkcji myFunction należy wywołać w ten sposób:

char array[51];
memset(array, 0, 51); /* All bytes are set to '\0' */
printf("%s", myFunction(array, 50)); /* The buf_len argument  is 50, not 51. This is to make sure the string in buf is always null-terminated (array[50] is always '\0') */

Jednak wskaźnik jest nadal używany.

ChainLooper
źródło
2

Typ zwracanej funkcji to pojedynczy znak ( char). Powinieneś zwrócić wskaźnik do pierwszego elementu tablicy znaków. Jeśli nie możesz użyć wskaźników, masz przerąbane. :(

hrnt
źródło
2

A co powiesz na ten:

void print_month(int month)
{
    switch (month)
    {
        case 0:
            printf("January");
            break;
        case 1:
            printf("february");
            break;
        ...etc...
    }
}

I nazwij to miesiącem, który obliczasz gdzie indziej.

Sebastiaan M
źródło
1
+1 nie to, o co prosił OP, ale prawdopodobnie tego oczekuje od ciebie zadanie, ponieważ nie może używać wskaźników.
Vitim.us
Nawet printf używa wskaźników. Wskaźnik jest jak nóż - niezbędny do życia i pracy, ale musisz trzymać go za rękojeść i używać ostrej strony do cięcia, bo inaczej będziesz się źle bawić. Niefortunne umieszczenie spacji w definicji funkcji jest błędem mózgu wielu nowych programistów C. char * func (char * s); char func (char * s); char func * char * s); są takie same, ale wszystkie wyglądają inaczej, i aby skomplikować zamieszanie, * jest także operatorem usuwania referencji dla zmiennych, które są wskaźnikami.
Chris Reid
1

A charto tylko pojedynczy znak jednobajtowy. Nie może przechowywać ciągu znaków ani nie jest wskaźnikiem (którego najwyraźniej nie możesz mieć). Dlatego nie możesz rozwiązać swojego problemu bez użycia wskaźników (co char[]jest cukrem syntaktycznym).

Nick Bedford
źródło
1

Jeśli naprawdę nie możesz użyć wskaźników, zrób coś takiego:

char get_string_char(int index)
{
    static char array[] = "my string";
    return array[index];
}

int main()
{
    for (int i = 0; i < 9; ++i)
        printf("%c", get_string_char(i));
    printf("\n");
    return 0;
}

Magiczna liczba 9 jest okropna, a to nie jest przykład dobrego programowania. Ale o co chodzi. Zauważ, że wskaźniki i tablice to to samo (coś w rodzaju), więc jest to trochę oszustwo.

Sebastiaan M
źródło
Zwykle, jeśli musisz wdrożyć takie rozwiązania problemów z pracą domową, twoje wstępne założenia są błędne.
hrnt
1

Cóż, w swoim kodzie próbujesz zwrócić String(w C, który jest niczym innym jak tablicą znaków zakończoną znakiem null), ale typ zwracania twojej funkcji jest tym, charktóry sprawia ci wszystkie kłopoty. Zamiast tego powinieneś napisać to w ten sposób:

const char* myFunction()
{

    return "My String";

}

I zawsze dobrze jest zakwalifikować swój typ constprzy przypisywaniu literałów w C do wskaźników, ponieważ literały w C nie są modyfikowalne.

Cheshar
źródło
0

Prototyp funkcji stwierdza, że ​​funkcja zwróci znak. W związku z tym nie możesz zwrócić ciągu w swojej funkcji.

user224579
źródło
0
char* myFunction()
{
    return "My String";
}

W języku C literały ciągów są tablicami ze stałą statyczną klasą pamięci, więc zwrócenie wskaźnika do tej tablicy jest bezpieczne. Więcej szczegółów znajduje się w pytaniu o przepełnienie stosu „Czas życia” literału ciągu w C

Oleg Karavan
źródło
0

Zwróć ciąg z funkcji

#include <stdio.h>

const char* greet() {
  return "Hello";
}

int main(void) {
  printf("%s", greet());
}
namiętni ewolucje
źródło