Можно ли изменить длину строки после того как память под нее была выделена

Тип string, предназначенный для работы со строками символов в кодировке Unicode, является встроенным типом C#. Ему соответствует базовый класс System.String библиотеки .NET.

Строки типа string

Тип string, предназначенный для работы со строками символов в кодировке Unicode, является встроенным типом C#. Ему соответствует базовый класс System.String библиотеки .NET.

Создать строку можно несколькими способами:

string s;                       // инициализация отложена
string t = "qqq";               // инициализация строковым литералом
string u = new string(' ', 20); // конструктор создает строку из 20 пробелов
char[] a = { '0', '0', '0' };   // массив для инициализации строки
string v = new string( a );     // создание из массива символов

Для строк определены следующие операции:

  • присваивание ( = );
  • проверка на равенство ( == );
  • проверка на неравенство ( != );
  • обращение по индексу ( [] );
  • сцепление (конкатенация) строк ( + ).

Несмотря на то, что строки являются ссылочным типом данных, на равенство и неравенство проверяются не ссылки, а значения строк. Строки равны, если имеют одинаковое количество символов и совпадают посимвольно.

Обращаться к отдельному элементу строки по индексу можно только для получения значения, но не для его изменения. Это связано с тем, что строки типа string относятся к так называемым неизменяемым типам данных. Методы, изменяющие содержимое строки, на самом деле создают новую копию строки. Неиспользуемые «старые» копии автоматически удаляются сборщиком мусора.

В классе System.String предусмотрено множество методов, полей и свойств, позволяющих выполнять со строками практически любые действия. Некоторые элементы класса приведены в таблилце 6.3, с остальными можно ознакомиться по учебнику.

Таблица
6.3.
Некоторые элементы класса System.String

Название Вид Описание
Compare Статический метод Сравнение двух строк в лексикографическом (алфавитном) порядке. Разные реализации метода позволяют сравнивать строки и подстроки с учетом и без учета регистра и особенностей национального представления дат и т. д.
Concat Статический метод Конкатенация строк. Метод допускает сцепление произвольного числа строк
Copy Статический метод Создание копии строки
Format Статический метод Форматирование в соответствии с заданными спецификаторами формата (см. далее)
IndexOf, IndexOfAny, LastIndexOf, LastIndexOfAny Методы Определение индексов первого и последнего вхождения заданной подстроки или любого символа из заданного набора
Insert Метод Вставка подстроки в заданную позицию
Join Статический метод Слияние массива строк в единую строку. Между элементами массива вставляются разделители (см. далее)
Length Свойство Длина строки (количество символов)
Split Метод Разделение строки на элементы, используя заданные разделители. Результаты помещаются в массив строк
Substring Метод Выделение подстроки, начиная с заданной позиции
ToCharArray Метод Преобразование строки в массив символов
ToLower, ToUpper Методы Преобразование символов строки к нижнему или верхнему регистру

Пример применения методов приведен в листинге 6.8.

using System;
namespace ConsoleApplication1
{   class Class1
    {   static void Main()
        {
            string s = "прекрасная королева Изольда";
            Console.WriteLine( s );
            string sub = s.Substring( 3 ).Remove( 12, 2 );              // 1
            Console.WriteLine( sub );

            string[] mas = s.Split(' ');                                // 2
            string joined = string.Join( "! ", mas );
            Console.WriteLine( joined );

            Console.WriteLine( "Введите строку" );
            string x = Console.ReadLine();                              // 3
            Console.WriteLine( "Вы ввели строку " + x );

            double a = 12.234;
            int b = 29;
            Console.WriteLine( " a = {0,6:C}  b = {1,2:X}", a, b );     // 4
            Console.WriteLine( " a = {0,6:0.##}  a = {1,5:0.# ' руб. '}",
                               a, b );                                  // 5
        }
    }
}


Листинг
6.8.
Работа со строками типа string

Результат работы программы:

прекрасная королева Изольда
красная корова Изольда
прекрасная! королева! Изольда
Введите строку
не хочу!
Вы ввели строку не хочу!
a = 12,23p.  b = 1D
a = 12,23 a=29 pуб.
Форматирование строк

В операторе 4 из листинга 6.7 неявно применяется метод Format, который заменяет все вхождения параметров в фигурных скобках значениями соответствующих переменных из списка вывода. После номера параметра можно задать минимальную ширину поля вывода, а также указать спецификатор формата, который определяет форму представления выводимого значения.

В общем виде параметр задается следующим образом:

{n [,m[:спецификатор_формата]]}

Здесь n — номер параметра. Параметры нумеруются с нуля, нулевой параметр заменяется значением первой переменной из списка вывода, первый параметр — второй переменной, и т. д. Параметр m определяет минимальную ширину поля, которое отводится под выводимое значение. Если выводимому числу достаточно меньшего количества позиций, неиспользуемые позиции заполняются пробелами. Если числу требуется больше позиций, параметр игнорируется.

Спецификатор формата, как явствует из его названия, определяет формат вывода значения. Например, спецификатор C (Currency) означает, что параметр должен форматироваться как валюта с учетом национальных особенностей представления, а спецификатор Х (Hexadecimal) задает шестнадцатеричную форму представления выводимого значения.

В операторе 5 используются так называемые пользовательские шаблоны форматирования. Если приглядеться, в них нет ничего сложного: после двоеточия задается вид выводимого значения посимвольно, причем на месте каждого символа может стоять либо #, либо 0. Если указан знак #, на этом месте будет выведена цифра числа, если она не равна нулю. Если указан 0, будет выведена любая цифра, в том числе и 0.

Строки типа StringBuilder

Возможности, предоставляемые классом string, широки, однако требование неизменности его объектов может оказаться неудобным. В этом случае для работы со строками применяется класс StringBuilder, определенный в пространстве имен System.Text и позволяющий изменять значение своих экземпляров. О нем можно прочитать в учебнике [4].

Вопросы и задания для самостоятельной работы студента

  1. Перечислите способы описания массивов.
  2. Чем отличается хранение в памяти массивов из величин значимого и ссылочного типов?
  3. Является ли размерность частью описания массива?
  4. Может ли размерность массива описана переменной (а не константой)?
  5. Можно ли изменить размерность массива после выделения памяти под него?
  6. Какие виды массивов используются в С#?
  7. Что происходит, если количество инициализаторов массива не соответствует заявленной размерности?
  8. Что происходит при присваивании массивов?
  9. Опишите два-три метода сортировки массивов.
  10. Опишите основные методы и свойства класса System.Array
  11. Какие ограничения имеет оператор foreach по сравнению с оператором for?
  12. Что такое кодировка Unicode?
  13. Какие средства работы с отдельными символами предоставляет C#?
  14. Опишите основные методы и свойства класса string.
  15. Можно ли изменить длину строки после того, как память под нее была выделена?
  16. Какое основное ограничение имеет класс string?
  17. Какие существуют возможности форматирования строк?
  18. Перечислите спецификации формата.
  19. Опишите пользовательский формат вещественного числа с двумя ведущими нулями и тремя знаками после запятой.
  20. Изучите по справочной системе свойства и методы стандартного класса System.Array.
  21. Изучите по справочной системе свойства и методы стандартного класса System.String.
  22. Изучите по справочной системе свойства и методы стандартного класса System.Char.
  23. Изучите по справочной системе свойства и методы стандартного класса System.Text.StringBuilder.
  24. Изучите разделы стандарта C#, касающиеся массивов.
  25. Изучите разделы стандарта C#, касающиеся символов.
  26. Изучите разделы стандарта C#, касающиеся строк.

Лабораторные работы

Лабораторная работа 5. Одномерные массивы

В одномерном массиве, состоящем из n вещественных элементов, вычислить:

  • сумму отрицательных элементов массива;
  • произведение элементов массива, расположенных между максимальным и минимальным элементами.

Упорядочить элементы массива по возрастанию.

Лабораторная работа 6. Двумерные массивы

Дана целочисленная прямоугольная матрица. Определить:

  • количество строк, не содержащих ни одного нулевого элемента;
  • максимальное из чисел, встречающихся в заданной матрице более одного раза.

Лабораторная работа 7. Строки

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

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

Задание выполнить двумя способами: без использования элементов стандартных классов System.Array, System.Char и System.String и с их использованием.

Задание: упорядочить предложения по возрастанию количества содержащихся в них слов.

The simplest approach is to pre-allocate enough memory in your array to hold the longest string you expect to store in it.

An array is a region of storage; it has a size (number of elements) that’s fixed at the point where it’s created. You can’t expand an existing array — though you can use realloc to replace an array by a longer one.

A string is a sequence of characters up to and including a terminating '' null character.

An array may contain a string — and the length of the string is not the same thing as the size of the array.

Note that the length of a string does not include the terminating ''; `»hello» has a length of 5 and a size of 6.

Your declaration:

char a[] = "stackoverflow";

causes the compiler to create a with a size just big enough to hold the string used to initialize it. It’s equivalent to:

char a[14] = "stackoverflow"; /* 13 characters + 1 for the '' */

You can shorten the string stored in the array a, but it can’t hold a string with a length greater than 13:

strcpy(a, "STACK");

After this, the array a is still 14 characters in size (that never changes), but its contents are now:

"STACKverflow"

but anything after the first '' will probably be ignored.

If you anticipate storing a longer string, you can create it with a larger size:

char a[100] = "stackoverflow";
strcat(a, " is fun!";

This approach can be both wasteful (the array is likely to be bigger than it needs to be at the moment) and error-prone (it’s difficult to avoid overflowing the array bounds by storing an overly long string in it). But if you’re sufficiently careful, it can be good enough.

There are some string manipulation functions that make it easier to avoid overflowing your array. strncat is a safer version of strcat that lets you specify the size of the target array. There are also some non-standard functions like strlcpy and strlcat.

(strncpy, however, is not recommended.)

Other approaches are possible. For example, you can dynamically allocate memory space using malloc(), and reallocate it (replacing your array by a larger or smaller one) using realloc. You still have to be careful not to read or write past the end of an array — and you have to keep track of the array size yourself. It’s a more flexible approach, but it imposes more overhead, both on the performance of your program and on you as a programmer.

C (unlike C++, BTW) doesn’t have a simple mechanism for managing strings of varying lengths. The C string facilities, in some cases, are what higher-level mechanisms are built on top of.

The simplest approach is to pre-allocate enough memory in your array to hold the longest string you expect to store in it.

An array is a region of storage; it has a size (number of elements) that’s fixed at the point where it’s created. You can’t expand an existing array — though you can use realloc to replace an array by a longer one.

A string is a sequence of characters up to and including a terminating '' null character.

An array may contain a string — and the length of the string is not the same thing as the size of the array.

Note that the length of a string does not include the terminating ''; `»hello» has a length of 5 and a size of 6.

Your declaration:

char a[] = "stackoverflow";

causes the compiler to create a with a size just big enough to hold the string used to initialize it. It’s equivalent to:

char a[14] = "stackoverflow"; /* 13 characters + 1 for the '' */

You can shorten the string stored in the array a, but it can’t hold a string with a length greater than 13:

strcpy(a, "STACK");

After this, the array a is still 14 characters in size (that never changes), but its contents are now:

"STACKverflow"

but anything after the first '' will probably be ignored.

If you anticipate storing a longer string, you can create it with a larger size:

