Опубликован: 02.02.2011 | Уровень: для всех | Доступ: свободно
Лекция 7:

Решение задач с использованием указателей

< Лекция 6 || Лекция 7: 12 || Лекция 8 >
Аннотация: В лекции рассматриваются определение, реализация одиночного и многочисленного перенаправления с помощью указателей, определение, способы объявления, разработка и вызов функций с переменным числом параметров, дается обзор типичных ошибок, связанных с некорректным использованием указателей.

Цель лекции: изучить функции с переменным числом параметров и приемы построения программ, используя указатели, научиться решать задачи с использованием функций с переменным числом параметров в языке С++.

Указатель на указатель

Указатель на указатель является формой многочисленного перенаправления или цепочки указателей ( рис. 6.1).

Представление одиночного и многочисленного перенаправлений

Рис. 6.1. Представление одиночного и многочисленного перенаправлений

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

Многочисленное перенаправление может и дальше расширяться. Но существует немного случаев, когда необходимо что-то более мощное, чем указатель на указатель. Излишнее перенаправление приводит к концептуальным ошибкам, которые очень трудно исправлять. Переменная, являющаяся указателем на указатель, должна быть описана определенным образом. В С++ это выполняется путем помещения двух звездочек перед именем. Например, следующее объявление сообщает компилятору, что nb – это указатель на указатель типа float:

float **nb;
/*nb – это не указатель на число с плавающей точкой, 
а указатель на указатель на вещественное число*/

Для получения доступа к значению, адресуемому указателем на указатель, следует применить оператор разыменования * два раза, как показано в следующем примере:

#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[]){
  int *p, x, **q;
  x = 10;
  p = &x;
  q = &p;
  printf("%d", **q); /* вывод значения х */
  system("pause");
  return 0;
}

Здесь р объявляется как указатель на целое, а q – это указатель на указатель на целое. Вызов printf() выводит число 10 на экран.

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

//цепочка указателей на указатели
#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[]){
  int i=88;
  int *pi=&i;
  int **ppi=&pi;
  int ***pppi=&ppi;
  cout << "\n***pppi = " << ***pppi;
  system("pause");
  return 0;
}

Ассоциативность унарной операции разыменования выполняется справа налево, поэтому последовательно обеспечивается доступ к участку памяти с адресом pppi, затем к участку с адресом (*pppi)== ppi, затем к (*ppi)== pi, затем к (*pi)== i. С помощью скобок последовательность разыменований можно пояснить таким выражением *(*(*pppi))).

Функции с переменным числом параметров

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

Синтаксис определения функции с переменным числом параметров:

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

где тип – тип, возвращаемого функцией значения; имяимя функции; спецификация_явных_параметровсписок спецификаций параметров, количество и типы которых фиксированы и известны в момент компиляции. Эти параметры можно назвать обязательными.

После списка обязательных параметров ставится запятая, а затем многоточие (...), которое показывает, что дальнейший контроль соответствия количества и типов параметров при обработке вызова функции производить не нужно. При обращении к функции все параметры: и обязательные, и необязательные – будут размещаться в памяти друг за другом. Следовательно, определив адрес обязательного параметра как *p=&k, где pуказатель, а k – обязательный параметр, можно получить адреса и всех остальных параметров: оператор k++; выполняет переход к следующему параметру списка. Еще одна сложность заключается в определении конца списка параметров, поэтому каждая функция с переменным числом параметров должна иметь механизм определения количества и типов параметров. Существует два подхода:

  1. известно количество параметров, которое передается как обязательный параметр (см. Пример 1 );
  2. известен признак конца списка параметров (см. Пример 2 ).

Пример 1. Найти сумму последовательности чисел, если известно количество чисел.

#include "stdafx.h"
#include <iostream>
using namespace std;
float sum(int k,...);
//явный параметр k задает количество чисел

int _tmain(int argc, _TCHAR* argv[]){
  cout << "\n4+6=" << sum(2,4,6);
  cout << "\n1+2+3+4=" << sum(4,1,2,3,4);
  system("pause");
  return 0;
}
float sum(int k, ...) {
  int *p=&k;//настроили указатель на параметр k
  float s=0;
  for(;k!=0;k--)
    s+=*(++p);
  return s;
}

Пример 2. Найти среднее арифметическое последовательности чисел, если известен признак конца списка параметров (цифра '0').

#include "stdafx.h"
#include <iostream>
using namespace std;
float arifm(int k, ...);

int _tmain(int argc, _TCHAR* argv[]){ 
  cout << "\n(4+6)/2=" << arifm(4,6,0);
  cout << "\n(1+2+3+4)/4=" << arifm(1,2,3,4,0);
  system("pause");
  return 0;
}
float arifm(int k, ...) {
  int *p=&k;//настроили указатель на параметр k
  float s=*p; //значение первого параметра присвоили s
  int i;
  for(i=1;(*p)!=0;i++) //пока нет конца списка
    s+=*(++p);
  return s/(i-1);
}

Проблемы, связанные с использованием указателей

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

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

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

// программа некорректна  
void main (){
  int x, *p;
  x = 10;
  *p = x;
}

Данная программа присваивает значение 10 некоторому неизвестному участку памяти. Указатель p не получал адреса памяти, которую можно использовать. Следовательно, он содержит неопределенное значение. Такого рода проблемы часто незаметны, когда программа невелика, поскольку p, скорее всего, содержит "безопасный" адрес, не принадлежащий коду, данным, стеку или операционной системе. По мере роста программы растет и вероятность того, что неправильное использование указателя испортит жизненно важную часть программы. Неожиданно программа может зависнуть. Направление решения таких проблем совершенно очевидно: следует убедиться, что указатель указывает на некоторую допустимую область. Хотя в таких случаях ошибки достаточно непросто обнаружить: частая инициализация указателей (или некорректная инициализация) затрудняет поиск ошибок.

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

// программа некорректна  
void main (){
  int x, *p;
  x = 10;
  p = x;
  printf("%d",*p);
}

Вызов printf() не выводит значения x, которое равно 10, на экран. В результате возникает ошибка из-за неправильного оператора присваивания

p = x;

Этот оператор присваивает значение 10 указателю р, который должен содержать адрес, а не значение. Для устранения ошибки следует написать

p = &x;

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

Ключевые термины

Многочисленное перенаправление – это адресация указателем адреса другого указателя, что в конечном итоге сводится к адресации участка памяти.

Необязательные параметры – это неизвестные на момент компиляции параметры в прототипе функции.

Обязательные параметры – это известные на момент компиляции параметры в прототипе функции.

Одиночное перенаправление – это адресация указателем некоторого участка памяти.

Указатель на указатель – это указатель, реализующий многочисленное перенаправление.

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

Краткие итоги

  1. Указатель может реализовать одиночное или многочисленное перенаправление в зависимости от его объявления.
  2. Для получения значения, адресуемого указателем при многочисленном перенаправлении, необходимо применить операцию разыменования несколько раз.
  3. В языке С++ предусмотрены объявление и вызов функций с переменным числом параметров.
  4. В прототипе функции с переменным числом параметров описываются спецификации обязательных параметров, список необязательных параметров обозначается многоточием.
  5. Для функций с переменным числом параметров существуют два основных способа контроля количества параметров в списке: через передачу количества параметров и по признаку конца списка параметров.
  6. Использование указателей в программах требует внимательно относиться к их инициализации, присваиванию значений, выполнению операций. Ошибки, связанные с указателями, относятся к трудноустранимым.
< Лекция 6 || Лекция 7: 12 || Лекция 8 >
Денис Курбатов
Денис Курбатов
Владислав Нагорный
Владислав Нагорный

Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки?

Спасибо!

Андрей Садовщиков
Андрей Садовщиков
Россия