12.3 – Спецификаторы доступа public и private

Добавлено 19 июня 2021 в 11:57

Открытые и закрытые члены

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

struct DateStruct // члены по умолчанию открытые
{
    int month; // по умолчанию открытый, доступен всем
    int day;   // по умолчанию открытый, доступен всем
    int year;  // по умолчанию открытый, доступен всем
};
 
int main()
{
    DateStruct date;
    date.month = 10;
    date.day = 14;
    date.year= 2020;
 
    return 0;
}

В этой программе мы объявляем переменную типа DateStruct, а затем напрямую обращаемся к ее членам, чтобы инициализировать их. Это работает, потому что все члены структуры по умолчанию являются открытыми. Открытые (public) члены – это члены структуры или класса, к которым можно получить доступ извне структуры или класса. В этом случае функция main() находится вне структуры, но она может напрямую обращаться к членам month, day и year, поскольку они являются открытыми.

С другой стороны, рассмотрим следующий почти идентичный класс:

class DateClass // члены по умолчанию закрытые
{
    int m_month; // закрытый по умолчанию, может быть доступен только другим членам
    int m_day;   // закрытый по умолчанию, может быть доступен только другим членам
    int m_year;  // закрытый по умолчанию, может быть доступен только другим членам
};
 
int main()
{
    DateClass date;
    date.m_month = 10;  // ошибка
    date.m_day = 14;    // ошибка
    date.m_year = 2020; // ошибка
 
    return 0;
}

Если бы вы попытались скомпилировать эту программу, вы бы получили ошибки. Это связано с тем, что по умолчанию все члены класса являются закрытыми. Закрытые (private) члены – это члены класса, к которым могут получить доступ только другие члены класса. Поскольку main() не является членом DateClass, у нее нет доступа к закрытым членам date.

Спецификаторы доступа

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

class DateClass
{
public: // обратите внимание на использование здесь ключевого
        // слова public и двоеточия
    int m_month; // открытый, доступен всем
    int m_day;   // открытый, доступен всем
    int m_year;  // открытый, доступен всем
};
 
int main()
{
    DateClass date;
    date.m_month = 10;   // ok, потому что m_month теперь открытый
    date.m_day = 14;     // ok, потому что m_day теперь открытый
    date.m_year = 2020;  // ok, потому что m_year теперь открытый
 
    return 0;
}

Поскольку члены DateClass теперь открыты, к ним можно получить доступ напрямую через main().

Ключевое слово public вместе с последующим двоеточием называется спецификатором доступа. Спецификаторы доступа определяют, у кого есть доступ к членам, которые следуют за спецификатором. Каждый из членов «получает» уровень доступа предыдущего спецификатора доступа (или, если он не указан, спецификатора доступа по умолчанию).

C++ предоставляет 3 ключевых слова для различных спецификаторов доступа: public, private и protected. public и private используются для того, чтобы сделать членов, которые следуют за ними, открытыми или закрытыми соответственно. Третий спецификатор доступа, protected, работает так же, как и private. Мы обсудим разницу между спецификаторами доступа private и protected при рассмотрении наследования.

Смешивание спецификаторов доступа

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

Как правило, переменные-члены обычно делаются закрытыми, а функции-члены обычно становятся открытыми. В следующем уроке мы подробнее рассмотрим, почему.

Правило


Делайте переменные-члены закрытыми, а функции-члены открытыми, если у вас нет веской причины поступать иначе.

Давайте посмотрим на пример класса, который использует как спецификаторы доступа, как private, так и public:

#include <iostream>
 
class DateClass // члены по умолчанию закрыты
{
    int m_month; // закрытый по умолчанию, может быть доступен только другим членам
    int m_day;   // закрытый по умолчанию, может быть доступен только другим членам
    int m_year;  // закрытый по умолчанию, может быть доступен только другим членам
 
public:
    void setDate(int month, int day, int year) // открытый, может получить доступ любой
    {
        // setDate() может получить доступ к закрытым членам класса,
        // потому что она является членом самого класса
        m_month = month;
        m_day = day;
        m_year = year;
    }
 
    void print() // открытый, может получить доступ любой
    {
        std::cout << m_month << '/' << m_day << '/' << m_year;
    }
};
 
int main()
{
    DateClass date;
    date.setDate(10, 14, 2020); // ok, потому что setDate() открытая
    date.print();               // ok, потому что print() открытая
    std::cout << '\n';
 
    return 0;
}

Эта программа печатает:

10/14/2020

Обратите внимание, что хотя мы не можем получить доступ к переменным-членам date m_month, m_day и m_year непосредственно из main (поскольку они являются закрытыми), мы можем получить к ним доступ косвенно через открытые функции-члены setDate() и print()!

Группа открытых членов класса часто называется открытым интерфейсом. Поскольку только открытые члены могут быть доступны извне класса, открытый интерфейс определяет, как программы, использующие класс, будут взаимодействовать с этим классом. Обратите внимание, что main() ограничена установкой даты и печатью даты. Класс защищает переменные-члены от прямого доступа или редактирования.

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

