Форум программистов, компьютерный форум CyberForum.ru

С++ для начинающих

Войти
Регистрация
Восстановить пароль
 
 
Рейтинг: Рейтинг темы: голосов - 22, средняя оценка - 4.82
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
#1

Как правильно из одного cpp подключить другой - C++

06.10.2012, 00:02. Просмотров 3192. Ответов 34
Метки нет (Все метки)

Есть 2 .cpp файла: a.cpp и b.cpp. Когда пишу:

C++
1
2
/* a.cpp */
#include "b.cpp"
Выдает ошибку. Как правильно из одного cpp подключить другой?
После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
07.10.2012, 21:50     Как правильно из одного cpp подключить другой #21
Сообщение было отмечено автором темы, экспертом или модератором как ответ
func.h
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Следующая конструкция ifndef-define-endif имеет название "страж включения".
// Она нужна для предотвращения множественного включения одного и того же файла
// в другой файл. Суть работы следующая: при первом включении файла с помощью
// define символ FUNC_H (или любой другой, главное, чтобы он был по возможности
// уникальным и совпадал в ifndef и define) ещё не определён; препроцессор
// определяет этот символ и включает весь текст, который находится между ifndef
// и endif. При повторном включении файла символ FUNC_H будет ужё определён, и
// препроцессор уже не будет включать текст, находяцийся между ifndef и endif
#ifndef FUNC_H
#define FUNC_H
 
// Прототип функции. Говорит компилятору, что эту функцию можно вызвать с двумя
// аргументами типов double и char соответственно (или совместимых с ними), а
// также, возможно, присвоить её результат переменной типа int (или совместимого
// с ним)
int func(double d, char c);
 
#endif // FUNC_H
func.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// В данном случае подключение этого заголовочного файла не обязательно,
// поскольку ничего нового компилятору он не скажет. В общем случае, когда в
// заголовочном файле определяется больше информации (например, какие-то
// пользовательские типы и т.д.), которая используется в данном файле
// реализации, подключить этот файл необходимо. Я обычно всегда подключаю
// заголовочный файл в файле реалиации, это позволяет связать эти два файла в
// единый модуль (позволяет связать программисту, а не компилятору)
#include "func.h"
 
// Здесь всё стандартно, подключаем заголовочные файлы, функции из которых будем
// использовать в файле реализации
#include <iostream>
 
// Реализация функции, объявленной в заголовочном файле
int func(double d, char c)
{
    // Выводим на экран значения переданных в функцию аргументов
    std::cout << "double d = " << d << "; char c = " << c << std::endl;
    
    // Возвращаем из функции ASCII-код второго аргумента; приведение типа
    // здесь необязательно, но явное лучше, чем неявное
    return static_cast<int>(c);
}
main.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Файл, в котором используется функция, объявленная в func.h и реализованная в
// func.cpp
#include <iostream>
 
// Смысл включения здесь этого файла в следующем: когда компилятор встрети вызов
// func, ему не обязательно знать, как эта функция реализована; нужно только
// знать, аргументы каких типов можно в неё передавать и значение какого типа
// она возвращает. В этом ему поможет прототип функции.
// По факту препроцессор, который вызывается ещё ДО компиляции, просто подставит
// вместо этого оператора всё, что в файле func.h располагается между ifndef
// и endif. Т.е. вместо
// 
// #include "func.h"
// 
// будет текстово подставлено
//
// #define FUNC_H
//
// int func(double d, char c);
// 
#include "func.h"
 