char a[100] = "stackoverflow";
strcat(a, " is fun!";

This approach can be both wasteful (the array is likely to be bigger than it needs to be at the moment) and error-prone (it’s difficult to avoid overflowing the array bounds by storing an overly long string in it). But if you’re sufficiently careful, it can be good enough.

There are some string manipulation functions that make it easier to avoid overflowing your array. strncat is a safer version of strcat that lets you specify the size of the target array. There are also some non-standard functions like strlcpy and strlcat.

(strncpy, however, is not recommended.)

Other approaches are possible. For example, you can dynamically allocate memory space using malloc(), and reallocate it (replacing your array by a larger or smaller one) using realloc. You still have to be careful not to read or write past the end of an array — and you have to keep track of the array size yourself. It’s a more flexible approach, but it imposes more overhead, both on the performance of your program and on you as a programmer.

C (unlike C++, BTW) doesn’t have a simple mechanism for managing strings of varying lengths. The C string facilities, in some cases, are what higher-level mechanisms are built on top of.

1 / 1 / 2

Регистрация: 16.12.2016

Сообщений: 149

1

Можно ли изменить размерность массива после выделения памяти под него?

12.02.2019, 23:39. Показов 1695. Ответов 5


Можно ли изменить размерность массива после выделения памяти под него?

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Эксперт .NET

16931 / 12508 / 3286

Регистрация: 17.09.2011

Сообщений: 20,745

12.02.2019, 23:47

2

Нет.



1



1 / 1 / 0

Регистрация: 11.02.2018

Сообщений: 32

13.02.2019, 00:06

3

m4ksc0r Но есть прекрасная альтернатива массиву называется List.



0



Эксперт .NET

6270 / 3898 / 1567

Регистрация: 09.05.2015

Сообщений: 9,188

13.02.2019, 00:27

4

Можно использовать Array.Resize, но это будет уже новый массив, а не измененный старый.



1



Администратор

Эксперт .NET

15254 / 12293 / 4905

Регистрация: 17.03.2014

Сообщений: 24,893

Записей в блоге: 1

13.02.2019, 00:31

5

m4ksc0r, размерность нет, размер одномерного да (Array.Resize)



1



2 / 1 / 1

Регистрация: 08.02.2019

Сообщений: 10

13.02.2019, 12:35

6

Массив — не может изменить свой размер. Можно только создать массив больше и скопировать туда все элементы.
Для того что бы увеличить размерность — нужно сделать то же самое



0



Немного о строках в Си, или несколько вариантов оптимизировать неоптимизируемое

Время прочтения
9 мин

Просмотры 161K

Хабра, привет!

Не так давно у со мной произошел довольно-таки интересный инцидент, в котором был замешан один из преподавателей одного колледжа информатики.

Разговор о программировании под Linux медленно перешел к тому, что этот человек стал утверждать, что сложность системного программирования на самом деле сильно преувеличена. Что язык Си прост как спичка, собственно как и ядро Linux (с его слов).

У меня был с собой ноутбук с Linux, на котором присутствовал джентльменский набор утилит для разработки на языке Си (gcc, vim, make, valgrind, gdb). Я уже не помню, какую цель мы тогда перед собой поставили, но через пару минут мой оппонент оказался за этим ноутбуком, полностью готовый решать задачу.

И буквально на первых же строках он допустил серьезную ошибку при аллоцировании памяти под… строку.

char *str = (char *)malloc(sizeof(char) * strlen(buffer));

buffer — стековая переменная, в которую заносились данные с клавиатуры.

Я думаю, определенно найдутся люди, которые спросят: «Разве что-то тут может быть не так?».
Поверьте, может.

А что именно — читайте по катом.

Немного теории — своеобразный ЛикБез.

Если знаете — листайте до следующего хэдера.

Строка в C — это массив символов, который по-хорошему всегда должен заканчиваться » — символом конца строки. Строки на стеке (статичные) объявляются вот так:

char str[n] = { 0 }; 

n — размер массива символов, то же, что и длина строки.

Присваивание { 0 } — «зануление» строки (опционально, объявлять можно и без него). Результат такой же, как у выполнения функций memset(str, 0, sizeof(str)) и bzero(str, sizeof(str)). Используется, чтобы в неинициализированных переменных не валялся мусор.

Так же на стеке можно сразу проинициализировать строку:

char buf[BUFSIZE] = "default buffer textn";

Помимо этого строку можно объявить указателем и выделить под нее память на куче (heap):

char *str = malloc(size);

size — количество байт, которые мы выделяем под строку. Такие строки называются динамическими (вследствие того, что нужный размер вычисляется динамически + выделенный размер памяти можно в любой момент увеличить с помощью функции realloc() ).

В случае со стековой переменной, для определения размера массива я использовал обозначение n, в случае с переменной на куче — я использовал обозначение size. И это прекрасно отражает истинную суть отличия объявления на стеке от объявление с аллоцированием памяти на куче, ведь n как правило используется тогда, когда говорят о количестве элементов. А size — это уже совсем другая история…

Думаю. пока хватит. Идем дальше.

Нам поможет valgrind

В своей предыдущей статье я также упоминал о нем. Valgrind (раз — вики-статья, два — небольшой how-to) — очень полезная программа, которая помогает программисту отслеживать утечки памяти и ошибки контекста — как раз те вещи, которые чаще всего всплывают при работе со строками.

Давайте рассмотрим небольшой листинг, в котором реализовано что-то похожее на упомянутую мной программу, и прогоним ее через valgrind:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define HELLO_STRING "Hello, Habr!n"

void main() {
  char *str = malloc(sizeof(char) * strlen(HELLO_STRING));
  strcpy(str, HELLO_STRING);
  printf("->t%s", str);
  free(str);
}

И, собственно, результат работы программы:

[indever@localhost public]$ gcc main.c 
[indever@localhost public]$ ./a.out 
->	Hello, Habr!

Пока ничего необычного. А теперь давайте запустим эту программу с valgrind!

[indever@localhost public]$ valgrind --tool=memcheck ./a.out 
==3892== Memcheck, a memory error detector
==3892== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==3892== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==3892== Command: ./a.out
==3892== 
==3892== Invalid write of size 2
==3892==    at 0x4005B4: main (in /home/indever/prg/C/public/a.out)
==3892==  Address 0x520004c is 12 bytes inside a block of size 13 alloc'd
==3892==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==3892==    by 0x400597: main (in /home/indever/prg/C/public/a.out)
==3892== 
==3892== Invalid read of size 1
==3892==    at 0x4C30BC4: strlen (vg_replace_strmem.c:454)
==3892==    by 0x4E89AD0: vfprintf (in /usr/lib64/libc-2.24.so)
==3892==    by 0x4E90718: printf (in /usr/lib64/libc-2.24.so)
==3892==    by 0x4005CF: main (in /home/indever/prg/C/public/a.out)
==3892==  Address 0x520004d is 0 bytes after a block of size 13 alloc'd
==3892==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==3892==    by 0x400597: main (in /home/indever/prg/C/public/a.out)
==3892== 
->	Hello, Habr!
==3892== 
==3892== HEAP SUMMARY:
==3892==     in use at exit: 0 bytes in 0 blocks
==3892==   total heap usage: 2 allocs, 2 frees, 1,037 bytes allocated
==3892== 
==3892== All heap blocks were freed -- no leaks are possible
==3892== 
==3892== For counts of detected and suppressed errors, rerun with: -v
==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)

==3892== All heap blocks were freed — no leaks are possible — утечек нет, и это радует. Но стоит опустить глаза чуть пониже (хотя, хочу заметить, это лишь итог, основная информация немного в другом месте):

==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
3 ошибки. В 2х контекстах. В такой простой программе. Как!?

Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — ». Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!n»), он будет проигнорирован.

Чуть выше результата исполнения программы, строки -> Hello, Habr! есть подробный отчет, что и где не понравилось нашему драгоценному valgrind. Предлагаю самостоятельно посмотреть эти строчки и сделать выводы.

Собственно, правильная версия программы будет выглядеть так:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define HELLO_STRING "Hello, Habr!n"

void main() {
  char *str = malloc(sizeof(char) * (strlen(HELLO_STRING) + 1));
  strcpy(str, HELLO_STRING);
  printf("->t%s", str);
  free(str);
}

Пропускаем через valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out 
->	Hello, Habr!
==3435== 
==3435== HEAP SUMMARY:
==3435==     in use at exit: 0 bytes in 0 blocks
==3435==   total heap usage: 2 allocs, 2 frees, 1,038 bytes allocated
==3435== 
==3435== All heap blocks were freed -- no leaks are possible
==3435== 
==3435== For counts of detected and suppressed errors, rerun with: -v
==3435== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Отлично. Ошибок нет, +1 байт выделяемой памяти помог решить проблему.

