1. 类的默认成员函数
默认成员函数就是**⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数**
如图 有 构造函数 和 析构函数 ,拷贝复制函数 , 赋值重载函数,这四个最为重要。

c++98 下,对于一个类,我们不显示写的情况下编译器默认会生成6个默认成员函数,其中前4个为我们重点学习对象,后面两个 取地址重载不重要,我们稍微了解⼀下即可。c++11 以后还会增加两个默认成员函数,我们以后在继续学习。
我们需要明确默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯ 去学习:
• 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
• 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
下面对默认成员函数的讲解都是从这两个方面展开学习
1.1 构造函数
1.11 定义
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造 ,但是构造函数的主要任务 并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化 对象 。构造函数的本质 : 是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数**⾃动调⽤**的 特点就完美的替代的了Init。
1.12 构造函数的特点
先看前四点
-
函数名与类名相同。
-
⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3.对象实例化时系统会⾃动调⽤对应的构造函数。
- 构造函数可以重载
先来看第一种构造函数,无参构造函数,即没有形参,满足上述特点,函数名与类名相同,无返回值。调试发现,当对象实例化时会自动调用 该构造函数初始化。
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "/" << _month << "/" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
构造函数可以重载 ,如图,带参构造函数,实例化时后面要加括号和传实参,创建对象d2时,会自动调用带参的构造函数。
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2025,5,21);
return 0;
}
注意不能这样实例化,因为编译器分不清这是函数声明,还是实例化。记住就行
cpp
Date d3();
cpp
// 3.全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
全缺省的构造函数,显然比带参的构造函数更加方便,我们不传参时会以缺省参数为默认值,需要传参时也可以继续传。
那无参构造函数还需不需要呢,首先我们之前讲过,对于全缺省的构造函数和无参函数,再不给全缺省函数传实参时,会存在调用歧义,并且这里的全缺省的构造函数显然比无参构造函数更加实用,所以我们也不需要 无参构造函数了。
上面我们讲述了构造函数的前四个特点,接下来我们来进入第二关
-
如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
-
⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。(注意并不是只有编译器默认生成构造函数才叫默认构造函数)
但是这三个函数有且只有⼀个存在,不能同时存在 。(当我们不写时,编译器默认生成,但是当我们显示实现时,编译器就不会生成了,以及全缺省构造函数和无参函数也不能同时存在,所以这三个函数有且只有一个存在)⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。
要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造。
- 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求 ,也就是说是是否初始 化是不确定的,看编译器。( 大多数编译器对内置成员变量的初始化没有要求,为了方便,建议以后不管编译器对内置成员函数的初始化有没有要求,我们都自己实现**)对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始 化** 。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return ;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a;
int _capacity;
int _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue q;
return 0;
对于MyQueue这个类,我们没有写构造函数,编译器自动生成的构造函数,对于自定义类型 Stack
回去调用它的默认构造函数进行初始化。所以说编译器默认生成的构造函数并不是一点作用都没有

2. 析构函数
2.1 定义
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁 ,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对 象中资源的清理释放⼯作 。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date类中(没有对资源的申请)即也没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
2.2 特点
同样先看前四点
-
析构函数名是在类名前加上字符**~**。
-
⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
-
⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
-
对象⽣命周期结束时,系统会⾃动调⽤析构函数。
-
跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理 ,⾃定类型成员会 调⽤他的析构函数。
6. 还要注意的是,即使我们显示写了自定义类型的析构函数,它还是会去调用自定义成员变量的析构函数
7. 如果类中没有申请资源时,可以不需要自己写析构函数, 直接使⽤编译器⽣成的默认析构函数,如Date类,
如果默认生成的析构函数就可以使用,也不用自己写析构函数,如MyQueue,
如果涉及资源申请,一定要自己手动实现析构函数,否则会造成资源泄露。如stack
8.****⼀个局部域的多个对象,C++规定后定义的先析构,如stack的特点 后进先出,即后进来的先出去,也就是后定义的先释放先析构。
cpp
#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()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack s;
return 0;
}
当对象的生命周期结束时,会自动调用析构函数,即上面,当mian函数结束时,会自动调用析构函数。
cpp
#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()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
private:
Stack Pushst;
Stack Popst;
};
int main()
{
MyQueue mq;
return 0;
}
我们不写时,编译器对于内置类型不做处理,对自定义类型,**如上图的MyQueue,编译器默认生成的MyQueue的析构函数会去调用 Stack的析构函数,**和上面的构造函数一样。
6.还要注意的是,即使我们显示写了自定义类型的析构函数,它还是会去调用自定义成员变量的析构函数如图,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数

显示实现了,但是还是会去调用Stack的析构函数。先调用自己的,再调用成员变量的。
也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数


8.****⼀个局部域的多个对象,C++规定后定义的先析构,如stack的特点 后进先出,即后进来的先出去,也就是后定义的先释放先析构。

如图依次创建对象 s1,s2,s3,但是调用析构函数,是先s3,s2,再s1.
3. 对比c 和 c++ 实现的栈
对⽐⼀下⽤C++和C实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确 (不需要手动初始化栈以及每次返回前,都要Destory)实⽅便了很多,不会再忘记调⽤Init和Destory函数了,也⽅便了不少。
c版本
cpp
// ⽤之前C版本Stack实现
bool isValid(const char* s) {
ST st;
STInit(&st);
while (*s)
{
// 左括号⼊栈
if (*s == '(' || *s == '[' || *s == '{')
{
STPush(&st, *s);
}
else // 右括号取栈顶左括号尝试匹配
{
if (STEmpty(&st))
{
STDestroy(&st);
return false;
}
char top = STTop(&st);
STPop(&st);
// 不匹配
if ((top == '(' && *s != ')')
|| (top == '{' && *s != '}')
|| (top == '[' && *s != ']'))
{
STDestroy(&st);
return false;
}
}
++s;
}
// 栈不为空,说明左括号⽐右括号多,数量不匹配
bool ret = STEmpty(&st);
STDestroy(&st);
return ret;
}
c++版本
cpp
#include<iostream>
using namespace std;
// ⽤最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) {
Stack st;
while (*s)
{
if (*s == '[' || *s == '(' || *s == '{')
{
st.Push(*s);
}
else
{
// 右括号⽐左括号多,数量匹配问题
if (st.Empty())
{
return false;
}
// 栈⾥⾯取左括号
char top = st.Top();
st.Pop();
// 顺序不匹配
if ((*s == ']' && top != '[')
|| (*s == '}' && top != '{')
|| (*s == ')' && top != '('))
{
return false;
}
}
++s;
}
// 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题
return st.Empty();
}
由于析构函数会帮我们释放申请的资源,即替代了Destory,且会自动调用,相比c语言中,每次return前都要手动释放资源,方便很多,也不用担心会忘记释放资源,
c++中stack 对象创建时会自动调用构造函数初始化,也不需要我们手动初始化了。