int main()
{
    double d = 3.14;
    char c = 'X';
    
    // Здесь происходит вызов функции func. При этом тело этой функции
    // компилятору видеть ненужно, ведь на стадии компиляции ещё нет никакого
    // вызова функции, есть только код, который этот вызов осуществляет. Здесь
    // компилятор проверит (на основании прототипа, который уже был текстово
    // включён в данный файл препроцессором), подходят ли типы аргументов,
    // передаваемых в функцию (если необходимо преобразование и компиилятор
    // может выполнить его автоматически, перед вызовом будет подставлен код,
    // выполняющий преобразование), а также можно ли результат функции присвоить
    // переменной того типа, которой он присваивается (опять же, если необходимо
    // преобразование - оно будет по возможности выполнено перед сохранение
    // результата в соответствующую переменную)
    int result = func(d, c);
    
    std::cout << result << std::endl;
    
    return 0;
}
Компиляция:
Bash
1
2
3
g++ -ansi -Wall -c -o func.o func.cpp
g++ -ansi -Wall -c -o main.o main.cpp
g++ -o main func.o main.o
Ключ -ansi говорит компилятору не делать никаких поблажек и строжайшим образом отыскивать все несоответствия стандарту. Ключ -Wall запрещает подавлять какие-либо, даже несущественные, предупреждения, и сообщать обо всём, что он посчитал хоть сколько нибудь неверным. Можно было бы обойтись и без них, но я так привык.
Клюс -c говорит, что необходимо остановится сразу после генерации объектного кода, а не вызывать линкёр и генерировать бинарный файл.
В первой строке отдельно компилируется файл func.cpp, на выходе будет объектный файл func.o. Здесь компилятору неважно, что эта функция где-то вызывается. Ему вообще не важно, вызывается она или нет. Его просят откомпилировать исходный файл в объектный, и он это делает.
Во второй строке отдельно компилируется файл main.cpp, на выходе объектный файл main.o. Компилятор при встрече вызова функции, определения которой не видит, вместо кода вызова этой функции (в котором фигурирует адрес, по которому она располагается в исполняемом файле) поставит заглушку, содержащую всю необходимую информацию, по которой линкёр сможет заглушку эту изменить на реальный адрес вызываемой функции (важно, чтобы он не ошибся и не подставил туда какой-то другой адрес, поэтому и нужна информативная заглушка). Всё, больше компилятору ничего не нужно, он не хочет видеть здесь тело функции, а возлагает поиск этого тела на линкёр.
В третьей строке, хотя и вызывается компилятор g++, на самом деле он сразу же вызовет линкёр (утилита ld), который сделает следующее (в неформальном виде, только для общего понимания процесса): отыщет все заглушки, поставленные компилятором, расшифрует информацию, которая в них хранится, найдёт в других объектных файлах реализации функций, соответствующих заглушкам, и заменит заглушки на код вызовов этих функций. В случае, если реализации какой-либо функции найдено не будет - линкёр выдаст ошибку о неразрешённой внешней ссылке. В случае множественного определения также будет выдана ошибка. Именно из-за всего, написанного выше, линкёру и нужно передавать ВСЕ объектные модули, которые ранее компилировались по-отдельности, ведь линкёр должен располагать всеми адресами всех используемых функций.
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
07.10.2012, 21:53  [ТС]     Как правильно из одного cpp подключить другой #22
alsav22, хм, вы правы. Попробовал - почти все прояснилось. Единственное:

Я правильно понимаю, что если есть некий заголовочный файл, в нем прототипы функций, то компилятор полезет в точно так же названный cpp файл за этими функциями?
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
07.10.2012, 22:03     Как правильно из одного cpp подключить другой #23
Цитата Сообщение от Kgfq Посмотреть сообщение
что если есть некий заголовочный файл, в нем прототипы функций, то компилятор полезет в точно так же названный cpp файл за этими функциями?
Нет, так он делать не будет. Прочтите моё сообщение, там есть очень важный вывод (в явном виде, правда, не оформленный): во-первых, вызов компилятора заключается в неявном вызове кучи различных вспомогательных утилит (препроцессор, лексический анализатор, синтаксический анализатор, редактор связей и много чего ещё). Во-вторых, важно понимать, что все эти утилиты распределяют обязанности между собой. Компилятор не занимается линковкой, а линкёр - компиляцией. У каждого свои задачи, требующие своей входной информации.
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
07.10.2012, 22:03  [ТС]     Как правильно из одного cpp подключить другой #24
silent_1991, хм. Не совсем понимаю смысл создавать отдельный cpp, если потом в него включается .h. Ведь есть в хедере изменить хоть 1 несущественный символ, компилировать придется заново. Разве это не сводит всю прелесть на нет?

