欢迎大家来到小鸥的类和对象第二篇博客~
目录
类的默认成员函数
构造函数
构造函数的特点:
析构函数
析构函数的特点:
拷贝构造函数
拷贝构造的特点:
结语:
本篇会着重讲解类和对象中的难点:类的默认成员函数
由于篇幅原因,本篇只讲解构造,析构以及拷贝构造三个默认成员函数,运算符重载等内容将结合Date类在下一篇讲解
类的默认成员函数
默认成员函数就是指我们没有手动显示实现的函数,编译器会在实例化类的对象时自动生成这些默认成员函数。
一个类在我们不写的情况下,会自动生成6个默认成员函数(C++11以后还增加了两个默认成员函数:移动构造和移动赋值),默认成员函数重要且复杂,学习目标主要有两个:
- 我们不显示实现时,编译器默认生成的函数的行为是什么,确认是否满足需求;
- 当编译器生成的默认成员函数不满足需求时,学会显示实习这些函数。
构造函数
构造函数虽然叫构造,但它的作用并不是用来创建对象的,而当创建一个类类型的对象时,每个对象在创建完成后都要进行初始化,构造函数的作用就在于此,将创建好的实例化对象进行初始化。
构造函数的特点:
函数名和类名相同;
无返回值(区别于返回值为void的无返回值函数,构造函数连void都不用写出来,只有单独的函数名);
对象在实例化时会自动调用该类对应的构造函数;
构造函数可以重载;
若类中没有显示定义构造函数,那么C++编译器将会自动生成一个无参的构造函数,若显示定义构造函数则编译器不会生成;
无参构造函数、全缺省构造函数、编译器自动生成的构造函数,都叫做默认函数,而不是单指编译器默认生成的构造函数为默认构造函数。
三个默认 构造函数,在一个类类型中有且仅有一个,不能同时存在。且要注意,无参函数和全缺省函数调用时可能会产生歧义。
总结起来就是:不传实参就可以调用的构造函数为默认构造函数;
编译器生成的默认构造函数为无参函数,且对内置类型(说明: C++把类型分成内置类型(基本类型)和⾃定义类型。 内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等, ⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。)的成员变量没有硬性的初始化要求,即对内置类型是否初始化是不确定的,看编译器。而对于自定义类型的成员变量,会要求调用这个成员变量的默认构造函数来初始化。如果没有就会报错
#include <iostream>
using namespace std;
class hdmo
{
public:
//1.无参数构造函数
/*hdmo()
{
_year = 1;
_month = 1;
_day = 1;
}*/
//2.含参构造函数
/*hdmo(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//3.全缺省构造函数
hdmo(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()
{
hdmo h1;//不传递实参时不需要加()
//hdmo h1();//(错误的写法)函数声明还是定义对象?
h1.Print();
hdmo h2(2024, 3, 4);//当给构造函数传递实参时,直接在定义对象时后面加上(实参)
h2.Print();
return 0;
}
注意点:
- 只有在要给构造函数传递实参时才在定义对象的后面使用 (实参) 用于传递;
- 无参数传递的时候,也不能在定义对象处加(),因为这样会和函数声明分不开,产生歧义
由于编译器自动生成的构造函数对于内置类型是否初始化不确定的,所以大多数时候构造函数都是要自己实现的
析构函数
我们知道构造函数是对成员变量的初始化,那么马上来到的析构函数则是完成对象中资源的清理释放⼯作(不是销毁),在对象的生命周期结束前,会自动调用析构函数来释放需要释放的空间(比如动态申请的空间);当对象不存在需要释放的变量时,则可以不用写。
需要注意的是:像局部对象或者函数都是存在栈帧的,当栈帧销毁时,(如内置类型)就自动释放了空间了,此时是不需要用到析构函数的;需要用到的场景一般都为我们自己申请空间的情况,即需要用到free的情况。
析构函数的特点:
- 析构函数就是相对于构造函数在类名前面加上~。
- 没有参数和返回值(和构造函数一样,void也不需要加)。
- 每个类只有一个析构函数,若未显示定义,则系统自动生成默认析构函数。
- 对象的生命周期结束时,自动调用。
- 和构造函数相似,编译器自动生成的析构函数对内置类型不做处理,当存在自定义类型时,则会调用对应的析构函数。
- 不论是否显示定义析构函数,都会自动调用自定义类型的析构函数,没有则会报错;即便显示定义的析构函数中不包含调用自定义类型析构函数的语句,系统也会自动去调用自定义类型的析构函数防止内存泄漏,即自定义类型的析构函数,无论如何都会调用。
- 当类中没有申请空间时,析构函数就可以不写,直接使用编译器自动生成的;但当存在空间的申请时,则必须显示定义析构函数,否则将导致内存泄漏。
- 当一个局部域存在多个需要析构的对象时,C++规定后定义的先析构(和数据结构中栈的先进后出相似)。
//初始化栈
#include <iostream>
using namespace std;
class stack
{
public:
//析构函数(释放空间)
~stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//构造函数(变量初始化)
stack(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail!");
return;
}
_capacity = n;
_top = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
class MyQueue
{
public:
//自动生成构造函数,两个stack变量的初始化就会自动调用stack类中的默认构造函数,即stack(int n = 4);
//所以MyQueue类中的构造函数就不需要手动实现了
//自动生成的析构函数会自动调用stack类中的析构函数来释放两个stack类的变量
~MyQueue()
{
;//即便显式定义时不调用stack的析构函数也不会出错,因为编译器也会自动去调用。
}
private:
stack pushstack;
stack popstack;
};
拷贝构造函数
定义:如果一个构造函数的第一个参数是自身类类型的引用,且其他的所有参数都有默认值(缺省参数),则这个构造函数也叫做拷贝构造函数,即拷贝构造函数是一个特殊的构造函数。
拷贝构造的作用就是在初始化时,解决想要使用相同类型对象来初始化新对象的情况
拷贝构造的特点:
- 拷贝构造函数也是构造函数的一个重载函数;
- 拷贝构造函数的第一个参数必须为类类型对象的引用,如果使用传值调用编译器会直接报错,因为语法逻辑将导致引发无穷递归(C++中包含类对象的传值调用,拷贝数据时会先调用对象对应的拷贝构造函数)。
- C++规定自定义类型对象进行拷贝行为时必须调用拷贝构造函数,所以自定义类型传值传参和传值返回都会调用拷贝构造。
- 当没有显示定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝会对内置类型进行值拷贝(浅拷贝),自定义类型则会调用它的拷贝构造。
- 当一个类中只包含了内置类型,且没有指向资源时(动态内存申请),就可以不显示写出拷贝构造,编译器自动生成的拷贝构造就可以满足需求;但如果类中有自定义类型或者指向资源的内容,就需要我们显示的写出拷贝构造函数。若一个类中显示写出了析构函数并释放了资源,则它就需要显示的写出拷贝构造函数。
- 函数中的传值返回会产生一个临时对象调用拷贝构造,传引用返回返回的是返回对象的别名,不会产生拷贝。若返回的对象是一个当前函数局部域的一个局部对象,那么函数结束时该对象就销毁了,此时传引用返回就会出错,因为该引用所代表的对象已经销毁,此时就类似一个野指针的错误。虽然传引用返回可以减少拷贝所消耗的资源,但是前提要搞清楚返回对象的作用域。
#include <iostream>
using namespace std;
typedef int STDataType;
class stack
{
public:
//构造函数(初始化栈)
stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = n;
}
//析构函数(释放动态空间)
~stack()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
//拷贝构造
stack(const stack& st)
{
//创建一个大小相同的新栈,将原来栈的内容复制到新栈
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
//判断空间是否足够
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == nullptr)
{
perror("realloc fail!");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
private:
STDataType* _a;
int _top;
int _capacity;
};
//两个栈实现队列
class MyQueue
{
public:
//
private:
stack pushst;
stack popst;
};
int main()
{
stack st1;
st1.Push(1);
st1.Push(2);
//stack st2 = st1;
stack st2(st1);//调用拷贝构造
MyQueue mq1;
//MyQueue中自动生成的拷贝构造会自动调用stack的拷贝构造
MyQueue mq2 = mq1;
return 0;
}
结语:
本篇的讲解就到这里,有不足的地方请大家指正,互相成长,下一篇将结合Date类实现运算符重载内容,欢迎再次到来~
个人主页:海盗猫鸥-CSDN博客
本期专栏:C++_海盗猫鸥的博客-CSDN博客
感谢大家关注~