c++—类和对象(下)-八大成员函数
2026/6/16 22:00:21 网站建设 项目流程

构造函数

概念

构造函数是一个特殊的成员函数,定义类对象时自动调用,用于对对象进行初始化

特性

  1. 函数名与类名相同
  2. 该函数没有返回值,参数可以自己设置
  3. 构造函数可以重载,也就是说可以有多个参数列表不同的构造函数
  4. 如果自己没有显示定义任意构造函数(包括拷贝构造,移动构造等),编译器会为我们生成无参默认构造函数

构造函数的调用

class A { public: A(){} A(int x, int y) {} }; int main() { A a;//调用无参构造 A b(1,2);//调用有参构造 }

默认构造函数的缺陷

编译器自动生成的默认构造函数有如下特性:

对内置类型数据:没有定义要不要去初始化,有可能处理,也有可能不处理。

对自定义类型数据:会调用这个自定义类型里面无参构造函数(不论是默认构造函数还是自定义的无参构造函数)去初始化这个自定义类型数据。这里又分成三种情况:

  1. 一种是这个自定义类型里没有无参构造函数(比如自定义了有参构造,编译器不会生成默认构造函数,而我们也没有自定义无参构造),找不到默认构造函数就会报错。
  2. 第二种是这个自定义类型里有默认构造函数(这时编译器自动生成),调用了默认构造函数之后,对这个自定义类型里的内置类型不处理,遇到自定义类型重复以上步骤。
  3. 第三种是这个自定义类型里有自定义的无参构造函数,他一般能正确初始化这个自定义类型,因为这是由我们控制的嘛。

最终对于内置类型有可能处理,有可能不处理。所以我们一般要自己显示写构造函数。当然,在某些情况下,也可能不需要我们写构造函数,比如下面的Queue:

c++11为这个缺陷打上了补丁:

在类中定义成员变量的时候就可以赋予成员变量初始值,之后无论哪种构造函数(编译器生成的,自己定义的)都会使用这个初始值初始化该成员变量,我们把被赋予的值叫做缺省值。

注意:如果构造函数内部也对内置类型做了相关初始化,就以构造函数内部的初始化为准,这也是为什么叫缺省值的原因,可用可不用嘛。


析构函数

概念

析构函数在对象生命周期结束后自动调用,其目的是销毁对象内部的动态资源。

特性

  1. 析构函数名是~ + 类名
  2. 析构函数无参数无返回值
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认析构函数。注意:析构函数不能重载(因为它没有参数,无法区分)
  4. 对象生命周期结束时,自动调用析构函数
  5. 在同一生命周期内,按属性定义的顺序对对象进行构造,而析构的顺序和对象定义的顺序相反

默认析构函数的缺陷

class Example { std::string str; // 成员对象 int* ptr; // 原始指针 public: // 编译器生成的默认析构函数等价于: ~Example() noexcept { // 1. 调用成员对象的析构函数(str.~string()) // 2. 忽略原始指针(ptr不会自动被delete) // 3. 隐式标记为 noexcept,表示不会抛异常 } };

可以看出,如果有动态资源最好还是自己写析构函数


拷贝构造

概念

拷贝构造是指在创建对象的时候将已有对象的内容拷贝到该对象中。实际上拷贝构造就是构造函数的一个特殊分支,本质上还是初始化。

特性

  • 拷贝构造是构造函数的一种重载形式。所以除了参数,拷贝构造和构造函数的其他部分没什么不同。
  • 拷贝构造的参数只有一个,且必须是类的类型的引用(用指针也可以,只是相对引用来说比较麻烦),如果直接传值的话会报错因为会引发无穷递归:想要把d1拷贝给d2,先得把d1拷贝给形参date,然后在用date拷贝d2。而想把d1拷贝给date就得先把d1拷贝给新的形参date,这样一直循环发生无穷递归。
  • 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数按字节拷贝对象,类似于memcpy,这种拷贝叫做浅拷贝,或者值拷贝

如果对象中有指向动态资源的指针的时候还使用浅拷贝,那这两个指针就会指向同一块空间,这样两个对象就不是独立的了,这不是我们想要的。这时候就要深拷贝,需要我们自己定义一个拷贝构造函数,在里面开创新的空间,然后把里面内容改成和要拷贝的对象里指针所指向空间一样的。

使用

拷贝构造的使用方法有两种:A st2(st1)或者A st2 = st1。举个例子:

class A { }; int main() { A x; A y(x);//调用拷贝构造 A z = x;//调用拷贝构造 }

赋值运算符重载

赋值运算符重载也是运算符重载,但它有以下注意点:

  1. 和其他运算符重载不同,赋值重载必须是成员函数,不能定义在全局
  2. 同构造,析构,拷贝函数一样,如果没有在类里实现赋值重载,类中会自动生成默认赋值运算符重载(该默认成员函数的行为与拷贝构造类似),所以定义了全局的赋值运算符重载也没用,因为类中的默认赋值运算符重载会先匹配到。
  3. 如果自己没定义赋值运算符重载,编译器会生成默认的赋值运算符重载,它按字节拷贝赋值。
  4. 赋值重载和拷贝构造区分:“Date st1 = st2”是拷贝构造,“st1= st2”是赋值重载,简单来说,拷贝构造针对的是刚要创建的对象,赋值重载针对的是两个已存在的对象。
  5. 赋值重载必须有返回值,因为有连续赋值的情况。有两种返回方式,一种是返回引用。一种是返回拷贝。总的来说返回引用效率高一点。赋值运算符重载调用过后,如果返回对象的生命周期在对象返回后没有结束,并且没有析构,那就返回引用,否则返回拷贝(使用已经销毁对象的引用会导致不可预知的错误)。

取地址运算符重载和const取地址运算符重载

取地址运算符重载和const取地址运算符重载也是运算符重载,但它们有以下注意点:

  1. 和其他运算符重载不同,取地址运算符重载必须是成员函数,不能定义在全局
  2. 同构造,析构,拷贝函数一样,如果没有在类里实现取地址运算符重载,类中会自动生成默认取地址运算符重载,所以定义了全局的取地址运算符重载也没用,因为类中的默认取地址运算符重载会先匹配到。
  3. 这一部分不需要自己编写,一般用编译器默认生成的就好。就算自己实现也比较简单,就是重载两份&运算符而已:
class MyClass { public: MyClass* operator&() { // 自定义取地址逻辑 return this; // 通常返回this指针 } }; class MyClass { public: const MyClass* operator&() const { // 自定义const对象的取地址逻辑 return this; // 通常返回this指针 } };

注意的是:在成员函数中有const和不加const可以构成函数重载。


移动构造和移动赋值重载(c++11)

移动构造和移动赋值重载分别是构造和赋值重载的两种形式,他们以右值引用为参数,在内部可以不进行拷贝而是直接交换右值的资源,下面是这两种成员函数的样子:

class A { public: A(A &&x) //移动构造 { //.... } A& operator=(A &&x) //移动赋值重载 { //.... } };

关于移动构造和移动赋值重载在引用一节有讲,这里只说说编写他们要注意的地方:

  1. 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值重载跟上面移动构造完全类似)
  3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

运算符重载

运算符重载是 C++ 中允许程序员为自定义类型重新定义运算符行为的特性,使得类对象可以像内置类型一样使用运算符。运算符重载既可以是全局函数,也可以是成员函数。

两个类如何比较?

定义一个比较函数?如下:

class A { public: A(int x) { a = x;} int a; }; class B { public: B(int y) { b = y; } int b; }; bool isequal(A x, B y) { return x.a == y.b; } int main() { A x(1); B y(2); isequal(x, y); }

这样的代码可读性不高,也比较麻烦,所以有了运算符重载

使用

  1. 函数名为关键字operator加上某个运算符,如:operator<,operator=,operator==。
  2. 函数参数个数与操作符的操作数个数保持一致(别忘了非静态类成员函数都有一个隐含的this参数代表调用者,因此运算符重载作为非静态成员函数时并且运算符操作数为2时,我们应该设置的参数个数为1)。
  3. 函数参数中必须有至少一个自定义类型的参数(非静态成员函数自动就有自定义类型的参数:this指针),都是内置类型并没有意义
  4. 函数的返回值由运算符决定,比如operator==的返回值是bool,operator+的返回值为int,double等。
  5. 不能用其他符号链接operator构成函数名,只能用内置运算符。而且,在内置运算符中,.*,::,?:,. ,sizeof也不能用来运算符重载。
  6. 运算符重载函数收到参数的顺序和操作数的顺序一致
  7. 编译器的规则是当遇到一个类使用运算符时自动调用运算符重载(先在类里面找,再到全局中找,找不到报错。)

因此可以得出两种比较两个类的方法:

class A { public: A(int x) { a = x;} bool operator==(const B& x) { return a == x.b; } int a; }; class B { public: B(int y) { b = y; } int b; }; int main() { A x(1); B y(2); cout<<(x == y);//实际上x==y 最终转换成x.operator==(y)!!! }
class B { public: B(int y) { b = y; } int b; }; class A { public: A(int x) { a = x;} int a; }; bool operator==(const A& y,const B& x) { return y.a == x.b; } int a; int main() { A x(1); B y(2); cout<<(x == y);//实际上x==y 最终转换成operator==(x,y)!!! }

特殊的运算符重载++

前置++和后置++的函数名相同,但是前置++应该返回的是++后的值,而后置++返回++之前的值,他们的实现不同,函数名相同,这就需要函数重载。然而,之前规定了运算符的参数个数与操作数个数一致,所以前置和后置++函数的参数都只有this指针。迫不得已,又规定给后置++的参数中多加一个int来区分这两个函数。

这个int参数不需要使用,仅仅为了区分前置和后置++:


成员函数的删除

class A { A() = delete;//删除默认构造函数 A& operator=(const A& x) = delete;//删除赋值重载函数 void func() = delete;//删除普通函数 };

成员函数删除规则

  1. 删除拷贝构造/移动构造会连带删除默认构造:如果类需要默认构造,需显式定义或使用= default

  2. 删除析构函数会导致所有默认构造和拷贝控制被删除:此类对象无法被构造、拷贝或销毁,通常仅用于特殊场景(如单例模式禁止析构)。

  3. 继承链中父类的删除操作会影响子类:子类无法绕过父类已删除的函数(如父类禁止拷贝,子类也无法拷贝)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询