Cpp_复习整理_1_类与对象
写在开头
大一终于结束了,经过了一年的C++学习,是时候对所学知识进行整理与总结了。
其实老早就可以总结了,但是我懒癌,并且这个博客前天才刚建好,故这个系列只对大一下所学的“面向对象编程”部分进行整理与总结。
面向过程编程 VS 面向对象编程
面向过程编程
是一种传统的设计方法,围绕着功能进行编程。
并且往往具有以下的特征
- 用一个函数实现一个功能
- 所有的数据都是公用的
- 一个函数可使用任何一组数据
- 一组数据可被多个函数使用
但是这样结构化的程序设计,在程序规模增大的过程中,渐渐显示出不足。
软件业的目标是更快、更正确、更经济地建立软件。其中,就需要实现两个目标:
- 如何更高效的实现函数的复用?
- 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。
而结构化程序设计中,程序的大量函数、变量之间的关系错综复杂,要抽取部分代码来实现复用,会变得十分困难。
面向对象编程
因此,面向对象的程序设计方法,应运而生。
而设计程序的过程,就是设计类的过程。
是围绕着现实世界的实体(对象)进行设计;程序设计者从“设计函数功能”转向“设计类和对象”,即如何用属性和行为来描述一个实体,如何向实体发送消息以调度实体的行为。这是一种以认识世界的方法为参考的程序设计方法,更为自然,有利于大型程序的组织和实现。
类与对象的实现
如何声明一个类
1 |
|
如上,我们声明了一个矩形类,其中包含了public的成员函数:构造函数
,析构函数
,设置长宽函数setData
,面积函数area
,以及周长函数perimeter
,以及private的数据成员:len
,wid
。
类中可以设置变量
作为数据成员,也可以设置函数
作为成员函数(同时也被称作方法
)。
注意:类本身的类型不能作为
数据成员
的类型!但是指向类本身的指针
可以。1
2
3
4
5
6class obj{
public:
obj childObj;//不合法
obj* nextObj;//合法
};
成员访问限定符
类中的成员可以设置成员访问限定符
,即public
,protected
,private
。
如果一个成员没有给出成员访问限定符
,则默认为private
。
public
公有的:权限最开放,在类的外部,用户也可以通过成员运算符(.
和->
)来访问该数据成员和成员函数。private
私有的:在类的外部,用户不能通过成员运算法(.
和->
)来访问该数据成员和成员函数。只有类的成员函数
和友元函数
才能访问。protected
受保护的:如果不进行类的派生和继承
,其访问特性与private
是完全相同的。
1 |
|
常成员
常数据成员
用const
进行修饰的数据成员称为常数据成员
。
1 |
|
- 任何函数都不能对
const
数据成员赋值。 - 构造函数对
const
数据成员进行初始化时只能通过参数初始化列表
进行。 const
数据成员的值不能改变,所以不能对其进行赋值,只能对其进行初始化。因此const
数据成员在初始化时必须进行赋值。- 如果类中有多个构造函数,则每个构造函数必须都要初始化
const
数据成员。
常成员函数
1 |
|
const
作为函数的一部分,可以作为函数重载
的区分,因此在声明
和定义
时都必须要有const
关键字。- 而在函数调用时,
const
不一定是必须的。若不是同时具有**两个同类型同名同参的函数,区别仅仅const
关键字,这时候函数调用时const
就不是必须的。 - 常成员函数可以引用
const
或者非const
数据成员,但是不能“修改”它们。即:常成员函数不能更新任何数据成员
。因此,常成员函数多用于数据的输出等操作。 const
成员函数不能调用非const
成员函数,而非const
成员函数可以调用const
成员函数。
静态成员
静态数据成员
是一种特殊的数据成员,以关键字static
开头。
1 |
|
属于类,是类的派生,不属于任何一个单独的对象,由类的所有对象共享的数据,不占用具体实例的空间。
只能在类外初始化,如果未赋初值,则默认为0。不能用
参数初始化列表
进行初始化。1
2int student::data=114514;//不加static
const int student::data1=1919810;static
数据成员在程序开始运行时被分配空间,到程序结束时才释放空间——也就是它有着全局寿命。静态数据成员的使用:可以通过类名引用,如
student::data
;也可以通过对象名引用,如stu.data1
。建议使用类名来引用静态成员,代码更清晰,通过对象名的引用也仅仅是使用了该对象的“类型”。静态数据成员相当于
类域的全局变量
。静态数据成员可以成为成员函数的参数,而普通成员变量不可以。
1
2
3
4
5
6class student{
int data1;
static int data2;
void fun1(int data1);//ERROR
void fun2(int data2);//OK,静态数据成员相当于确定值
};
静态成员函数
1 |
|
和静态数据成员一样,是类的一部分,而不是对象的一部分。
调用时使用类名或者对象名来访问。
1 |
|
静态成员函数不隐含
this
指针,故不能访问非静态成员,通常只用于访问静态成员。但非静态成员函数可以调用静态成员函数。而普通成员函数具有
this
指针,访问类内的成员时,直接写数据成员名或成员函数名,前方自动略写了(*this).
这种访问称为默认访问。不能同时用
static
和virtual
两个关键字修饰一个成员函数,即:虚函数不能是static函数。因为虚函数必须要通过对象调用,必须要有隐藏的this
指针。并且static
成员函数是在编译时静态决议的,而virtual
成员函数是动态决议的(运行时才绑定)。
友元函数
友元类
如果将类B
声明为类A
的友元类,那么友元类B
中的所有函数都是类A
的友元函数,可以访问类A
中的所有成员。
1 |
|
注意:通常不把整个类声明成友元,只把有需要的成员函数声明成友元,这样更安全。
友元函数
友元函数
不是一个类的成员函数。通常的习惯是类内声明+类外定义
。友元函数是独立于任何类的一般的外界函数,所以它没有this
指针,即不支持默认访问。
1 |
|
友元成员
注意与友元函数
的区别。
一个类的成员函数可以声明为另一个类的友元,这类成员被称为友元成员
。
1 |
|
友元函数和友元成员可统称为“友元函数”,可以访问类的private
成员。
友元的注意点
友元不具有传递性:
A是B的朋友,而B是C的朋友,但是A不是C的朋友。
友元关系是单向的,不具有交换性。
A
声明B
是自己的朋友,但是B并不是A的朋友,因为B
没有声明A
是自己的朋友。1
2
3
4
5class A{
friend B;
...
};
class B{...};B
可以访问A
的私有成员,但A
不能访问B
的私有成员。友元关系不受
成员访问限定符
的影响,友元的声明可以出现在类中的任何地方。
成员函数的类内与类外定义
类内定义
如上一段代码,函数定义就属于类内定义。直接将函数体写在类声明内部。
内联函数-inline关键字
可以在函数首部的前方添加inline
关键字,使其成为内联函数
,其效果为,在调用到该函数的地方直接将函数体内的代码展开,节省了调用的过程所花费的时间。
inline
说明对于编译器只是一个建议,编译器可以选择忽略这个建议。例如将一个长达1000行的函数声明为内联函数
,编译器会无视这个inline
的建议,仍旧将这个函数设置为普通函数。- 在类内直接定义的函数,不需要
inline
修饰,编译器也一般会将其化为内联函数
。
慎用内联函数
内联函数
在每次调用函数的位置复制函数的代码。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。所以一般会选择频繁调用和代码量较少的函数设置为内联函数
。- 每一处
内联函数
的调用都会复制代码,会增加程序的代码量,增加内存开销。 - 函数中若带有循环,则不宜选用
内联函数
。函数本身运行时间就比调用时间长,已经没有必要使用内联函数
。
类外定义
1 |
|
类外定义需要现在类内写出函数声明
,然后在类外通过返回值类型 类域分辨名::函数名(形参列表)
来进行函数定义
。
- 建议:函数定义都应该在类外进行,并置于cpp文件内;而声明均在类内,并置于头文件内。这是一个好习惯。
怎样使用一个类和对象
定义一个对象
1 |
|
如此,便定义了rec1
和rec2
这两个对象。其中rec1
使用的是构造函数的默认参数,一个长一宽一的矩形,而rec2
是一个长二宽二的矩形。
常对象
定义对象时指定对象为常量,其数据成员的值在对象被调用时不能被改变。
1 |
|
常对象必须在定义时初始化,并且不能被更新。
为了防止
非const
成员函数修改常对象中的数据成员的值,**常对象只能调用const成员函数
,以及构造函数
和析构函数
。为何如此决绝?
因为程序设计时,
函数声明
与函数定义
大概率不会存在于同一个源文件
中,而程序编译时是以源文件
为单位的,因此,系统无法检测出函数声明
与函数定义
之间是否存在矛盾(例子:函数声明了不作修改,但是函数定义中却对某个数据成员进行了“不小心“的修改),只有等到了连接(link)甚至是运行阶段才能发现,因此编译系统值检查函数声明,只要发现调用了非const
的成员函数,就直接报错,即使这个函数并不会对任何数据成员进行修改。
非const
对象可以调用const
成员函数。常对象中可以存在
非const
成员函数,但是无法被该对象调用。而常对象中的所有数据成员均具有常数据成员
的特征。
构造函数
- 构造函数是每一个类都必须会有的函数。
- 构造函数是一种特殊的成员函数,无需用户调用,在定义新对象时由程序自动调用执行。
- 它的名称和类名一致,且没有函数类型,
void
也不行;因此,构造函数也没有返回值。 - 构造函数可以
重载
。 - 构造函数的分类
- 无参构造函数
- 含参、带默认值的构造函数
- 复制构造函数(拷贝构造函数)
无参构造函数
1 |
|
如此,便是一个最简单的构造函数,它没有任何参数,也没有任何实质性的作用。
重要:如果用户没有自定义任何类型的构造函数,则编译器会提供
缺省的无参构造函数
,即上方的构造函数,没有任何形式的参数,并且函数体为空。因此,常常显式地给出一个public
的无参构造函数,以便定义对象时被系统自动调用。tips:即使用户定义的是
含参的构造函数
或是拷贝构造函数
,该缺省的无参构造函数也不会提供。
含参构造函数
1 |
|
这是一个带有两个参数,且其中一个带有默认值的双参构造函数
。
在定义对象时,在对象名后紧跟实参来调用。
1 |
|
参数初始化列表
对于构造函数来进行对象的初始化,还可以采用参数初始化列表
的形式进行,写法为:
1 |
|
在原函数首部的括号后面加上一个:
,采用变量名(值)
的形式完成对成员的初始化赋值。
单参构造函数的特殊用法
1 |
|
注意:这两种写法写在一起会产生二义性错误
,如果调用时只给一个实参,系统将无法确定该调用哪一个构造函数。
这两个构造函数的共同特点是: 有且只有一个无默认值
的形参
在某些特定的情况下可以起到类型转换的作用。
比如:
1 |
|
此处等号右侧,int
整数首先通过单参构造函数rectangle(5)
,生成一个临时的无名对象
,再通过拷贝构造函数
来构造rec1
。并且构造完成之后,这个临时无名对象
即刻被析构
。
拷贝构造函数
1 |
|
拷贝构造函数也是缺省构造函数的一种重载,其参数列表为同类对象的一个常引用
,当用户没有给定拷贝构造函数的时候,编译器会加上一个缺省的拷贝构造函数
。
浅拷贝与深拷贝
缺省的拷贝构造函数作用是将两个对象的数据成员进行一一复制的赋值,即简单的对拷,是“浅拷贝”构造函数。
在不涉及动态分配的空间时,这种浅拷贝大多时候不会有问题。
但是,如果类中包括了动态分配的空间,且在析构时对申请的空间进行了释放,则通过浅拷贝构造的对象,就可能导致析构函数多次释放堆内存
的问题。
例子:
1 |
|
生成的a
,b
两个对象中*p
的值均为5,且两者均指向同一个内存空间,也就是说二者的5是同一个5。
- 这时当
main
函数结束时,系统就会报错,因为在析构a
时,p
中的地址所指向的空间已经在析构b
时被释放了,这时再释放这个地址所指的空间,也就是多次释放内存
的问题。
所以为了避免这个问题,需要我们自己显式定义拷贝构造函数,而不是使用缺省的拷贝构造函数。上面例子的拷贝构造函数可以给出:
1 |
|
- tips:此处提到析构顺序是先
b
后a
,看上去不合常理,但这确实是这样,程序存储变量的位置属于栈
的结构,也就是LIFO表
,即后进先出表
,先构造的后析构,a
比b
先构造,所以b
比a
先析构
何时会调用拷贝构造函数
用类的一个对象去“初始化”另一个对象时。
1
2
3
4
5
6//形式1
Cat cat1;
Cat cat2(cat1);
//形式2
Cat cat1;
Cat cat2 = cat1;对象作为函数参数传递,按照
传值调用
的方式传递给另一个对象时,生成对象副本。1
2
3
4
5
6
7void fun(Box b){...}
int main(){
Box box1(12,15,18);
fun(box1);
return 0;
}fun
函数的实参为box1
,形参为b
,且调用方式为传值调用
,参数传递时会调用拷贝构造函数,创建出box1
的对象副本给b
,参数传递的效果等同于Box b=box1
或者Box b(box1)
。函数返回值为类对象,当函数调用返回时,系统会调用拷贝构造函数生成一个
临时无名对象
,返回到主调函数
处。1
2
3
4
5
6
7
8
9
10Box fun(){
Box box1(12,15,18);
return box1;
}
int main(){
Box box2;
box2 = fun();
return 0;
}调用
fun
函数返回时,系统生成box1
的副本,是一个临时无名对象
,完成给box2
的赋值之后即刻消亡。为什么要使用常引用的参数?
使用
引用
作为形参的好处:不会调用拷贝构造函数创建
局部变量
,相应的,也不用在函数调用结束时去调用析构函数来消亡这个局部变量
,节省了函数调用的时间,提高了运行效率。添加
const
,使用常引用
的好处:防止函数中对实参进行修改。
析构函数
1 |
|
这是一个析构函数
,并且,如果用户没有自定义析构函数,那么系统会添加缺省的析构函数
,也就是这个样子——函数体中没有任何代码。
析构函数
的名称和构造函数
类似,也是与类名一致,区别是前面加上了一个表示取反的~
,用来表示其与构造函数
相反的作用——实现对象的撤销
。析构函数
没有函数类型,void
也不行,因此也没有返回值。析构函数
不能有任何参数,因此,它也不能重载(reload)
,一个类只能有一个析构函数
;但是,后续类的继承
中,它可以被覆盖(override)
。- 执行完
析构函数
,并不意味着对象已经消亡,析构函数
的作用是对需要撤销的对象进行清理,以防止出现资源泄漏
的问题。而对于对象所占的空间的撤销,并不是由析构函数
来完成的。(由delete
清除new
运算符定义的对象时,先调用析构函数
,然后再收回分配的内存。) - 如果类中的数据成员为
指针
,且关联动态分配
的空间时,必须手工添加析构函数
。
什么时候调用析构函数
delete
运算符导致析构函数调用。- 编译器产生的
临时对象
不再需要时。 - 程序运行结束时。
简言之就是:一个对象消亡时(生命周期
结束),会调用析构函数
。
赋值运算符的重载函数
在类定义时,系统会给出四个缺省的函数。它们是无参构造函数
,拷贝构造函数
,析构函数
。而剩下的一个,就是赋值运算符的重载函数
。
缺省的赋值运算符重载函数形式为:
1 |
|
实现的是成员的简单对拷,属于浅拷贝
。
那么前面也说过,最好是实现深拷贝
,所以需要手写赋值运算符的重载函数。
本篇结束
简单总结了一下类的构成,本篇代码均在Typora中手打,没有经过编译器的语法检查,可能存在谬误。如有发现请指出。