1. 默认成员函数概念
一个类在如果说里面为空,那么编译器会自动生成对应的一些成员函数来对类进行初始化,拷贝,析构等等工作。如果说一个类里面的成员是自定义类型的,那么编译器也会生成对应的函数,只不过可能函数里面是空的(因为编译器并不知道该怎么做)。
2. 构造函数
2.1 概念
cpp
class student
{
public:
void stu(string name,int age,string class)
{
_name=name;
_age=age;
_class=class
}
void getInformation()
{
cout<<_name<<endl;
cout<<_age<<endl;
cout<<_class<<endl;
}
private:
int _age;
string _name;
string _class;
};
int main()
{
student s1;
student s2;
s1.stu("小明",18,"一班");
s2.stu("小红",19,"二班");
s1.getInformation();
s2.getInformation();
return 0;
}
各位看,如果说在这个代码里面我们每次都要先初始化再通过stu函数对它进行赋值那是不是太麻烦了呢。所以我们这个时候就引入了一个叫做构造函数的东西。我们再看下面这个代码,通过这样的方式我们就可以方便的对函数进行初始化,减少了我们的代码量。
cpp
class student
{
public:
student()//无参数的构造函数
{}
student(string name,int age,string class)//有参数的构造函数
{
_name=name;
_age=age;
_class=class
}
void getInformation()
{
cout<<_name<<endl;
cout<<_age<<endl;
cout<<_class<<endl;
}
private:
int _age;
string _name;
string _class;
};
int main()
{
student s1;//调用无参数的构造函数
student s2("小红",19,"二班");//调用有参数的构造函数
s1.getInformation();
s2.getInformation();
return 0;
}
2.2 特性
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
构造函数如果说我们一旦自己写了,那么编译器就不会再自动生成了。我们来看下面这个代码,我们在生成s1的时候会报错,这是因为在上面student这个类里面并没有无参数的构造函数,编译器是可以自己生成的,但是由于我们已经写了一个构造函数使用编译器就不会自己生成了,于是就会报错。
cpp
class student
{
public:
//student()//无参数的构造函数
//{}
student(string name,int age,string class)//有参数的构造函数
{
_name=name;
_age=age;
_class=class
}
void getInformation()
{
cout<<_name<<endl;
cout<<_age<<endl;
cout<<_class<<endl;
}
private:
int _age;
string _name;
string _class;
};
int main()
{
student s1;//这一块代码会报错
student s2("小红",19,"二班");//调用有参数的构造函数
s1.getInformation();
s2.getInformation();
return 0;
}
我们这里要引入一个概念:内置类型与自定义类型。
内置类型很好理解就是一个参数的类型是int,double,string这种编译器里面自己本来就有的。
自定义类型就是比如说我们上面写的s1和s2这种。
如果说一个类里面的成员是自定义类型的,那么编译器也会生成对应的函数,只不过可能函数里面是空的(因为编译器并不知道该怎么做)。
那如果说我们想要实例化很多份,但是有不想自己一个个实例化那怎么办呢,我们可以写成下面这种方式。在下面这个代码里面s2我们在初始化的时候给了值,那么我们就不会调用构造函数里面的"小明",19,"一班"这些信息。而是会采用main函数里面的值,而s1这会调用这些信息。
那么在构造函数里面给值,我们一般叫这个值为缺省值。
cpp
class student
{
public:
student()//无参数的构造函数
{}
student(string name="小明",int age=19,string class="一班")//有参数和缺省值的构造函数
{
_name=name;
_age=age;
_class=class
}
void getInformation()
{
cout<<_name<<endl;
cout<<_age<<endl;
cout<<_class<<endl;
}
private:
int _age;
string _name;
string _class;
};
int main()
{
student s1;//调用有参数和缺省值的构造函数
student s2("小红",19,"二班");//调用有参数和缺省值的构造函数
s1.getInformation();
s2.getInformation();
return 0;
}
3. 析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值类型。
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载,即不可以像构造函数一样写很多个。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
我们来看下面这个代码,我们是需要手动释放资源的,因为我们在构造函数里面malloc了一个_a,
如果我们不对_a进行free的话,会导致内存泄漏问题。同时我们还要把_a指向空。
cpp
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
4. 拷贝构造
4.1 概念
通过编译器自动调用拷贝构造函数以深浅拷贝的方式把一个变量赋值给另一个变量。
4.2 特性
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用**传值方式编译器直接报错,**因为会引发无穷递归调用。
PS:自己写的拷贝构造我个人认为很大程度上就是为了防止深浅拷贝误用。
浅拷贝的本质就是在内存上使两个变量指向同一个地址。这在某一些场景上是可以使用的,因为可以提高代码的运行效率。
深拷贝的本质就是在一个变量把值赋给另一个变量的时候,并不直接赋值,而是通过临时变量,即A先产生一个临时变量,然后这个临时变量把值给B。最后这个临时变量销毁。通过这样的方式来实现赋值。所以在效率上会有所降低。同时我们要注意,临时变量是具有常性的(即const),所以我们的拷贝构造要加const。
那么两者误用会发生什么问题呢?
我们也知道浅拷贝的本质就是在内存上使两个变量指向同一个地址,但是如果说这两个变量在程序结束的时候是要触发析构函数的话,那么就会使同一块地方被析构两次,这就会出现问题,所以我们这个时候就要使用深拷贝。
我们来看下面这个代码,它的实现原理就是通过深拷贝来实现的。
cpp
class Stack
{
public:
//Stack(int capacity = 4)
//{
// cout << "stack()" << endl;
// _a = (int*)malloc(sizeof(int) * 4);
// if (_a == NULL)
// {
// perror("malloc fail");
// return;
// }
// _capacity = 4;
// _top = 0;
//}
Stack(const Stack& st)
{
cout << "" << endl;
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
//~Stack()
//{
// cout << "~Stack()" << endl;
// free(_a);
// _a = nullptr;
// _top = 0;
// _capacity = 0;
//}
private:
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
对了,如果说我们没有写然后编译器自动生成的那个拷贝构造是浅拷贝。
5. 赋值运算符重载
5.1 概念
- 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回***this**:要复合连续赋值
其实说白了它的本质就是重新定义原有的一些符号,使它可以在这个类里面使用。
5.2 特点
注意:我们只能重载成类的成员函数不能重载成全局函数。
我们来看下面两个代码,一般的大于号和小于号是无法识别日期类大小的判断,所以我们通过operator的方式来使大于号和小于号可以识别日期类大小的判断。
各位可以理解为函数,只不过长的特殊一点。
cpp
bool operator>(const Data& x1, const Data& x2)
{
if (x1._year < x2._year)
return true;
else if (x1._year == x2._year && x1._month < x2._month)
return true;
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
return true;
return false;
}
bool operator<(const Data& x1, const Data& x2)
{
if (x1._year > x2._year)
return true;
else if (x1._year == x2._year && x1._month > x2._month)
return true;
else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
return true;
return false;
}
PS:我们要注意在 运算符重载里面的参数要加const。
当我们没有实现运算符重载时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
我们来看下面这个代码,如果说我们没有自己实现operator=的话,那么下面这个代码就是浅拷贝了,会对同一个地址析构两次。同时会导致原本的那个s1并没有被析构掉,这就会导致内存泄漏问题。
cpp
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
int main()
{
Stack s1(10);
Stack s2(20);
s1=s2;
return 0;
}