Что интересно, в большинстве случаев и первая и вторая программа будут работать одинаково, но если память, выделенная под строку, в которую не влез символ окончания, не была занулена, то функция printf(), при выводе такой строки, выведет и весь мусор после этой строки — будет выведено все, пока на пути printf() не встанет символ окончания строки.

Однако, знаете, (strlen(str) + 1) — такое себе решение. Перед нами встают 2 проблемы:

  1. А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку? Аргументы мы не поддерживаем.
  2. Внешний вид. Строка с объявлением переменной выглядит просто ужасно. Некоторые ребята к malloc еще и (char *) умудряются прикручивать, будто под плюсами пишут. В программе где регулярно требуется обрабатывать строки есть смысл найти более изящное решение.

Давайте придумаем такое решение, которое удовлетворит и нас, и valgrind.

snprintf()

int snprintf(char *str, size_t size, const char *format, ...); — функция — расширение sprintf, которая форматирует строку и записывает ее по указателю, переданному в качестве первого аргумента. От sprintf() она отличается тем, что в str не будет записано байт больше, чем указано в size.

Функция имеет одну интересную особенность — она в любом случае возвращает размер формируемой строки (без учета символа конца строки). Если строка пустая, то возвращается 0.

Одна из описанных мною проблем использования strlen связана с функциями sprintf() и snprintf(). Предположим, что нам надо что-то записать в строку str. Конечная строка содержит значения других переменных. Наша запись должна быть примерно такой:

char * str = /* тут аллоцируем память */;
sprintf(str, "Hello, %sn", "Habr!");

Встает вопрос: как определить, сколько памяти надо выделить под строку str?

char * str = malloc(sizeof(char) * (strlen(str, "Hello, %sn", "Habr!") + 1));

— не прокатит. Прототип функции strlen() выглядит так:

#include <string.h>
size_t strlen(const char *s);

const char *s не подразумевает, что передаваемая в s строка может быть строкой формата с переменным количеством аргументов.

Тут нам поможет то полезное свойство функции snprintf(), о котором я говорил выше. Давайте посмотрим на код следующей программы:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
/* Т.к. snprintf() не учитывает символ конца строки, прибавляем его размер к результату */
  size_t needed_mem = snprintf(NULL, 0, "Hello, %s!n", "Habr") + sizeof(''); 
  char *str = malloc(needed_mem);
  snprintf(str, needed_mem, "Hello, %s!n", "Habr");
  printf("->t%s", str);
  free(str);
}

Запускаем программу в valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out 
->	Hello, Habr!
==4132== 
==4132== HEAP SUMMARY:
==4132==     in use at exit: 0 bytes in 0 blocks
==4132==   total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated
==4132== 
==4132== All heap blocks were freed -- no leaks are possible
==4132== 
==4132== For counts of detected and suppressed errors, rerun with: -v
==4132== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[indever@localhost public]$ 

Отлично. Поддержка аргументов у нас есть. Благодаря тому, что мы в качестве второго аргумента в функцию snprintf() передаем ноль, запись по нулевому указателю никогда не приведет к Seagfault. Однако, несмотря на это функция все равно вернет необходимый под строку размер.

Но с другой стороны, нам пришлось завести дополнительную переменную, да и конструкция

size_t needed_mem = snprintf(NULL, 0, "Hello, %s!n", "Habr") + sizeof('');

выглядит еще хуже, чем в случае с strlen().

Вообще, + sizeof(») можно убрать, если в конце строки формата явно указать » (size_t needed_mem = snprintf(NULL, 0, «Hello, %s!n», «Habr»);), но это возможно отнюдь не всегда (в зависимости от механизма обработки строк мы можем выделить лишний байт).

Надо что-то сделать. Я немного подумал и решил, что сейчас настал час воззвать к мудрости древних. Опишем макрофункцию, которая будет вызывать snprintf() с нулевым указателем в качестве первого аргумента, и нулем, в качестве второго. Да и про конец строки не забудем!

#define strsize(args...) snprintf(NULL, 0, args) + sizeof('')

Да, возможно, для кого-то будет новостью, но макросы в си поддерживают переменное количество аргументов, и троеточие говорит препроцессору о том, что указанному аргументу макрофункции (в нашем случае это args) соответствует несколько реальных аргументов.

Проверим наше решение на практике:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define strsize(args...) snprintf(NULL, 0, args) + sizeof('')

void main() {
  char *str = malloc(strsize("Hello, %sn", "Habr!"));
  sprintf(str, "Hello, %sn", "Habr!");
  printf("->t%s", str);
  free(str);
}

Запускаем с valgrund:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out 
->	Hello, Habr!
==6432== 
==6432== HEAP SUMMARY:
==6432==     in use at exit: 0 bytes in 0 blocks
==6432==   total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated
==6432== 
==6432== All heap blocks were freed -- no leaks are possible
==6432== 
==6432== For counts of detected and suppressed errors, rerun with: -v
==6432== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Да, ошибок нет. Все корректно. И valgrind доволен, и программист наконец может пойти поспать.

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

Речь идет о функции asprintf:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <stdio.h>

int asprintf(char **strp, const char *fmt, ...);

В качестве первого аргумента она принимает указатель на строку (**strp) и аллоцирует память по разыменованному указателю.

Наша программа, написанная с использованием asprintf() будет выглядеть так:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
  char *str;
  asprintf(&str, "Hello, %s!n", "Habr");
  printf("->t%s", str);
  free(str);
}

И, собственно, в valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out 
->	Hello, Habr!
==6674== 
==6674== HEAP SUMMARY:
==6674==     in use at exit: 0 bytes in 0 blocks
==6674==   total heap usage: 3 allocs, 3 frees, 1,138 bytes allocated
==6674== 
==6674== All heap blocks were freed -- no leaks are possible
==6674== 
==6674== For counts of detected and suppressed errors, rerun with: -v
==6674== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Все отлично, но, как видите, памяти всего было выделено больше, да и alloc’ов теперь три, а не два. На слабых встраиваемых системах использование это функции нежелательно.
К тому же, если мы напишем в консоли man asprintf, то увидим:

CONFORMING TO
       These functions are GNU extensions, not in C or POSIX.  They are also available under *BSD.  The FreeBSD implementation sets strp to  NULL on error.

Отсюда ясно, что данная функция доступна только в исходниках GNU.

Заключение

В заключение я хочу сказать, что работа со строками в C — это очень сложная тема, которая имеет ряд нюансов. Например, для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc() — calloc забивает выделяемую память нулями. Ну или после выделения памяти использовать функцию memset(). Иначе мусор, который изначально лежал на выделяемом участке памяти, может вызвать вопросы при дебаге, а иногда и при работе со строкой.

Больше половины моих знакомых си-программистов (большинство из них — начинающие), решивших по моей просьбе задачу с выделением памяти под строки, сделали это так, что в конечном итоге это привело к ошибкам контекста. В одном случае — даже к утечке памяти (ну, забыл человек сделать free(str), с кем не бывает). Собственно говоря, это и сподвигло меня на создание сего творения, которое вы только что прочитали.

