Повернення рядка 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.