Указатели в C/C++ для новичков (Часть 2)

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

  • Что общего у массивов и указателей;
  • Как выделять память;
  • Что из себя представляют строки;

Кому интересно, прошу под спойлер!

И так. Для начала узнаем, что же такое массив. Вот небольшой код:

#include <stdio.h>

void main()
{
	int array[3]; //массив из трех элементов
	array[0] = 0;
	array[1] = 1;
	array[2] = 2;
	printf("%p, %irn", array, *array);
}

Странный код, правда? Создается массив. Потом мы его пытаемся вывести на экран как указатель(wtf?). Мало того, получаем значение массива, как будто он указатель. Бред? А вот и нет! Массив и есть указатель. Почти. Дело в том, что написав:

int array[3];

Мы скомандовали компьютеру: «выделить последовательно(один блок за одним) память, для трех элементов типа int, указатель на первый элемент поместить в переменную array«. Теперь попробуем наоборот, работать с указателем как с массивом:

#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:

int var1 = 0;

Компьютер выделит память для нашей переменной, и мы получим что-то вроде:

Теперь создадим указатель:

int* pointer1 = NULL;

Получим что-то вроде:

Теперь сделаем так:

pointer1 = &var1;

Как вы уже догадались в указатель pointer1 помещается адрес переменной var1. То есть получается вот так:

Что же происходит с массивами? Объявим массив:

int array1[3];

В результате компьютер выделит память размером в 3 блока. Указатель на первый элемент поместит в переменную array1. Абстрактно получится так:

Блоки памяти для элементов массива выделяются последовательно, указатель на первый элемент помещается в array1. По сути, когда вы пишите array1[2] компьютер это расценивает как «переместится на две позиции в памяти из указателя array1«. Вот вам пример:

#include <stdio.h>

void main()
{
	int array1[2];
	array1[0] = 0;
	array1[1] = 1;
	int* pointer1 = NULL;
	pointer1 = array1;
	++pointer1;
	printf("%irn", *pointer1);
}

В результате получим:

А можно и так:

#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’). Строки можно инициализировать явно:

char str[] =  "My string!";

В результате будет инициализирован массив символов размером в 11 символов(10 символы строки и символ ‘\0’).

Или например вот так:

char* str = "My string!";

Можно создать сначала массив, а потом заполнить его строкой. Например с помощью функции strcpy:

char str1[20];
strcpy(str1, "My string!");

Строки в чистом С очень отличаются от строк в паскале. Например что бы их сравнить нужно использовать функцию strcmp, потому что выражение типа:

char* str1 = "Joke!";
char* str2 = "Joke!";
if (str1 == str2)
    //do something

Будет давать неверный результат, ведь str1 и str2 — указатели на первый элемент массива, и сравнивается их равенство, но никак не строки! Это необходимо запомнить. Есть функция strcmp, возвращает ноль — если строки равны. Использовать вот так:

char* str1 = "My STR!";
char* str2 = "My STR!";
if (!strcmp(str1, str2))
    //do something

Поскольку strcmp вернет 0, то !0(не 0) будет true.

Теперь напишем функцию, которая будет добавлять символ в конец строки. Прототип нашей функции:

char* AppendChar(const char* input, char character);

Принимает в качестве аргументов строку input, модификатор const который, означает, что в процессе выполнения функции, строка изменена не будет, и символ character. Теперь реализация:

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 возвращает размер переменной/типа в байтах. Он не умеет вычислять размер динамической памяти, поэтому не используйте с указателями!

Теперь немного о С++. Что бы выделить память указателю, достаточно написать:

char* myString = new char[40];

Указателю будет выделена память размером в 40 символов. Для очистки:

delete(myString);

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

Спрашивайте свои ответы. Пишите пожелания/просьбы. И не забывайте сообщать мне об орфографических ошибках :D!

Удачи.

Комментарии 23

  • Сделаю вывод сразу по двум частям. Спасибо за старания. Но… Очень сухо, мало объяснений, одни примеры, и некоторые из них неудачные, и только запутывают. Самое лучшее, что я читал на эту тему и что мне самому помогло разобраться в указателях — это статья 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 и других библиотек.

  • Прочитал обе статьи про указатели, автору огромная благодарность!!! Всё описано простым и доступным языком, примеры, всё в тему!!! Проштудировал большое количество статей, эта самая лучшая! Продолжай в том же духе!!!

  • Классно,спс,статья многое объясняет

  • «Чтобы» и «итак» пишется слитно; на пунктуацию внимания не обращала: это отдельная тема. Отличная статья, кстати!

  • Спасибо. Хорошая статья. Продолжай к динамическому выделению памяти

  • понравились иллюстрации выделения памяти. Сразу стало понятно.
    Там где выводим строку, какую нужно подключить библиотеку?

    • Пардон за долгий ответ, так сказать.

      Думаю вам уже не актуально, но зацените мой блог на английском. Ссылка в шапке.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *