Ошибки линковщика c

nexen

187 / 180 / 25

Регистрация: 27.01.2012

Сообщений: 1,335

1

Ошибки линковщика

29.08.2012, 10:13. Показов 2535. Ответов 7

Метки нет (Все метки)


Студворк — интернет-сервис помощи студентам

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

Есть один главный хэдер, подключащий всё :

Chat Server.h

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __CHATSERVER_H_
#define __CHATSERVER_H_
 
#include "resource.h"
 
#include "Defines.h"
#include "Classes.h"
#include "GlobalVariables.h"
 
#include "User.h"
#include "Room.h"
#include "Compare.h"
#include "Server.h"
#include "WndProcs.h"
 
#endif /*__CHATSERVER_H_*/

server.h

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
45
46
47
48
49
50
51
52
#include "Chat Server.h"
 
#ifndef __SERVER_H__
#define __SERVER_H__
 
class SERVER
{
private :
    BOOL _s_status;
    HANDLE _s_thread;
 
    SOCKET _s_socket;
    sockaddr_in _s_address;
 
    set <USER*, COMPARE> _s_users;
    set <ROOM*, COMPARE> _s_rooms;
 
    list <BYTE> _s_usersID;
    list <BYTE> _s_roomsID;
 
public :
    SERVER();
    ~SERVER();
 
    BOOL    Start();
    BOOL    Accept();
    BOOL    Stop();
 
    USER*   UserNameUsed(char*);
    ROOM*   RoomNameUsed(char*);
 
    BOOL    SendToRoom(char*, ROOM*);
    BOOL    SendToAll(char*);
 
    BOOL    UserEnterRoom(USER*, ROOM*);
    BOOL    UserExitRoom(USER*, ROOM*);
 
    BOOL    AddUser(USER*);
    BOOL    DelUser(USER*);
    BOOL    AddRoom(ROOM*);
    BOOL    DelRoom(ROOM*);
 
    BOOL    CreateMessage(char*) const;
    BOOL    SetMessageParams(char*, const PACKET_COMMAND&, const BYTE&, const BOOL&) const;
    BOOL    GetMessageParams(const char*, PACKET_COMMAND&, BYTE&, BOOL&) const;
 
    const char* GetStatusString() const;
};
 
DWORD CALLBACK AcceptShell(LPVOID);
 
#endif /*__SERVER_H__*/

room.h

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "Chat Server.h"
 
#ifndef __ROOM_H__
#define __ROOM_H__
 
class ROOM
{
public :
    BOOL _r_ID;
    char* _r_name;
    set<USER*, COMPARE> _r_usersIn;
 
    ROOM();
    ~ROOM();
};
 
#endif /*__ROOM_H__*/

user.h

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
#include "Chat Server.h"
 
#ifndef __USER_H__
#define __USER_H__
 
class USER
{
public :
    BOOL _u_ID;
    char* _u_name;
    HANDLE _u_thread;
    set <ROOM*, COMPARE> _u_roomsIn;
 
    sockaddr_in _u_address;
    int _u_addressSize;
    SOCKET _u_socket;
 
 
    USER();
    ~USER();
 
    BOOL Connection();
};
 
DWORD CALLBACK ConnectionShell(LPVOID);
 
#endif /*__USER_H__*/

compare.h

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Chat Server.h"
 
#ifndef __COMPARE_H__
#define __COMPARE_H__
 
class COMPARE
{
public :
    COMPARE();
    bool operator() (ROOM*, ROOM*) const;
    bool operator() (USER*, USER*) const;
}
 
#endif /*__COMPARE_H__*/

classes.h

C++
1
2
3
4
5
6
7
8
9
#ifndef __CLASSES_H__
#define __CLASSES_H__
 
class ROOM;
class USER;
class SERVER;
class COMPARE;
 
#endif /*__CLASSES_H__*/

И в глобальных переменных маты на экземпляр класса SERVER

server.h

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef __GLOBALVARIABLES_H__
#define __GLOBALVARIABLES_H__
 
#ifdef __MAIN_CPP__
#define NAMESPACE
#else /*!(__MAIN_CPP__)*/
#define NAMESPACE extern
#endif /*__MAIN_CPP__*/
 
NAMESPACE WNDCLASSEX wcex;
NAMESPACE HWND hWndMain;
NAMESPACE HWND hDlgMain;
NAMESPACE HINSTANCE hInstMain;  
NAMESPACE WSADATA wsaData;
NAMESPACE TCHAR szTitle[STRING_SIZE];                   
NAMESPACE TCHAR szWindowClass[STRING_SIZE]; 
NAMESPACE SERVER server;
 
#endif /*__GLOBALVARIABLES_H__*/

В каждом cpp-файле есть только :

C++
1
2
#include "stdafx.h"
#include "Chat Server.h"

Собственно я запутался. 165 ошибок о том, что не определен SERVER, хоть и объявлен, о том, что не имеют ROOM’ы поля, да и вообще без

100 грамм

понятия работы линковщика не разобраться

Добавлено через 2 часа 31 минуту
up-to-date



0



DU

1500 / 1146 / 165

Регистрация: 05.12.2011

Сообщений: 2,279

29.08.2012, 12:56

2

возможно все из-за того, что в compare.h в 12 строке пропущена ;
по хорошему нужны спп файлы и все ошибки.

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

C++
1
2
3
4
5
6
7
8
9
10
// Chat Server.h
....
#include "Server.h"
....
 
 
 
// Server.h
#include "Chat Server.h"
....

Какое-то рекурсивное включение

нигде не увидел #include <set>

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



1



187 / 180 / 25

Регистрация: 27.01.2012

Сообщений: 1,335

