1,类的6个默认成员函数
如果一个类中什么成员都没有,简称空类。
默认成员函数:用户自己没有显示实现,编译器会自动生成的成员函数叫做默认成员函数。
重点是构造,析构,拷贝构造和赋值重载这四个函数。
2,构造函数
构造函数是特殊的成员函数,名字虽然叫做构造,但他不是用来开空间的,而是来完成初始化工作的。当实例化对象完成后,就会调用构造函数来对成员变量进行初始化。
构造函数的特点:
(1)函数名与类名相同。
(2)无返回值(返回值什么都不写,void也不写)
(3)对象实例化时系统会自动调用构造函数
(4)构造函数可以重载
class Date
{
public:
//构造函数可以重载
Date()//无参构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year,int month,int day)//带参构造函数,全缺省
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参的构造函数
d1.print();
Date d2(2024, 7, 26);//调用带参的构造函数
d2.print();
return 0;
}
(5)如果类中没有显示定义构造函数,那么c++编译器会自动生成一个无参的默认构造函数。
如果类中定义了,编译器将不会再生成。
class Date
{
public://调用编译器自动生成的构造函数
void print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参的构造函数
d1.print();
return 0;
}
(6)无参构造函数,全缺省构造函数,和我们不写时编译器自动生成构造函数,都叫做默认的构造函数。但这三个函数不能同时存在,不然调用的时候会存在歧义。
说明: c++中,把类型分为内置类型和自定义类型。内置类型就是语言提供的原生数据类型,如int,double等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
(7)我们不写时,编译器自动生成的构造函数,对内置类型成员变量没有要求,是否初始化取决于编译器。而对于自定义类型成员变量,要求调用这个成员变量的默认构造函数,若是没有默认构造函数,编译器就会报错。
3,析构函数
析构函数与构造函数的功能相反,它完成的不是对对象本身的销毁,不如局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们管。c++规定,在对象销毁时,会调用析构函数,完成对象中资源清理释放工作。
析构函数的特点:
(1)析构函数名是在类名前加上~
(2)无参数,无返回值
(3)一个类只能有一个析构函数,若是未显示定义,系统会自动生成默认的析构函数。
(4)对象生命周期结束时,系统会自动调用析构函数。
下面是栈的部分代码:
typedef int DataType;
class stack
{
public:
//构造函数
stack(size_t capacity=3)
{
//需要开空间
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("maloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
~stack()//析构函数
{
//释放资源
if (_array)
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
}
void push(DataType x)
{
_array[_size] = x;
_size++;
}
private:
DataType* _array;
int _size;
int _capacity;
};
int main()
{
stack st;//自动调用构造
st.push(1);
st.push(2);
return 0;//结束调用析构
}
(5)和构造函数类似,我们不写时,编译器自动生成的析构函数对内置类型不做处理,对自定义类型会调用它的默认析构函数。
(6)还有一点,我们显示写了析构函数,对于自定义类型,也会调用它本身的析构,也就是说无论写不写析构函数,自定义类型都会调用它本身的默认析构函数。
对于下面的代码,需要析构自定义类型。就要调用它自己的析构函数。
class Time
{
public:
~Time()
{
cout << "析构Time" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;//自定义类型
};
int main()
{
Date d1;
return 0;
}
注:先调用的后析构,比如上面的Date类,对象d1,d2,先析构d1,再析构d2.
4,拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,那么这个构造函数就叫做拷贝构造函数。也就是说,拷贝构造函数是特殊的构造函数。
拷贝构造函数的特点:
(1)拷贝构造函数是构造函数的重载,
(2)拷贝构造函数的第一个参数必须是类类型对象的引用,必须是引用的方式,传值的方式编译器会报错,因为语法逻辑上会引发无穷递归。(理由在后面)
class Date
{
public:
Date(int year=1,int month=1,int day=1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << "年" << _month << "月" <<_day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 15);//实例化
d1.print();
Date d2(d1);//用d1拷贝构造出d2,也可以写成 Date d2=d1
d2.print();
}
(3)若未生成拷贝构造函数,编译器会自动生成默认拷贝构造函数。自动生成的拷贝构造函数对内置类型变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型会调用它的默认拷贝构造函数。
(4)为什么传参的时候必须是引用传参,传值传参不行,这就要提一下,c++规定:所有传值传参需调用拷贝构造函数。
class Date
{
public:
Date(int year=1,int month=1,int day=1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造
{
cout << "拷贝构造" << endl;
}
void print()
{
cout << _year << "年" << _month << "月" <<_day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
void func(Date d)
{
}
int main()
{
Date d1;
func(d1);//传值传参
return 0;
}
这样就能看出,在传值传参时都会调用拷贝构造函数。
拷贝构造函数不能传值传参的原因:下图中,可以看出会无限递归下取。
(5)像Date这样的类,没有指向什么空间资源,编译器生成的拷贝构造就够用了,也就是浅拷贝。而对stack栈这样的类,涉及到开辟空间,就需要我们自己实现,也就是深拷贝。
栈的拷贝构造函数:
typedef int DataType;
class stack
{
public:
//构造函数
stack(size_t capacity=3)
{
//需要开空间
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("maloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
~stack()//析构函数
{
//释放资源
if (_array)
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
}
stack(const stack& st)//拷贝构造函数
{
//需要对_array所指向的资源,创建同样大的资源再拷贝
_array = (DataType*)malloc(sizeof(DataType) * _capacity);
if (_array == nullptr)
{
perror("maloc fail");
return;
}
memcpy(_array, st._array, sizeof(DataType) * _size);
_size = st._size;
_capacity = st._capacity;
}
void push(DataType x)
{
_array[_size] = x;
_size++;
}
private:
DataType* _array;
int _size;
int _capacity;
};
int main()
{
stack st1;//自动调用构造
stack st2(st1);
return 0;//结束调用析构
}
可以看出st1,st2拷贝成功, 并且是两个独立的空间。
若是使用编译器生成的默认拷贝构造函数,就是浅拷贝,就会使st1,st2指向同一块空间。
5,赋值运算符重载
5.1运算符重载
(1)当运算符被用于类类型对象时,c++允许我们通过运算符重载指定新的含义。c++规定,类类型对象使用运算符时,必须转化成调用对应的运算符重载,若没有对应的运算符重载,编译器就会报错。
(2)运算符重载是具有特殊名字的函数,它的名字有operator和后面要定义的运算符构成,例如operator+,operator++等。
(3)如果一个重载运算符函数是成员函数时,则它的第一个运算对象默认传给隐士的this指针。
因此运算符重载作为成员函数时,参数比运算对象少一个。
(4)5个不能重载的运算符:.* :: sizeof ?: .
class Date
{
public:
Date(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;
};
//放在全局
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{
Date d1(2024, 7, 15);
Date d2(2024, 7, 15);
d1 == d2;//调用运算符重载,也可以写operator==(d1,d2)
return 0;
}
可以看作就是一个普通的函数调用,但是这样写,有一个问题,我们的成员变量都必须写成共有的才行,不建议这样写,所以我们可以把他重载为成员函数, 代码如下:
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)//判断两个日期是否相等
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
void print()
{
cout << _year << "年" << _month << "月" <<_day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 15);
Date d2(2024, 7, 15);
d1 == d2;//调用运算符重载,也可以写operator==(d1,d2)或者d1 .operator==(d2);
return 0;
}
同时,这样写在传参的时候,会有一个隐士的this指针,可以少传一个参数。
注:如果全局和成员函数中有相同的运算符重载函数,优先调用成员函数中的。
5.2赋值运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d1=d2
Date& operator=(const Date& d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
return *this;
}
//d1==d2
bool operator==(const Date& d2)
{
return _year == d2._year && ._month == d2._month && _day == d2._day;
}
void print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 15);
Date d2(2024, 7, 15);
d1 = d2;//调用赋值重载拷贝
//Date d3=d2;//调用拷贝构造
//Date d3(d2);//调用拷贝构造
return 0;
}
和拷贝构造函数不同,赋值重载函数是对两个已经存在的对象的赋值。而拷贝构造是用一个对象去初始化另一个对象。
注:
1,参数类型:const T&,使用引用可以提高效率。
2,返回值类型T&,同样提高效率,有返回值目的是为了支持来连续赋值。
3,赋值运算符必须重载为成员函数,不能重载为全局函数。
4,用户没有实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,
注意,内置类型成员变量时直接赋值的,而自定义类型时需要调用自己的赋值运算符重载的。
6,取地址运算符重载
6.1const成员函数
(1)将const修饰的成员函数叫做const成员函数,const修饰成员函数,将const放在成员列表的后面。
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//这里d1是非const,也可以调用const成员函数,是一种权限的缩小
Date d1(2024, 7, 20);
d1.print();
const Date d2(2024, 7, 15);
d2.print();
return 0;
}
所以如果希望对象不被修改, 建议在函数后都加上const。
6.2取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
const Date* operator&()const//取地址运算符重载,const Date d2;&d2调用该重载
{
return this;
//return nullptr;//当不想被取到地址时,可以随意返回一个地址
}
Date* operator&()//取地址运算符重载
{
return this;
//return nullptr;//当不想被取到地址时,可以随意返回一个地址
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 20);
const Date d2(2024, 7, 15);
cout << &d1 << &d2<<endl;
return 0;
}