Повернення рядка C з функції


109

Я намагаюся повернути рядок C з функції, але це не працює. Ось мій код.

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

В mainЯ кличу це наступним чином :

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

Я також спробував деякі інші способи myFunction, але вони не працюють. Наприклад:

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

Примітка. Мені заборонено використовувати вказівники!

Невелика інформація щодо цієї проблеми:

Є функція, яка з’ясовує, який саме місяць. Наприклад, якщо це 1, то він повертає січень і т.д.

Тому , коли він збирається друкувати, він робить це так: printf("Month: %s",calculateMonth(month));. Тепер проблема полягає в тому, як повернути цей рядок з calculateMonthфункції.


10
На жаль, у цьому випадку вам потрібні покажчики.
— Нік Бедфорд

1
@Hayato Добре, я вважаю, що ми тут дорослі, і знаю, що це повинно повернутися 0, це було просто заради прикладу
— lox

3
return 0мається на увазі за замовчуванням лише в C99 (і C ++), але не в C90.
— hrnt

1
Тоді ви цього не зможете зробити, крім тупих хакків, які все-таки справді просто зламали маніпуляції вказівниками. Покажчики існують з причини ...: |
— GManNickG

Відповіді:


222

Ваш підпис функції повинен бути:

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

Фон:

Це настільки фундаментально для C&C ++, але дискусій має бути трохи більше.

У C (& C ++ для цього питання) рядок - це лише масив байтів, що закінчується нульовим байтом - отже, термін "string-zero" використовується для представлення саме цього аромату рядка. Існують і інші види струн, але в C (& C ++) цей аромат притаманний самій мові. Інші мови (Java, Pascal тощо) використовують різні методології, щоб зрозуміти "мій рядок".

Якщо ви коли-небудь використовуєте API Windows (який знаходиться на C ++), ви побачите досить регулярно функціонуючі параметри, такі як: "LPCSTR lpszName". Частина 'sz' представляє це поняття 'string-zero': масив байтів з нульовим (/ zero) термінатором.

Пояснення:

Заради цього «вступу» я вживаю слова «байти» та «символи» взаємозамінно, тому що простіше навчитися цьому. Пам’ятайте, що існують і інші методи (широкоформатні та багатобайтові системи символів ( mbcs )), які використовуються для впорядкування міжнародних символів. UTF-8 є прикладом mbcs. Заради вступу я спокійно пропускаю все це.

Пам'ять:

Це означає, що рядок типу "мій рядок" фактично використовує 9 + 1 (= 10!) Байт. Це важливо знати, коли ви нарешті дістанетесь до динамічного розподілу рядків.

Отже, без цього "завершення нуля" у вас немає рядка. У пам'яті висить масив символів (також його називають буфером).

Довговічність даних:

Використання функції таким чином:

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

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

... як правило, ви потрапляєте на вас із випадковими керованими винятками / несправностями сегмента тощо, особливо "вниз".

Коротше кажучи, хоча моя відповідь правильна - 9 разів з 10 ви закінчите програму, яка виходить з ладу, якщо ви використовуєте її таким чином, особливо якщо ви вважаєте, що це "хороша практика" робити це саме так. Коротше кажучи: це взагалі ні.

Наприклад, уявіть собі деякий час у майбутньому, ниткою тепер потрібно якось маніпулювати. Як правило, кодер "пройде легкий шлях" і (спробує) написати такий код:

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

Тобто, ваша програма буде збій , тому що компілятор (може / не може) випустив пам'ять , використовувану szBufferна той час printf()в main()називаються. (Ваш компілятор також повинен попереджати вас про подібні проблеми.)

Є два способи повернути рядки, які не будуть так легко перетворюватися.

  1. повертаються буфери (статичні або динамічно виділені), які живуть деякий час. У C ++ використовуйте 'допоміжні класи' (наприклад, std::string) для управління довговічністю даних (що вимагає зміни повернення значення функції) або
  2. передайте буфер до функції, яка заповнюється інформацією.

Зауважте, що неможливо використовувати рядки без використання покажчиків на C. Як я показав, вони є синонімами. Навіть у C ++ із класами шаблонів завжди у фоновому режимі завжди буфери (тобто вказівники).

Отже, щоб краще відповісти на (тепер модифіковане запитання). (Напевно, може бути запропоновано безліч "інших відповідей".)

Безпечніші відповіді:

Приклад 1, використовуючи статично виділені рядки:

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"
}

Що "static" робить тут (багатьом програмістам не подобається такий тип "виділення"), що рядки потрапляють у сегмент даних програми. Тобто вона постійно виділяється.

Якщо ви перейдете на C ++, ви використовуєте подібні стратегії:

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;
    }
}

... але, мабуть, простіше використовувати допоміжні класи, наприклад std::string, якщо ви пишете код для власного користування (а не є частиною бібліотеки, якою слід ділитися з іншими).

Приклад 2, використовуючи визначені позивачем буфери:

Це більш "безглуздий" спосіб передачі струн навколо. Дані, що повертаються, не підлягають маніпулюванню стороною, що телефонує. Тобто, приклад 1 легко може зловживати стороною, яка телефонує, і піддавати вас помилкам програми. Таким чином, це набагато безпечніше (хоча і використовує більше рядків коду):

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"
}

Є багато причин, чому другий метод кращий, особливо якщо ви пишете бібліотеку, щоб її використовували інші (вам не потрібно замикатися на певну схему розподілу / розсилки, треті сторони не можуть порушити ваш код, і вам не потрібно посилатися на певну бібліотеку управління пам’яттю), але, як і весь код, від вас залежить те, що вам найбільше подобається. З цієї причини більшість людей вибирають наприклад 1, поки їх не спалили стільки разів, що більше не відмовляються писати це;)

Відмова:

Я пішов на пенсію кілька років тому, і зараз моє C трохи іржаве. Цей демо-код повинен усі правильно компілюватись із C (хоча це добре для будь-якого компілятора C ++).


2
Насправді, функція повинна повертати a char *, оскільки рядкові літерали на C мають тип char[]. Вони, однак, не повинні змінюватися жодним чином, тому повернення const char*є кращим (див. Securecoding.cert.org/confluence/x/mwAV ). Повернення char *може знадобитися, якщо рядок буде використано у застарілій або зовнішній бібліотечній функції, яка (на жаль) очікує char*як аргумент, навіть жорсткий, вона буде читатися лише з неї. З іншого боку, C ++ має рядкові літерали const char[]типу (і, оскільки C ++ 11, ви також можете мати std::stringлітерали).
— TManhente

17
@cmroanirgo мій префікс заявляє читачеві, що функцію створив користувач. Я вважаю цілком розумним використовувати в такому контексті.
— кількісне

4
відповідно до тут: stackoverflow.com/questions/9970295/… , ви можете повернути рядковий
— буквал

6
Код, позначений fraught with problemsу розділі "Довговічність даних", насправді є абсолютно дійсним. Лінійні рядки мають статичний час життя в C / C ++. Дивіться посилання, про яке згадує Георгій.
— chengiz

1
@cmroanirgo Повернення рядкових літералів - це добра практика та гарний стиль. Це не «загрожує проблемами», і воно не зірветься 9 із 10 разів: ніколи не вийде з ладу. Навіть компілятори з 80-х (принаймні тих, що я використовував) правильно підтримують необмежений термін служби рядкових літералів. Примітка. Я не впевнений, що ви мали на увазі під редагуванням відповіді: я все ще бачу, що це говорить про схильність до збоїв.
— cesss

12

Рядок змінного струму визначається як вказівник на масив символів.

Якщо ви не можете мати покажчики, за визначенням ви не можете мати рядки.


Ви можете передати масив у функцію , а потім працювати на цьому масиві: void foo( char array[], int length). Звичайно, arrayце вказівник під кришкою, але це не "явно" вказівник, і тому може бути більш інтуїтивно зрозумілим для тих, хто навчається масивів, але хто ще не зовсім вивчив покажчики.
— jvriesem

12

Зверніть увагу на цю нову функцію:

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

Я визначив "масив" як статичний. В іншому випадку, коли функція закінчується, змінна (і покажчик, який ви повертаєтеся) виходить із сфери застосування. Так що пам'ять виділяється в стеці, і це буде зіпсована. Недоліком цієї реалізації є те, що код не є ретентом і не є безпечним для потоків.

Іншою альтернативою було б використовувати malloc для розподілу рядка в купі, а потім звільнити правильні місця вашого коду. Цей код буде безпечним і поточним.

