引子:
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
提示:六大函数分别是默认构造函数,拷贝构造函数,移动构造函数,默认析构函数,赋值重载运算符,移动赋值运算符!
那本讲的重点是构造函数,析构函数,拷贝函数,以及栈的c++编写!
附录:关系图如下:
构造函数:
什么是构造函数?简单来说就是完成初始化的工作,你可以理解为类似init();
构造函数的水很深!
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。如下图:
从中我们可以看出我们没有进行初始化,但是,我们可以打印出对应的值,在这里就用到了默认函数“构造函数!”
注意:有时你看看到的是0,0,0!不要认为是初始化为0的,注意:这只是有些编译器,不是C++的标准,c++没有规定,而且我们也要从最坏情况考虑!
在这里我就不得不讲一下构造函数的基本特征了!
其特征如下:
1. 函数名与类名相同。(什么意思呢?就是我们手写初始函数时是类名)
2. 无返回值。(也就是说是void类型)
3. 对象实例化时编译器自动调用对应的构造函数。(默认构造函数)(重点!)
4. 构造函数可以重载。(可以重载,但是注意可能会有编译器的调用歧义!)
ok,铁汁们,我们来重点讲一下第三点自动调用对应的构造函数啊!
C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
那什么是默认的构造函数?
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(否则会有歧义!) 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。(注意:对于内置类型(基本类型)c++不做处理,初始化!而且编译器不报错误!)
其实也很好理解:
我们只要记住不用传参数就可以调用的函数就是默认构造函数!而且一般情况下我们都要写显示定义哦!,也就是要写初始化!(因为:这是一个嵌套原理,老铁,你可以想自定义类型最后还是内置类型)
注意默认构造函数只能有一个。(否则会有歧义!)的理解:
如图(正解):(我们有二种方式进行构造函数!如下);
注意:在c++中nullptr才是空指针!(小知识!)
C++11的补丁:
针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值!
析构函数:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
同构造函数一样:默认析构函数内置类型(基本类型)和自定义类型。
1,对于内置类型(基本类型)c++不做处理,而且编译器不报错误!)
2,对于自定义类型,去调用它的自定义析构函数!
注意:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
如图分析以下二张图就可以知道为什么栈的实例(c++的受益者):
对于析构函数的总结:
对于构造函数,析构函数的顺序,生命周期的理解:
注意:一个类只有一个构造函数与析构函数,任何说有二个构造函数或二个析构函数都是错的!
我总结以下几个原则!
1,构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构!
2,对象析构要在生存作用域结束的时候才进行析构!
3:类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象!
4,全局对象先于局部对象进行构造,局部对象按照出现的顺序进行构造,无论是否为static!
5,需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构!
拷贝函数:
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)(如比较大小时,我们不希望进行改变本身值的大小!所以用const!),在用已存 在的类类型对象创建新对象时由编译器自动调用。
其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。所以为
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
我们看以下二张图!
用引用!
没用引用:
这是为何?
其实是使用传值方式编译器直接报错, 因为会引发无穷递归调用了。
原理图:
拷贝也有二种形式!
以下是第二种:
3,若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。(此处要进行资源的考虑!若要进行资源管理则要自显示!)
对于第三点的原理图:
针对:构造函数,析构函数以及拷贝函数等等等的函数都要注意的一点就是:内存泄漏问题!
内存泄漏(Memory Leak)是指程序在分配内存后没有正确释放,导致这部分内存不再可用,并且无法通过常规的手段进行管理和重用!
在C++中,内存泄漏通常发生在以下几种情况:
-
使用new或malloc分配的堆内存没有使用delete或free进行释放。
-
指针在不再需要内存时没有被设置为nullptr,导致后续可能发生非法访问。
-
类的析构函数没有正确释放对象持有的资源。
-
长时间运行的应用中,内存泄漏累积到一定程度会导致内存耗尽。
解决方法:
-
使用智能指针(如std::unique_ptr, std::shared_ptr)管理资源,自动释放内存。
-
确保所有指针在不再使用后被正确地释放或置nullptr。
-
使用内存分析工具(如Valgrind, AddressSanitizer)检测内存泄漏。
-
定期审查和测试代码,确保所有内存分配有对应的释放操作。
-
在类的析构函数中正确释放所有需要释放的资源。
-
在开发周期中实施内存泄漏检测和防护策略。
栈的实例(c++的受益者)
代码:
typedef int STDataType;
class STack
{
public:
STack()
{
_a = nullptr;
_top = 0;
_capacity = 0;
cout << "调用了构造函数" << endl;
}
~STack()
{
cout << "调用了析构函数~Stack()" << endl;
if (_a)
{
free(_a);
_a= nullptr;
_capacity = 0;
_top = 0;
}
}
void StackPush(STDataType date)
{
//assert(_a);
if (_capacity == _top)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail!");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top] = date;
_top++;
}
bool Empty()
{
return _top == 0;
}
STDataType Top()
{
return _a[_top-1];
}
void print()
{
cout << _a << " " << _top << " " << _capacity <<" ";
}
void pop()
{
_top--;
}
private:
STDataType* _a=nullptr;
int _top=0; // 栈顶
int _capacity=0; // 容量
};
int main()
{
STack T1;
T1.print();
T1.StackPush(1);
T1.StackPush(3);
T1.StackPush(1);
T1.StackPush(8);
T1.StackPush(23);
T1.StackPush(63);
T1.StackPush(675);
while (!T1.Empty())
{
cout << T1.Top() << " ";
T1.pop();
}
cout << endl;
T1.print();
cout << endl;
return 0;
}
结果:
谢幕:
今天就到这啦,大家多多给我一点支持!
下次,我将分享运算符重载!