Я надеюсь, кому-то эта статья будет полезной. К чему я это все городил — никакой язык не бывает прост. Везде есть свои тонкости. И чем больше тонкостей языка вы знаете, тем лучше ваш код.

Я верю, что после прочтения этой статьи ваш код станет чуточку лучше :)
Удачи, Хабр!

Я обьявил строку длинной 42 символа но l3 равна 47 Что пошло не так?

Нет, вы нигде не объявляли строку длиной 42 символа. В этом фрагменте кода

char * s3 = new char[42];
l3 = strlen(s3);

вы объявили одну переменную: переменную s3, имеющую тип указателя char *. В этом фрагменте кода вообще нигде не используется строка.

Строка — это последовательность символов, ограниченная завершающем нулем, то есть символом ''.

Указатель s3 инициализирован адресом первого байта динамически выделенной памяти размером в 42 байта. Однако сама динамически выделенная память никак не инициализирована, поэтому к ней бессмысленно применять функцию strlen.

Вы могли бы инициализировать выделенную память нулями, например, следующим образом

char * s3 = new char[42]();

и тогда бы эта память в самом своем начале содержала бы пустую строку, то есть строку, которая состоит из одного символа ''.

В этом случае результат применения функции strlen будет равен 0.

Из описания функции strlen в стандарте C

The strlen function returns the number of characters that precede the
terminating null character.

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

int n = 42;
char *str = new char[n];

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

char str[42];

то оператор sizeof( str ) вернул бы вам значение 42. Этот оператор возвращает размер памяти, занимаемой массивом, но тем, не менее, он не пригоден для определении длины строки, которая может храниться в этом массиве.

Например,

char str[42] = "Hello";

std::cout << sizeof( str ) << std::endl;
std::cout << std::strlen( str ) << std::endl;

Первое из этих предложений выведет 42, в то время как второе предложение — 5.

Оператор sizeof бессмысленно применять к указателю на выделенную динамически память, как, например,

char *str = new char[42];
size_t n = sizeof( str );

потому что он вернет не размер выделенной динамической памяти, а размер самого указателя, которой в зависимости от используемой системы может быть равным, например, 4 или 8.

Теги: Си память, malloc, calloc, realloc, free, Ошибки выделения памяти, Висячие указатели, Динамические массивы, Многомерные динамические массивы.

malloc

В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции.
С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать
самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется
выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

void * malloc(size_t size);

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL.
Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	p = (int*) malloc(100);

	free(p);
}

После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем
его квадратами чисел по порядку. После этого выводим и удаляем массив.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	const int maxNumber = 100;
	int *p = NULL;
	unsigned i, size;

	do {
		printf("Enter number from 0 to %d: ", maxNumber);
		scanf("%d", &size);
		if (size < maxNumber) {
			break;
		}
	} while (1);

	p = (int*) malloc(size * sizeof(int));

	for (i = 0; i < size; i++) {
		p[i] = i*i;
	}

	for (i = 0; i < size; i++) {
		printf("%d ", p[i]);
	}

	_getch();
	free(p);
}

Разбираем код

p = (int*) malloc(size * sizeof(int));

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Выделение памяти.

Выделение памяти.

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может
им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc «выделяет память», то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё.
Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим.
Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она
продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся.
Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит «создание» или «удаление» памяти. На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Теперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти,
начиная с которого она может им пользоваться.
Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

Очевидно, что информация о размере выделенного участка должна где-то храниться. Есть несколько решения этой проблемы.

  • 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
  • 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free «подсматривает», сколько памяти необходимо удалить.

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

Работа с двумерными и многомерными массивами

Для динамического создания двумерного массива сначала необходимо создать массив указателей, после чего каждому из элементов этого массива
присвоить адрес нового массива.

Для удаления массива необходимо повторить операцию в обратном порядке — удалить сначала подмассивы, а потом и сам массив указателей.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define COL_NUM 10
#define ROW_NUM 10

void main() {
	float **p = NULL;
	unsigned i;

	p = (float**) malloc(ROW_NUM * sizeof(float*));
	for (i = 0; i < ROW_NUM; i++) {
		p[i] = (float*) malloc(COL_NUM * sizeof(float));
	}

	//Здесь какой-то важный код
	for (i = 0; i < ROW_NUM; i++) {
		free(p[i]);
	}
	free(p);
}

Сначала мы создаём массив указателей, а после этого каждому элементу этого массива присваиваем адрес вновь созданного массива. Это значит, что можно

  • 1. Создавать массивы «неправильной формы», то есть массив строк, каждая из которых имеет свой размер.
  • 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим «треугольный» массив и заполним его значениями

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int **A;
	int i, j;
	A = (int**) malloc(SIZE * sizeof(int*));

	for (i = 0; i < SIZE; i++) {
		A[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			A[i][j] = i * j;
		}
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			printf("%d ", A[i][j]);
		}
		printf("n");
	}

	for (i = SIZE-1; i > 0; i--) {
		free(A[i]);
	}
	free(A);
	
	_getch();
}

Чтобы создать трёхмерный массив, по аналогии, необходимо сначала определить указатель на указатель на указатель, после чего выделить память под массив указателей на указатель,
после чего проинициализировать каждый из массивов и т.д.

calloc

Функция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

void* calloc(size_t num, size_t size);

realloc

Ещё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый
указатель и новый размер памяти в байтах:

void* realloc(void* ptr, size_t size)

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает,
где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места.
Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	//Массив указателей на слова
	char **words;
	//Строка, которая используется для считывания введённого пользователем слова
	char buffer[128];
	//Счётчик слов
	unsigned wordCounter = 0;
	//Длина введённого слова
	unsigned length;
	//Размер массива слов. Для уменьшения издержек на выделение памяти 
	//каждый раз будем увеличивать массив слов не на одно значение, а на
	//SIZE_INCREMENT слов
	unsigned size = SIZE_INCREMENT;
	int i;

	//Выделяем память под массив из size указателей
	words = (char**) malloc(size*sizeof(char*));
	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		//Функция strcmp возвращает 0, если две строки равны
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}
		//Определяем длину слова
		length = strlen(buffer);
		//В том случае, если введено слов больше, чем длина массива, то
		//увеличиваем массив слов
		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			words = (char**) realloc(words, size*sizeof(char*));
		}
		//Выделяем память непосредственно под слово
		//на 1 байт больше, так как необходимо хранить терминальный символ
		words[wordCounter] = (char*) malloc(length + 1);
		//Копируем слово из буффера по адресу, который 
		//хранится в массиве указателей на слова
		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%sn", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.

Ошибки при выделении памяти

