В предыдущей части я рассказал вам об основах указателей в С/С++. Поскольку работа с памятью немного отличается в С++(там она упрощена), эта статья будет ориентирована именно на С. Чистый С сложнее, но если его понять, С++ покажется легким. В конце статьи, я коротко расскажу о работе с памятью в С++. И так сегодня мы узнаем:
- Что общего у массивов и указателей;
- Как выделять память;
- Что из себя представляют строки;
Кому интересно, прошу под спойлер!
И так. Для начала узнаем, что же такое массив. Вот небольшой код:
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> void main() { int array[3]; //массив из трех элементов array[0] = 0; array[1] = 1; array[2] = 2; printf("%p, %irn", array, *array); } |
Странный код, правда? Создается массив. Потом мы его пытаемся вывести на экран как указатель(wtf?). Мало того, получаем значение массива, как будто он указатель. Бред? А вот и нет! Массив и есть указатель. Почти. Дело в том, что написав:
1 |
int array[3]; |
Мы скомандовали компьютеру: «выделить последовательно(один блок за одним) память, для трех элементов типа int, указатель на первый элемент поместить в переменную array«. Теперь попробуем наоборот, работать с указателем как с массивом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> #include <stdlib.h> //для работы с функциями malloc и free void main() { int* pointer = NULL; //создаем указатель на тип int pointer = (int*) malloc(sizeof(int) * 3); //выделяем память для 3 элементов типа int if (pointer) { pointer[0] = 0; //работаем с указателем как с массивом pointer[1] = 1; pointer[2] = 2; int i = 0; for (i = 0; i < 3; ++i) printf("%irn", pointer[i]); } free(pointer); //освобождаем память } |
С функциями malloc и free мы разберемся чуть пожже, просто запомните что они работают с памятью(выделяет malloc, free — освобождает). Поскольку указатель указывает(да да, я знаю) на область памяти, то этой области не обязательно должна быть присвоена переменная. То есть, можно как в нашем примере выделить память для указателя, и работать с ней.
Примечание: переменные и массивы при объявлении получают память в стеке, при выделении памяти вручную, блоки создаются в куче. Гугл ваш друг.
Как вы уже знаете, переменная массива тоже указатель(на первый элемент), так что нам мешает создать указатель, выделить блок памяти и присвоить ему значение первого элемента? А ничего. Функция malloc выделает указанный размер памяти в байтах и возвращает указатель на его первый элемент. Поскольку она универсальная, то возвращает указатель типа void*, его нужно явно преобразовать в тип int*. Для того, что бы вычислить необходимое количество байт я использовал оператор sizeof, он возвращает размер переменной или типа. Функция free освобождает память, выделенную под указатель.
Примечание: sizeof вычисляется на этапе компиляции, и если ее применять в указателям(то есть пытаться вычислить динамическую память) она всегда будет возвращать размер указателя.
Важно: всегда освобождайте выделенную память! Думаю все слышали о такой вещи, как «утечка памяти»? Это и есть блоки памяти, которую не освободили. То есть, создался указатель, ему выделился блок памяти(назовем его блок1). Потом указателю выделили другой блок памяти(блок2). В результате этого, блок1 попрежнему воспринимается операционной системой как используемый, но доступа к нему уже нет(указателя нет, ничего на него не указывает). Освободится он лишь в случае закрытия программы или перезагрузки компьютера. Поэтому следуйте правилу: «Использовал указатель? — Освободи память! Присвой указателю NULL!».
Все очень запутанно, поэтому я попробую графически вам показать. Память состоит из бит. 8 бит = 1 байт. Это думаю все знают. Операционная система работает с памятью, как с блоками байт. Не будем заморачиваться размерами, а представим себе что int и int* занимают одинаковый размер блока в памяти.
Примечание: на всех компьютерах реализация размеров int разная. То есть для x86 — один размер, для x64 — другой.
Вот иллюстрация нашей памяти:
Создадим переменную, типа int:
1 |
int var1 = 0; |
Компьютер выделит память для нашей переменной, и мы получим что-то вроде:
Теперь создадим указатель:
1 |
int* pointer1 = NULL; |
Получим что-то вроде:
Теперь сделаем так:
1 |
pointer1 = &var1; |
Как вы уже догадались в указатель pointer1 помещается адрес переменной var1. То есть получается вот так:
Что же происходит с массивами? Объявим массив:
1 |
int array1[3]; |
В результате компьютер выделит память размером в 3 блока. Указатель на первый элемент поместит в переменную array1. Абстрактно получится так:
Блоки памяти для элементов массива выделяются последовательно, указатель на первый элемент помещается в array1. По сути, когда вы пишите array1[2] компьютер это расценивает как «переместится на две позиции в памяти из указателя array1«. Вот вам пример:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> void main() { int array1[2]; array1[0] = 0; array1[1] = 1; int* pointer1 = NULL; pointer1 = array1; ++pointer1; printf("%irn", *pointer1); } |
В результате получим:
А можно и так:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> void main() { int array1[2]; array1[0] = 0; array1[1] = 1; int* pointer1 = NULL; pointer1 = array1; printf("%irn", pointer1[1]); } |
Получили тоже самое. Выходит, что с указателем и с массивом можно работать почти одинаково, но нужно не забывать про осторожность. Указатель можно сдвинуть через-чур и получить ошибки в программе. Например, если под указатель выделили 4 байта, а сдвинулись на 5(5 раз сделать ++%pointer_name%) то теперь указатель указывает на блок памяти, в котором может быть системная/важная информация, вовсе не принадлежащая нашей программе.Теперь о магических сложных строках в С. Строка в С — это массив символов заканчивающийся символом конца строки (‘\0’). Строки можно инициализировать явно:
1 |
char str[] = "My string!"; |
В результате будет инициализирован массив символов размером в 11 символов(10 символы строки и символ ‘\0’).
Или например вот так:
1 |
char* str = "My string!"; |
Можно создать сначала массив, а потом заполнить его строкой. Например с помощью функции strcpy:
1 2 |
char str1[20]; strcpy(str1, "My string!"); |
Строки в чистом С очень отличаются от строк в паскале. Например что бы их сравнить нужно использовать функцию strcmp, потому что выражение типа:
1 2 3 4 |
char* str1 = "Joke!"; char* str2 = "Joke!"; if (str1 == str2) //do something |
Будет давать неверный результат, ведь str1 и str2 — указатели на первый элемент массива, и сравнивается их равенство, но никак не строки! Это необходимо запомнить. Есть функция strcmp, возвращает ноль — если строки равны. Использовать вот так:
1 2 3 4 |
char* str1 = "My STR!"; char* str2 = "My STR!"; if (!strcmp(str1, str2)) //do something |
Поскольку strcmp вернет 0, то !0(не 0) будет true.
Теперь напишем функцию, которая будет добавлять символ в конец строки. Прототип нашей функции:
1 |
char* AppendChar(const char* input, char character); |
Принимает в качестве аргументов строку input, модификатор const который, означает, что в процессе выполнения функции, строка изменена не будет, и символ character. Теперь реализация:
1 2 3 4 5 6 7 8 9 |
char* AppendChar(const char* input, char character) { //создадим новую строку и выделим память для нее char* output = (char*) malloc(strlen(input) + sizeof(char) * 2); strcpy(output, input); //копируем туда входящую строку output[strlen(input)] = character; //добавляем наш символ output[strlen(input) + 1] = '\0'; //добавляем признак конца строки return output; } |
Сначала мы выделяем память для «выходной» строки. Размер памяти задаем как strlen(input) + sizeof(char) * 2, то есть размер входящей строки плюс два символа: новый символ и признак конца строки ‘\n’.
Потом мы копируем в output саму input. Следующая операция заменяет символ ‘\0’ нашим символом, а следующий(пустой) — ‘\0’.
Подытожим:
- Переменная массива — это указатель на его первый элемент.
- С указателем можно работать также как и с массивом, использую квадратные скобки. Предварительно нужно выделить ему память с помощью функции malloc.
- Строки в С представляют собой массивы символов(char), длинна массива равна длине строки +1, в конец записывается признак конца строки, символ ‘\0’. Для их использования, необходимо создавать массивы или использовать указатели.
- После того, как указатель стал не нужным, необходимо освободить выделенную ему память с помощью функции free, а самому указателю присвоить NULL. Это обезопасит ваш код!
- Для работы со строками в С есть специальные функции. Например strcmp — сравнивает строки, в случае совпадения возвращает ноль.
- Оператор sizeof возвращает размер переменной/типа в байтах. Он не умеет вычислять размер динамической памяти, поэтому не используйте с указателями!
Теперь немного о С++. Что бы выделить память указателю, достаточно написать:
1 |
char* myString = new char[40]; |
Указателю будет выделена память размером в 40 символов. Для очистки:
1 |
delete(myString); |
Подробнее читайте в гугле, там есть особенности связанные с классами(ООП). Чистый С не имеет классов, перегруженных функций и прочих вкусностей С++.
Спрашивайте свои ответы. Пишите пожелания/просьбы. И не забывайте сообщать мне об орфографических ошибках :D!
Удачи.
Сделаю вывод сразу по двум частям. Спасибо за старания. Но… Очень сухо, мало объяснений, одни примеры, и некоторые из них неудачные, и только запутывают. Самое лучшее, что я читал на эту тему и что мне самому помогло разобраться в указателях — это статья Andrew Peace «A Beginner’s Guide to Pointers» — кратко доходчиво и ничего лишнего.
Спасибо за грамотную критику. Попытаюсь сделать лучше.
spasibo! no mojete napisat’ po bol’she for example 😀
Окей, скоро напишу статью «Указатели в примерах!».
Вот код с массивами из примера выше:
#include
void main()
{
int array1[2];
array1[0] = 0;
array1[1] = 1;
int* pointer1 = NULL;
pointer1 = array1;
++pointer1;
printf("%irn", *pointer1);
}
Вопрос : pointer1 = array1; pointer1 присваевается адрес array1,например адрес array1 равен 0x5A3?А в строке(++pointer;) префиксное увеличение даст 0x5A4 или передвинет на следущий элемент массива,но тогда следущий элемент array1[0] после array1,а не array1[1].
В компиляторе все работает,получается где-то я ошибься…
Дело в том, что сама переменная массива (то есть без индексных скобок «[]») является указателем на первый элемент массива. Если ее сделать:
int *pointer1;
int array1[5];
а потом
*pointer1 = array1;
То по сути, теперь pointer1 тот же массив. То есть он указывает на первый элемент массива array1. Если его инкрементировать, он сдвинет адрес и будет указывать на следующий элемент массива. Что и случается в этом примере.
Просто все дело в том, что массив это указатель на первый элемент набора элементов.
#include
#include
using namespace std;
void main()
{
int array1[3];
array1[0] = 0;
array1[1] = 1;
array1[2] = 2;
// вывожу элементы массива
cout << array1[0] << endl; //0
cout << array1[1] << endl; //1
cout << array1[2] << endl; //2
pointer1 = array1;
cout << array1 << endl; // hex(т.к. указатель адрес в памяти)
cout << pointer1 << endl; // hex(т.к. указатель адрес в памяти)
cout << ++pointer1 << endl; // hex+4байта(как раз размер типа int равен 4 байта,я так понял в зависимости от типа среда расчитывает,где находиться следущий элемент массива)
printf("%irn", *pointer1); /* а вот тут уже array[1],я так догадываюсь, что вместо %i подставился номер элемента массива*/
cin.get();
}
Спасибо за помощь)))
P.S жду новых статей на тему С++
Пожалуйста.
По поводу вопроса о printf. Почитай-те как эта функция выводит в консоль данные. %i — это директива для printf, которая говорит, что элемент будет типа integer.
Учту (про статьи о С++).
Скоро будет продолжение уроков по с++???
А есть пожелания? 🙂
Если будет время и придумаю тему — напишу.
Как происходит работа памяти во время выполнения функций и
классов.
#include
using namespace std;
int result(int variable);
void main ()
{
int x = 10;
result(x);
cout << x;
cin.get ();
cin.get ();
}
int result(int variable)
{
cout << variable*2 << endl;
int x;
x = 30;
return x;
}
Например,значение переменной копируется в функцию при передачи ее как аргумента, поэтому при переприсвоении переменной внутри функции она во внешнем окружении не меняется,вот такие ньюансы хотелось бы понять.
Я понял. Кстати, интересная тема. Попробую что-то написать новое.
Интересует ли программирование на С\С++ в Linux? Назревает проект, по ходу дела может получиться пару статей написать.
К сожалению уровень моих знаний С++ пока низкий, но идея С/C++в linux очень даже интересная, можно еще и в windows с использованием MFC и других библиотек.
Прочитал обе статьи про указатели, автору огромная благодарность!!! Всё описано простым и доступным языком, примеры, всё в тему!!! Проштудировал большое количество статей, эта самая лучшая! Продолжай в том же духе!!!
Спасибо :), рад был помочь.
Классно,спс,статья многое объясняет
Пожалуйста. Рад, что смог помочь.
«Чтобы» и «итак» пишется слитно; на пунктуацию внимания не обращала: это отдельная тема. Отличная статья, кстати!
С ошибками у меня плохо.
Спасибо. Хорошая статья. Продолжай к динамическому выделению памяти
понравились иллюстрации выделения памяти. Сразу стало понятно.
Там где выводим строку, какую нужно подключить библиотеку?