• 构造函数详解


    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.
    "; }
        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.
    "; }
        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 = '';
            }
        }
    
        // 拷贝构造函数
        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));
    
  • 相关阅读:
    mysql 基础sql语句
    mysql存储引擎概述
    docker命令总结
    python链接postgresql
    Log4.net示例
    postgresql 使用游标笔记
    npm常用命令
    Nginx命令
    Ubuntu命令总结
    NHibernate总结
  • 原文地址:https://www.cnblogs.com/yanghh/p/12980542.html
Copyright © 2020-2023  润新知