一、 类的6个默认成员函数
1.构造函数
一种特殊的成员函数,特点:①函数名和类名相同
②函数无返回值
③类实例化时编译器自动调用
④可以重载(一个类可以有多个构造函数)
功能:初始化类对象。
分类:普通构造函数;默认构造函数。
默认构造函数有三种:无参构造函数;全缺省构造函数;编译器自动生成并调用的构造函数。
(没有自己定义构造函数时,编译器会自动生成合适的默认构造函数(参数列表和函数体均为空,不对内置类型成员变量做处理,但是会自动调用自定义类型成员变量的构造函数对其进行初始化);一旦定义了构造函数,编译器不会再自动生成默认构造函数,如果定义的构造函数中没有合适的构造函数,编译器会报错)。
因为编译器调用的默认构造函数不对内置类型(int、char等类型)成员变量做处理,因此,成员变量有内置类型变量时最好自己写构造函数;也可以不写,但是要提前在声明时给内置类型的成员变量赋缺省值(类内初始化)。
注意:默认构造函数只能存在一个(因为参数为空的构造函数和全缺省构造函数同时存在的话,在创建类对象但不传参时会有歧义,编译器会报错)。
以日期类为例:
class Date
{
public:
//无参构造函数
//1号
Date()
{}
//一般构造函数
//2号
Date(int year,int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//部分缺省构造函数
//3号
Date(int year, int month)
{
_year = year;
_month = month;
_day = 3;
}
//4号
Date(int year)
{
_year = year;
_month = 3;
_day = 3;
}
//全缺省构造函数
//5号
Date()
{
_year = 2023;
_month = 4;
_day = 4;
}
//6号
Date(int year = 2023, int month = 4, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
//打印日期类对象的成员变量的值
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用1号、5号或6号构造函数都能完成对象的创建(其中1号和5号不构成重载),故有
//歧义
Date d2(2022);//调用4号构造函数完成对象的创建
Date d3(2022, 1);//调用3号构造函数完成对象的创建
Date d4(2022, 2, 2);//2号和6号构造函数不构成重载,故有歧义
d1.Print();
d2.Print();
d3.Print();
d4.Print();
return 0;
}
运行以上代码,可以得到以下结论:
1.同一个类的构造函数不可以出现两种默认构造函数(无参构造和全缺省构造函数),在定义类对 象时可能会产生歧义导致程序的崩溃。
为什么:定义类对象时若没有参数的传递,则可以调用无参构造函数,也可以调用全缺省构造函 数。
2.注意函数重载的定义:函数的函数名相同,返回类型不同或者参数的类型不同或者参数的个数不 同都可以构成函数重载。
部分缺省函数定义时需要注意。
3.形参和实参的数目是相同的,无参构造函数和全缺省构造函数是两个特例。
2.析构函数
一种特殊的成员函数,特点:①函数名为~类名
②函数无返回值
③类对象生命周期结束时编译器自动调用
④不可重载(一个类只能有一个构造函数)
功能:释放类对象中的资源(堆区等栈帧销毁后仍未释放的空间)
自己没定义析构函数,类对象生命周期结束时编译器自动调用默认析构函数,对内置成员不做处理(一般不需要处理,在类对象生命周期结束时就已经释放了),当内置成员出现指针类型变量时,需要注意是否将其指向动态开辟的空间,如果有,则需自己定义析构函数,防止内存泄漏的发生;对自定义类型成员会调用其自己的析构函数。
所以,当类的成员变量只有内置类型变量时(有指针类型变量则需自行判断),可以不用自己写析构函数,例如:日期类。
3.拷贝构造函数(复制构造函数)
一种特殊的成员函数,是构造函数的一个重载函数;拷贝构造函数的参数只有一个,且必须是类的引用,否则会无穷递归。
为什么?
类对象进行传值传参时会调用拷贝构造函数,若拷贝构造函数的形参是类类型,传值传参时会自调,导致无穷递归的结果。(内置类型编译器可以直接拷贝(浅拷贝/值拷贝);自定义类型不可以直接拷贝,必须通过拷贝构造函数来实现)
注意:为了避免定义的类对象为常对象或者出现将赋值等式的左值和右值写反的情况造成的错误,一般可以将拷贝构造函数的形参定义为常引用类型。
例如:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1;
Date d2(d1); //将d1拷贝给d2
Date d3 = d1; //将d1拷贝给d3
return 0;
}
不写拷贝构造函数,编译器会自动生成默认拷贝构造函数。其会对内置类型的成员变量进行拷贝(浅拷贝),因为自定义类型数据不能进行浅拷贝,容易出现问题,所以,在需要深拷贝时,需要自己定义拷贝构造函数。
浅拷贝 vs 深拷贝:有无各自独立的空间<地址>
浅拷贝又名值拷贝,即将一个变量的值拷贝给另一个变量,如果成员变量中有指针类型的变量,则浅拷贝拷贝的时指针类型的数值,拷贝后两个指针变量都指向同一块地址空间。而深拷贝则可以做到拷贝后,两指针指向的地址空间不同(具体的实现方法和规则之后再进行介绍)。
拷贝构造函数典型调用场景: ①使用已存在对象创建新对象
②函数参数类型为类类型对象
③函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
4.赋值重载函数
1.运算符重载
功能:对自定义类型进行运算(保持运算符的特性)
返回值 operator要重载的运算符(参数列表)
{
//…函数体
}
例如:
Date& Date::operator+=(int day)
{
_day = day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
注意:①不能创建新的操作符:比如operator@
②重载操作符必须有一个类类型参数
③用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
④作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为 隐藏的this指针
作为全局函数重载时,其形参数目和操作数相同。
⑤.* :: sizeof ?: . 注意以上5个运算符不能重载。
运算符重载和函数重载无关。
2.赋值重载函数(特殊的运算符重载函数)
未定义时,编译器会自动生成默认赋值重载函数,性质与默认构造函数相同。
如果类内没有定义赋值重载函数,编译器会自动生成,所以赋值重载函数不可以定义成全局函数,不然会和自动生成的默认赋值重载函数冲突。
例如:
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
赋值重载函数的返回类型是类的引用,是为了保存“=”连续赋值的特性。
注意:
1.默认生成的拷贝构造和赋值重载:
a.内置类型完成浅拷贝/值拷贝——按字节一个一个拷贝
b.自定义类型,去调用这个成员的拷贝构造和赋值重载
2.区分拷贝构造和赋值重载:
①拷贝构造是特殊的构造函数,在类定义时将已经定义好的类对象拷贝给定义的新的类对象
②赋值重载:针对两个已经定义的类的对象进行
3.前置++和后置++的重载
例如:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自
动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一
份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
//由此可以看出来,后置++的重载比前置++的重载要多两次拷贝构造。
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; //d=2022,1,13;d1=2022,1,14
d = ++d1; //d=2022,1,15;d1=2022,1,15
}
由此可得:
前置++和后置++都修改了类对象本身,但是返回值不同,前置++的返回值时+1后的结果,后置++返回的是+1前的结果。所以前置++的返回类型为类的引用,也不需要对原本的类对象进行拷贝构造;而后置++的返回类型只能是原本的类对象的拷贝对象,拷贝原本的对象时要调用一次拷贝构造,函数返回由于返回的是拷贝对象的临时拷贝,要调用一次拷贝构造函数。
综上:1.前置++的重载函数的返回类型为类的引用,后置++的重载函数的返回类型是类类型
2.前置++的重载函数比后置++的重载函数少两次拷贝构造函数的调用。前置++:0次,
后置++:2次。
5.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&() const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容。
二、const成员函数
const修饰的是*this,即调用成员函数的类对象,故const成员函数不能修改类的成员变量。
不改变成员变量的成员函数后面都可加const,防止权限的扩大。
(此处的权限是指变量是否可以被修改,这个概念只有在使用指针和引用的赋值时才需要考虑。)
例如:void Display() const
例如:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
int main()
{
Test();
return 0;
}
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?不可以
2. 非const对象可以调用const成员函数吗是哟个?可以
3. const成员函数内可以调用其它的非const成员函数吗?不可以
4. 非const成员函数内可以调用其它的const成员函数吗?可以
三、总结
本文主要介绍了C++中类的六大特殊的成员函数以及const成员函数相关的知识点,如果有不足或者错误的地方,烦请各位大佬指正!