1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить
указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc.
Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	char **words;
	char buffer[128];
	unsigned wordCounter = 0;
	unsigned length;
	unsigned size = SIZE_INCREMENT;
	int i;

	if (!(words = (char**) malloc(size*sizeof(char*)))) {
		printf("Error: can't allocate memory");
		_getch();
		exit(1);
	}

	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}

		length = strlen(buffer);

		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			if (!(words = (char**) realloc(words, size*sizeof(char*)))) {
				printf("Error: can't reallocate memory");
				_getch();
				exit(2);
			}
		}

		if (!(words[wordCounter] = (char*)malloc(length + 1))) {
			printf("Error: can't allocate memory");
			_getch();
			exit(3);
		}

		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%sn", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

Хотелось бы добавить, что ошибки выделения памяти могут случиться, и просто выходить из приложения и выкидывать ошибку плохо. Решение
зависит от ситуации. Например, если не хватает памяти, то можно подождать некоторое время и после этого опять попытаться выделить память, или использовать
для временного хранения файл и переместить туда часть объектов. Или выполнить очистку, сократив используемую память и удалив ненужные объекты.

2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте — его размер. При
удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	if (!(p = (int*) malloc(100 * sizeof(int)))) {
		printf("Error");
		exit(1);
	}
	//Изменили указатель
	p++;
	//Теперь free не может найти метаданные об объекте
	free(p);
	//На некоторых компиляторах ошибки не будет
	_getch();
}

Таким образом, если указатель хранит адрес, то его не нужно изменять. Для работы лучше создать дополнительную переменную указатель,
с которой работать дальше.

3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так
называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес
области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p = NULL;
	int i;

	p = (int*) malloc(SIZE * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		p[i] = i;
	}
	free(p);
	for (i = 0; i < SIZE; i++) {
		printf("%i ", p[i]);
	}
	_getch();
}

Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.

Если же мы напишем

free(p);
p = NULL;

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

4. Освобождение освобождённой памяти. Пример

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	b = (int*) malloc(sizeof(int));
	free(a);
	free(b);
	_getch();
}

Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан
кому-нибудь для использования. Решение здесь такое же как и раньше — обнулить указатель явно после удаления:

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	a = NULL;
	b = (int*) malloc(sizeof(int));
	free(a);//вызов free(NULL) ничего не делает
	free(b);
	b = NULL;
	_getch();
}

5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя
p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

	for (i = 0; i < SIZE; i++) {
		p1[i] = i;
	}
	p2 = realloc(p1, SIZE * 5000 * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p1[i]);
	}
	printf("n");
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p2[i]);
	}
	_getch();
}

Рассмотрим код ещё раз.

	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

Теперь оба указателя хранят один адрес.

	p2 = realloc(p1, SIZE * 5000 * sizeof(int));

А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента,
но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить
много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же
потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае).
Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое
p1. В этом случае поведение не определено.

Два указателя на одну область памяти это вообще-то не ошибка. Бывают ситуации, когда без них не обойтись. Но это
очередное минное поле для программиста.

Различные аргументы realloc и malloc.

При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться,
но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) :) Понимайте это, как хотите.

Примеры

1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто
эти измерения из-за погрешности «плавают» или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться
от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном
случае — это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n
чисел меньше.

Скользящее среднее.

Скользящее среднее.

Пусть есть ряд
1, 4, 4, 6, 7, 8, 9, 11, 12, 11, 15
Тогда если период среднего будет 3, то мы получим ряд
(1+4+4)/3, (4+4+6)/3, (4+6+7)/3, (6+7+8)/3, (7+8+9)/3, (8+9+11)/3, (9+11+12)/3, (11+12+11)/3, (12+11+15)/3
Видно, что сумма находится в «окне», которое скользит по ряду. Вместо того, чтобы каждый раз в цикле находить сумму, можно найти её для
первого периода, а затем вычитать из суммы крайнее левое значение предыдущего периода и прибавлять крайнее правое значение следующего.
Будем запрашивать у пользователя числа и период, а затем создадим новый массив и заполним его средними значениями.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_INCREMENT 20

void main() {
	//Считанные числа
	float *numbers = NULL;
	//Найденные значения
	float *mean = NULL;
	float readNext;
	//Максимальный размер массива чисел
	unsigned maxSize = MAX_INCREMENT;
	//Количество введённых чисел
	unsigned curSize = 0;
	//Строка для считывания действия
	char next[2];
	//Шаг
	unsigned delta;
	//float переменная для хранения шага
	float realDelta;
	unsigned i, j;
	//Сумма чисел
	float sum;

	numbers = (float*) malloc(maxSize * sizeof(float));
	do {
		//Пока пользователь вводит строку, которая начинается с y или Y,
		//то продолжаем считывать числа
		printf("next? [y/n]: ");
		scanf("%1s", next);
		if (next[0] == 'y' || next[0] == 'Y') {
			printf("%d. ", curSize);
			scanf("%f", &readNext);
			if (curSize >= maxSize) {
				maxSize += MAX_INCREMENT;
				numbers = (float*) realloc(numbers, maxSize * sizeof(float));
			}
			numbers[curSize] = readNext;
			curSize++;
		} else {
			break;
		}
	} while(1);

	//Считываем период, он должен быть меньше, чем
	//количество элементов в массиве. Если оно равно,
	//то результатом станет среднее арифметическое всех введённых чисел
	do {
		printf("enter delta (>=%d): ", curSize);
		scanf("%d", &delta);
		if (delta <= curSize) {
			break;
		}
	} while(1);
	realDelta = (float) delta;

	//Находим среднее для первого периода
	mean = (float*) malloc(curSize * sizeof(float));
	sum = 0;
	for (i = 0; i < delta; i++) {
		sum += numbers[i];
	}

	//Среднее для всех остальных
	mean[0] = sum / delta;
	for (i = delta, j = 1; i < curSize; i++, j++) {
		sum = sum - numbers[j-1] + numbers[i];		
		mean[j] = sum / realDelta;
	}

	//Выводим. Чисел в массиве mean меньше на delta
	curSize = curSize - delta + 1;
	for (i = 0; i < curSize; i++) {
		printf("%.3f ", mean[i]);
	}

	free(numbers);
	free(mean);
	_getch();
}

Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.

2. Сортировка двумерного массива. Самый простой способ сортировки — перевести двумерный массив MxN в одномерный размером M*N,
после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место
под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента
можно найти следующим образом:

j = k / N;
i = k - j*M;

Заполним массив случайными числами и отсортируем

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_SIZE_X 20
#define MAX_SIZE_Y 20