29.08.2012, 13:25

 [ТС]

3

DU, искал слона, а вот мышь(«;») то и не увидел.. Поменял местами в char server.h пару инклюдов и все стало нормально : )
Set объявлен в stdafx. Об этом забыл написать.
С константами пока не заморачивался, ибо это уже косметический эффект. Сейчас бы заставить хотя бы запускаться.
А разве компилятор включает полностью хэдеры, даже если что-то из них не используется?
Насчет «рекурсивного включения» — так я делал раньше, чтобы можно было объявлять глобальные переменные этих классов, типа SERVER server.

Не очень хочется в каждом файле прописывать отдельно все инклюды. Ведь куда проще в одном файле что-то изменить. Как это делается в нормальных проектах?
p.s И всё же хочется почитать про линковщик, ибо такую логику мой мозг понять отказывается



0



1500 / 1146 / 165

Регистрация: 05.12.2011

Сообщений: 2,279

29.08.2012, 13:36

4

в коде есть зависимости. чем их меньше тем лучше.
если класс A ничего не должен знать о классе B, то и не надо инклуд от класса B включать в класс А.
Иначе любые изменения в хидере класса B приведут к перекомпиляции кода для класса A. Если бы вы большой проект организовали таким же образом, то его сборка занимала бы существенное время. Како-го было бы ждать скажем минут 15 сборки после простой правки кода? А час? А если таких правок много?



1



5496 / 4891 / 831

Регистрация: 04.06.2011

Сообщений: 13,587

29.08.2012, 13:51

5

Глобальные переменные в каком файле находятся?



0



187 / 180 / 25

Регистрация: 27.01.2012

Сообщений: 1,335

29.08.2012, 14:12

 [ТС]

6

alsav22, уже все нормально. Чуть переписал код и сделал server — указателем. Так проще работать с созданием и удалением сервера (конструктором и деструктором), чем со статической переменной.
Случайно обозвал глобальные переменные, как второй «server.h».
DU, спасибо, учту.
Кстати, мне вот интересно, а есть ли смысл гвардить cpp-файлы? По логике — нет, но вдруг?



0



1500 / 1146 / 165

Регистрация: 05.12.2011

Сообщений: 2,279

29.08.2012, 14:26

7

гвардятся инклуды. с сpp файлами нет таких проблем. они в обрабатываемой еденице трансляции встречаются только один раз по определению. translation unit — это сpp файл + все инклуды



1



В астрале

Эксперт С++

8048 / 4805 / 655

Регистрация: 24.06.2010

Сообщений: 10,562

29.08.2012, 14:27

8

nexen, .cpp файлы не включаются через #include, потому очевидно нет.



1



Процесс компиляции программ

  • Запись лекции №1
  • Запись лекции №2
  • Запись лекции №3
  • Практика

Зачем нам нужно это изучать?

  • У студентов часто возникают с этим проблемы — когда компилятор пишет ошибку, а человек не понимает, что ему говорят.
  • Если вы делаете ошибку в организации программы, причём такую ошибку, которая сразу к проблеме не приводит, то бывает такое, что при компиляции чуть-чуть по-другому всё сломается. Причём даже в крупных компаниях
    такое случается.

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

Базовые знания об этапах компиляции.

Обычно мы компилируем программу как g++ program.cpp. А вот чего мы пока не знаем, так это того, что g++ не делает всю работу самостоятельно, а вызывает другие команды, которые выполняют компиляцию по частям. И если посмотреть, что там, то происходит cc1plus, потом as, в конце collect2, который вызывает ld. Давайте попытаемся это повторить.

Дальше будет перечисление стадий с указанием двух моментов: как их можно выполнить руками и какое расширение обычно имеет результат этой стадии.

  1. Препроцессирование. Выполняется при помощи g++ -E (если дополнительно передать ключ -P, то вывод будет чуть короче), выходной файл обычно имеет расширение .i. На файл с расширением .i можно и глазами посмотреть — в нём будет куча текста вместо #include, а потом наш код. Собственно, #include — директива препроцессора, которая тупо вставляет указанный файл в то место, где написана. Также препроцессор занимается макросами (#define). О них позже.
  2. Трансляция. Выполняется при помощи g++ -S, выходной файл обычно имеет расширение .s. «Трансляция» — это (с английского) «перевод». Кого и куда переводим? Наш язык в ассемблер. Если передать параметр -masm=intel, можно уточнить, в какой именно ассемблер переводить (как было сказано в 01_asm, ассемблеры отличаются в зависимости от инструмента).
  3. Ассемблирование. Выполняется специальной утилитой as, выходной файл обычно имеет расширение .o (и называется объектным файлом). На данном этапе не происходит ничего интересного — просто инструкции, которые были в ассемблере, перегоняются в машинный код. Поэтому файлы .o бесполезно смотреть глазами, они бинарные, для этого есть специальные утилиты, например, objdump. Про него будет рассказано чуть позже.
  4. Линковка. Выполняется простым вызовом g++ от объектного файла. На выходе даёт исполняемый файл. Нужна, если файлов несколько: мы запускаем препроцессор, трансляцию и ассемблирование независимо для каждого файла, а объединяются они только на этапе линковки. Независимые .cpp файлы называют единицами трансляции. Разумеется, только в одной единице должен быть main. В этом main‘е, кстати, можно не делать return 0, его туда вставит компилятор.
    Сто́ит сказать, что информация о линковке верна до появления модулей в C++20, где можно доставать данные одного файла для другого. Там появляется зависимость файлов друг от друга, а значит компилировать их надо в определённом порядке.

Классическая схема этапов компиляции выглядит так:

Compilation graph

