Введение
Ошибки "неопределенный символ" (undefined symbol errors) являются распространенной проблемой в программировании на C++, которая часто ставит в тупик новичков в процессе компиляции и линковки. Эти ошибки возникают, когда компилятор не может найти реализацию функции или переменной, которая используется в вашем коде. В этой лабораторной работе вы узнаете, как идентифицировать, понимать и решать различные типы ошибок "неопределенный символ" на практических примерах.
К концу этой лабораторной работы вы получите четкое представление о процессе компиляции и линковки, распространенных причинах ошибок "неопределенный символ" и эффективных стратегиях диагностики и исправления этих проблем в ваших C++ проектах.
Понимание ошибок "неопределенный символ"
На этом шаге мы рассмотрим, что такое ошибки "неопределенный символ", и создадим простой пример, чтобы продемонстрировать, как они возникают.
Что такое ошибки "неопределенный символ"?
Ошибки "неопределенный символ" обычно появляются на этапе линковки (linking) компиляции, когда линковщик не может найти реализацию функции или переменной, которая объявлена и используется в вашем коде. Процесс компиляции C++ состоит из нескольких этапов:
- Препроцессинг (Preprocessing): Раскрывает макросы и включает заголовочные файлы
- Компиляция (Compilation): Преобразует исходный код в объектные файлы
- Линковка (Linking): Объединяет объектные файлы и разрешает ссылки
Когда линковщик не может разрешить ссылку на символ, он выдает ошибку "неопределенный символ" (undefined symbol) или "неопределенная ссылка" (undefined reference).
Создание простого примера
Давайте создадим простой пример, чтобы продемонстрировать ошибку "неопределенный символ". Мы создадим два файла:
- Заголовочный файл с объявлениями функций
- Основной файл, который использует эти функции, но без предоставления реализаций
Сначала создадим заголовочный файл. Создайте новый файл с именем calculator.h в редакторе:
#ifndef CALCULATOR_H
#define CALCULATOR_H
// Function declarations
int add(int a, int b);
int subtract(int a, int b);
#endif
Теперь создадим основной файл, который использует эти функции. Создайте новый файл с именем main.cpp:
#include <iostream>
#include "calculator.h"
int main() {
int result1 = add(5, 3);
int result2 = subtract(10, 4);
std::cout << "Addition result: " << result1 << std::endl;
std::cout << "Subtraction result: " << result2 << std::endl;
return 0;
}
Компиляция кода
Теперь давайте скомпилируем нашу программу и понаблюдаем за ошибкой. Откройте терминал и запустите:
g++ main.cpp -o calculator_app
Вы должны увидеть сообщения об ошибках, подобные этим:
/tmp/cc7XaY5A.o: In function `main':
main.cpp:(.text+0x13): undefined reference to `add(int, int)'
main.cpp:(.text+0x26): undefined reference to `subtract(int, int)'
collect2: error: ld returned 1 exit status
Понимание ошибки
Сообщения об ошибках указывают на то, что линковщик не может найти реализации функций add и subtract, которые объявлены в заголовочном файле и используются в main.cpp.
Это происходит потому, что:
- Мы предоставили только объявления функций в
calculator.h - Мы не предоставили реализации для этих функций
- Линковщик не может найти определения функций при сборке исполняемого файла
На следующем шаге мы исправим эту ошибку, предоставив реализации для недостающих функций.
Решение ошибок, связанных с отсутствием реализации
На этом шаге мы исправим ошибки "неопределенный символ", с которыми столкнулись на предыдущем шаге, предоставив реализации для объявленных функций.
Создание файла реализации
Наиболее распространенный способ исправить ошибки "неопределенный символ" - это реализовать недостающие функции. Давайте создадим новый файл с именем calculator.cpp, который будет содержать реализации наших функций:
#include "calculator.h"
// Function implementations
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
Компиляция нескольких исходных файлов
Теперь, когда у нас есть файл реализации, нам нужно скомпилировать все наши исходные файлы вместе. Есть два основных способа сделать это:
Метод 1: Скомпилировать все исходные файлы одновременно
g++ main.cpp calculator.cpp -o calculator_app
Метод 2: Скомпилировать файлы по отдельности, а затем слинковать их
g++ -c main.cpp -o main.o
g++ -c calculator.cpp -o calculator.o
g++ main.o calculator.o -o calculator_app
Давайте используем первый метод для простоты. Выполните следующую команду:
g++ main.cpp calculator.cpp -o calculator_app
На этот раз компиляция должна пройти успешно без каких-либо ошибок. Теперь вы можете запустить программу:
./calculator_app
Вы должны увидеть следующий вывод:
Addition result: 8
Subtraction result: 6
Понимание исправления
Давайте разберемся, почему наше решение сработало:
- Мы создали отдельный файл реализации (
calculator.cpp), который содержит фактический код для наших функций. - Мы включили заголовочный файл в файл реализации, чтобы обеспечить согласованность между объявлениями и реализациями.
- Мы скомпилировали оба исходных файла вместе, что позволило линковщику найти реализацию каждой функции.
Это разделение объявления и реализации является распространенной практикой в программировании на C++, поскольку оно:
- Отделяет интерфейс (объявления) от реализации
- Позволяет лучше организовать код
- Поддерживает принцип сокрытия информации
Изучение различных сценариев ошибок
Давайте рассмотрим еще один распространенный сценарий, который приводит к ошибкам "неопределенный символ". Создайте новый файл с именем math_utils.h:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// Function declarations with a missing implementation
double square(double x);
double cube(double x);
// Variable declaration without definition
extern int MAX_VALUE;
#endif
Теперь создайте файл с именем test_math.cpp:
#include <iostream>
#include "math_utils.h"
int main() {
double num = 5.0;
std::cout << "Square of " << num << ": " << square(num) << std::endl;
std::cout << "Maximum value: " << MAX_VALUE << std::endl;
return 0;
}
Попробуйте скомпилировать этот код:
g++ test_math.cpp -o math_test
Вы снова увидите ошибки "неопределенный символ", но на этот раз как для функции, так и для переменной:
/tmp/ccjZpO2g.o: In function `main':
test_math.cpp:(.text+0x5b): undefined reference to `square(double)'
test_math.cpp:(.text+0x79): undefined reference to `MAX_VALUE'
collect2: error: ld returned 1 exit status
Это демонстрирует, что ошибки "неопределенный символ" могут возникать как с функциями, так и с переменными, когда они объявлены, но не определены.
Обработка проблем с линковкой библиотек
Ошибки "неопределенный символ" часто возникают, когда ваш код использует внешние библиотеки, но не связывается с ними должным образом. На этом шаге мы рассмотрим, как решить эти типы ошибок.
Создание программы, использующей внешние библиотеки
Давайте создадим простую программу, которая использует математические функции из стандартной математической библиотеки C. Создайте новый файл с именем math_example.cpp:
#include <iostream>
#include <cmath>
int main() {
double x = 2.0;
// Using functions from the math library
double square_root = sqrt(x);
double log_value = log(x);
double sine_value = sin(M_PI / 4);
std::cout << "Square root of " << x << ": " << square_root << std::endl;
std::cout << "Natural log of " << x << ": " << log_value << std::endl;
std::cout << "Sine of PI/4: " << sine_value << std::endl;
return 0;
}
Компиляция без правильной линковки библиотеки
Сначала попробуем скомпилировать эту программу без явной линковки с математической библиотекой:
g++ math_example.cpp -o math_example
В некоторых системах это может сработать, потому что стандартная библиотека может быть связана автоматически. Однако во многих системах Linux вы увидите ошибку, подобную следующей:
/tmp/ccBwPe5g.o: In function `main':
math_example.cpp:(.text+0x57): undefined reference to `sqrt'
math_example.cpp:(.text+0x73): undefined reference to `log'
math_example.cpp:(.text+0x9b): undefined reference to `sin'
collect2: error: ld returned 1 exit status
Решение ошибки линковки
Чтобы исправить это, нам нужно явно связать с математической библиотекой, используя флаг -lm:
g++ math_example.cpp -o math_example -lm
Теперь компиляция должна пройти успешно. Давайте запустим программу:
./math_example
Вы должны увидеть вывод, похожий на следующий:
Square root of 2: 1.41421
Natural log of 2: 0.693147
Sine of PI/4: 0.707107
Понимание линковки библиотек
Флаг -l сообщает компилятору связаться с определенной библиотекой:
-lmсвязывается с математической библиотекой (libm)-lpthreadсвязывается с библиотекой потоков POSIX-lcurlсвязывается с библиотекой cURL
Для системных библиотек компилятор знает, где их найти. Для пользовательских или сторонних библиотек вам также может потребоваться указать путь к библиотеке, используя флаг -L.
Создание пользовательской библиотеки
Давайте создадим простую пользовательскую библиотеку, чтобы продемонстрировать этот процесс. Сначала создайте заголовочный файл с именем geometry.h:
#ifndef GEOMETRY_H
#define GEOMETRY_H
// Function declarations for our geometry library
double calculateCircleArea(double radius);
double calculateRectangleArea(double length, double width);
#endif
Теперь создайте файл реализации с именем geometry.cpp:
#include "geometry.h"
#include <cmath>
// Implementation of geometry functions
double calculateCircleArea(double radius) {
return M_PI * radius * radius;
}
double calculateRectangleArea(double length, double width) {
return length * width;
}
Давайте создадим основную программу, которая использует нашу геометрическую библиотеку. Создайте файл с именем geometry_app.cpp:
#include <iostream>
#include "geometry.h"
int main() {
double radius = 5.0;
double length = 4.0;
double width = 6.0;
std::cout << "Circle area (radius=" << radius << "): "
<< calculateCircleArea(radius) << std::endl;
std::cout << "Rectangle area (" << length << "x" << width << "): "
<< calculateRectangleArea(length, width) << std::endl;
return 0;
}
Компиляция и линковка нашей пользовательской библиотеки
У нас есть два варианта использования нашей библиотеки:
Вариант 1: Скомпилировать все вместе
g++ geometry_app.cpp geometry.cpp -o geometry_app -lm
Вариант 2: Создать статическую библиотеку и связаться с ней
## Compile the library file to an object file
g++ -c geometry.cpp -o geometry.o
## Create a static library (archive)
ar rcs libgeometry.a geometry.o
## Compile the main program and link to our library
g++ geometry_app.cpp -o geometry_app -L. -lgeometry -lm
Давайте используем вариант 1 для простоты:
g++ geometry_app.cpp geometry.cpp -o geometry_app -lm
Запустите программу:
./geometry_app
Вы должны увидеть вывод, похожий на следующий:
Circle area (radius=5): 78.5398
Rectangle area (4x6): 24
Ключевые моменты о линковке библиотек
- Ошибки "неопределенный символ" часто возникают, когда библиотеки не связаны должным образом
- Используйте
-l<library>для линковки с библиотекой - Для пользовательских библиотек вам может потребоваться указать путь к библиотеке с помощью
-L<path> - Статические библиотеки имеют расширение
.a(в Linux/macOS) - Динамические библиотеки имеют расширение
.soв Linux (.dllв Windows,.dylibв macOS)
Отладка проблем с пространствами имен и областями видимости
Еще одним распространенным источником ошибок "неопределенный символ" являются проблемы с пространствами имен и областями видимости. На этом шаге мы рассмотрим, как они могут приводить к ошибкам "неопределенный символ" и как их разрешить.
Создание примера пространства имен
Давайте создадим пример, который демонстрирует ошибки "неопределенный символ", связанные с пространством имен. Создайте файл с именем utils.h:
#ifndef UTILS_H
#define UTILS_H
namespace Math {
// Function declarations in Math namespace
double multiply(double a, double b);
double divide(double a, double b);
}
namespace Text {
// Function declarations in Text namespace
std::string concatenate(const std::string& a, const std::string& b);
int countWords(const std::string& text);
}
#endif
Теперь создайте файл реализации utils.cpp:
#include <string>
#include <sstream>
#include "utils.h"
namespace Math {
// Implementations in Math namespace
double multiply(double a, double b) {
return a * b;
}
double divide(double a, double b) {
return a / b;
}
}
namespace Text {
// Implementations in Text namespace
std::string concatenate(const std::string& a, const std::string& b) {
return a + b;
}
int countWords(const std::string& text) {
std::istringstream stream(text);
std::string word;
int count = 0;
while (stream >> word) {
count++;
}
return count;
}
}
Создание программы с проблемами пространства имен
Давайте создадим основной файл, который неправильно использует эти пространства имен. Создайте файл с именем namespace_example.cpp:
#include <iostream>
#include <string>
#include "utils.h"
int main() {
// Incorrect: Functions called without namespace qualification
double product = multiply(5.0, 3.0);
std::string combined = concatenate("Hello ", "World");
std::cout << "Product: " << product << std::endl;
std::cout << "Combined text: " << combined << std::endl;
return 0;
}
Компиляция программы с проблемами пространства имен
Попробуйте скомпилировать программу:
g++ namespace_example.cpp utils.cpp -o namespace_example
Вы должны увидеть ошибки, подобные этим:
namespace_example.cpp: In function 'int main()':
namespace_example.cpp:7:22: error: 'multiply' was not declared in this scope
double product = multiply(5.0, 3.0);
^~~~~~~~
namespace_example.cpp:8:25: error: 'concatenate' was not declared in this scope
std::string combined = concatenate("Hello ", "World");
^~~~~~~~~~~~
Эти ошибки возникают потому, что функции определены внутри пространств имен, но мы пытаемся вызвать их, не указывая пространство имен.
Исправление проблем с пространством имен
Давайте исправим проблемы с пространством имен. Создайте исправленную версию с именем namespace_fixed.cpp:
#include <iostream>
#include <string>
#include "utils.h"
int main() {
// Method 1: Fully qualified names
double product = Math::multiply(5.0, 3.0);
std::string combined = Text::concatenate("Hello ", "World");
std::cout << "Product: " << product << std::endl;
std::cout << "Combined text: " << combined << std::endl;
// Method 2: Using directive (less preferred)
using namespace Math;
double quotient = divide(10.0, 2.0);
std::cout << "Quotient: " << quotient << std::endl;
// Method 3: Using declaration (more targeted)
using Text::countWords;
int words = countWords("This is a sample sentence.");
std::cout << "Word count: " << words << std::endl;
return 0;
}
Компиляция исправленной программы
Теперь скомпилируйте исправленную программу:
g++ namespace_fixed.cpp utils.cpp -o namespace_fixed
Она должна скомпилироваться без ошибок. Давайте запустим программу:
./namespace_fixed
Вы должны увидеть вывод, похожий на следующий:
Product: 15
Combined text: Hello World
Quotient: 5
Word count: 5
Понимание разрешения пространства имен
Давайте разберемся с различными способами решения проблем с пространством имен:
- Полностью квалифицированные имена (Fully qualified names): Наиболее явный метод, всегда добавляющий префикс к функции с ее пространством имен (
Math::multiply) - Директива using (Using directive): Переносит все идентификаторы из пространства имен в область видимости (
using namespace Math;) - Объявление using (Using declaration): Переносит определенные идентификаторы в область видимости (
using Text::countWords;)
Каждый метод имеет свое место, но использование полностью квалифицированных имен или целевых объявлений using обычно предпочтительнее, чтобы избежать потенциальных конфликтов имен.
Общие ошибки, связанные с областью видимости
Проблемы с областью видимости также могут вызывать ошибки "неопределенный символ":
- Статические переменные против внешних переменных (Static vs. extern variables): Переменные, объявленные с
static, видны только в пределах их единицы трансляции - Доступ к членам класса (Class member access): Закрытые члены недоступны за пределами класса
- Анонимные пространства имен (Anonymous namespaces): Символы в анонимных пространствах имен видны только в пределах их файла
Давайте создадим простой пример проблемы, связанной с областью видимости. Создайте файл с именем scope_example.cpp:
#include <iostream>
// This variable is only visible in this file
static int counter = 0;
void incrementCounter() {
counter++;
}
int getCounterValue() {
return counter;
}
// This function is in an anonymous namespace and only visible in this file
namespace {
void privateFunction() {
std::cout << "This function is private to this file" << std::endl;
}
}
int main() {
incrementCounter();
incrementCounter();
std::cout << "Counter value: " << getCounterValue() << std::endl;
privateFunction(); // This works because we're in the same file
return 0;
}
Этот пример должен скомпилироваться и запуститься без ошибок:
g++ scope_example.cpp -o scope_example
./scope_example
Ожидаемый вывод:
Counter value: 2
This function is private to this file
Однако, если вы попытаетесь получить доступ к counter или privateFunction из другого файла, вы получите ошибки "неопределенный символ" из-за их ограниченной области видимости.
Продвинутые методы отладки
На этом заключительном шаге мы рассмотрим более продвинутые методы диагностики и решения ошибок "неопределенный символ".
Использование флагов компилятора и компоновщика
Флаги компилятора и компоновщика могут предоставить больше информации о том, что идет не так. Создайте файл с именем debug_example.cpp:
#include <iostream>
// Forward declaration without implementation
void missingFunction();
int main() {
std::cout << "Calling missing function..." << std::endl;
missingFunction();
return 0;
}
Давайте скомпилируем это с подробным выводом:
g++ debug_example.cpp -o debug_example -v
Это предоставит вам подробную информацию о процессе компиляции и линковки. Вы увидите ошибку "неопределенная ссылка" для missingFunction.
Использование инструмента nm
Инструмент nm показывает символы в объектных файлах и библиотеках. Это может быть полезно для проверки того, определен ли символ на самом деле.
Давайте создадим простую программу с файлом реализации. Сначала создайте functions.h:
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
void sayHello();
void sayGoodbye();
#endif
Затем создайте functions.cpp:
#include <iostream>
#include "functions.h"
void sayHello() {
std::cout << "Hello, world!" << std::endl;
}
// Notice: sayGoodbye is not implemented
Теперь создайте greetings.cpp:
#include "functions.h"
int main() {
sayHello();
sayGoodbye(); // This will cause an undefined symbol error
return 0;
}
Скомпилируйте файл реализации в объектный файл:
g++ -c functions.cpp -o functions.o
Теперь давайте используем nm, чтобы увидеть, какие символы определены в объектном файле:
nm functions.o
Вы должны увидеть вывод, похожий на следующий:
U __cxa_atexit
U __dso_handle
0000000000000000 T _Z8sayHellov
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit
Обратите внимание, что sayHello определена (обозначено T для раздела text/code), но нет символа для sayGoodbye. Это подтверждает, что у функции отсутствует реализация.
Диагностика с помощью инструмента ldd
Инструмент ldd показывает зависимости библиотек для исполняемого файла. Это полезно, когда у вас есть проблемы с линковкой библиотек.
Давайте создадим простой пример, который использует библиотеку pthread. Создайте файл с именем thread_example.cpp:
#include <iostream>
#include <pthread.h>
void* threadFunction(void* arg) {
std::cout << "Thread running" << std::endl;
return nullptr;
}
int main() {
pthread_t thread;
int result = pthread_create(&thread, nullptr, threadFunction, nullptr);
if (result != 0) {
std::cerr << "Failed to create thread" << std::endl;
return 1;
}
pthread_join(thread, nullptr);
std::cout << "Thread completed" << std::endl;
return 0;
}
Скомпилируйте с библиотекой pthread:
g++ thread_example.cpp -o thread_example -pthread
Теперь используйте ldd, чтобы проверить зависимости библиотек:
ldd thread_example
Вы должны увидеть вывод, в котором перечислены все общие библиотеки, от которых зависит исполняемый файл, включая библиотеку pthread.
Общие причины и решения для ошибок "неопределенный символ"
Давайте обобщим общие причины ошибок "неопределенный символ" и их решения:
| Причина | Решение |
|---|---|
| Отсутствие реализации функции | Реализуйте функцию или свяжитесь с файлом, содержащим реализацию |
| Отсутствие линковки библиотеки | Добавьте соответствующий флаг -l (например, -lm для math) |
| Проблемы с пространством имен | Используйте квалифицированные имена (Namespace::function) или директивы/объявления using |
| Ограничения области видимости | Убедитесь, что символы доступны из вызывающей области видимости |
| Искажение имен символов | Используйте extern "C" для взаимодействия C/C++ или правильное деманглирование |
| Ошибки инстанцирования шаблонов | Предоставьте явное инстанцирование шаблона или переместите реализацию в заголовочный файл |
Создание контрольного списка для отладки
Вот систематический подход к отладке ошибок "неопределенный символ":
Определите точный неопределенный символ
- Внимательно посмотрите на сообщение об ошибке
- Используйте
nm, чтобы проверить, существует ли символ в объектных файлах
Проверьте наличие проблем с реализацией
- Убедитесь, что все объявленные функции имеют реализации
- Убедитесь, что файлы реализации включены в компиляцию
Проверьте линковку библиотеки
- Добавьте необходимые флаги библиотеки (например,
-lm,-lpthread) - Используйте
ldd, чтобы проверить зависимости библиотек
- Добавьте необходимые флаги библиотеки (например,
Изучите пространство имен и область видимости
- Проверьте квалификацию пространства имен
- Проверьте видимость и область видимости символа
Посмотрите на проблемы с искажением имен
- Добавьте
extern "C"для взаимодействия C/C++
- Добавьте
Обработайте ошибки, связанные с шаблонами
- Переместите реализации шаблонов в заголовочные файлы
- Предоставьте явное инстанцирование при необходимости
Заключительный пример: объединение всего вместе
Давайте создадим всеобъемлющий пример, который демонстрирует лучшие практики для избежания ошибок "неопределенный символ". Мы создадим небольшой проект с правильной организацией:
- Сначала создайте структуру каталогов:
mkdir -p library/include library/src app
- Создайте заголовочные файлы в каталоге include. Сначала создайте
library/include/calculations.h:
#ifndef CALCULATIONS_H
#define CALCULATIONS_H
namespace Math {
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
}
#endif
- Создайте реализацию в
library/src/calculations.cpp:
#include "calculations.h"
namespace Math {
double add(double a, double b) {
return a + b;
}
double subtract(double a, double b) {
return a - b;
}
double multiply(double a, double b) {
return a * b;
}
double divide(double a, double b) {
return a / b;
}
}
- Создайте основное приложение в
app/calculator.cpp:
#include <iostream>
#include "calculations.h"
int main() {
double a = 10.0;
double b = 5.0;
std::cout << a << " + " << b << " = " << Math::add(a, b) << std::endl;
std::cout << a << " - " << b << " = " << Math::subtract(a, b) << std::endl;
std::cout << a << " * " << b << " = " << Math::multiply(a, b) << std::endl;
std::cout << a << " / " << b << " = " << Math::divide(a, b) << std::endl;
return 0;
}
- Скомпилируйте все правильно:
g++ -c library/src/calculations.cpp -I library/include -o calculations.o
g++ app/calculator.cpp calculations.o -I library/include -o calculator
- Запустите приложение:
./calculator
Вы должны увидеть правильный вывод:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
Этот пример демонстрирует правильное разделение объявления и реализации, пространства имен и правильную компиляцию и линковку. Следуя этим практикам, вы можете избежать большинства ошибок "неопределенный символ".
Резюме
В этой лабораторной работе вы узнали, как диагностировать и решать ошибки "неопределенный символ" в программах на C++. Теперь вы понимаете:
- Основные причины ошибок "неопределенный символ", включая отсутствие реализаций и проблемы с линковкой библиотек
- Как правильно структурировать программы на C++ с отдельными заголовочными файлами и файлами реализации
- Методы линковки с внешними библиотеками с использованием соответствующих флагов компилятора
- Как решать проблемы, связанные с пространством имен и областью видимости, которые приводят к неопределенным символам
- Продвинутые методы отладки с использованием таких инструментов, как
nmиldd, для выявления и исправления проблем с символами
Эти навыки необходимы для разработчиков на C++, поскольку ошибки "неопределенный символ" являются одними из наиболее распространенных проблем, возникающих во время компиляции и линковки. Систематически анализируя эти ошибки и применяя соответствующие исправления, вы можете разрабатывать более надежные приложения на C++ с меньшим количеством проблем во время сборки.
Помните о необходимости следовать лучшим практикам, таким как поддержание согласованности объявлений и реализаций, правильная организация вашего кода с использованием пространств имен и понимание процесса линковки при работе с библиотеками. С этими инструментами и методами вы теперь хорошо подготовлены к решению ошибок "неопределенный символ" в ваших проектах на C++.



