文章目录
- 10 类和对象
- 10.1 封装
- 10.1.1 封装的意义
- 10.1.2 struct和class的区别
- 10.1.3 成员属性设置为私有
- 10.2 对象的初始化和清理
- 10.2.1 构造函数和析构函数
- 10.2.2 构造函数的分类及调用
- 10.2.3 关于拷贝构造函数调用时机
- 10.2.4 构造函数调用规则
- 10.2.5 深拷贝和浅拷贝
- 10.2.6 初始化列表
- 10.2.7 对象成员
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();
}
执行以上的代码我们可以看出,实际上对象成员是先构造的,而后类对象再构造;而对于析构来说,类对象先析构,而后是对象成员。这就好比搭积木,你要搭个人出来肯定要先搭好胳膊和腿;同理,如果你要拆积木,也肯定是先拆胳膊和腿。