【C++】类和对象(中)

目录

类的6个默认成员函数

构造函数

析构函数

拷贝构造函数

运算符重载

赋值运算符重载

const成员函数

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


类的6个默认成员函数

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

空类并不是什么都没有,任何类什么都不写时,编译器会自动生成以下6个默认成员函数

默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数

【C++】类和对象(中)_第1张图片

构造函数

C++的很多函数是为了填C语言的坑,比如用C语言写的栈和队列,定义的变量经常忘记初始化就拿来用,导致程序崩溃;甚至调用未初始化的函数使得变量被制成随机值

于是有了构造函数,保证对象一定被初始化         

特性: 

构造函数是特殊的成员函数,虽然名字叫构造,但主要任务并不是开空间创造对象,而是

初始化对象

  1.  函数名与类名相同
  2. 无返回值(也不用写void)
  3. 对象实例化时自动调用对应的构造函数
  4. 构造函数可以重载

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

构造函数的定义

class Date
{
public:
    Date(int year, int month, int day) //带参构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date() //无参构造函数
    {
        _year = 1994;
        _month = 8;
        _day = 8;
    }
private:
int _year;
int _month;
int _day;
};

int main()
{
     Date d1(2023, 7, 28); //调含参构造函数
     Date d2; //调无参构造函数
     //无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}

 如果传一个或两个参数,就要重新定义一个构造函数

这时定义一个全缺省构造函数就会比较方便 

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

C++类型分类:

内置类型 / 基本类型: int / double / char/指针等等

自定义类型: struct / class

默认生成构造函数:1.内置类型成员不做处理

2.自定义类型成员会去调用他的默认构造函数 

默认生成构造函数内置类型成员不做处理

 但你可以给定一个值

【C++】类和对象(中)_第2张图片

这并不是初始化而是缺省值 

析构函数

概念

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

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

析构函数是特殊的成员函数, 其特征如下:

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

对于日期类我们没有什么资源需要清理,可以不用显示写的,默认生成的析构函数就够用,现在实现一个栈的析构函数;
因为栈我们是用数组实现的,我们先需要向堆申请一块空间,最后对象销毁时,堆上的空间并没有释放,如果没有释放会造成内存泄漏,所以需要我们实现一个析构函数去完成这块的清理工作: 

//析构函数先定义的后析构,后定义的先析构
class Stack
{
public:
	// 栈的构造函数
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		
		_top = 0;
		_capacity = capacity;
	}
		
	// 栈的析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
		
private:
	int* _a;
	int _top;
	int _capacity;
};

 默认生成的析构函数的特点:

跟构造函数类似:a、内置类型不处理 b、自定义类型成员会去调用它的析构

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

C c;
int main()
{
    A a;
    B b;
    static D d;
}

 程序析构顺序:B A D C

分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象

2、全局对象先于局部对象进行构造

3、局部对象按出现顺序构造,无论是否为static

4、所以构造顺序为C A B D

拷贝构造函数

在创建对象时,我们可以创建一个与已存在的对象一模一样的新对象

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

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

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用 
class Date
{
public:
 	// 构造函数
 	Date(int year = 1, int month = 1, int day = 1)
 	{
 		_year = year;
 		_month = month;
 		_day = day;
 	}
 	
 	// 拷贝构造函数
 	Date(Date d)
 	{
 		_year = d._year;
 		_month = d._month;
 		_day = d._day;
 	}
 	
 private:
 	int _year;
 	int _month;
 	int _day;
};

int main()
{
    Date d1(2023, 8, 3);
    Date d2(d1);
}

 上面代码的拷贝构造函数,如果使用传值的方式,

调用拷贝构造要先传参,而传参又是一个拷贝构造,层层递归,无穷无尽

解决方式:加引用

// 拷贝构造函数
    Date(const Date& d) //注意加const防止被修改
 	{
 		_year = d._year;
 		_month = d._month;
 		_day = d._day;
 	}

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

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

浅拷贝的两大问题

  1. 一个对象修改会影响另一个对象
  2. 会析构两次,程序崩溃 

解决方式:自己实现深拷贝

运算符重载

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

也具有其返回值类型,函数名字以及参数列表, 其返回值类型与参数列表与普通的函数类似

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

函数原型: 返回值类型 operator操作符(参数列表)

注意:

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

int main()
{
    Date d1(2023, 8, 3);
    Date d2(2023, 8, 4);

    d1 == d2;
    d1 < d2;
    d1++;
    d1 + 100;
}

定义两个日期类对象,但是现在的日期类对象并不好用

比如判断两个日期是否相等,谁大谁小,日期自增,计算100天后是哪天

它并不支持==,<,++,+等运算符的运算

这是因为内置类型可以直接使用运算符运算,编译器知道如何运算

而自定义类型则无法直接使用运算符,因为编译器也不知道如何运算

想支持可以自己实现一个运算符重载

实现一个日期类比较相等和相减

class Date
{
public:
 	// 构造函数
 	Date(int year = 1, int month = 1, int day = 1)
 	{
 		_year = year;
 		_month = month;
 		_day = day;
 	}
 	
    bool operator==(const Date& x) //因为有隐藏指针,为了参数匹配,所以只能传一个参数
    {
    return _year == x._year
        && _month == x._month
        && _day == x._day;
    } 

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


int main()
{
    Date d1(2023, 8, 3);
    Date d2(2023, 8, 4);

    cout << d1.operator == (d2) << endl;
}

 private里面的成员是私有的,在外面不能访问
可以直接把函数写到类里面

赋值运算符重载

1.赋值运算符重载格式

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

注意: 赋值运算符只能重载成类的成员函数不能重载成全局函数,因为重载成全局的,类里面又会自动生成一个,这样编译器就无法分辨

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

注意:内置类型成员变量是直接赋值的, 而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值 

下面是日期类赋值运算符重载: 

Date& Date::operator=(const Date& d) 
{
	if (this != &d) //this是第一个操作数的地址,如果两个操作数地址一样就跳过赋值过程
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	return *this; //返回值为了支持连续赋值
}
Date d1(2023, 8, 10); // 构造函数
Date d2=d1; // 拷贝构造函数,并不是调用的赋值运算符重载函数
Date d3; // 构造函数
d3=d2; // 赋值运算符重载函数

拷贝构造和赋值运算符重载的区别

用一个已存在的对象去初始化另一个对象时,即使写的是 = ,此时也是调用它的拷贝构造函数

已经存在的两个对象之间赋值拷贝,此时是赋值运算符重载

int main()
{
	
	Date d1(23, 8, 10);
    // 拷贝构造
	Date d2 = d1;   
    Date d2(d1)
    // 赋值运算符重载
	d2 = d1;   

	return 0;
}

前置++和后置++重载 

// ++d1;
// d1++;
// 直接按特性重载,无法区分
// 特殊处理,使用重载区分,后置++重载增加一个int参数跟前置构成函数重载进行区分
Date& operator++(); // 前置
Date operator++(int); // 后置

Date& operator--(); // 前置
Date operator--(int); // 后置

 Date operator++(int),这里的int只起到占位的作用,不会作为参数使用

const成员函数

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

// void Date::Print(const Date* const this)
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

【C++】类和对象(中)_第3张图片

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

这两个默认成员函数大多数情况下都不需要自己写,默认生成的就够用

【C++】类和对象(中)_第4张图片

你可能感兴趣的:(C++,c++,开发语言,学习方法,程序人生,职场和发展)