Constructors in C++

Language:中文/EN

Constructors in C++

Each class defines how its objects are initialized through one or more special member functions known as constructors. The constructor’s task is to initialize the data members of a class object. Whenever an object of the class is created, the constructor is executed.

The constructor’s name is the same as the class name, but it does not have a return type. A class can include multiple constructors, similar to other overloaded functions, but different constructors must differ in the number or type of parameters.

Constructors cannot be declared as const. During the creation of constant objects, values can be assigned to parameters until the object is fully created and acquires the const attribute.

Default Constructor

When we do not provide a constructor for a class, the compiler implicitly defines one for us, known as the synthesized default constructor. It initializes the class’s data members according to the following rules:

  • If there are in-class initializers, they are used to initialize the members.
  • If not, the member is default-initialized.

The synthesized default constructor is only suitable for very simple classes. For regular classes, you must define your own constructor for the following reasons:

  1. The compiler only creates a default constructor if it finds that the class does not contain any constructors.
  2. For some classes, the synthesized default constructor might perform incorrect operations, such as default-initializing arrays or pointers, resulting in undefined values.
  3. Sometimes the compiler cannot create a default constructor for certain classes. If the class contains members of other classes that do not have default constructors, the compiler cannot perform default initialization.

We can use =default to define a default constructor when we need both regular and default constructors.

class Sales_data {
    Sales_data() = default;  // Default constructor
    Sales_data(const std::string &s) : bookNo(s) {}
}

Constructor Initializer List

For other constructors of the Sales_data class:

Sales_data(const std::string &s) : bookNo(s) {}  // `s` as the initial value for `bookNo`
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
// `s` as the initial value for `bookNo`, `n` as the initial value for `units_sold`, `p*n` as the initial value for `revenue`

The part that appears between the parameter list and the braces is called the constructor initializer list. It is responsible for assigning initial values to one or several data members of the newly created object. Note that constructors should not easily override in-class initializers unless the new value differs from the original.

It is important to note that the initialization order of members is consistent with their order of appearance in the class definition: the first member is initialized first, followed by the second, and so on. The order of initial values in the constructor initializer list does not affect the actual initialization order, so it’s best to align the order of initial values in the constructor with the order of member declarations.

class X {
    int i;
    int j;
public:
    X(int val) : j(val), i(j) {}
    // This will cause an error because `i` is initialized before `j`
};

Delegating Constructors

The new C++11 standard extends the functionality of constructor initializer lists, allowing us to define delegating constructors. A delegating constructor uses other constructors of its class to perform its initialization process, or in other words, delegates some or all of its responsibilities to other constructors.

class Sales_data {
public:
    Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt * price) {}
    // The following constructors delegate to other constructors
    Sales_data() : Sales_data(" ", 0, 0) {}
    Sales_data(std::string s) : Sales_data(s, 0, 0) {}
    Sales_data(std::istream &is) : Sales_data() { read(is, *this); }
}

Implicit Class Type Conversion

In a class, constructors that accept a string or an istream define the conversion rules from these types to the class.

string null_book = "9999";
item.combine(null_book);
// The compiler automatically creates a `Sales_data` object from the given `string`

Note that the compiler will only perform one step of type conversion. For example, converting from a literal to a string object and then to a Sales_data object is not allowed.

When we do not need type conversion, we can use the explicit keyword to suppress it.

class Sales_data {
public:
    Sales_data() = default;
    explicit Sales_data(const std::string &s) : bookNo(s) {} // Cannot convert from `string` to `Sales_data`
    explicit Sales_data(std::istream &);
}

When not using implicit conversion, explicit conversion can be used instead.

item.combine(static_cast<Sales_data>(cin));