Массивы в c++

Различия между указателями и массивами

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


#include <iostream> int main() { int array = { 5, 8, 6, 4 }; std::cout << sizeof(array) << ‘\n’; // выведется sizeof(int) * длина array int *ptr = array; std::cout << sizeof(ptr) << ‘\n’; // выведется размер указателя return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13

#include <iostream>

intmain()

{

intarray4={5,8,6,4};

std::cout<<sizeof(array)<<‘\n’;// выведется sizeof(int) * длина array

int*ptr=array;

std::cout<<sizeof(ptr)<<‘\n’;// выведется размер указателя

return;

}

Результат выполнения программы:

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

Второе различие возникает при использовании оператора адреса . Используя адрес указателя, мы получаем адрес памяти переменной указателя. Используя адрес массива, возвращается указатель на целый массив. Этот указатель также указывает на первый элемент массива, но информация о типе отличается. Вряд ли вам когда-нибудь понадобится это использовать.

Передача массивов в функции

На уроке №75 мы говорили, что, из-за того, что копирование больших массивов при передаче в функцию является очень затратной операцией, C++ не копирует массив. При передаче массива в качестве аргумента в функцию, массив распадается в указатель на массив и этот указатель передается в функцию:

#include <iostream> void printSize(int *array) { // Здесь массив рассматривается как указатель std::cout << sizeof(array) << ‘\n’; // выведется размер указателя, а не длина массива! } int main() { int array[] = { 1, 2, 3, 4, 4, 9, 15, 25 }; std::cout << sizeof(array) << ‘\n’; // выведется sizeof(int) * длина массива printSize(array); // здесь аргумент array распадается на указатель return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

#include <iostream>

voidprintSize(int*array)

{

// Здесь массив рассматривается как указатель

std::cout<<sizeof(array)<<‘\n’;// выведется размер указателя, а не длина массива!

}

intmain()

{


intarray={1,2,3,4,4,9,15,25};

std::cout<<sizeof(array)<<‘\n’;// выведется sizeof(int) * длина массива

printSize(array);// здесь аргумент array распадается на указатель

return;

}

Результат выполнения программы:

Обратите внимание, результат будет таким же, даже если параметром будет фиксированный массив:

#include <iostream> // C++ неявно конвертирует параметр array[] в *array void printSize(int array[]) { // Здесь массив рассматривается как указатель, а не как фиксированный массив std::cout << sizeof(array) << ‘\n’; // выведется размер указателя, а не размер массива! } int main() { int array[] = { 1, 2, 3, 4, 4, 9, 15, 25 }; std::cout << sizeof(array) << ‘\n’; // выведется sizeof(int) * длина массива array printSize(array); // здесь аргумент array распадается на указатель return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

#include <iostream>

// C++ неявно конвертирует параметр array[] в *array

voidprintSize(intarray)

{

// Здесь массив рассматривается как указатель, а не как фиксированный массив

std::cout<<sizeof(array)<<‘\n’;// выведется размер указателя, а не размер массива!

}

intmain()

{

intarray={1,2,3,4,4,9,15,25};

std::cout<<sizeof(array)<<‘\n’;// выведется sizeof(int) * длина массива array

printSize(array);// здесь аргумент array распадается на указатель

return;

}

Результат выполнения программы:

В примере, приведенном выше, C++ неявно конвертирует параметр из синтаксиса массива () в синтаксис указателя (). Это означает, что следующие два объявления функции идентичны:


void printSize(int array[]); void printSize(int *array);

1 2

voidprintSize(intarray);

voidprintSize(int*array);

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

Рекомендуется использовать синтаксис указателя, поскольку он позволяет понять, что параметр будет обработан как указатель, а не как фиксированный массив, и определенные операции, такие как в случае с оператором sizeof, будут выполняться с параметром-указателем (а не с параметром-массивом).

Совет: Используйте синтаксис указателя () вместо синтаксиса массива () при передаче массивов в качестве параметров в функции.

Формальные и фактические параметры

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

Например, пусть есть функция, которая возвращает квадрат числа и функция, которая суммирует два числа.

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

//Формальные параметры имеют имена a и b
//по ним мы обращаемся к переданным аргументам внутри функции
int sum(int a, int b) {
	return a+b;
}

float square(float x) {
	return x*x;
}

void main() {
	//Фактические параметры могут иметь любое имя, в том числе и не иметь имени
	int one = 1;
	float two = 2.0;

	//Передаём переменные, вторая переменная приводится к нужному типу
	printf("%d\n", sum(one, two));
	//Передаём числовые константы
	printf("%d\n", sum(10, 20));
	//Передаём числовые константы неверного типа, они автоматически приводится к нужному
	printf("%d\n", sum(10, 20.f));
	//Переменная целого типа приводится к типу с плавающей точкой
	printf("%.3f\n", square(one));
	//В качестве аргумента может выступать и вызов функции, которая возвращает нужное значение
	printf("%.3f\n", square(sum(2 + 4, 3)));

	getch();
}

Обращаю внимание, что приведение типов просиходит неявно и только тогда, когда это возможно. Если функция получает число в качестве аргумента, то нельзя ей передать переменную строку, например «20» и т.д

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

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

