Как написать прототип функции

Функции (functions) в C++: перегрузки и прототипы

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

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

  • Зачем их использовать
  • Перегрузка функций
  • Прототипы функций
  • Достоинства

Что такое функции

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

#include <iostream>

using namespace std;

int main() {

    cout << «Функция очень хороший инструмент в программировании»;

    cout << «С помощью его можно улучшить свой уровень программирования»;

system («pause»)

return 0;

}

А вот если бы мы использовали функции, то у нас получилось бы так:

#include <iostream>

using namespace std;

void func () {  // функция

  cout << «Функция очень хороший инструмент в программировании»;

  cout << «С помощью его можно улучшить свой уровень программирования»;

}

int main() {

  func();  // вызов функции

system («pause»)

return 0;

}

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

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

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

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

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

<тип данных, который будет возвращаться функцией> <имя> (<аргументы функции>) {

  < блок

    кода >

}

Давайте разберем эту конструкцию:

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

Но если мы не собираемся ничего передавать, а например, хотим просто вывести строку, то на месте <тип данных> можно указать тип void. Также вы можете указать тип int, это ни на что не повлияет.

void stroka() {

  cout << «Выводим строку без всяких переменных»;

}

  • Имя функции. Нам нужно задать функции имя (исключениями являются зарезервированные слова в C++, имена начинающиеся с цифр, а также имена разделенные пробелом).

Лучше всего задавать такое имя, которое будет говорить вам в будущем, за что отвечает эта функция. И тогда в будущем вам не придется  вспоминать, за что она отвечает.

  • Аргументы функции. В скобках (после имени функции) могут находиться аргументы функции. Аргумент функции — это значение, которое можно передать функции при ее вызове. Если аргумент функции не один, а их несколько, то их нужно разделять запятой.

int sum(int b, int c) {  // у нас аргументы функции это b и c

Если аргументов в функции нет, то в скобках можно указать тип void. Но писать его необязательно, он стоит по умолчанию.

void stroka(void) {

  cout << «Просто выводим строку»;

}

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

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

int sum(int b, int c) {  // у нас аргументы функции это b и c

  return a + b;  // он возвращает a + b

}

Если вы не знали main() — это тоже функция.

Как вызывать функцию

Для вызова функций вам нужно использовать такую конструкцию:

<имя функции> (<аргументы, если они есть>);

Например, выше для вызова функции stroka() (эта функция находится выше) нам нужно использовать такую конструкцию:

Как видите, мы не вписывали аргументы в круглые скобки, так как мы их не указали при создании функции.

Зачем использовать функции

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

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

А если бы вы использовали функцию, которая выводила сообщение «Привет!», то тогда бы вам пришлось только найти эту функцию и изменить ее!

Перегрузка функций

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

Все дело в том, что у каждой функции есть свое полное имя (или по-другому сигнатура). Параметры функции — это вся информация о функции. В эту информацию входят:

  • Имя функции.
  • Число аргументов функции.
  • Типы аргументов.

Именно поэтому компилятор считает функции с одинаковыми именами разными, если сигнатуры соответственно тоже разные.

Перегрузка функций — это создание функций с одинаковыми именами, но с разными сигнатурами (полными именами).

В примере ниже все функции разные, хотя и имена у них одинаковые:

int func(int a) {

  // выполнение функции номер 1 

}

double func() {

  // выполнение функции номер 2

}

int func(double b, double h) {

  // выполнение функции номер 3

}

long long func(int n, int c, int g) {

  // выполнение функции номер 4

}

Распространенной ошибкой является мнение, что тип возвращаемого значения функции входит в полное имя функции.

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

int func() {

}

double func() {

  //ошибка: функция с таким именем уже существует

}

long long func() {

  //ошибка: функция с таким именем уже существует

}

Прототипы функций

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

int main() {

    Sum_numbers(1, 2);

    return 0;

}

int Sum_numbers(int a, int b) {

  cout << a + b;

}

Так, при вызове функции Sum_numbers() внутри функции main() компилятор не знает ее полное имя.

Конечно компилятор C++ мог просмотреть весь код и определить полное имя функции, но этого он делать не умеет и нам приходится с этим считаться.

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

Прототип функции — это функция, в которой отсутствует блок кода (тело функции). В прототипе функции находятся:

  • Полное имя функции.
  • Тип возвращаемого значения функции.

Вот как правильно должна была выглядеть программа написанная выше:

int Sum_numbers(int a, int b);   // прототип функции

int main() {

    Sum_numbers(1, 2);

    return 0;

}

int Sum_numbers(int a, int b) {  // сама функция

  cout << a + b;

}

Так, с помощью прототипа мы объясняем компилятору, что полным именем функции является Sum_numbers(int a, int b), и кроме этого мы говорим компилятору о типе возвращаемого значения, у нас им является тип int.

Плюсы использования функций

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

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

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

Закрепление функций

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

Тест на тему «Функции в C++»

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

If loading fails, click here to try again

Попробуй пройти тест. Если ты действительно понял весь материал по данной теме, то ты без труда сможешь набрать 100 из 100. Удачи!

Надеемся, вам понравилась данная статья. Нам будет приятно, если вы поделитесь данным уроком со своими друзьями. Если вам что-то не понятно в данной статье, ниже вы можете оставить свой комментарий. Удачи!

Теги: Функции в си, прототип, описание, определение, вызов. Формальные параметры и фактические параметры. Аргументы функции, передача по значению, передача по указателю. Возврат значения.

Введение

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

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

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

Мы уже знакомы с многими функциями и знаем, как их вызывать – это функции библиотек stdio, stdlib, string, conio и пр. Более того, main – это тоже функция.
Она отличается от остальных только тем, что является точкой входа при запуске приложения.
Функция в си определяется в глобальном контексте. Синтаксис функции:

<возвращаемый тип> <имя функции>(<тип1> <арг1>, <тип1> <арг2>, ...) {
	<тело функции>
}

Самый простой пример – функция, которая принимает число типа float и возвращает квадрат этого числа

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

float sqr(float x) {
	float tmp = x*x;
	return tmp;
}

void main() {
	printf("%.3f", sqr(9.3f));
	getch();
}

Внутри функции sqr мы создали локальную переменную, которой присвоили значение аргумента. В качестве аргумента функции передали число 9,3.
Служебное слово return возвращает значение переменной tmp. Можно переписать функцию следующим образом:

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

В данном случае сначала будет выполнено умножение, а после этого возврат значения. В том случае, если функция ничего не возвращает, типом возвращаемого значения будет void.
Например, функция, которая печатает квадрат числа:

void printSqr(float x) {
	printf("%d", x*x);
	return;
}

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

void printSqr(float x) {
	printf("%d", x*x);
}

Если функция не принимает аргументов, то скобки оставляют пустыми. Можно также написать слово void:

void printHelloWorld() {
	printf("Hello World");
}

эквивалентно

void printHelloWorld(void) {
	printf("Hello World");
}

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

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

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

#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("%dn", sum(one, two));
	//Передаём числовые константы
	printf("%dn", sum(10, 20));
	//Передаём числовые константы неверного типа, они автоматически приводится к нужному
	printf("%dn", sum(10, 20.f));
	//Переменная целого типа приводится к типу с плавающей точкой
	printf("%.3fn", square(one));
	//В качестве аргумента может выступать и вызов функции, которая возвращает нужное значение
	printf("%.3fn", 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();
}

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

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

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

void change(int a) {
	a = 100;
	printf("%dn", a);
}

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

Программы выведет
200
100
200

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

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

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

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

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

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

Здесь также была создана локальная переменная, но так как передан был адрес, то мы изменили значение переменной 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.

Объявление функции и определение функции. Создание собственной библиотеки

В си можно объявить функцию до её определения. Объявление функции, её прототип, состоит из возвращаемого
значения, имени функции и типа аргументов. Имена аргументов можно не писать. Например

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

//Прототипы функций. Имена аргументов можно не писать
int odd(int);
int even(int);

void main() {
	printf("if %d odd? %dn", 11, odd(11));
	printf("if %d odd? %dn", 10, odd(10));
	getch();
}

//Определение функций
int even(int a) {
	if (a) {
		odd(--a);
	} else {
		return 1;
	}
}

int odd(int a) {
	if (a) {
		even(--a);
	} else {
		return 0;
	}
}

Это смешанная рекурсия – функция odd возвращает 1, если число нечётное и 0, если чётное.

Обычно объявление функции помещают отдельно, в .h файл, а определение функций в .c файл. Таким образом, заголовочный файл представляет
собой интерфейс библиотеки и показывает, как с ней работать, не вдаваясь в содержимое кода.

Давайте создадим простую библиотеку. Для этого нужно будет создать два файла – один с расширением .h и поместить туда прототипы функций,
а другой с расширением .c и поместить туда определения этих функций. Если вы работаете с IDE, то .h файл необходимо создавать в папке
Заголовочные файлы, а файлы кода в папке Файлы исходного кода. Пусть файлы называются File1.h и File1.c

Перепишем предыдущий код. Вот так будет выглядеть заголовочный файл File1.h

#ifndef _FILE1_H_
#define _FILE1_H_

int odd(int);
int even(int);

#endif

Содержимое файла исходного кода File1.c

#include "File1.h"

int even(int a) {
	if (a) {
		odd(--a);
	} else {
		return 1;
	}
}

int odd(int a) {
	if (a) {
		even(--a);
	} else {
		return 0;
	}
}

Наша функция main

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

void main() {
	printf("if %d odd? %dn", 11, odd(11));
	printf("if %d odd? %dn", 10, odd(10));
	getch();
}

Рассмотрим особенности каждого файла. Наш файл, который содержит функцию main, подключает необходимые ему библиотеки а также заголовочный
файл File1.h. Теперь компилятору известны прототипы функций, то есть он знает возвращаемый тип, количество и тип аргументов и имена
функций.

Заголовочный файл, как и оговаривалось ранее, содержит прототип функций. Также здесь могут быть подключены используемые библиотеки.
Макрозащита #define _FILE1_H_ и т.д. используется для предотвращения повторного копирования кода библиотеки при компиляции.
Эти строчки можно заменить одной

#pragma once

int odd(int);
int even(int);

Файл File1.c исходного кода подключает свой заголовочный файл. Всё как обычно логично и просто. В заголовочные файлах принято кроме
прототипов функций выносить константы, макроподстановки и определять новые типы данных. Кроме того, именно в заголовочных файлах можно
обширно комментировать код и писать примеры его использования.

Передача массива в качестве аргумента

Как уже говорилось ранее, имя массива подменяется на указатель, поэтому передача одномерного массива эквивалентна передаче указателя. Пример:
функция получает массив и его размер и выводит на печать:

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

void printArray(int *arr, unsigned size) {
	unsigned i;
	for (i = 0; i < size; i++) {
		printf("%d ", arr[i]);
	}
}

void main() {
	int x[10] = {1, 2, 3, 4, 5};
	printArray(x, 10);
	getch();
}

В этом примере функция может иметь следующий вид

void printArray(int arr[], unsigned size) {
	unsigned i;
	for (i = 0; i < size; i++) {
		printf("%d ", arr[i]);
	}
}

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

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

void printArray(int arr[][5], unsigned size) {
	unsigned i, j;
	for (i = 0; i < size; i++) {
		for (j = 0; j < 5; j++) {
			printf("%d ", arr[i][j]);
		}
		printf("n");
	}
}

void main() {
	int x[][5] = { 
		{ 1, 2, 3, 4, 5},
		{ 6, 7, 8, 9, 10}};
	printArray(x, 2);
	getch();
}

Либо, можно писать

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

void printArray(int (*arr)[5], unsigned size) {
	unsigned i, j;
	for (i = 0; i < size; i++) {
		for (j = 0; j < 5; j++) {
			printf("%d ", arr[i][j]);
		}
		printf("n");
	}
}

void main() {
	int x[][5] = {
		{ 1, 2, 3, 4, 5},
		{ 6, 7, 8, 9, 10}};
	printArray(x, 2);
	getch();
}

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

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

#define SIZE 10

unsigned* getLengths(const char **words, unsigned size) {
	unsigned *lengths = NULL;
	unsigned i;
	lengths = (unsigned*) malloc(size * sizeof(unsigned));
	for (i = 0; i < size; i++) {
		lengths[i] = strlen(words[i]);
	}
	return lengths;
}

void main() {
	char **words = NULL;
	char buffer[128];
	unsigned i;
	unsigned *len = NULL;
	words = (char**) malloc(SIZE * sizeof(char*));

	for (i = 0; i < SIZE; i++) {
		printf("%d. ", i);
		scanf("%127s", buffer);
		words[i] = (char*) malloc(128);
		strcpy(words[i], buffer);
	}

	len = getLengths(words, SIZE);
	for (i = 0; i < SIZE; i++) {
		printf("%d ", len[i]);
		free(words[i]);
	}
	free(words);
	free(len);
	getch();
}

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

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

#define SIZE 10

void getLengths(const char **words, unsigned size, unsigned *out) {
	unsigned i;
	for (i = 0; i < size; i++) {
		out[i] = strlen(words[i]);
	}
}

void main() {
	char **words = NULL;
	char buffer[128];
	unsigned i;
	unsigned *len = NULL;
	words = (char**) malloc(SIZE * sizeof(char*));

	for (i = 0; i < SIZE; i++) {
		printf("%d. ", i);
		scanf("%127s", buffer);
		words[i] = (char*) malloc(128);
		strcpy(words[i], buffer);
	}

	len = (unsigned*) malloc(SIZE * sizeof(unsigned));
	getLengths(words, SIZE, len);
	for (i = 0; i < SIZE; i++) {
		printf("%d ", len[i]);
		free(words[i]);
	}
	free(words);
	free(len);
	getch();
}

На этом первое знакомство с функциями заканчивается: тема очень большая и разбита на несколько статей.

Q&A

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

Параметры командной строки

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

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

Описание
функции

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

Прототип
функции имеет вид:

тип_результата
имя_функции
(список)
;

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

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

Пример
описания функции fun,
которая имеет три параметра типа int,
один параметр типа double
и возвращает результат типа double:

double
fun(int, int, int, double);

Пример
описания для вышеприведенной функции
Min:

int
Min (int x, int y);

либо

int
Min (int, int);

13.4. Область видимости.

Область
видимости (действия) объекта (переменной
и др.) – это та часть кода (текста)
программы, в которой его можно использовать.

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

Основное
правило
видимости

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

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

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

Областью
действия глобальных данных считается
файл, в котором они определены, от точки
описания до его окончания.

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

Объявление в одном и том же блоке на
одном и том же уровне (или глобально — в
одном и том же файле) переменных с
одинаковыми именами вызывает ошибку
компиляции.

Сказанное
иллюстрирует пример:

void
fun(void);

void
fun2(int a);

int
a=10;

void
main(){

cout<<a<<endl; //
a=10

int
a=20;

cout<<a<<endl;
// a=20

do{

int
a=30;

cout<<a<<endl;
// a=30

}
while(0);

cout<<a<<endl;
// a=20

for
(int i=0; i<1; i++) {

int
a=40;

cout<<i<<endl; //
i=0

cout<<a<<endl; //
a=40

}

cout<<a<<endl; //
a=20

fun();

fun2(a);

}

void
fun(void){

cout<<a<<endl; //
a=10

}

void
fun2(int a){

cout<<a<<endl; //
a=20

}

Некоторые
уточнения:

  1. Параметры
    функции являются локальными переменными,
    их область действия — вся функция;

  2. Если
    переменная объявлена внутри круглых
    скобок какого-либо оператора (например,
    for
    (int i=0; i<n: i++) { … } ),
    то
    областью ее действия одни компиляторы
    считают этот оператор (вместе с его
    телом), а другие — весь блок, в котором
    он находится. Рекомендуется поэтому
    использовать подобное место объявления,
    только если это не может привести к
    двусмысленности.

  3. Область
    действия метки — вся функция (даже если
    метка лежит во вложенном блоке).

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

Замечание.
Некоторые компиляторы не допускают
переход с помощью
goto
внутрь
области видимости переменной извне
(«перескакивая» ее объявление),
даже если сама эта переменная при этом
не используется, т.к. это мешает им
распределять память:

goto
M;

int
i;

M: //
Может
вызвать ошибку компиляции

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

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

Добавлено 10 апреля 2021 в 17:09

Взгляните на этот, казалось бы, правильный пример программы:

#include <iostream>
 
int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << 'n';
    return 0;
}
 
