面向对象(c艹)一个一个记
面向过程程序设计OPP(Oriented Procedural Programming)
将复杂过程简单的按功能分层从而解决问题
编程是面向操作的,编程的单位是函数
规范的过程化程序: 过程的功能划分 / 编写
功能与数据分离
不符合人们对现实世界的认识
要保持功能与数据的相容困难
自顶向下的设计方法
限制了软件的可重用性,
降低开发效率,
软件系统难以维护。
结合在对象中,按对象组织
继承
子类自动共享父类数据和方法的机制,它由类的派生体现。一个子类直接继承父类的全部描述,同时可修改和扩充,继承是对父类的重用机制。
E.G
1 | // 派生类:圆锥体 |
这段代码定义了一个名为Cone
(圆锥体)的派生类,它继承自Circle
(圆形)基类。这种结构体现了面向对象编程中的继承特性,下面详细解释其各部分含义:
1. 类的继承关系
1 | class Cone : public Circle |
class Cone
:声明一个名为Cone
的类(圆锥体)。: public Circle
:表示Cone
是Circle
的公有派生类(public
为继承方式)。
这意味着:Cone
会继承Circle
中所有的非私有成员(包括成员变量和成员函数),可以直接使用基类的功能,同时扩展自己的特性。
(例如:圆形的圆心坐标X,Y
、半径r
等属性,圆锥体也需要,因此无需重复定义,直接继承即可。)
2. 私有成员变量
1 | private: |
private
:访问权限修饰符,标识该部分成员仅能在Cone
类内部使用,外部无法直接访问。double height
:定义了圆锥体特有的成员变量height
(高度),这是Cone
在基类Circle
基础上扩展的属性(圆形没有高度,圆锥有)。
3. 公有成员函数(接口)
1 | public: |
这些是Cone
类对外提供的接口,用于操作和访问类的成员,具体功能如下:
构造函数
Cone(...)
(名称与类名相同,无返回类型,可重载,):
用于初始化圆锥体对象,参数包括:X,Y
(圆心 / 顶点坐标,继承自Circle
)、r
(底面半径,继承自Circle
)、h
(高度,默认值为 1)。
构造函数会先调用基类Circle
的构造函数初始化继承的属性(如X,Y,r
),再初始化自己的height
。可初始化成员变量
1
2
3
4
5
6
7
8class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {} // 初始化列表(推荐,最直观显式)
// 或在函数体中赋值:
// MyClass(int val) { value = val; }
};可调用父类构造函数(继承场景)
在派生类的构造函数中,必须显式调用父类的构造函数(除非父类有默认构造函数(如果类中未定义任何构造函数,编译器会自动生成一个隐式默认构造函数(无参数)。
1
2
3
4
5
6
7
8
9class Base {
public:
Base(int x) {}
};
class Derived : public Base {
public:
Derived(int x, int y) : Base(x) {} // 调用父类构造函数
};
不能被声明为
const
、virtual
或static
- 构造函数不能是
const
(因为它会修改对象状态)。 - 构造函数不能是
virtual
(虚函数依赖于对象的存在,而构造函数正在创建对象)。 - 构造函数不能是
static
(静态函数属于类,而构造函数属于对象)。
委托构造函数(C++11+)
构造函数可以调用同一个类的其他构造函数,避免代码重复。
1
2
3
4
5
6
7class MyClass {
public:
MyClass(int x) : value(x) {}
MyClass() : MyClass(0) {} // 委托给另一个构造函数
private:
int value;
};
setter 和 getter 函数:
setHeight(double h)
:设置圆锥的高度(修改height
的值)。getHeight() const
:返回圆锥的高度(读取height
的值)。
这是封装特性的体现:通过函数间接访问私有变量**(在内部)**,避免外部直接修改,保证数据安全性。
功能计算函数:
calculateArea() const
:计算圆锥的表面积(基类Circle
若有计算圆面积的函数,则此处重写为圆锥的表面积)。calculateVolume() const
:计算圆锥的体积(圆锥特有的功能,基类Circle
没有)。
信息打印函数:
printInfo() const
:打印圆锥的所有信息(如圆心坐标、半径、高度、表面积、体积等),通常会结合继承自基类的信息和自身的信息。
总结:结构的核心意义
- 继承复用:
Cone
通过继承Circle
,直接复用了圆形的属性(如圆心、半径),无需重复定义,减少代码冗余。 - 扩展功能:在继承的基础上,
Cone
增加了自身特有的属性(高度)和方法(体积计算、表面积计算等),实现了 “圆锥是一种特殊的圆形(带高度)” 的逻辑关系。 - 封装接口:通过公有成员函数对外提供访问接口,隐藏内部实现细节(如
height
的存储方式),符合面向对象的封装原则。
这种结构使得代码更具扩展性和维护性,例如未来若需要修改圆形的属性(如增加颜色),圆锥体也能自动继承该特性。这段代码定义了一个名为Cone
的类,它是从Circle
类派生而来的,这意味着Cone
继承了Circle
的属性和方法。这种继承关系形成了面向对象编程中的父子类结构。
代码结构解析:
- 类定义:
class Cone : public Circle
:Cone
类公开继承自Circle
类,因此Cone
可以访问Circle
的公有成员。
- 私有成员变量:
double height;
:圆锥的高度,这是Cone
类特有的属性。
- 构造函数:
Cone(double X, double Y, double r, double h = 1)
:初始化圆锥的位置(继承自Circle
的X
和Y
)、底面半径(继承自Circle
的r
)和高度h
(默认值为 1)。
- 成员函数:
setHeight(double h)
:设置圆锥的高度。getHeight() const
:返回圆锥的高度。calculateArea() const
:计算圆锥的表面积(可能包括底面积和侧面积)。calculateVolume() const
:计算圆锥的体积。printInfo() const
:打印圆锥的信息,可能包括位置、半径、高度、表面积和体积。
const
关键字放在函数声明的后面,其作用是表明这个函数属于常量成员函数。如果写在前面则表示的是其返回值是常量
1. 常量成员函数的功能
- 保护对象状态:常量成员函数不可以对调用它的对象的非静态数据成员进行修改。
- 适配常量对象:只有常量成员函数才能够被常量对象调用。
2. 代码示例与说明
下面是一个包含常量成员函数的类:
1 | class Circle { |
3. 常量对象与函数调用规则
- 常量对象:只能调用常量成员函数。
- 非常量对象:既能调用常量成员函数,也能调用非常量成员函数。
1 | const Circle c1(5.0); // 常量对象,同时进行了初始化 |
4. 技术原理
隐式
this
指针的类型:
- 在常量成员函数里,
this
指针的类型是const ClassName*
。 - 在非常量成员函数中,
this
指针的类型是ClassName*
。
- 在常量成员函数里,
5. 实际应用场景
- 访问器(Getter)函数:通常会被声明为常量成员函数,比如
getRadius()
。 - 不修改对象的计算函数:像
calculateArea()
就属于这类函数。 - 操作符重载:例如
operator==
通常也会被声明为常量成员函数。
6. 注意要点
- 函数重载:常量版本和非常量版本的同一函数可以同时存在。
1 | class MyClass { |
- 可变数据成员(mutable):被
mutable
修饰的数据成员,能够在常量成员函数中被修改。
1 | class Counter { |
继承关系说明:
- 父类(基类):
Circle
类(假设包含圆心坐标X
、Y
和半径r
)。 - 子类(派生类):
Cone
类通过继承获得了Circle
的属性,并添加了自己的属性height
。
多态
在继承体系结构中,同一消息为不同的对象接受时可产生完全不同的行动
利用多态性用户可发送一个通用的信息,而将所有的实现细节都留给接受消
息的对象自行决定
template
template:声明这是一个模板。
typename T:声明一个类型参数T,T可以是任何类型(如int、double、string等)。
typename 也可以用 class 替代(如 template
1 | template<typename T> |
数据结构与数据访问
1 | int *p1 = (int *)malloc(sizeof(int) * length); |
C++函数的新特性
引用
对一个数据可以使用引用(reference)的方式声明,引用的作用是为一个变量起一个别名
1 | int a ; |
在一条语句中声明多个引用时应逐一声明
1 | int& x=a, y = b,z = c ; //error |
声明引用变量必须进行初始化,引用未定义变量称悬挂引用。
将前面声明的引用重新变为另一变量的别名是个逻辑错误.
1 | int y =5, z = 3; |
&在此不是求地址运算,而是起标识作用。
引用声明完毕后,相当于目标变量名有两个名称,
声明一个引用,不是新定义了一个变量,引用本身不占存储单元,系统也不给引用分配存储单元。
引用即用别名引用这个变量,目的是为了消除指针
引用传递的特点
消除了复制大量数据的开销,有利提高执行效率;
在被调用函数中直接使用形参变量,提高可读性;
安全性较差,被调用函数能直接访问和修改调用者的数据。
fun( const T& value);
若要传递较大的对象,用常量引用参数模拟按值调用.
要指定引用常量,在参数声明的类型说明符前面加上const
内联函数inline(以相应代码代替)
C++为降低小程序调用开销的一种机制。
默认参数值 default parameter value
函数参数的默认值使得在函数调用时可不指定参数。
建议性声明:不能含有复杂结构控制语句和递归调用
函数重载
常用于处理不同数据类型而功能类似的同名函数;
函数默认参数
经常需要用相同的参数调用同一函数时,简化函数调用。
当函数调用时,若实参数个数少于形参数的总数时,
则所缺参数自动取函数参数表中设置的缺省值。:
当函数声明时,由右至左指定默认参数的值
1 | int volumn( int length, int width = 1, int highth =1); |
1 | int volumn( int length, int width = 1, int highth =1); |
初识类
封装(encapsulate)
- 把全部属性和全部行为封装在一起,
形成一个不可分割的独立单位(即对象)。 - 信息隐蔽(information hiding)
对象的外部不能直接地存取对象属性,只能通过几个允许外部使用的服务与对象发生联系。 - 对象间通发送消息进行交互.
类是面向对象编程的程序基本单位
程序模块是各种由类构成的
类是逻辑上相关数据和函数的封装
类是对问题的抽象描述
1 | class 类名 { |
成员函数
在类的外部定义成员函数
1 | 返回类型 类名::成员函数名(参数列表) |
在类内直接定义成员函数, 默认创建为内联函数
如果成员函数在类体外定义,要用inline声明为内联函数
域运算符“∷”,成员运算符“.”
在类外定义函数时,应指明成员函数的作用域
在成员函数引用本对象的数据成员时,只需直接写数据成员名,
这时C++系统会把它默认为本对象的数据成员。
保护 protected
除了类本身的成员函数和说明为友元函数或友元类的成员函数可以访问保
护成员外,该类的派生类的成员也可以访问。
private 在首次出现时可以忽略
对象的使用
同类对象之间可以相互赋值
1 | Time tA, tB; |
成员访问运算符“.” 和**“->”(对象指针名->成员名)**
1 | cout << t.hour << pTime ->min(相当于访问所指对象的成员min) << (*pTime).sec; |
软件工程的一个最基本的原则就是将接口与实现分离,信息隐蔽是软件工程中一个非常重要的概念。
自定义类库头文件.h
文件中有用户自行设计的类的定义,包括类的外部接口(公有成员函数的原型)。任何需要使用这些类的源程序,只要在文件中包含这些头文件即可。
1 | //point.h |
1 | //point.cpp |
在面向对象的程序开发中,一般做法是将类的声明放在指定的头文件中,用户如果想用该类,只要把有关的头文件包含进来即可,不必在程序中重复书写类的声明,在程序中就可以用该类来定义对象.为了实现信息隐蔽,对类成员函数的定义一般不放在头文件中,而另外放在一个文件中。
构造函数与析构函数
(自定义)默认构造函数
1 | <类名>::<默认构造函数名>() |
析构函数
构造函数的反函数,析构函数是用于取消对象成员函数,
当一个对象生命期结束时,系统自动调用析构函数。
- 析构函数名字为**符号“~”**加类名;
- 析构函数没有参数和返回值。
- 一个类中只可能定义一个析构函数,
析构函数不能重载。- 析构函数的作用
进行清除对象,释放内存等;
- 析构函数的作用
- 一个类中只可能定义一个析构函数,
- 析构函数没有参数和返回值。
1 | 类名:: |
自动调用
(1) 一个对象当其结束生命周期时 ;
(2) 使用new运算符创建的对象,
在使用delete运算符释放该对象时;
一般析构函数的调用顺序与构造函数相反。
1 | class Rational{ |
一般情况下,如果类中的数据都在栈里,程序员不需要开发自定义的拷贝构造函数
默认拷贝构造函数
1 | A b ( a ) |
对象复制与对象赋值是不同的
静态数据成员的初始化与一般数据成员不同,外部静态数据成员初始化的格式如下:
<类型> <类名>::<静态数据成员> = <值>;
3)在引用静态数据成员时采用格式:
<类名>::<静态数据成员> <对象名>. <静态数据成员>
静态数据成员 vs 全局变量
有了静态数据成员,各对象之间(即不依赖于对象使用)的数据有了沟通的渠道,实现数据共享 。 全局变量破坏了封装的原则,不符合面向对象程序的要求。
公用静态数据成员与全局变量的作用域不同
静态数据成员的作用域只限于定义该类的作用域内
静态成员函数只能访问静态数据成员、静态成员函数和类以外的函数和数据,不能访问类中的非静态数据成员(因为非静态数据成员只有对象存在时才有意义)。但静态数据成员和静态成员函数可由任意访问权限许可的函数访问。和一般成员函数类似,静态成员函数也有访问限制,私有静态成员函数不能由外界访问。静态成员函数没有this指针,因此,静态成员函数只能直接访问类中的静态成员,若要访问类中的非静态成员时,必须借助对象名或指向对象的指针。
1 |
|
static 数据是类共有的,static 函数可以类名调用,也可以对象调用,普通成员函数能访问static数据
const成员变量只能由构造函数通过初始化列表对该数据成员进行初始化
若成员函数不修改对象,则声明为const.
const关键词可以参与区分重载函数。
const 对象只能调用它的const 成员函数,而不能调用其他成员函数。
直接初始化
分配空间的同时进行初始化. 一般数组成员较少.
Box b[3] = {Box(1),Box(1,1),Box(1,1,1)};
间接初始化
先分配空间,之后完成初始化.
Box a[50];//先调用默认
for( int i = 0; i<50;i++){ a[i].set(i, i, i); }
1 | Box box [3] ; |
类的组合
1 | class Point |
构造函数: 必须首先初始化内嵌对象的数据
1 | Circle ::Circle(double r, int x, int y):radius(r), center(x,y){ } |
成员函数: 可以使用内嵌对象调用其函数. 注意访问权限控制!
1 | void setCenter(int x,int y){ |
成员对象的初始化
一个对象如果有**“成员对象”(即它的成员数据不是普通类型,而是“类”类型的),那么在实现构造函数时应对“成员对象”进行初始化**)
方式是在构造函数中增加构造参数,指明“成员对象”构造的方式
若没有“成员对象”构造方式的声明,系统默认调用“成员对象”的无参的构造函数。
组合关系
一个类包含另一个类的对象
描述整体拥有部分的关系,即has-a关系
该类不与其他类共享对象的引用。即**“整体”端重数只能是1**
如果这种类的对象生命周期结束,被包含的对象的生命周期也会结束。
1 | class Textfield; |
依赖关系
描述两个类对象之间短暂的相互作用
依赖关系表示一个类的对象短暂使用了另一个类对象,代表类之间**“uses-a”关系**
1 | class Time{ |
1 | class Printer { // 打印机类 |
Student类的成员不包含打印机Printer的对象或者指针,即二者不具有“拥有has-a’关系。只有学生对象调用usePrinter( )函数时,学生对象与打印机对象才建立关系,并且在该函数执行完毕后,二者关系就结束了。一种短暂的”使用关系”,即“use-a”关系。依赖关系除了被依赖方作为依赖方的函数参数,还可能作为依赖方的函数中的临时对象。
友元
1 |
|
友元 函数 与友元类
友元函数和友元类
可以访问另一个类的私有和保护(稍后更多)成员(区别于组合)
友元函数不是类的成员函数
友元函数在类范围之外定义
友元的特性
友元是**“给予”的,而不是“索要”的**
非对称性(如果 B 是 A 的友元,A 不一定是 B 的友元)
非传递性(如果 A 是 B 的友元,B 是 C 的友元,A 不一定是 C 的友元)
友元的主要用途
提供了一种访问类成员的更方便快捷的途径
为运算符重载的实现提供了更方便的途径
友元可以访问类的任何成员(可不通过成员函数),这破环了类的封装性,因此要谨慎使用友元
有权从类外部更改类的内部状态。 因此推荐使用成员函数而不是友元来改变状态
friend 声明
friend 函数
Keyword friend
friend int myFunction( int x );
声明在类内,保证这个函数可以在类外访问类成员
1 | class Accumulator{ |
friend 类
在类名前加 friend 保证该类可以访问类成员,可以让整个类成为另一个类的友元。 这使友元类的所有成员函数都可以访问其他类的私有成员。友元类的所有函数都是友元函数
也可以不把整个类声明为友元, 仅仅只声明一个或多个函数为另一个类的友元函数. 这类似于声明普通函数成为友元,除了使用包含 className:: 前缀
友元常用于定义重载运算符时。当两个或多个类需要以一种亲密的方式一起工作时,不常使用友元。使一个类成为友元只需要作为前向声明该类存在。 但是,使特定的类的成员函数成为友元则需要首先看到成员函数类的完整声明.
继承(不允许继承循环)
继承的概念
派生类具有基类的特性
共享基类的成员函数
使用基类的数据成员
派生类新增成员(拓展)
定义自己的数据成员
定义独特的成员函数
派生类改造基类
- 重写基类某些成员函数
C++中单继承派生类的定义形式如下:
1 | class 派生类名 : [继承方式] 基类名 |
继承方式包括:
public(公有继承)
private(私有继承,默认)
protected(保护继承)(保护成员在本类与派生类中能直接访问)
C++中多重继承派生类的定义形式如下
1 | class 派生类名: [继承方式] 基类名1, [继承方式] 基类名2,…, |
1 | class Assistant : protected Student, Teacher//对Teacher默认是私有继承 |
继承方式决定了基类成员在派生类中的访问权,
这种访问来自两个方面:
- 派生类中
新增函数成员访问从基类继承来的成员
- 派生类外部
通过派生类的对象访问从基类继承的成员
访问私有继承的成员
1 | class Student { int number; char school[10]; |
1 | class Student { …… }; |
CollegeStudentl类访问基类,Student的能力没有变化,
在私有继承的情况下,通过派生类**对象(并非类内)**无法访问基类的任何成员
1 | class A |
三种继承方式的对比
一般采用不会改变基类成员访问权限的公有继承。
私有继承:
基类的可被继承的成员都成了其直接派生类的私有成员,
无法再进一步派生,
实际上私有继承相当于终止了基类成员的继续派生。
保护继承:
基类的可被继承的成员都成了直接派生类的保护成员,
保护继承保证了最上层基类的成员依然能被继承树中的
次级子类所继承。
访问级别不能升只能降,一层一层来看即可
派生类的构造函数
派生类的构造与析构函数
• 创建派生类对象时调用基类的构造函数来初始化基类数据。
• 执行派生类的析构函数时,基类的析构函数也将被调用。
派生类构造函数的定义方式:
1 | 派生类名(参数总表):基类名(基类构造函数参数表1), 对象成员(参数表2) |
1 | class Base1 { public: Base1() { cout << "Base1" << endl; } }; |
1. 初始化顺序的决定因素
- 基类:按照派生类定义时基类的声明顺序初始化(无论初始化列表中如何排列)。
- 成员变量:按照成员在类中声明的顺序初始化(与初始化列表顺序无关)。
- 派生类自身:最后执行派生类构造函数的函数体。
1 | class Base1 { public: Base1() { cout << "Base1" << endl; } }; |
1 | Base2 |
为什么初始化顺序固定?
- 成员变量依赖:若成员变量的初始化依赖于其他成员的顺序,固定顺序可避免潜在错误。
- 基类依赖:若基类的初始化顺序被用户随意调整,可能导致基类未完全初始化就被使用。
最佳实践
- 保持初始化列表顺序与声明顺序一致:提高代码可读性,避免混淆。
- 避免成员间的初始化依赖:若必须依赖,通过构造函数体或成员函数处理。
**构造函数执行顺序:**基类->派生类中对象成员->派生类
派生类构造函数的几点说明
1)派生类构造函数的定义中可省略对基类构造函数的调用其条件是在基类中必须有默认的构造函数或者根本没有定义构造函数。
2)当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。
继承中的同名成员访问
多重继承时不同基类成员同名也可以用**类名限定符“::”**来解决。
1 | class Student |
继承时的同名成员隐藏规则
派生类定义了与基类相同的成员,此时基类的同名成员在派生类内不可见,派生类成员隐藏了同名的基类成员.
基类成员与派生类成员同名,可以通过类名限定符“::”来解决。其语法为:类名 : : 成员
1 | class Student : public Person{ // 派生类 |
继承中成员同名有两种情况:
1.基类成员与派生类成员同名
2.多重继承时不同基类成员同名
1 | class Person |
类族中的赋值兼容
公有继承时,一个派生类的对象可用于基类对象适用的地方,需要基类对象的任何地方都可以使用派生类对象替代。
赋值兼容规则有三种情况:
(1)派生类的对象可以赋值给基类的对象。
base Obj = derived Obj;
(2)派生类的对象可以初始化基类的引用。
base& base_Obj = derived_obj;
(3)派生类的对象的地址可以赋给指向基类的指针。
base *pBase = &derived_obj;
多态
指同样的消息被不同类型的对象接收时,产生不同行为的现象。(同一名字,多种语义;同个接口,多种方法)
静态多态的概念
在程序编译时系统就能够确定要调用的是哪个函数,也被称为编译时多态。
函数重载
运算符重载
函数重载注意事项
不能仅靠函数的返回值来区别重载函数,必须从形式参数上区别开来。
派生类中的同名成员函数
• 使用 :: 加以区分
• 使用对象加以区分
1 | MonkyKong sun; |
1 | class B :public A |
动态多态性
指程序在编译时并不能确定要调用的函数,直到运行时系统才能动态地确定操作所针对的具体对象,它又被称为运行时多态
动态多态是通过虚函数(virtual function)实现。
1 | class Base |
虚函数
C++中的虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或者基类引用来访问这个同名函数(最重要区别)。虚函数成员声明的语法为:
1.virtual只能使用在类定义的函数原型声明中,
不能在成员函数实现的时候使用,也不能用来限定类外的普通函数。
2.用virtual声明类的非静态的成员函数,只用于类的继承层次结构中。
不能将类外的普通函数(友员)和静态成员函数声明成虚函数。
virtual具有继承性
在派生类中重新定义虚函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数完全相同。
否则不能实现多态性, 为函数重载.
虚函数是在基类中冠以关键字 virtual 的非静态成员函数。
继承体系:判断成员函数所在的类是否会作为基类;虚函数为类族提供了一种公共接口。
重写函数:该函数在类被继承后有无可能被更改功能;允许在派生类中对基类的虚函数重新定义
调用形式:是否通过基类指针或引用调用该虚函数;赋值兼容性原则
1 | class Follower // 徒弟类 |
虚析构函数
构造函数不能是虚函数
建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数可以是虚函数
虚析构函数用于指引 delete 运算符正确析构动态对象
当基类的析构函数为虚函数时,无论指针指的是同一类族的哪一个类对象,对象撤销时,系统会采用动态关联,调用相应的析构函数,完成该对象的清理工作。
习惯把析构函数声明为虚函数,即使基类并不需要析构函数,以确保撤销动态存储空间时能够得到正确的处理。
1 | class Base |
实现动态多态
• 基类声明虚函数
• 派生类重写虚函数
• 基类指针或引用调用
在许多情况下,在基类中不能给出有意义的虚函数定义,这时可把它说明成纯虚函数,把它的定义留给派生类来做。
定义纯虚函数的一般形式为:
1 | class 类名{ |
①纯虚函数没有函数体;
②最后面的“=0” 不表示函数返回值为0
③这是一个声明语句。
纯虚函数的作用
在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义, 否则无法实现多态性。
抽象类的概念
如果一个类中至少有一个纯虚函数,那么这个类被成为抽象类(abstract class**)**。
抽象类必须用作派生其他类的基类,不能作为返回或参数类型,可使用指向抽象类的指针支持运行时多态性。而不能用于直接创建对象实例。
1 | p->getArea(); (*p).draw( );//作指针做对象时的不同写法以区分 |
派生类中应重写基类中的纯虚函数,否则派生类仍将被看作一个抽象类。
1 | void test1(TwoDimensionalShape & t){ |
如果二维图形又派生出了椭圆形,用于测试的test函数需要需改吗?这有何意义?
- test 函数是否需要修改?
不需要修改。
若椭圆形(Ellipse)
是TwoDimensionalShape
的派生类,且正确重写了基类中的show()
、draw()
、getArea()
虚函数(假设这三个函数在TwoDimensionalShape
中是虚函数),则test1
和test2
函数可以直接接收Ellipse
对象(或指针 / 引用)并正确调用派生类的实现。 - 意义:
这体现了面向对象的多态性,具体意义如下:- 代码复用性:新增派生类(如椭圆形)时,无需修改已有的
test1
、test2
等通用函数,只需保证派生类遵循基类的接口规范(重写虚函数),即可直接使用这些函数进行测试。 - 扩展性:系统可以轻松扩展新的二维图形类型(如椭圆形、三角形等),而不影响原有代码的逻辑,符合 “开闭原则”(对扩展开放,对修改关闭)。
- 接口统一性:通过基类的引用或指针调用派生类的方法,屏蔽了不同派生类的实现差异,使代码更简洁、通用,降低了模块间的耦合度。
- 代码复用性:新增派生类(如椭圆形)时,无需修改已有的
1 | class TwoDimensionalShape { |
override
是一个关键字,用于显式声明派生类中的成员函数重写(覆盖) 了基类中的虚函数(virtual
函数)。它的主要作用是增强代码的可读性和安全性。
(3) 在类的层次结构中,顶层或最上面的几层可以是抽象基类。
抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
为什么引⼊多态
利⽤多态性可以设计和实现⼀个易于扩展的系统。增强代码的通⽤性
区别
对比维度 | 重载(Overload) | 多态(Polymorphism) |
---|---|---|
定义 | 同一作用域内,多个函数名相同但参数列表(参数类型、个数、顺序)不同的函数。 | 基类与派生类中,派生类重写(override )基类的虚函数,通过基类指针 / 引用调用时,**根据对象实际类型执行(多种对象对应执行)**对应函数。 |
实现阶段 | 编译时确定(静态多态)。编译器根据函数参数列表匹配对应的函数。 | 运行时确定(动态多态)。程序运行时根据对象实际类型调用对应的函数。 |
作用范围 | 同一类中(或全局函数),函数名相同但参数不同(不完全等同于重写,属于新建,仅名字相同)。 | 继承关系中,基类与派生类之间,函数名、参数列表、返回值完全相同(相同信息重写虚函数带来的不同处理响应)。 |
核心依赖 | 函数参数列表的差异(与返回值无关)。 | 基类虚函数、派生类重写、基类指针 / 引用能指向相应派生类对象并执行相应的函数。 |
二、联系
- 都是代码复用的手段
- 重载允许同一功能(函数名)适配不同参数,避免为相似功能起不同名字(如
printInt
、printDouble
)。 - 多态允许通过统一接口(基类函数)操作不同派生类对象,简化代码逻辑(如用
Shape*
统一管理圆形、方形等)。
- 重载允许同一功能(函数名)适配不同参数,避免为相似功能起不同名字(如
- 都体现 “一个接口,多种实现” 的思想
- 重载:同一函数名对应多种参数组合的实现。
- 多态:同一虚函数接口(基类)对应派生类的多种重写实现。
- 都依赖编译器的处理
- 重载依赖编译器在编译时根据参数匹配函数(静态绑定)。
- 多态依赖编译器对虚函数表的处理,实现运行时动态绑定。
三、总结
- 重载是 “横向” 的函数扩展(同一类内,同名不同参),解决同一功能的不同参数适配问题,属于静态多态。
- 多态是 “纵向” 的函数扩展(继承体系中,重写虚函数),解决不同派生类对象的统一接口调用问题,属于动态多态。
模板
模板可以实现类型参数化(包括新类型),C++模板包括 函数模板和类模板两种类型**。**
函数模板就解决函数重载中多次定义函数的问题。
类模板就是对一批仅仅成员数据类型不同的类的抽象。
泛型编程(generic programming)
模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
1 | int square ( int x ) |
通过模板可以产生类或函数的集合,使它们操作不同的数据类型,避免需要为每种数据类型产生一个单独的类或函数。
1 | template <class 类型参数名1 ,class 类型参数 2,…> |
关键字class也可以使用关键字typename;
在template语句与函数模板定义语句<返回类型>之间不允许有别的语句
函数模板允许使用多个类型参数,但在template定义部分的每个形参前必须有关键字typename或class,
函数形式参数表中可以使用模板类型参数,也可以使用一般类型参数.
模板参数说明的每个类型参数必须在函数定义形参表中至少出现一次;
类模板主要用于数据存储(容器)类。表示和算法不受所包含的元素类型的影响。
一个类模板在类层次结构中
既可以是基类也可以是派生类:
Ø 类模板可以从模板类派生
Ø 类模板可以从非模板类派生
Ø 模板类可以从类模板派生
Ø 非模板类可以从类模板派生
EasyX 基础
- 在项目中创建一个.cpp源文件(右键源文件 -> 添加 -> 新建项 -> 设置文件名 first.cpp)
EasyX 基本概念
绘图窗口与设备
initgraph 函数用于初始化绘图窗口
1 | HWND initgraph( |
示例1:创建禁用最小化和关闭按钮的绘图窗口
1 | initgraph(800, 600, EX_NOMINIMIZE | EX_NOCLOSE); |
示例2:窗口开启 EX_SHOWCONSOLE 模式,可以进行代码调试
1 | initgraph(800, 600, EX_SHOWCONSOLE); // 带控制台的图形窗口 |
在 EasyX 中,设备分两种,一种是默认的绘图窗口,另一种是 IMAGE 对象。
通过 SetWorkingImage 函数可以设置当前用于绘图的设备。设置当前用于绘图的设备后,所有的绘图函数都会绘制在该设备上。
坐标
在 EasyX 中,坐标分两种:物理坐标和逻辑坐标。
- 物理坐标
物理坐标是描述设备的坐标体系。
坐标原点在设备的左上角,X 轴向右为正,Y 轴向下为正(特点),度量单位是像素(Pixel)。
坐标原点、坐标轴方向、缩放比例都不能改变。
- 逻辑坐标
逻辑坐标是在程序中用于绘图的坐标体系。
坐标默认的原点在窗口的左上角,X 轴向右为正,Y 轴向下为正,度量单位是点。
默认情况下,逻辑坐标与物理坐标是一一对应的,一个逻辑点等于一个物理像素。
在 EasyX 中,凡是没有特殊注明的坐标,均指逻辑坐标。
坐标相关函数
函数用法 | 函数说明 |
---|---|
void setorigin ( int x, int y ) | 用于设置坐标原点。 |
void setaspectratio ( float xasp, float yasp ) | 通过设置 x 和 y 方向上的缩放因子,从而修改绘图的缩放比例或坐标轴方向。 |
范例:
1 | #include <graphics.h> |
颜色
EasyX 使用 24bit 真彩色,有四种表示颜色的方法:https://docs.easyx.cn/zh-cn/color,通过 setlinecolor 函数可以设置线条颜色
用预定义常量表示颜色( 常量名要大写 )
用16进制数字表示颜色( 0xBBGGRR ),注意颜色的顺序与RGB宏相反
用 RGB 宏合成颜色( RGB(RRGGBB) )
用 HSLtoRGB、HSVtoRGB 转换其他色彩模型到 RGB 颜色
范例:
1 | #include <graphics.h> |
EasyX 图形绘制函数(33个)
https://docs.easyx.cn/zh-cn/drawing-func
函数用法 | 函数说明 |
---|---|
void circle ( int x, int y, int radius ) | 画无填充的圆 |
fillcircle | 画有边框的填充圆 |
solidcircle | 画无边框的填充圆 |
clearcircle | 用当前背景色清空圆形区域 |
void ellipse ( int left, int top, int right, int bottom ) | 画无填充的椭圆 |
fillellipse | 画有边框的填充椭圆 |
solidellipse | 画无边框的填充椭圆 |
clearellipse | 用当前背景色清空椭圆区域 |
void pie ( int left, int top, int right, int bottom, double stangle, double endangle ); | 画无填充的扇形 |
fillpie | 画有边框的填充扇形 |
solidpie | 画无边框的填充扇形 |
clearpie | 用当前背景色清空扇形区域 |
void rectangle ( int left, int top, int right, int bottom ) | 画无填充的矩形 |
fillrectangle | 画有边框的填充矩形 |
solidrectangle | 画无边框的填充矩形 |
clearrectangle | 用当前背景色清空矩形区域 |
void roundrect ( int left, int top, int right, int bottom, int ellipsewidth, int ellipseheight ) | 画无填充的圆角矩形 |
fillroundrect | 画有边框的填充圆角矩形 |
solidroundrect | 画无边框的填充圆角矩形 |
clearroundrect | 用当前背景色清空圆角矩形区域 |
void polygon ( const POINT *points, int num ); | 画无填充的多边形 |
fillpolygon | 画有边框的填充多边形 |
solidpolygon | 画无边框的填充多边形 |
clearpolygon | 用当前背景色清空多边形区域 |
void putpixel ( int x, int y, COLORREF color ) | 画点 |
void line ( int x1, int y1, int x2, int y2 ) | 画直线 |
void arc ( int left, int top, int right, int bottom, double stangle, double endangle ) | 画椭圆弧 |
void polyline ( const POINT *points, int num ) | 画多条连续的直线 |
void polybezier ( const POINT *points, int num ) | 画三次方贝塞尔曲线 |
void floodfill ( int x, int y, COLORREF color, int filltype = FLOODFILLBORDER ) | 填充区域 |
COLORREF getpixel ( int x, int y ) | 获取坐标点的颜色 |
int getwidth ( ) | 获取绘图区的宽度 |
int getheight ( ) | 获取绘图区的高度 |
双缓冲绘图
双缓冲绘图通过在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,从而减少对屏幕的直接绘图操作,提高绘图效率、消除屏幕闪烁,广泛应用于游戏开发、图形界面等领域。
函数用法 | 函数说明 |
---|---|
void BeginBatchDraw () | 开始批量绘图 |
void EndBatchDraw () void EndBatchDraw ( int left, int top, int right, int bottom ) // 指定区域 |
结束批量绘制,并执行(指定区域内)未完成的绘制任务 |
void FlushBatchDraw () void FlushBatchDraw ( int left, int top, int right, int bottom ) // 指定区域 |
执行(指定区域内)未完成的绘制任务 |
https://docs.easyx.cn/zh-cn/other-func
自动移动的圆(帧数控制)
1 | //include <windows.h> |
GetTickCount 是一个 Windows 系统函数,用于获取从操作系统启动以来所经过的毫秒数,通过在代码中的不同位置调用该函数,并计算两次调用之间的差值,可以得知某段代码或某个操作的执行时间。
注:GetTickCount 的值会在系统启动后约49.7天((2^32-1) ms)后回绕到0,这是因为其返回值是一个32位无符号整数,可以使用 GetTickCount64 代替,需添加 windows.h 头文件。
EasyX 进阶
图像处理
https://docs.easyx.cn/zh-cn/image-func
函数用法 | 函数说明 |
---|---|
void loadimage ( IMAGE* pDstImg, // 保存图像的 IMAGE 对象指针 LPCTSTR pImgFile, // 图片文件名 int nWidth = 0, // 图片的拉伸宽度 int nHeight = 0, // 图片的拉伸高度 bool bResize = false //是否调整IMAGE的大小以适应图片 ) |
从文件中读取图像。如果pDstImg为NULL,则读取到绘图窗口 |
void putimage ( int dstX, // 绘制位置的 x 坐标 int dstY, // 绘制位置的 y 坐标 IMAGE *pSrcImg, // 要绘制的 IMAGE 对象指针 DWORD dwRop = SRCCOPY // 三元光栅操作码 ); |
在当前设备上绘制指定图像 |
void putimage ( int dstX, // 绘制位置的 x 坐标 int dstY, // 绘制位置的 y 坐标 int dstWidth, // 绘制的宽度 int dstHeight, // 绘制的高度 IMAGE *pSrcImg, // 要绘制的 IMAGE 对象指针 int srcX, // 绘制内容在 IMAGE 对象中的左上角 x 坐标 int srcY, // 绘制内容在 IMAGE 对象中的左上角 y 坐标 DWORD dwRop = SRCCOPY // 三元光栅操作码 ) |
在当前设备上绘制指定图像(指定宽高和起始位置) |
void Resize ( IMAGE* pImg, int width, int height ) | 调整指定绘图设备的尺寸,pImg 如果为 NULL 表示默认绘图窗口 |
void rotateimage ( IMAGE *dstimg, IMAGE *srcimg, double radian, COLORREF bkcolor = BLACK, bool autosize = false, bool highquality = true ) |
旋转 IMAGE 中的绘图内容 |
void saveimage ( LPCTSTR strFileName, IMAGE* pImg = NULL ) |
保存绘图内容至图片文件,支持 bmp / gif / jpg / png / tif 格式 |
void SetWorkingImage ( IMAGE* pImg = NULL ) | 设定当前的绘图设备,如果参数为 NULL,表示绘图设备为默认绘图窗口 |
IMAGE* **GetWorkingImage **() | 获取当前的绘图设备,如果返回值为 NULL,表示当前绘图设备为绘图窗口 |
void getimage ( IMAGE* pDstImg, // 保存图像的 IMAGE 对象指针 int srcX, // 要获取图像区域左上角 x 坐标 int srcY, // 要获取图像区域的左上角 y 坐标 int srcWidth, // 要获取图像区域的宽度 int srcHeight // 要获取图像区域的高度 ) |
从当前绘图设备中获取图像 |
DWORD* GetImageBuffer ( IMAGE* pImg = NULL ) | 获取绘图设备的显示缓冲区指针,pImg 如果为 NULL,表示默认的绘图窗口 |
HDC GetImageHDC ( IMAGE* pImg = NULL ) | 获取绘图设备句柄(HDC) |
IMAGE 类
1 | class IMAGE(int width = 0, int height = 0); |
在内存中保存图像信息。
loadimage 函数
范例1:loadimage 直接读取图片至绘图窗口
1 |
|
注:修改窗口大小,可以显示图片部分内容,但只能从绘图窗口的坐标原点(左上角)开始显示图片
范例2:loadimage 直接读取图片至绘图窗口并进行图片或窗口缩放
1 |
|
注1:图片缩放后的尺寸小于窗口尺寸,则窗口会有黑边;若大于窗口尺寸,则图片显示不全
注2:第五个参数若为 true,则会调整窗口以适应图片的大小
注3:从磁盘中读取大量图片显示的情况下,使用 loadimage 直接读取图片至绘图窗口性能较差
范例3:loadimage 读取本地图片文件,输出图片宽度和高度
1 | #include <graphics.h> |
注:本例中的图片内容不会在窗口内显示
putimage 函数
范例1:putimage 在绘图窗口显示图像
1 |
|
范例2:putimage 截取图像部分内容进行显示
1 |
|
范例3:putimage 三元光栅操作码
1 |
|
注:putimage 第四个参数是 三元光栅操作码 ,它定义了源图像与目标图像的位合并形式,默认值为 SRCCOPY 详见
https://docs.easyx.cn/zh-cn/putimage
透明贴图
范例1:通过PS制作原图的掩码图和前景图,再进行三元光栅操作叠加而成
1 |
|
范例2:TransparentBlt 函数实现
1 |
|
TransparentBlt 是 Windows GDI(Graphics Device Interface)中的一个函数,用于在绘制位图时支持透明效果。
函数说明:第1个参数为目标设备,第2、3个参数是输出目标矩形左上角坐标,第4、5个参数是目标矩形的宽和高,参数6-10与1-5类似,第11个参数是透明底色(若图片是透明图片,默认为BLACK)
注:此方法只支持 PNG 格式的图片
范例3:AlphaBlend 函数实现(推荐)
1 |
|
AlphaBlend 是 Windows GDI 中用于实现 Alpha 混合(透明/半透明) 绘制的函数,比 TransparentBlt 更强大,支持 逐像素透明度(Alpha 通道) 和 整体透明度(全局 Alpha)。
注:此方法只支持 PNG 格式的图片
图片动画
图片动画的核心是一系列静态的图像(动画帧)。每一帧都是一张静态的图片,但它们之间略有不同,通常表现为物体的位置、形状或颜色的微小变化。这些帧按照特定的顺序排列,并以一定的速度连续播放,使得观者感受到运动的效果。
范例:角色动画
1 |
|
Resize 函数
GetImageBuffer 函数
范例1:GetImageBuffer 通过直接操作显示缓冲区绘制渐变的蓝色
1 |
|
范例2:图像翻转
1 |
|
消息处理
https://docs.easyx.cn/zh-cn/msg-func
消息缓冲区可以缓冲 63 个未处理的消息。每次获取消息时,将从消息缓冲区取出一个最早发生的消息。
函数用法 | 函数说明 |
---|---|
ExMessage getmessage ( BYTE filter = -1 ) void getmessage ( ExMessage *msg, BYTE filter = -1 ) |
从消息缓冲区获取一个消息。如果缓冲区中没有消息,则程序会一直等待(阻塞式) |
bool peekmessage ( ExMessage *msg, BYTE filter = -1, bool removemsg = true) | 从消息缓冲区获取一个消息,并立即返回 |
void flushmessage ( BYTE filter = -1 ) | 清空消息缓冲区 |
参数说明:
- msg:指向消息结构体 ExMessage 的指针,用来保存获取到的消息。
- filter:指定要获取的消息范围,默认 -1 获取所有类别的消息。可以用以下值或值的组合获取指定类别的消息
标志 | 描述 |
---|---|
EX_MOUSE | 鼠标消息。 |
EX_KEY | 按键消息。 |
EX_CHAR | 字符消息。 |
EX_WINDOW | 窗口消息。 |
- removemsg:在 peekmessage 处理完消息后,是否将其从消息队列中移除。
ExMessage 结构体
https://docs.easyx.cn/zh-cn/exmessage
1 | struct ExMessage |
message :可以分为四大类:EX_MOUSE(鼠标11项)、EX_KEY(键盘2项)、EX_CHAR(字符1项)、EX_WINDOW(窗口3项)
union :共用体中存储具体消息的数据
鼠标消息
范例:跟随鼠标移动的圆
1 |
|
键盘消息
范例1:用键盘控制小球
1 |
|
虚拟键代码 https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
优化后
1 |
|
其它函数
设置窗口标题
范例:使用 GetHWnd 和 SetWindowText 函数设置窗口标题
1 |
|
弹窗消息
在Visual C++(VC)中,MessageBox 函数是一个常用的 Windows API 函数,用于显示一个模态对话框,其中包含文本、标题、图标和按钮等。以下是函数的详细用法:
1 | int MessageBox( |
参数说明
- hWnd:指定消息框的父窗口句柄。如果此参数为NULL,则消息框没有父窗口,且作为顶级窗口显示。
- lpText:要在消息框中显示的文本。
- lpCaption:消息框的标题。如果此参数为NULL,则默认标题为“Error”。
- uType:用于指定消息框的内容和行为的标志。这可以是一个或多个以下常量的组合:
- MB_OK:消息框包含一个“确定”按钮。
- MB_OKCANCEL:消息框包含“确定”和“取消”按钮。
- MB_YESNO:消息框包含“是”和“否”按钮。
- MB_YESNOCANCEL:消息框包含“是”、“否”和“取消”按钮。
- MB_ICONEXCLAMATION、MB_ICONWARNING、MB_ICONINFORMATION、MB_ICONQUESTION、MB_ICONERROR等:用于指定消息框中显示的图标。
返回值
函数返回一个整数值,表示用户点击的按钮。例如:
- IDOK:用户点击了“确定”按钮。
- IDCANCEL:用户点击了“取消”按钮。
- IDYES:用户点击了“是”按钮。
- IDNO:用户点击了“否”按钮。
1 |
|
注:在使用 MessageBox 函数之前,需要包含 windows.h 头文件(如果已经包含了 graphics.h 头文件则可以省略)
播放音频
mciSendString 是 Windows API 中的一个函数,用于向媒体控制接口(Media Control Interface,MCI)设备发送命令字符串。这个函数常用于控制多媒体设备,如音频和视频播放,支持 MPEG, AVI, WAV, MP3 等多种格式。
范例:播放背景音乐
1 |
|