C++ 构造函数的理解

C++构造函数的理解

相对于C语言来讲,C++有一个比较好的特性就是构造函数,即类经过一个或者几个特殊的成员函数来控制其对象的初始化过程。构造函数的任务,就是初始化对象的数据成员,不管什么时候只要类的对象被建立,就会执行构造函数。c++

构造函数的语法

构造函数的名字必须和类名相同,与其余函数不同的是,构造函数没有返回值,并且其必须是公有成员,由于私有成员不容许外部访问,且函数不能声明为const类型,构造函数的语法是这样的:函数

class Test
{
    public:
        Test(){std::cout<<"Hello world!"<<std::endl;}
};
Test object; 
int main(){return 1;}

在main函数执行以前,object被定义时就会调用Test函数,输出"Hello world!"。this

这里只是示范了一个最简单的构造函数的形式,其实构造函数是个比较复杂的部分,有很是多神奇的特性。指针

构造函数的种类

默认构造函数

当咱们程序中并无显式的定义构造函数时,系统会提供一个默认的构造函数,这种编译器建立的构造函数又被称为合成的默认构造函数,合成构造函数的初始化规则是这样的:c++11

  • 若是存在类内的初始值,用它来初始化成员。在C++11的新特性中,C++11支持为类内的数据成员提供一个初始值,建立对象时,类内初始值将用于初始化数据成员。若是在构造函数中又显式地初始化了数据成员,则使用显式初始化的值。
  • 不然,默认初始化该成员。默认初始化意味着和C语言同样的初始化方式,当类对象为全局变量时,在系统加载时初始化为0,而做为局部变量时,因为数据在栈上分配,成员变量值不肯定。

须要注意的是,只有当用户没有显式地定义构造函数时,编译器才会为其定义默认构造函数。code

在某些状况下,默认构造函数是不合适的:对象

  • 如上所说,内部定义的类调用默认构造函数会致使成员函数的值是未定义的。
  • 若是类中包含其余类类型的数据成员或者继承自其余类,且这个类没有默认构造函数,那么编译器将没法初始化该成员。上面提到了能够在类内给成员一个初始值,可是这只对于普通变量,并不支持类的构造。
    当咱们除了自定义的其余构造函数,还须要一个默认构造函数时,咱们能够这样定义:继承

    Test() = default;
    这个构造函数不接受任何参数,等于默认构造函数。接口

初始化列表的构造方式

首先,咱们先须要分清初始化和赋值的概念,初始化就是在新建立对象的时候给予初值,而赋值是在两个已经存在的对象之间进行操做。在构造方式上,这两种是不一样的。内存

构造函数支持初始化列表,它负责为新建立的对象的一个或者几个数据成员赋初值,初始化列表的语法是这样的:

class Test
{
    public:
        Test(int a):x(a),y(2){}
        int x;
        int y;
};

初始化的列表的一个优点是时间效率和空间效率比赋值要高,同时在const类型成员的构造时,普通的赋值构造函数是非法的。当咱们建立一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其常量属性。

因此咱们能够用这种方式为const成员变量写值。

拷贝构造函数

拷贝构造函数的通常形式是这样的:

class Test
{
    public:
        Test(const Test &ob){
            x = ob.x;
            y = ob.y;
        }
    private:
        int x;
        int y;
};

能够很清楚地看出来,构造过程就是将另外一个同类对象的成员变量一一赋值,const修饰是由于限定传入对象的只读属性。看到上面的示例,不知道有没有朋友有所疑问:

为何在构造函数中,用户能够访问到外部同类对象ob的私有变量,不是说私有变量只能经过类的公共函数(通常是get()方法)来访问吗,为何这里能够直接使用ob.x,ob.y ??

若是你有这样的问题,首先不得不认可你是个善于观察且有必定基础的学者,可是对封装的概念并非很清楚。

其实不只仅构造函数能够访问同类对象的私有变量,普通成员函数也能够访问:

class Test
{
    public:
        Test(){};
        void func(const Test& ob){
            std::cout<<ob.x<<std::endl;
        }
        
