面向对象的程序设计
一、 概述
1. 程序设计的泛型
- 面向过程的程序设计
程序=过程+调用 - 面向对象的程序设计
程序=对象+消息
面向对象程序的基本元素是对象,面向对象程序的主要结构特点是:- 程序一般由类的定义和类的使用两部分组成;
- 程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后﹐启动有关方法完成相应的操作。
2.基本概念
-
对象
描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体
-
类
描述一组对象的公共属性和行为的抽象描述 -
消息和方法
调用对象中的函数就是向该对象传送一个消息﹐要求该对象实现某一行为(功能﹑操作)。
对象所能实现的行为(操作),在程序设计方法中称为方法, 它们是通过调用相应的函数来实现的,在C++语言中方法是通过成员函数来实现的。
3. 基本特征
-
抽象
对于一组具有相同属性和行为的对象,可以抽象成一种类型,在C++中,这种类型就称为类(class),类是对象的抽象,而对象是类的实例 -
封装
面向对象的程序中使用一个对象时,只能通过对象与外界的操作接口来操作它,而不必知道实现细节。
-
继承
以面向对象程序设计的观点,继承所表达的是对象类之间相关的关系。这种关系使得某一类可以继承另外一个类的特征和能力。若类之间具有继承关系,则它们之间具有下列几个特性:
- 类间具有共享特征(包括数据和操作代码的共享)﹔
- 类间具有差别或新增部分(包括非共享的数据和操作代码)﹔
- 类间具有层次结构。
继承可以分为单继承和多继承。
-
多态
面向对象系统的多态性指不同的对象收到不同的消息执行不同的操作。
C++支持两种多态:- 编译时多态
通过函数重载(包括运算符重载实现)
- 运行时多态
通过虚函数实现
- 编译时多态
4. 优点
- 提高程序的重用性
- 可控制程序的复杂性
- 可改善程序的可维护性
- 更好的支持大型程序设计
- 增强了计算机处理信息的范围
- 很好地使用新的硬件环境
二、C++概述
C++在非面向对象方面的扩充
- 注释
- 输入输出
- 局部变量
C++运行在任何地方说明局部变量
- 结构、联合、枚举可以直接作为类型名
- const
const和#define的区别
#define
是在预编译时进行字符替换,不占存储单元,但是容易出错。- const定义的常量是有类型的,占用存储单元、有地址、可以有指针指向它,更安全。
- 函数原型
在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前必须对所调用的函数作函数原型声明,以说明函数的名称、参数类型与个数,以及函数返回值的类型。
- 内联函数
在C++中,被inline修饰的函数,在编译时函数体内的代码直接插入到调用函数的位置
- 带有默认参数的函数
在C++中,如果函数的参数中有默认参数,则在函数调用时,可以不传入参数的值,而是使用默认参数的值。
- 函数的重载
函数的重载是指两个函数名相同,但是参数列表不同的函数。
- 作用域运算符“::”
若两个同名变量,局部变量的优先权更高,会屏蔽全局变量。加上作用域运算符“::”,就可以访问全局变量。
- 无名联合
- 强制类型转换
- 运算符new和delete
- 引用
建立引用的作用是作为变量另起一个名字,变量的引用通常被认为是变量的别名。
类型 & 引用名=变量名;
引用和变量同步更新,使用同一地址- 引用作为函数参数
- 引用作为函数返回值
三、类和对象
1. 对象指针
每个对象初始化后都会在内存中占有一定的空间,因此既可以通过对象名访问一个对象,也可以通过指针访问一个对象。
- 对象指针的声明
类名 *指针名;
在一般情况下,用运算符"."来访问对象的成员;当使用对象指针访问时,使用操作符"->"来访问对象的成员。
2.this指针
对于一个类的若干对象,对象的数据部分存储在不同的空间,但函数代码都在同一个空间中。因此,在调用一个对象时,编译器将该对象的起始位置赋给this
指针。
3.对象的赋值和复制
- 对象的赋值
对象的赋值是指将一个对象的数据成员逐位复制给另一个对象。
等价于o2 = o1;
o2.a=o1.a; o2.b=o1.b;
两个对象之间的赋值,只是将对象的数据成员复制给另一个对象,而不是将对象的指针复制给另一个对象。
- 拷贝构造函数
- 定义拷贝构造函数
拷贝构造函数的格式如下:类名::类名(const 类名 &参数名) { //复制数据成员 //复制指针成员 }
- 调用拷贝构造函数
- 赋值法
Point o2 = o1;
- 代入法
Point o2(o1);
- 赋值法
- 拷贝构造函数的调用时机
- 用一个对象初始化/赋值另一个对象
- 当函数的形参是一个对象时,调用拷贝构造函数
- 当函数的返回值是一个对象时,调用拷贝构造函数,将当前对象复制给一个临时对象并传到该函数的调用处
- 定义拷贝构造函数
4.友元
友元是指两个类之间的关系,友元允许类之间的类成员函数访问类的私有成员。
友元函数
友元函数可以是不属于任何类的非成员函数,也可以是属于另一类的成员函数。
- 非成员函数
- 友元函数的声明
class 类名{ public: //... friend 函数A(参数列表); private: //... }; 函数A(参数列表) { //... }
- 友元函数的调用
调用函数A,可以访问类中的私有成员。
- 友元函数的声明
- 成员函数
- 友元函数的声明
class 类B; class 类A; class 类B{ public: //... friend 类A::函数A(参数列表); private: //... };
- 友元函数的声明
友元类
当类Y被说明为类X的友元时,类Y的所有成员函数都成为类X的友元函数,这就意味着作为友元类Y中的所有成员函数都可以访问类X中的所有成员(包括私有成员)。
- 友元类的声明
class 类X{ public: //... friend class 类Y; private: //... };
友元关系是单向的,不具有交换性。
4.类的组合
在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,也称为子对象。
- 对象成员的声明
class 类A{ public: //... 类B b; private: //... };
- 对象成员的初始化
X::X(形参表):b(参数表){}
5.常类型
常引用
在说明引用时用const
修饰,则被说明的引用为常引用。如果用常引用作形参,便不会产生对实参的不希望的更改.
在实际应用中,常引用往往用来作函数的形参,这样的参数称为常参数。
void f(const int &x){
x=10;//错误
}
常对象
在说明对象时用const
修饰,则被说明的对象为常对象。常对象的数据成员值在对象的整个生存周期内都不会改变。
常对象成员
- 常数据成员
只有构造函数初始化时可以赋值,其他函数不能修改。class 类A{ public: //... const int a; private: //... };
- 常成员函数
class 类A{ public: //... int a() const{ return 0; } private: //... };
- 常成员函数不能更改普通数据成员,也不能调用普通成员函数。
- 常对象只能通过常成员函数访问。
- 常成员函数可以访问常数据成员,也可以访问普通数据成员;常数据成员可以被常成员函数访问,也可以被普通成员函数访问。
const
可以用来区分函数(同名函数可以根据是否为常函数重载)
数据成员 | 普通成员函数 | 常成员函数 |
---|---|---|
普通数据成员 | 访问 √ 修改 √ | 访问 √ 修改 × |
常数据成员 | 访问 √ 修改 × | 访问 √ 修改 错 |
常对象的数据成员 | 访问 × 修改 × | 访问 √ 修改 × |
四、派生和继承
1. 派生类
声明派生类的一般格式如下:
class 类X:[继承方式] 类Y{
public:
//...
};
- 共有继承:public
- 私有继承:private
- 受保护继承:protected
基类中的成员 | 在公有派生类中的访问属性 | 在私有派生类中的访问属性 | 在保护派生类中的访问属性 |
---|---|---|---|
公有成员 | 公有 | 私有 | 保护 |
私有成员 | 不可直接访问 | 不可直接访问 | 不可直接访问 |
保护成员 | 保护 | 私有 | 保护 |
2.派生类的构造和析构
- 派生类构造函数的格式如下:
class 类X:类Y{ public: 类X(形参表):类Y(参数表){} };
派生类的构造和析构函数的调用顺序为:
1.父类的构造函数
2.父类的默认构造函数
3.派生类的构造函数
4.派生类的默认构造函数
5.派生类的析构函数
6.父类的析构函数
7.父类的默认析构函数
3. 基类成员在派生类中的访问属性的调整
同名成员
同名成员采用以下方式访问:基类名::成员名
访问声明
对于私有继承,基类的公有函数可以被派生类内部访问,但是无法在外界调用,因此需通过调用派生类的成员函数间接的调用基类的成员。
4. 多重继承
C++中允许多重继承,声明的格式如下:
class 类X:public 类Y,private 类Z{
public:
//...
};
对于基类成员的访问必须是无二义的。可以通过以下方法消除二义性:
obj.X::func()
5. 虚基类
如果一个类有多个直接基类,而这些基类有一个共同的基类,则在最底层的派生类中会保留多份这个间接共同基类的成员。在使用这些成员是,必须考虑二义性问题。
若Base类中含有数据成员a,Base1和Base2中都不含,在Derived类中,调用a的时候,需要指明是哪个基类的成员,否则会产生二义性。
将中间的基类改为虚基类,可以解决二义性问题。
虚基类的定义格式如下:
class Base{
//
};
class Base1:virtual public Base{
//
};
此时的继承结构如下:
虚基类的初始化
- 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
- 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
- 对于非虚基类,构造函数的执行顺序仍是先左后右﹐自上而下。
- 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数﹐再调用派生类的构造函数。
- 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都自动被忽略。
class X:public Y,virtual public Z{ }; X one;
在这个例子中,X类的构造函数会先调用Z类的构造函数,再调用Y类的构造函数,最后调用X类的构造函数。
基类和派生类对象之间的赋值兼容关系
根据赋值兼容规则,在基类 Base的对象可以使用的任何地方,都可以用派生类Derived的对象来替代,但只能使用从基类继承来的成员。具体表现在以下几个方面。
- 派生类对象可以向基类对象赋值
- 派生类对象可以初始化基类对象
- 派生类对象的地址可以赋给指向基类对象的指针
- 如果函数的形参是基类对象或者基类对象的引用,在调用函数时可以用派生类对象作为实参。
五、多态性
1. 多态类型
在C++中,多态性的实现和联编(也叫绑定)这一概念有关。一个源程序经过编译、连接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运行之前就完成的联编称为静态联编,又叫前期联编;而在程序运行时才完成的联编叫动态联编,也称后期联编。
编译时多态
静态联编支持的多态性称为编译时多态。也叫静态多态。在C++中,是通过函数重载(包括运算符重载)和模板实现的。
运行时多态
动态联编支持的多态性称为运行时多态。也叫动态多态。在C++中,是通过虚函数实现的。
2. 运算符重载
类外定义的运算符重载
C++为运算符重载提供了一种方法,即在进行运算符重载时,必须定义一个运算符重载函数,其名字为operator,后随一个要重载的运算符。
重载运算符有以下规则
- 部分运算符不能重载
. .* :: Sizeof ?:
- 只能重载C++已有的运算符,不能重载自定义运算符
- 重载功能应与原有功能类似
- 重载不能改变运算符的操作数个数
- 重载不能改变优先级
- 重载不能改变运算符的结合性
- 重载函数的参数至少有一个是类对象
- 重载函数可以是普通函数,也可以是友元函数
- 类的运算符必须重载,“=”除外
友元运算符重载函数
- 定义格式为:
friend T operator+(const T&, const T&);
- 友元运算符重载函数的调用
- 显式调用
operator+(a, b)
- 隐式调用
a + b
- 显式调用
成员运算符重载函数
- 定义格式为:
void operator +(int a) { // }
- 成员运算符重载函数的调用
- 单目:无需参数,使用自身数据成员
- 双目:一个参数
成员运算符重载函数也可以显示调用或隐式调用
++和--的重载
C++ 2.1后,可以通过在运算符函数参数表中是否插入关键字int来区分前缀或后缀使用。
- 前缀重载
ob.operator++();//成员函数前缀重载 ob.operator++(X &ob);//友元函数前缀重载
- 后缀重载
ob.operator++(int);//成员函数后缀重载 ob.operator++(X &ob,int);//友元函数后缀重载
在显式调用后缀重载时,int传入0。
=的重载
使用=赋值一个对象,默认会将数据成员逐个复制。这种复制是一中浅层复制的方法,可能会有一些问题。
-
指针悬挂问题
当一个对象的数据成员是指针时,复制后的对象会指向该成员同一块内存区域,而原区域无法访问,也无法撤销,产生指针悬挂问题。
因此要避免指针悬挂问题,需要使用重载=,完成深层复制。
[]的重载
[]可以看作一个双目运算符,X[Y]
中,X是左操作数,Y是右操作数。
- []的重载定义格式为:
T operator[](int);
3. 类型转换
用户自定义的类与其他数据类型之间的转换,分为以下两周方法:
- 通过转换构造函数进行类型转换
- 通过类型转换函数进行类型转换
转换构造函数
转换构造函数是构造函数的一种。定义方法同构造函数类似:
- 定义格式为:
类名A (类名B b){ // };
- 转换构造函数的调用
或者A a(b);
A(b);
类型转换函数
类型可以将类型转换为其他类型。定义格式为:
c++ operator 类型(){ return 值; }
- 类型转换函数的调用
类型(值);
4. 虚函数
虚函数是重载的另一种表现形式。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是说在运行时才决定如何动作。
虚函数的引入
C++中规定,基类的对象指针可以指向它的公有派生类的对象,但是只能访问派生类中从基类继承来的成员。
例如:
class A{
public:
void f(){
cout << "A::f()" << endl;
}
};
class B:public A{
public:
void f(){
cout << "B::f()" << endl;
}
void g(){
cout << "B::g()" << endl;
}
};
int main(){
A *pa = new B;
pa->f();//输出:A::f()
pa->g();//c错误
return 0;
}
解决方法:
class A{
public:
virtual void f(){
cout << "A::f()" << endl;
}
};
class B:public A{
public:
virtual void f(){
cout << "B::f()" << endl;
}
};
int main(){
A *pa = new B;
pa->f();//输出:B::f()
return 0;
}
使用规则
- 只需要声明虚函数原型,在类外定义虚函数时,不需要再加virtual
- 虚函数被重新定义时,函数原型必须和基类中完全相同
- 基类中的函数为虚函数时,派生类中的同类函数必须为虚函数,因此virtual关键字可写可不写。
- 一个虚函数无论被公有继承多少次,仍保持虚函数特性
- 虚函数必须是其所在类的成员函数,不能是友元函数,也不能是静态成员函数
- 只有通过指针访问虚函数才有获得运行时多态
- 可以设置虚析构函数
虚函数与重载函数的关系
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式。
当普通的函数重载时,其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。
当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名,返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。
如果仅仅返回类型不同,其余均相同,系统会给出错误信息;若仅仅函数名相同,而参数的个数,类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。
纯虚函数和抽象类
纯虚函数是一个在基类中说明的虚函数﹐它在该基类中没有定义,但要求在它的派生类中根据需要对它进行定义,或仍然说明为纯虚函数。
定义如下:
virtual void f()=0;
如果一个类至少有一个纯虚函数,那么这个类就是抽象类。抽象类的使用有以下规则:
- 由于抽象类中至少包含有一个没有定义功能的纯虚函数,因此抽象类只能用作其他类的基类,不能建立抽象类对象。
- 抽象类不能用作参数类型、函数返回类型或显式转换的类型。但可以声明指向抽象类的指针变量,此指针可以指向它的派生类,进而实现多态性。
- 如果在抽象类的派生类中没有重新说明纯虚函数﹐则该函数在派生类中仍然为纯虚函数,而这个派生类仍然还是一个抽象类。
六、模板与异常处理
1.函数模板和模板函数
声明格式:
template <typename/class T>
T max(T a, T b);
2.模板类
声明格式:
template <typename T>
class A;
使用格式:
类模模板名<实际类型> obj;
3.异常处理
C++处理异常的机制是由检查、抛出和捕获3个部分组成,分别由3种语句来完成:try(检查),throw(抛出)和 catch(捕获)。
本文作者:asdio
本文链接:https://www.cnblogs.com/agitm/p/16061305.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步