目录
概念引入:
一、构造函数
问题引入:
1)构造函数的概念
2)构造函数的特性
二、析构函数
1)析构函数概念
2)析构函数特性
三、拷贝构造函数
1)拷贝构造函数概念
示例代码:
2)深拷贝
3)拷贝构造函数特性
四、赋值运算符重载
运算符重载
赋值运算符重载
概念引入:
C++中的默认成员函数是系统自动生成的,如果没有手动编写该类的成员函数,编译器就会自动为该类生成默认成员函数。默认成员函数包括默认构造函数、默认析构函数和默认拷贝构造函数等。
- 默认构造函数:当创建对象时,如果没有显式地调用构造函数,系统会自动调用默认构造函数来初始化对象。默认构造函数不接受任何参数,也不返回任何值。
- 默认析构函数:当对象被销毁时,系统会自动调用析构函数来清理对象。默认析构函数不接受任何参数,也不返回任何值。
- 默认拷贝构造函数:当将一个对象赋值给另一个对象时,系统会自动调用拷贝构造函数来完成对象的复制。默认拷贝构造函数会将原对象的所有成员变量逐个复制给新对象。
除了以上三种默认成员函数外,还有默认赋值运算符、取地址运算符等。这些默认成员函数可以让我们更方便、更高效地使用C++语言进行面向对象编程。
以上有六个默认成员函数,但是我们今天只探讨前4个,后面两个实际操作中我们很少直接编写,一般都是使用默认生成式的。
一、构造函数
问题引入:
在C++编程语言中,构造函数和析构函数的出现与对象及对象的生命周期管理密切相关。在现实世界中,每个事物都有其生命周期,会在某个时候出现也会在另外一个时候消亡。类似地,程序是对现实世界的反映,其中的对象就代表了现实世界的各种事物,自然也就具有生命周期,也会被创建和销毁。
因此,为了恰当地管理对象的生命周期,特别是对象的初始化和清理工作,C++引入了构造函数和析构函数这两个特殊的成员函数。每一个类都有一个默认的构造函数和析构函数;构造函数在类定义时由系统自动调用,析构函数在类被销毁时由系统自动调用。
具体来说,构造函数主要用于完成对象的初始化工作,它的名字和类名相同,一个类可以有多个构造函数。如果程序员没有手动编写构造函数,编译器会默认生成一个构造函数。另一方面,析构函数则用于完成对象的清理工作,它的名字是类名前面加一个~符号。当对象的生命期结束时,会自动执行析构函数。
总的来说,构造函数和析构函数的出现,让程序员可以更加方便、准确地管理对象的生命周期,这是C++面向对象编程特性的一个重要体现。
1)构造函数的概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次(用一个已经存在的对象去初识另一个和对象)
2)构造函数的特性
不允许出现这正形式的调用:!!!
Date d1();
这种类型的调用,因为它可以被认为成函数的声明,就好比:
Date fun1();
所以,如果想定义一个日期类,则不能加括号,不然括号内需要加参数,
//1普通版
Date()
{m_year = 2023;m_month = 10;m_day = 17;
}
void Date::Init(int year,int month,int day)
{m_year = year;m_month = month;m_day = day;
}
//2融合版
Date(int year = 2023,int month = 10,int day = 17)
{m_year = year;m_month = month;m_day = day;
}
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 关于编译器生成的默认成员函数,很多同学会有疑惑:不手动实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来它生成的默认构造函数又没什么用?我们在定义对象时调用了编译器生成的默认构造函数,但是对象的成员变量_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数没有实质性作用吗?
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数,而内置类型则不会,这就是为什么全是int类型的成员变量的日期类我们观察不到其初始化的原因。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值,自动生成的构造函数会根据此初始化。
class Date
{
private:int _year = 2000;//在此处的默认值可以用作构造函数初始化的参考int _month = 1;int _day = 1;
}
如果我们用两个栈实现一个队列类就可以体会到默认构造的作用了:
class MyQueue
{
private:Stack _pushst;Stack _popst;
};
二、析构函数
1)析构函数概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么清除的呢?析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2)析构函数特性
- 析构函数名是在类名前加上字符 “~”。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
- 默认生成的析构函数不会对内置类型进行操作,不然释放掉不该释放的内容将会出问题。
析构函数是类的一种特殊的成员函数,其名称与类名相同但增加一个波浪线符号(~)。当对象超出范围或通过调用delete显式销毁对象时,析构函数会自动被调用。析构函数往往用来做“清理善后”的工作,例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存。此外,析构函数也可以有多个,如果没有手动写析构函数,编译器会生成一个默认的析构函数并自动调用。
析构函数的调用时机主要有以下几种:
- 对象生命周期结束:当对象超出作用域或被显式销毁时,析构函数会自动被调用,用来释放对象占用的内存空间。
- delete操作符:当使用delete删除指针类对象时,会直接调用析构函数来清理内存。
- 包含关系:如果对象Dog是对象Person的成员,那么在Person的析构函数被调用时,Dog对象的析构函数也会被自动调用。
三、拷贝构造函数
1)拷贝构造函数概念
拷贝构造函数,也称为复制构造函数,是一种特殊的构造函数。它是当创建新对象时,使用同一类中之前已创建的对象来初始化新创建的对象。这种构造函数由编译器自动调用,用于一些基于同一类的其他对象的构建及初始化。
拷贝构造函数的形式参数必须是引用,通常为const引用,以便能够处理常量对象和非常量对象的复制。这样,它既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数。
在编程实践中,拷贝构造函数常常用于以下情况:通过使用另一个同类型的对象来初始化新创建的对象;复制对象把它作为参数传递给函数;以及从函数返回一个对象时复制该对象。
值得注意的是,如果程序员没有显式定义拷贝构造函数,那么编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会将原对象的成员变量值赋值给新对象的相应成员变量。
拷贝构造函数的使用加场景类似于:
//使用a创建b的过程
int a = 1;
int b = a;
- 直接传值传参(浅拷贝):由于C++的特性会出现析构两次的问题
- 引用拷贝:但是对于复制对象的操作会影响到原对象(因为引用底层是指针,所以引用拷贝使用时没有问题,但是实际会影响原来的对象)
- C++规定,自定义类型的传参就需要调用拷贝构造函数。
示例代码:
#include <iostream>
#include <cstring> class MyClass {
public: char* data; // 构造函数 MyClass(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } // 拷贝构造函数 MyClass(const MyClass& other) { data = new char[strlen(other.data) + 1]; // 重新分配内存 strcpy(data, other.data); // 深拷贝 } // 析构函数 ~MyClass() { delete[] data; } // 用于展示数据 void show() const { std::cout << "Data: " << data << std::endl; }
}; // 按值传递
void byValue(MyClass obj) { obj.show();
} // 按引用传递
void byReference(MyClass& obj) { obj.show();
} int main() { MyClass original("Hello, World!"); // 按值传递 std::cout << "Calling byValue:" << std::endl; byValue(original); std::cout << "After byValue call, original:" << std::endl; original.show(); // original 的数据没有改变 // 按引用传递 std::cout << "Calling byReference:" << std::endl; byReference(original); std::cout << "After byReference call, original:" << std::endl; original.show(); // original 的数据没有改变 return 0;
}
按值传递可能出现的问题
在
byValue
函数中,传递对象会触发拷贝构造函数。若拷贝构造函数不正确(如仅进行浅拷贝),则会导致多个对象指向同一内存。在一个对象被销毁时,另一个对象访问已被释放的内存,进而引发未定义行为。按引用传递的安全性
在
byReference
函数中,使用引用传递,因此不会发生对象的拷贝。原始对象的状态保持不变,不会出现内存访问的问题。
2)深拷贝
深拷贝,是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。深拷贝会复制所有字段,并复制字段所指向的动态分配内存。深拷贝发生在对象及其引用的对象被复制时。对于基本数据类型,如预定义类型Int32,Double等,深拷贝复制所有基本数据类型的成员变量的值。对于引用数据类型的成员变量,深拷贝申请新的存储空间,并复制该引用对象所引用的对象。
深拷贝是一种特殊的拷贝方式,它不仅复制了对象的基本数据类型成员变量的值,还为引用类型的成员变量申请了新的存储空间,并递归复制了这些引用对象所引用的其他对象。这样,源对象与拷贝对象就完全独立,任一对象的修改都不会影响到另一个对象。
需要注意的是,在C++中,对于基本类型的数据以及简单的对象,它们之间的拷贝非常简单,通常是按位复制内存。但对于复杂对象和包含指针或动态内存分配的对象来说,需要进行深拷贝来确保两个对象不会相互影响。 默认情况下,基本数据类型(number,string,null,undefined,boolean)的操作都是深拷贝。
3)拷贝构造函数特性
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
- 拷贝构造函数典型调用场景:使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象
四、赋值运算符重载
运算符重载
- C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- 当然有特殊情况,以下五个运算符不可以重载!!!!
* :: sizeof ? :
赋值运算符重载
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
--《C++ Prime》
- 赋值运算符重载格式参数类型:const T&,传递引用可以提高传参效率返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this :要复合连续赋值的含义
- 赋值运算符只能重载成类的成员函数不能重载成全局函数//:原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
在实际编码中,建议在需要动态资源管理的类中实现拷贝构造函数、赋值操作符、析构函数,以遵循“遵循Rule of Three”的原则,确保资源管理正确无误。
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
且不能改变操作符的操作数。