void main() {
	int **mrx = NULL;
	int tmp;
	unsigned i, j, ip, jp, k, sizeX, sizeY, flag;

	printf("cols: ");
	scanf("%d", &sizeY);
	printf("rows: ");
	scanf("%d", &sizeX);

	//Если введённый размер больше MAX_SIZE_?, то присваиваем
	//значение MAX_SIZE_?
	sizeX = sizeX <= MAX_SIZE_X? sizeX: MAX_SIZE_X;
	sizeY = sizeY <= MAX_SIZE_Y? sizeY: MAX_SIZE_Y;

	//Задаём начальное значение для генератора псевдослучайных чисел
	srand(time(NULL));
	//Выделяем память под массив указателей
	mrx = (int**) malloc(sizeX * sizeof(int*));
	for (i = 0; i < sizeX; i++) {
		//Выделяем память под строку и сразу же заполняем элементы
		//случайными значениями
		mrx[i] = (int*) malloc(sizeY * sizeof(int));
		for (j = 0; j < sizeY; j++) {
			mrx[i][j] = rand();
		}
	}

	//Выводим массив
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		printf("n");
	}

	//Сортируем пузырьком, обходя все sizeX*sizeY элементы
	do {
		flag = 0;
		for (k = 1; k < sizeX * sizeY; k++) {
			//Вычисляем индексы текущего элемента
			j = k / sizeX;
			i = k - j*sizeX;
			//Вычисляем индексы предыдущего элемента
			jp = (k-1) / sizeX;
			ip = (k-1) - jp*sizeX;
			if (mrx[i][j] > mrx[ip][jp]) {
                tmp = mrx[i][j];
                mrx[i][j] = mrx[ip][jp];
                mrx[ip][jp] = tmp;
				flag = 1;
            }
		}
	} while(flag);

	printf("-----------------------n");
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		free(mrx[i]);
		printf("n");
	}
	free(mrx);

	_getch();
}

3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_BINOM_HEIGHT 20