int add(int x, int y)
{
    return x + y;
}

Вы ожидаете, что эта программа даст результат:

The sum of 3 and 4 is: 7

Но на самом деле она вообще не компилируется! Visual Studio выдает следующую ошибку компиляции:

add.cpp(5) : error C3861: 'add': identifier not found

Причина, по которой эта программа не компилируется, заключается в том, что компилятор последовательно компилирует содержимое исходных файлов. Когда компилятор достигает вызова функции add в строке 5 в функции main, он не знает, что такое add, потому что мы определили add только в строке 9! Это вызывает ошибку, «identifier not found» (идентификатор не найден).

Более старые версии Visual Studio выдали бы дополнительную ошибку:

add.cpp(9) : error C2365: 'add' : redefinition; previous definition was 'formerly unknown identifier'

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

Лучшая практика


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

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

Вариант 1. Изменение порядка определений функций

Один из способов решения проблемы – переупорядочить определения функций так, чтобы add была определена перед main:

#include <iostream>
 
int add(int x, int y)
{
    return x + y;
}
 
int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << 'n';
    return 0;
}

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

Кроме того, этот вариант не всегда возможен. Допустим, мы пишем программу, которая имеет две функции A и B. Если функция A вызывает функцию B, а функция B вызывает функцию A, то нет способа упорядочить функции таким образом, чтобы компилятор был доволен. Если вы сначала определите A, компилятор пожалуется, что не знает, что такое B. Если вы сначала определите B, компилятор пожалуется, что не знает, что такое A.

