类和对象(2)

2024-08-12 08:22:33 浏览数 (2)

类的6个默认成员函数

我们需要从下面这两个方面来学习默认成员函数:

1 我们不写时,编译器默认生成的函数行为是什么

2 编译器默认生成的函数不满足我们的需求,我们需要怎样更改

如果一个类中什么都没有,那么被称为空类。任何类在什么都不写的情况下,会自动生成6个默认成员函数。

构造函数

可以通过Init给对象设置日期,但如果每次创建都用该方法调用日期,未免有一些麻烦。

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象由编译器自动调用,以保证每个成员都有一个合适的初始值,并且在对象整个声明周期只调用一次。

1 函数名与类名相同

2 无返回值(返回值什么都不需要给,也不需要void)

3 对象实例化时编译器自动调用对应的构造函数

4 构造函数可以重载(同一个类中,可以定义多个构造函数,只要它们的参数列表不同即可)

代码语言:javascript复制
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(2015, 1, 1); // 调用带参的构造函数
  
      // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
      // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
      Date d3();

}

5 如果类中没有显式定义构造函数,那么在c 中编译器会自动生成一个无参的默认构造函数,用户如果显式定义,则编译器不再自动生成。

代码语言:javascript复制
class Date
 {
  public:
 /*
 // 如果用户显式定义了构造函数,编译器将不再生成
 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;   
   return 0;
}

构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构

对象析构要在生存作用域结束的时候才进行析构

将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数

将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成

无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用 

6 关于编译器生成的默认构造函数,有些同学可能觉得没有用,因为在不实现构造函数的情况下,编译器会生成默认的构造函数。 

d对象调用了编译器默认生成的函数,但是_year _month _day 依旧是随机值。也就是说编译器默认生成的默认构造函数没有用吗?

c 把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据型,如int,char等等,自定义类型就是struct,class,union等自定义的类型,编译器默认生成的构造函数会对自定义类型成员_t调用它的默认成员的函数。

代码语言:javascript复制
class Time
{
public:
 Time()
 {
 cout << "Time()" << endl;
 _hour = 0;
 _minute = 0;
 _second = 0;
 }
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
 int _year;
 int _month;
 int _day;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 return 0;
}

内置类型成员变量在类中声明可以给默认值。 

代码语言:javascript复制
class Time
{
public:
 Time()
 {
 cout << "Time()" << endl;
 _hour = 0;
 _minute = 0;
 _second = 0;
 }
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 return 0;
}

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

代码语言:javascript复制
class Date
{
public:
 Date()
 {
 _year = 1900;
 _month = 1;
 _day = 1;
 }
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private:
 int _year;
 int _month;
 int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
 Date d1;
}

不能的  因为在调用的时候不知道调用那个函数 然后编译器就是出现一个调用不明确的错误 你的一个是全缺省 一个是无参的构造函数  那你现在没有传参 不知道调用哪一个

总结:大多数情况,构造函数都需要我们自己去实现,少数情况类似MyQueue且Stack有默认构造时,MyQueue自动生成就可以用。

特性

构造函数是特殊的成员函数,主要任务并不是构造函数,而是初始化对象。

 析构函数

概念

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特性

1 析构函数的函数名是在类名之前加上符号~

2 无参数无返回值(不需要void)

3 一个类只能有一个析构函数,如果没有定义析构函数,则会生成默认的析构函数

4对象生命周期结束时,自动调用析构函数

5 跟构造函数类似,我们不写编译器自动生成的析构函数对内置成员不做处理,自定义成员会调用他的析构函数

6 后定义的先析构(和栈一样,后进先出)

7如果类中没有申请资源时析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

 拷贝构造函数

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

1 拷贝构造函数是构造函数的一个重载

2 拷贝构造函数的第一个参数必须是类类型对象的引用,且任何额外的参数都有默认值,使用传值方式编译器直接报错,因为语法层面会引发无穷递归调用

每次调用拷贝构造要先传值传参,传值传参是一种拷贝, 又形成一个新的拷贝构造函数,就形成了无穷递归。

如果加上引用,因为d是d1的别名,所以不会形成新的拷贝构造。

3 若未显式定义,编译器会生成默认的拷贝构造函数(适配c语言)。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

程序不能析构两次,程序崩溃

4 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5如果一个类显式实现了析构并释放资源,那么他需要写拷贝构造,否则就不需要

6

传值返回不会返回st,会返回st的拷贝

代码语言:javascript复制
Stack func2()
{
   Stack st;
   return st;
}
int main()
{ 
   Stack ret=func2();
   return 0;
}

传址引用 

返回st的别名,st销毁

传值返回会产生一个临时对象调用拷贝构造,传值返回,返回的是对象的别名,没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么引用返回是有问题的,相当于一个野引用,类似一个野指针。传引用返回可以减少拷贝,但是一定要确保返回对象在当前函数结束后还在,才能引用返回。

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。

赋值运算符重载

运算符重载

是为了增强代码的可读性加入的。运算符重载是具有特殊函数名的函数。

关键字operator后面接需要重载的运算符符号

重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。

如果第一个重载运算符函数是成员函数,则第一个运算符对象传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。

运算符重载以后,其优先级和结合性与对应的内置操作符运算符保持一致。

.*       ::        sizeof        ?:        .       注意以上5个运算符不能重载。

一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意 义,但是重载operator 就没有意义。

重载 运算符时,有前置 和后置 ,运算符重载函数名都是operator ,无法很好的区分。C 规定,后置 重载时,增加一个int形参,跟前置 构成函数重载,方便区分。

重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。

重载操作符至少有一个类类型参数!不能通过运算符重载改变内置类型对象的含义,如:int operator (int x,int y)

代码语言:javascript复制
//成员函数指针类型
typedef void(A::*PF) ();
int main()
{
   //void(A::*pf)()=nullptr;
   //typedef之后就可以这么写:
   PF pf=nullptr;
   //c  规定成员函数必须要加&才能取到函数指针
   pf=A::func;
}

有隐含的this指针,想要(*pf)()这样回调函数并不能成功

那如果 A aa;  (*pf)(&aa);    这样能够成功吗?

也是不能成功的。c 规定,只能这样能成功A aa;  (aa.*pf)(); 

.*是在指针回调函数的时候使用的。

函数声明

函数名重载:函数名相同,参数不同

运算符重载:重新定义符号 

两个运算符重载的函数可以构成函数重载,因为他们的函数名相同,参数不同

赋值运算符重载

是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,而拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

如图所示,上面的是赋值重载,下面的是拷贝构造(先初始化,再拷贝)。

特点

1 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝

d就是d2的别名,this就是d1 

2 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景

如下图,上方为拷贝构造,下方为构造(第一个的话其实就是你传递进来的那个对象,第二个是一个指针,指向的是一个地址)

传值返回也会生成一个拷贝 ,出了作用域,d3也就是*this还在,那么就白白生成了一块空间。

3 实现析构的类都需要拷贝构造和赋值重载

日期函数

代码语言:javascript复制
bool Date::CheckDate() const//const关键字用于指明该函数不会修改类中的任何成员变量
{
  if(_month<1||_month>12||_day<1||_day>GetMonthDay(_year, _month))
  {
     return false;
  }
  else
     return true;
}
代码语言:javascript复制
Date Date::(int year,int month,int day)
{  
   _year = year;  
   _month = month;  
   _day = day; 
   if(!CheckDate())
   {
      cout<<"非法日期:";
      Print();
   }
}
// void Date::Print(const Date* const this)
//这个意思表示这个指针的指向和这个对象本身,都不能进行修改

void Date::Print() const
{
	//  _year;
	cout << _year << "/" << _month << "/" << _day << endl;
}
bool Date::operator<(const Date&d) const
{
    if (_year < d._year)
	{
		return true;
	}
    else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			return _day < d._day;
		}
	}

	return false;
}
bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}

bool Date::operator>=(const Date& d) const
{
	return !(*this < d);
}

bool Date::operator==(const Date& d) const
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

bool Date::operator!=(const Date& d) const
{
	return !(*this == d);
}
Date& Date::operator =(int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}

	_day  = day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		  _month;
		if (_month == 13)
		{
			_year  ;
			_month = 1;
		}
	}

	return *this;
}
Date Date::operator (int day) const//x int不需要改变当前对象x的值,所以用tmp暂且把当前对象的值保存起来
{
	Date tmp = *this;
	tmp  = day;

	return tmp;
}
Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this  = (-day);
	}

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day  = GetMonthDay(_year, _month);
	}

	return *this;
}

const成员函数

将const修饰的"成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数

隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

请思考下面的几个问题:

1. const对象可以调用非const成员函数吗?

const 对象不能调用非 const 成员函数,因为非 const 成员函数可能会修改对象的内部状态,而 const 对象保证在其生命周期内不会被修改。编译器会阻止这种调用,以确保 const 对象的不可变性。

2. 非const对象可以调用const成员函数吗?

答案是肯定的。非 const 对象当然可以调用 const 成员函数。const 成员函数保证不会修改对象的任何成员变量(至少不通过显式的方式),但这并不阻止非 const 对象调用它们。这提供了一种方式来提供对对象状态的只读访问,同时仍然允许非 const 对象进行其他可能的修改。

3. const成员函数内可以调用其它的非const成员函数吗?

答案是否定的。const 成员函数内部不能直接调用非 const 成员函数,因为这样做可能会违反 const 成员函数的保证——即不会修改对象的任何成员变量。如果 const 成员函数需要调用另一个函数,并且该函数可能修改成员变量,那么它应该调用另一个 const 成员函数,或者通过某种方式(如使用指针或引用指向非 const 对象)绕过 const 约束(但这通常不是推荐的做法)。

4. 非const成员函数内可以调用其它的const成员函数吗?  

答案是肯定的。非 const 成员函数内部可以自由地调用 const 成员函数。这是因为 const 成员函数保证不会修改对象的任何成员变量,所以它们可以在任何情况下被安全调用,包括从非 const 成员函数内部。这允许非 const 成员函数提供对对象状态的只读访问,同时仍然能够执行其他可能的修改。

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

代码语言:javascript复制
class Date
{ 
public :
 Date* operator&()
 {
 return this ;这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
 
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容。

0 人点赞