1.类的默认成员函数
1.1六个默认成员函数
一、构造函数
1. 作用:用于创建对象时进行初始化操作。
2. 特点:与类同名,可以有多个重载形式。如果程序员没有显式定义构造函数,编译器会自动生成一个默认构造函数。
二、析构函数
1. 作用:在对象销毁时被自动调用,用于释放对象占用的资源。
2. 特点:与类名相同,前面加上波浪线(~)。同样,如果程序员没有定义析构函数,编译器会自动生成一个默认析构函数。
三、拷贝构造函数
1. 作用:用一个已有的对象来初始化另一个同类对象。
2. 特点:参数是本类对象的常量引用,形如 类名(const 类名&) 。如果没有自定义,编译器会生成默认拷贝构造函数,进行浅拷贝。
四、赋值运算符重载函数
1. 作用:将一个对象赋值给另一个同类对象。
2. 特点:形如 类名& operator=(const 类名&) 。如果没有自定义,编译器会生成默认的赋值运算符重载函数,同样可能进行浅拷贝。
五、取地址运算符重载函数
1. 作用:返回对象的地址。
2. 特点:有两个版本, 类名* operator&() 用于普通取地址, const 类名* operator&() const 用于常量对象取地址。编译器默认生成的版本简单地返回对象的地址。
六、移动构造函数和移动赋值运算符(C++11 引入)
1. 作用:用于高效地转移资源,避免不必要的拷贝。
2. 特点:移动构造函数的参数是一个右值引用,形如 类名(类名&&) 。移动赋值运算符的参数也是右值引用,形如 类名& operator=(类名&&) 。如果没有显式定义,编译器可能不会生成高效的移动版本。
1.2对象被销毁的几种情况
一、局部对象
1. 当定义局部对象的作用域结束时,该对象被销毁。
- 例如在函数内部定义的对象,当函数执行完毕返回时,函数内的局部对象会被销毁。
以下是一个示例:
void someFunction() {
MyClass obj;
// 其他代码
}
// 当 someFunction 执行完毕返回后,obj 对象被销毁。
二、动态分配的对象
1. 使用 delete 操作符释放动态分配的对象时,该对象被销毁。
例如:
MyClass* ptr = new MyClass();
// 其他代码
delete ptr; // 此时动态分配的对象被销毁。
三、全局对象和静态对象
1. 在程序结束时,全局对象和静态对象被销毁。
- 全局对象在整个程序的运行期间都存在,只有在程序结束时才会被销毁。
- 静态对象在其声明的作用域内保持存在,直到程序结束。
四、异常导致程序提前退出
1. 如果在程序执行过程中发生异常导致程序提前退出,那么在程序退出之前,已经创建的对象会按照创建的逆序被销毁。
总的来说,对象被销毁的时机取决于对象的生命周期和程序的执行流程。析构函数的自动调用确保了在对象被销毁时能够进行必要的资源清理和收尾工作。
2.构造函数
2.1构造函数的特点
#include <iostream>
using namespace std;
class Date
{
public:
//1.无参构造函数
//构造函数名与类名相同,无返回值
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//函数可以重载
//2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//3.全缺省构造函数
// 全缺省和无参一般不会同时写,同时写会导致重载调用不明确
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//对象实例化时系统会自动调用对应的构造函数
Date d1; //调⽤默认构造函数
d1.Print();//1/1/1
Date d2(2024, 6, 6); // 调⽤带参的构造函数
d2.Print(); //2024/6/6
//Date d3(2022);
//d3.Print();
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //不传实参能调用的构造,就叫做默认构造
d1.Print();
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public:
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //不传实参能调用的构造,就叫做默认构造
d1.Print();
return 0;
}
由于类中有一个带参构造函数,所以系统不会自动生成默认构造函数,并且带参构造函数不是默认构造函数,所以编译运行会报错:没有合适的默认构造参数可以用。
7. 我们如果不写构造函数,编译器默认生成的构造对内置类型成员变量的初始化没有要求,也就是说是否初始化是不确定的,看编译器。对于自定义类型成员变量,会调用这个成员变量的默认构造函数进行初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
MyQueue中没有写构造函数, 成员变量popst和pushst是自定义类型,所以编译器会生成默认构造函数对其进行初始化。
我们如果将构造函数Stack中的缺省值去掉,使得类中没有默认构造函数,这样就会报错
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
3.析构函数
在 C++等语言中,析构函数通常在对象生命周期结束时自动被调用,无论你是否显式地编写了析构函数。
如果没有显式地编写析构函数,编译器可能会自动生成一个默认的析构函数。这个默认析构函数通常会在对象超出作用域、被 delete 操作符删除或者在其他适当的时候被调用,以执行一些基本的清理操作,比如释放对象内部不涉及动态分配资源的成员变量所占的内存空间等。
但是,如果对象中包含需要特殊清理操作的资源,比如动态分配的内存、打开的文件、网络连接等,就需要我们显式地编写析构函数来确保这些资源被正确释放。
3.1析构函数的作用
1. 资源清理
- 在对象被销毁时,析构函数会自动被调用,用于释放对象在生存期间所占用的资源,比如释放动态分配的内存、关闭文件、释放网络连接等。
- 例如在 C++中,如果一个类的对象在运行过程中动态分配了内存,在析构函数中可以使用 delete 操作符释放这些内存,以防止内存泄漏。
2. 执行收尾工作
- 可以在析构函数中执行一些在对象生命周期结束时需要进行的收尾操作,如保存数据、输出日志等。
3.2析构函数的特点
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值。 (这⾥跟构造类似,也不需要加void)
3.⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4.当对象的生命周期结束时,无论是正常结束(如函数执行完毕、对象超出作用域等)还是异常结束(如抛出异常导致程序提前退出),系统会自动调用析构函数。
#include<iostream>
using namespace std;
class SimpleClass
{
public:
SimpleClass()
{
cout << "Constructor called." << endl;
}
~SimpleClass()
{
cout << "Destructor called." << endl;
}
};
int main()
{
SimpleClass obj;
return 0;
}
对象实例化是自动调用构造函数
生命周期结束时自动调用析造函数
5.跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
6.还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
//~MyQueue()
//{
//
//}
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
当程序结束也就是生命周期结束时,系统自动调用析构函数
3.3“析构”的含义
在“析构函数”中,“析构”的意思是分解、解构或销毁对象的结构并释放相关资源。
具体来说:
一、分解对象结构
当一个对象的生命周期结束时,析构函数会被自动调用以执行一些特定的操作。这就好像对一个已经完成使命的复杂结构体进行拆解。例如在 C++中,如果一个类包含多个成员变量,其中一些可能是指针指向动态分配的内存,析构函数可以负责释放这些内存资源,确保不会出现内存泄漏。这一过程类似于将一个精心构建起来的结构逐步拆解,把各个组成部分妥善处理。
二、释放相关资源
析构函数不仅可以释放内存资源,还可以处理其他类型的资源,如关闭文件、释放网络连接、释放数据库连接等。它的作用是在对象不再需要时,将对象在其生命周期内占用的各种资源归还给系统,使得这些资源可以被其他程序或对象再次使用。这就如同在一个工程结束后,对工程中使用的各种设备和材料进行回收和清理,以便为下一个项目做好准备。
总之,“析构”意味着在适当的时候对对象进行清理和资源回收,确保程序的资源使用更加高效和安全。