Як зазначається в коментарі, це дуже погана практика, оскільки зловмисник може потім ввести код у вашу програму (йому / їй потрібно відкрити код за допомогою GDB, потім зробити точку перерви та змінити значення повернутої змінної для переповнення та забава тільки починається).

Набагато більше рекомендується дозволити абоненту впоратися з розподілом пам'яті. Дивіться цей новий приклад:

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;
}

Зауважте, що єдиний вміст, який можна змінити, - це той, який користувач. Ще один побічний ефект - цей код зараз безпечний для потоків, принаймні з точки зору бібліотеки. Програміст, який викликає цей метод, повинен переконатися, що розділ пам'яті використовується безпечно для потоків.


2
Це взагалі поганий спосіб вести справи. Char * може управляти оточуючим кодом. Тобто ви можете робити такі речі: strcpy (myFunction (), "Дійсно довга струна"); і ваша програма вийде з ладу через порушення доступу.
— cmroanirgo

Щось не вистачає поблизу "тієї, що користувач" .
— Пітер Мортенсен

8

Ваша проблема з типом повернення функції - вона повинна бути:

char *myFunction()

... і тоді ваша оригінальна рецептура запрацює.

Зауважте, що ви не можете мати рядки C без участі покажчиків десь уздовж рядка.

Також: Підготуйте попередження компілятора. Це повинно було попередити вас про ту зворотну лінію, яка перетворює char *на charбез чіткого виступу.


1
Я думаю, що підпис повинен містити char *, оскільки рядок є буквальним, але якщо я не помиляюся, компілятор прийме це.
— Лука

5

Виходячи з вашої щойно доданої події з питанням, чому б просто не повернути ціле число від 1 до 12 за місяць, а дозволити функції main () використовувати оператор перемикання або сходинку if-else для вирішення, що друкувати? Це, звичайно, не найкращий шлях - char * - але в контексті такого класу я думаю, що це, мабуть, найелегантніше.


3

Ви можете створити масив у виклику, який є основною функцією, і передати масив виклику, який є вашою myFunction (). Таким чином, myFunction може заповнити рядок у масив. Однак вам потрібно оголосити myFunction () як

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

І в головній функції, myFunction слід викликати таким чином:

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') */

Однак покажчик все ще використовується.


2

Тип повернення функції - це один символ ( char). Ви повинні повернути вказівник на перший елемент масиву символів. Якщо ви не можете використовувати вказівники, то вас накрутили. :(


2

А як щодо цього:

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

І зателефонуйте, що з місяцем, який ви обчислюєте десь ще.


1
+1 не те, що запитував ОП, але, мабуть, це очікує від вас завдання, оскільки він не може використовувати вказівники.
— Vitim.us

Навіть printf використовує покажчики. Вказівник схожий на ніж - необхідний для життя і роботи, але вам потрібно тримати його за ручку і використовувати гостру сторону для вирізання або вам буде погано провести час. Невдале розміщення пробілів у визначенні функції - це помилка мозку для багатьох нових програмістів на C. char * func (char * s); char func (char * s); char func * char * s); всі однакові, але всі виглядають по-різному, і до складної плутанини * також є оператором відліку для змінних, які є покажчиками.
— Кріс Рейд

1

A char- це лише один однобайтовий символ. Він не може зберігати рядок символів, а також не вказівник (якого, мабуть, не можна мати). Тому ви не можете вирішити свою проблему без використання покажчиків (для яких char[]це синтаксичний цукор).


1

Якщо ви дійсно не можете використовувати вказівники, зробіть щось подібне:

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;
}

Магічне число 9 жахливе, і це не є прикладом хорошого програмування. Але ви розумієте. Зауважте, що вказівники та масиви - це одне й те саме (вид), тож це трохи обман.


Зазвичай, якщо вам потрібно втілити такі рішення проблем домашнього завдання, то ваші попередні припущення помилкові.
— hrnt

1

Ну, у своєму коді ви намагаєтесь повернути a String(в C, що є не що іншим, як нульовим масивом символів), але тип повернення вашої функції charвикликає всі проблеми для вас. Замість цього слід написати це так:

const char* myFunction()
{

    return "My String";

}

І завжди добре кваліфікувати ваш тип const, призначаючи літерали в C покажчикам, оскільки літерали на З не змінюються.


0

У прототипі вашої функції зазначено, що ваша функція поверне знак. Таким чином, ви не можете повернути рядок у своїй функції.



Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.