类和对象大一整理过相关文章,这里不在重复只作记录补充
概述
编程分类
- 面向过程
- 面向对象
类和对象的关系
类是对象分类的依据,对象是类的实例
对象关系
HAS-A 关系(聚合)
- HAS-A 符合的模式:A 拥有 B 或者 A 包含 B
- 一个对象是另一个对象的一部分,被包含的对象是容器对象的部件,大的对象由小的组件构成
IS-A 关系(继承)
-
IS-A 符合的模式:A 是一种 B,A 非常像 B
-
类为现实世界中拥有属性和行为的对象建模
-
继承为现实世界中对象的层次关系建模
定义和使用类
类的结构定义通常放在头文件中,说明类在没有实例化不占用空间,而类的成员函数的具体实现(定义)一般放在 .cpp
文件中
- public :本类和派生类可以访问,客户通过对象访问
- private :只能本类访问,不希望用户和子类访问
- protected :本类和子类可访问不希望客户使用
封装与隐藏
公有成员是对象对外开放的接口,私有程序隐藏在类的内部
分数的类和对象,记录的分子和分母不能直接修改,只能通过提供的修改函数接口进行修改
如果某个函数只在函数内部作为辅助函数使用,那么通常也定义为私有,例如化简函数 gcd()
在堆中使用类和对象
和提供的基础数据类型一样,也可以像他们一样使用 new
作为关键字在堆中动态创建对象,使用完后用 delete
关键字显式释放空间
类隐藏的成员
C++ 的类和对象会在定义时自动帮你创建许多隐藏的成员,例如默认构造函数、默认析构函数、this 常量指针、默认赋值函数等等,虽然代码中没写但是确实存在
this 指针
- 每个类中都隐含 this 指针成员,不用定义
- 对于所定义的每个对象,this 永远指向当前对象的地址
- 通过对象调用成员函数时隐含着都要传递 this 指针作为实参
- this 指针是常量指针,不能再指向别的对象
- 在成员函数中访问成员数据或者其他成员函数,可以通过
this->
进行限定,同常省略
1 |
|
还是很有用的,无疑可以通过下列程序实现相同的效果
1
2
3
4
5
6
7
8
9
void Fraction::setNum(int a){
num=a;
}
void Fraction::setDen(int b){
if(b==0)return ;
den=b;
}但是参数的名字可读性很差而且存在需要传入多个的时候再给每个参数取名字也影响函数整体的可读性,借助 this 指针则可以参数和类内部成员变量的名字相同,解决了冲突
this 的作用
实际上在调用函数的时候 this 被传递了,可以看出来函数实际借助 this 指针将对象本身的地址传入
析构与构造函数
构造函数
- 类的特殊成员函数,函数名也与类名相同
- 不能有返还类型,void 也不行
- 构造函数的访问权限是公有但是不应该被显示调用
- 构造函数支持重载
- 构造函数的目的:构造对象时由系统自动调用用于初始化对象数据成员
- 在类定义中没有定义构造函数时编译系统会自动生成一个没有实质功能的默认构造函数
在定义类是可以重载多个构造函数,但是只要你实现了一个版本的构造函数那么系统就不会自动生成默认构造函数
1 |
|
多个版本的构造函数和带缺省值的构造函数
可以对于传入不同数量的参数情况分别写一个重载构造函数,为了方便只写一个带缺省值的构造函数
1 | class Fraction{ |
1 | class Fraction{ |
类内初始化
C++ 11 之前类和对象内部数据成员在定义时直接初始化是错误的,但是 C++ 11 规定可以类内初始化
1 | class Fraction{ |
委托构造函数
- 委托构造函数时,先调用委托的版本构造函数再调用函数自身
1 | class Fraction{ |
类型转换构造函数
只有一个参数的构造函数比较特殊可以实现类型转换,又被成为类型转换构造函数,借助这个函数可以将整数转换为对应的类并赋值给 Fraction 对象
可以看到 4 被作为一个参数转换为了一个 Fraction 类并将类赋值给了 c
拒绝隐式类型转换
上述如果并不想编译器在执行时默认进行类型转换,可以借助 explicit
关键字限制类型转换构造函数
1 | class Fraction{ |
初始化列表
其实类和对象重载构造函数实现对数据成员的初始化并不是真正意义上的初始化,可以看做两个步骤:先定义,后初始化。类和对象可以借助初始化列表实现定义的同时完成初始化,在效率生更高。所以建议用初始化列表来对数据成员进行初始化
1 | class Fraction{ |
拷贝构造函数
- 拷贝构造函数是一种特殊的构造函数,形参就是本类对象的常引用
注意:重载拷贝构造函数的时候要是形参不是常引用会编译报错
为什么必须常引用?如果不是引用类型那么函数需要将实参拷贝一份赋给形参,此时又需要调用拷贝构造函数,如此反反复复就陷入死循环了
- 用途是建立一个新对象时用一个已经存在的同类型对象去初始化这个新对象
- 每个类必须有一个拷贝构造函数,可以自定义,否则编译器自动生成一个默认构造函数用于同类型之间的复制
- 使用
const
引用是一种契约,防止在复制的时候将原对象意外的修改 - 在构造函数中可以通过 f 对象访问私有成员
1 | Fraction(const Fraction&f){ |
注意:默认的拷贝构造函数就是将原对象的数据成员一一复制给新对象,但是当涉及指针等复杂成员时,复制的时候就是浅复制了,此时再使用默认构造函数时就会在释放空间是发生重复释放同一地址,此时必须要自定义重载拷贝构造函数。否则的话视情况而定,简单的构造函数用默认的就可以了
拷贝构造函数的调用时机
- 使用现有对象初始化新对象
- 调用函数,对象按值传递
- 函数返还对象,对象按值传递
对象作为函数的参数
总共调用两次构造函数
1 | void fun(Fraction f){//每次调用相当于定义局部变量 |
对象作为函数的返还值
总共调用五次构造函数
1 | Fraction Copy(Fraction f){//每次调用相当于定义局部变量 |
构造函数的 =
在程序中可以用 =
来用某个对象给新的对象初始化,也可以用 =
给两个对象互相赋值。但是注意前者是调用构造函数进行的初始化操作,后者不调用拷贝构造函数只是内部成员之间的赋值。
1 | Fraction f2=f1;//调用一次构造函数初始化 |
很容易看出来调用构造函数创建新对象的时候的
=
就是进行初始化,对于两个已经存在的对象=
就是普通的赋值
构造函数的调用时机
- 定义对象
- new 动态创建对象
- 调用函数时对象进行值传递
- 函数返还对象时进行值传递
注意:构造函数由系统自动调用,所以全局对象的构造函数在 main() 之前调用!
阻止拷贝构造函数
传统做法是将拷贝构造函数声明为私有,这样就不能调用了
1 | class Fraction{ |
借用 C++ 11 引入的 delete
关键字可以用来禁止生成缺省版本的函数,和 default
相反, default
是强制生成缺省版本的函数
1 | class Fraction{ |
析构函数
- 类的特殊成员函数,函数名为
~类名称
- 不能有返还类型, void 也不行
- 析构函数不能有参数,只有一个版本,不能重载
为什么不能有参数?如果真的有那么析构函数本身的参数析构又调用自身,如此一直递归就停不下来了
-
析构函数通常是
public
类型 -
析构函数的目的是在删除对象或者对象超出生存周期时供系统调用完成清理工作(例如释放你动态申请的堆空间)
-
如果类定义没有定义析构函数,编译系统会自动生成一个默认析构函数
-
再定义类的时候,可以自己定义析构函数已完成特定的释放清理工作,通常情况下在构造函数中用
new
分配的内存空间,需要在析构函数中用delete
释放
动态内存分配
一维数组类动态分配空间
设想一个类包含多个元素,客户在每次使用的时候存入的元素的个数不确定,那么怎么保证类开够足够大的数组来存储这些元素呢?一个好的解决办法就是为数组动态分配空间,根据每次客户指定的元素的最大个数来申请空间,想要访问某个单元的时候就借助索引下标
类的定义
1 | class Array1D{ |
类动态内存分配的相关函数定义
1 |
|
类其他成员函数的定义
1 | bool Array1D::validIndex(int index){ |
拷贝与赋制问题
浅拷贝
- 若没有编写拷贝构造函数和赋值运算符,编译器会生成缺省的拷贝构造函数和赋值运算符,用源对象中的数据成员一一初始化或赋值目标对象
- 若类动态分配内存,缺省的拷贝构造函数和赋值运算符仅仅初始化或赋值指针,而没有分配内存,会导致内存的问题,称之为浅拷贝
自定义拷贝构造函数
为了解决上述问题,这时候不得不自定义拷贝构造函数了,当然了也有技巧使构造函数更加安全高效
1 | Array1D::Array1D(const Array1D& a){ |
重构——提取重复的代码并调用辅助函数
上述的拷贝构造函数和构造函数都有申请动态内存分配的代码,我们可以将重复的代码提炼成一个辅助函数并声明为保护或者私有供内部成员函数使用
1 | void Array1D::copyData(int *data,int s){//提炼的辅助函数 |
或者借助 C++11 的构造函数委托调用,拷贝构造函数借助构造函数实现
1 | Array1D::Array1D(int *p,int s){//构造函数 |
右值引用
C++ 中的引用必须绑定一个左值,无法定义一个常量或者表达式的引用(因为他们都是右值)
不懂左值和右值请参考该博文,笼统的来说:
- 左值就是占用内存空间的变量(类似容器,可以放不同事物),右值不占用内存空间只是临时的储存在寄存器中(类似某个具体事物)
- 左值可以放在等号左右两侧(左值可以转换为右值),右值只能放在等号右侧
函数参数修改为 const int &
后可以传递参数但是不能修改参数(即传入的左值也不能修改了)
C++11 增加了 &&
,可以用临时的右值并进行修改(传统编译器是不允许右值作为引用参数传递的)
1 | void fun(int &&a);//传入右值并可以在函数中被修改 |
move 的动机
- 当对象非常庞大,如array数组类,复制构造 1 个数组的开销非差大。复制 1 个临时数组时,可以考虑使用移动的策略,将临时数组的“内部数据”直接移动到目标对象,而不是重新构建目标对象
- 移动复制是一种破坏性复制,源对象的数据和状态被转移到目标对象,复制后源对象不再有效
增加右值引用和 move 语义
C++11 可以加入相应函数的右值引用来实现 move 语义的移动式赋值,编译器会自动根据上下文环境自行选择不同函数使用来加快运行效率
1 | class Array1D{ |
例如:对于传参和函数返还的临时对象的创建拷贝和析构,编译器会优化该过程采用
move
的方法直接将地址转移。当然有时编译器无法理解,这是我们可以指定move
告诉编译器采用移动复制
移动构造函数实现
1 | Array1D::Array1D(Array1D &&a){//移动构造函数定义 |
移动时直接接管参数对象的数据,然后将其成员置空
普通拷贝和移动实例
1 | Array1D a(100);//普通拷贝构造,需要额外给100分配内存 |
强制移动的实例
1 | Array1D a(100); |
通过 move 操作,强制将 a 移动并移动构造对象 b 移动后,a 对象不再有效其数据已经被移走
特殊成员
静态数据成员
1 | class Dog{ |
- 静态数据成员在内存中只有一份,不管内存中是否存在对象或者存在多少个对象,静态数据成员只有一份,没有对象时,静态数据就已经存在了,只能通过类名访问
- 公有静态数据成员:可以在成员函数中随意访问,也可以通过类名称或对象访问
- 私有静态数据成员:不能通过类名或对象访问,只能在成员函数内部访问
- 静态数据成员在建立后保存在堆中(再次定义直接略过),直至程序结束时自动释放空间
静态数据成员相当于一个局部的全局变量,只是在作用域内对象公用,并没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性。例如:
- 上述在类中定义后那么通过只能通过其他对象或者类名访问
- 如果在 for 循环中定义那么只能在该循环内访问
并且静态数据成员可以实现信息隐藏。静态数据成员可以是
private
成员,而全局变量不能
1
2
3
4
5 for(int i=0;i<5;i++){
static int a=6;//只会执行一次,下次遇见直接跳过
...
}
cout<<a<<endl;//编译器报错
- 静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。所以一般静态数据成员不能在类声明中定义(在类的内部先声明,在类外部进行定义),只有静态常量成员可以在类内部定义,因为它既然是
const
的,那程序就不会再去试图初始化了
如上图显示,静态数据成员和类的函数一样只有一份,各个对象共用
1 | class Dog{ |
静态成员函数
- 多个对象“共享”同一个静态数据成员可以随意访问,容易造成混乱→ C 中的全局变量
解决办法:将静态数据定义为私有并定义公有静态成员函数,通过普通成员函数或者静态成员函数间接访问静态数据成员
- 系统不为静态成员函数传递隐含的 this 指针,因此静态成员函数不能直接访问普通数据成员,也不能访问普通成员函数,只能访问不用 this 指针的静态数据成员、其他静态成员函数或者通过类名和对象间接访问
1 | class Test{ |
sum 方法中不能直接访问包含 a,b,c 普通数据成员,但是可以通过类名或对象访问
类中使用友元
- 通过私有类型,实现了封装与隐藏,提高了软件的可靠性、可重用性和可维护性,但牺牲了性能。因为增加了函数调用带来的系统开销,所以 C++ 又提供了打破封装的方法
- 友元是出于执行效率的考虑,允许类外部的函数或其它的类通过对象访问本类的私有成员,称为友元函数或友元类
- 友元打破了封装和数据隐藏,除非一些特殊场合,应该慎用。用多了就失去了使用类封装的本意
计算两点距离普通函数版本——封装间接访问
普通成员函数不能访问私有成员,所有只能借助类提供的公有成员函数作为借口间接访问
1 | class Point{ |
计算两点距离友元函数版本——封装后借助类对象访问
友元函数可以借助类对象访问私有成员
1 | class Point{ |
友元函数的声明放在私有部分和公有部分的作用效果是一样的
-
友元函数不是类的成员函数,只是类之外的一个普通函数,友元函数的声明必须在类的内部,定义在类的外部
-
friend
只是在类内部声明一下这个普通函数是我的朋友。一旦普通函数被声明为某个类的友元函数,在函数体内便可以通过对象访问类的私有成员,而不是直接访问成员 -
因为友元函数本质还是普通函数,不属于类的内部成员函数,所以类外部定义时是不能加类的前缀名的
1 | double Point::getDis( const Point &a, const Point &b){//错误一:不是类的成员函数不加前缀 |
计算两点距离成员函数版本——直接访问私有成员
成员函数内部可以访问私有成员
1 | class Point{ |
类的常成员函数
- 定义:类中某些函数只会读取数据成员而不会修改数据成员,作为一种契约和良好习惯,应该将该成员函数声明为常成员函数
- 方法:在函数头的结尾加上
const
关键字 - 在常成员函数中,如果修改数据成员(或调用非常成员函数以间接修改)编译器将报错
- 静态成员函数不能声明为常成员函数,普通函数不能使用
const
修饰
常成员函数的声明
const
本质上修饰的 this 指针
1 |
|
setValue 要修改数据成员,不能声明为常成员函数,getSize 和 getValue 只读取数据成员,应该声明为常成员函数
常成员函数的定义
- 常成员函数声明和定义时都要加
const
关键词 - 良好编程习惯——尽可能将成员函数声明为常成员函数,防止对数据成员的意外修改
1 | int Array1D::getValue(int index) const{ |
类的常对象
- 声明为常量的对象,其数据成员不能被修改
- 因为常对象的参数不能被修改,所以通过常对象只能调用确定不会修改常对象的常成员函数和公有静态成员函数,不能调用非常成员函数
- 通常常引用和指向常对象的指针,也不能修改对象数据成员
下列各种常对象控制访问
1 |
|
内联函数
一般使用内联函数的情况很少,了解这里即可
-
定义在头文件中的成员函数自动成为内联函数
-
编译器并不能保证内联一定成功
-
作用:不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,适用于功能简单,规模较小又使用频繁的函数(一般五行以下)
-
限制:内联函数不能有循环体,switch语句,不能使用递归,也不能进行异常接口声明
-
代价:内联是,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间相比于函数调用的开销较大那么效率的收获会很少。另一方面每一处内联函数的调用都要复制代码将使程序的总代码量增大消耗更多的内存空间。
内联函数和宏定义的区别:
- 内联函数和宏的区别在于宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关联上内联扩展,将与一般函数一样进行调用比较方便
- 另外,宏定义在使用时只是简单的文本替换,并没有做严格的参数检查,也就不能享受 C++ 编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
关键字使用总结
关键字 | 使用方法 |
---|---|
inline | 尽量只在定义时出现,声明可有可无 |
const | 成员函数的声明和定义都必须填写 |
static | 只在声明时出现,定义时不能出现 |
friend | 只在声明时出现,定义时不能出现 |
函数缺省值 | 只在声明时出现,定义时不能出现 |
对象成员
-
创建复杂类时经常将简单类对象作为其成员
-
简单类组合构成复合类,构成了“有一个”关系(HAS-A)就像汽车有发动机、车轮、传动装置等
-
当一个类的对象作为另一个类的成员时,称该对象为对象成员,这种方法称为“组合技术”
-
包含对象作为数据成员的类称为容器类,当创建容器类对象时,对象成员的初始化由构造函数的初始化列表提供
三角形类的例子
例如一个三角形类:我们可以看作是 3 个点连线组成的图形,先封装好点的操作,然后再利用点的类作为三角形操作的。这样做的好处是与直接处理点的函数相比,当传入点的参数由笛卡尔坐标变为极坐标或者球坐标的时候,前者不仅需要重新修改点的操作,还要修改受关联的三角形操作,而后者只需要在点的类中将传入的坐标继续处理为笛卡尔坐标即可,三角形的操作不需要修改。对于工程类项目这种逻辑分层的观点更有利于维护代码
不定义 Point
类,直接将 6 个坐标值 (x1,y1,x2,y2,x3,y3) 保存在 Triangle
中存在的缺陷:
getArea()
方法比较复杂
上述方法则只用利用
Point
类提供的接口函数求出点之间距离后计算面积即可
- 点的处理逻辑将来难以复用到其他程序中
上述方法则可以将点的逻辑用到其他的例如四边形、长方体等类中
- 点的逻辑发生变化(变更为三维点),将影响到
getArea()
的计算
上述方法
getArea()
则不需要更改,只用将Point
类的接口函数getDis()
进行修改
而通过组合 Point
类得到 Triangle
类,对客户代码是透明的,用户只关心 Triangle
类的接口而不关心其实现,Triangle
只关心 Point
类的接口,而不用关心其实现
构造与析构过程
三角类构造其实就是三个点的构造,但是注意构造的顺序由类定义的顺序决定,而不是初始化列表的顺序。初始化列表只是指出参数赋给谁,但是并不决定初始化的顺序