类和对象(中)
- 一、类的默认成员函数
- 二、构造函数
-
- [2.1 默认构造函数](#2.1 默认构造函数)
- [2.2 显式构造函数的实现](#2.2 显式构造函数的实现)
- 三、析构函数
- 四、拷贝构造函数
-
- [4.1 拷贝函数的简单演示](#4.1 拷贝函数的简单演示)
- [4.2 拷贝构造函数的意义](#4.2 拷贝构造函数的意义)
- [4.3 拷贝构造函数的注意事项](#4.3 拷贝构造函数的注意事项)
一、类的默认成员函数
类的默认成员函数是在我们不显示实现的前提下,编译器隐式实现的成员函数,总共有六个,示意图如下

常用的默认成员函数就是这六个
1、初始化、销毁:构造函数、析构函数
2、拷贝复制:拷贝构造、复制重载
3、取地址重载:编译器自带的默认成员函数基本是够用的,不细讲
二、构造函数
作用:构造函数并不是为成员变量开空间,而是在成员变量实例化的时候初始化成员变量
构造函数的三种形式1、默认构造函数:系统自己生成的构造函数
2、显式定义构造函数:我们写的无参的、全缺省的构造函数(注:这两个函数不可以同时存在)
3、拷贝构造函数:使用一个已有的类的实例来构造一个新的实例
2.1 默认构造函数
cpp
#pragma once
#include<iostream>
using namespace std;
typedef int DataType ;
class Stack
{
DataType* arr;
int _size;
int _capacity;
};
};
这是未显式实现构造函数的日期类,我们来看看这个类中的成员变量被初始化成了什么样子

这里的int类型成员变量是被初始化成了随机值(也就是垃圾值),而int*类型的成员变量则是初始化成了空指针,这显然不是我们想要的结果,这时候我们就应该想办法实现自己的构造函数了
2.2 显式构造函数的实现
1、构造函数的名字就是类的名字,返回值是void并且不显式写出
2、构造函数可以构成函数重载
3、构造函数可以缺省
我们来实现一下
cpp
Stack(int capacity = 4)
{
_capacity = capacity;
_stack = (int*)malloc(sizeof(DataType)* _capacity);
_size = 0;
}
我们来看看缺省调用的结果

可以看到:_capacity和_size都被初始化了,_stack也分配了空间,这是符合我们的预期的
注意
如果一个类中包含了其他的自定义类型,而这个自定义类型也正好有构造函数,这个类没有写构造函数,就会自动调取其他自定义类型的构造函数,如果没有,就会报错
三、析构函数
析构函树主要是用来释放申请的空间(即malloc、calloc的空间),其他的内置类型成员函数是不归他管的,这些在栈区的成员函数会在生命周期结束后 自动销毁所以像是Date类这样的类型实际上是不需要析构的
3.1析构函数的注意事项
- 析构函数的函数名是~+类名,返回值是void且不写、没有参数(和构造函数是挺像的)
- 析构函数在类的生命周期结束时自动调用
- 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
析构函数的实现
cpp
~Stack()
{
free(_stack);
}
3.2析构函数的调用顺序
在有多个类需要析构的时候,析构函数遵循先构造的先析构的原则
四、拷贝构造函数
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
4.1 拷贝函数的简单演示
这里我使用Date日期类来展现
cpp
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
我们在主函数中试一试效果
cpp
void test5()
{
Date d1(2026, 1, 30);
Date d2 = d1;
std::cout << d1 << std::endl<< d2 << std::endl;
//这里的输入流重载后面会讲到
}

4.2 拷贝构造函数的意义
系统的默认拷贝构造函数是在有限场景下适用的
在你没有自己写拷贝构造函数的时候,系统默认是将传入的类的数据逐个字节的拷贝(亦可称之为浅拷贝)
这种拷贝方式在诸如日期的情景下是完全够用的,但是在拷贝在堆区申请了内存的数据时就显得很鸡肋,这时候我们浅拷贝的数据实际上是一个地址。
这会使得两个类指向的空间是同一个,然后在程序的末尾,先后调用析构函数,同一块空间被释放两次,函数报错(就成功的寄掉了)
这时候我们就需要自己写一个拷贝函数,从而将数据完美无误的迁移过去
这里可以参考我之前讲的一道力扣题链表的终极考验:随机链表的复制辅助理解
4.3 拷贝构造函数的注意事项
1、函数第一个参数一定是类类型对象的引用
类类型的传值引用是要调用一次拷贝构造、创造一个临时变量
如果我们写的拷贝构造函数中第一个参数类型是类类型的值,在调用这个类类型的时候就会调用这个拷贝构造函数,这时候又会回到原来的困境,然后一直调用、一直没法彻底的结束这个进程,然后就死循环了。

2、传值返回
传值返回是创建了一个临时的变量,这时候会调用一次拷贝构造函数,但是要注意的是这个临时变量是不可修改的且生命周期是短暂的,的想要延长他的生命周期就需要使用const左值引用
cpp
lass MyClass {
public:
int value;
MyClass(int v) : value(v) {}
void modify() { value = 100; }
};
MyClass createObject() {
return MyClass(42); // 返回临时对象
}
int main() {
const MyClass& ref = createObject(); // 临时对象生命周期延长
// ref.value = 20; // 通过const引用也不能修改
3、传引用返回
传引用返回相当于是返回了原变量的地址,这种返回方式开销较小。
但是如果我们的变量在出了它的作用域之后就销毁了,这时候引用返回指向了一块被释放的空间,这是有问题的,这时候就出现了野引用(像是野指针)