请回答C++类和对象【中】

image-20220208165228061

image-20220208113207896

1. 类的六个默认函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的

类里面成员函数我们什么都不写的时候,编译器会自动生成6个函数,这6个函数就叫默认(缺省)成员函数

class Date {};

image-20220127143319421

2. 构造函数

构造函数顶替的就是Init()函数,Init的风险是可能会没有初始化就使用该对象
C++为了解决这个问题也带来了构造函数

先不急,我们看看Java中的构造函数(构造器)

2.1 Java的构造器

构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。它有几个特点:

[修饰符] 方法名(形参列表){
方法体;
}

🍁 构造器的修饰符可以默认, 也可以是public protected private

🍁 构造器没有返回值

🍁 方法名和类名字必须一样

🍁 参数列表和成员方法一样的规则

🍁 构造器的调用, 由系统完成

2.1.2 Java构造器快速入门

🌿 方法名和类名相同

🌿 没有返回值

🌿 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化。

public Person(String pName, int pAge) {
System.out.println("构造器被调用~~ 完成对象的属性初始化");
name = pName;
age = pAge;
}
2.1.3 Java构造器使用细节

🍁 一个类可以定义多个不同的构造器,即构造器重载

比如:我们可以再给Person类定义一个构造器,用来创建对象的时候,只指定人名,不需要指定年龄

🍁 构造器名和类名要相同

🍁 构造器没有返回值

🍁 构造器是完成对象的初始化,并不是创建对象

🍁 在创建对象时,系统自动的调用该类的构造方法

🍁 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器也叫默认构造器)

🍁 一旦定义了自己的构造器,默认的构造器就覆盖了,就不能再使用默认的无参构造器,除非显式的定义一下

2.2 C++的构造函数

和Java很像

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

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

2.2.1 C++构造函数快速入门

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

🍁 函数名与类名相同。

🍁 无返回值。

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

🍁 构造函数可以__重载__。
Stack重载

class Stack{
public:
	Stack()
	{
		_a = nullptr;
		_size = _capacity = 0;
	}
	Stack(int capacity)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_size = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

这样我们就可以把Init()彻底淘汰了

一般初始化分两种初始化,给定值的初始化和不给定值的初始化,其实可以合二为一,变成缺省函数,其中全缺省函数最好用了

Date重载

class Date{
public:
	// 构造函数 -> 对象实例化的时候自动调用,这样就保证对象一定初始化
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	// 一般情况,对象初始化惯例都分两种,默认值初始化,给定值初始化
	// 合二为一,给一个全缺省的,这样定义的构造函数写一个就ok,好用
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

🍁关于编译器生成的默认成员函数,在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象依旧是随机值

此处有坑,编译器会区别对待

对于C内置类型(基本类型),也就是语言原生定义的类型,比如:int,char,double,还有指针不初始化

对于自定义类型:class,struct等的定义的类型,编译器会去调用他们的默认构造器函数初始化

class Date
{
private:
// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
// 自定义类型
	Time _t;
}; 

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

🍁 成员变量的命名风格(之前讲过带下划线)

小结:构造函数的细节很多,大多数情况下都要写构造函数完成初始化,建议写全缺省的构造函数

3. 析构函数

3.1 Intro of 析构函数

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

3.2 析构函数快速入门

析构函数的特点是:
🌿 析构函数名是在类名前加上字符 ~

🌿 无参数无返回值。(不能重载)

🌿 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

🌿 对象生命周期结束时,C++编译系统系统自动调用析构函数。
使用完成后编译器会自动调用析构函数完成资源的清理
这个函数对Date类好像没什么用,但是对于Stack类是很有用的

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}

	void Push(int x)
	{}

	// 像Stack这样的类,析构函数具有重大意义
	~Stack()
	{
		cout << "~Stack()析构函数" << endl;
		// 清理资源
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

当我们使用完这个Stack的时候,不用再去写一个StackDestroy,不用担心忘记在使用完之后调用Destroy函数,系统会在生命周期结束之后,在类里面找到析构函数,来清理

3.3 析构顺序

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	Stack st2;
	st2.Push(10);
	st2.Push(11);
	st2.Push(12);
	return 0;
}

上面的这段代码中谁先被析构呢?

image-20220127203616602

4. 拷贝构造函数

在创建对象时,可否创建一个与一个对象一某一样的新对象呢?

Java 可以new一个对象=原对象即可

C++呢?

img

可以通过拷贝构造函数

Date d1(2020, 5, 26);
Date d4(d1); 

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

4.1 拷贝构造函数快速入门

拷贝构造函数也是特殊的成员函数,其__特征__如下:
🍁 拷贝构造函数是构造函数的一个重载形式
🍁 拷贝构造函数的__参数只有一个__且必须使用引用传参,使用__传值方式会引发无穷递归调用__。

Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

为什么会发生无穷递归调用

image-20220128164427686

所以说

image-20220128164746799

🍁 传参的时候加上const,权限缩小

构造拷贝写错,下面是一段错误的代码,把要拷贝的和源写反了,会导致产生随机值,如何规避这个问题?