Есть похожая статья на хабре по теме.

Объявление и определение.

Очень хочется слинковать вот это:

// a.cpp:
int main() {
	f();
}
// b.cpp:
#include <cstdio>

void f() {
	printf("Hello, world!n");
}

Это не компилируется, а точнее ошибка происходит на этапе трансляции a.cpp. В тексте ошибки написано, что f не определена в области видимости. Всё потому, что для того чтобы вызвать функцию, надо что-то про неё знать. Например, если мы передаём в функцию int — это один ассемблерный код, а если double — то совершенно другой (потому что разные calling convention’ы могут быть). Поэтому на этапе трансляции нужно знать сигнатуру функции. Чтобы указать эту сигнатуру, в C++ есть объявления:

// a.cpp:			
void f(); // Вот это объявление.

int main() {
	f();
}
// b.cpp:
#include <cstdio>

void f() {
	printf("Hello world");
}

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

Кстати, написать объявление бывает полезно даже если у нас один файл. Например, в таком файле:

#include <cstdio>

int main() {
	f();
}

void f() {
	printf("Hello, worldn");
}

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

Ошибки линковки. Инструменты nm и objdump. Ключевое слово static.

Рассмотрим такой пример:

// a.cpp
#include <cstdio>

void f()
{
	printf("Hello, a.cpp!n");
}
// b.cpp
#include <cstdio>

void f()
{
	printf("Hello, b.cpp!n");
}
// main.cpp
void f();

int main()
{
	f();
}

Тут вам на этапе линковки напишут, что функция f() определяется дважды. Чтобы красиво посмотреть, как это работает, можно использовать утилиту nm. Когда вы сгенерируете a.o и вызовете nm -C a.o, то увидите что-то такое:

                 U puts
0000000000000000 T f()

Что делает ключ -C, оставим на потом. На то что тут находится puts вместо printf, тоже обращать внимание не надо, это просто такая оптимизация компилятора — когда можно заменить printf на puts, заменяем.
А обратить внимание надо на то, что puts не определена (об этом нам говорит буква U), а функция f() — определена в секции .text (буква T). У main.cpp, понятно, будет неопределённая функция f() и определённая main. Поэтому, имея эти объектные файлы, можно слинковать main.cpp и a.cpp, а можно — main.cpp и b.cpp. Без перекомпиляции. Но нельзя все три вместе, ведь f() будет определена дважды.

Если мы хотим посмотреть на объектные файлы поподробнее, нам понадобится утилита objdump. У неё есть бесчисленное много ключей, которые говорят, что мы хотим увидеть. Например -x — выдать вообще всё. Нам сейчас нужно -d — дизассемблирование и -r — релокации. Когда мы вызовем objdump -dr -Mintel -C main.o, мы увидим, что на месте вызова функции f находится call и нули. Потому что неизвестно, где эта функция, надо на этапе линковки подставить её адрес. А чтобы узнать, что именно подставить, есть релокации, которые информацию об этом и содержат. В общем случае релокация — информация о том, какие изменения нужно сделать с программой, чтобы файл можно было запустить.

Давайте теперь вот на что посмотрим. Пусть в нашем файле определена функция f(). И где-то по случайному совпадению далеко-далеко также определена функция f(). Понятно, что оно так не слинкуется. Но мы можем иметь ввиду, что наша функция f нужна только нам и никак наружу не торчит. Для этого имеется специальный модификатор: static. Если сделать на такие функции nm, то можно увидеть символ t вместо T, который как раз обозначает локальность для единицы трансляции. Вообще функции, локальные для одного файла сто́ит помечать как static в любом случае, потому что это ещё помогает компилятору сделать оптимизации.

Глобальные переменные.

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

extern int x; // Объявление.

int x;        // Определение.

И точно также в глобальных переменных можно писать static. А теперь пример:

// a.cpp
extern int a;

void f();

int main()
{
	f();
	a = 5;
	f();
}
// b.cpp
#include <cstdio>

int a;

void f()
{
	printf("%dn", a);
}

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

Декорирование имён. extern "C".

Обсуждённая нами модель компиляции позволяет использовать несколько разных языков программирования. Пока ЯП умеет транслироваться в объектные файлы, проблемы могут возникнуть только на этапе линковки. Например, никто не мешает вам взять уже готовый ассемблерник и скомпилировать его с .cpp файлом. Но в вызове ассемблера есть одна проблема. Тут надо поговорить о такой вещи как extern "C". В языке C всё было так: имя функции и имя символа для линковщика — это одно и то же. Если мы скомпилируем файл

// a.c <-- C, не C++.
void foo(int)
{
	// ...
}

То имя символа, которое мы увидим в nm будет foo. А в C++ появилась перегрузка функций, то есть void foo(int) и void foo(double) — это две разные функции, обе из которых можно вызывать. Поэтому одно имя символа присвоить им нельзя. Так что компилятор mangle’ит/декорирует имена, то есть изменяет их так, чтобы символы получились уникальными. nm даже может выдать вам эти имена (в данном случае получится _Z3fooi и _Z3food). Но у вас есть и возможность увидеть их по-человечески: для этого существует уже упомянутый ключ -C, который если передать программе nm, то она раздекорирует всё обратно и выдаст вам имена человекочитаемо. objdump‘у этот ключ дать тоже можно. А ещё есть утилита
c++filt, которая по имени символа даёт сигнатуру функции.

Так вот, extern "C" говорит, что при линковке нам не нужно проводить декорацию. И если у нас в ассемблерном файле написано fibonacci:, то вам и нужно оставить имя символа как есть:

extern "C" uint32_t fibonacci(uint32_t n);

