类和对象(中)

目录

类的6个默认成员函数

构造函数

概念

特性 

析构函数

拷贝构造函数

特性 

赋值运算符重载

赋值运算符重载方法怎么写?

总结

const成员

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


类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

class Date {};

构造函数

概念

class Date
{
public:
void Init(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.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}

对于上面的Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

这时候就引入构造函数的概念。

构造函数:构造函数是一个特殊的成员函数,名字与类名相同,并且不能有返回值类型,创建类类型对象时由编译器自动调用,在对象整个生命周期内只调用一次。

调用这个函数的目的:保证每个数据成员都有 一个合适的初始值。

public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

特性 

1.构造函数是可以重载的。

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(2015, 1, 1); // 调用带参的构造函数
Date d2; // 调用无参构造函数
}

这时候上面有一个无参的和一个有参的,如果在添加一个

Date d3();

d3.Print();

这时候d3就不是创建一个新对象,而是一个函数声明,即:编译器认为声明一个函数名为d3,没有参数,返回值类型为Date类型的函数。

d3.Print()编译报错

当调用无参构造函数创建对象时,对象之后的括号必须省略。


2.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date d1;
return 0;
}

如果用户没有显示实现任何构造方法,则编译器会给该类生成一个无参的默认构造方法调用。

如果用户显式定义了构造函数,编译器将不再生成无参的构造方法。


3. 关于编译器生成的默认成员函数,很多朋友会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

具体看生不生成就要看具体的场景,要看这个代码生成有没有意义。

注意:虽然Date类例子中,编译器生成的无参构造方法意义不大,但并不代表无参构造方法就没有作用。

class Time
{
public:

Time(int hour=0,int minute=0,int second=0)
{

_hour = 0;
_minute = 0;
_second = 0;
cout<<"Time(int,int,int)"<<endl;
}

void Print()
{
  cout<<hour<<":"<<minute<<":"<<second<<endl;
}

private:
int _hour;
int _minute;
int _second;

};


class Date
{

public:

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
t.Print();
}

private:
        // 基本类型(内置类型)
int _year;
int _month;
int _day;

      // 自定义类型
Time _t;
};

int  main()
{
  Date d;
  d.Print();
  return 0;
}

在上面分析中:我们日期类中必须要用Time类的构造方法来完成初始化,因为Time类已经显示定义的构造方法。所以编译器要给Date生成构造方法。

所以分析可得:上面编译器给日期类生成构造方法就是有意义的。(如果Time类没有构造方法,则编译器就不会给Date类生成构造方法)。

总结可得:

虽然c++语法规定了,在类中,如果用户没有显示定义任何构造函数,则编译器一定会生成一份无参得构造函数。

但是在具体编译器实现过程中,就会跟语法稍微有所出入,因为编译器可能会考虑程序运行效率问题,如果编译器感觉生成得构造方法没有意义,则不在生成。


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

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;
}

析构函数

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

析构函数的特性:

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

下面是一个析构函数的示例: 

typedef int DataType;

class Stack
{

public:
   Stack(size_t capacity = 3)
  {
    _array = (DataType*)malloc(sizeof(DataType) * capacity);
    if (NULL == _array)
   {
     printf("malloc申请空间失败!!!");
      return;
   }
      _capacity = capacity;
      _size = 0;
 } 

void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}

//析构函数
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}

private:
DataType* _array;
int _capacity;
int _size;
};

void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}

 5 .关于编译器自动生成的析构函数,是否会完成一些事情呢?

注意:像下面Date类一样,如果对象中没有涉及到任何资源管理时,该类的析构函数可以不用给出。 

class Date
{
public:
	
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;

};

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

  


拷贝构造函数

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

现在有一个需求:

如下图代码,我们想要 d2创建好了之后,想要和d1中的日期完全相同,这时候就用到了拷贝构造。

class Date
{
public:

	Date(int year = 1900, 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(2022, 11, 12);

	//需求:d2创建好了之后,想要和d1中的日期完全相同
	Date d2(d1);
	//Date d2(2022, 11, 12);
	return 0;
}

特性 

1. 拷贝构造函数是构造函数的一个重载形式。
2.
拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

代码示例如下:

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}

拷贝构造函数调用场景:用已经存在的对象构造新对象时。

为什么传值会引发无穷递归?

因为在进行传值传参时,实参传给形参是把实参的值拷贝一份给形参,而我的实参d1是自定义类型的,需要调用拷贝构造,传值传参是要调用拷贝构造的,但是我如果不想调用拷贝构造呢?就需要引用传参。如果不引用的话就会一直调用拷贝构造,如下图:

解释:就是我们要 把d1传给拷贝构造,就需要调用拷贝构造,这时候就需要把整个Date拷贝一份,这时候就需要传参,传参的话就要继续调用Date,就这样一直传递下去。


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

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

如下图:

拷贝过程就是逐字节拷贝的,就是将d1中的内容,原封不动的拷贝到d2中,就是值拷贝。

对于上面的Date 类编译器并没有生成拷贝构造函数,但是完成了拷贝构造的工作。

