Как вынести класс в отдельный файл c

Обновлено: 01.07.2024

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

Разделяемые классы

Существует несколько ситуаций, когда желательно разделение определения класса.

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

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

Ключевое слово partial указывает, что другие части класса, структуры или интерфейса могут быть определены в пространстве имен. Все части должны использовать ключевое слово partial . Для формирования окончательного типа все части должны быть доступны во время компиляции. Все части должны иметь одинаковые модификаторы доступа, например public , private и т. д.

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

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

Модификатор partial недоступен в объявлениях делегатов или перечислений.

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

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

Они эквивалентны следующим объявлениям:

Следующие элементы объединяются из всех определений разделяемого типа:

  • XML-комментарии
  • интерфейсы
  • атрибуты параметров универсального параметра
  • атрибуты классов
  • члены

В качестве примера рассмотрим следующие объявления:

Они эквивалентны следующим объявлениям:

Ограничения

Имеется несколько правил, которые необходимо выполнять при работе с определениями разделяемого класса.

Дополнительные сведения см. в разделе Ограничения параметров типа.

Примеры

В следующем примере поля и конструктор класса Coords объявлены в одном определении разделяемого класса, а член PrintCoords — в другом определении разделяемого класса.

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

Разделяемые методы

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

  • У него нет модификаторов доступа (включая private по умолчанию).
  • Он возвращает значение void.
  • У него нет параметров out.
  • У него нет ни одного из следующих модификаторов: virtual, override, sealed, new или extern.

Любой метод, не соответствующий всем этим ограничениям (например, метод public virtual partial void ), должен предоставлять реализацию. Эта реализация может быть предоставлена генератором источника.

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

  • Код шаблона: шаблон резервирует имя и подпись метода, чтобы созданный код мог вызвать метод. Эти методы придерживаются ограничений, которые позволяют разработчику решить, следует ли реализовать этот метод. Если метод не реализован, то компилятор удаляет сигнатуру метода и все вызовы этого метода. Вызовы метода, включая любые результаты, которые могли бы произойти от оценки аргументов в вызовах, не имеют эффекта во время выполнения. Таким образом, любой код в разделяемом классе может свободно использовать разделяемый метод, даже если реализация не предоставлена. Во время компиляции и выполнения программы не возникнут никакие ошибки, если метод будет вызван, но не реализован.
  • Генераторы источников: генераторы источников предоставляют реализацию методов. Разработчик может добавить объявление метода (часто с атрибутами, считанными генератором источника). Разработчик может написать код, который вызывает эти методы. Генератор источника выполняется во время компиляции и обеспечивает реализацию. В этом сценарии не соблюдаются ограничения для разделяемых методов, которые не могут реализовываться часто.
  • Объявления разделяемого метода должны начинаться с контекстного ключевого слова partial.
  • Сигнатуры разделяемого метода в обеих частях разделяемого типа должны совпадать.
  • Разделяемые методы могут иметь модификаторы static и unsafe.
  • Разделяемые методы могут быть универсальными. Ограничения налагаются на ту часть объявления разделяемого метода, где находится определение, и могут дополнительно повторяться в разделе реализации. Имена параметров и типов параметров необязательно должны совпадать в объявлении реализации и в объявлении определения.
  • Можно использовать делегат в качестве определенного и реализованного разделяемого метода, но его нельзя использовать в качестве разделяемого метода, который только определен.

На этом уроке мы рассмотрим работу классов с заголовочными файлами в языке С++.

Отделение объявления от реализации

Все классы, которые мы использовали до сих пор, были достаточно простыми, поэтому мы записывали методы непосредственно внутри тела классов, например:

К счастью, язык C++ предоставляет способ отделить «объявление» от «реализации». Это делается путем определения методов вне тела самого класса. Для этого просто определите методы класса, как если бы они были обычными функциями, но в качестве префикса добавьте к имени функции имя класса с оператором разрешения области видимости ( :: ).

Вот наш класс Date с конструктором Date() и методом setDate(), определенными вне тела класса. Обратите внимание, прототипы этих функций все еще находятся внутри тела класса, но их фактическая реализация находится за его пределами:

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

Вот еще один пример класса с конструктором, определенным извне, со списком инициализации членов:

Классы и заголовочные файлы

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

Вот наш класс Date, но уже разбитый на файлы .cpp и .h:

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

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

Параметры по умолчанию

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

Библиотеки

Разделение объявления класса и его реализации очень распространено в библиотеках, которые используются для расширения возможностей вашей программы. Вы также подключали такие заголовочные файлы из Стандартной библиотеки С++, как iostream, string, vector, array и другие. Обратите внимание, вы не добавляли iostream.cpp, string.cpp, vector.cpp или array.cpp в ваши проекты. Ваша программа нуждается только в объявлениях из заголовочных файлов, чтобы компилятор смог проверить корректность вашего кода в соответствии с правилами синтаксиса языка C++. Однако реализации классов, находящихся в Стандартной библиотеке С++, содержатся в предварительно скомпилированном файле, который добавляется на этапе линкинга. Вы нигде не встречаете этот код.