У функций с разными сигнатурами, но помеченных как extern "C", после компиляции не будет информации об типах их аргументов, поэтому это слинкуется, но работать не будет (ну либо будет, но тут UB, так как, например, типы аргументов ожидаются разные).

Линковка со стандартной библиотекой.

Возьмём теперь объявление printf из cstdio и вставим его объявление вручную:

extern "C" int printf(const char*, ...);

int main() {
	printf("Hello, world!");
}

Такая программа тоже работает. А где определение printf, возникает вопрос? А вот смотрите. На этапе связывания
связываются не только ваши файлы. Помимо этого в параметры связывания добавляются несколько ещё объектных файлов и несколько библиотек. В нашей модели мира хватит информации о том, что библиотека — просто набор объектных файлов. И вот при линковке вам дают библиотеку стандартную библиотеку C++ (-lstdc++), математическую библиотеку (-lm), библиотеку -libgcc, чтобы если вы делаете арифметику в 128-битных числах, то компилятор мог вызвать функцию __udivti3 (деление), и кучу всего ещё. В нашем случае нужна одна — -lc, в которой и лежит printf. А ещё один из объектных файлов, с которыми вы линкуетесь, содержит функцию _start (это может быть файл crt1.o), которая вызывает main.

Headers (заголовочные файлы). Директива #include.

Если мы используем одну функцию во многих файлах, то нам надо писать её сигнатуру везде. А если мы её меняем, то вообще повеситься можно. Поэтому так не делают. А как делают? А так: декларация выделяется в отдельный файл. Это файл имеет расширение .h и называется заголовочным. По сути это же происходит в стандартной библиотеке. Подключаются заголовочные файлы директивой #include <filename>, если они из какой-то библиотеки, или #include "filename", если он ваш. В чём разница? Стандартное объяснение — тем, что треугольные скобки сначала ищут в библиотеках, а потом в вашей программе, а кавычки — наоборот. На самом желе у обоих вариантов просто есть список путей, где искать файл, и эти списки разные.

Но с заголовками нужно правильно работать. Например, нельзя делать #include "a.cpp". Почему? Потому что все определённые в a.cpp функции и переменные просочатся туда, куда вы его подключили. И если файл у вас один, то ещё ничего, а если больше, то в каждом, где написано #include "a.cpp", будет определение, а значит определение одного и того же объекта будет написано несколько раз.
Аналогичной эффект будет, если писать определение сразу в заголовочном файле, не надо так.

К сожалению, у директивы #include есть несколько нюансов.

Предотвращение повторного включения.

Давайте поговорим про структуры. Что будет, если мы в заголовочном файле создадим struct, и подключим этот файл? Да ничего. Абсолютно ничего. Сгенерированный ассемблерный код будет одинаковым. У структур нет определения по сути, потому что они не генерируют код. Поэтому их пишут в заголовках. При этом их методы можно (но не нужно) определять там же, потому что они воспринимаются компилятором как inline. А кто такой этот inline и как он работает — смотри дальше. Но со структурами есть один нюанс. Рассмотрим вот что:

// a.cpp:
#include "y.h" //    -->    `struct x{};`.
#include "z.h" //    -->    `struct x{};` ошибка компиляции, повторное определение.

Стандартный способ это поправить выглядит так:

// x.h:
#ifndef X_H // Если мы уже определили макрос, то заголовок целиком игнорируется.
#define X_H	// Если не игнорируется, то помечаем, что файл мы подключили.

struct x {};

#endif // В блок #ifndef...#endif заключается весь файл целиком.

Это называется include guard. Ещё все возможные компиляторы поддерживают #pragma once (эффект как у include guard, но проще). И на самом деле #pragma once работает лучше, потому что не опирается на имя файла, например. Но его нет в стандарте, что грустно.

Есть один нюанс с #pragma once‘ом. Если у вас есть две жёстких ссылки на один файл, то у него проблемы. Если у вас include guard, то интуитивно понятно, что такое разные файлы — когда макросы у них разные. А вот считать ли разными файлами две жёстких ссылки на одно и то же — вопрос сложный. Другое дело, что делать так, чтобы источники содержали жёсткие
или символические ссылки, уже довольно странно.

Forward-декларации.

// a.h
#ifndef A_H
#define A_H

#include "b.h" // Nothing, then `struct b { ... };`

struct a {
	b* bb;
};
#endif
// b.h
#ifndef B_H
#define B_H

#include "a.h" // Nothing, then `struct a { ... };`

struct b {
	a* aa;
};
#endif
// main.cpp
#include "a.h" // `struct b { ... }; struct a { ... };`
#include "b.h" // Nothing.

Понятно, в чём проблема заключается. Мы подключаем a.h, в нём — b.h, в нём, поскольку мы уже зашли в a.h, include guard нам его блокирует. И мы сначала определяем структуру b, а потом — a. И при просмотре структуры b, мы не будем знать, что такое a.

Для этого есть конструкция, называемая forward-декларацией. Она выглядит так:

// a.h
#ifndef A_H
#define A_H

struct b;

struct a {
	b* bb;
};
#endif
// b.h
#ifndef B_H
#define B_H

struct a;

struct b {
	a* aa;
};
#endif

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

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

  • Во-первых, из-за времени компиляции. Большое количество подключений в заголовочных файлах негативно влияет на него, потому что если меняется header, то необходимо перекомпилировать все файлы, которые подключают его (даже не непосредственно), что может быть долго.
  • Второй момент — когда у нас цикл из заголовочных файлов, это всегда ошибка, даже если там нет проблем как в примере, потому что результат компиляции зависит от того, что вы подключаете первым.

Пока структуру не определили, структура — это incomplete type. Например, на момент объявление struct b; в коде выше, b — incomplete. Кстати, в тот момент, когда вы находитесь в середине определения класса, он всё ещё incomplete.
Все, что можно с incomplete типами делать — это объявлять функции с их использованием и создавать указатель. Становятся полным типом после определения.
Пока что информация об incomplete-типах нам ни к чему, но она выстрелит позже.

Правило единственного определения.

А теперь такой пример:

// a.cpp
#include <iostream>

struct x {
	int a;
	// padding
	double b;
	int c;
	int d;
};

x f();

int main() {
	x xx = f();
	std::cout << xx.a << " "
	          << xx.b << " "
	          << xx.c << " "
	          << xx.d << std::endl;
}
// b.cpp
struct x {
	int a;
	int b;
	int c;
	int d;
	int e;
};

x f() {
	x result;
	result.a = 1;
	result.b = 2;
	result.c = 3;
	result.d = 4;
	result.e = 5;
	return result;
};

Тут стоит вспомнить, что структуры при линковке не играют никакой роли, то есть линковщику всё равно, что у нас структура x определена в двух местах. Поэтому такая программа отлично скомпилируется и запустится, но тем не менее она является некорректной. По стандарту такая программа будет работать неизвестно как, а по жизни данные поедут. А именно 2 пропадёт из-за выравнивания double, 3 и 4 превратятся в одно число (double), а 5 будет на своём месте, а x::e из файла a.cpp будет просто не проинициализирован. Правило, согласно которому так нельзя, называется one-definition rule/правило единственного определения. Кстати, нарушением ODR является даже тасовка полей.

Inlining.

int foo(int a, int b) {
	return a + b;
}

int bar(int a, int b) {
	return foo(a, b) - a;
}

Если посмотреть на ассемблерный код для bar, то там не будет вызова функции foo, а будет return b;. Это называется inlining — когда мы берём тело одной функции и вставляем внутрь другой как оно есть. Это связано, например, со стилем программирования в текущем мире (много маленьких функций, которые делают маленькие вещи) — мы убираем все эти абстракции, сливаем функции в одну и потом оптимизируем что там есть.

Но есть один нюанс…

Модификатор inline.

// a.c
void say_hello();

int main() {
	say_hello();
}
// b.c
#include <cstdio>

void say_hello() {
	printf("Hello, world!n");
}

Тут не произойдёт inlining, а почему? А потому что компилятор умеет подставлять тело функций только внутри одной единицы трансляции (так как inlining происходит на момент трансляции, а тогда у компилятора нет функций из других единиц).

Тут умеренно умалчивается, что модель компиляции, которую мы обсуждаем — древняя и бородатая. Мы можем передать ключ -flto в компилятор, тогда всё будет за’inline’ено. Дело в том, что при включенном режиме linking time optimization, мы откладываем на потом генерацию кода и генерируем его на этапе линковки. В таком случае линковка может занимать много времени, поэтому применяется при сборке с оптимизациями. Подробнее о режиме LTO — сильно позже.

Но тем не менее давайте рассмотрим, как без LTO исправить проблему с отсутствием inlining’а. Мы можем написать в заголовочном файле тело, это поможет, но это, как мы знаем, ошибка компиляции. Хм-м, ну, можно не только написать функцию в заголовочном файле, но и пометить её как static, но это, даёт вам свою функцию на каждую единицу трансляции, что, во-первых, бывает просто не тем, что вы хотите, а во-вторых, кратно увеличивает размер выходного файла.

Поэтому есть модификатор inline. Он нужен для того, чтобы линковщик не дал ошибку нарушения ODR. Модификатор inline напрямую никак не влияет на то, что функции встраиваются.. Если посмотреть на inline через nm, то там увидим W (weak) — из нескольких функций можно выбрать любую (предполагается, что все они одинаковые).

По сути inline — указание компилятору, что теперь за соблюдением ODR следите вы, а не он. И если ODR вы нарушаете, то это неопределённое поведение (ill-formed, no diagnostic required). ill-formed, no diagnostic required — это ситуация, когда программа некорректна, но никто не заставляет компилятор вам об этом говорить. Он может (у GCC есть такая возможность: если дать g++ ключи -flto -Wodr, он вам об этом скажет), но не обязан. А по жизни линковщик выберет произвольную из имеющихся функций (например, из первой единицы трансляции или вообще разные в разных местах):

// a.cpp
#include <cstdio>

inline void f() {
	printf("Hello, a.cpp!n");
}

void g();

int main() {
	f();
	g();
}
// b.cpp
inline void f() {
	printf("Hello, b.cpp!n");
}

void g() {
	f();
}

Если скомпилировать этот код с оптимизацией, обе функции f будут за’inline’ены, и всё будет хорошо. Если без, то зависит от порядка файлов: g++ a.cpp b.cpp может вполне выдавать Hello, a.cpp! два раза, а g++ b.cpp a.cppHello, b.cpp! два раза.

Если нужно именно за’inline’ить функцию, то есть нестандартизированные модификаторы типа __forceinline, однако даже они могут игнорироваться компилятором. Inlining функции может снизить производительность: на эту тему можно послушать доклад Антона Полухина на C++ Russia 2017.

Остальные команды препроцессора.

#include обсудили уже вдоль и поперёк. Ещё есть директивы #if, #ifdef, #ifndef, #else, #elif, #endif, которые дают условную компиляцию. То есть если выполнено какое-то условие, можно выполнить один код, а иначе — другой.

Определение макроса.

И ещё есть макросы: определить макрос (#define) и разопределить макрос (#undef):

#define PI 3.14159265
double circumference(double r) {
    return 2 * PI * r;
}

Текст, который идет после имени макроса, называется replacement. Replacement отделяется от имени макроса пробелом и распространяется до конца строки. Все вхождения идентификатора PI ниже этой директивы будут заменены на replacement. Самый простой макрос — object-like, его вы видите выше, чуть более сложный — function-like:

#define MIN(x, y) x < y ? x : y

printf("%d", MIN(4, 5));

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

#include <cerrno>

int main() {
	int errno = 42;
}

И получить отрешённое от реальности сообщение об ошибке. А дело всё в том, что это на этапе препроцессинга раскрывается, например, так:

int main() {
	int (*__errno_location()) = 42;
}

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

Что ещё не видит препроцессор, так это синтаксическую структуру и приоритет операций. Более страшные вещи получаются, когда пишется что-то такое:

#define MUL(x, y) x * y

int main() {
	int z = MUL(2, 1 + 1);
}

Потому что раскрывается это в

int main() {
	int z = 2 * 1 + 1;
}

Это не то что вы хотите. Поэтому когда вы такое пишите, нужно во-первых, все аргументы запихивать в скобки, во-вторых — само выражение тоже, а в-третьих, это вас никак не спасёт от чего-то такого:

#define max(a, b) ((a) < (b) ? (a) : (b))

int main() {
	int x = 1;
	int y = 2;
	int z = max(x++, ++y);
}

Поэтому перед написанием макросов три раза подумайте, нужно ли оно, а если нужно, будьте очень аккуратны. А ещё, если вы используете отладчик, то он ничего не знает про макросы, зачем ему знать. Поэтому в отладчике написать «вызов макроса» Вы обычно не можете. Cм. также FAQ Бьярна Страуструпа о том, почему макросы — это плохо.

Ещё #define позволяет переопределять макросы.

#define STR "abc"
const char* first = STR; // "abc".
#define STR "def"
const char* second = STR; // "def".

Replacement макроса не препроцессируется при определении макроса, но результат раскрытия макроса препроцессируется повторно:

#define Y foo
#define X Y   // Это не `#define X foo`.
#define Y bar // Это не `#define foo bar`.
X             // Раскрывается `X` -> `Y` -> `bar`.

Также по спецификации препроцессор никогда не должен раскрывать макрос изнутри самого себя, а оставлять вложенный идентификатор как есть:

#define M { M }
M   // Раскрывается в { M }.

Ещё пример:

#define A a{ B }
#define B b{ C }
#define C c{ A }
A // a{ b{ c{ A } } }
B // b{ c{ a{ B } } }
C // c{ a{ b{ C } } }

Условная компиляция. Проверка макроса.

Директивы #ifdef, #ifndef, #if, #else, #elif, #endif позволяют отпрепроцессировать часть файла, лишь при определенном условии. Директивы #ifdef, #ifndef проверяют определен ли указанный макрос. Например, они полезны для разной компиляции:

#ifdef __x86_64__
typedef unsigned long uint64_t;
#else
typedef unsigned long long uint64_t;
#endif

Директива #if позволяет проверить произвольное арифметическое выражение.

#define TWO 2
#if TWO + TWO == 4
// ...
#endif

Директива #if препроцессирует свой аргумент, а затем парсит то, что получилось как арифметическое выражение. Если после препроцессирования в аргументе #if остаются идентификаторы, то они заменяются на 0, кроме идентификатора true, который заменяется на 1.

Одно из применений #ifndef — это include guard, которые уже обсуждались ранее.

Константы.

Понадобилась нам, например, $pi$. Традиционно в C это делалось через #define. Но у препроцессора, как мы знаем, есть куча проблем. В случае с константой PI ничего не случится, вряд ли кто-то будет называть переменную так, особенно большими буквами, но всё же.

А в C++ (а позже и в C) появился const. Но всё же, зачем он нужен, почему нельзя просто написать глобальную переменную double PI = 3.141592;?

  1. Во-первых, константы могут быть оптимизированы компилятором. Если вы делаете обычную переменную, компилятор обязан её взять из памяти (или регистров), ведь в другом файле кто-то мог её поменять. А если вы напишете const, то у вас не будет проблем ни с оптимизацией (ассемблер будет как при #define), ни с адекватностью сообщений об ошибках.
  2. Во-вторых, она несёт документирующую функцию, когда вы пишете const с указателями. Если в заголовке функции написано const char*, то вы точно знаете, что вы передаёте в неё строку, которая не меняется, а если char*, то, скорее всего, меняется (то есть функция создана для того, чтобы менять).
  3. В-третьих, имея const, компилятор может вообще не создавать переменную: если мы напишем return PI * 2, то там будет возвращаться константа, и никакого умножения на этапе исполнения.

Кстати, как вообще взаимодействует const с указателями? Посмотрим на такой пример:

int main() {
	const int MAGIC = 42;
	int* p = &MAGIC;
}

Так нельзя, это имеет фундаментальную проблему: вы можете потом записать *p = 3, и это всё порушит. Поэтому вторая строка не компилируется, и её надо заменить на

Но тут нужно вот на что посмотреть. У указателя в некотором смысле два понятия неизменяемости. Мы же можем сделать так:

int main() {
	const int MAGIC = 42;
	const int* p = &MAGIC;
	// ...
	p = nullptr;
}

Кто нам мешает так сделать? Да никто, нам нельзя менять содержимое p, а не его самого. А если вы хотите написать, что нельзя менять именно сам указатель, то это не const int*/int const*, а int* const. Если вам нужно запретить оба варианта использования, то, что логично, const int* const или int const* const. То есть

int main() {
	int* a;
	*a = 1;      // ok.
	a = nullptr; // ok.

	const int* b;       // Синоним `int const* b;`
	*b = 1;      // Error.
	b = nullptr; // ok.

	int* const c;
	*c = 1;      // ok.
	c = nullptr; // Error.

	const int* const d; // Синоним `int const* const d;`
	*d = 1;      // Error.
	d = nullptr; // Error.
}

Теперь вот на что посмотрим:

int main() {
	int a = 3;
	const int b = 42;

	int* pa = &a;        // 1.
	const int* pca = &a; // 2.
	int* pb = &b;        // 3.
	const int* pcb = &b; // 4.
}

Что из этого содержит ошибку? Ну, в третьем точно ошибка, это мы уже обсудили. Также первое и четвёртое точно корректно. А что со вторым? Ну, нарушает ли второе чьи-то права? Ну, нет. Или как бы сказали на парадигмах программирования, никто не нарушает контракт, мы только его расширяем (дополнительно обещая неизменяемость), а значит всё должно быть хорошо. Ну, так и работают неявные преобразования в C++, вы можете навешивать const везде, куда хотите, но не можете его убирать.

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

Пишу простое консольное приложение
При попытке скомпилировать проект линковщик выдает следующее:
введите сюда описание изображения

Соответствующие переменные валяются в IIndexer.h

#pragma once
#include <Windows.h>

 DEFINE_GUID(CLSID_Indexer,
    0xa888f560, 0x58e4, 0x11d0, 0xa6, 0x8a, 0x0, 0x0, 0x83, 0x7e, 0x31, 0x0);

DEFINE_GUID(IID_IIndexer,
    0xa888f561, 0x58e4, 0x11d0, 0xa6, 0x8a, 0x0, 0x0, 0x83, 0x7e, 0x31, 0x0);


class IIndexer : public IUnknown
{
public:

};

От интерфейса наследуется класс Indexer(файл Indexer.h)

#pragma once
#include "IIndexer.h"

class Indexer : public IIndexer
{
public:
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv);
    virtual ULONG   STDMETHODCALLTYPE AddRef();
    virtual ULONG   STDMETHODCALLTYPE Release();
private:
    DWORD m_lRef;
public:
    Indexer();
    ~Indexer();
};

Нужные мне переменные CLSID_Indexer и IID_IIndexer используются в Indexer.cpp

#include "Indexer.h"

Indexer::Indexer()
{
    m_lRef = 0;
}

Indexer::~Indexer()
{

}

ULONG Indexer::AddRef()
{
    InterlockedIncrement(&m_lRef);
    return m_lRef;
}

ULONG Indexer::Release()
{
    InterlockedDecrement(&m_lRef);
    if (m_lRef == 0)
    {
        delete this;
        return 0;
    }
    else
    {
        return m_lRef;
    }
}

HRESULT Indexer::QueryInterface(REFIID riid, void **ppv)
{
    *ppv = 0;

    if (riid == IID_IUnknown || riid == IID_IIndexer)
    {
        *ppv = this;
    }

    if (*ppv)
    {
        AddRef();
        return S_OK;
    }
    else
    {
        return E_NOINTERFACE;
    }
}

В main.cpp лежит следующий код

#include <iostream>
#include "IIndexer.h"

int main()
{
    IIndexer *indexer;
    CoInitialize(NULL);
    HRESULT res = CoCreateInstance(CLSID_Indexer,
        NULL,
        CLSCTX_INPROC,
        IID_IIndexer,
        (LPVOID*)&indexer);

    if (indexer == NULL) std::cout << "FAILED TO CREATE OBJECT" << std::endl;
    else std::cout << "SUCCESS" << std::endl;

    return EXIT_SUCCESS;
}

Попробовал решение по коду ошибки, но не особо получилось. Подскажите,пожалуйста, почему проект не собирается?

Доброе время суток!

Читаю книгу «Программирование на языке С++
в среде Qt Creator» от Е. Р. Алексеев, Г. Г. Злобин, Д. А.Костюк,
О. В.Чеснокова, А. С.Чмыхало

Столкнулся с такой проблемой:

Содержимое файла *.pro:

TEMPLATE = app
TARGET = ParentExample
QT += widgets

HEADERS += 
    parentwidget.h

SOURCES += 
    main.cpp 
    parentwidget.cpp

Содержимое файла parentwidget.cpp:

#include "parentwidget.h"
#include <QLabel>
#include <QPushButton>

ParentWidget::ParentWidget(QWidget* parent):QWidget(parent)
{
    //используем указатели, чтобы дочерние элементы не удалились после завершения работы конструктора
    QLabel* ILabel = new QLabel(this); //this - это экземпляр класс ParentWidget, указывается родительский виджет
    (*ILabel).setGeometry(50,0,100,30);
    (*ILabel).setText("Привет, товарищ!");
    QPushButton* lPushButton = new QPushButton(this);
    (*lPushButton).setGeometry(50,50,100,30);
    (*lPushButton).setText("Жми!");
    setGeometry(x(),y(),300,150);
    setWindowTitle("Наследник Образец");


}

Содержимое файла parentwidget.h:

#ifndef PARENTWIDGET_H
#define PARENTWIDGET_H

#include <QWidget>

class ParentWidget : public QWidget
{
    Q_OBJECT
public:
    explicit ParentWidget(QWidget *parent = 0);

signals:

public slots:
};

#endif // PARENTWIDGET_H

В итоге при попытке собрать и запустить проект у меня появляются ошибки такого рода:

parentwidget.obj:-1: ошибка: LNK2001: неразрешенный внешний символ ""public: virtual struct QMetaObject const * __cdecl ParentWidget::metaObject(void)const " (?metaObject@ParentWidget@@UEBAPEBUQMetaObject@@XZ)"

parentwidget.obj:-1: ошибка: LNK2001: неразрешенный внешний символ ""public: virtual void * __cdecl ParentWidget::qt_metacast(char const *)" (?qt_metacast@ParentWidget@@UEAAPEAXPEBD@Z)"

parentwidget.obj:-1: ошибка: LNK2001: неразрешенный внешний символ ""public: virtual int __cdecl ParentWidget::qt_metacall(enum QMetaObject::Call,int,void * *)" (?qt_metacall@ParentWidget@@UEAAHW4Call@QMetaObject@@HPEAPEAX@Z)"

debugwidgets.exe:-1: ошибка: LNK1120: неразрешенных внешних элементов: 3

Очень много в выдаче по этой проблеме, но я не обладаю соответствующими знаниями, чтобы извлечь из этого что-то полезное.
Есть какие-нибудь мысли?=)

Регулярно пишу на C++ — хочется порой писать и о C++. 
Давно я вообще ничего сюда не писал — самое время совместить приятное с полезным. И с C++:)

Среди всех проблем, которые могут возникнуть на пути запуска C++-программы, есть такие, которые понять сложнее всего, — ошибки линковки. Одну из таких я попытаюсь сейчас описать: это ошибка множественного определения символов (multiple definition).

Вот, скажем, есть такой (искусственнейший) пример:

// main.cpp

#include «ComplicatedHello.h»

int main()
{
    ComplicatedHello obj;
    obj.SayHello();
    obj.MakeHoldeeSay();
}

Есть объект, который может сказать Hello сам, а может попросить это сделать другой объект, который содержит в себе.

// ComplicatedHello.h

#pragma once

#include «SimpleHello.h»

class ComplicatedHello
{
public:
    void SayHello()
    {
        Hello();
    }

    void MakeHoldeeSay()
    {
        m_Holdee.SayHello();
    }
private:
    SimpleHello m_Holdee;
};

// SimpleHello.h

#pragma once

#include «Hello.h»

class SimpleHello
{
public:
    void SayHello()
    {
        Hello();
    }
};

И простой объект, и сложный используют для общения одну единственную функцию Hello() — в ней-то и будет вся соль.

