C++类和对象---(中)

本文介绍了C++中的构造函数、析构函数及其作用,包括无参构造函数、全缺省构造函数、析构函数的调用顺序。探讨了拷贝构造函数的概念和作用,并分析了系统默认的拷贝构造函数行为。此外,还讲解了友元函数和友元类,以及运算符重载,特别是赋值运算符和关系运算符的重载。最后讨论了前置和后置自增运算符的重载,以及流插入和流提取运算符的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


在了解什么是构造函数之前,需要先知道C++中都有哪些数据类型。

C++中的两种类型

1.内置数据类型:如int,char,指针,数组,double等等

2.自定义类型:struct/class 定义的类型。

构造函数

在写链表的时候,经常需要对其进行初始化操作,但是免不了会忘记了对其进行初始化操作,C++为了避免这一类情况,提出了构造函数这一概念。

构造函数是一个特殊的成员函数,函数名与类名相同,实例化对象时由编译器自动调用,需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特征如下:

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

注意: 如果我们在类中没有定义构造函数,则C++编译器会生成一个默认的无参构造函数,如果我们定义了构造函数,则C++不会生成默认的无参构造函数。

编译器提供的默认无参构造函数会做些什么?
总结下来就两点:
1.对于内置数据类型,编译器提供的默认无参构造不会对其进行初始化。
这里以学生类为例,有学生姓名和学号两个成员变量,这里没有写构造函数,所以系统会提供默认无参的默认构造函数,这里通过调试可以看出学生姓名m_name和学号m_num都没有被进行初始化。
在这里插入图片描述

2.对于自定义类型,编译器提供的默认无参构造会调用该自定义类型的构造函数。
同样以学生类为例,引入成绩类的对象为学生类的成员。
为了显示编译器提供的默认无参构造会调用自定义类型的构造函数,这里对于成绩类,我们为其显示定义一个默认构造函数。
在这里插入图片描述

无参构造函数的分类

有两种情况都属于无参构造函数
1.没有参数的构造函数
如学生类下的:

在这里插入图片描述

2.全缺省的构造函数
如:
这里一定要是全缺省!
在这里插入图片描述

需要注意,即使人为写了构造函数,在创建对象的时候,如果当前这个对象的类内有自定义对象,还是会调用这个对象的构造函数:
在这里插入图片描述

析构函数

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象一般存储在栈上,待函数结束,编译器会自动对其进行销毁。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
这里析构函数一般都有哪些场景需要调用呢:
1.堆区申请的内存,如malloc,new出来的变量,这类变量的特征大都是不会被编译器自动销毁,因此可能会发生内存泄露,就需要我们进行人为的清理。
析构函数的特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象销毁时编译器自动调用对应的析构函数。

注意:和构造函数相似,如果我们没有自己定义析构函数,则编译器会提供一个默认的析构函数。

编译器提供的析构函数会做些什么?
和构造函数一样,总共两种情况:
1.对于内置数据类型:该析构函数不会对其进行任何操作
2.对于自定义类型:该析构函数会调用该自定义类型的析构函数
这里grade是student类的一个成员,在student类对象销毁时,调用了grade类的析构函数。
在这里插入图片描述
在这里插入图片描述

构造函数和析构函数的顺序

在学生类中创建了两个对象s1和s2,这里两个对象的构造和析构顺序是怎么样的呢?
在这里插入图片描述
先给出结论:
构造函数的顺序:s1先进行构造,接着构造s2。
析构函数的顺序:s2先进行析构,接着析构s1。

可以看出析构和构造的顺序相反。
这里我们通过图片进行解析:
因为对象是创建在函数栈帧上:
所以这里s1先进栈,接着才到s2进栈。所以说构造的顺序是s1先构造,接着才到s2
在这里插入图片描述
栈是一种先进后出的结构,这里s2后进栈,所以在出栈的时候s2先出栈。
所以析构函数的顺序是s2先进行析构,接着到s1。
在这里插入图片描述

拷贝构造函数

概念:拷贝构造函数也是一种构造函数,通过传入的对象,拷贝出一份与该对象一模一样的对象,这就是拷贝构造函数的作用。
举例说明:世界上第一只克隆羊多莉,多莉其实就是普通羊的一种拷贝。

拷贝构造函数的特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发>无穷递归调用。
  3. 在用已存在的类对象创建新对象时由编译器自动调用。

注意:如果我们显示定义了拷贝构造函数,编译器则不会再提供构造函数

相同的是,如果我们没有自己定义拷贝构造函数,编译器会自己提提供一个拷贝构造函数,但是其又完成什么功能呢?
看一段代码:

class person {
public:
	person(string name,int age) {
		m_name = name;
		m_age = age;

	}
	string m_name;
	int m_age;
};


int main()
{
	person p1("tom",20);
	person p2(p1);
	cout <<"名字:"<< p1.m_name << " "<<"年龄:" << p1.m_age << endl;
	cout << "名字:" << p2.m_name << " " << "年龄:" << p2.m_age << endl;
}

在这里插入图片描述
这里没有显示定义拷贝构造函数,在创建新对象p2的时候调用了系统的构造函数,可以看出,p1和p2的年龄和岁数是一样的。
这里我们给出结论:
系统的自动生成的拷贝构造函数:
1.内置类型成员,会完成字节序的拷贝。
如上图的姓名m_name,年龄m_age。
2.自定义类型成员,会调用他的拷贝构造
如下面的代码:

class student {
public:
	student() {

	}
	student(student&s1){
		cout << "student类的构造函数" << endl;
	}
};
class person {
public:
	
	student s1;

};
int main()
{
	person p1;
	person p2(p1);
}

这里的s1为student的对象,因此在初始化他的时候会调用student类的构造函数。

关于拷贝构造函数实参为什么需要用引用进行接收:
假设我们不使用引用进行接收,看看会发生什么?
在这里插入图片描述
ps:拷贝构造函数还涉及了深浅拷贝的问题,在后面的博客我们会详细进行说明

友元

概念:在C++中,一个类的私有和保护变量在类外是无法访问,但是总有些场景需要去访问这些数据,因此C++引入了友元这一概念。

友元又分为友元函数和友元类:
友元的作用:
当一个类声明另一个类或者函数为其友元时,友元函数/类就可以访问该类里的所有数据。
友元的特征:
1.友元关系是单向的:如A是B的友元,那意味着A可以访问B的所有数据,但是B并不能访问A的所有数据
在这里插入图片描述

2.友元不具有传输性:如A是B的友元,B是C的友元,但是这不意味着A是C的友元。
在这里插入图片描述
1.友元函数
格式:friend +函数返回值+ 函数名+函数参数();

class student {
	friend void display();//友元声明
public:
	student(int year,int month,int day)
	{
		m_year = year;
		m_month = month;
		m_day = day;
	}
private:
	int m_year;
	int m_month;
	int m_day;
};


void display()
{
	student s1(2020,12,10);
	cout << s1.m_year << "--" << s1.m_month << "--" << s1.m_day;
}


int main()
{
	display();
}

2.友元类
格式:friend +class+类名;

class student {
	friend class person;//声明友元类
public:
	
	student(int year,int month,int day)
	{
		m_year = year;
		m_month = month;
		m_day = day;
	}
private:
	int m_year;
	int m_month;
	int m_day;
};

class person {
public:
	void display(student&s1)
	{
		cout << s1.m_year << "--" << s1.m_month << "--" << s1.m_day << endl;
	}
};

int main()
{
	student s1(2020, 10, 8);
	person p1;
	p1.display(s1);
}

ps:在声明友元函数和友元类的时候需要将友元声明写在类主体的前面。

运算符重载

在C++中,加减乘除等一些运算符只适用与内置类型的计算,对于自定义类型其无法使用,但是在C++中有很多场景又需要用到这些运算符,因此C++引入了运算符重载的概念,通过运算符重载,使得自定义类型可以使用这些运算符。
在这里插入图片描述
注意:
1不能通过连接其他符号来创建新的操作符:比如operator@
2作为成员函数充值该是,操作符有一个默认的形参this,限定为第一个形参
3 * :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现

赋值运算符重载

其实C++对于赋值运算符重载这一块已经为我们进行了实现。如果我们没有显示定义赋值运算符的重载版本,则C++会提供一个默认的赋值运算符重载的版本。
这里我们先进行显示定义一个赋值运算符的重载版本,了解其具体的实现原理:
我们在重载运算符的时候,C++为了统一,其格式都是:
返回值+operator+运算符名字(函数参数)
如日期类赋值运算符重载:Date& operator=(Date&dd)

class Date {
public:
	Date()
	{

	}
	void operator=(Date&dd)
	{
		m_year = dd.m_year;
		m_month = dd.m_month;
		m_day = dd.m_day;
		
	}
	Date(int year,int month,int day)
	{
		m_year = year;
		m_month = month;
		m_day = day;
	}
	int m_year;
	int m_month;
	int m_day;
};


int main()
{
	Date d1(2020, 1, 20);
	Date d2;
	d2=d1;
	cout << d2.m_year << "--" << d2.m_month << "--" << d2.m_day << endl;
}

大家可能有注意到这里赋值运算符的返回值是void的,这里返回值类型是我们自己根据需求进行书写的。
这种版本的赋值运算符是不支持连续赋值的,也就是形如d1=d2=d3,这种表达式的赋值,赋值运算符的计算顺序是从右到左,先将d3赋值给d2,但是上面的版本中,赋值完以后的返回值是void类型,这时就无法继续将返回值赋值给d1。因此这里我们需要修改下这个版本的赋值运算符
也就是将对象进行返回
修改如下:

class Date {
public:
	Date()
	{

	}
	Date& operator=(Date&dd)
	{
		m_year = dd.m_year;
		m_month = dd.m_month;
		m_day = dd.m_day;
		return *this;
		
	}
	Date(int year,int month,int day)
	{
		m_year = year;
		m_month = month;
		m_day = day;
	}
	int m_year;
	int m_month;
	int m_day;
};


int main()
{
	Date d1(2020, 1, 20);
	Date d2;
	d2=d1;
	
	cout << d2.m_year << "--" << d2.m_month << "--" << d2.m_day << endl;

}

成员函数重载赋值运算符虽然我们写的是d1=d2,但是编译器在实现的时候其实会将其转换为 d2.operator=(d1)

ps:赋值运算符只能通过成员函数进行重载,因为编译器提供了默认的重载版本。

对于系统提供的赋值运算符重载,都会做些什么呢?
1.对于内置数据类型:其会完成字节序的拷贝
2.对于自定义类型:会调用其赋值运算符

关系运算符重载

关系运算符有 > ,< ,==,>= ,<=。
这里我们实现==运算符和<运算符的重载,关于剩余的几个运算符,基本原理是一样的。

==运算符重载

1.全局函数重载==运算符

class person
{
public:
	person(int a,int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;

};

bool operator==(person&p1,person&p2)//全局函数重载==运算符
{
	if ((p1.m_a == p2.m_a) && (p1.m_b == p2.m_b))
		return true;
	else
		return false;
}
int main()
{
	person p1(10, 20);
	person p2(10, 20);
	cout << (p1 == p2) << endl;
}

这里p1=p2会被转换成operator==(p1,p2);

2.成员函数实现==运算符重载

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	bool operator==(person & p2)
	{
		if ((m_a == p2.m_a) && (m_b == p2.m_b))
			return true;
		else
			return false;
	}

};

int main()
{
	person p1(10, 20);
	person p2(10, 2);
	cout << (p1==p2) << endl;
}

这里p1=p2会被转换成p1.operator==(p2);

>运算符重载

1.全局函数实现>运算符重载

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;


};
bool operator>(person&p1, person&p2)
{
	if ((p1.m_a > p2.m_b) && (p1.m_b > p2.m_b))
		return true;
	else
		return false;
}

int main()
{
	person p1(0, 20);
	person p2(10, 2);
	cout << (p1>p2) << endl;
}

这里p1>p2会被转换成operator>(p1,p2);

2.成员函数实现>运算符重载

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	bool operator>( person&p2)
	{
		if ((m_a > p2.m_b) && (m_b > p2.m_b))
			return true;
		else
			return false;
	}


};


int main()
{
	person p1(11, 3);
	person p2(10, 2);
	cout << (p1>p2) << endl;
}

这里p1>p2会被转换成p1.operator>(p2);

前置自增运算符重载

1.全局函数重载前置自增运算符

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	


};
person& operator++(person &p1)
{
	p1.m_a++;
	p1.m_b++;
	return p1;
}


int main()
{
	person p1(11, 3);
	
	++p1;
	cout << p1.m_a << " " << p1.m_b << endl;
}

++p1在调用的时候会被转换成operator++(p1);

2.成员函数重载前置自增运算符


class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	person& operator++()
	{
		m_a++;
		m_b++;
		return *this;
	}


};



int main()
{
	person p1(11, 3);
	
	++p1;
	cout << p1.m_a << " " << p1.m_b << endl;
}

++p1在调用的时候会被转换成p1.operator++();

后置自增运算符重载

因为前置++和后置++的函数名都是operator++,为了区分两个函数,C++要求后置++的参数中带一个int类的占位参数,也就是operator++(int),需要注意int需要写在参数的最后一个。

1.全局函数重载后置自增运算符

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	


};

person operator++(person&p1,int)
{
	person pp = p1;
	p1.m_a++;
	p1.m_b++;
	return pp;
}


int main()
{
	person p1(11, 3);
	
	p1++;
	cout << p1.m_a << " " << p1.m_b << endl;
}

p1++的调用会被转换成operator++(p1, 2);并且需要注意返回的对象不能是引用类型,因为这个对象是一个局部对象,出了这个函数作用域就会被回收。

2.成员函数重载后置自增运算符

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	
	person operator++(int)
	{
		person pp = *this;
		m_a++;
		m_b++;
		return  pp;
	}

};




int main()
{
	person p1(11, 3);
	
	p1++;
	cout << p1.m_a << " " << p1.m_b << endl;
}

p1++会被转换成p1.operator++(10);

流体取和流插入运算符

在了解重载原理之前,我们需要知道cout是什么?

cout是iostream类库中的一个ostream类对象
并且cout是一个全局对象,全局只有一个,因此在传参的时候必须使用引用接收,返回的时候也需要使用引用
cout是流插入运算符,以cout<<a为例,流插入就是将数据插入/打印到终端/显示器上

cout一般不通过成员函数进行重载:
因为在进行需要两个操作数的运算的时候,例如
其中p1和p2是两个对象,当进行这种双操作数的运算的时候,默认左操作数是运算符重载的第一个参数。右操作数是第二个参数。
而通过成员和函数重载cout<<p1。
这时候cout是左操作数,其也就是成员函数的第一个参数,但是以为成员函数含有this指针,所以就不能这样写。只能是p1<<cout。

person p1;
person p2;
person p3=p1+p2;

在这里插入图片描述
所以如果想要使用成员函数重载流插入运算符就需要这样写:

class person
{
public:
	void operator<< (ostream &o)
	{
		o << m_a;
	}


	int m_a = 100;

};


int main()
{
	person p1;
	p1 << cout;
}

全局函数重载流插入运算符

class person
{
public:
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	
	

};
ostream& operator<<(ostream&cout, person&p1)
{
	cout << p1.m_a << endl;
	cout << p1.m_b << endl;
	return cout;
}
int main()
{
	person p1(11, 3);
	cout << p1;
}

注意:
因为要满足cout<<对象的格式,所以cout必须做第一个参数,
并且为了满足cout<<p1<<p2这类链式编程的需求,所以需要返回cout这个对象。

流提取运算符重载

流提取体去运算符>>是ostream类库下istream类下的一个对象。
流提取运算符,以cin>>a为例,所谓流提取就是从变量中提取变量的值
同样的,cin也是一个全局对象,所以参数在接收的时候也得使用引用,并且返回时也得使用引用。

cin通过成员函数重载时还是会碰到和cout成员函数重载一样的问题,所以一般使用全局函数重载>>运算符

class person
{
public:
	person()
	{

	}
	person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	int m_a;
	int m_b;
	
	

};
istream& operator>>(istream&cin, person&p1)
{
	cin >> p1.m_a;
	cin >> p1.m_b;
	return cin;
}

int main()
{
	person p1(11, 3);
	person p2;
	cin >> p2;
	
}

cin>>p2会被转换成operator>>(cin,p2)

关于赋值运算符和拷贝构造函数的调用:

先看一段代码,这里main函数中的Date d2=d1会调用拷贝构造函数还是赋值运算符重载呢?

class Date {
public:
	Date()
	{

	}
	Date& operator=(Date&d)
	{
		cout << "赋值运算符" << endl;
		return *this;
	}
	Date(Date&d) {
		cout << "拷贝构造函数" << endl;

	}
	int m_year;

};

int main()
{
	Date d1;
	Date d2 = d1;
	
	
}

结果是调用拷贝构造函数,为什么呢?这里我们需要知道一个概念,构造函数是对象被定义的时候进行调用
这里Date d2=d1就是在d2被定义的时候进行拷贝构造。
接着再看一段代码:

这段代码中的d2=d1调用的又是哪个函数呢?

class Date {
public:
	Date()
	{

	}
	Date& operator=(Date&d)
	{
		cout << "赋值运算符" << endl;
		return *this;
	}
	Date(Date&d) {
		cout << "拷贝构造函数" << endl;

	}
	int m_year;

};

int main()
{
	Date d1;
	Date d2;
	d2 = d1;
	
	
}

这次调用的是赋值运算符重载,赋值运算符是在对象被定义了以后,再对其进行赋值的时候才会被调用。
有点类似:
int a;定义变量
a=10;定义出变量以后再对其进行赋值

const修饰类成员函数

有时候我们希望this指针指向的对象,该对象的数据不被某个函数修改,这时就可以用const修饰类成员函数,const修饰的其实是this指针,使得该成员函数无法改变成员变量的值。被const修饰的函数又叫做常函数。
注:const不能修饰析构函数和构造函数
格式:返回值+函数名+()+const
{
//函数主体
}
日期类的display函数被const修饰以后,就不能对成员变量进行修改了。
在这里插入图片描述

const修饰对象

被const的对象称作常对象。
常对象的成员变量是不可以修改的,所以常对象只能调用常函数,不能调用普通成函数。
并且不可以修改成员变量的值。

class person {
public:
	 person(int year) 
		:m_year(year)
	{}
	void doshow() const
	{
		cout << m_year << endl;
	}

	void print()
	{
		cout << m_year << endl;
	}

		int m_year;
};


int main()
{
	const person p1(10);
	p1.doshow();//常对象只能调用常函数
	p1.print();//常对象不能调用普通函数
}
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

凤梨罐头@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值