构造函数的理解牧野星辰

相对于C语言来说,C++有一个比较好的特性就是构造函数,即类通过一个或者几个特殊的成员函数来控制其对象的初始化过程。构造函数的任务,就是初始化对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

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

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

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

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

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

如上所说,内部定义的类调用默认构造函数会导致成员函数的值是未定义的。

如果类中包含其他类类型的数据成员或者继承自其他类,且这个类没有默认构造函数,那么编译器将无法初始化该成员。上面提到了可以在类内给成员一个初始值,但是这只对于普通变量,并不支持类的构造。当我们除了自定义的其他构造函数,还需要一个默认构造函数时,我们可以这样定义:

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

首先,我们先需要分清初始化和赋值的概念,初始化就是在新创建对象的时候给予初值,而赋值是在两个已经存在的对象之间进行操作。在构造方式上,这两种是不同的。

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

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

所以我们可以用这种方式为const成员变量写值。

拷贝构造函数的一般形式是这样的:

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

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

如果你有这样的问题,首先不得不承认你是个善于观察且有一定基础的学者,但是对封装的概念并不是很清楚。

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

这样的写法不会报错且能够正常运行,但是如果func()的函数是这样的:

那我们还能不能访问ob的私有变量呢?答案肯定是不行的,这不用说。那我们回到上面的问题,为什么可以访问同类对象的私有变量?

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

通俗地来说,我们定义类中成员访问权限的初衷是为了保护私有成员不被外部其他对象访问到,一般情况下私有成员被外部访问的方式就是通过公共的函数接口(public),而在类的内部,任何成员函数都能访问私有成员,这种保护是针对不同的类之间的,所以我们是在定义类的时候来指定访问权限,而不是在定义对象的时候再指定访问权限。

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

既然是这样,类内的构造函数以及其它函数都是类的成员函数,自然可以访问所有数据。

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

在定义类的时候,我们可以这样:

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

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

既然编译器生成的默认拷贝赋值构造函数就能完成任务,为什么我们还要自己去定义构造函数呢?这是不是多此一举?

非也!!!

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

然后编译运行:

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

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

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

所以,在使用编译器默认的合成构造函数时,我们要非常小心这一类的陷阱,即使是目前没有指针成员函数,也要自己写拷贝赋值构造函数,这样有利于代码的扩展和维护。

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

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

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

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

由于右值引用只能绑定到临时对象,我们可以知道它的特点:

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

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

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

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

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

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

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

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

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

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

为什么C++11要添加这个新的特性呢?从效率上出发,在程序运行的时候,由于中间过程会出现各种各样的临时变量,每创建一个临时变量,就会多一次对资源的构造和析构的消耗,如果我们能将临时变量的资源接管过来,就可以省下相应的构造和析构所带来的消耗。

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

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

运行结果:

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

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

这样又是什么结果呢?

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

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

