Как использовать memset при обработке строк в C++?

Я из Python и недавно изучаю C++. Я изучал функцию C/C++ под названием memset и следовал онлайн-примеру с веб-сайта https://www.geeksforgeeks.org/memset-in-cpp/, где я получил несколько ошибок компиляции:

/**
 * @author      : Bhishan Poudel
 * @file        : a02_memset_geeks.cpp
 * @created     : Wednesday Jun 05, 2019 11:07:03 EDT
 * 
 * Ref: 
 */

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

int main(int argc, char *argv[]){
    char str[] = "geeksforgeeks";

    //memset(str, "t", sizeof(str));
    memset(str, 't', sizeof(str));

    cout << str << endl;

    return 0;
}

Ошибка при использовании одинарных кавычек 't'
Это печатает дополнительные символы.

tttttttttttttt!R@`

Ошибка при использовании "t" с двойными кавычками

$ g++ -std=c++11 a02_memset_geeks.cpp 
a02_memset_geeks.cpp:17:5: error: no matching function for call to 'memset'
    memset(str, "t", sizeof(str));
    ^~~~~~
/usr/include/string.h:74:7: note: candidate function not viable: no known
      conversion from 'const char [2]' to 'int' for 2nd argument
void    *memset(void *, int, size_t);
         ^
1 error generated.

Как использовать memset в C++?

Дальнейшее изучение
Отличный туториал с недостатками memset приведен здесь: https://web.archive.org/web/20170702122030/https:/augias.org/paercebal/tech_doc/doc.en/cp.memset_is_evil.html

"t" и 't' не одно и то же.
SergeyA 05.06.2019 17:14

большинство онлайн-ресурсов для обучения С++ - дерьмо, и, на самом деле, этот сайт не является исключением, попробуйте вместо этого: stackoverflow.com/questions/388242/…

463035818_is_not_a_number 05.06.2019 17:17

Я использовал одинарную кавычку 't', но все равно получаю дополнительные символы на выходе.

BhishanPoudel 05.06.2019 17:18

После того, как вы это сделаете, это больше не будет корректно завершённой нулем C-строкой. Вы потеряли 0. Попробуйте вместо этого передать sizeof(str)-1.

Hans Passant 05.06.2019 17:19

@HansPassant Тогда как его использовать? Является ли он устаревшим и не используется в настоящее время?

BhishanPoudel 05.06.2019 17:21

Зачем вообще использовать memset в C++? Причина, по которой существуют старые функции C, заключается в обратной совместимости.

klutt 05.06.2019 17:23

Это заряженный пистолет, вы навели его на левую ногу и нажали на курок. Вы должны целиться правильно.

Hans Passant 05.06.2019 17:23

это весьма актуально: stackoverflow.com/a/8590379/4117728

463035818_is_not_a_number 05.06.2019 17:23

Также используйте std::string вместо char[]

klutt 05.06.2019 17:24

@HansPassant Значит, он должен прострелить себе правую ногу? ;)

dbush 05.06.2019 17:26

@dbush: Или, возможно, хуже ...

Lightness Races in Orbit 05.06.2019 17:27

Вы не должны менять вопрос под людьми, которые на него отвечают. Если вы берете комментарий или ответ, и он все еще не работает, вы можете задать другой вопрос, но такого рода редактирование, которое заменяет один вопрос другим, разрушительно.

SergeyA 05.06.2019 17:31

Не используйте std::memset, используйте станд::заполнить, это так же быстро и безопасно.

Galik 05.06.2019 18:04

Вопросы, относящиеся к данным ответам или содержащие редактирование чего-то, что относится к ответу, всегда выглядят немного странно, имхо. Вы можете ответить на свой вопрос, хотя

463035818_is_not_a_number 05.06.2019 18:21

Если вы внимательно посмотрите на страницу, которую вы цитируете, и посчитаете, вы заметите, что "geeksforgeeks" имеет 13 символов, а строка t, которая представляет вывод, имеет 14. Таким образом, пример кода также создает дополнительный вывод. Как видно из ответов, это не неожиданность — код просто неверный.

Pete Becker 06.06.2019 06:11

Вам действительно следует избегать using namespace std - это плохая привычка, и может молча изменить смысл вашей программы, когда вы этого не ожидаете. Привыкайте использовать префикс пространства имен (std намеренно очень короткий) или импортировать только нужные имена в наименьший разумный объем. Неужели так сложно написать std::memset?

Toby Speight 06.06.2019 10:10

@HansPassant Стрелять в правую ногу лучше? =П

Shamtam 06.06.2019 14:47

@SergeyA Я не уверен, почему этот вопрос отложен, он имеет полный MWE, правильно решает вопрос и не содержит вульгарных слов или каких-либо плохих комментариев.

BhishanPoudel 07.06.2019 16:47

В исходном вопросе @astro123 "t" был заключен в двойные кавычки, и я проголосовал за то, чтобы закрыть его как опечатку (очевидно, что memset не принимает указатели в качестве второго аргумента). И затем вы отредактировали вопрос, полностью изменив его значение - он не стал допустимым вопросом, на который можно ответить, но тем самым вы аннулируете предыдущие ответы, что на самом деле не так уж и здорово.

SergeyA 07.06.2019 17:26

IMO единственный правильный ответ на этот вопрос: «Вы этого не сделаете» - по крайней мере, пока вы изучаете С++. Mybe, когда вы эксперт. И даже тогда, наверное, нет.

Wouter van Ooijen 11.06.2019 19:50
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
29
20
8 650
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Error when using single quotes 't' This prints extra characters.

Это потому, что вы перезаписали нулевой терминатор.

Терминатор является частью размера массива (массив не волшебный), хотя это не часть размера строки логический.

Итак, я думаю, вы имели в виду:

memset(str, 't', strlen(str));
//               ^^^^^^

Error when using "t" with double quotes

Совершенно другая вещь. Вы сказали компьютеру установить каждый символ в строке как строку. Не имеет смысла; не будет компилироваться.


How to use memset in C++?

Не надо.

Либо используйте безопасный тип std::fill в сочетании с std::begin и std::end:

std::fill(std::begin(str), std::end(str)-1, 't');

(Если вы беспокоитесь о производительности, не беспокойтесь: это просто делегирует memset, где это возможно, через специализацию шаблона, оптимизация не требуется, без ущерба для безопасности типов; пример здесь в libstdС++.)

Или просто std::string для начала. ?


I was learning the fuction memset in C++ from https://www.geeksforgeeks.org/memset-in-cpp/ where the example is given as below

Не пытайтесь изучать C++ на случайных веб-сайтах. Вместо этого получите себе Хорошая книга.

к сожалению, это действительно sizeof в исходном примере. Жаль, что такой код используется для "обучения" c++ :(

463035818_is_not_a_number 05.06.2019 17:25

Я изучаю C++ и учусь онлайн от geeksforgeeks.org/memset-in-cpp, пример взят оттуда, никаких предупреждений там не было. Спасибо за информацию об использовании.

BhishanPoudel 05.06.2019 17:25

Обновлено с учетом обоих комментариев.

Lightness Races in Orbit 05.06.2019 17:26

Кроме того, я из фона Python, где одинарные кавычки и двойные кавычки одинаковы, поэтому я также получил еще одну ошибку.

BhishanPoudel 05.06.2019 17:28

@astro123 Еще одна причина работать с хорошей книгой. В C++ есть разные виды литералов, которые полностью отличаются от Python.

Lightness Races in Orbit 05.06.2019 17:28

Этот сайт (geeksforgeeks) должен быть навсегда забанен.

SergeyA 05.06.2019 17:32

@astro123: обучение онлайн на сайте geeksforgeeks.org/memset-in-cpp Первая проблема. Этот учебник имеет серьезную ошибку в его крошечном примере. Это не редкость на geeksforgeeks.org. Есть хорошие вещи немного, но они часто смешиваются с плохими вещами, и пока вы не станете экспертом вы не будете знать, как отличить. В отличие от Stack Overflow, у geeksforgeeks нет механизма голосования, чтобы люди могли просматривать сообщения и указывать их качество, поэтому у вас нет возможности узнать, каким из них доверять.

Peter Cordes 06.06.2019 02:23

@PeterCordes это позор, ТАК Документация пошла так, как это было ... явно есть спрос на кураторские руководства, за которые проголосовали. Я уверен, что со временем кто-нибудь придумает правильный дизайн.

mbrig 06.06.2019 20:18

Почему "не"? Разве реализация memset не часто значительно быстрее? fill не всегда может быть реализован для использования директивы сборки (даже в -O4), в то время как memset всегда должен использовать ее, если она доступна. Также никогда не следует использовать strlen. Просто никогда. если вы знаете размер строки во время компиляции (и sizeof действительно), используйте его. Если вы не знаете этого во время компиляции, strlen небезопасно.

grovkin 06.06.2019 22:01

@grovkin Нет, основная реализация будет делегировать memset через специализацию, т. Е. Когда аргументы шаблона предполагают, что это требуется (уровень оптимизации не требуется) - например libstdС++. Не нужно пытаться «превзойти тулчейн», потому что он лучше нас. С другой стороны, вы жертвуете своей безопасностью типов, и я имеют видел давние скрытые ошибки, когда кто-то менял тип и не сканировал все его варианты использования, одним из которых был непослушный memset на том, что раньше было массивом C.

Lightness Races in Orbit 07.06.2019 12:21

@LightnessRacesinOrbit вы ожидаете, что шаблоны будут специализированы для char *, но я видел, как компиляторы, используемые в производственном коде (я думаю, это был Sun), работали в 10 раз медленнее при использовании copy() вместо memcpy для векторов. Тот факт, что в языке есть механизмы для обработки этого, не означает, что используемый вами компилятор справится с этим. И когда проблема имеет практическое, а не теоретическое значение, вы должны использовать те инструменты, которые у вас есть, а не те, которые, по вашему мнению, у вас должны быть.

grovkin 07.06.2019 21:12

@grovkin Такую откровенно нестандартную реализацию не следует использовать в первую очередь.

Lightness Races in Orbit 08.06.2019 02:33

@LightnessRacesinOrbit еще раз, когда проблема имеет практическое, а не теоретическое значение, вы должны использовать инструменты, которые у вас есть, а не те, которые, по вашему мнению, у вас должны быть. Я вижу, что у вас все еще есть strlen в ответе, кстати. Эта функция никогда не должна использоваться. В любом коде.... когда-либо.

grovkin 08.06.2019 02:43
Ответ принят как подходящий

Эта декларация

char str[] = "geeksforgeeks";

объявляет массив символов, содержащий строку, представляющую собой последовательность символов, включая завершающий нулевой символ '\0'.

Вы можете представить объявление следующим эквивалентным способом

char str[] = 
{ 
    'g', 'e', 'e', 'k', 's', 'f', 'o', 'r', 'g', 'e', 'e', 'k', 's', '\0'
};

Этот вызов функции memset

memset(str, 't', sizeof(str));

переопределяет все символы массива, включая завершающий ноль.

Итак, следующее утверждение

cout << str << endl;

приводит к неопределенному поведению, поскольку он выводит символы до тех пор, пока не встретится завершающий ноль.

Вместо этого вы могли бы написать

#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', sizeof( str ) - 1 );
    
    std::cout << str << '\n';
}

Или следующим образом

#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', std::strlen( str ) );
    
    std::cout << str << '\n';
}

Это сохраняет конечный ноль неизменным в массиве.

Если вы хотите переопределить все символы массива, включая завершающий ноль, вам следует заменить этот оператор

std::cout << str << '\n';

для этого заявления

std::cout.write( str, sizeof( str ) ) << '\n';

как это показано в программе ниже, потому что массив теперь не содержит строки.

#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', sizeof( str ) );
    
    std::cout.write( str, sizeof( str ) ) << '\n';
}

Что касается этого звонка

memset(str, "t", sizeof(str));

то тип второго аргумента (то есть тип const char *) не соответствует типу второго параметра функции, имеющего тип int. См. объявление функции

void * memset ( void * ptr, int value, size_t num );

Таким образом, компилятор выдает сообщение об ошибке.

Помимо массивов символов (которые очень часто используются даже в C++) вы также можете использовать стандартный класс std::string (или std::basic_string), который имитирует строки.

В этом случае нет необходимости использовать стандартную функцию C memset для заполнения строки одним символом. Самый простой способ сделать это следующий

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.assign( s.length(), 't' );
    
    std::cout << s << '\n';
}

Другой способ — использовать стандартный алгоритм std::fill или std::fill_n, заявленный в шапке <algorithm>. Например

#include <iostream>
#include <string>
#include <iterator>
#include <algorithm>

int main()
{
    std::string s( "geeksforgeeks" );
    
    std::fill( std::begin( s ), std::end( s ), 't' );
    
    std::cout << s << '\n';
}

или

#include <iostream>
#include <string>
#include <iterator>
#include <algorithm>

int main()
{
    std::string s( "geeksforgeeks" );
    
    std::fill_n( std::begin( s ), s.length(), 't' );
    
    std::cout << s << '\n';
}

Вы даже можете использовать метод replace класса std::string одним из следующих способов:

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.replace( 0, s.length(), s.length(), 't' );
    
    std::cout << s << '\n';
}

Или

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.replace( std::begin( s ), std::end( s ), s.length(), 't' );
    
    std::cout << s << '\n';
}

Исходный пост ясно указывает на то, что пользователь пытается изучить C++. Упомяните, по крайней мере, что ничего из этого не имеет значения, если вы используете std::string, который следует использовать здесь вместо того, чтобы использовать этот сложный C материал. (Возможно, это будет полезно знать, но не в начале курса)

JVApen 05.06.2019 19:52

@JVApen Исходное сообщение ясно указывает на то, что пользователь пытается узнать, как использовать memset с массивами символов. :)

Vlad from Moscow 06.06.2019 14:13

Хороший ответ. Если вы хотите, чтобы это было лучше для OP: обратите внимание на разницу в системе типов. C++ имеет статическую систему типов, в которой переменные имеют фиксированный статический тип. Python имеет полностью динамическую систему типов, в которой значения имеют тип, а переменные — нет. Вероятно, это источник его путаницы с 't' и "t".

Yakk - Adam Nevraumont 06.06.2019 17:45

Что вы подразумеваете под «строками имитирует»?

Ruslan 06.06.2019 23:30

@ Рэй Ты ошибаешься. Для начала правильное объявление будет выглядеть как const char *str = "geeksforgeeks"; И в любом случае строковые литералы в C и C++ неизменяемы. Любая попытка изменить строковый литерал приводит к неопределенному поведению.

Vlad from Moscow 07.06.2019 15:35

@VladfromMoscow Ты прав, конечно. Я не уделял должного внимания тому, что именно делалось после звонка strlen. Я пересматриваю свое предложение: вы также можете объяснить, что если строка была объявлена ​​как const char *str = "geeksforgeeks";, sizeof больше не будет сообщать длину строки, а скорее размер указателя. (Даже если объявление его как указателя на строковый литерал в этом конкретном примере приводит к дальнейшим проблемам, я видел достаточно людей, делающих ошибку, делая sizeof указателя на строку, и я думаю, что стоит объяснить, почему это не работает. )

Ray 07.06.2019 17:03

@ Рэй Спасибо. Но это будет слишком широкий ответ на простой вопрос. :)

Vlad from Moscow 07.06.2019 17:07

Это правильный синтаксис для memset...

void* memset( void* dest, int ch, std::size_t count );

Converts the value ch to unsigned char and copies it into each of the first count characters of the object pointed to by dest. If the object is a potentially-overlapping subobject or is not TriviallyCopyable (e.g., scalar, C-compatible struct, or an array of trivially copyable type), the behaviour is undefined. If count is greater than the size of the object pointed to by dest, the behaviour is undefined.

(источник)

Для первого синтаксиса memset(str, 't', sizeof(str));. Компилятор пожаловался на лишний размер. Он печатает 18 раз tttttttttttttt!R@. Я предлагаю попробовать sizeof(str) -1 для массива символов.

Для второго синтаксиса memset(str, "t", sizeof(str)); вы предоставляете второй параметр как строку. По этой причине компилятор жалуется на ошибку: неверное преобразование из ‘const char*’ в ‘int’

потенциально перекрывающийся подобъект чего? Автоматически UB не изменяет объектное представление других объектов в C++. Например, uint32_t имеет полностью определенное представление объекта (за исключением порядка следования байтов). Так что неясно, о каком перекрытии вы говорите, потому что memset принимает только один аргумент указателя; другие аргументы по значению. Эта формулировка имеет смысл для memcpy, которая запрещает перекрытие, в отличие от memmove.
Peter Cordes 06.06.2019 02:26

@PeterCordes Честно говоря, эта фраза была заимствована с сайта cppreference.com. Поэтому, если это неправильно, cppreference.com необходимо исправить.

Lightness Races in Orbit 06.06.2019 11:48

@LightnessRacesinOrbit: на cppref эта фраза является гиперссылкой на определение, что имеет смысл. Вполне правдоподобно, что это UB, если memset также может изменять байты другого объекта (поскольку указатель является подобъектом структуры, объявленной с помощью [[no_unique_address]], что позволяет компилятору делать все, что он хочет, включая создание битовых полей для узкого или логические типы, я думаю). Я менее ясен в части «подобъект базового класса»; возможно, это UB, потому что он может перезаписать указатель vtable?

Peter Cordes 06.06.2019 11:57

@PeterCordes - речь идет о что-то вроде этого. Здесь base легко копируется, но это небезопасно для memset (или memmove) или потому, что это потенциально перекрывающийся подобъект. Обратите внимание, что sizeof(base) == 8, но когда он используется в качестве основы derived (который сам имеет член char), sizeof(derived) == 8! Таким образом, члены производного хранятся в дополнении base. Следовательно, небезопасно перезаписывать произвольный base& на memset, так как в этом случае вы также уничтожите производный член.

BeeOnRope 21.01.2020 19:44

Обратите также внимание на то, как это отражается в генерации кода на gcc для обнуления base в b = base{}: он выполняет запись qword и byte, поскольку не может безопасно расширить это до одной записи qword, потому что отступы могут использоваться повторно. Затем см. base2 и derived2: они идентичны, за исключением того, что base2 — это struct, а не class. Затем он становится агрегатом, и, я думаю, перекрытие запрещено (обратите внимание, как меняется b = base2{} codegen).

BeeOnRope 21.01.2020 19:53

@BeeOnRope: Вы имеете в виду dword + byte до нуля base (вы сказали qword дважды). Интересно. Единственная разница между классом и структурой заключается в том, что класс по умолчанию имеет значение private:, а структура — public:. Похоже, что размещение производных членов в дополнении базы зависит только от видимости и переключается, если вы используете эти теги, чтобы иметь частные члены в структуре base2 и общедоступные члены базового класса. godbolt.org/z/3VLeiS

Peter Cordes 21.01.2020 22:22

@Peter Да, это связано с видимостью, хотя я не знал, почему. Как и выше, я думал, что это ключ к тому, является ли base Aggregate или нет (в основном, самая POD-подобная вещь, которую предлагает C++). Это не отключение стандартной раскладки, что я проверял.

BeeOnRope 22.01.2020 01:50

@BeeOnRope: я думаю, что это может быть дизайнерское решение, которое могло пойти в любом случае. А может и нет: согласно itanium-cxx-abi.github.io/cxx-abi/abi.html#PODЭтот ABI использует определение POD только для определения размещать ли объекты в хвостовой части подобъекта базового класса. Хотя стандарты со временем расширили определение POD, они также запретили программисту напрямую читать или записывать базовые байты подобъекта базового класса, например, с помощью memcpy. (x86-64 использует тот же C++ ABI). С некоторыми вещами о том, как развивался POD в C++.

Peter Cordes 22.01.2020 02:04

@PeterCordes - верно, разработанное решение должно быть в контексте ABI платформы, а не только на уровне компилятора, поскольку все должны с этим согласиться, верно? В любом случае, единственное свойство, которое я обнаружил, которое не противоречило практике в отношении того, может ли дополнение использоваться производным классом, было «агрегатным». См. здесь. base это POD, тривиальная и стандартная раскладка, но все равно небезопасная. Однако это не совокупность. Конечно, это не доказательство :).

BeeOnRope 22.01.2020 03:36

@BeeOnRope: Ах, я не знал, что «совокупность» имеет особое техническое значение, которое включает в себя отсутствие частных/защищенных членов. Что такое агрегаты и POD и в чем их особенность?. Я не проверял это, но я думаю из примечаний C++ ABI к "POD", что (некоторый черновик) ISO C++ должен сказать, что вы можете наступать на заполнение агрегата, но не обязательно вообще для любого POD/тривиально -копируемый тип. Таким образом, вы можете поместить производные члены в это заполнение, когда база не является агрегатом. Это то, с чем соглашается этот C++ ABI.

Peter Cordes 22.01.2020 03:49

@PeterCordes - да, я только что закончил читать (пролистывать) и этот FAQ :). Я не нахожу слова «агрегат» в связанном с вами Itanium ABI. ABI был написан задолго до многих изменений в стандарте C++, упомянутых в FAQ, и до того, как некоторые термины вообще появились. В частности, в более позднем стандарте были введены более тонкие различия, о которых не знал документ ABI. 1/х

BeeOnRope 22.01.2020 03:54

Я не читал ABI, но, основываясь на поиске, я не могу найти язык, охватывающий этот случай. Он упоминает «подобъект базового класса» в разделе, который вы связали как «потенциально перекрывающийся подобъект» одного типа (другой является членами данных с no_unique_address), но все дальнейшие ссылки на «подобъект базового класса» не кажутся уместными (они о vtables), и дальнейшие ссылки на «подобъект po», похоже, касаются случая члена данных, а не случая базового класса. Я делаю конкретный вопрос по теме, свяжу его здесь.

BeeOnRope 22.01.2020 03:55

@PeterCordes — к вашему сведению.

BeeOnRope 22.01.2020 04:07

Влад услужливо ответил на первую часть вашего вопроса, но я чувствую, что вторую часть можно было бы объяснить немного более интуитивно:

Как уже упоминалось, 't' — это персонаж, тогда как "t" — это нить, а строки имеют нулевой терминатор в конце. Это делает "t" массив не из одного, а из два символов — ['t', '\0']! Это делает ошибку memset более интуитивно понятной — она может достаточно легко преобразовать один char в int, но задыхается, когда получает массив char. Как и в Python, int(['t', '\0']) (или ord(['t', '\0'])) не вычисляется.

А если быть еще более точным, то при передаче «t» адрес «t» передается в «t». Поэтому, если бы он был преобразован в параметр int в memset, это был бы указатель на 't', преобразованный в int, а не значение строки, преобразованное в int.

grovkin 06.06.2019 21:58

Другие вопросы по теме