C++从0到1的入门级教学(十)——类和对象

举报
ArimaMisaki 发表于 2022/08/09 01:08:34 2022/08/09
【摘要】 文章目录 10 类和对象10.1 封装10.1.1 封装的意义10.1.2 struct和class的区别10.1.3 成员属性设置为私有 10.2 对象的初始化和清理10.2.1 构造函数...

10 类和对象

C++面向对象的三大特性:封装、继承、多态

C++认为万事万物皆为对象,对象上有其属性和行为

例如

人可以作为对象,属性有姓名、年龄、身高、体重。。。行为有走、跑、跳、吃饭、唱歌。。。

具有相同性质的对象,我们可以抽象为类,人属于人类,车属于车类。

10.1 封装

10.1.1 封装的意义

封装:封装是C++面向对象三大特性之一,其将属性和行为作为一个整体,表现生活中的事物;并可以将属性和行为加以权限控制

封装意义一:在设计类的时候,属性和行为写在一起,表现事物

语法class 类名{访问权限:属性/行为};

示例1:设计一个圆类

#include <iostream>
using namespace std;

//圆周率
const double PI = 3.14;

//设计一个圆类,求圆的周长
//圆求周长的公式:2*PI*半径

//class 代表设计一个类,类后面紧跟着的就是类的名称
class Circle
{
public:
	//属性:半径
	int m_r;

	//行为:获取圆的周长
	double calculateZC()
	{
		return 2 * PI * m_r;
	}
};

int main()
{
	//通过这个圆类创建具体的圆(对象)
	Circle c1;
	//给圆对象的属性进行赋值
	c1.m_r = 10;

	cout << "圆的周长为:" << c1.calculateZC() << endl;
	system("pause");
	return 0;
}

结果:圆的周长为:62.8

示例二:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

#include <iostream>
#include <string>
using namespace std;

//设计学生类
class Student
{
public://公共权限
	//属性
	string m_Name;//姓名
	int m_Id;//学号

	//行为
	void showStudent()
	{
		cout << "姓名:" << m_Name << "学号:" << m_Id << endl;
	}
};

int main()
{
	//实例化
	Student s1;
	s1.m_Name = "张三";
	s1.m_Id = 1;
	s1.showStudent();

	system("pause");
	return 0;
}

结果:姓名:张三学号:1

对于上面的代码,可以改进一下:

#include <iostream>
#include <string>
using namespace std;

//设计学生类
class Student
{
public://公共权限
	//属性
	string m_Name;//姓名
	int m_Id;//学号

	//行为
	void showStudent()
	{
		cout << "姓名:" << m_Name << "学号:" << m_Id << endl;
	}

	void setName(string name)
	{
		m_Name = name;
	}

	void setId(int id)
	{
		m_Id = id;
	}
};

int main()
{
	//实例化
	Student s1;
	s1.setName("张三");
	s1.setId(1);
	s1.showStudent();

	system("pause");
	return 0;
}

封装意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

英文名 中文名 说明
public 公共 成员类内可以访问、类外也可访问
protected 保护 成员类内可以访问、类外不可访问,但是在继承时,protected的内容可以被子类访问
private 私有权限 成员类内可以访问、类外不可访问,但是在继承时,private的内容不可以被子类访问

10.1.2 struct和class的区别

在C++中和struct和class唯一的区别就在于默认的访问权限不同;对于struct来说,其默认权限为公共;而对于class来说,其默认权限为私有


10.1.3 成员属性设置为私有

这块知识就和java中的封装对应上了,在java中,我们时常用private对类中的成员属性进行私有化,然后用set去写入,用get去读。

在C++中,我们同样的将所有成员属性设置为私有,这样的好处是可以自己控制读写权限,并且在写入数据的过程中,我们还可以用函数来控制其写入数据的有效性。

如果想体验这个知识点,可以试着做一下下面的案例:

#include <iostream>
#include <string>
using namespace std;

//设计人类
class Person 
{
private:
	//姓名
	string m_Name;
	//年龄
	int m_Age;

public:
	void setName(string name)
	{
		m_Name = name;
	}

	void setAge(int age)
	{
		m_Age = 0;
		if(age>0 && age < 200)
			m_Age = age;
	}

	string getName()
	{
		return m_Name;
	}

	int getAge()
	{
		return m_Age;
	}
};

int main()
{
	Person p1;
	p1.setName("张三");
	p1.setAge(-1);
	cout << p1.getName() << endl;
	cout << p1.getAge() << endl;
}

结果:张三
0


10.2 对象的初始化和清理

生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全,而对于C++来说,C++的面向对象编程也是来源于生活,所以每个对象都会有初始设置以及对象销毁前的清理数据的设置。

10.2.1 构造函数和析构函数

C++利用了构造函数和析构函数解决上面说的初始化和销毁化问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事。因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数。并且两个都是是空实现。

和java实际上很类似,java中也存在构造函数,但是没有析构函数,java如果没有构造方法,那么其自带一个无参构造;如果写了构造方法,那么系统原有自带的无参构造会消失,这也意味着我们写完有参构造后还要再写一个无参构造。

java没有析构函数的原因是,其运行在JVM上,当对象消失,那么JVM会启用垃圾回收机制,回收分配给对象的资源;而C++不会自动回收,因此当对象消失时,需要提供析构函数来回收资源。

构造函数语法为类名(){},需要注意的是,C++中的构造函数没有返回值也不用写void,构造名称和类名相同。构造函数可以有参数,所以可以发生重载,即可以同时拥有空参构造方法有参构造方法。程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调用一次。

对于析构函数来说,其语法为~类名(){},需要注意的是,析构函数同构造函数的特点基本一样,有一点不一样的是析构函数不可以有参数,因此不能写多个析构函数。

在visual studio中,如果你用了系统提示的class创建,那么他会帮你把构造和析构全部写好,如果要写多余的构造自己补充即可。

10.2.2 构造函数的分类及调用

两种分类方式:

按参数分 按类型分
有参构造 普通构造
无参构造(默认构造) 拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法

拷贝构造

除了拷贝构造之外,其他的都是普通构造。在学习其他编程语言的过程中,我们并没有听过拷贝构造,其是C++中特有的一种构造方法,通过类名 (const 类名 &对象名){拷贝内容}即可定义拷贝构造,拷贝构造常用语将某一个对象的属性拷贝给另外一个对象。

括号法

括号法很简单,但是学过java的同学可能会混,什么意思呢。

如果是调用无参构造,那么只需类名 对象名,系统会自动调用,而在java中,我们通常是类名 对象名 = new 构造器(),也就是说即使是无参我们也会把括号写上;而在C++中调用无参构造时不能写括号,因为写了括号,C++会误以为你是写了一个函数的声明。

如果是有参构造,只需要类名 对象名(参数)即可。

如果是拷贝构造,只需要类名 对象名(被拷贝的对象)

显式法

如果不用括号法,可以用显式法来调用构造函数,从形式上看,显式法更像java的构造调用。

对于无参构造,显式法和括号法一样。

对于有参构造,显式法格式为:类名 对象名 = 类名(参数)

对于拷贝构造,显式法格式为:类名 对象名 = 类名(被拷贝的对象)

需要注意的是,类名(参数)类似于java学习中的匿名内部类,在C++中被我们称为匿名对象,其特点是当前行执行结束后,系统会立即回收掉匿名对象。

还有一点是:不要利用拷贝函数来初始化一个匿名对象,如:类名(对象名),如果你尝试这么做,编译器会认为其等价于类名 对象名,即对象的声明。

隐式转换法

隐式转换法实际上是显式法的简略版,但是你说简略实际上它还没括号法简略呢,所以不想了解也没啥问题,虽然别人写的代码你可能看不懂哈哈。

对于有参构造:隐式转换法格式为:类名 对象名 = 参数。对于拷贝构造也是同样的原理。

10.2.3 关于拷贝构造函数调用时机

C++中需要拷贝构造函数的情况通常有三种:

  • 使用一个已经吃那完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值的方式返回局部对象

示例:

#include<iostream>
using namespace std;

class Person
{
public:
	int m_Age;

	Person()
	{
		cout << "Person默认构造函数调用" << endl;
	}

	Person(int age)
	{
		cout << "Person有参构造函数调用" << endl;
		m_Age = age;
	}

	Person(const Person& p)
	{
		cout << "Person拷贝构造函数调用" << endl;
		m_Age = p.m_Age;
	}

	~Person() 
	{
		cout << "Person默认析构函数调用" << endl;
	}

};

//构造拷贝函数调用时机
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test01() 
{
	Person p1(20);
	Person p2(p1);

	cout << "p2的年龄为:" << p2.m_Age << endl;
}

//2、值传递的方式给函数参数传值
void doWork(Person p) 
{
	
}

void test02()
{
	Person p;
	doWork(p);
}

//3、值方式返回局部对象
Person doWork2()
{
	Person p1;
	return p1;
}

void test03()
{
	Person p = doWork2();
}


int main()
{
	//test01();
	//test02();
	test03();
}

10.2.4 构造函数调用规则

默认情况下,C++编译器至少给一个类添加三个函数:

  • 默认构造函数,无参,函数体为空
  • 默认析构函数,无参,函数体为空
  • 默认拷贝构造函数,对所有属性值进行拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++将不在提供无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++将不会提供其他构造函数

乍一看,上面的规则似乎和Java中的很类似。

如果想要了解上述的知识点,我们可以手动做一下下列的案例:

#include<iostream>
using namespace std;

//构造函数的调用规则
/*- 默认构造函数,无参,函数体为空
- 默认析构函数,无参,函数体为空
- 默认拷贝构造函数,对所有属性值进行拷贝*/

class Person 
{
public:
	int m_Age;
	
	Person() 
	{
		cout << "Person的默认构造函数调用" << endl;
	}

	Person(int age) 
	{
		cout << "Person的有参构造函数调用" << endl;
	}

	/*person(const person &p) 
	{
		cout << "person的拷贝构造函数调用" << endl;
		m_age = p.m_age;
	}*/

	~Person() 
	{
		cout << "Person的默认析构函数调用" << endl;
	}
};

void test01()
{
	Person p;
	p.m_Age = 18;

	Person p2(p);

	cout << "p2的年龄为:" << p2.m_Age << endl;
}

int main() 
{
	test01();
}

10.2.5 深拷贝和浅拷贝

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新生成空间,然后再进行拷贝

深浅拷贝是面试最常见的问题,需要严加重视。

我们现在来思考一个问题,如果我们想要利用拷贝构造函数在堆区生成一块空间怎么办?一般思路应该如下:

#include <iostream>
using namespace std;

class Person
{
public:
	int m_Age;
	int* m_Height;

	Person() 
	{
		cout << "Person的默认构造函数被调用" << endl;
	}

	Person(int age, int height) 
	{
		cout << "Person的有参构造函数调用" << endl;
		m_Age = age;
		m_Height = new int(height);
	}

	~Person()
	{
		if (m_Height != NULL)
		{
			delete(m_Height);
			m_Height = NULL;
		}
		cout << "Person的析构函数调用";
	}

private:

};

void Test01() 
{
	Person p1(18,160);
	cout << "p1的年龄为:" << p1.m_Age << endl;

	Person p2(p1);
	cout << "p2的年龄为:" << p2.m_Age << endl;
}

int main() 
{
	Test01();
}

经过上面的代码敲试,你会发现这段代码是会报错的。因为这里出现了一个问题——重复释放内存。

我们知道,上述代码的p2是由p1拷贝而来,在执行析构函数时,p2的代码写于p1之后,而析构函数位于栈空间,那么根据栈后入先出的特点,则p2应该首先执行析构函数。

在你没有编写拷贝构造函数时,系统会默认给出一个拷贝构造函数,这个拷贝构造函数执行的是一个浅拷贝的过程,相当于我们平时说的值传递,它的作用是赋值p1对象里所有的值给p2。

当我们执行上面的代码时,p1的析构也给了p2,p1中的m_Height作为在堆区开辟的数据,p2从p1拷贝了m_Height的指针,也就是说,P2.m_Height和P1.m_Height共享这个指针。当p2先执行析构,该指针就被释放掉;而轮到p1执行析构时,没有指针可释放,所以就报错了。编译器写的浅拷贝构造函数如下:

Person(const Person &p)
{
	cout<<"Person 拷贝构造函数调用"<<endl;
	m_Age = p.m_Age;
	m_Height = p.m_Height;
}

既然弄明白了上述的问题,我们要做的,实际上就是要再开辟一块堆区空间,使得p2和p1的m_Height空间不一样。如下所示:

#include <iostream>
using namespace std;

class Person
{
public:
	int m_Age;
	int* m_Height;

	Person() 
	{
		cout << "Person的默认构造函数被调用" << endl;
	}

	Person(int age, int height) 
	{
		cout << "Person的有参构造函数调用" << endl;
		m_Age = age;
		m_Height = new int(height);
	}

	~Person()
	{
		if (m_Height != NULL)
		{
			delete(m_Height);
			m_Height = NULL;
		}
		cout << "Person的析构函数调用";
	}

	Person(const Person& p) 
	{
		cout << "Person 拷贝构造函数调用" << endl;
		m_Age = p.m_Age;
		//深拷贝
		m_Height = new int(*p.m_Height);
	}

private:

};

void Test01() 
{
	Person p1(18,160);
	cout << "p1的年龄为:" << p1.m_Age << endl;
	cout << "p1的身高为:" << p1.m_Height << endl;

	Person p2(p1);
	cout << "p2的年龄为:" << p2.m_Age << endl;
	cout << "p2的身高为:" << p2.m_Height << endl;
}

int main() 
{
	Test01();
}

10.2.6 初始化列表

我们知道构造函数实际上就是用于初始化对象,C++提供了初始化列表语法,用于初始化对象中的属性。其语法如下所示:

构造函数():属性1(值1),属性2(值2)…

#include <iostream>
using namespace std;

//初始化列表
class Person
{
public:
	int m_A;
	int m_B;
	int m_C;

	//传统初始化方法
	/*Person(int a, int b, int c)
	{
		m_A = a;
		m_B = b;
		m_C = c;
	};*/

	//初始化列表
	/*Person() :m_A(10), m_B(20), m_C(30) 
	{
		
	}*/

	//初始化列表
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) 
	{
	}
};

void test01() 
{
	//Person p(10, 20, 30);
	Person p(10,20,30);
	cout << "m_A=" << p.m_A << endl;
	cout << "m_B=" << p.m_B << endl;
	cout << "m_C=" << p.m_C << endl;
}

int main()
{
	test01();
}

10.2.7 对象成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。

如:

class A{}
class B
{
	A a
}

由此引发一个问题,当创建B对象时,A和B的构造和析构的顺序谁先谁后。

#include <iostream>
using namespace std;
#include <string>

class Phone
{
public:
	//手机品牌名称
	string m_PName;

	Phone(string pName) 
	{
		m_PName = pName;
	};

private:

};


class Person
{
public:
	//姓名
	string m_Name;
	//手机
	Phone m_Phone;

	Person(string name, string pName) :m_Name(name), m_Phone(pName) 
	{
	
	}

private:

};

void test01()
{
	Person p("张三", "苹果X");
	cout << p.m_Name << "拿着" << p.m_Phone.m_PName << endl;
}

int main() 
{
	test01();
} 

执行以上的代码我们可以看出,实际上对象成员是先构造的,而后类对象再构造;而对于析构来说,类对象先析构,而后是对象成员。这就好比搭积木,你要搭个人出来肯定要先搭好胳膊和腿;同理,如果你要拆积木,也肯定是先拆胳膊和腿。

文章来源: blog.csdn.net,作者:ArimaMisaki,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/chengyuhaomei520/article/details/123877700

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。