C++:类与对象---类的六个默认成员函数

一、前言

  如果一个类中什么成员都没有,简称为空类

class classname {};//空类


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

二、六个默认成员函数

1、初始化和清理

1.1、构造函数

 1.1.1 概念

我们在写C语言程序时,例如栈、队列等在实现的过程中,我们通常会先对结构体成员进行初始化,,因此需要写出一个初始化函数Init来实现这个功能,后续由我们自己手动调用该函数进而对结构体成员进行初始化。

我们举例说明:

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

  我们在Data类中写了一个初始化函数Init,我们每次创建一个对象时,都需要手动调用该函数才能保证每个数据成员都有一个合适的初始值。比较麻烦。那么有没有好的方法在对象创建时,就将信息设置进去呢?

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

1.1.2 特性

  构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
并不是开空间创建对象,而是初始化对象。

其特征如下:
 
1. 函数名与类名相同。
2. 无返回值。 
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。

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

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

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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函
数,d1可以定义通过。
  将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再
生成,无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用。

  是因为我们上述写的构造函数是带参数的,没有屏蔽之前d1是可以通过的,解除屏蔽后编译器不会生成默认的构造函数,所以d1此时定义会发现没有合适的构造函数,此时带上参数即可。

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

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

解释:

以上这个程序是运行不了的,因为它同时存在了无参构造函数和全缺省构造函数,存在两个默认构造函数,所以是运行不了的。默认构造函数只能存在一个。

1.2、析构函数

1.2.1 概念

  通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

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

1.2.2 特性

  析构函数是特殊的成员函数,其特征如下: 
1. 析构函数名是在类名前加上字符 ~。这个符号在C语言中是按位取反的意思,但是在这里不是。
2. 无参数无返回值类型。 

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。  

typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 3)
    {
        _array = (DataType*)malloc(sizeof(DataType) * capacity);
        if (NULL == _array)
        {
            perror("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);
   }

比如上述实现栈的时候,我们就可以不用像C语言在最后手动调用销毁函数,C++自动调用。

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

2、拷贝复制函数

2.1 拷贝构造函数

2.1.1 概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

  那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
  拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。

2.1.2 特性

拷贝构造函数也是特殊的成员函数,其特征如下:  

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

  我们之前在C语言学习过程中,我们将一个变量传给函数,形参一般有两种:一是传值调用,对变量的临时拷贝,二是传址调用,也就是变量的地址。

  我们在C++中想创建两个一模一样的对象,那我们在这里只能用传址调用,也就是引用。因为传值调用会造成无限递归。

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

#include<iostream>
using namespace std;
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(1, 2, 3);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

我们没有定义拷贝构造函数,但是编译器会生成默认的拷贝构造函数,我们依然可以得到自己想要的结果:

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。当涉及到资源申请时,就需要自己定义,因为编译器不知道我们想要什么样子的拷贝。

typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }
    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType *_array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2(s1);
    return 0;
}

  这时在涉及资源申请时,我们不去定义拷贝构造函数时,这里就会崩溃。

  那正确的应该如何定义呢?

  定义拷贝构造函数:

Stack(const Stack& d)
{
    _array = (DataType*)malloc(d._capacity * sizeof(DataType));
    if (nullptr == _array)
    {
        perror("malloc申请空间失败");
        return;
    }
    memcpy(_array, d._array, sizeof(DataType) * d._size);
 
    _size = d._size;
    _capacity = d._capacity;
   
}

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

2.2.赋值运算符重载

2.2.1 概念

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

  函数名字为:关键字operator后面接需要重载的运算符符号。

需要注意的是:

  • 不能通过连接其他符号来创建新的操作符:比如operator@ 
  • 重载操作符必须有一个类类型参数 
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义 
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
  • 藏的this
  • .*   ::  sizeof  ?:   .    注意以上5个运算符不能重载。这个经常在笔试选择题中出
  • 现。     
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date( const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void operator =(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, 2, 30);
	Date d2(d1);
	Date d3 = d1;
	d1.print();
	d2.print();
	d3.print();
	return 0;
}

2.2.2 特性

1. 赋值运算符重载格式

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

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& operator=(const Date& d)
    {
        if(this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        
        return *this;
    }
private:

3、取地址以及const取地址操作符重载

3.1 const成员

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2024, int month = 2, int day = 14)
	{
	_year = year;
	_month = month;
	_day = day;
    }
 	
	void print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date a(2024, 2, 19);
	a.print();
}

 当我们用const修饰类a时,我们这时候调用a对象的print()函数,我们会发现:

  “不能将"this"指针从"const Date"转换为"Date &"”,那么&a就是const Date*类型,是只读的,但你指向的内容变成了this 指针Date*类型的,变成了可读可写的,所以权限被放大,是不可以的。

  所以我们这时候要将this指针变成const Date*类型的,那应该怎么做呢?

在C++中,在该函数后面加const,将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。这样this指针就变成了const Date *类型。

注意:

  •   成员函数,如果是一个对成员变量只进行访问的函数,建议加const 这样const对象和非const对象都可以使用。
  •   成员函数,如果是一个对成员变量只进行读写访问的函数,不能加const,否则不能修改成员变量

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

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

class Date
{ 
public :
 Date* operator&()
 {
 return this ;

 }
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

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

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值