	Date( Date& d)
	{
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}
Date d1(2020, 5, 26);
Date d4(d1); 

方法就是加上修饰符const

Date(const Date& d)
	{
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}

🍁 若未__显示定义__,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
贝,这种拷贝叫做浅拷贝,或者值拷贝。

class Date
{
	public:
		Date(int year = 1900, int month = 1, int day = 1)
		{
			_year = year;
			_month = month;
			_day = day;
		}
	private:
		int _year;
		int _month;
		int _day;
};
int main()
{
	Date d1;
		// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
	Date d2(d1);
	return 0;
}

🍁 编译器生成的__默认拷贝构造函数__已经可以完成__字节序的值拷贝__了,我们还需要__自己实现__吗?像
日期类这样的类是没必要的。但是还有些类要自己去实现的

class Stack
{
public:
	Stack(int capacity = 4)
	{
		if (capacity <= 0)
		{
			_a = nullptr;
			_size = _capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int)*capacity);
			_capacity = capacity;
			_size = 0;
		}
	}
	~Stack()
	{
		free(_a);
		_size = _capacity = 0;
		_a = nullptr;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

比如说这个类,假如说我拷贝构造了一次,但是因为里面有析构函数,析构函数中存在free(),当我析构的时候会产生问题,由于拷贝的时候是传引用,所以两个指针指向同一个间却需要被free两次,我们之前学过malloc的空间不能多次free,所以程序若运行会产生报错

还有一个问题是,这两者是不能共用同一个空间的,是其中一个对象插入删除数据会导致另外一个对象也被修改,因此像Stack类不适合编译器默认生成的浅拷贝

image-20220128184422987

image-20220128190332727

4.3 拷贝构造什么时候会被调用

在什么情况下编译器会调用拷贝构造函数:(三种情况)

🌸 用类的一个对象去初始化另一个对象时

   Complex c1(c2);
   Complex c1=c2;

注:下面这2种时候不调用

Complex c1,c2;//赋值重载
    c1=c2; //赋值重载

🌸 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

🌸 当函数的返回值是类的对象时

5. 赋值运算符重载

5.1 运算符重载

内置类型,语言层面就支持运算符
自定义类型,默认不支持。C++可以用运算符重载来让类对象支持用某个运算符,需要那个就重载哪一个

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

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

	bool operator==(Date d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	int main()
{
	d1 == d2; // operator==(d1, d2);
	//d1 < d2;
	return 0;
}
5.1.1 Java中同样效果的思考

联系上面的栗子,联想到了Java中的重写Equals方法和Hashcode

Java中,判断两个对象在逻辑上是否相等,如根据类的成员变量来判断两个类的实例是否相等,而继承Object中的equals方法只能判断两个引用变量是否是同一个对象。这样我们往往需要重写equals()方法。

我们向一个没有重复对象的集合中添加元素时,集合中存放的往往是对象,我们需要先判断集合中是否存在已知对象,这样就必须重写equals方法。

而C++重写的运算符,因为自定义类型不像内置类型可以直接比较,所以必须借助函数来比较,把它也写成==,增加了可读性

5.1.2 运算符重载和函数重载有关系吗?

image-20220128191019981

5.2 运算符重载快速入门

🍁 不能通过连接其他符号来创建新的操作符:比如operator@

🍁 重载操作符必须有一个类类型或者枚举类型的操作数

🍁 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义

🍁 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的

🍁 操作符有一个默认的形参this,限定为第一个形参

🍁 .*::sizeof?:. 注意以上5个运算符不能重载(*可以,.*不可以)

下面来一些🌰

判断日期<的重载
bool operator<(Date x)
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year)
		{
			if (_month < x._month)
			{
				return true;
			}
			else if (_month == x._month)
			{
				if (_day < x._day)
				{
					return true;
				}
			}
		}
		return false;
	}
int main()
{
	cout << (d1 < d2) << endl;
	return 0;
}
实现一个数组类
class Array
{
public:
	Array()
	{
		for (int i = 0; i < 10; ++i)
		{
			_a[i] = i * 10;
		}
	}

	int& operator[](size_t pos)
	{
		return _a[pos];
	}
private:
	int _a[10];
};
int main()
{
	Array ay;
	cout << ay[0] << endl; // ay.operator[](&ay, 0);
	cout << ay[1] << endl;
	cout << ay[2] << endl;
	cout << ay[3] << endl;
	// 修改 -- 赋值给了返回对象的引用(别名)
	ay[0] = 100;
	ay[1] = 200;
	ay[2] = 300;
	ay[3] = 400;
	cout << ay[0] << endl; // ay.operator[](&ay, 0);
	cout << ay[1] << endl;
	cout << ay[2] << endl;
	cout << ay[3] << endl;
	return 0;
}