    private:
        int x=2;
};

这样的写法不会报错且可以正常运行,可是若是func()的函数是这样的:

void func(const AnotherClass& ob){
            std::cout<<ob.x<<std::endl;
        }

那咱们还能不能访问ob的私有变量呢?答案确定是不行的,这不用说。那咱们回到上面的问题,为何能够访问同类对象的私有变量?

其实答案并不难理解,类的封装性是针对类而不是针对类对象。

通俗地来讲,咱们定义类中成员访问权限的初衷是为了保护私有成员不被外部其余对象访问到,通常状况下私有成员被外部访问的方式就是经过公共的函数接口(public),而在类的内部,任何成员函数都能访问私有成员,这种保护是针对不一样的类之间的,因此咱们是在定义类的时候来指定访问权限,而不是在定义对象的时候再指定访问权限。

再者,相同类对象,对于全部的私有变量,彼此知根知底,也就没有什么保护的必要。

既然是这样,类内的构造函数以及其它函数都是类的成员函数,天然能够访问全部数据。

赋值运算符重载

同时,类的构造能够用重载赋值运算符来实现,即"="。

class Test
{
    public:
        Test& operator=(const Test &ob){
            x = ob.x;
            y = ob.y;
            return this;
        }
    private:
        int x;
        int y;
};

在定义类的时候,咱们能够这样:

Test ob1;
Test ob2 = ob1;

默认拷贝构造函数的陷阱

当咱们没有指定拷贝构造函数或者没有重载赋值运算符时,系统会生成默认的对应构造函数,分别为合成拷贝构造函数和合成拷贝赋值运算符。即便用户没有在类中定义相对应拷贝赋值操做,咱们照样可使用它:

Test ob1;
Test ob2(ob1);
Test ob3 = ob2;

编译器生成的默认拷贝赋值构造函数会将对应的成员一一赋值,是否是很是方便?

既然编译器生成的默认拷贝赋值构造函数就能完成任务,为何咱们还要本身去定义构造函数呢?这是否是画蛇添足?

非也!!!

若是类型成员所有都是普通变量是没有问题的,可是若是涉及到指针,简简单单地复制指针也是没有问题的,最要命的是若是指针指向的动态内存,这样就会有两个不一样类的成员指向同一片动态内存,而析构函数在释放内存时,必然形成double free,咱们能够看下面的例子:

class Test
{
    public:
        Test(){p = new int(4);}
        ~Test(){delete p;}
        int *p;
};
Test ob1;
Test ob2 = ob1;
int main(){}

而后编译运行:

g++ -std=c++11 test.cpp -o test
./test

这段程序不作任何事,仅仅是经过编译器生成的合成拷贝赋值运算符,运行结果:

*** Error in `./a.out': double free or corruption (fasttop): 0x085dca10 *** Aborted (core dumped)

很明显,和上面所提到的同样,动态内存的double free致使程序终止。为了观众朋友们能更清晰地理解这个过程,咱们再来对程序作一个step by step解析:

  • 构造类对象ob1,这是调用了构造函数,为ob1.p分配了内存空间。
  • 用合成拷贝赋值构造函数构造类对象ob2 = ob1,至关于执行了语句:ob2.p = ob1.p;
  • main()函数执行完毕,全局函数的运行周期结束,系统回收内存,先调用ob1的析构函数,将ob1.p指向的内存释放。
  • 调用ob2的析构函数,将ob2.p指向的内存释放,可是因为ob2.p的内存已经在上一步被释放,因此形成了double free。

事实上,这种现象在C++中有两个专用名词来描述:"浅拷贝"和"深拷贝"。

因此,在使用编译器默认的合成构造函数时,咱们要很是当心这一类的陷阱,即便是目前没有指针成员函数,也要本身写拷贝赋值构造函数,这样有利于代码的扩展和维护。

可是,话说回来,若是我每次实现一个很简单的需求,都要定义复制拷贝构造函数,一个一个成员去赋值,这样也是很烦人的,在新标准下,C++提供了一种方法来"解决"这个问题。

阻止拷贝

用户能够禁止使用拷贝函数,只要做这样声明:

Test(Test &ob) = delete;
Test &operator(Test &ob) = delete;
事实上,部分编译器默认禁止合成的拷贝赋值构造函数。

这样,在使用者想使用默认的拷贝赋值构造函数时,编译器将无情地报错。
***

移动构造函数

在说到移动构造函数以前,咱们得先介绍一下新标准下一种新的引用类型——右值引用。右值引用就是必须绑定到右值的引用,左值的引用用&,而右值的引用则用&&。右值引用有一个重要的性质,即只能绑定到一个将要销毁的对象。

通俗地说,右值一般为临时变量,字面值,未接受的返回值等等,它们没有固定地址。
而左值一般是变量。总而言之,左值持久,右值短暂。

下面是引用和右值引用的示例:

int x = 30;
int &r = x;  //正确,左值引用
int &&r = x; //错误,x为左值,&&r为右值引用
int &&r = 3; //正确,右值引用
const int &r = 3;  //正确,const左值能够对右值引用

因为右值引用只能绑定到临时对象,咱们能够知道它的特色:

  • 所引用的对象将要被销毁
  • 该对象没有其余用户
    这两个特性则意味着:使用右值引用的代码能够自由地接管所引用的对象的资源。可想而知,右值引用的特色是"窃取"而不是"生成",在效率上天然就有所提升。

若是如今有一个左值,咱们想将它做为右值来处理,应该怎么办呢?答案是std::move()函数,语法是这样的:

int x = 30;
int &&r = std::move(x);

可是正如右值的特性而言,将左值转换成右值的时候,你得确保这个左值将再也不使用,建议使用std::move(),由于这样的函数名老是容易出现命名冲突。

让咱们再回到移动构造函数,各位朋友们应该从前面的铺垫已经猜到了这是个什么样的实现,是的,它的特色就是接受一个右值做为参数来进行构造。实现是这样的:

class Test
{
    public:
        Test(){p = new int(10);}
        ~Test(){delete p;}
        Test(Test &&ob) noexcept{
            p = ob.p;
            ob.p = nullptr;
        }
        int *p;
};

可能朋友们看了上面的实现会有两个疑问:

  • 为何函数要加上noexcept声明?
  • 为何要加上 ob.p=nullptr 这个操做?
    刚刚咱们提到了拷贝赋值构造函数的浅拷贝问题(即指针部分仅仅是复制),很显然,那样是不行的。可是在移动构造函数中,咱们依然是浅拷贝,为何这样又能够?

从上面的示例能够看出移动构造函数的参数是一个右值引用,咱们上面有提到,移动构造函数的特色是"窃取"而不是生成。就至关于将目标对象的内容"偷过来",既然目标对象的内存原本就是存在的,因此不会由于失败问题而抛出异常。当咱们编写一个不抛出异常的移动操做时,有必要通知标准库,这样它就不会为了可能的异常处理而作一些额外工做,这样能够提高效率。

再者,咱们将右值对象的内容偷过来,可是右值对象依然是存在的,它依旧会调用析构函数,若是咱们不将右值的动态内存指针赋值为null,右值对象调用析构函数时将释放掉这部分咱们好不容易偷过来的内存。就像上面的例子所示,咱们不得不将ob.p指针置为空。
口说无凭,咱们来看下面的示例:

class Test
{
    public:
        Test(void){p=new int(50);
        }
        Test(Test &&ob) noexcept{
            p = ob.p;
            //ob.p = nullptr;     
        }
        ~Test(){delete p;}
        int *p;
};
Test ob1;
int main()
{
    Test ob2 (std::move(ob1));
}

在示例中,咱们将ob.p = nullptr;这条语句注释,而后使用无参构造函数构造ob1,而后将ob1转为右值来构造ob2.咱们来看运行结果:

*** Error in `./a.out': double free or corruption (fasttop): 0x09f12a10 ***
Aborted (core dumped)