Так же, не понимаю, разве двойное включение <iostream> не сделает хуже?
ведь оба файла компилируются отдельно. И получается в обоих .o будет код из iostream
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
07.10.2012, 22:06     Как правильно из одного cpp подключить другой #25
Цитата Сообщение от Kgfq Посмотреть сообщение
Ведь есть в хедере изменить хоть 1 несущественный символ, компилировать придется заново
Теоретически да. Практически же реализацию редактировать придётся куда чаще, чем объявление. Хедер будет редактироваться только в связи с какими-либо значительными изменениями (введение нового пользовательского типа, новой функции, изменение тела класса и т.д.), который в любом случае повлекут за собой перекомпиляцию реализации, в которой также необходимо отслеживать эти изменения и поддерживать их в актуальном состоянии.
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
07.10.2012, 22:09  [ТС]     Как правильно из одного cpp подключить другой #26
silent_1991, а как линкер поймет, из какого файла брать реализацию?
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
07.10.2012, 22:11     Как правильно из одного cpp подключить другой #27
Kgfq, ну ведь на момент вызова у него есть вся необходимая информация, все файлы с реализацией ему предоставлены. Здесь в ход вступает такая вещь, как таблица символов, которая является, по сути, индексом всего содержимого объектного файла. Если линкёр находит в таблице символов символ, соответствующий разрешаемой им в данный момент заглушке, он берёт из этой таблицы смещение и подставляет его вместо заглушки.
Возможно, я несколько не точен в деталях реализации, но концепция такая.
alsav22
5416 / 4812 / 442
Регистрация: 04.06.2011
Сообщений: 13,587
07.10.2012, 22:13     Как правильно из одного cpp подключить другой #28
Цитата Сообщение от Kgfq Посмотреть сообщение
Ведь есть в хедере изменить хоть 1 несущественный символ, компилировать придется заново
Какая разница, в данном случае, что куда подключено? Или вы хотите вносить изменения в программу, и чтобы потом ничего не перекомпилировалось? И как вы себе это представляете?
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
07.10.2012, 22:22  [ТС]     Как правильно из одного cpp подключить другой #29
alsav22, как я себе представлял это - реализация должна абстрагироваться от остального кода.
Какая разница, какие классы есть в коде, когда этой функции нужно подать определенное значение и она должна его как-то изменить.
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
07.10.2012, 22:32     Как правильно из одного cpp подключить другой #30
Kgfq, так она и абстрагирована. Возьмите мой пример. В main.cpp абсолютно ничего неизвестно о реализации func. Всё это дело склеивается в единый монолит на самой последней стадии - стадии линковки.
alsav22
5416 / 4812 / 442
Регистрация: 04.06.2011
Сообщений: 13,587
07.10.2012, 22:50     Как правильно из одного cpp подключить другой #31
Цитата Сообщение от Kgfq Посмотреть сообщение
alsav22, как я себе представлял это - реализация должна абстрагироваться от остального кода.
И хотите .cpp с реализацией подключать к другим файлам. Где логика?
Kgfq
74 / 37 / 2
Регистрация: 23.09.2012
Сообщений: 408
07.10.2012, 23:05  [ТС]     Как правильно из одного cpp подключить другой #32
silent_1991,
alsav22,

Спасибо за подробные объяснения, я разобрался. Единственное, еще вопрос, когда стоит начинать задумываться о разделении прототипа и реализации?
alsav22
5416 / 4812 / 442
Регистрация: 04.06.2011
Сообщений: 13,587
07.10.2012, 23:14     Как правильно из одного cpp подключить другой #33
Цитата Сообщение от Kgfq Посмотреть сообщение
когда стоит начинать задумываться о разделении прототипа и реализации?
К правильному, желательно, сразу привыкать. Неверное, есть смысл помещать прототипы отдельно, в заголовочном файле, когда много функций и они используются во многих файлах.

Добавлено через 4 минуты
Стоит сказать ещё о шаблонах. Прототип шаблона и реализация должны быть в одном файле.
silent_1991
Эксперт С++
4958 / 3034 / 149
Регистрация: 11.11.2009
Сообщений: 7,027
Завершенные тесты: 1
08.10.2012, 01:12     Как правильно из одного cpp подключить другой #34
Kgfq, задумываться стоит сразу. Ещё почитайте о системах сборки, например, make. Именно с ними все прелести раздельной компиляции показываются наружу.
На счёт шаблонов: иными словами, шаблон - единственный случай, когда подключается не объявление, а полностью реализация. Такова особенность шаблонов.
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
08.10.2012, 04:50     Как правильно из одного cpp подключить другой
Еще ссылки по теме:
Как правильно подключить curl библиотеку? C++
Как правильно подключить свой заголовочный файл? C++
C++ Как правильно подключить шаблонный класс (VS2008)?
Как правильно подключить набор сертификатов в curl? C++
SDL 2. Как правильно прилинковать/подключить к Visual Studio? C++

Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
warchief
36 / 36 / 3
Регистрация: 09.08.2012
Сообщений: 114
08.10.2012, 04:50     Как правильно из одного cpp подключить другой #35
Цитата Сообщение от Kgfq Посмотреть сообщение
silent_1991, хм, за теорию спасибо, стало яснее. Не могли бы вы сделать проект с maim.cpp, func.h и func.cpp, что бы я наглядно увидел?
func.h
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
 
// в header пишутся только объявления
 
extern int GLobalInt;
 
void Foo(int i);
 
class FooClass
{
public:
    FooClass();
 
    void Init(int i);
 
private:
    int m_i;
};
func.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "func.h"
 
int GLobalInt = 0;
 
void Foo(int i)
{
   i += 10;
   printf("%d", i);
}
 
FooClass::FooClass()
{
   m_i = 0;
}
 
void FooClass::Init(int i)
{
   m_i = i;
   printf("%d", m_i);
}
main.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "func.h"
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   GLobalInt = 10;
   printf("%d", GLobalInt);
 
   Foo(GLobalInt);
 
   FooClass fc;
   fc.Init(GLobalInt);
 
   system("PAUSE");
   return 0;
}
Yandex
Объявления
08.10.2012, 04:50     Как правильно из одного cpp подключить другой
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru