c++的构造函数种类大全

1.构造函数

从语法上讲,构造函数是与类同名的函数。功能是用于初始化数据成员。构造函数没有返回类型,参数可以有,也可以没有,没有参数的构造函数称为默认构造函数,也称为无参构造函数。有些情况下,必须提供默认的构造函数,否则就会编译报错。

class Student
{
public:
    Student(int initAge);  // 有参构造函数
    void setAge(int age);
    int getAge(int age);

private:
    int m_age;
};
int Student::getAge(int age)
{
    return m_age;
}

void Student::setAge(int age)
{
    m_age = age;
}

Student::Student(int initAge)
{
    setAge(initAge);
}

2.使用构造函数

构造函数用来创建对象并初始化其值。在基于栈和堆区都可以使用构造函数

在栈中使用构造函数

Student st1(9), st2(11);
// 或者使用同一初始化语法
// Student st1{9}, st2{11};
cout << "st1.age = " << st1.getAge() << endl;
cout << "st2.age = " << st2.getAge() << endl;

注意不要显示的调用构造函数。下面的方式都是错误的:

Student st.Student(10); 
或者
Student st;
st.Student(10);

在堆上使用构造函数(下面三种方式等同)

// 使用智能指针
auto smartStu{make_unique<Student>(10)};

// Student *pSt{new Student(5)}; 或者
Student *pSt{new Student{6}};

Student *pStu{nullptr};
pStu = new Student{16};

3.提供多个构造函数

一个类中可以提供多个构造函数。所有的构造函数都是相同的名字,但是不同的构造函数有不同的参数数量或者不同的参数类型。这种又称为重载,编译的时候,编译器会选择与参数匹配的哪个构造函数。

class A
{
public:
    A(int a) : m_a(a) { cout << "m_a = " << m_a << endl; }
    A(double b) : m_b(b) { cout << "m_b = " << m_b << endl; }

private:
    int m_a;
    double m_b;
};

int main()
{
	A a{10}, b{1.1};
}

4.默认构造函数

如果类中没有指定任何构造函数,那么编译器会自动生成无参构造函数。类中所有的对象成员都可以调用编译器默认生成的构造函数,但是不会初始化语言的基本类型,例如int,double。然后如果声明了任何构造函数,编译器则不在自动生成无参的的构造函数。那么如果在上述代码中声明无参对象A a;则就会报错。

显示默认的默认构造函数

在c++11及之后,可以不用手动编写空的默认的构造函数,可以使用default关键字,让编译器生成一个默认的构造函数。

class Student
{
public:
    Student() = default;   // 指定编译器生成默认的无参构造函数
    Student(int initAge);
    void setAge(int age);
    int getAge() const;

private:
    int m_age;
};

显示删除的默认构造函数

当定义一个只有静态方法的类时,这个类没有任何构造函数,也不想让编译器生成默认的构造函数。那么可以使用delete关键字显示的删除默认构造函数。

class Student
{
public:
	Student() = delete;
};

注意: 如果类的数据成员具有删除的构造函数,那么该类的构造函数也会自动删除!

5.构造函数初始化器

在构造函数参数列表与构造函数体之间使用冒号开始,逗号分隔的方式初始化数据成员。这种方式就是构造函数初始化器。

class A
{
public:
    // 使用初始化器初始化数据成员
    A(int a, double b) : m_a(a), m_b(b) {}

private:
    int m_a;
    double m_b;
};

使用构造函数初始化器与在构造函数体内初始化数据成员不同,当c++创建某个对象时,必须在调用调用构造函数前创建对象的所有数据成员。如果数据成员本身就是对象,那么在创建这个数据成员时,必须为其调用构造函数。在构造函数体内给某个对象赋值时,并没有真正创建这个对象。只是改变对象的值。构造函数初始化器允许在创建数据成员时赋初值,这样做比在后面赋值效率高。

如果类的数据成员是具有默认构造函数的类的对象,那么就不必在构造函数初始化器中显示的初始化对象。若是没有,则必须显示初始化对象。

class A
{
public:
    A(int a){};
};

class B
{
public:
    B(){};
private:
    A a;
};

上述代码,编译会直接报错,因为类A没有默认的无参构造函数。解决方式如下:

class A
{
public:
    A(int a){};
};

class B
{
public:
    // 在类B中的构造函数中显示的初始化对象
    B() : a{2} {};

private:
    A a;
};

关于构造函数初始化器需要特别注意,数据成员的初始化顺序是按照它们在类定义中出现的先后顺序来决定的。而不是在构造函数初始化器中的顺序。

class Foo
{
public:
    Foo(double value) : m_value{value}
    {
        cout << "Foo::m_value = " << m_value << endl;
    };

private:
    double m_value{0};
};

class MyClass
{
public:
    MyClass(double value) : m_value{value}, m_foo{m_value}
    {
        cout << "MyClass m_value = " << m_value << endl;
    }

private:
    double m_value{0};
    Foo m_foo;
};

int main()
{
    MyClass instance{1.2};
    return 0;
}

输出:

Foo::m_value = 1.2
MyClass m_value = 1.2

上面的代码输出的一点毛病没有。但是如果将MyClass类的数据成员调换一下顺序呢?

class MyClass
{
public:
    MyClass(double value) : m_value{value}, m_foo{m_value}
    {
        cout << "MyClass m_value = " << m_value << endl;
    }

private:
    Foo m_foo;          // 顺序调换了
    double m_value{0};
};

输出结果为:

Foo::m_value = 4.64117e-310
MyClass m_value = 1.2

显然,这和我们预期的值不同。你可能会认为基于构造函数的初始化器,先初始化m_value,再调用Foo的构造函数时使用m_value。但是c++并不是这样执行的。是按照数据成员在类中的顺序对数据成员进行初始化。因此,这里首先使用了未初始化的m_value,调用Foo的构造函数。

6.拷贝构造函数

c++中有一种特殊的构造函数,称为拷贝构造函数,允许所创建的对象是另一个对象的副本。以下是student类拷贝构造函数声明:

class Student
{
public:
    Student(const Student &src){}; // 拷贝构造函数
};

与其他构造函数类似,它也没有返回值。在拷贝构造函数内,应该复制源对象的所有数据成员。示例如下:

class Student
{
public:
    Student(const Student &src) : m_value{src.m_value} {}; // 拷贝构造函数

private:
    double m_value;
};

如果没有编写拷贝构造函数,编译器会自动生成一个,用源对象中相应的数据成员的值初始化新对象的每个数据成员。如果数据成员是对象,初始化意味着调用它的拷贝构造函数。一般情况下,拷贝构造函数不需要程序员编写。

何时调用拷贝构造函数

  1. c++中传递函数的参数默认是值传递,这意味着函数或者方法接收某个值或者对象的副本。因此,无论何时给函数或者方法传递一个对象,编译器都会调用新对象的拷贝构造函数进行初始化。注意 向函数或者方法传递对象时,为避免复制对象,可以采用对象的引用作为参数。按引用传递对象通常比按值传递对象的效率高。因为只需要复制对象的地址,不需要复制全部对象内容。

  2. 当函数按值返回对象的时候,也会调用拷贝构造函数。

  3. 显示的调用拷贝构造函数。

    Student s1(1.0);
    Student s2{s1};
    

拷贝构造函数也可以使用default让编译器生成默认的,也可以使用delete删除拷贝构造函数,对象无法被复制。这样可以用于禁止按值传递对象。

注意: 如果类的数据成员具有删除的拷贝构造函数,那么该类的拷贝构造函数也会自动删除!

7.初始化列表构造函数

初始化列表构造函数(initializer-list constructor)将std::initializer_list< T >作为第一个参数,并且没有任何其他参数。std::initializer_list< T >类模版定义在<initializer_list>中。

class MyClass
{
public:
    MyClass(initializer_list<double> args)
    {
        for (const auto &value : args)
        {
            m_vec.push_back(value);
        }
    }

    void print() const
    {
        for (const auto &value : m_vec)
        {
            cout << value << ", ";
        }
        cout << endl;
    }

private:
    vector<double> m_vec;
};

int main()
{
    MyClass a{1, 2, 3, 4, 5};
    a.print();
    return 0;
}

初始化列表并不限于构造函数,还可以用于普通函数。

8.委托构造函数

委托构造函数允许构造函数调用同一个类的其他构造函数。然而这个调用不能放在构造函数体内,而必须放在构造函数初始化器中,且必须是列表中唯一的成员初始化器。

class MyClass
{
public:
    MyClass(double val) : MyClass{doubleToString(val)} {}
    MyClass(string val) : m_value{val} {}

    string doubleToString(double val)
    {
        return to_string(val);
    }

private:
    string m_value;
};

当使用委托构造函数的时候,要注意避免出现构造函数的递归。例如:

class MyClass 
{
    MyClass(char c) : MyClass{1.2} {}
    MyClass(double d) : MyClass{'m'} {}
};

9.转换构造函数和显示构造函数

下列单参数double和string_view构造函数可以用于将double或者string_view转换为MyClass。这种构造函数成为转换构造函数。编译器可以使用这些构造函数执行隐式转换。

class MyClass
{
public:
    MyClass() = default;
    MyClass(double value) : m_double{value} { cout << "double " << value << endl; }
    MyClass(string_view value) : m_string{value} { cout << "string " << value << endl; }
    MyClass(const MyClass &src) = default;

private:
    double m_double;
    string m_string;
};

int main()
{
    MyClass s(1.1);
    
    // 发生隐式转换,转换成MyClass
    s = 5;
    s = "hello"sv; // sv是c++17引入的,将字符数组字面量转成 string_view 。
    return 0;
}

编译器发生了隐式转换可能并不总是我们想要的结果,可以通过将构造函数标记为explicit,可以防止编译器执行此类隐式转换,该关键字指在类定义中出现。

class MyClass
{
public:
    MyClass() = default;
    MyClass(double value) : m_double{value} { cout << "double " << value << endl; }
    explicit MyClass(string_view value) : m_string{value} { cout << "string " << value << endl; }
    MyClass(const MyClass &src) = default;

private:
    double m_double;
    string m_string;
};

在c++11之前,转换构造函数只能有一个参数,在c++11之后,由于支持列表初始化,转换构造函数可以有多个参数。例如:

class MyClass
{
public:
    MyClass(int val) : m_a{val} {}
    MyClass(int va1, int va2) : m_a{va1}, m_b{va2} {}

private:
    int m_a;
    int m_b;
};

void process(const MyClass &c)
{
    // do something
}

int main()
{
    // 发生了隐式转换,int类型转换成了MyClass
    process(1);
    process({1});
    process({1, 2});

    return 0;
}

为了避免发生隐式转换,将两个构造函数都标记为explicit。

explicit MyClass(int val) : m_a{val} {}
explicit MyClass(int va1, int va2) : m_a{va1}, m_b{va2} {}

有个explicit关键字,则必须显示地执行这些转换

process(MyClass(1));
process(MyClass{1});
process(MyClass{1, 2});

从c++20开始,可以将布尔参数传递给explicit,以将其转换为有条件的explicit。语法如下:

explicit(true) MyClass(int);

当然,仅仅编写explicit(true)就等价于explicit,但是它在使用所谓类型萃取的泛型模板代码中更加有用。使用类型萃取,可以查询给定类型的某些属性,例如某个类型是否可以转换成另外一个类型。类型萃取的结果可以用作explicit()的参数。类型萃取允许编写高级泛型代码。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