果真如我所料,出现了double free的错误,这是由于在移动构造函数中传入的右值对象ob在使用完后调用了析构函数释放了p,而对象ob2偷到的仅仅是一个指针的值,指针指向的内容已经被释放了,因此在程序执行完成以后再调用析构函数时就会出现double free的错误。
为了再验证一个问题,咱们将上面的例子中加上ob.p = nullptr;,并将main()函数改为这样:

class Test
{
    public:
        Test(void){p=new int(50);
        }
        Test(Test &&ob) noexcept{
            p = ob.p;
            ob.p = nullptr;
        }
        ~Test(){delete p;}
        int *p;
};
Test ob1;
int main()
{
    Test ob2 (std::move(ob1));
    cout<<*ob1.p<<endl;
}

咱们来看看已经被转换成右值的ob1个什么状况,运行结果是这样的:

Segmentation fault (core dumped)

好吧,其实这是显而易见的,ob1.p已经在移动构造函数中被置为nullptr了。

为何C++11要添加这个新的特性呢?从效率上出发,在程序运行的时候,因为中间过程会出现各类各样的临时变量,每建立一个临时变量,就会多一次对资源的构造和析构的消耗,若是咱们能将临时变量的资源接管过来,就能够省下相应的构造和析构所带来的消耗。

隐式转换构造函数

C++中,当类有一个构造函数接收一个实参,它实际上定义了转换为此类类型的隐式转换机制,又是咱们把这种构造函数称为转换构造函数。

官方解释老是像数学公式同样难以理解,通俗地说,当一个类A有其中一个构造函数接受一个实参(类型B)时,在使用时咱们能够直接使用那个构造函数参数类型B来临时构造一个类A的对象,好像我也没解释清楚?好吧,直接上代码看:

class Test{
public:
    Test(string s,int para = 1){
        str = s;
    }
    void add(Test ob){
        str += ob.str;
    }
    string str;
};
Test ob1("downey");
int main()
{
    ob1.add(string("downey!"));
    cout<<ob1.str<<endl;
}

运行结果:

downeydowney!

如码所示,Test类有一个构造函数,能够接收一个string类的实参(能够由一个实参构造并不表明只能有一个形参),而add()方法接受一个Test类类型参数,在调用add()方法时,咱们直接传入一个string类型,触发隐式转换功能,编译器将自动以string做为实参构造一个Test的临时类对象来传入add()方法,程序结束以后将释放临时变量。
须要注意的是,隐式转换只支持一次转换,若是咱们将main()函数改为这样:

int main()
{
    ob1.add("downey!");   
    cout<<ob1.str<<endl;
}

编译器须要将"downey"转换成string类型,而后再进行一次转换,这样是不支持的。在编译阶段就会报错:

error: no matching function for call to XXX

同时,若是咱们在声明add()函数时习惯性地使用了左值引用:

void add(Test &ob){      //使用引用,&
        str += ob.str;
    }

这样又是什么结果呢?

答案是,编译出错。这又是为何?若是你有仔细看上面的隐式转换过程就能够知道,在使用隐式转换时生成了一个临时变量(类型同函数形参),而临时变量是右值,是不能使用左值引用的。报错信息以下:

error: no matching function for call to XXX  //左值引用不匹配,因此这里找不到匹配的方法。

阻止隐式转换

使用explicit关键字修饰函数能够阻止构造函数的隐式转换,并且explicit只支持直接初始化时使用,也就是在类内使用,同时,只对一个实参的构造函数有效。在STL中咱们随时能够看到explicit的影子。
下面是示例:

class Test{
public:
    explicit Test(string s,int para = 1){
        str = s;
    }
    void add(Test ob){
        str += ob.str;
    }
    string str;
};
Test ob1("downey");
int main()
{
    ob1.add(string("downey!"));    //报错,no matching function for call to XXX,由于这里不支持隐式转换
    cout<<ob1.str<<endl;
}

同时,若是用户试图在类外声明时使用explicit关键字,将会报错:

error: only declarations of constructors can be ‘explicit’

结语

C++真是魔鬼!!!

好了,关于C++构造函数的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

相关文章
相关标签/搜索