Контроль доступа работает на основе класса

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

#include <iostream>
 
class DateClass // члены по умолчанию закрыты
{
	int m_month; // закрытый по умолчанию, может быть доступен только другим членам
	int m_day;   // закрытый по умолчанию, может быть доступен только другим членам
	int m_year;  // закрытый по умолчанию, может быть доступен только другим членам
 
public:
	void setDate(int month, int day, int year)
	{
		m_month = month;
		m_day = day;
		m_year = year;
	}
 
	void print()
	{
		std::cout << m_month << '/' << m_day << '/' << m_year;
	}
 
	// Обратите внимание на добавление этой функции
	void copyFrom(const DateClass &d)
	{
		// Обратите внимание, что мы можем получить доступ
        // к закрытым членам d напрямую
		m_month = d.m_month;
		m_day = d.m_day;
		m_year = d.m_year;
	}
};
 
int main()
{
	DateClass date;
	date.setDate(10, 14, 2020); // ok, потому что setDate() открытая
	
	DateClass copy;
	copy.copyFrom(date); // ok, потому что copyFrom() открытая
	copy.print();
	std::cout << '\n';
 
	return 0;
}

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

В приведенном выше примере copyFrom() является членом DateClass, что дает ей доступ к закрытым членам DateClass. Это означает, что copyFrom() может напрямую обращаться не только к закрытым членам неявного объекта, с которым она работает, но также означает, что она имеет прямой доступ к закрытым членам DateClass параметра d! Если бы параметр d был другого типа, этого не было бы.

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

Снова о структурах и классах

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

Вот и всё!

(Хорошо, если быть педантичным, есть еще одно незначительное отличие – структуры наследуются от других классов открытым (public) образом, а классы наследуются закрытым (private) образом. Мы рассмотрим, что это означает, в следующей главе, но этот конкретный момент практически не имеет значения, так как в любом случае вы никогда не должны полагаться на значения по умолчанию).

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

Вопрос 1

a) Что такое открытый член?

Открытый член – это член класса, к которому может получить доступ кто угодно.

b) Что такое закрытый член?

Закрытый член - это член класса, к которому могут получить доступ только другие члены класса.

c) Что такое спецификатор доступа?

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

d) Сколько существует спецификаторов доступа, и какие они?

Три. public, private и protected.


Вопрос 2

a) Напишите простой класс с именем Point3d. Класс должен содержать:

  • три закрытых переменных-члена типа int с именами m_x, m_y и m_z;
  • открытую функцию-член с именем setValues(), которая позволяет вам устанавливать значения для m_x, m_y и m_z;
  • открытую функцию-член с именем print(), которая печатает Point3d в следующем формате: <m_x, m_y, m_z>.

Убедитесь, что следующая программа выполняется правильно:

int main()
{
    Point3d point;
    point.setValues(1, 2, 3);
 
    point.print();
    std::cout << '\n';
 
    return 0;
}

Она должна напечатать:

<1, 2, 3>

#include <iostream>
 
class Point3d
{
private:
	int m_x{};
    int m_y{};
    int m_z{};
 
public:
	void setValues(int x, int y, int z)
	{
		m_x = x;
		m_y = y;
		m_z = z;
	}
 
	void print()
	{
		std::cout << '<' << m_x << ", " << m_y << ", " << m_z << '>';
	}
};
 
int main()
{
    Point3d point;
    point.setValues(1, 2, 3);
 
    point.print();
    std::cout << '\n';
 
    return 0;
}

b) Добавьте функцию с именем isEqual() в свой класс Point3d. Следующий код должен работать правильно:

int main()
{
    Point3d point1;
    point1.setValues(1, 2, 3);
 
    Point3d point2;
    point2.setValues(1, 2, 3);
 
    if (point1.isEqual(point2))
    {
        std::cout << "point1 and point2 are equal\n";
    }
    else
    {
        std::cout << "point1 and point2 are not equal\n";
    }
 
    Point3d point3;
    point3.setValues(3, 4, 5);
 
    if (point1.isEqual(point3))
    {
        std::cout << "point1 and point3 are equal\n";
    }
    else
    {
        std::cout << "point1 and point3 are not equal\n";
    }
 
    return 0;
}

#include <iostream>
 
class Point3d
{
private:
	int m_x, m_y, m_z;
 
public:
	void setValues(int x, int y, int z)
	{
		m_x = x;
		m_y = y;
		m_z = z;
	}
 
	void print()
	{
		std::cout << '<' << m_x << ", " << m_y << ", " << m_z << '>';
	}
 
	// Мы можем здесь использовать тот факт, что контроль
    // доступа работает на основе класса, для прямого доступа
    // к закрытым членам параметра Point3d p
	bool isEqual(const Point3d &p)
	{
		return (m_x == p.m_x && m_y == p.m_y && m_z == p.m_z);
	}
};
 
int main()
{
	Point3d point1;
	point1.setValues(1, 2, 3);
 
	Point3d point2;
	point2.setValues(1, 2, 3);
 
	if (point1.isEqual(point2))
	{
		std::cout << "point1 and point2 are equal\n";
	}
	else
	{
		std::cout << "point1 and point2 are not equal\n";
	}
 
	Point3d point3;
	point3.setValues(3, 4, 5);
 
	if (point1.isEqual(point3))
	{
		std::cout << "point1 and point3 are equal\n";
	}
	else
	{
		std::cout << "point1 and point3 are not equal\n";
	}
 
	return 0;
}

Вопрос 3

А теперь давайте попробуем кое-что посложнее. Давайте с нуля напишем класс, реализующий простой стек. Если вам нужно напомнить, что такое стек, просмотрите урок «11.8 – Стек и куча».

Класс должен называться Stack и содержать:

  • закрытый фиксированный массив чисел int длиной 10.
  • закрытое число int для отслеживания размера стека.
  • открытая функция-член с именем reset(), которая устанавливает размер равным 0.
  • открытая функция-член с именем push(), которая помещает значение в стек. push() должна возвращать false, если массив уже заполнен, и true в противном случае.
  • открытая функция-член с именем pop(), которая извлекает значение из стека и возвращает его. Если в стеке нет значений, программа должна завершиться через assert.
  • открытая функция-член с именем print(), которая печатает все значения в стеке.

Убедитесь, что следующая программа выполняется правильно:

int main()
{
	Stack stack;
	stack.reset();
 
	stack.print();
 
	stack.push(5);
	stack.push(3);
	stack.push(8);
	stack.print();
 
	stack.pop();
	stack.print();
 
	stack.pop();
	stack.pop();
 
	stack.print();
 
	return 0;
}

Эта программа должна напечатать:

( )
( 5 3 8 )
( 5 3 )
( )

#include <array>
#include <cassert>
#include <iostream>
 
class Stack
{
private:
	// Мы используем std::array для хранения элементов
	using container_type = std::array<int, 10>;
	// Для удобства добавляем псевдоним типа для типа индексов
	using size_type = container_type::size_type;
 
private:
	container_type m_array; // Здесь мы собираемся хранить данные нашего стека
	size_type m_next{ 0 };  // Это будет содержать индекс следующего свободного элемента в стеке
 
public:
 
	void reset()
	{
		m_next = 0;
	}
 
	bool push(int value)
	{
		// Если стек уже заполнен, возвращаем false и выходим
		if (m_next == m_array.size())
			return false;
		
        // устанавливаем следующий свободный элемент в значение value,
        // а затем увеличиваем m_next
		m_array[m_next++] = value; 
		return true;
	}
 
	int pop()
	{
		// Если в стеке нет элементов, подтверждаем
		assert (m_next > 0 && "Can not pop empty stack");
 
		// m_next указывает на следующий свободный элемент, поэтому
        // последний действительный элемент - это m_next -1.
        // мы хотим сделать что-то вроде этого:
        // int val = m_array[m_next-1]; // получаем последний действительный элемент
        // --m_next; // m_next теперь на один меньше, так как мы только что
        //           // удалили верхний элемент
        // return val; // возвращаем элемент
        // это можно свести к следующему:
		return m_array[--m_next];
	}
 
	void print()
	{
		std::cout << "( ";
		for (size_type i{ 0 }; i < m_next; ++i)
			std::cout << m_array[i] << ' ';
		std::cout << ")\n";
	}
};
 
int main()
{
	Stack stack;
 
	stack.print();
 
	stack.push(5);
	stack.push(3);
	stack.push(8);
	stack.print();
 
	stack.pop();
	stack.print();
 
	stack.reset();
	stack.print();
 
	return 0;
}

Совет


Вместо того, чтобы писать собственную реализацию стека каждый раз, когда он вам нужен, используйте std::stack. Подобно std::array и std::vector, вы можете указать тип элемента при его создании.

#include <iostream>
#include <stack>
 
// std::stack предоставляет доступ только к самому верхнему элементу.
// Если мы хотим распечатать все элементы, нам нужно скопировать стек
// (передав его по значению) и извлекать элементы, пока стек не опустеет.
void printStack(std::stack<int> stack)
{
	std::cout << "( ";
	while (!stack.empty())
	{
		std::cout << stack.top() << ' ';
		stack.pop();
	}
	std::cout << ")\n";
}
 
int main()
{
	// Создаем std::stack, содержащий числа int.
	std::stack<int> stack{};
 
	printStack(stack);
 
	stack.push(5);
	stack.push(3);
	stack.push(8);
	printStack(stack);
 
	stack.pop();
	printStack(stack);
 
	// Чтобы очистить стек, присвойте ему пустой стек.
	stack = {};
	printStack(stack);
 
	return 0;
}

Теги

C++ / CppLearnCppprivatepublicДля начинающихКласс (программирование)ОбучениеОбъектно-ориентированное программирование (ООП)ПрограммированиеСпецификатор доступа

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.


  • 2023-02-24radioprog

    Точно? Ссылку на источник видели?

  • 2023-02-22Андрей Таранов

    прикольно все украли у ravesli. Интересно, они знают?