void main() {
	int** binom = NULL;
	unsigned height;
	unsigned i, j;

	printf("Enter height: ");
	scanf("%d", &height);
	height = height <= MAX_BINOM_HEIGHT? height: MAX_BINOM_HEIGHT;

	binom = (int**) malloc(height * sizeof(int*));
	for (i = 0; i < height; i++) {
		binom[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	binom[0][0] = 1;
	for (i = 1; i < height; i++) {
		binom[i][0] = binom[i][i] = 1;
		for (j = i - 1; j > 0; j--) {
			binom[i][j] = binom[i-1][j-1] + binom[i-1][j];
		}
	}

	for (i = 0; i < height; i++) {
		for (j = 0; j <= i; j++) {
			printf("%4d ", binom[i][j]);
		}
		free(binom[i]);
		printf("n");
	}
	free(binom);

	_getch();
}

Если Вы желаете изучать этот материал с преподавателем, советую обратиться к
репетитору по информатике

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email

Структура программы на си

 

Очень часто возникают задачи обработки массивов данных, размерность которых заранее неизвестна. В этом случае возможно использование одного из двух подходов:

  • выделение памяти под статический массив, содержащий максимально возможное число элементов, однако в этом случае память расходуется не рационально;
  • динамическое выделение памяти для хранение массива данных.

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

int *p; // указатель на тип int

Начальный адрес статического массива определяется компилятором в момент его объявления и не может быть изменен.

Для динамического массива начальный адрес присваивается объявленному указателю на массив в процессе выполнения программы.

Стандартные функции динамического выделения памяти

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

Функции динамического распределения памяти:

void* malloc(РазмерМассиваВБайтах);
void* calloc(ЧислоЭлементов, РазмерЭлементаВБайтах);

Для использования функций динамического распределения памяти необходимо подключение библиотеки <malloc.h>:

Поскольку обе представленные функции в качестве возвращаемого значения имеют указатель на пустой тип void, требуется явное приведение типа возвращаемого значения.

Для определения размера массива в байтах, используемого в качестве аргумента функции malloc() требуется количество элементов умножить на размер одного элемента. Поскольку элементами массива могут быть как данные простых типов, так и составных типов (например, структуры), для точного определения размера элемента в общем случае рекомендуется использование функции

которая определяет количество байт, занимаемое элементом указанного типа.

Память, динамически выделенная с использованием функций calloc(), malloc(), может быть освобождена с использованием функции

«Правилом хорошего тона» в программировании является освобождение динамически выделенной памяти в случае отсутствия ее дальнейшего использования. Однако если динамически выделенная память не освобождается явным образом, она будет освобождена по завершении выполнения программы.

Динамическое выделение памяти для одномерных массивов

Форма обращения к элементам массива с помощью указателей имеет следующий вид:

int a[10], *p; // описываем статический массив и указатель
int b;
p = a; // присваиваем указателю начальный адрес массива
… // ввод элементов массива
b = *p; // b = a[0];
b = *(p+i) // b = a[i];

Пример на Си: Организация динамического одномерного массива и ввод его элементов.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
int main()
{
  int *a;  // указатель на массив
  int i, n;
  system(«chcp 1251»);
  system(«cls»);
  printf(«Введите размер массива: «);
  scanf(«%d», &n);
  // Выделение памяти
  a = (int*)malloc(n * sizeof(int));
  // Ввод элементов массива
  for (i = 0; i<n; i++)
  {
    printf(«a[%d] = «, i);
    scanf(«%d», &a[i]);
  }
  // Вывод элементов массива
  for (i = 0; i<n; i++)
    printf(«%d «, a[i]);
  free(a);
  getchar();   getchar();
  return 0;
}

Результат выполнения программы:
Динамическое выделение памяти

Динамическое выделение памяти для двумерных массивов

Пусть требуется разместить в динамической памяти матрицу, содержащую n строк и m столбцов. Двумерная матрица будет располагаться в оперативной памяти в форме ленты, состоящей из элементов строк. При этом индекс любого элемента двумерной матрицы можно получить по формуле

index = i*m+j;

где i — номер текущей строки; j — номер текущего столбца.

Рассмотрим матрицу 3×4 (см. рис.)
Матрица 3х4
Индекс выделенного элемента определится как

index = 1*4+2=6

 
Объем памяти, требуемый для размещения двумерного массива, определится как

n·m·(размер элемента)

Однако поскольку при таком объявлении компилятору явно не указывается количество элементов в строке и столбце двумерного массива, традиционное обращение к элементу путем указания индекса строки и индекса столбца является некорректным:

a[i][j] — некорректно.

Правильное обращение к элементу с использованием указателя будет выглядеть как

*(p+i*m+j),
где

  • p — указатель на массив,
  • m — количество столбцов,
  • i — индекс строки,
  • j — индекс столбца.

Пример на Си Ввод и вывод значений динамического двумерного массива

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
int main()
{
  int *a;  // указатель на массив
  int i, j, n, m;
  system(«chcp 1251»);
  system(«cls»);
  printf(«Введите количество строк: «);
  scanf(«%d», &n);
  printf(«Введите количество столбцов: «);
  scanf(«%d», &m);
  // Выделение памяти
  a = (int*)malloc(n*m * sizeof(int));
  // Ввод элементов массива
  for (i = 0; i<n; i++)  // цикл по строкам
  {
    for (j = 0; j<m; j++)  // цикл по столбцам
    {
      printf(«a[%d][%d] = «, i, j);
      scanf(«%d», (a + i*m + j));
    }
  }
  // Вывод элементов массива
  for (i = 0; i<n; i++)  // цикл по строкам
  {
    for (j = 0; j<m; j++)  // цикл по столбцам
    {
      printf(«%5d «, *(a + i*m + j)); // 5 знакомест под элемент массива
    }
    printf(«n»);
  }
  free(a);
  getchar();   getchar();
  return 0;
}

Результат выполнения
Ввод и вывод значений динамического двумерного массива

Возможен также другой способ динамического выделения памяти под двумерный массив — с использованием массива указателей. Для этого необходимо:

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

Динамическое выделение памяти под двумерный массив

Графически такой способ выделения памяти можно представить следующим образом.
Выделение памяти под двумерный массив - графическое представление
При таком способе выделения памяти компилятору явно указано количество строк и количество столбцов в массиве.
Пример на Си

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
int main()
{
  int **a;  // указатель на указатель на строку элементов
  int i, j, n, m;
  system(«chcp 1251»);
  system(«cls»);
  printf(«Введите количество строк: «);
  scanf(«%d», &n);
  printf(«Введите количество столбцов: «);
  scanf(«%d», &m);
  // Выделение памяти под указатели на строки
  a = (int**)malloc(n * sizeof(int*));
  // Ввод элементов массива
  for (i = 0; i<n; i++)  // цикл по строкам
  {
    // Выделение памяти под хранение строк
    a[i] = (int*)malloc(m * sizeof(int));
    for (j = 0; j<m; j++)  // цикл по столбцам
    {
      printf(«a[%d][%d] = «, i, j);
      scanf(«%d», &a[i][j]);
    }
  }
  // Вывод элементов массива
  for (i = 0; i < n; i++)  // цикл по строкам
  {
    for (j = 0; j < m; j++)  // цикл по столбцам
    {
      printf(«%5d «, a[i][j]); // 5 знакомест под элемент массива
    }
    printf(«n»);
  }
  // Очистка памяти
  for (i = 0; i < n; i++)  // цикл по строкам
    free(a[i]);   // освобождение памяти под строку
  free(a);
  getchar();   getchar();
  return 0;
}

Результат выполнения программы аналогичен предыдущему случаю.

С помощью динамического выделения памяти под указатели строк можно размещать свободные массивы. Свободным называется двухмерный массив (матрица), размер строк которого может быть различным. Преимущество использования свободного массива заключается в том, что не требуется отводить память компьютера с запасом для размещения строки максимально возможной длины. Фактически свободный массив представляет собой одномерный массив указателей на одномерные массивы данных.

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

Пример на Си: Свободный массив

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
  int **a;
  int i, j, n, *m;
  system(«chcp 1251»);
  system(«cls»);
  printf(«Введите количество строк: «);
  scanf(«%d», &n);
  a = (int**)malloc(n * sizeof(int*));
  m = (int*)malloc(n * sizeof(int)); // массив кол-ва элеменов в строках массива a
                     // Ввод элементов массива
  for (i = 0; i<n; i++) 
  {
    printf(«Введите количество столбцов строки %d: «, i);
    scanf(«%d», &m[i]);
    a[i] = (int*)malloc(m[i] * sizeof(int));
    for (j = 0; j<m[i]; j++) {
      printf(«a[%d][%d]= «, i, j);
      scanf(«%d», &a[i][j]);
    }
  }
  // Вывод элементов массива
  for (i = 0; i<n; i++) 
  {
    for (j = 0; j<m[i]; j++) 
    {
      printf(«%3d «, a[i][j]);
    }
    printf(«n»);
  }
  // Освобождение памяти
  for (i = 0; i < n; i++)
  {
    free(a[i]);
  }
  free(a);
  free(m);
  getchar(); getchar();
  return 0;
}

Результат выполнения
Свободный массив

Перераспределение памяти

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

  • Выделить блок памяти размерности n+1 (на 1 больше текущего размера массива)
  • Скопировать все значения, хранящиеся в массиве во вновь выделенную область памяти
  • Освободить память, выделенную ранее для хранения массива
  • Переместить указатель начала массива на начало вновь выделенной области памяти
  • Дополнить массив последним введенным значением

Все перечисленные выше действия (кроме последнего) выполняет функция

void* realloc (void* ptr, size_t size);

  • ptr — указатель на блок ранее выделенной памяти функциями malloc(), calloc() или realloc() для перемещения в новое место. Если этот параметр равен NULL, то выделяется новый блок, и функция возвращает на него указатель.
  • size — новый размер, в байтах, выделяемого блока памяти. Если size = 0, ранее выделенная память освобождается и функция возвращает нулевой указатель, ptr устанавливается в NULL.

Размер блока памяти, на который ссылается параметр ptr изменяется на size байтов. Блок памяти может уменьшаться или увеличиваться в размере. Содержимое блока памяти сохраняется даже если новый блок имеет меньший размер, чем старый. Но отбрасываются те данные, которые выходят за рамки нового блока. Если новый блок памяти больше старого, то содержимое вновь выделенной памяти будет неопределенным.

 
Пример на Си Выделить память для ввода массива целых чисел. После ввода каждого значения задавать вопрос о вводе следующего значения.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
int main()
{
  int *a = NULL, i = 0, elem;
  char c;
  do {
    printf(«a[%d]= «, i);
    scanf(«%d», &elem);
    a = (int*)realloc(a, (i + 1) * sizeof(int));
    a[i] = elem;
    i++;
    getchar();
    printf(«Next (y/n)? «);
    c = getchar();
  } while (c == ‘y’);
  for (int j = 0; j < i; j++)
    printf(«%d «, a[j]);
  if (i>2) i -= 2;
  printf(«n»);
  a = (int*)realloc(a, i * sizeof(int)); // уменьшение размера массива на 2
  for (int j = 0; j < i; j++)
    printf(«%d «, a[j]);
  getchar(); getchar();
  return 0;
}

Результат выполнения
realloc()

Назад: Язык Си

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Можно ли вернуть деньги если перевел на карту ошибочно тинькофф
  • Можно ли вернуть деньги если перевел на карту ошибочно рнкб
  • Можно ли избежать ошибок опираясь на чужой жизненный опыт
  • Можно ли вернуть деньги если перевел на карту ошибочно киви
  • Можно ли вернуть деньги если перевел на карту ошибочно беларусбанк

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии