一、类的6个默认成员函数的引出
1.默认成员函数的概念:用户没有显示实现,编译器会生成的成员函数。
class Date
{
public :
void Print ()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
void InitDate(int year , int month , int day)
{
_year = year;
_month = month;
_day = day;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
int main()
{
Date d1, d2;
d1.InitDate(2024,5,23);
d2.InitDate(2024,6,23);
d1.Print();
d2.Print();
return 0;
}
对于日期类,可以通过InitDate公有的方法给对象设置内容,但是每次创建对象都要调用该方法进行初始化太过于麻烦,因此构造函数便产生了。
(1)作用:
构造函数的作用是:在对象创建时,就将信息传入进去;它是一个特殊的成员函数,名字与类名相同 ,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
(2)特性:
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
class Date
{
public :
// 1.无参构造函数
Date ()
{}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2 (2024, 5, 1); // 调用带参的构造函数
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如下:声明了无参函数func,该函数返回类型为Date。
class Date
{
public :
// 1.无参构造函数
Date ()
{}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
Date func();//函数声明
还需要提醒的是:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义(不论定义多少,只要满足重载就行),编译器则不再生成,因此在显示定义构造函数时要考虑周全(避免出现只定义无参数的构造或只有有参数的构造,最好是定义全缺省的构造)。
class Date
{
public :
// 1.无参构造函数
Date ()
{
_year=2000;
_month=1;
_day=2;
}
// 2.带参构造函数
Date (int year=2024, int month=5 , int day=1 )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void test()
{
Date d1;//调用默认构造(会出调用现歧义)
}
上面的代码说明:无参数的默认构造和全缺省的默认构造虽然满足函数重载,但是会出现调用歧义。因此在应用的时候,一般都用全缺省的默认构造。
(3)特别说明:
当我们不自己写默认构造函数时,会调用编译器自动生成的默认构造,但是编译器自动生成的默认构造只对自定义类型初始化,对内置类型没有规定是否初始化(有些编译器会处理)!C++11对此打了补丁(可以对内置类型变量给缺省值)。下面代码可以说明
class Date
{
public :
void print()
{
cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private :
int _year=1 ;//给缺省值
int _month=1 ;
int _day =1;
};
那么编译器自己生成的默认构造意义何在?这当然是有意义的,在一个类的成员变量都为自定义类型的时候,这个时候便会对自定义类型的成员变量进行初始化(调用它的默认构造,如果它的默认构造是编译器自动生成的,则里面的内置类型变量仍为随机值)。
(4)总结:
1、自定义类型构造的尽头还是要对内置类型的成员进行构造,所以一般情况下,构造函数都要我们自己去实现。
2、只有少数情况下不要自己写默认构造,比如类的成员变量都为自定义类型,且这些自定义类型的默认构造最好不是编译器自动生成的,而是自己显示定义的。
2、析构函数
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)//构造
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()//析构
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
int main()
{
SeqList a;
a.~SeqList();//显示调用析构
}
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)//构造
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
int main()
{
SeqList a;
}//程序结束自动调用编译器自动产生的析构
(3)总结:
对于对象里开辟了空间资源的,需要自己写析构清理;对象里面没有额外开辟空间的,编译器自动生成的析构足够了。
3、拷贝构造
(1). 拷贝构造函数是构造函数的一个重载形式,用同类型的对象进行拷贝初始化。
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//防止对被拷贝对象的修改,所以加上const修饰更为稳妥
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
Date d2(d1);//Date d2=d1;这样也是拷贝构造,以赋值的形式。
return 0;
}
(4)总结:
1、如果没有管理资源,一般情况不需要写拷贝构造,系统默认生成的就够了。
2、如果都是自定义类型成员,内置类型成员没有指向资源,默认生成的拷贝构造就可以。
3、一般情况下,不需要写析构函数的,就不需要写拷贝构造。
4、如果内部有指针或一些值指向资源,需要显示写拷贝构造(深拷贝),且要显示写析构。
4、赋值重载
(1)运算符重载:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返
回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1、不能通过连接其他符号来创建新的操作符:比如operator@
2、重载操作符必须有一个类类型或者枚举类型的操作数
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)//全局定义,则类中的成员变量要是公有(这样才能访问),所以将private注释掉了
{
return d1._year == d2._year;
&& d1._month == d2._month;
&& d1._day == d2.—_day;
}
int main()
{
Date d1(2024,5,1);
Date d2(2024,5,2);
operator==(d1,d2);//显示调用
d1==d2;//转换调用,编译器会转换成operator==(d1,d2);
return 0;
}
当运算符重载定义在全局时,就破坏了封装性。要保证封装,有3种方式:1、提供这些成员的get和set函数(java中常见)2、在类内部声明友元(friend关键字)3、将此重载为成员函数。
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return this->_year == d_year;//this可不写
&& this->_month == d._month;
&& this->_day == d._day;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,5,1);
Date d2(2024,5,2);
d2.operator==(d1);//显示调用
d1==d2;//转换调用,编译器会转换成d1.operator==(d2);
return 0;
}
如果全局和局部都定义了,那么优先调用类里面的函数。
(2)赋值运算符重载:
赋值重载与拷贝构造是有区别的。拷贝构造是用一个已初始化的对象初始化一个没有初始化的对象;而赋值重载是将一个已初始化对象的值拷贝给另外一个已初始化的对象。
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator=(const Date& d)//赋值重载;这样写完备吗?d1=d2=d3;这样的赋值还支持吗?
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,5,1);
Date d2(2024,5,2);
Date d3=d1;//拷贝构造
d1=d2;//赋值重载
return 0;
}
上面的赋值重载如果以没有返回值的形式写,那么在连续赋值的操作情况下就行不通了。因此可以如下面这个代码进行改进:
Date operator=(const Date& d)//传值返回
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date& operator=(const Date& d)//传引用返回
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
传值返回,返回的是对象的拷贝;传引用返回,返回的是对象的别名(实际还是指向同样的空间)
传引用返回的效率要高一些,但是要确保返回的对象此时还没有被销毁。这里总结一下:
1、返回对象生命周期到了,会析构,传值返回。
2、返回对象生命周期没到,不会析构,传引用返回。
所以综上,在d1=d2时,调用赋值重载,此时返回的对象是*this也就是d1,而d1此时的生命周期还没有截止,所以不会被销毁,所以这里传引用会更好,效率会更高(减少拷贝)。
(3)cin和cout打印自定义类型
c++的流插入和流提取默认支持内置类型,但是他们却不默认支持自定义类型。这是因为cout和cin是全局对象,分别叫ostream和istream。
int i=0;
cout<<i;
cout.operator<<(i);//两者等价
因此,在这里就知道了为什么cout/cin能自动识别类型,这是因为这些流插入(输出)重载构成函数重载。
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
void Date::operator<<(ostream& out)
{
out<<_year<<" "<<_month<<" "<<_day<<endl;
}
int main()
{
Date d1(2024,5,1);
d1.operator(cout);
d1<<cout;//两者等价
return 0;
}
这里可能有人会有疑问:平时写输出都是cout在前面,为什么这里在后面?其实原因很简单。这是因为运算符重载中参数顺序和操作数顺序是一致的;而d1传的参数是隐式的this指针,它已经占有了最左边的位置。因此,operator想重载为成员函数是可以的,但是不符合正常逻辑,但当重载为全局的时候就符合正常逻辑了,但此时要访问对象的私有成员就不行了,要把对象的私有成员变为公有(public)但这是就不满足封装了。
void operator<<(ostream& out,const Date& d)//定义为全局
{
out<<d._year<<" "<<d._month<<" "<<d._day<<endl;
}
cout<<d;//此时可以这样写
此外,如果想保持一定的封装特性,我们可以在全局定义,在类内部声明友元。
5、const成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐
含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public :
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private :
int _year=1 ;
int _month=1 ;
int _day =1;
};
int main()
{
const Date d1(2024,5,30);
d1.print();//会报错,因为此时为权限的放大
return 0;
}
上述的代码中,因为d1被const修饰,所以d1只能被只读,而成员函数print里的隐式参数this的类型为Date* 可读可写,所以是权限的放大。那么有什么解决方法呢?因为this指针是隐式的,所以直接修改它的类型行不通。这里就要用到const成员函数了。操作如下:
class Date
{
public :
Date(int year = 2024, int month = 5, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()const //const 成员函数
{
cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private :
int _year=1 ;
int _month=1 ;
int _day =1;
};
int main()
{
const Date d1(2024,5,30);
d1.print();//会报错,因为此时为权限的放大
return 0;
}
此时在函数括号后面加一个const就可以了,这里的const是修饰参数(隐式的*this对象)。
6、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};