问题:编译器生成的拷贝构造||虽然没有生成但是也可以完成拷贝构造的工作:既然编译器已经可以完成了,那拷贝构造还需要用户自己写吗?

答案:像日期类这种没有涉及到资源管理时,可写可不写,因为编译器就可以完成拷贝的工作,如果需要自己再去实现,注意:编译器是按照值的方式拷贝的-----即:将一个对象中的内容原封不动的拷贝到另一个对象中(浅拷贝)

如果类中涉及到资源管理时,则拷贝构造是必须要实现的。

下面的程序就出现了崩溃,因为它发生了浅拷贝,要用深拷贝去解决

typedef int DataType;

struct Stack
{
public:
	Stack()
	{
		_array = (DataType*)malloc(10 * sizeof(DataType));
		if (NULL == _array)
		{
			assert(false);
			return;
		}
		_capacity = 3;
		_size = 0;
		cout << "Stack():" << this << endl;

	}

	void Push(DataType data)
	{
		_array[_size] = data;
		++_size;
	}
	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}
	DataType Top()
	{
		assert(!Empty());
		return _array[_size - 1];
	}

	bool Empty()
	{
		return 0 == _size;
	}

	int Size()
	{
		return _size;
	}


	//析构函数
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
		cout << "~Stack():" << this << endl;
	}


private:
	void _checkCapacity();


private:
	DataType* _array;
	size_t _capacity;
	size_t _size;
};

void TestStack()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	

	Stack s2(s1);


} 

int main()
{
	TestStack();
	
	return 0;
}

为什么出现了程序崩溃?


4.拷贝构造函数典型调用场景

class Date
{
public:

Date(int year, int month, int day)
{
   _year=year;
   _month=month;
   _day=day;
   cout << "Date(int,int,int):" << this << endl;
}

Date(const Date& d)
{
   _year=d._year;
   _month=d._month;
   _day=d._day;
cout << "Date(const Date& d):" << this << endl;
}

~Date()
{
cout << "~Date():" << this << endl;
}

private:
int _year;
int _month;
int _day;
};



根据相面代码

void TestDate1()

{

   //只要创建对象,就必须调用构造函数

  //拷贝构造是:用已经存在的对象构造新对象时调用

 //其余创建新对象的场景调用的基本都是构造函数

 Date d1(2022,11,15);

//拷贝构造函数调用场景1

 Date d2(d1);

}

//拷贝构造函数调用场景2:以值得方式传参

void TestDate2(Date d)

{

   Date dd;

}

//拷贝构造函数调用场景3:以值的方式返回对象

void TestDate3(Date d)

{

     Date d;

   return d;

}

Date TestDate4()

{

 return Date(2022,11,5);

}

注意:

1.以值得方式返回时,如果返回的是匿名对象,则编译器不会在用匿名对象拷贝构造临时对象,而是直接将匿名对象返回了。

 匿名对象:没有名字的对象

2.如果参数是以值得方式传递,实参如果也是匿名对象,也会少一次拷贝构造

int main()

{

   Date md;

  TestDate1();

  TestDate2(md);

  TestDate3();

   return 0;

}


 
class Date
{
public:
Date(int year, int month, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}


赋值运算符重载

什么时候调用赋值运算符重载?

请看下面的代码

void TestDate()

{

   Date d1(2022,11,15); //拷贝构造

   Date d2(d1);

   

   Date d3;

   d3=d2;        //用已经存在的对象 给另一个已经存在的对象赋值

   

 Date d4=d3;  //拷贝构造

}

语法:如果说程序员没有显示定义赋值运算符重载,则编译器会自动生成一份。

实际情况:编译器不一定会生成,但是编译器一定会完成赋值的工作。

当类中涉及到资源管理时,赋值运算符重载也是必须要实现的。 

赋值运算符重载方法怎么写?

1.什么是运算符重载

2.赋值运算符该怎么写

class Date
{
public:

	Date(int year = 1900, 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;
		
	}

	~Date()
	{
		cout << "~Date():" << this << endl;
	}
	//需求:检测两个日期类型对象是否相等

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

int main()
{

	Date d1(2022, 11, 15);
	Date d2(d1);

	if (d1.IsEqual(d2))
	{
		cout << "d1==d2" << endl;
	}
	else
	{
		cout << "d1 != d2" << endl;
	}

   //注意:自定义类型不支持“==”的运算符
	//      因为编译器不知道该怎么比较
	//      如果一定要使用“==”来比较自定义类型对象
	//    必须告诉编译器比较的规则
	if (d1==d2)
	{
		cout << "d1==d2" << endl;
	}
	else
	{
		cout << "d1 != d2" << endl;
	}

  return 0;
}

所以引入了赋值运算符重载。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)


// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date& d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._day;
	}
       if (d1==d2)                   //if(d1.operator==(d2))
   //现在这两个代码就是等价的

     {
        cout << "d1==d2" << endl;
    }
    else
    {
        cout << "d1 != d2" << endl;
    }

 

 在底层可以看见会调用上面的运算符重载。


