Подставляемые (инлайн-) функции
Некоторые функции в языке Си++ можно определить с использованием специального служебного слова inline. Спецификатор inline позволяет определить функцию как встраиваемую, иначе говоря подставляемую или "открыто подставляемую", или "инлайн-функцию". Например, следующая функция определена как подставляемая:
inline float module(float x = 0, float у = 0) {
return sqrt(x * x + у * у);
}
Функция module () возвращает значение типа float, равное "расстоянию" от начала координат на плоскости до точки с координатами (х,у), определяемыми значениями фактических параметров. В теле функции вызывается библиотечная функция sqrt() для вычисления вещественного значения квадратного корня положительного аргумента. Так как подкоренное выражение в функции всегда неотрицательно, то специальных проверок не требуется. Обрабатывая каждый вызов встраиваемой функции, компилятор "пытается" подставить в текст программы код операторов ее тела. Спецификатор inline для функций, не принадлежащих классам, определяет для функций внутреннее связывание. Во всех других отношениях подставляемая функция является обычной функцией, т.е. спецификатор inline в общем случае не влияет на результаты вызова функции, она имеет обычный синтаксис определения и описания, подчиняется всем правилам контроля типов и области действия. Однако вместо команд передачи управления единственному экземпляру тела функции компилятор в каждое место вызова функции помещает соответствующим образом настроенные команды кода операторов тела функции. Тем самым при многократных вызовах подставляемой функции размеры программы могут увеличиться, однако исключаются затраты на передачи управления к вызываемой функции и возвраты из нее. Как отмечает проект стандарта Си++, кроме экономии времени при выполнении программы, подстановка функции позволяет проводить оптимизацию ее кода в контексте, окружающем вызов, что в ином случае невозможно.
Наиболее эффективно использовать подставляемые функции в тех случаях, когда тело функции состоит всего из нескольких операторов. Идеальными претендентами на определение со спецификатором inline являются несложные короткие функции. Удобны для подстановки функции, основное назначение которых - вызов других функций либо выполнение преобразований типов.
Так как компилятор встраивает код подставляемой функции вместо ее вызова, то определение функции со спецификатором inline должно находиться в том же модуле, что и обращение к ней, и размещается до первого вызова. Синтаксис языка не гарантирует обязательной подстановки кода функции для каждого вызова функции со спецификатором inline.
Проект стандарта перечисляет следующие причины, по которым функция со спецификатором inline будет трактоваться как обычная функция (не подставляемая):
• встраиваемая функция слишком велика, чтобы выполнить ее подстановку;
• встраиваемая функция рекурсивна;
• обращение к встраиваемой функции в программе размещено до ее определения;
• встраиваемая функция вызывается более одного раза в выражении;
• встраиваемая функция содержит цикл, переключатель или оператор перехода.
Еще одна особенность подставляемых функций - невозможность их изменения без перекомпиляции всех частей программы, в которых эти функции вызываются.
Функции и массивы
Формальными параметрами функции могут быть имена массивов или указателей (имя массива - это тоже указатель только константный), также указатель может быть типом возвращаемого функцией значения. Рассмотрим эти возможности.
Прежде всего следует сказать, что всё, что мы ранее узнали о передаче данных в функцию с помощью аппарата формальных и фактических параметров, а именно:
при компиляции программы проверяется строгое соответствие типов формальных и фактических параметров (если типы не совпадают и для этих типов не определено преобразование типов по умолчанию, то компилятор выдаёт ошибку);
при вызове функции формальным параметрам присваивается значение фактических параметров в строгом соответствии с их положением в списке параметров;
в точке вызова функции создаётся неименованный объект, совпадающий с типом функции, который инициализируется значением выражения, стоящего после оператора return;
выполняется и в случае, когда формальными параметрами и типом функции является указатель. Однако, как указатель по своему функциональному значению отличается от переменной, так и способы передачи/возврата данных в/из функцию с помощью указателей и переменных отличаются по своей сути. Поэтому различают способы передачи данных по значению (с помощью переменной ) и по указателю (ссылке).
Ниже приведён пример функции len(), в которой формальный параметр z является не переменной, а именем массива char [] (константным указателем). Как видно из примера первое требование о совпадении типов формального z и фактического f параметров выполнено, поэтому компилятор по поводу не совпадения типов не выдаст сообщения об ошибке. При вызове функции len(f) формальному параметру z присваивается значение фактического параметра f, т.е. z настраивается на начало символьного массива "Язык Си++", определенного в теле функции main(). Таким образом, функция len() получает через свой формальный параметр z доступ к массиву (фрагменту памяти), определённому в другом месте программы и может делать с ним всё что угодно, если при выполнении операций над этим фрагментом она не выходит за его пределы. Такой способ передачи данных в функцию и называется передачей данных по указателю.
К сожалению, нельзя определить длину массива, передаваемого через формальный параметр, применяя, например, операцию sizeof(z) к имени формального параметра. Поэтому при передаче массивов через механизм параметров возникает задача определения в теле функции количества элементов массива (длины фрагмента памяти), использованного в качестве фактического параметра. При работе со строками, т.е. с массивами типа char[], последний элемент каждого из которых имеет значение '\0', затруднений практически нет. Анализируется каждый элемент, пока не встретится символ ' \0', и это считается концом строки-массива (фрагмента памяти). В следующей программе функция len() для определения длины строки (фрагмента памяти), передаваемой в функцию с помощью параметра z, перебирает элементы массива до тех пор пока не найдёт элемент со значением ' \0':
//Программа 6.5
#include "stdafx.h"
#include <iostream>
int len(char z[]){
int m = 0;
while (z[m++]);
return m-1;
}
void main() {
char f[] = "Language C++";
std::cout<<"\nThe length of string \"Language C++\" equal "<<len(f); }
Если массив-параметр функции не есть символьная строка, то нужно либо использовать только массивы фиксированного, заранее определенного размера, либо передавать значение размера массива в функцию явным образом. Часто это делается с помощью дополнительного параметра. Следующая программа иллюстрирует эту возможность на примере функции для вычисления скалярного произведения между двумя многомерными векторами, каждый из которых представлен одномерным массивом-параметром:
//Программа 6.6
#include "stdafx.h"
#include <iostream>
#include <math.h>
float scalar(int n, float x[], float y[]){
float a = 0;
for(int i = 0; i < n; i++) a += x[i]*y[i];
return a;
}
void main(){
float E[] = { 1, 1, 1, 1, 1, 1, 1,3};
float G[] = {-1, -1, -1, -1, -1, -1, -1,3};
std::cout <<"\n (E, G) = "<< scalar (7, E, G);
getchar();
}
Результат выполнения программы: (E, G) = -7
Так как имя массива есть указатель, связанный с началом массива, то любой массив, используемый в качестве параметра, как уже говорилось ранее, может быть изменен за счет выполнения операторов тела функции. Например, в следующей программе функция max_vect() формирует массив z, каждый элемент которого равен максимальному из соответствующих значений двух других массивов-параметров (х и у):
//Программа 6.7
#include "stdafx.h"
#include <iostream>
void max_vect(int n, int *x, int *y, int* z){
for(int i =0;i<n; i++) z[i] = x[i] > y[i] ? x[i]: y[i];
}
void main(){
int a[] = { 1, 2, 3, 4, 5, 6, 7};
int b[] = { 7, 6, 5, 4, 3, 2, 1};
int c[7];
max_vect(7,a,b,c);
for (int i = 0; i < 7; i++)
std:: cout <<"\t"<< c[i];
getchar();
}
Результат выполнения программы: 7654567
Как и в функции scalar(), параметр int n служит для определения размеров массивов-параметров.
Рассмотрим случай, когда типом функции является указатель. В качестве такой функции, возвращающей указатель на массив, рассмотрим функцию fusion(), формирующую новый динамический массив на основании двух целочисленных массивов. Новый массив должен включать все элементы двух исходных массивов.
//Программа 6.8
#include "stdafx.h"
#include <iostream>
int *fusion(int n, int* a, int m, int* b){
int* f = new int[n + m]; /* Создание динамического массива*/
int ia = 0, ib = 0, ix = 0, i;
for (i = 0; i < n; i++) f[i] = a[i];
for ( ; i < n+m; i++) f[i] = b[i-n];
return f;
}
void main(void) {
int c[] = { 1, 3, 5, 7, 9 };
int d[] = { 0, 2, 4, 5 };
int *h; // Указатель для массива с результатом
int kc=sizeof(c)/sizeof(c[0]);/*Количество элементов в с[]*/
int kd = sizeof(d)/sizeof(d[0]);/*Количество элементов в d[]*/
h = fusion(kc, c, kd, d);
std::cout<< "\nresult of integration of arrays:\n";
for (int i = 0; i < kc + kd; i++) std::cout << " " << h[i];
delete [] h; //Освобождение динамической памяти
getchar();
}
Результат выполнения программы:
Результат объединения массивов:
1 3 5 7 9 0 2 4 5
На примере функции fusion() подробно рассмотрим, что происходит, если типом функции является указатель.
При выполнении тела функции fusion() оператор new динамический выделяет фрагмент памяти (массив) размером (n+m)*sizeof(int) байт и возвращает адрес начала этого фрагмента в памяти. Этот адрес запоминается в локальном указателе f. Далее обращаясь к элементам динамического массива с помощью f[], мы присваиваем им необходимые нам значения. При выполнении оператора return и выходе из функции fusion() в точке вызова создаётся неименованный объект типа int*, который инициализируется значением хранящимся в f (адресом начала динамического массива в памяти), после чего все локальные (но не динамические) объекты функции fusion() (n, a, m, b, f, ia, ib, ix, i) уничтожаются. Затем значение неименованного объекта типа int* (адрес начала динамического массива) присваивается указателю h.
Таким образом, функция fusion() через указатель вернула результат своей работы в вызывающую функцию main(). Эти результаты h[i] мы печатаем на экране. Если динамический массив нам уже больше не нужен, то мы можем его уничтожить, как показано в программе с помощью оператора delete [] h.
Большой ошибкой была бы попытка использовать не динамический, а статический локальный массив, т.е. вместо строки программы int* f = new int[n + m] написать строку int f[100]. Потому что локальный массив после выхода из функции fusion() в отличие от динамического будет уничтожен и попытка обратиться к нему из main() c помощью h[i] приведёт к зависанию программы. К сожалению, во время компиляции такие ошибки не обнаруживаются.
Это правило можно обобщить сформулировав его так: указатель возвращаемый функцией не должен быть настроен на локальный объект определённый в теле этой же самой функции. Указатель может быть настроен на динамический объект, на глобальный объект или на локальный объект, определённый вне этой функции, доступ к которому был получен через формальный параметр, и соответственно имеющий большую продолжительность существования, чем локальные объекты этой функции.
Как уже говорилось, особенность и в некотором смысле недостаток языка Си++ (и его предшественника языка Си) - несамоопределенность массивов, под которой понимается невозможность по имени массива (по указателю на массив) определять его размерность и размеры по каждому измерению. Несамоопределенность массивов затрудняет их использование в качестве параметров функций. Действительно, простейшая функция - транспонирование квадратной матрицы требует, чтобы ей были известны не только имя массива, содержащего элементы матрицы, но и размеры этой матрицы. Если такая функция транспонирования матрицы для связи по данным использует аппарат параметров, то в число параметров должны войти указатель массива с элементами матрицы и целочисленный параметр, определяющий размеры матрицы. Однако здесь возникают затруднения, связанные с одним из принципов языков Си и Си++. По определению, многомерные массивы как таковые не существуют. Если мы описываем массив с несколькими индексами, например, так:
double prim[6][4][2];
то мы описываем не трехмерный массив, а одномерный массив с именем prim, включающий шесть элементов, каждый из которых имеет тип double[4] [2]. В свою очередь, каждый из этих элементов есть одномерный массив из четырех элементов типа double [2]. И, наконец, каждый из этих элементов является массивом из двух элементов типа double.
Мы не случайно так подробно еще раз остановились на особенностях синтаксиса и представления многомерных массивов. Дело в том, что эти тонкости не бросаются в глаза при обычном определении массива, когда его размеры (и размерность) фиксированы и явно заданы в определении. Однако при необходимости передать с помощью параметра в функцию многомерный массив начинаются неудобства и неприятности. Вернемся к функции транспонирования матрицы.
Наивное, неверное и очевидное решение - определить заголовок функции таким образом:
void transponir(double x[][], int n).
Здесь n - предполагаемый порядок квадратной матрицы; double х [ ][ ] - попытка определить двухмерный массив с заранее неизвестными размерами. На такую попытку транслятор отвечает гневным сообщением:
Error ...: Size of type is unknown or zero
И он прав - при описании массива (и при спецификации массива-параметра) неопределенным может быть только первый (самый левый) размер. Вспомним - массив всегда одномерный, а его элементы должны иметь известную и фиксированную длину. В массиве х [][] не только неизвестно количество элементов одномерного массива, но и ничего не сказано о размерах этих элементов.
Примитивнейшее разрешение проблемы иллюстрирует следующая программа:
//Программа 6.9
#include "stdafx.h"
#include <iostream>
void transp(int n, float d[][3]){
float r;
int i, j;
for ( i = 0; i < n - 1; i++)
for( j = i + 1; j < n; j++){
r = d[i][j];
d[i] [j] = d[j][i];
d[j][i] = r;
}
}
void main(){
float x[3][3] = { 0, 1, 1, 2, 0, 1, 2, 2, 0 };
int n = 3;
transp(3,x);
for (int i = 0; i < n; i++) {
std::cout <<"\n Cтрока " <<(i+1) <<":";
for (int j = 0; j < n; j++) std::cout<< "\t" << x[i][j];
}
getchar();
}
Результат выполнения программы:
Строка 1: 0 2 2
Строка 2: 1 0 2
Строка 3: 1 1 0
Примитивность и нежизненность продемонстрированного решения состоят в том, что в функции transp () массив-параметр специфицирован с фиксированным вторым размером, т.е. транспонируемая квадратная матрица может быть только с размерами 3х3.
Указанные ограничения, на возможность применения многомерных массивов в качестве параметров, можно обойти несколькими путями. Первый путь - подмена многомерного массива одномерным и имитация внутри функции доступа к многомерному массиву. (Здесь будут полезны макроопределения с индексами в качестве параметров.) Второй путь - использование вспомогательных массивов указателей на массивы. Третий путь предусматривает применение классов для представления многомерных массивов. Об этом будет сказано позже.
Подробно остановимся только на представлении и передаче матриц (двухмерных массивов) с использованием вспомогательных массивов указателей на одномерные массивы. Одномерные массивы служат в этом случае для представления строк матриц. Так как, и вспомогательный массив указателей, и массивы-строки матрицы являются одномерными, то их размеры могут быть опущены в соответствующих спецификациях формальных параметров. Тем самым появляется возможность обработки в теле функции двухмерных, а в более общем случае и многомерных массивов с изменяющимися размерами. Конкретные значения размеров должны передаваться в тело функции либо с помощью дополнительных параметров, либо с использованием глобальных (внешних) переменных.
Следующая программа иллюстрирует один из способов передачи в функцию информации о двухмерном массиве, размеры которого заранее неизвестны. Функция trans() выполняет транспонирование квадратной матрицы, определенной вне тела функции в виде двухмерного массива. Параметры функции: int n - порядок матрицы; double *p[] - массив указателей на одномерные массивы элементов типа double. В теле функции обращение к элементам обрабатываемого массива осуществляется с помощью двойного индексирования. Здесь p[i] - указатель на одномерный массив (на строку матрицы с элементами типа double), p[i][j] - обращение к конкретному элементу двухмерного массива. Текст программы:
//Программа 6.10
#include "stdafx.h"
#include <iostream>
void trans(int n, double *p[]){
double x;
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++) {
x = p[i][j];
p[i][j] = p[j][i];
p[j][i] = x;
}
}
void main(){
double A[4][4]={ 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34, 41, 42, 43, 44 };
double *ptr[] = { (double *)&A[0], (double *)&A[1],
(double *)&A[2], (double *)&A[3] };
int n = 4;
trans(n, ptr);
for (int i = 0; i < n; i++){
std::cout <<"\n row " << (i+1) <<":";
for (int j =0; j < n; j++) std::cout<< "\t" <<A[i][j];
}
getchar();
}
Результаты выполнения программы:
строка 1: 11 21 31 41
строка 2: 12 22 32 42
строка 3: 13 23 33 43
строка 4: 14 24 34 44
В основной программе матрица представлена двухмерным массивом с фиксированными размерами double A[4][4]. Такой массив нельзя непосредственно использовать в качестве фактического параметра вместо формального параметра со спецификацией double *р[], поэтому вводится дополнительный вспомогательный массив указателей double *ptr [ ]. В качестве начальных значений элементам этого массива присваиваются адреса строк матрицы, т.е. &А[0], &A[l], &А[2], &А[3], преобразованные к типу double*. Дальнейшее очевидно из текста программы.
В следующей программе матрица формируется в основной программе как совокупность одномерных динамических массивов строк матрицы и динамического массива указателей на эти массивы-строки. Элементы массива указателей имеют тип int*, с массивом указателей в целом связывается указатель int **pi. Для простоты опущены проверки правильности выделения памяти и выбрано фиксированное значение (n==3) порядка матрицы. Функция fill() присваивает элементам квадратной матрицы значения "подряд": а[0][0] = 0, а[0][1] =1 . . . и т.д., т.е. a[i] [j] = (i * n) + j, где n - порядок матрицы. В иллюстративных целях указатель mat на массив указателей на строки матрицы специфицирован в заголовке без использования квадратных скобок: int** mat. Первый из параметров функции fill() со спецификацией int n определяет размеры квадратной матрицы. Текст программы:
//Программа 6.11
#include "stdafx.h"
#include <iostream>
void fill(int n, int** mat){
int k = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) mat[i][j] = k++;
}
void main() {
int **pi;
int m = 3, i;
pi = new int* [m];
for (i = 0; i < m; i++) pi[i] = new int [m];
fill(m, pi);
for (i = 0; i < m; i++) {
std::cout<< "\n строка "<< (i+1)<<":";
for (int j = 0; j < m; j++) std::cout<< "\t"<< pi[i][j];
}
for (i = 0; i < m; i++) delete[] pi[i];
delete [] pi;
getchar();
}
Результаты выполнения программы:
строка 1: 0 1 2
строка 2: 3 4 5
строка 3: 6 7 8
Так как матрица создается как набор динамических массивов, то в конце программы помещены операторы delete для освобождения памяти.
Многомерный массив с переменными размерами, сформированный в функции, непосредственно невозможно вернуть в вызывающую программу как результат выполнения функции. Однако возвращаемым функцией значением может быть указатель на одномерный массив указателей на одномерные массивы с элементами известной размерности и заданного типа. В следующей программе функция single_matr() возвращает именно такой указатель, так как имеет тип int **. В тексте функции формируется набор одномерных массивов с элементами типа int и создается массив указателей на эти одномерные массивы. Количество одномерных массивов и их длины определяются значением параметра функции, специфицированного как int n. Совокупность создаваемых динамических массивов представляет квадратную матрицу порядка n. Диагональным элементам матрицы присваиваются единичные значения, остальным - нулевые, т.е. матрица заполняется как единичная диагональная. Локализованный в функции single_matr() указатель int** р "настраивается" на создаваемый динамический массив указателей и используется в операторе возврата из функции как возвращаемое значение. В основной программе вводится с клавиатуры желаемое значение порядка матрицы (int n), а после ее формирования печатается результат. Текст программы:
//Программа 6.12
#include "stdafx.h"
#include <iostream>
int **single_matr(int n) { // n - нужный размер матрицы
int** p; // Вспомогательный указатель на формируемую матрицу:
p = new int* [n];/*Массив указателей на строки - одномерные массивы:*/
if (p == NULL){
std::cout <<"He создан динамический массив!";
exit(1);
}
for (int i = 0; i < n; i++){ /* Формирование строки элементов типа int: */
p[i] = new int [n];
if (p[i] == NULL){
std::cout <<"He создан динамический массив!";
exit(1);
}
for (int j = 0; j < n; j++)
if (j != i) p[i][j] = 0;
else p[i][j] = 1;
}
return p;
}
void main(){
int n; // Порядок матрицы
std::cout <<"\n Input order of matrix: ";
std::cin >>n;
int **matr, i; // Указатель для формируемой матрицы
matr = single_matr(n);// Создание единичной матрицы:
for (int i = 0; i < n; i++) {
std::cout << "\n row " << (i + 1) <<":"; /* Цикл печати элементов строки: */
for (int j = 0; j < n; j++)
std::cout <<"\t" <<matr[i][j];
}// Очистка памяти от динамических массивов:
for (i = 0; i < n; i++) delete[] matr[i];
delete[] matr;
getchar();
}
Результат выполнения программы:
Введите порядок матрицы: 4_ <Enter>
строка 1: 1 0 0 0
строка 2: 0 1 0 0
строка 3: 0 0 1 0
строка 4: 0 0 0 1
Обратите внимание на тот факт, что динамические массивы создаются в функции, вызванной из основной программы, а доступ к ним выполняется в тексте основной программы. Здесь же освобождается память от динамических массивов перед окончанием программы.
Дата добавления: 2020-12-11; просмотров: 375;