构造函数详解

1. 构造函数基本概念

1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;

2)构造函数在定义时可以有参数;

3)没有任何返回类型的声明;

二个特殊的默认构造函数:

1)默认无参构造函数:当类中没有定义构造函数时,编译器提供一个默认的无参构造函数,并且其函数体为空。

2)默认拷贝构造函数:当类中没有定义拷贝构造函数时,编译器提供一个默认的拷贝构造函数,简单的进行成员变量的值复制。

构造函数调用规则:

1)当类中没定义任何构造函数时,C++编译器会提供默认无参构造函数和默认拷贝构造函数。

2)当类中定义了拷贝构造函数时,C++编译器不会提供无参数构造函数。

3)当类中定义了任意的非拷贝构造函数(即:当类中提供了有参构造函数或无参构造函数),C++编译器不会提供默认无参构造函数。

4)默认拷贝构造函数进行的是浅拷贝。

5)当类中定义了拷贝构造函数时,C++编译器不会提供移动构造函数了。

2. 构造函数的分类及调用

我们来看如下代码:

class Test
{
private:
    int a, b;

public:
    Test() {}                  // 无参数构造函数
    Test(int a, int b) {}      // 带参数的构造函数
    Test(const Test &obj) {}   // 赋值构造函数

public:
    void init(int _a, int _b)
    {
        a = _a;
        b = _b;
    }
};

1)无参数构造函数:调用方法如下

Test t1, t2;
Test t1 = Test();        // 这样才是调用默认构造函数,这时必须带有括号

2)带参数构造函数

Test t1(20, 10);         // 括号法: C++编译器默认调用有参构造函数 
Test t2 = (20, 10);      // 等号法: C++编译器默认调用有参构造函数
Test t3 = Test(20, 10);  // 直接调用构造构造函数法: 程序员手工调用构造函数产生了一个对象

3)赋值(拷贝)构造函数:顾名思义,即由其它对象来初始化自己。下面介绍赋值构造函数的三种调用场景(调用时机)。

a. 定义变量时,用对象1初始化对象2

class Test
{
public:
    Test() { cout << "我是构造函数,自动被调用了" << endl; }
    Test(int _a) : a(_a) {}
    Test(const Test &obj2) { cout << "我也是构造函数,我是通过另外一个对象obj2,来初始化我自己" << endl; }
    ~Test() { cout<<"我是析构函数,自动被调用了"<<endl; }

private:
    int a;
};

int main()
{
    Test a1;
    Test a2 = a1; // 用 a1 初始化 a2
    Test a3(a1);  // 这样写也是用 a1 初始化 a3
    return 0;
}

b. 实参变量初始化形参变量

class Location 
{ 
public:
    Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; }
    Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; }
    ~Location() { cout << X << "," << Y << " Object destroyed." << endl; }
    int GetX()  { return X; }     
    int GetY()  { return Y; }

private:
    int X, Y;
};

void f(Location  p)   
{ 
    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; 
}

int main()
{  
    Location A(1, 2);
    f(A);  // 调用f会构造一个临时对象p,此时会调用拷贝构造函数
    return 0;
}

c. 函数返回匿名对象,会在栈上面通过拷贝构造函数产生一个临时对象(一般会被编译器优化),然后原来的栈变量被析构。

之后就取决于程序员怎么来接收这个匿名对象,不同的接法差别在于会不会多一次赋值运算符的调用。

注:可以在编译时设置编译选项-fno-elide-constructors用来关闭返回值优化效果。

class Location 
{ 
public:
    Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; }
    Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; }
    ~Location() { cout << X << "," << Y << " Object destroyed." << endl; }
    int GetX()  { return X; }     
    int GetY()  { return Y; }

private:
    int X, Y;
};

void f(Location p)   
{ 
    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; 
}

/*
 * 当函数需要返回一个对象,他会在栈中创建一个临时对象,存储函数的返回值。
 * 这个临时对象也是匿名对象,构造它时会调用拷贝构造函数,用A来初始化这个匿名对象。
 * 然后函数调用结束,A被销毁.
 * 但是这个临时对象的构造一般会被编译器优化掉,所以自己测试的时候一般不会调用拷贝构造函数了。 
 */
Location g()
{
    Location A(1, 2);
    return A;
}

int main()
{  
    Location B;
    B = g();           // 若返回的匿名对象,赋值给另外一个同类型的对象,那么匿名对象会被析构。(会调用赋值运算符)
    Location C = g();  // 若返回的匿名对象,来初始化另外一个同类型的对象,那么匿名对象会直接转成新的对象。(啥也不调用)
    return 0;
}

4)移动构造函数:C++11引入移动语义----临时对象资源的控制权(堆内存)全部交给目标对象。注意一下,临时对象和目标对象是两个独立的不同对象,

移动构造函数也不是说将临时对象直接变成目标对象,只是将临时对象所控制的资源进行浅拷贝(拷贝指针),而没有了深拷贝然后临时对象就无法

访问这个资源了,但临时对象本身还是要被析构的。因为浅拷贝是难以避免的,所以类如果没有堆上的资源,也就没必要实现移动构造函数。

下面举个例子:

static unsigned int cCount;  //统计拷贝构造函数调用次数
static unsigned int mCount;  //统计移动构造函数调用次数

class MyString
{
public:
    // 构造函数
    MyString(const char* cstr = 0)
    {
        if (cstr) 
        {
            m_data = new char[strlen(cstr) + 1];
            strcpy(m_data, cstr);
        }
        else 
        {
            m_data = new char[1];
            *m_data = '\0';
        }
    }

    // 拷贝构造函数
    MyString(const MyString& str) 
    {
        cCount++;
        m_data = new char[strlen(str.m_data) + 1];
        strcpy(m_data, str.m_data);
    }

    // 移动构造函数
    MyString(MyString&& str)
    {
        mCount++;
        m_data = str.m_data;  // 目标对象接管堆上资源
        str.m_data = nullptr; // 临时对象不再指向那个资源了
    }

    ~MyString() { delete[] m_data; }

private:
    char* m_data;
};

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);    // 先分配好1000个空间
    for(int i = 0; i < 1000; i++)
    {
        vecStr.push_back(MyString("hello"));
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;
    return 0;
}

运行可知道程序调用了1000次的移动构造函数,这样就不会去重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针

指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。

抛出一个问题:我们知道const引用也是能够被右值初始化的,那编译器怎么知道调用哪个构造函数呢?是拷贝还是移动?

编译器判断传入的参数是一个右值,会认为移动构造函数是一个更好的匹配。

对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供

std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是

用移动构造函数吧。。。

注意一下:将一个临时对象赋值给 T &&x 是延长临时对象的生命周期的做法(不会移动或者拷贝),是右值引用。若赋值给 T x 则会触发移动或者赋值构造函数。

还是上面的类,现在改写一下main函数。

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);      //先分配好1000个空间
    for(int i = 0; i < 1000; i++)
    {
        MyString tmp("hello");
        vecStr.push_back(tmp); //调用的是拷贝构造函数
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;

    cCount = 0;
    mCount = 0;

    vector<MyString> vecStr2;
    vecStr2.reserve(1000);     //先分配好1000个空间
    for(int i = 0; i < 1000; i++)
    {
        MyString tmp("hello");
        /*
         * 调用的是移动构造函数
         * 此时tmp指向的资源已经为null了,但对象在表达式结束时尚未析构,作用域结束后才析构
         */
        vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;
    return 0;
}

// 输出如下
cCount:1000
mCount:0
cCount:0
mCount:1000

需要注意一下:如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就

去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!

3. 构造函数隐式转换

用单个实参(也可以有多个实参,但是除了第一个参数,其它参数必须有默认值)来调用的构造函数定义了从形参类型类类型的一个隐式转换。

隐式转换没有特别的语法,只要类型满足构造函数的参数即可以触发。简单举个例子

class Test  
{  
public:  
    bool same(const Test &rbs) const { return isbn == rbs.isbn; }  
    Test(const std::string &book = "7115145547") : isbn(book) {}  
private:  
    std::string isbn;  
};  

int main()
{
    Test trans;  
    string null_book = "9-999-99999-9";  
    trans.same(null_book);    // 这里会发生隐式类型转换,从string转换为test(因为有构造函数可以用一个string做参数),建立一个临时的类的对象
    return 0; 
}

为了避免这个情况的发生,可以将类的构造函数声明为explicit,然后显示调用:

explicit Test(const std::string &book = "7115145547") : isbn(book) {}
trans.same(Test(null_book));

原文链接: https://www.cnblogs.com/yanghh/p/12980542.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/198011

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午7:44
下一篇 2023年2月12日 下午7:44

相关推荐