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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

一.类的6个默认成员函数

二.构造函数

 2.1构造函数的概念

2.2构造函数的特性

三.析构函数

3.1析构函数的概念

3.2析构函数的特性

四.拷贝构造函数

4.1拷贝构造函数的概念

4.2拷贝构造函数的特性

五.赋值运算符重载

5.1运算符重载

5.2赋值运算符重载

5.3前置++和后置++重载

6.const成员

6.1 const成员函数

6.2const对象


一.类的6个默认成员函数

        在上篇文章中我们谈到了空类即类里面什么都没有我们把类中没有任何成员的类称之为空类        

        空类真的空吗空类真的里面什么都没有

        答案是否定的即使类里面什么都不写编译器也会默认生成6个成员函数由编译器自动生成的成员函数称之为默认成员函数

        6个默认成员函数初始化、清理拷贝、赋值取地址重载

        示例图如下

二.构造函数

 2.1构造函数的概念

         我们在之前学习数据结构时实现数据结构的时候都会有一个初始化函数往往在定义后需要手动调用初始化函数那么我们可以定义的时候同时初始化呢

        C++中引入了构造函数构造函数是一个特殊的成员函数名字与类名相同创建对象时自动调用以对成员变量进行初始化并且定义之后只会调用一次

2.2构造函数的特性

        构造函数名字虽然名字是构造其功能并不是创建一个对象而是初始化对象

构造函数的函数名与类名相同并且无返回值并不是写void而是没有返回值这个选项

构造函数可以重载即可以按需实现不同的初始化

构造函数是由编译器自动调用的

        构造函数的重载

#include<iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		cout << "无参构造" << endl;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "带参构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//调用无参构造
	Date d2(2023, 5, 19);//调用有参构造
	return 0;
}

运行结果

可以看到当我们定义一个对象不传参数时编译器会调用无参构造当定义对象时传参就会调用带参构造

        但是既然涉及到函数重载那么不得不提到的二义性就是带有默认参数的函数和无参函数的冲突的问题

        在上面的基础上我们加一个全默认参数的构造函数

#include<iostream>
using namespace std;
class Date
{
public:
	//无参构造
	Date()
	{
		cout << "无参构造" << endl;
	}
	//带参构造
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "带参构造" << endl;
	}
	//全默认参数构造
	Date(int year = 2023, int month = 5, int day = 19)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "全默认参数构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//到底是调用无参构造还是全默认参数构造呢
	Date d2(2023, 5, 19);//调用有参构造
	return 0;
}

编译报错信息

报错信息显示对重载函数的调用不明确即调用构造的时候出现二义性因此我们以后自己定义构造函数应该避免这种二义性即无参构造函数和全默认参数构造函数不能同时出现

       默认构造函数

        当我们没有定义构造函数时编译器会自动生成一个无参的默认的构造函数如果我们已经定义了构造函数则编译器将不再生成

        编译器自动生成的、无参的构造函数、全默认参数的构造函数这三者都称之为默认构造函数显然这三者在一个类中只能存在一种一般建议类中包含一个默认构造函数

        编译器生成的默认构造函数有什么用呢我们来看下面的代码

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _sce;
};
class Date
{
public:
	//自己没有定义构造函数因此编译器会自动生成一个
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	
private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time ti;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;

我们声明一个时间Time类并定义了它的构造函数然后声明了一个日期Date类其成员变量有它自己的日期即内置类型也有时间类即自定义类型我们来看编译器默认生成的构造函数会做什么处理

        运行结果

 可以看到对于内置类型编译器自动生成的构造函数并没有处理而对于自定义类型会去调用它的默认构造函数

注意调用默认构造不需要在定义对象时在对象后面加一个空括号因为这样编译器会把它当成函数的声明而不是对象的定义

三.析构函数

3.1析构函数的概念

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

3.2析构函数的特性

        析构函数是特殊的成员函数其有如下的特点

1.析构函数的函数名为类名前加~字符~字符我们知道是按位取反的功能此处就是顾名思义与构造函数功能相反

2.和构造函数一样没有返回值但析构函数没有参数

3.析构函数不能重载即一个类只能有一个析构函数如果自己没有定义析构函数则编译器自动生成如果已经定义则编译器不再生成

4.对象声明周期结束时会自动调用析构函数

5.编译器自动生成的析构函数的作用我们来看以下代码

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time的构造函数" << endl;
	}
	~Time()
	{
		cout << "Time的析构函数" << endl;
	}
private:
	int _hour;
	int _minute;
	int _sce;
};
class Date
{
public:
	//自己没有定义构造函数因此编译器会自动生成一个
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	
	
private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time ti;
};
int main()
{
	Date d1;
	return 0;
}

我们声明一个时间Time类并定义了它的构造函数然后声明了一个日期Date类其成员变量有它自己的日期即内置类型也有时间类即自定义类型我们来看编译器默认生成的析构函数会做什么处理

        运行结果

可以看到类似于构造函数析构函数对内置类型不做处理对自定义类型会调用它的析构函数 

6.如果类中没有申请资源时可以不写析构函数即使用编译器自动生成的即可对于有自愿申请如在堆上开辟空间等则需要自己学析构函数不然会造成内存泄漏 

7.析构函数调佣的顺序为先构造的后析构后构造的先析构

代码

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time的构造函数" << endl;
	}
	~Time()
	{
		cout << "Time的析构函数" << endl;
	}
private:
	int _hour;
	int _minute;
	int _sce;
};
class Date
{
public:
	Date()
	{
		cout << "Date的构造函数" << endl;
	}
	~Date()
	{
		cout << "Date的析构函数" << endl;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	
	
private:
	int _year;
	int _month;
	int _day;
	
	
};
int main()
{
	Time t1;
	Date d1;
	return 0;
}

先定义了一个时间对象后定义了一个日期对象运行结果

可以看到构造顺序为先构造Time,后构造Date而析构的顺序为先析构Date,后析构Time即先构造的后析构后构造的先析构。 这是由于局部对象存储在栈区栈区有类似于数据结构的栈的特性即先进后出后进先出

四.拷贝构造函数

4.1拷贝构造函数的概念

        在日常生活中我们有时候会见到双胞胎即两个人长得几乎一模一样那在创建对象时能否用一个已经存在的对象去创建一个新对象使得两个对象一模一样呢

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

4.2拷贝构造函数的特性

        拷贝函数是特殊的成员函数其有如下的特点

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

2.拷贝构造函数的参数只有一个且必须是对象的引用如果使用传值方式会引发无情递归而报错

       这是因为传参时形参是实参的拷贝由于是对象的拷贝因此传值过程中会调用拷贝构造函数然后再次进行传值而这次传值又是一次拷贝即调用拷贝构造函数因此会无穷地递归下去

3.类似于构造函数和析构函数如果用户没有定义则编译器会生成默认的拷贝构造函数。默认拷贝构造函数是按照字节进行拷贝的类似于C语言中memcpy函数这种拷贝称为浅拷贝

       默认拷贝构造函数对于内置类型按照字节进浅拷贝对于自定义类型会调用它的拷贝构造函数

代码

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time的构造函数" << endl;
	}
	Time(const Time& t)
	{
		cout << "Time的拷贝构造函数" << endl;
	}
private:
	int _hour;
	int _minute;
	int _sce;
};
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;
	Time t;
	
};
int main()
{
	Date d1(2020,5,23);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

我们先定义了日期对象d1然后用d1拷贝构造d2,运行结果

 可以看到对于内置类型默认拷贝构造按照字节拷贝成功对于自定义类型编译器去调用其拷贝构造函数

        对于内置类型按照字节拷贝已经能够顺利拷贝那么我们还需要自己定义拷贝构造函数吗

分析一下代码

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
			_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		
		_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;
}

运行结果

程序崩溃了以上代码由于对象在堆上申请了空间在拷贝时按照字节拷贝即两个对象的array指针的值是一样的即两个指针指向用一块空间

        带来的问题

插入数据会相互影响

对同一块空间释放两次造成程序崩溃

        因此如果类中没有申请资源时拷贝构造函数可写可不写对于资源申请的类则必须需要自己写拷贝构造函数否则默认拷贝构造函数即浅拷贝会带来问题

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

使用已存在对象创建新对象

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& t)
	{
		_year = t._year;
		_month = t._month;
		_day = t._day;
		cout << "Date的拷贝构造函数" << endl;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2020,5,23);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

运行结果

 函数参数为对象

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& t)
	{
		_year = t._year;
		_month = t._month;
		_day = t._day;
		cout << "Date的拷贝构造函数" << endl;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void fun(Date t)
{

}
int main()
{
	Date d1(2023,5,23);
	fun(d1);
	return 0;
}

运行结果

 函数返回值为对象

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& t)
	{
		_year = t._year;
		_month = t._month;
		_day = t._day;
		cout << "Date的拷贝构造函数" << endl;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date fun()
{
	Date d1(2023,5,23);
	return d1;
}
int main()
{
	fun();
	return 0;
}

运行结果

        为了提高程序的效率减少拷贝过程对象传参尽量使用引用函数返回值能够用引用返回就用引用返回

五.赋值运算符重载

5.1运算符重载

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

语法返回值类型 operator运算符参数列表

        对于内置类型比如整形等加减乘除等运算符可以直接对它们进行计算但是可以实现两个对象用+运算符进行相加吗显然是不能的这时候就需要利用运算符重载

 运算符重载的特点

1.重载运算符必须有一个对象类型参数

2.用于内置类型的运算符不能改变其含义

3.运算符重载可以重载为全局函数和对象的成员函数

重载为全局函数 以重载运算符-为例

#include<iostream>
using namespace std;
class Student
{
public:
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	void Print()
	{
		cout << "成绩" <<_score<<endl;
	}
private:
	int _score;
};
int operator-(const Student& s1, const Student& s2)
{
	return s1._score - s2._score;
}
int main()
{
	Student st1(100);
	Student st2(60);
	cout << st1 - st2 << endl;
	return 0;
}

可以看到重载为全局函数会出现无法访问的情况因为成员变量是私有的在类外不能直接访问

解决办法

将运算符重载函数声明为类的友元函数

将成员变量设置为共有

通过类中的成员函数访问私有的成员变量

         前面两种方法破坏了类的封装性故推荐第三种即通过成员函数访问私有的成员变量

重载为类的成员函数同样以运算符-为例 

#include<iostream>
using namespace std;
class Student
{
public:
	int operator-( const Student& s2)
	{
		return _score - s2._score;
	}
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	void Print()
	{
		cout << "成绩" <<_score<<endl;
	}
private:
	int _score;
};

int main()
{
	Student st1(100);
	Student st2(60);
	cout << st1 - st2 << endl;//调用opeartor-函数
	return 0;
}

         通过与全局函数对比可以发现重载为成员函数少了一个参数因为类的非静态成员函数有隐藏的this指针

4    .* :: sizeof  ?:   .    注意以上5个运算符不能重载。这个经常出现在笔试题中

5.2赋值运算符重载

1.赋值运算符重载格式

返回值类型T &返回引用提高效率

函数参数const T &,传递引用可以提高效率

返回*this以满足赋值的连续性

检测是否自己为自己赋值

2. 赋值运算符只能重载为成员函数
include<iostream>
using namespace std;
class Student
{
public:
	int operator-( const Student& s2)
	{
		return _score - s2._score;
	}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			_score = s._score;
		}
		return *this;
	}
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	void Print()
	{
		cout << "成绩" <<_score<<endl;
	}
private:
	int _score;
};

int main()
{
	Student st1(100);
	Student st2(60);
	cout << st1 - st2 << endl;
	st1 = st2;//调用operator=函数
	return 0;
}

原因是如果类中没有赋值运算符重载函数编译器会默认生成一个与重载的全局函数发生冲突

 3.当用户没有在类中定义运算符重载函数时编译器会默认生成一个类似于拷贝函数以字节拷贝的方式进行拷贝对于内置类型会进行浅拷贝对于自定义类型会去调用它的赋值运算符重载函数这种浅拷贝对于没有进行资源申请的内置类型是可行的但是对于数据结构的栈这种进行资源申请的类则会出现问题因此如果类中未涉及到资源管理赋值运算符是否实现都可以一旦涉及到资源管理则必须要实现

5.3前置++和后置++重载

        1.前置++的格式引用返回  opeartor++

Student& operator++()
	{
		_score++;
		return *this;
	}

        2.后置++的格式引用返回  opeartor++int,C++规定后置++重载时多增加一个int类型的参数但调用函数时该参数不用传递编译器自动传递

函数

Student& operator++(int)
	{
		Student temp(_score);
		_score++;
		return temp;
	}

调用处

st1++;

编译器优化

operator++(0)

前置++和后置++的区别就在于函数参数后者有一个int的占位参数调用的时候有编译器自动识别并调用前置--和后置--同理 

6.const成员

       6.1 const成员函数

        const成员函数const修饰的成员函数称之为const成员函数const放在函数参数列表后面格式如下返回值类型 函数名参数列表const

        const修饰成员函数实际上是修饰隐藏的this指针使得this指针变为常量指针即不可以改变成员变量

6.2const对象

        const对象在定义对象时用const修饰的对象称之为const对象类似于const修饰的变量其权限为只读不能修改cosnt对象里面的成员变量全都为只读变量

        1.const对象调用非const函数

#include<iostream>
using namespace std;
class Student
{
public:
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	void Print()
	{
		cout << "成绩" << _score << endl;
	}
private:
	int _score;
};

int main()
{
	const Student st1(100);//定义const对象
	st1.Print();//const对象调用非const函数
	return 0;
}

 const对象的成员变量为只读而调用的函数的权限为可读可写属于权限放大故报错

报错信息

 2.const对象调用const函数

#include<iostream>
using namespace std;
class Student
{
public:
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	void Print()const
	{
		cout << "成绩" << _score << endl;
	}
private:
	int _score;
};

int main()
{
	const Student st1(100);//定义const对象
	st1.Print();//const对象调用const函数
	return 0;
}

const对象的成员变量为只读cosnt函数的权限也为只读属于权限平移故编译通过

运行结果

3.const函数调用其他的非const函数

#include<iostream>
using namespace std;
class Student
{
public:
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	int GetScore()//非const函数
	{
		return _score;
	}
	void Print()const
	{
		cout << GetScore()<< endl;//const函数中调用非const函数
	}
private:
	int _score;
};

int main()
{
	const Student st1(100);//定义const对象
	st1.Print();//const函数里面调用非const函数
	return 0;
}

const函数内的权限为只读而在函数中调用非const函数非const函数的权限为可读可写属于权限放大故报错

报错信息

’4.非const函数调用const函数

#include<iostream>
using namespace std;
class Student
{
public:
	Student()
	{
		
	}
	Student(int score)
	{
		_score = score;
	}
	Student(const Student & t)
	{
		_score = t._score;
	}
	int GetScore()const//const函数
	{
		return _score;
	}
	void Print()
	{
		cout << GetScore()<< endl;//非const函数中调用const函数
	}
private:
	int _score;
};

int main()
{
	Student st1(100);
	st1.Print();//const函数里面调用非const函数
	return 0;
}

 非const函数的权限为可读可写调用const函数const函数的权限为只读属于权限缩小故编译通过

运行结果

总结const修饰的对象和函数以及它们与其他非const修饰的函数的互相调用关系只需判断它们各自的权限然后判断是否是权限放大是权限放大则会报错反之如果是权限平移或者缩小则编译通过

好啦关于类和对象二就先学到这如果对您有所帮助欢迎一键三连您的支持是我创作的最大动力

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“【C++】类和对象(二)” 的相关文章