THE END
0.C++构造函数详解:初始化对象的艺术本文详细介绍了C++中的构造函数,包括其概念、类型(无参、带参及全缺省)、默认构造函数的意义以及C++11中对缺省值的处理。通过实例说明了构造函数如何简化对象初始化过程。 💐 🌸 🌷 🍀 🌹 🌻 🌺 🍁 🍃 🍂 🌿 🍄🍝 🍛 🍤 jvzquC41dnuh0lxfp0tfv8qwjcuscw=361gsvrhng1jfvjnnu1746=5943?
1.C++:类的默认成员函数如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。 【默认成员函数概念】:用户没有显式实现,编译器会生成的成员函数称为默认成员函数 其中两个默认成员函数是用来初始化和清理的分别为:构造函数、析构函数 jvzquC41dnuh0lxfp0tfv8|gkzooa=:253>138ftvkimg8igvcomu8655;<25;5
2.构造函数:c++C++ language reference Welcome back to C++ (Modern C++) Lexical conventions Basic concepts Built-in types Declarations and definitions Built-in operators, precedence, and associativity Expressions Statements Namespaces Enumerations Unions Functions jvzquC41oujo0vnetqyph}3eqo5{j6hp1noctjw{1u77z€6c:0gtr
3.C++构造函数和析构函数(Constructors&Destructors)详解C语言由于global object的诞生比程序进入更早点,所以global object的constructor执行的时间更早于程序的进入点,所谓的default constructor就是没有指定任何的参数的constructor,这篇文章主要介绍了C++ 构造函数和析构函数的相关知识,需要的朋友可以参考下+ 目录 GPT4.0+Midjourney绘画+国内大模型 会员永久免费使用!【 如果你想jvzquC41yy}/lk:30pku1ywqitgn1<7278;k:}3jvo
4.构造函数详解类的6个默认的成员函数 构造函数的概念: 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。 构造函数的特性 函数名与类名相同。 无返回值。 jvzquC41yy}/extpqvk/exr1errvuyqwu/tpvn4eqpyut~hvqt4ivvq
5.Java构造函数具有公共访问级别的构造函数可以在程序的任何部分中使用。 具有私有访问级别的构造函数只能在声明它的同一类中使用。 具有受保护访问级别的构造函数可以在具有在其中声明类的相同包的程序中以及在任何包中的任何后代类内使用。 具有包级访问权限的构造函数可以在声明其类的同一个包中使用。 jvzquC41yy}/y
6.C++拷贝构造函数(复制构造函数)详解当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数(Copy Constructor)。 下面的例子演示了拷贝构造函数的定义和使用: #include<iostream> #include<string> usingnamespacestd; classStudent{ public: Student(stringname="",intage=0,floatscore=0.0f);//普通构造函数 jvzquC41e0hjcwhjgpm/pny1xkkx1;8560nuou
7.PHP:构造函数和析构函数Please be aware of when using __destruct() in which you are unsetting variables Consider the following code: ; } function__destruct() { if($this->error_reporting===true)$this->show_report(); unset($this->error_reporting); jvzquC41yy}/rqu0pgz0njsiwcmf0xtr70jfexs
8.构造函数(C++)|MicrosoftLearn如果类未定义移动构造函数,则在没有用户声明的复制构造函数、复制赋值运算符、移动赋值运算符或析构函数时,编译器会生成隐式构造函数。 如果未定义显式或隐式移动构造函数,则原本使用移动构造函数的操作会改用复制构造函数。 如果类声明了移动构造函数或移动赋值运算符,则隐式声明的复制构造函数会定义为已删除。jvzquC41fqit0vnetqyph}3eqo5{j6hp1evq1lur1euou}wwevusu6hrr
9.C++构造函数详解:一篇搞懂所有构造函数知识(含代码+图解)当你没有定义任何构造函数时,编译器会自动生成一个“空的默认构造函数”。 classStudent{ public: intage; }; intmain(){ Student s;// 默认构造函数被调用(编译器生成) s.age =20; } AI写代码cpp 运行 但如果你一旦写了带参数的构造函数,编译器就不会再生成默认构造函数了,你需要手动写一个! jvzquC41dnuh0lxfp0tfv8|gkzooa?:2:2>9:8ftvkimg8igvcomu866;66:9<=
10.C++——构造函数构造函数是C++中一种特殊的成员函数,它在创建类对象时自动调用,用于初始化对象。 构造,那构造的是什么呢? 构造成员变量的初始化值,内存空间等 一、构造函数的基本概念 定义:构造函数是与类同名的特殊成员函数 特点: 没有返回类型(连void都没有) 创建对象时自动调用 通常声明为public(除非有特殊需求) 可以重载(一个类可以有多个构 jvzquC41dnuh0lxfp0tfv8|gkzooa?6448:548ftvkimg8igvcomu86692617A8
11.JAVA中的构造函数(方法)java这篇文章主要介绍了JAVA中的构造函数(方法),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教+ 目录 一、什么是构造函数 Java构造函数,也叫构造方法,是JAVA中一种特殊的函数。与函数名相同,无返回值。 作用:一般用来初始化成员属性和成员方法的,即new对象产生后,就调用了对象的属性和方法。jvzquC41yy}/lk:30pku1ywqitgn1<6:687cov3jvo
12.C++超详细讲解构造函数C语言可以看到使用编译器生成的默认构造函数我们的日期仍然是随机值。 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 jvzquC41yy}/lk:30pku1jwvkerf1;:2448/j}r
13.构造函数前面的示例显示了初始化新对象的实例构造函数。 类或结构还可以声明静态构造函数,该构造函数初始化类型的静态成员。 静态构造函数是无参数的。 如果未提供静态构造函数来初始化静态字段,C# 编译器会将静态字段初始化为默认值,如C# 类型的默认值文章中所列。 jvzquC41oujo0vnetqyph}3eqo5{j6hp1noctjw{1cif7qg|j0gtr
14.构造函数(C++)|MicrosoftLearn如果类未定义移动构造函数,则在没有用户声明的复制构造函数、复制赋值运算符、移动赋值运算符或析构函数时,编译器会生成隐式构造函数。 如果未定义显式或隐式移动构造函数,则原本使用移动构造函数的操作会改用复制构造函数。 如果类声明了移动构造函数或移动赋值运算符,则隐式声明的复制构造函数会定义为已删除。jvzquC41fqit0vnetqyph}3eqo5{j6HP1evq1lur1euou}wwevusu6hrrA|jg€Bouxi.3=5
15.构造函数前面的示例显示了初始化新对象的实例构造函数。 类或结构还可以声明静态构造函数,该构造函数初始化类型的静态成员。 静态构造函数是无参数的。 如果未提供静态构造函数来初始化静态字段,C# 编译器会将静态字段初始化为默认值,如C# 类型的默认值文章中所列。 jvzquC41oujo0vnetqyph}3eqo5{j6hp1noctjw{1cif7qg|j
16.构造函数基实例构造函数运行。以 Object.Object 开头从每个基类到直接基类的任何实例构造函数。 实例构造函数开始运行。 该类型的实例构造函数运行。 对象初始值设定项运行。 如果表达式包含任何对象初始值设定项,则它们在实例构造函数运行后运行。 对象初始值设定项按文本顺序运行。 使用new 运算符创建实例时,将执行上述操作。 jvzquC41fqit0vnetqyph}3eqo5{j6hp1fuupny1eunbty4rtqmscvrkpi3hwrig1erbu|ju/cte/|ytwezt1ltpuvxve}ttu
17.C++的6种构造函数c++构造函数以值方式返回局部对象(由于编译器的RVO【返回值优化】,所以不会返回对象时不会调用拷贝构造) 构造函数调用规则:默认情况下,编译器会给类至少添加三个函数:默认构造、拷贝构造、析构函数。如果自定义了有参构造,就不再提供无参构造,但会提供拷贝构造;如果自定义了拷贝构造,就不再提供其他构造函数。 jvzquC41dnuh0lxfp0tfv8|gkzooa=:3:8:378ftvkimg8igvcomu86626>86;5