Вариант 2. Использование предварительного объявления

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

Предварительное объявление позволяет нам сообщить компилятору о существовании идентификатора до его фактического определения.

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

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

Вот прототип функции для функции add:

// Прототип функции включает в себя тип возвращаемого значения, имя, 
// параметры и точку с запятой. Тело функции отсутствует!
int add(int x, int y); 

Итак, вот наша исходная программа, которая не компилировалась, использующая прототип функции в качестве предварительного объявления для функции add:

#include <iostream>
 
int add(int x, int y); // предварительное объявление add() (с использованием прототипа функции)
 
int main()
{
    // это работает потому, что мы предварительно объявили add() выше
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << 'n'; 
    return 0;
}
 
int add(int x, int y) // хотя тело add() не определено до этого момента
{
    return x + y;
}

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

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

int add(int, int); // корректный прототип функции

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

Лучшая практика


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

Забываем о теле функции

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

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

Рассмотрим следующую программу:

#include <iostream>
 
int add(int x, int y); // предварительное объявление add() с использованием прототипа функции
 
int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << 'n';
    return 0;
}
 
// примечание: нет определения для функции add

В этой программе мы даем предварительное объявление add и вызываем add, но нигде не определяем add. Когда мы пытаемся скомпилировать эту программу, Visual Studio выдает следующее сообщение:

Compiling...
add.cpp
Linking...
add.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)
add.exe : fatal error LNK1120: 1 unresolved externals

Как видите, программа скомпилировалась нормально, но проблема возникла на этапе компоновки, потому что int add(int, int) никогда не определялась.

Другие типы предварительных объявлений

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

Объявления против определений

В C++ вы часто будете слышать слова «объявление» и «определение», часто взаимозаменяемые. Что они имеют в виду? Теперь у вас достаточно знаний, чтобы понять разницу между ними.

Определение фактически реализует (для функций или типов) или создает экземпляр (для переменных) идентификатора. Вот несколько примеров определений:

int add(int x, int y) // реализует функцию add()
{
    int z{ x + y }; // создает экземпляр переменной z
 
    return z;
}

Определение требуется, чтобы удовлетворить компоновщик (линкер). Если вы используете идентификатор без определения, компоновщик выдаст ошибку.

Правило одного определения (или сокращенно ODR, one definition rule) – это хорошо известное правило в C++. ODR состоит из трех частей:

  1. В заданном файле функция, объект, тип или шаблон могут иметь только одно определение.
  2. В заданной программе объект или обычная функция может иметь только одно определение. Это выделено потому, что программы могут иметь более одного файла (мы рассмотрим это в следующем уроке).
  3. Типы, шаблоны, встроенные функции и переменные могут иметь идентичные определения в разных файлах. Мы еще не рассмотрели большинство из этих вещей, поэтому не беспокойтесь об этом сейчас – мы вернемся к ним, когда это будет уместно.

Нарушение пункта 1 правила одного определения приведет к тому, что компилятор выдаст ошибку переопределения. Нарушение пункта 2 правила одного определения может привести к тому, что компоновщик выдаст ошибку переопределения. Нарушение пункта 3 правила одного определения приведет к неопределенному поведению.

Вот пример нарушения пункта 1:

int add(int x, int y)
{
     return x + y;
}
 
int add(int x, int y) // нарушение ODR, мы уже определили функцию add
{
     return x + y;
}
 
int main()
{
    int x;
    int x; // нарушение ODR, мы уже определили x
}

Поскольку указанная выше программа нарушает пункт 1 правила одного определения, компилятор Visual Studio выдает следующие ошибки компиляции:

project3.cpp(9): error C2084: function 'int add(int,int)' already has a body
project3.cpp(3): note: see previous definition of 'add'
project3.cpp(16): error C2086: 'int x': redefinition
project3.cpp(15): note: see declaration of 'x'

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


Функции, которые имеют общий идентификатор, но имеют разные параметры, считаются отдельными функциями. Мы обсудим это далее в уроке «8.9 – Перегрузка функций».

Объявление – это инструкция, которая сообщает компилятору о существовании идентификатора и информацию о его типе. Вот несколько примеров объявлений:

// сообщает компилятору о функции с именем "add", которая принимает два параметра типа int 
// и возвращает значение типа int. Тела нет!
int add(int x, int y); 

// сообщает компилятору о целочисленной переменной с именем x
int x; 

Объявление – это всё, что нужно компилятору. Вот почему мы можем использовать предварительное объявление, чтобы сообщить компилятору об идентификаторе, который на самом деле не будет определен позже.

В C++ все определения также служат объявлениями. Вот почему int x появляется в наших примерах как для определений, так и для объявлений. Поскольку int x – это определение, оно же и объявление. В большинстве случаев определение служит нашим целям, поскольку оно удовлетворяет и компилятор, и компоновщик. Явное объявление нам нужно предоставить только тогда, когда мы хотим использовать идентификатор до того, как он будет определен.

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

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

Примечание автора