5.3 赋值运算符重载

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
	}
5.3.1 赋值运算符快速入门

赋值运算符有如下关键点

🍁 参数类型

🍁 返回值

🍁 检测是否自己给自己赋值

🍁 返回*this

🍁 一个类如果__没有显式定义__赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2018101);
	// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
	d1 = d2;
		return 0;
}
5.3.2 赋值运算符重载和拷贝构造函数一样吗

赋值拷贝

这里传参的时候传引用的原因是,如果直接传值调用的时候要调拷贝构造,不是一定要传引用,但是建议加上

	void operator=(const Date& d) // void operator=(Date* this, const Date& d)
	{  
			_year = d._year;
			_month = d._month;
			_day = d._day;
	}

拷贝构造

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

🌿 不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝,赋值拷贝时两个对象已经都存在了,都被初始化过了,现在想把一个对象,赋值拷贝给另一个对象

🌿 一样的是,编译器默认生成赋值运算符跟拷贝构造的特性是一致的
🍒针对内置类型,会完浅拷贝,也就是说像Date这样的类不需要我们自己写赋值运算符重载,Stack就得自己写

​ 🍒 针对自定义类型,也一样,它会调用他的赋值运算符重载完成拷贝

但是这里的=赋值还不够好,类比内置类型,都有连续赋值的形式

对于i=j=k,应该先是k赋给j,然后这个表达式返回值是j,把j再赋给i的形式,于是我们试着改写成连续赋值的形式

  	Date& operator=(const Date& d) // void operator=(Date* this, const Date& d)
	{
		if (this != &d) // 检查如果不是自己给自己赋值,才需要拷贝
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

返回值如果是对象的话会调用拷贝构造,为防止调用,用引用返回的方式,不过,要用引用作为返回值的话,除了作用域之后要保证还在才可以使用,这里还在,所以可以用

为了防止自己给自己赋值,加一个判断

6. 对前面知识点的总结:

C++的类中有6个默认成员函数,其中四个
🍁 构造函数 – 初始化,大部分情况下,都需要我们自己写构造函数

🍁 析构函数 – 清理内对象中资源

🍁 拷贝构造函数 – 拷贝初始化,特殊构造函数 深浅拷贝问题

🍁 赋值运算符重载 – 也是拷贝行为,但是不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝,赋值拷贝时两个对象已经都存在了,都被初始化过了,现在想把一个对象,赋值拷贝给另一个对象

针对我们不写编译默认生成的总结一下:

我们不写编译器会自动生成,虽然会自动生成,但是好多时候还是需要我们自己写,因为生成的那个不一定好用

🍁 构造和析构的特性是类似的,我们不写编译器内置类型不处理,自定义类型调用它的构造和析构处理

🍁 拷贝构造和赋值重载特性是类似的,内置类型会完成浅拷贝,自定义类型会调用他们的拷贝构造和赋值重载

7. const 成员

7.1 const修饰类成员函数

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

image-20220130171501905

7.2 const修饰成员函数快速入门

下面这种情况很容易出错

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

一不小心把==写成了=,导致成员变量被修改,其实我们发现好像我已经写了const,但是这个const保护的是传入的对象的值,不能被修改,而不是this*指向的对象,保护对象

于是我们简易就利用好const修饰成员函数

	bool operator==(const Date& d) const // bool operator==(const Date* this, const Date& d)
	{
		return (_year == d._year)
			&& (_month == d._month)
			&& (_day == d._day);
	}

这种就没有必要加,虽然可以运行,加也不是随便加,重点还有一个是注意⚠️ 不能权限放大

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

7.3 回顾const修饰指针

语句\修饰项p1*p1
const Date *p1FT
Date const *p1FT
Date *const p1TF

7.4 const修饰对象注意事项

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

不可以,权限放大

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

可以,权限缩小

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

不可以,成员函数中,编译器处理以后在成员(成员变量/成员函数)前面都会加this->,所以普通成员函数可以调用其他的普通的成员函数,但是权限放大的话不可以

image-20220130174110190

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

可以,权限缩小

image-20220130174027407

小结:

加const,修饰的是*this,好处:函数中不小心改变的成员变量,编译时就会被检查出来
建议:成员函数中,不需要改变成员变量,建议都加上const

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

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 他们基本没有自己实现的价值
	// 除非你不想让别人获取Date类对象的地址,才有必要自己实现
	Date* operator&()
	{
	  return this;
        //return nullptr;
	}
	const Date* operator&() const
	{
		return this;
        //return nullptr;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

这两个运算符一般不需要重载,该功能不是特别有价值,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!怎么弄呢,就是返回空指针

感谢阅读,干净又卫生,兄弟们

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值