Перегрузка конструктора
В C++ у класса может быть несколько конструкторов, каждый со своими параметрами.
Это так называемая «перегрузка конструктора». Благодаря ей объекты инициализируются по-разному, чем обеспечивается гибкость при их создании.
Начнем с простого примера:
class Rectangle {
private:
double width;
double height;
public:
// Конструктор по умолчанию
Rectangle() : width(1.0), height(1.0) {}
// Конструктор с одним параметром
Rectangle(double size) : width(size), height(size) {}
// Конструктор с двумя параметрами
Rectangle(double w, double h) : width(w), height(h) {}
double area() const {
return width * height;
}
};
int main() {
Rectangle r1; // Используется конструктор по умолчанию
Rectangle r2(5.0); // Используется конструктор с одним параметром
Rectangle r3(3.0, 4.0); // Используется конструктор с двумя параметрами
std::cout << "Area of r1: " << r1.area() << std::endl;
std::cout << "Area of r2: " << r2.area() << std::endl;
std::cout << "Area of r3: " << r3.area() << std::endl;
return 0;
}
Классом Rectangle
демонстрируется три различных конструктора, у каждого из которых конкретная цель.
Делегирование конструкторов в версии C++11 и новее
В C++11 появилось делегирование конструкторов: одним конструктором вызывается другой того же класса. Благодаря этому уменьшается дублирование кода:
class Circle {
private:
double radius;
std::string color;
public:
// Основной конструктор
Circle(double r, const std::string& c) : radius(r), color(c) {}
// Делегирующий конструктор
Circle() : Circle(1.0, "white") {}
// Другой делегирующий конструктор
Circle(double r) : Circle(r, "white") {}
double area() const {
return 3.14159 * radius * radius;
}
void display() const {
std::cout << "A " << color << " circle with radius " << radius << std::endl;
}
};
int main() {
Circle c1;
Circle c2(2.5);
Circle c3(3.0, "red");
c1.display();
c2.display();
c3.display();
return 0;
}
Здесь основным конструктором инициализируются radius
и color
, а другими ему делегируются значения по умолчанию.
Списки инициализаторов
Множественные конструкторы лаконично обрабатываются списками инициализаторов, особенно имея в виду коллекции:
#include <vector>
#include <initializer_list>
class IntegerSet {
private:
std::vector<int> elements;
public:
IntegerSet() {}
IntegerSet(std::initializer_list<int> list) : elements(list) {}
void add(int value) {
elements.push_back(value);
}
void display() const {
for (int elem : elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};
int main() {
IntegerSet set1;
IntegerSet set2 = {1, 2, 3, 4, 5};
set1.add(10);
set1.add(20);
std::cout << "Set 1: ";
set1.display();
std::cout << "Set 2: ";
set2.display();
return 0;
}
В этом примере показано, как конструктором списка инициализаторов создаются объекты с изменяемым числом первоначальных элементов.
Явные конструкторы
Ключевым словом explicit
предотвращаются неявные преобразования и инициализация копии.
explicit
применяется в конструкторах с одним параметром — во избежание нежелательных преобразований типов:
class Box {
private:
double volume;
public:
explicit Box(double v) : volume(v) {}
double getVolume() const { return volume; }
};
void processBox(const Box& box) {
std::cout << "Processing box with volume: " << box.getVolume() << std::endl;
}
int main() {
Box b1(100.0); // OK
processBox(b1); // OK
// Box b2 = 200.0; // Ошибка: инициализация копии недопустима
// processBox(300.0); // Ошибка: неявное преобразование недопустимо
processBox(Box(400.0)); // OK: явная конструкция
return 0;
}
Благодаря explicit
предотвращаются малозаметные баги, которые появляются из-за неожиданных неявных преобразований.
Конструкторы копирования и перемещения
Это специальные конструкторы, которыми из имеющихся объектов создаются новые:
#include <cstring>
class String {
private:
char* data;
public:
// Конструктор по умолчанию
String() : data(nullptr) {}
// Конструктор с Си-строкой
String(const char* str) {
if (str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
} else {
data = nullptr;
}
}
// Конструктор копирования
String(const String& other) {
if (other.data) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
} else {
data = nullptr;
}
}
// Конструктор перемещения
String(String&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// Деструктор
~String() {
delete[] data;
}
void display() const {
if (data) {
std::cout << data;
} else {
std::cout << "(empty)";
}
std::cout << std::endl;
}
};
int main() {
String s1("Hello");
String s2 = s1; // Конструктор копирования
String s3 = std::move(s1); // Конструктор перемещения
std::cout << "s1: ";
s1.display();
std::cout << "s2: ";
s2.display();
std::cout << "s3: ";
s3.display();
return 0;
}
В этом примере демонстрируются конструкторы копирования и перемещения, важные для эффективного создания объектов и передачи ресурсов.
Реальный сценарий: класс подключения к базе данных
Рассмотрим практическое применение множественных конструкторов в классе подключения к базе данных:
#include <string>
#include <iostream>
class DBConnection {
private:
std::string host;
int port;
std::string username;
std::string password;
std::string database;
public:
// Конструктор по умолчанию
DBConnection()
: host("localhost"), port(3306), username("root"), password(""), database("") {}
// Конструктор с хостом и портом
DBConnection(const std::string& h, int p)
: DBConnection() {
host = h;
port = p;
}
// Конструктор со всеми параметрами
DBConnection(const std::string& h, int p, const std::string& u,
const std::string& pwd, const std::string& db)
: host(h), port(p), username(u), password(pwd), database(db) {}
// Конструктор со строкой подключения
explicit DBConnection(const std::string& connectionString) {
// Парсинг строки подключения и задание членов
// Это упрощенная версия
size_t pos = connectionString.find(':');
if (pos != std::string::npos) {
host = connectionString.substr(0, pos);
port = std::stoi(connectionString.substr(pos + 1));
}
}
void connect() {
std::cout << "Connecting to " << host << ":" << port
<< " (User: " << username << ", DB: " << database << ")" << std::endl;
// Сам код подключения помещается сюда
}
};
int main() {
DBConnection conn1;
DBConnection conn2("db.example.com", 5432);
DBConnection conn3("db.example.com", 5432, "admin", "secret", "mydb");
DBConnection conn4("192.168.1.100:3307");
conn1.connect();
conn2.connect();
conn3.connect();
conn4.connect();
return 0;
}
В этом примере показано, как множественными конструкторами обеспечивается гибкость при инициализации объекта подключения к базе данных с различными уровнями детализации.
Производительность
При реализации множественных конструкторов учитываются такие аспекты производительности:
- Порядок инициализации: члены инициализируются в порядке их объявления в классе, а не порядке их появления в списке инициализации конструктора.
- Аргументы по умолчанию против множественных конструкторов: иногда эти аргументы эффективнее множественных конструкторов.
- Делегирование конструктора: несмотря на удобство, злоупотребление этим делегированием сказывается на производительности из-за многочисленных вызовов функций.
Вот пример сравнения аргументов по умолчанию с множественными конструкторами:
class Example {
private:
int a, b, c;
public:
// Использование аргументов по умолчанию
Example(int x = 0, int y = 0, int z = 0) : a(x), b(y), c(z) {}
// Альтернатива: множественные конструкторы
/*
Example() : a(0), b(0), c(0) {}
Example(int x) : a(x), b(0), c(0) {}
Example(int x, int y) : a(x), b(y), c(0) {}
Example(int x, int y, int z) : a(x), b(y), c(z) {}
*/
void display() const {
std::cout << a << ", " << b << ", " << c << std::endl;
}
};
int main() {
Example e1;
Example e2(1);
Example e3(1, 2);
Example e4(1, 2, 3);
e1.display();
e2.display();
e3.display();
e4.display();
return 0;
}
Версия с аргументами по умолчанию лаконичнее и потенциально эффективнее: с нею избегаются повторные определения функций.
Типичные ошибки и как их избежать
- Непреднамеренные преобразования. Чтобы избежать неявных преобразований, в конструкторах с одним параметром используйте
explicit
. - Злоупотребление делегированием конструктора. Это делегирование удобно, но злоупотребление им чревато трудностями понимания кода.
- Инициализируются не все члены. Всегда инициализируйте все члены либо в теле конструктора, либо в списке инициализации.
- Циклические зависимости при инициализации. Будьте осторожны с ними при инициализации членов.
- Игнорирование правила трех/пяти. Правило трех: если определяете оператор присваивания копированием, конструктор копирования или деструктор, реализуйте все три. Правило пяти: в версии C++11 и новее используйте еще и оператор присваивания перемещением, а также конструктор перемещения.
Заключение
Множественными конструкторами на C++ обеспечивается гибкость при создании объектов, поэтому классы инстанцируются разными способами.
Понимание этих концепций — от простой перегрузки до сложных вроде списков инициализаторов и делегирования конструкторов — важно для проектирования надежных и гибких классов.
Читайте также:
- Удаление последнего символа строки на C++: методы и их применение
- Языки C и C++. Где их используют и зачем?
- 9 странностей Python для C++ программистов
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: C++ Multiple Constructors: Practical Guide