В обычном языке термин «объявление» обычно используется для обозначения «чистого объявления», а «определение» используется для обозначения «определения, которое также служит объявлением». Таким образом, мы обычно называем int x; определением, хотя это и определение, и объявление.

Небольшой тест

Вопрос 1

Что такое прототип функции?

Ответ

Прототип функции – это инструкция объявления, которая включает в себя имя функции, тип возвращаемого значения и параметры. Он не включает в себя тело функции.


Вопрос 2

Что такое предварительное объявление?

Ответ

Предварительное объявление сообщает компилятору, что идентификатор существует до того, как он будет фактически определен.


Вопрос 3

Как мы даем предварительное объявление для функций?

Ответ

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


Вопрос 4

Напишите прототип для этой функции (используйте предпочтительную форму с именами):

int doMath(int first, int second, int third, int fourth)
{
     return first + second * third / fourth;
}

Ответ

// Не забывайте точку с запятой в конце, так как это инструкция.
int doMath(int first, int second, int third, int fourth);

Вопрос 5

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

a)

#include <iostream>
int add(int x, int y);
 
int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << 'n';
    return 0;
}
 
int add(int x, int y)
{
    return x + y;
}

Ответ

Не компилируется. Компилятор будет жаловаться, что add(), вызываемая в main(), не имеет того же количества параметров, что и та, что была предварительно объявлена.

b)

#include <iostream>
int add(int x, int y);
 
int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << 'n';
    return 0;
}
 
int add(int x, int y, int z)
{
    return x + y + z;
}

Ответ

Не компилируется. Компилятор будет жаловаться, что не может найти подходящую функцию add(), которая принимает 3 аргумента, потому что функция add(), которая была предварительно объявлена, принимает только 2 аргумента.

c)

#include <iostream>
int add(int x, int y);
 
int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4) << 'n';
    return 0;
}
 
int add(int x, int y, int z)
{
    return x + y + z;
}

Ответ

Не линкуется. Компилятор сопоставит предварительно объявленный прототип add с вызовом функции add() в main(). Однако функция add(), которая принимает два параметра, никогда не была реализована (мы реализовали только ту, которая принимает 3 параметра), поэтому компоновщик будет жаловаться.

d)

#include <iostream>
int add(int x, int y, int z);
 
int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << 'n';
    return 0;
}
 
int add(int x, int y, int z)
{
    return x + y + z;
}

Ответ

Компилируется и линкуется. Вызов функции add() соответствует прототипу, который был предварительно объявлен, реализованная функция также совпадает.

Теги

C++ / CppLearnCppДля начинающихОбучениеПрограммирование

Функции

Определение и описание функций

Последнее обновление: 05.01.2023

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

Определение функции

Формальное определение функции выглядит следующим образом:

тип имя_функции(параметры)
{
	выполняемые_инструкции
}

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

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

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

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

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

#include <stdio.h>

void hello()
{
	printf("Hello!n");
}

int main(void)
{	
	return 0;
}

Кроме стандартной функции main здесь также определена функция hello. Эта функция имеет тип void, то есть
фактически она ничего не возвращает. Она не имеет никаких параметров, поэтому после названия функции идут пустые круглые скобки. И все, что делает данная функция, — просто
выводит на консоль строку «Hello!».

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

Вызов функции

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

имя_функции(аргументы);

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

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

#include <stdio.h>

// определение функции
void hello()
{
	printf("Hello!n");
}

int main(void)
{	
	hello();	// вызов функции
	hello();	// вызов функции
	return 0;
}

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

hello();

В итоге программа два раза выведет строку «Hello».

Прототип или описание функции

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

#include <stdio.h>

int main(void)
{
	hello();
	hello();
	return 0;
}

void hello()
{
	printf("Hello!n");
}

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

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

тип имя_функции(параметры);

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

void hello();

Применим прототип функции:

#include <stdio.h>

// описание
void hello(void);

int main(void)
{
	hello();
	hello();
	return 0;
}

// определение
void hello()
{
	printf("Hello!n");
}

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

Стоит отметить, что если функция не принимает никаких параметров, то в ее прототипе в скобках указывается void, как в примере выше с функцией hello.

Представление программы в виде функций

Прототипы функций

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

Описан прототип функции gcd, возвращающей целое значение, с двумя целыми аргументами. Имена аргументов x и y здесь являются лишь комментариями, не несущими никакой информации для компилятора. Их можно опускать, например, описание

является вполне допустимым.

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

Пример: вычисление наибольшего
общего делителя

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

( gcd — от слов greatest common divisor ). Основная функция main лишь вводит исходные данные, вызывает функцию gcd и печатает ответ. Описание прототипа функции gcd располагается в начале текста программы, затем следует функция main и в конце — реализация функции gcd. Приведем полный текст программы:

#include <stdio.h> // Описания стандартного ввода-вывода

int gcd(int x, int y); // Описание прототипа функции

int main() {
    int x, y, d;
    printf("Введите два числа:n");
    scanf("%d%d", &x, &y);
    d = gcd(x, y);
    printf("НОД = %dn", d);
    return 0;
}

int gcd(int x, int y) { // Реализация функции gcd
    while (y != 0) {
        // Инвариант: НОД(x, y) не меняется
        int r = x % y;  // Заменяем пару (x, y) на
        x = y;          // пару (y, r), где r --
        y = r;          // остаток от деления x на y
    }
    // Утверждение: y == 0
    return x;   // НОД(x, 0) = x
}

Стоит отметить, что реализация функции gcd располагается в конце текста программы. Можно было бы расположить реализацию функции в начале текста и при этом сэкономить на описании прототипа. Это, однако, дурной стиль! Лучше всегда, не задумываясь, описывать прототипы всех функций в начале текста, ведь функции могут вызывать друг друга, и правильно упорядочить их (чтобы вызываемая функция была реализована раньше вызывающей) во многих случаях невозможно. К тому же предпочтительнее, чтобы основная функция main, с которой начинается выполнение программы, была бы реализована раньше функций, которые из нее вызываются. Это соответствует технологии «сверху вниз» разработки программы: основная задача решается сразу на первом шаге путем сведения ее к одной или нескольким вспомогательным задачам, которые решаются на следующих шагах.

Передача параметров функциям

В языке Си функциям передаются значения фактических параметров. При вызове функции значения параметров копируются в аппаратный стек, см. раздел 2.3. Следует четко понимать, что изменение формальных параметров в теле функции не приводит к изменению переменных вызывающей программы, передаваемых функции при ее вызове, — ведь функция работает не с самими этими переменными, а с копиями их значений! Рассмотрим, например, следующий фрагмент программы:

void f(int x);  // Описание прототипа функции

int main() {
    . . .
    int x = 5;
    f(x);
    // Значение x по-прежнему равно 5
    . . .
}

void f(int x) {
    . . .
    x = 0;  // Изменение формального параметра
    . . .   // не приводит к изменению фактического
            // параметра в вызывающей программе
}

Здесь в функции main вызывается функция f, которой передается значение переменной x, равное пяти. Несмотря на то, что в теле функции f формальному параметру x присваивается значение 0, значение переменной x в функции main не меняется.

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

Пример: расширенный алгоритм Евклида

Вернемся к примеру с расширенным алгоритмом Евклида, подробно рассмотренному в разделе 1.5.2. Напомним, что наибольший общий делитель двух целых чисел выражается в виде их линейной комбинации с целыми коэффициентами. Пусть x и y — два целых числа, хотя бы одно из которых не равно нулю. Тогда их наибольший общий делитель d = НОД(x,y) выражается в виде

где u и v — некоторые целые числа. Алгоритм вычисления чисел d, u, v по заданным x и y называется расширенным алгоритмом Евклида. Мы уже выписывали его на псевдокоде, используя схему построения цикла с помощью инварианта.

Оформим расширенный алгоритм Евклида в виде функции на Си. Назовем ее extGCD (от англ. Extended Greatest Common Divizor ). У этой функции два входных аргумента x, y и три выходных аргумента d, u, v. В случае выходных аргументов надо передавать функции указатели на переменные. Итак, функция имеет следующий прототип:

void extGCD(int x, int y, int *d, int *u, int *v);

При вызове функция вычисляет наибольший общий делитель от двух переданных целых значений x и y и коэффициенты его представления через x и y. Ответ записывается по переданным адресам d, u, v.

Приведем полный текст программы. Функция main вводит исходные данные (числа x и y ), вызывает функцию extGCD и печатает ответ. Функция extGCD использует схему построения цикла с помощью инварианта для реализации расширенного алгоритма Евклида.

#include <stdio.h> // Описания стандартного ввода-вывода

// Прототип функции extGCD (расш. алгоритм Евклида)
void extGCD(int x, int y, int *d, int *u, int *v);

int main() {
    int x, y, d, u, v;
    printf("Введите два числа:n");
    scanf("%d%d", &x, &y);
    if (x == 0 && y == 0) {
        printf("Должно быть хотя бы одно ненулевое.n");
        return 1; // Вернуть код некорректного завершения
    }

    // Вызываем раширенный алгоритм Евклида
    extGCD(x, y, &d, &u, &v);

    // Печатаем ответ
    printf("НОД = %d, u = %d, v = %dn", d, u, v);

    return 0;   // Вернуть код успешного завершения
}