void main() {
	char c;

	do {
		//Сохраняем возвращённое значение в переменную
		c = getch();
		printf("%c", c);
	} while(c != 'q');
	//Возвращённое значение не сохраняется
	getch();
}

6.2. Управление жизненным циклом динамического массива

Стандартный интеллектуальный указатель можно использовать для управления жизненным циклом динамического массива (см. ). Он имеет частичную специализацию для массивов, которая перегружает оператор вместо оператора и использует оператор в качестве удалителя по умолчанию. Вот пример:

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

arraySort([func,] arr, …)

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

Пример сортировки целочисленных значений:

Пример сортировки строковых значений:

Значения , и сортируются по следующему принципу:

  • Значения идут в начале массива.
  • Значения идут в конце массива.
  • Значения идут перед .
  • Значения идут перед .

Функция является функцией высшего порядка — в качестве первого аргумента ей можно передать лямбда-функцию. В этом случае порядок сортировки определяется результатом применения лямбда-функции на элементы массива.

Рассмотрим пример:

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

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

Элементы, указанные во втором массиве (), определяют ключ сортировки для элементов из исходного массива (), то есть . Так как лямбда-функция не использует , элементы исходного массива не влияют на порядок сортировки. Таким образом, ‘hello’ будет вторым элементом в отсортированном массиве, а ‘world’ — первым.


Ниже приведены другие примеры.

Примечание

Для улучшения эффективности сортировки применяется преобразование Шварца.

Передача адресов по ссылке

#include <iostream> // tempPtr теперь является ссылкой на указатель, поэтому любые изменения tempPtr приведут и к изменениям исходного аргумента! void setToNull(int *&tempPtr) { tempPtr = nullptr; // используйте 0, если не поддерживается C++11 } int main() { // Сначала мы присваиваем ptr адрес six, т.е. *ptr = 6 int six = 6; int *ptr = &six; // Здесь выведется 6 std::cout << *ptr; // tempPtr является ссылкой на ptr setToNull(ptr); // ptr было присвоено значение nullptr! if (ptr) std::cout << *ptr; else std::cout << » ptr is null»; 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

#include <iostream>

// tempPtr теперь является ссылкой на указатель, поэтому любые изменения tempPtr приведут и к изменениям исходного аргумента!

voidsetToNull(int*&tempPtr)

{

tempPtr=nullptr;// используйте 0, если не поддерживается C++11

}

intmain()

{

// Сначала мы присваиваем ptr адрес six, т.е. *ptr = 6

intsix=6;

int*ptr=&six;

// Здесь выведется 6

std::cout<<*ptr;

// tempPtr является ссылкой на ptr

setToNull(ptr);

// ptr было присвоено значение nullptr!

if(ptr)

std::cout<<*ptr;

else

std::cout<<» ptr is null»;

return;

}

Результат выполнения программы:

Наконец, наша функция setToNull() действительно изменила значение с на !

Передача аргументов

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

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

void change(int a) {
	a = 100;
	printf("%d\n", a);
}

void main() {
	int d = 200;
	printf("%d\n", d);
	change(d);
	printf("%d", d);
	getch();
}

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

Каким образом тогда можно изменить переменную? Для этого нужно передать адрес этой переменной. Перепишем функцию, чтобы она принимала указатель типа int

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

void change(int *a) {
	*a = 100;
	printf("%d\n", *a);
}

void main() {
	int d = 200;
	printf("%d\n", d);
	change(&d);
	printf("%d", d);
	getch();
}

Вот теперь программа выводит

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

В программировании первый способ передачи параметров называют передачей по значению, второй – передачей по указателю.

Запомните простое правило: если вы хотите изменить переменную, необходимо передавать функции указатель на эту переменную. Следовательно, чтобы изменить указатель, необходимо передавать указатель на указатель и т.д.

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

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

void init(int *a, unsigned size) {
	a = (int*) malloc(size * sizeof(int));
}

void main() {
	int *a = NULL;
	init(a, 100);
	if (a == NULL) {
		printf("ERROR");
	} else {
		printf("OKAY...");
		free(a);
	}
	getch();
}

Но эта функция выведет ERROR. Мы передали адрес переменной. Внутри функции init была создана локальная переменная a, которая хранит адрес массива. После выхода из функции эта локальная переменная была уничтожена. Кроме того, что мы не смогли добиться нужного результата, у нас обнаружилась утечка памяти: была выделена память на куче, но уже не существует переменной, которая бы хранила адрес этого участка.

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

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

void init(int **a, unsigned size) {
	*a = (int*) malloc(size * sizeof(int));
}

void main() {
	int *a = NULL;
	init(&a, 100);
	if (a == NULL) {
		printf("ERROR");
	} else {
		printf("OKAY...");
		free(a);
	}
	getch();
}

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

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

char* initByString(const char *str) {
	char *p = (char*) malloc(strlen(str) + 1);
	strcpy(p, str);
	return p;
}

void main() {
	char *test = initByString("Hello World!");
	printf("%s", test);
	free(test);
	getch();
}

В этом примере утечки памяти не происходит. Мы выделили память с помощью функции malloc, скопировали туда строку, а после этого вернули указатель. Локальные переменные были удалены, но переменная test хранит адрес участка памяти на куче, поэтому можно его удалить с помощью функции free.


С этим читают