Вне программ с открытым исходным кодом (где предоставляются оба файла: .h и .cpp), большинство сторонних библиотек предоставляют только заголовочные файлы вместе с предварительно скомпилированным файлом библиотеки. На это есть несколько причин:

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

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

Заключение

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

Во-первых, как упоминалось выше, это приведет к загромождению определения вашего класса.

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

Поэтому рекомендуется следующее:

Классы, используемые только в одном файле, и которые повторно не используются, определяйте непосредственно в файле .cpp, где они используются.

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

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

Нетривиальные методы определяйте в файле .cpp с тем же именем, что у класса.

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

Я начинаю с программирования на C. В настоящее время у меня большой файл, содержащий множество функций. Я хотел бы переместить эти функции в отдельный файл, чтобы код был легче читать. Тем не менее, я не могу понять, как правильно включать/компилировать и не могу найти пример в любом онлайн-учебнике, которое я нашел. Здесь упрощенный пример:

Как вы перемещаете функции C в отдельный файл? FYI: Я использую gcc.

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

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

Как я могу обойти эту проблему?

ОТВЕТЫ

Ответ 1

Ваш файл main.c

Ваш файл functions.h

Ваш файл functions.c

Скомпилируйте это с:

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

Вы можете сделать два файла: один с основной функцией, а другой с остальными.

Если ваша функция в другом файле использует глобальную переменную из другого модуля/файла, вам нужно добавить "extern" перед объявлением глобальной переменной в модуле вместе с определением функции. Это говорит компилятору, что вы обращаетесь к глобальной переменной из другого файла.

Ниже приведен пример, в котором определяется глобальная переменная "x" и вызывается функция "myfunction" в файле main.c. "Myfunction" определяется в файле functions.c. "X" также необходимо быть объявленным в файле functions.c, но так как он будет обращаться к "x" из другого файла, перед ним должен быть "extern". "myfunction" принимает внешнюю глобальную переменную "x", увеличивая ее на один и распечатывает его. Рассмотрим следующий код:

main.c будет выглядеть примерно так:

functions.c будет выглядеть примерно так:

Обратите внимание, что main.c использует "int x;" объявить x, тогда как functions.c использует "extern int x" для объявления x.

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

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

Значение x в myfunction равно: 2 Значение x в main равно: 2

Обратите внимание, что значение "x" также изменяется в основном, так как "x" является глобальной переменной.

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

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

Основные понятия

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

У каждого студента есть имя — name и фамилия last_name . Также, у него есть промежуточные оценки за весь семестр. Эти оценки мы будем записывать в целочисленный массив из пяти элементов. После того, как все пять оценок будут проставлены, определим средний балл успеваемости студента за весь семестр — свойство average_ball .

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

  • Методы класса — это его функции.
  • Свойства класса — его переменные.

Функция calculate_average_ball() просто делит сумму всех промежуточных оценок на их количество.

Модификаторы доступа public и private

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

Закрытые данные класса размещаются после модификатора доступа private . Если отсутствует модификатор public , то все функции и переменные, по умолчанию являются закрытыми (как в первом примере).

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

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

Функция set_average_ball() принимает средний балл в качестве параметра и присваивает его значение закрытой переменной average_ball . Функция get_average_ball() просто возвращает значение этой переменной.

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

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

Мы добавили в наш класс новые методы, а также сделали приватными все его свойства. Функция set_name() сохраняет имя студента в переменной name , а get_name() возвращает значение этой переменной. Принцип работы функций set_last_name() и get_last_name() аналогичен.

Функция set_scores() принимает массив с промежуточными оценками и сохраняет их в приватную переменную int scores[5] .

Теперь создайте файл main.cpp со следующим содержимым.

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

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

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

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

Скомпилируйте и запустите программу.

Отделение данных от логики

Вынесем реализацию всех методов класса в отдельный файл students.cpp.

А в заголовочном файле students.h оставим только прототипы этих методов.

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

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

В начале обучения мы говорили о пространствах имен (namespaces). Каждый класс в C++ использует свое пространство имен. Это сделано для того, чтобы избежать конфликтов при именовании переменных и функций. В файле students.cpp мы используем оператор принадлежности :: перед именем каждой функции. Это делается для того, чтобы указать компилятору, что эти функции принадлежат классу Students .

Создание объекта через указатель

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

При создании статического объекта, для доступа к его методам и свойствам, используют операция прямого обращения — « . » (символ точки). Если же память для объекта выделяется посредством указателя, то для доступа к его методам и свойствам используется оператор косвенного обращения — « -> ».

Конструктор и деструктор класса

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

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

Читайте также: