类和对象
C++是一门面向对象的语言,提到面向对象就不得不提到三大特点:封装、继承、多态。
首先来看第一大特点:封装
封装
在C++中类使用关键字class修饰 class [类名]{ }
举个例子:
以学生类为例
我们可以看到一个类里,包含了权限,属性,行为。
通俗的理解:
权限指的是类内部的成员的权限。这个成员可以是变量即成员变量或者是成员函数。属性指的是类的内部所需要的一些特性,或者说是字段,比如学生的姓名,学号,员工的工号,手机号等等诸如此类。行为指的是一些函数或者方法,来操纵这个类的成员。
假设我们通过类以及函数封装来实现一个求圆的周长和面积,应该怎么写:
其中,PI代表圆周率,是个常量,我们就用const来修饰,double类型。
然后不论是求面积还是周长,都需要圆的一个属性,那就是半径,所以我们需要给这个类的属性中一个半径r。
最后分别实现一个计算周长和面积的行为,也就是函数:calculateZC()和calculateMJ()。
面向对象中,类还是比较容易理解的,主要是通过抽象将一类事物统一的归为一个类,由一个类来管理操作。在实际使用时,我们需要实例化这个类,因为类是抽象的,是一个比较大的范围,但是用的时候,我闷需要创建一个对象,这个对象属于这个类,实际操纵的是这个对象,而非这个类。
访问权限
类在设计时,可以把属性和行为放在不同的权限里,加以控制,访问权限有三种:
- 访问权限类型:
- public:公共权限
- protected 保护权限
- private 私有权限
- 关于public:不仅类的内部成员之前可以访问,类的外部也可以访问。
- 关于protected :类内部可以访问,类外部不能访问。子类可以访问父类中的保护内容
- 关于private :类内部可以访问,类外部不可访问,子类也不可以访问父类中的私有内容
以学生类为例
这里给学生类中的成员均是public也就是公共权限,那就是说类内部可以访问,类外部也可以访问。
举个例子:
假设目前有个person类,包含公共权限的属性:姓名,包含保护权限的属性:房子、车子,包含私有属性的银行卡密码。
那么这样的话 ,我们可以看到在main函数中,姓名字段是可以用的。但是车和密码字段是不能直接在main函数中使用的,因为脱离了person类的访问。
同样对于方法func(),其中包含了一些保护权限和私有权限的属性,但是这个方法是在Person类内部的,但它同时又是public权限的,所以main函数中可以访问func这个函数(即便其中包含了私有属性)。
struct和class区别
在C++中,struct和class的唯一区别就是默认的访问权限不同,struct默认权限是公共,class默认权限是私有。
我们给出一个类 C1,给出一个struct C2,都定义了一个int类型的m_A,在main函数中c1的m_A是无法访问的,c2的m_A是可以访问修改的。
成员属性设置为私有
成员属性设置为私有的目的是,可以自己控制读写权限。有些成员你可以自行设置成可读可写或者仅读,仅写。并且对于需要修改的也就是写,可以检测数据有效性。
自由设置权限
例如:
上面这个例子。就对name设置了可读可写,对年龄设置了仅读,对idol设置了仅写权限。
通过成员方法,来间接控制成员属性的权限。默认所有的属性都是私有,这样一来比较容易控制其权限分配。
总结:只读就是get,只写就是set,可读写就是既有get又有set。这个在开发中是经常使用的。
检测数据有效性
还是上面的例子,假设我们要对年龄的设置加一个限制,要输入0~100岁之间,应如何去做。
这里就需要写一个set函数,首先让年龄可以写,然后对要设置的年龄加一个限制。
封装的案例
案例1
设置一个立方体类,能够求其表面积和体积,并且写出全局函数和成员函数来比较两个立方体是否相同。
其中,对于长宽高的判断,其实只需要在单独设置里判断即可,只要单独的设置了判断,那么面积体积处就不需要设置判断了。
另外,可以看出如果是在类外的全局函数,来比较两个立方体是否相等,我闷需要传入,两个对象,这里使用了引用,不需要额外拷贝,如果使用了成员函数的话,我们可以直接在类内访问到私有权限的属性,这样只需要传入相比较的对象即可,稍微简化了一步骤。
案例2
点和圆的关系,设计一个圆类,和一个点类,计算圆和点之间的关系。
其中一种解法(我自己写的,如有问题,请留言):
当然这种方式是默认圆心,是在原点,那当然还有可能圆心不在原点。应当如何做呢?
对象的初始化和清理
C++的面向对象也是来源于生活,生活中我们需要什么东西都会获取也就是初始化,不需要后就会丢掉,也就是清理,c++中也同样如此,每个对象都有自己的初始化和清理工作。
构造函数和析构函数
对象的初始化和清理是比较重要的安全问题,一个对象或者变量没有初始状态,对其使用的后果是未知的,同样,使用完一个对象和变量,没有及时清理,也会造成一定的安全问题。
在C++中利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作,这是编译器强制要求我们去做的事,因此如果我们不提供构造和析构,编译器会提供构造函数和析构函数是空实现。
构造函数:主要作用是在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在对象销毁前系统自动调用,执行清理工作。
构造函数语法:类名(){}
1.构造函数,没有返回值,也不需要写void
2.函数名与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次。
析构函数语法:~类名(){}
1.析构函数,没有返回值,也不需要写void
2.函数名称与类名相同,在名称前加上符号~
3.析构函数不可以有参数,因此不可能发生重载
4.程序在销毁前会自动调用析构,无需手动调用,而且只会调用一次。
举个例子
这里我们会直接得到输出是:
这里我们并没有去调用这个函数,但是其内容就是被调用过了。说明这个对象是会自动初始化和释放的。
如果我们自己不写构造函数和析构函数,编译器自动帮我们写了,只不过函数体是空的。
构造函数的分类以及调用
按照参数分类:
有参构造和无参构造
按类型分类:
普通构造和拷贝构造
三种调用方式:
括号法、显示法、隐式转换法
分类
首先是分类:举个例子来看
我们可以看到普通的构造类型,无论是否有参,都比较好理解,但是针对拷贝构造函数,它的格式需要稍微注意,因为是拷贝构造,所以传进来被拷贝的对象一定不能修改,要加const,另外需要使用引用传参数。
调用
调用同样根据上面的例子中我们给出的构造函数来分析。
可以看出三种方法的调用方式,相对来说还是比较多的。
其中需要注意的是:
1.调用默认构造函数的时候,不要加(),因为编译器可能会认为是一个函数声明而不是在创建对象。
2.不要利用拷贝构造函数来初始化匿名对象。会出现重定向错误,编译器会认为这是你写了第二遍的上面某个构造函数。
拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常有三种情况
1.使用一个已经创建完毕的对象来初始化一个新对象(复制)
2.值传递的方式给函数参数传值
3.以值方式返回局部对象
构造函数的调用规则
默认情况下,C++至少给一个类添加三个函数
1.默认构造函数(无参数,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认构造函数,对属性进行值拷贝
值得注意的规则是:
如果用户定义有参构造函数,c++不在提供默认无参构造,但会提供默认拷贝构造。
如果用户定义拷贝构造函数,c++不会在提供其他构造函数。
以上面的代码为例子,其输出结果是:
如果将自己写的拷贝构造函数注释掉的话会是什么结果呢?
再一次运行输出结果是:
可以看到少了一句我们自己写的输出,但是年龄仍然为19,说明其拷贝成功了。所以结论是:如果我们自己提供了拷贝构造,那么编译器就不会为我们提供了,如果我们没有写,编译器会默认提供空的拷贝函数,里面只有对所有的属性进行拷贝。
深拷贝和浅拷贝
浅拷贝:简单的赋值操作
深拷贝:在堆区重新申请空间,进行拷贝操作
通过下面案例分析一下:
这里我们调用test01这个函数后,是会报错的。
我们在Person类中提供了一个age 以及在堆区new了一个height,如果我们利用Person p2(p1),这是编译器提供的拷贝构造函数,因为是值拷贝,只会做浅拷贝操作。如果只是age那没问题,但是height是一个指针,值拷贝只会把指针地址原样拷贝过去。
构造函数执行完毕之后,会调用析构函数,释放,按照先进后出的原则,p2会先被释放,因此height的原本的地址就会被delete,就是释放过了,如果p1还要在执行一次释放,就会出问题了(非法操作)。
这时就需要我们去深拷贝来解决这个问题,深拷贝,重新在堆区开辟一段空间去保存身高,这样p1有一份身高,p2也有一份身高,只不过身高的值是一样的,但是指向这个值的地址是不同的,因此各自在释放的时候就不会出现非法问题了。
那么我们需要自己做一个拷贝构造函数,
来代替默认的拷贝构造函数,这样就解决了浅拷贝的问题。
实际上就是我们要重新开辟一段空间,来存放要拷贝堆区上的值。
初始化列表
C++中提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)....
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
其中,B类中有对象A作为成员,A为对象成员。
但是这印出了一个问题,当创建B对象时,A与B的构造和析构顺序是如何的呢?
以如下例子
到底是先执行了Person的构造函数还是先执行了Phone的构造函数呢?
运行结果是:
那也就意味着对象成员会先执行构造函数。
总结:当其他类的对象作为本类的成员,先构造其他类,在构造本身。析构的执行顺序和构造正好是相反的,便于理解我们可以参考先进后出的原则。
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员,静态成员分为:
1.静态成员变量:
所有对象共享一份数据
在编译阶段分配内存
类内声明,类外初始化
举个例子:
声明静态成员变量,其不属于某个对象,所有对象共享同一份数据,一个改就全改了。因此其有两种访问方式:1.通过对象访问2.通过类名访问
静态成员同样是具有权限的,可以自己设定其权限。
2.静态成员函数:
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
上述例子给出了静态成员函数pri(),m_Age是静态成员变量,phoneNumber为普通成员变量。
静态成员函数只能输出m_Age的值,如果输出m_phoneNumber的值就会报错。
这个原因主要是因为静态成员函数只有一份,但是非静态成员变量,需要通过变量去访问。所以静态成员函数并不知道要访问的是那个对象的成员变量。
当然,静态成员函数同样是有权限的。