void extGCD(int x, int y, int *d, int *u, int *v) {
    int a, b, q, r, u1, v1, u2, v2;
    int t; // вспомогательная переменная

    // инициализация
    a = x; b = y;
    u1 = 1; v1 = 0;
    u2 = 0; v2 = 1;

    // утверждение: НОД(a, b) == НОД(x, y)  &&
    //              a == u1 * x + v1 * y    &&
    //              b == u2 * x + v2 * y;

    while (b != 0) {
        // инвариант: НОД(a, b) == НОД(x, y)  &&
        //            a == u1 * x + v1 * y    &&
        //            b == u2 * x + v2 * y;
        q = a / b; // целая часть частного a / b
        r = a % b; // остаток от деления a на b
        a = b; b = r; // заменяем пару (a, b) на (b, r)

        // Вычисляем новые значения переменных u1, u2
        t = u2;         // запоминаем старое значение u2
        u2 = u1 - q * u2; // вычисляем новое значение u2
        u1 = t;           // новое u1 := старое u2

        // Аналогично вычисляем новые значения v1, v2
        t = v2;
        v2 = v1 - q * v2;
        v1 = t;
    }

    // утверждение: b == 0                 &&
    //              НОД(a, b) == НОД(m, n) &&
    //              a == u1 * m + v1 * n;

    // Выдаем ответ
    *d = a;
    *u = u1; *v = v1;
}

Пример работы программы:

Введите два числа:
187 51
НОД = 17, u = -1, v = 4

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

prototype объекта Function используется, когда функция используется в качестве конструктора с оператором new .Он станет прототипом нового объекта. prototype new

Примечание. Не все объекты Function имеют свойство prototype описание .

Атрибуты свойства Function.prototype.prototype
Writable yes
Enumerable no
Configurable no

Description

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

function Ctor() {}
const inst = new Ctor();
console.log(Object.getPrototypeOf(inst) === Ctor.prototype); 

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

Функция, имеющая свойство prototype , недостаточна для того, чтобы ее можно было использовать в качестве конструктора. Функции-генераторы имеют свойство prototype , но не могут быть вызваны с помощью new :

async function* asyncGeneratorFunction() {}
function* generatorFunction() {}

prototype функций-генераторов используется, когда они вызываются без new . Свойство prototype станет прототипом возвращаемого объекта Generator .

Следующие функции не имеют prototype и, следовательно, не подходят в качестве конструкторов, даже если свойство prototype позже назначается вручную:

const method = { foo() {} }.foo;
const arrowFunction = () => {};
const boundFunction = (function () {}).bind(null);
async function asyncFunction() {}

Ниже приведены допустимые конструкторы, имеющие prototype :

class Class {}
function fn() {}

prototype функции по умолчанию представляет собой простой объект с одним свойством: constructor , который является ссылкой на саму функцию. Он доступен для записи, неперечислим и настраивается.

Examples

Изменение прототипа всех экземпляров путем изменения свойства prototype

function Ctor() {}
const p1 = new Ctor();
const p2 = new Ctor();
Ctor.prototype.prop = 1;
console.log(p1.prop); 
console.log(p2.prop); 

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

Поля класса добавляют свойства к каждому экземпляру. Методы класса объявляют свойства функций в прототипе. Однако нет возможности добавить в прототип нефункциональное свойство. Если вы хотите разделить статические данные между всеми экземплярами (например, Error.prototype.name одинаково для всех экземпляров ошибок), вы можете вручную назначить его prototype класса.

class Dog {
  constructor(name) {
    this.name = name;
  }
}

Dog.prototype.species = "dog";

console.log(new Dog("Jack").species); 

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

class Dog {
  static {
    Dog.prototype.species = "dog";
  }
  constructor(name) {
    this.name = name;
  }
}

console.log(new Dog("Jack").species); 

Specifications

See also

  • Function
  • Наследование и цепочка прототипов


JavaScript

  • Function.prototype.length

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

  • Function.prototype.name

    Свойство имени объекта Function, доступное только для чтения, указывает функцию, указанную при ее создании, или может быть либо анонимной пустой строкой, либо анонимно.

  • Function.prototype.toString()

    Метод toString()возвращает представляющий исходный код указанной Функции.

  • Generator

    Объект Generator возвращается функцией и соответствует протоколу iterable протокола iterator Конструктор Generator недоступен глобально.

Понравилась статья? Поделить с друзьями:
  • Как написать протокол совещания при директоре школы
  • Как написать прощальные слова мужчине
  • Как написать прощальную записку
  • Как написать прощальное сообщение подруге
  • Как написать прощальное сообщение мужчине