其它类型的运算符重载

//注意:1.operator之后跟的一定是c++语言支持的 运算符
//不能凭空自己臆造运算符

bool operator<(const Date& left, const Date& right)
{
	return left._day < right._day;
}



//2.重载操作符必须有一个类类型参数
Date& operator+=(Date& d, int days)
{
	d._day += days;
	return d;
}

//重载的运算符必须要复合其含义
//d2=d1 可以的
//d3=d2=d1 必须支持连续赋值   必须是d1给d2赋值 然后d2给d3赋值 所以要返回的this

//d2=d1---> d2.operator(d1)
// d2传给this  d1传给d
Date& operator=(const Date& 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;
}

}


//前置++
Date& operator++()
{
   _day+=1;
   return *this;
}

//后置++
//语法中:为了区分前置和后置++
//规定:给后置++多添加一个int类型的参数,目的是为了让前置++和后置++形成函数重载
 
//后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,
//然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
 
Date& operator++(int)
{
    Date temp(*this);
   _day+=1;
   return temp;
}



int main()
{
   d1<d2

  d3=d2=d1;
  
  d3=d3; //自己给自己赋值

  d2=++d1;
  d2=d1++;
 
 
   return 0;
}

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5    .* :: sizeof ?: . 注意以上5个运算符不能重载。


总结

1. 赋值运算符重载格式
  参数类型:const T&,传递引用可以提高传参效率
  返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
 检测是否自己给自己赋值
 返回*this :要复合连续赋值的含义

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

4.注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。跟拷贝构造一样,如果只是浅拷贝就会造成内存泄露。


const成员

const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

class Date
{
   public:
         Date(int year=1900,int month=1,int day=1)
     
    {
       _year=year;
       _month=month;
       _day=day;
    }
   //this的类型:Date*const-->this的指向不能修改,this指向空间中的内容可以修改
   //可写入:可以修改成员变量
  void show()
  {
      cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
  }


  //const成员函数:被const修饰的成员函数
 // 特性:不能修改"成员变量"
 //const修饰成员函数,实际是在修饰this指针
 //this的类型:const Date*const
//只读:只能读取this中的成员,不嫩修改
void Print()const
{
    cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}
  
private:
   int _year;
   int _month;
   int _day;
};

int main()
{
  //普通对象即可以调用普通成员函数,也可以调用const成员函数
  //d1对象:是可读可写对象
  Date d1(2023.9.13);
  d1.show();
  d1.Print();

 //const对象:只能调用const成员函数
 //d2对象:只读的对象,成员函数只能读取该对象中的内容,不能修改
 const Date d2(d1);
 d2.Print();
 // d2.show();   //编译报错:d2是一个只读的对象,不允许修改该对象的内容,
                 //但是如果允许该对象调用普通的成员函数,在该成员函数中完全可能会修改const对象中的内容,代码不安全。
 return 0;
}

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

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

可以

void func1()
{}

void func2()const
{}

//非const成员函数内可以调用其它的const成员函数吗? 可以
//show(): 普通方法,该方法内部可以修改也可以不修改成员变量
//this的类型:Date*const   即:当前对象可以修改也可以不需要

void show()
{
  func1();//可以调用
  func2(); 
  _day+=1;
  cout<<_year<<"/"<<_month<<"/"<<_day<<endl;

}

//const成员函数内可以调用其它的非const成员函数吗? 不可以
//const成员函数内部只能调用const成员函数
//const成员函数:const本质修改this指针,表明该成员函数内部一定不会修改成员变量
//
void Print()const
{
  //func1();编译失败
  func2(); 
    cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}


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

不可以

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

可以


 问题:

const成员函数中,是不能对任何成员变量进行修改的,但是如果一定需要对某个成员变量修改呢?

class Date
{
//如果在const成员函数中,一定要修改某个成员变量时
//在定义该成员变量的时候,使用mutable关键字修改该成员即可
void func3()const
{
  //_month+=1;  //这个就会报错。
   _day+=1;
  
}

private:
int _year;
int _month;
mutable int _day;

};

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

int main()
{
    Date d1(2022, 11, 15);

    需求:在对对象取地址的同时需要将对象的地址打印出来
    Date*p = &d1;

}

这时候就需要

Date* operator&()
    {
        cout << this << endl;
        return this;
    }

如果是const类型的话

int main()
{
    Date d1(2022, 11, 15);

  
    Date*p = &d1;

    const Date d2(d1);
    const Date*p2 = &d2;

    return 0;
}

这时候const的对象就不能调用上面普通的方法来进行取地址。

然后引入下面的重载,但是却不能调用,这是为什么?

const Date* operator&(int)const   
   {                                       
		cout << this << endl;
	return this;
   }

解释:

注意:所以在参数列表中不能加任何参数,否则编译器会将&当成按位&来处理

正确的写法:

 //this的类型:const Date*const
const Date* operator&()const   
	   {                                         
		cout << this << endl;
		return this;
       }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值