// Hello.h

#pragma once

#include

void Hello()
{
    std::cout << «Hellon»;
}

И всё прекрасно. Прелесть этой версии в том, что тут только один .cpp файл — одна единица компиляции, с которой все остальные файлы связаны цепочкой включений. Включение (#include) являет собой простое копирование всего кода подключаемого заголовочного файла, и поэтому в конце получается этакий разросшийся main.cpp. Следовательно, при компиляции его (заголовки не компилируются в объектники — только разве что прекомпилируются, если сильно надо) создаётся только один объектный файл — main.o — какие уж тут проблемы линковки:)

А теперь сделаем код более бизнесовым — разделим ComplicatedHello как самый громоздкий файл в нашем проекте на интерфейс и реализацию. Получаем такое:

// ComplicatedHello.h

#pragma once

#include «SimpleHello.h»

class ComplicatedHello
{
public:
    void SayHello();
    void MakeHoldeeSay();

private:
    SimpleHello m_Holdee;
};

// ComplicatedHello.cpp

#include «ComplicatedHello.h»
#include «Hello.h»

void ComplicatedHello::SayHello()
{
    Hello();
}

void ComplicatedHello::MakeHoldeeSay()
{
    m_Holdee.SayHello();
}

Только при сборке на этот раз появилась неприятность: линковщик выдал ошибку multiple definition of ‘Hello()’. Он мог бы ещё добавить, что это нарушение одного из фундаментальных правил C++ — правила одного определения (http://en.wikipedia.org/wiki/One_Definition_Rule), но сдержался. Так ведь самое главное, что всё вроде как предусмотрено, чтобы это правило не нарушать: каждый заголовок защищён прагмой, никаких циклических зависимостей, даже code style выдержан — ну что тут может не нравиться…

Но если вернуться к единицам компиляции, то станет видно, что их теперь две: main и ComplicatedHello. Можно проследить, что окажется включено в каждую из них:

main <- ComplicatedHello.h <- SimpleHello.h <- Hello.h
ComplicatedHello <- SimpleHello.h <- Hello.h

Правило одного определения нарушается, потому что функция Hello() вместе со своим телом полностью лежит в заголовке и переходит в изначальном виде в оба объектника. По стандарту языка в каждой единице трансляции допускается (и, более того, необходимо) определение одной и той же функции, если она объявлена как inline (и ещё какое-то исключение для статических функций), а для обычных функций на всю программу должно быть только одно определение где-то в одном месте.
Действительно, если определить Hello() как inline, ошибка исчезает, но зачем менять сигнатуру из-за такой мелочи, когда можно всё сделать как следует? То есть сделать из Hello.h самостоятельную единицу компиляции (может, так, наоборот, и не следует в этом случае — просто так хочется):

// Hello.h (да, вот такой целый хэдер)

#pragma once

void Hello();

// Hello.cpp

#include «Hello.h»

#include

void Hello()
{
    std::cout << «Hellon»;
}

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

Кстати, заметили, что определение класса SimpleHello оказывается тоже в каждом объектнике по схеме выше? Можно не беспокоиться, для классов по стандарту это ок:)

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

Кстати, хорошая новость: я и сам почти поверил, что разобрался в этом всём. Успеееех=)

Понравилась статья? Поделить с друзьями:
  • Ошибки лиги наций
  • Ошибки летнего гардероба
  • Ошибки ленточного фундамента под забор
  • Ошибки ленина при его власти
  • Ошибки ленина кратко