面向过程程序设计OPP(Oriented Procedural Programming)

将复杂过程简单的按功能分层从而解决问题
编程是面向操作的,编程的单位是函数
规范的过程化程序: 过程的功能划分 / 编写

功能与数据分离

不符合人们对现实世界的认识
要保持功能与数据的相容困难

自顶向下的设计方法

限制了软件的可重用性,
降低开发效率,
软件系统难以维护。

4e9f2fabd86809a8bfd9e32c2ea93a51

结合在对象中,按对象组织

继承

子类自动共享父类数据和方法的机制,它由类的派生体现。一个子类直接继承父类的全部描述,同时可修改和扩充,继承是对父类的重用机制。

E.G

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 派生类:圆锥体
class Cone : public Circle
{
private:
double height; // 圆锥高度
public:
Cone(double X , double Y , double r , double h = 1)
void setHeight(double h) ;
double getHeight() const ;
double calculateArea() const ;
double calculateVolume() const
void printInfo() const ;
……
};

这段代码定义了一个名为Cone(圆锥体)的派生类,它继承自Circle(圆形)基类。这种结构体现了面向对象编程中的继承特性,下面详细解释其各部分含义:

1. 类的继承关系

1
class Cone : public Circle
  • class Cone:声明一个名为Cone的类(圆锥体)。
  • : public Circle:表示ConeCircle公有派生类public继承方式)。
    这意味着:Cone会继承Circle所有的非私有成员(包括成员变量和成员函数),可以直接使用基类的功能,同时扩展自己的特性。
    (例如:圆形的圆心坐标X,Y、半径r等属性,圆锥体也需要,因此无需重复定义,直接继承即可。)

2. 私有成员变量

1
2
private:
double height; // 圆锥高度
  • private:访问权限修饰符,标识该部分成员仅能在Cone类内部使用,外部无法直接访问。
  • double height:定义了圆锥体特有的成员变量height(高度),这是Cone在基类Circle基础上扩展的属性(圆形没有高度,圆锥有)。

3. 公有成员函数(接口)

1
2
3
4
5
6
7
8
public:
Cone(double X , double Y , double r , double h = 1); // 构造函数
void setHeight(double h); // 设置高度
double getHeight() const; // 获取高度
double calculateArea() const; // 计算表面积
double calculateVolume() const; // 计算体积
void printInfo() const; // 打印信息
……

这些是Cone对外提供的接口,用于操作和访问类的成员,具体功能如下:

  • 构造函数 Cone(...)(名称与类名相同,无返回类型,可重载,):
    用于初始化圆锥体对象,参数包括:X,Y(圆心 / 顶点坐标,继承自Circle)、r(底面半径,继承自Circle)、h(高度,默认值为 1)。
    构造函数会先调用基类Circle的构造函数初始化继承的属性(如X,Y,r),再初始化自己的height

    可初始化成员变量
    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass {
    private:
    int value;
    public:
    MyClass(int val) : value(val) {} // 初始化列表(推荐,最直观显式)
    // 或在函数体中赋值:
    // MyClass(int val) { value = val; }
    };
    可调用父类构造函数(继承场景)
    • 在派生类的构造函数中,必须显式调用父类的构造函数(除非父类有默认构造函数(如果类中未定义任何构造函数,编译器会自动生成一个隐式默认构造函数(无参数)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Base {
      public:
      Base(int x) {}
      };

      class Derived : public Base {
      public:
      Derived(int x, int y) : Base(x) {} // 调用父类构造函数
      };
    不能被声明为constvirtualstatic
    • 构造函数不能是const(因为它会修改对象状态)。
    • 构造函数不能是virtual(虚函数依赖于对象的存在,而构造函数正在创建对象)。
    • 构造函数不能是static静态函数属于类,而构造函数属于对象)。
    委托构造函数(C++11+)
    • 构造函数可以调用同一个类的其他构造函数,避免代码重复。

      1
      2
      3
      4
      5
      6
      7
      class 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的属性和方法。这种继承关系形成了面向对象编程中的父子类结构

代码结构解析:

  1. 类定义
    • class Cone : public CircleCone类公开继承自Circle类,因此Cone可以访问Circle的公有成员。
  2. 私有成员变量
    • double height;:圆锥的高度,这是Cone类特有的属性。
  3. 构造函数
    • Cone(double X, double Y, double r, double h = 1):初始化圆锥的位置(继承自CircleXY)、底面半径(继承自Circler)和高度h(默认值为 1)。
  4. 成员函数
    • setHeight(double h):设置圆锥的高度。
    • getHeight() const:返回圆锥的高度。
    • calculateArea() const:计算圆锥的表面积(可能包括底面积和侧面积)。
    • calculateVolume() const:计算圆锥的体积。
    • printInfo() const:打印圆锥的信息,可能包括位置、半径、高度、表面积和体积。

const 关键字放在函数声明的后面,其作用是表明这个函数属于常量成员函数。如果写在前面则表示的是其返回值是常量

1. 常量成员函数的功能

  • 保护对象状态:常量成员函数不可以对调用它的对象的非静态数据成员进行修改。
  • 适配常量对象只有常量成员函数才能够被常量对象调用。

2. 代码示例与说明

下面是一个包含常量成员函数的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r) {}

// 常量成员函数:不能修改对象状态
double getRadius() const {
return radius; // 允许,因为没有修改成员变量
}

// 常量成员函数:计算圆的面积
double calculateArea() const {
// radius = 10.0; // 错误!不可以修改成员变量
return 3.14 * radius * radius;
}

// 非常量成员函数:可以修改对象状态
void setRadius(double r) {
radius = r; // 允许,因为这不是常量成员函数
}
};

3. 常量对象与函数调用规则

  • 常量对象只能调用常量成员函数。
  • 非常量对象既能调用常量成员函数,也能调用非常量成员函数
1
2
3
4
5
6
7
const Circle c1(5.0); // 常量对象,同时进行了初始化
double area = c1.calculateArea(); // 正确,calculateArea是常量成员函数
// c1.setRadius(10.0); // 错误,常量对象不能调用非常量成员函数

Circle c2(3.0); // 非常量对象
c2.setRadius(10.0); // 正确
area = c2.calculateArea(); // 正确

4. 技术原理

  • 隐式this指针的类型

    • 在常量成员函数里,this 指针的类型是 const ClassName*
    • 在非常量成员函数中,this 指针的类型是 ClassName*

5. 实际应用场景

  • 访问器(Getter)函数:通常会被声明为常量成员函数,比如 getRadius()
  • 不修改对象的计算函数:像 calculateArea() 就属于这类函数。
  • 操作符重载:例如 operator== 通常也会被声明为常量成员函数。

6. 注意要点

  • 函数重载:常量版本和非常量版本的同一函数可以同时存在。
1
2
3
4
5
6
7
8
class MyClass {
public:
const char* getData() const { return constData; } // 常量版本
char* getData() { return data; } // 非常量版本
private:
char* data;
const char* constData;
};
  • 可变数据成员(mutable):被 mutable 修饰的数据成员,能够在常量成员函数中被修改。
1
2
3
4
5
6
7
8
class Counter {
private:
mutable int accessCount; // 可变数据成员
public:
void doSomething() const {
accessCount++; // 允许,因为accessCount是mutable的
}
};

继承关系说明:

  • 父类(基类)Circle类(假设包含圆心坐标XY和半径r)。
  • 子类(派生类)Cone类通过继承获得了Circle的属性,并添加了自己的属性height

多态

继承体系结构中,同一消息为不同的对象接受时可产生完全不同的行动

利用多态性用户可发送一个通用的信息,而将所有的实现细节都留给接受消
息的对象自行决定

template

template:声明这是一个模板。
typename T:声明一个类型参数T,T可以是任何类型(如int、double、string等)。
typename 也可以用 class 替代(如 template),两者在模板中含义相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<typename T>
class Stack {
private:
T buffer[100]; // 假设栈大小为100
int top; // 栈顶位置

public:
Stack() : top(-1) {} // 初始化栈顶为-1,表示空栈

bool push(const T& x) {
if (top >= 99) return false; // 栈满
buffer[++top] = x;
return true;
}

T pop() {
if (top < 0) throw "Stack is empty!"; // 栈空
return buffer[top--];
}
};
Stack<int> intStack; // 存储int的栈
Stack<double> doubleStack; // 存储double的栈

intStack.push(10);
doubleStack.push(3.14);

数据结构与数据访问

1
2
3
4
5
int *p1 = (int *)malloc(sizeof(int) * length); 
free(p1);
int *p2 = new int [length] ;
delete [ ]p2; // 释放数组用法
delete p2; // 释放单个元素

C++函数的新特性

引用

对一个数据可以使用引用(reference)的方式声明,引用的作用是为一个变量起一个别名

1
2
3
int a ; 
int &b = a; // 声明b是int a的引用
b = 20; // a = 20

在一条语句中声明多个引用时应逐一声明

1
2
int& x=a, y = b,z = c ; //error
int &x=a, &y=b, &z=c;

声明引用变量必须进行初始化,引用未定义变量称悬挂引用。
将前面声明的引用重新变为另一变量的别名是个逻辑错误.

1
2
int y =5, z = 3; 
int &x = z, &x = y

&在此不是求地址运算,而是起标识作用。

引用声明完毕后,相当于目标变量名有两个名称

声明一个引用,不是新定义了一个变量,引用本身不占存储单元,系统也不给引用分配存储单元。

引用即用别名引用这个变量,目的是为了消除指针

引用传递的特点

消除了复制大量数据的开销,有利提高执行效率;
在被调用函数中直接使用形参变量,提高可读性;
安全性较差,被调用函数能直接访问和修改调用者的数据。
fun( const T& value);
若要传递较大的对象,用常量引用参数模拟按值调用.
要指定引用常量,在参数声明的类型说明符前面加上const

内联函数inline(以相应代码代替)

C++为降低小程序调用开销的一种机制。
默认参数值 default parameter value
函数参数的默认值使得在函数调用时可不指定参数。

建议性声明:不能含有复杂结构控制语句和递归调用

函数重载

常用于处理不同数据类型功能类似同名函数;

函数默认参数

经常需要用相同的参数调用同一函数时,简化函数调用。
当函数调用时,若实参数个数少于形参数的总数时,
则所缺参数自动取函数参数表中设置的缺省值。:
当函数声明时,由右至左指定默认参数的值

1
2
3
4
int volumn( int length, int width = 1, int highth =1)
volumn(2);
volumn(2,2);
volumn(2, ,2);//中间参数默认
1
2
3
4
5
6
int volumn( int length, int width = 1, int highth =1)
int volumn( int length, int width )
int volumn( int length)
volumn(1);//有二义性,回出错
volumn(2,3);
volumn(1,2 ,3);

初识类

封装(encapsulate)

  • 把全部属性和全部行为封装在一起,
    形成一个不可分割的独立单位(即对象)。
  • 信息隐蔽(information hiding)
    对象的外部不能直接地存取对象属性,只能通过几个允许外部使用的服务与对象发生联系。
  • 对象间通发送消息进行交互.

类是面向对象编程的程序基本单位
程序模块是各种由类构成的
类是逻辑上相关数据和函数的封装
类是对问题的抽象描述

1
2
3
4
5
6
7
8
class 类名 {
private
//私有数据成员和成员函数;
protected
//保护数据成员和成员函数;
public
//公有数据成员和成员函数;
};

成员函数

类的外部定义成员函数

1
2
3
4
5
返回类型 类名::成员函数名(参数列表)
{
函数定义体
}
void A::show( ){ cout << m_a << m_b <<endl;}

类内直接定义成员函数, 默认创建为内联函数
如果成员函数在类体外定义,要用inline声明为内联函数

域运算符“∷”成员运算符“.”

在类外定义函数时,应指明成员函数的作用域

在成员函数引用本对象的数据成员时,只需直接写数据成员名,
这时C++系统会把它默认为本对象的数据成员。

保护 protected

除了类本身的成员函数和说明为友元函数或友元类的成员函数可以访问保

护成员外,该类的派生类的成员也可以访问

private 在首次出现时可以忽略

对象的使用

同类对象之间可以相互赋值

1
2
3
Time tA, tB;
tA.set (15,6,0);
tB = tA;

成员访问运算符“.” 和**“->”(对象指针名->成员名)**

1
cout << t.hour << pTime ->min(相当于访问所指对象的成员min) << (*pTime).sec;

软件工程的一个最基本的原则就是将接口与实现分离,信息隐蔽是软件工程中一个非常重要的概念。

自定义类库头文件.h
文件中有用户自行设计的类的定义,包括类的外部接口(公有成员函数的原型)。任何需要使用这些类的源程序,只要在文件中包含这些头文件即可。

1
2
3
4
5
6
7
8
9
10
11
//point.h
class Point
{
public:
double distance(Point & p);
void setX(double i);
void setY(double j);
private:
double x;
double y;
};
1
2
3
4
5
6
7
8
9
//point.cpp
#include<iostream>
#include<cmath>
#include"point.h"
using namespace std;
double Point::distance(Point & p){//定义要指明作用域
return sqrt((p.x-x)*(p.x-x)+(p.y-y)*(p.y-y)); }
void Point::setX(double i){x=i;}
void Point::setY(double j){y=j;}

在面向对象的程序开发中,一般做法是将类的声明放在指定的头文件中,用户如果想用该类,只要把有关的头文件包含进来即可,不必在程序中重复书写类的声明,在程序中就可以用该类来定义对象.为了实现信息隐蔽,对类成员函数的定义一般不放在头文件中,而另外放在一个文件中。

构造函数与析构函数

(自定义)默认构造函数

1
2
3
4
<类名>::<默认构造函数名>()
{ }
Time::Time( )
{ hour=min=sec = 0; }

析构函数

构造函数的反函数,析构函数是用于取消对象成员函数,
当一个对象生命期结束时,系统自动调用析构函数。

  • 析构函数名字为**符号“~”**加类名;
    • 析构函数没有参数和返回值
      • 一个类中只可能定义一个析构函数,
        析构函数不能重载
        • 析构函数的作用
          进行清除对象,释放内存等;
1
2
3
类名::
~默认析构函数名()
{ }//空函数

自动调用
(1) 一个对象当其结束生命周期时 ;
(2) 使用new运算符创建的对象
使用delete运算符释放该对象时;
一般析构函数的调用顺序与构造函数相反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rational{
public:
Rational(int nn=1,int mm=1); //构造函数
void print() ; // 输出化简的 分子/分母
void simple(); // 约分
double getValue(); // 返回分数值
Rational add(Rational & A); // r = r1.add(r2);
void sub(Rational & A , Rational &B); // r.sub(r1,r2);
Rational mul(Rational & A);
void _div(Rational & A, Rational &B);
private:
int m; // 分母
int n; // 分子
};

一般情况下,如果类中的数据都在栈里,程序员不需要开发自定义的拷贝构造函数

默认拷贝构造函数

1
A  b ( a )

对象复制与对象赋值是不同的

静态数据成员的初始化与一般数据成员不同,外部静态数据成员初始化的格式如下:

<类型> <类名>::<静态数据成员> = <值>;

3)在引用静态数据成员时采用格式:

<类名>::<静态数据成员> <对象名>. <静态数据成员>

静态数据成员 vs 全局变量

有了静态数据成员,各对象之间(即不依赖于对象使用)的数据有了沟通的渠道,实现数据共享 。 全局变量破坏了封装的原则,不符合面向对象程序的要求。

公用静态数据成员与全局变量的作用域不同

静态数据成员的作用域只限于定义该类的作用域内

静态成员函数只能访问静态数据成员、静态成员函数和类以外的函数和数据,不能访问类中的非静态数据成员(因为非静态数据成员只有对象存在时才有意义)。但静态数据成员和静态成员函数可由任意访问权限许可的函数访问。和一般成员函数类似,静态成员函数也有访问限制,私有静态成员函数不能由外界访问。静态成员函数没有this指针,因此,静态成员函数只能直接访问类中的静态成员,若要访问类中的非静态成员时,必须借助对象名或指向对象的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream.h>
class Counter
{ static int num ;
public :
Counter( ){ num++; }
Counter(){ num--; }
void setnum ( int i ) { num = i ; }
void shownum() { cout << num << '\t' ; }
static int get(){ return num ; }
} ;
int Counter :: num = 0 ;
void main ()
{ Counter a ; a.shownum() ;
Counter b ; b.shownum() ;
cout<<Counter::get()<<endl;
}

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
2
3
4
5
6
7
8
9
10
11
Box box [3] ;
Box box [3] = {Box(1),Box(1,1),Box(1,1,1)};
Box box [3] = {Box(),Box(),Box()};
Box box [3] = {Box, Box ,Box };
Box box [3] = {Box(1),Box(2), };
Box box [3] = {Box(1),Box(1) };
Box box [3] = {Box(1) };
Box box [3] = { };
Box box [3] = { 1,2,3};
Box *p =new Box[3];
Box *p =new Box[3]{Box(1),Box(1,1),Box(1,1,1) };

类的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Point
{ public:
Point(int=0,int=0);
void print( ) ;
void setX(int x);
void setY(int y);
int getX();
int getY();
private:
int x;
int y;
};
class Circle
{ public:
Circle(double r, Point p);
Circle(double r, int x, int y);
void setCenter(int x,int y);
Point getCenter ();
private:
double radius;
Point center;//类的嵌套
};

构造函数: 必须首先初始化内嵌对象的数据

1
2
Circle ::Circle(double r, int x, int y):radius(r), center(x,y){ }
Circle ::Circle(double r, Point p):radius(r), center(p){ }

成员函数: 可以使用内嵌对象调用其函数. 注意访问权限控制!

1
2
3
4
void setCenter(int x,int y){
center.setX(x); //center.x = x; compiler error!
center.setY(y); //center.y = y; compiler error!
}

成员对象的初始化

一个对象如果有**“成员对象”(即它的成员数据不是普通类型,而是“类”类型的),那么在实现构造函数时应对“成员对象”进行初始化**)

方式是在构造函数中增加构造参数,指明“成员对象”构造的方式

若没有“成员对象”构造方式的声明,系统默认调用“成员对象”的无参的构造函数。

组合关系

一个类包含另一个类的对象

描述整体拥有部分的关系,即has-a关系

该类不与其他类共享对象的引用。即**“整体”端重数只能是1**

如果这种类的对象生命周期结束,被包含的对象的生命周期也会结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Textfield;
class Botton;
class DialogueWindow{
Textfield textfield; //data member
Botton botton; //data member
};
class DialogueWindow{
Textfield textfield1; //data member
Textfield textfield2; //data member
Botton botton1; //data member
Botton botton2; //data member
};

依赖关系

描述两个类对象之间短暂的相互作用

依赖关系表示一个类的对象短暂使用了另一个类对象,代表类之间**“uses-a”关系**

1
2
3
4
5
6
7
8
class Time{
private:
int hour;
int minute;
int second;
public:
void print(){ cout<<hour<<“ ”<<minute<<“ ”<<second<<endl;}
};
1
2
3
4
5
6
7
8
9
10
11
12
class Printer {         // 打印机类
public: void print(){...} // 打印
};
class Student // 学生类
{ // 使用打印机
public: void usePrinter(Printer &p){ p.print(); }
……
}
int main(){
Printer printer; Student studnet;
student.usePrinter(printer);
}

Student类的成员不包含打印机Printer的对象或者指针,即二者不具有“拥有has-a’关系只有学生对象调用usePrinter( )函数时,学生对象与打印机对象才建立关系,并且在该函数执行完毕后,二者关系就结束了。一种短暂的”使用关系”,即“use-a”关系。依赖关系除了被依赖方作为依赖方的函数参数,还可能作为依赖方的函数中的临时对象

友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;
class A{
private: int x;
Public: A(){x=1;}
     friend class B;
};
class B{
Private: char c;
Public: B() {c='c';}
void something(){
A instance;
cout<<instance.x<<endl;
}
};
int main(){
B b_instance;
b_instance.something();

return 0;
}

友元 函数 与友元类

友元函数和友元类

可以访问另一个类的私有和保护(稍后更多)成员(区别于组合)

友元函数不是类的成员函数

友元函数在类范围之外定义

友元的特性

友元是**“给予”的,而不是“索要”的**

非对称性(如果 B 是 A 的友元,A 不一定是 B 的友元)

非传递性(如果 A 是 B 的友元,B 是 C 的友元,A 不一定是 C 的友元)

友元的主要用途

提供了一种访问类成员的更方便快捷的途径

为运算符重载的实现提供了更方便的途径

友元可以访问类的任何成员(可不通过成员函数),这破环了类的封装性,因此要谨慎使用友元

有权从类外部更改类的内部状态。 因此推荐使用成员函数而不是友元来改变状态

friend 声明

friend 函数

Keyword friend

friend int myFunction( int x );

声明在类内保证这个函数可以在类外访问类成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Accumulator{
private:
    int m_value;
public:
    Accumulator() { m_value = 0; }
    void add(int value) { m_value += value; }
    // 声明reset() 函数是本类的友元函数
    friend void reset(Accumulator &accumulator);
};
// reset() 现在是 Accumulator 的友元
void reset(Accumulator &accumulator){
    // 可以直接访问Accumulator 对象的任何数据
    accumulator.m_value = 0;
}

friend 类

在类名前加 friend 保证该类可以访问类成员,可以让整个类成为另一个类的友元。 这使友元类的所有成员函数都可以访问其他类的私有成员。友元类的所有函数都是友元函数

也可以不把整个类声明为友元, 仅仅只声明一个或多个函数为另一个类的友元函数. 这类似于声明普通函数成为友元,除了使用包含 className:: 前缀

友元常用于定义重载运算符时。当两个或多个类需要以一种亲密的方式一起工作时,不常使用友元。使一个成为友元只需要作为前向声明该类存在。 但是,使特定的类的成员函数成为友元则需要首先看到成员函数类的完整声明.

继承(不允许继承循环)

继承的概念

派生类具有基类的特性

  • 共享基类的成员函数

  • 使用基类的数据成员

派生类新增成员(拓展)

  • 定义自己的数据成员

  • 定义独特的成员函数

派生类改造基类

  • 重写基类某些成员函数

C++中单继承派生类的定义形式如下:

1
2
3
4
class 派生类名 : [继承方式] 基类名
{
派生类成员声明;
};

继承方式包括:

public(公有继承)
private(私有继承,默认
protected(保护继承)(保护成员在本类与派生类中能直接访问)

C++中多重继承派生类的定义形式如下

1
2
3
4
5
class 派生类名: [继承方式] 基类名1, [继承方式] 基类名2,…, 
[继承方式] 基类名n
{
派生类成员声明;
};
1
2
class Assistant : protected Student, Teacher//对Teacher默认是私有继承
{ …… };

fa7f516ffbe998538989d54213bb5837

继承方式决定了基类成员在派生类中的访问权,

这种访问来自两个方面

  • 派生类中

新增函数成员访问从基类继承来的成员

  • 派生类外部

通过派生类的对象访问从基类继承的成员

94de02253d18c73529beb0753b328c5d

98f37468b772cd29ba63bb8ba5dfef81

访问私有继承的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student { int number; char school[10]; 
protected: char name[10]; char sex;
public: void input_data( ); void print(); };

class CollegeStudent : private Student
{ char major[10]; //新增成员:专业
public:
void input_major( ){cin>>major ; }
// 输入专业
void print( ){ Student::print();
// 输出信息
cout<<“name:”<<name <<“ sex:”<<sex <<endl;
cout<<"major:"<<major<<endl; }
};
1
2
3
4
5
6
7
8
9
10
class Student { …… };
class CollegeStudent : private Student{...};

int main(){
Student s; s.input_data(); s.print();
CollegeStudent cs;
cs.input_data(); // 错误
cs.input_major();
cs.print();
return 0; }

CollegeStudentl类访问基类,Student的能力没有变化,

在私有继承的情况下,通过派生类**对象(并非类内)**无法访问基类的任何成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class A
{ public :
void get_XY( ) ;
void put_XY( ) ;
protected:
int x, y;
};
void A::get_XY( )
{ cin >> x >> y ; }
void A:: put_XY( )
{ cout << "x = "<< x << ", y = " << y << '\n' ; }
class B : public A
{ public :
int get_S() { s = x * y ; return s ; }
protected:
int s;
};
class C : public B
{ public :
void get_H() { cin >> h ; }
int get_V() {v = get_S() * h ; return v ; }
protected:
int h, v;
};
void main()
{ A objA ;
B objB ;
C objC ;
objA.get_XY() ;
objA.put_XY() ;
objB.get_XY() ;
cout << "S = " << objB.get_S() << endl ;
objC.get_XY() ;
objC.get_H();
cout << "V = " << objC.get_V() << endl ;}

a87a6f027be2e1b66c1c2497d61e59f3

4d2fe503e36e73613dfe44ad25b9ed9e

三种继承方式的对比

一般采用不会改变基类成员访问权限的公有继承

私有继承:

基类的可被继承的成员都成了其直接派生类的私有成员,

无法再进一步派生,

实际上私有继承相当于终止了基类成员的继续派生。

保护继承

基类的可被继承的成员都成了直接派生类的保护成员,

保护继承保证了最上层基类的成员依然能被继承树中的

次级子类所继承。

访问级别不能升只能降,一层一层来看即可

42f4358cb52c4ca97743131bb9022ee9

派生类的构造函数

派生类的构造与析构函数

创建派生类对象时调用基类的构造函数来初始化基类数据。

执行派生类的析构函数时,基类的析构函数也将被调用。

dcef7a843dbe532605e80a1b95c20b54

派生类构造函数的定义方式:

1
2
3
4
5
6
7
8
派生类名(参数总表):基类名(基类构造函数参数表1), 对象成员(参数表2)
{
派生类成员初始化;
}
Student(int no, char name[],char sex):Person(name,sex)
{ id = no; }
Student(int no, char name[],char sex):
Person(name,sex), id( no ){ }
1
2
3
4
5
6
7
8
9
class Base1 { public: Base1() { cout << "Base1" << endl; } };
class Base2 { public: Base2() { cout << "Base2" << endl; } };

class Derived : public Base2, public Base1 { // 基类声明顺序:Base2 → Base1
public:
Derived() : Base1(), Base2() { // 初始化列表顺序:Base1 → Base2(与声明顺序相反)
cout << "Derived" << endl;
}
};

1. 初始化顺序的决定因素

  • 基类:按照派生类定义时基类的声明顺序初始化(无论初始化列表中如何排列)。
  • 成员变量:按照成员在类中声明的顺序初始化(与初始化列表顺序无关)。
  • 派生类自身最后执行派生类构造函数函数体
1
2
3
4
5
6
7
8
9
class Base1 { public: Base1() { cout << "Base1" << endl; } };
class Base2 { public: Base2() { cout << "Base2" << endl; } };

class Derived : public Base2, public Base1 { // 基类声明顺序:Base2 → Base1
public:
Derived() : Base1(), Base2() { // 初始化列表顺序:Base1 → Base2(与声明顺序相反)
cout << "Derived" << endl;
}
};
1
2
3
Base2
Base1
Derived

为什么初始化顺序固定?

  • 成员变量依赖:若成员变量的初始化依赖于其他成员的顺序,固定顺序可避免潜在错误。
  • 基类依赖:若基类的初始化顺序被用户随意调整,可能导致基类未完全初始化就被使用。
最佳实践
  • 保持初始化列表顺序与声明顺序一致:提高代码可读性,避免混淆。
  • 避免成员间的初始化依赖:若必须依赖,通过构造函数体或成员函数处理。

**构造函数执行顺序:**基类->派生类中对象成员->派生类

派生类构造函数的几点说明

1)派生类构造函数的定义中可省略对基类构造函数的调用其条件是在基类中必须有默认的构造函数或者根本没有定义构造函数。
2)当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。

继承中的同名成员访问

多重继承时不同基类成员同名也可以用**类名限定符“::”**来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student
{
protected:
int id;
};
class Teacher
{
protected :
int id;
};
class Assistant: public Student, public Teacher
{ public:
void print(){
cout<<id<<endl; //error!访问是二义的
cout<<Student::id<<endl; //访问Student的id
cout<<Teacher::id<<endl; //访问Teacher的id
}
};

继承时的同名成员隐藏规则
派生类定义了与基类相同的成员,此时基类的同名成员在派生类内不可见,派生类成员隐藏了同名的基类成员.

基类成员与派生类成员同名,可以通过类名限定符“::”来解决。其语法为:类名 : : 成员

1
2
3
4
5
6
7
8
class Student : public Person{ // 派生类
private: int id; // 学号
……
public: void test(){ //测试函数
id=123; // 访问派生类成员
Person::id =456; // 访问基类成员
}
};

继承中成员同名有两种情况:
1.基类成员与派生类成员同名
2.多重继承时不同基类成员同名

1
2
3
4
5
6
7
8
9
10
11
12
class Person
{ protected: int id; }; // 身份号码
class Student : public Person
{ protected: int id; }; // 学号
class Teacher : public Person
{ protected: int id;}; // 职工号
class Assistant: public Student, public Teacher{
public: void test(){ // 测试
Student::id = 1001; // 正确:访问Student类的id
Teacher::id = 101; // 正确:访问Teacher类的id
Person::id =230×××0001; // 错误}
};

1e35cab60282a49190c05943c754245c

类族中的赋值兼容

公有继承时,一个派生类的对象可用于基类对象适用的地方,需要基类对象的任何地方都可以使用派生类对象替代

赋值兼容规则有三种情况:
(1)派生类的对象可以赋值给基类的对象
base Obj = derived Obj;
(2)派生类的对象可以初始化基类的引用
base& base_Obj = derived_obj;
(3)派生类的对象的地址可以赋给指向基类的指针
base *pBase = &derived_obj;

8b80309614a6abf7fa39b9b15d0abf52

多态

同样的消息不同类型的对象接收时,产生不同行为的现象。(同一名字,多种语义同个接口,多种方法)

静态多态的概念

在程序编译时系统就能够确定要调用的是哪个函数,也被称为编译时多态

  • 函数重载

  • 运算符重载

函数重载注意事项

不能仅靠函数的返回值来区别重载函数,必须从形式参数上区别开来。

派生类中的同名成员函数

使用 :: 加以区分

使用对象加以区分

1
2
3
4
5
MonkyKong sun; 
sun. fly ( );
sun . Follower :: fly ( );
Pig pigsy;
pigsy.fly();
1
2
3
4
5
6
7
class B :public A
char name[10];
A::Show ( )
Show ( )
Aobj . Show ( );
Bobj . Show ( );
Bobj . A :: Show ( );

动态多态性

指程序在编译时并不能确定要调用的函数,直到运行时系统才能动态地确定操作所针对的具体对象,它又被称为运行时多态

动态多态是通过虚函数(virtual function)实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base
{ public:
void show()
{ cout << "Base"<<endl ; }
};
class Derv1: public Base
{ public:
void show(){ cout << "Derv1"<<endl ; }
};
class Derv2: public Base
{ public:
void show(){ cout << "Derv2"<<endl ; }
};
Derv1 dv1;
Derv2 dv2;
dv1.show() ;
dv2.show() ;
Base* pBase;
pBase = &dv1;
pBase->show();
pBase = new Derv2();
pBase->show();
//通过基类指针只能访问从基类继承的成员

虚函数

C++中的虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或者基类引用来访问这个同名函数(最重要区别)。虚函数成员声明的语法为:

1.virtual只能使用在类定义的函数原型声明中,

不能在成员函数实现的时候使用,也不能用来限定类外的普通函数。

2.用virtual声明类的非静态的成员函数,只用于类的继承层次结构中。

不能将类外的普通函数(友员)和静态成员函数声明成虚函数。

virtual具有继承性

在派生类中重新定义虚函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数完全相同
否则不能实现多态性, 为函数重载.

虚函数是在基类中冠以关键字 virtual 的非静态成员函数。
继承体系:判断成员函数所在的类是否会作为基类;虚函数为类族提供了一种公共接口。
重写函数:该函数在类被继承后有无可能被更改功能;允许在派生类中对基类的虚函数重新定义
调用形式:是否通过基类指针或引用调用该虚函数;赋值兼容性原则

1
2
3
4
5
6
7
8
9
10
11
class Follower // 徒弟类
{ public:
virtual void fly( );
virtual bool fight( Ghost&);
};
class WuKong
: public Follower
{ public:
void fly();
bool fight( Ghost* );
};

虚析构函数

构造函数不能是虚函数

建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚函数

虚析构函数用于指引 delete 运算符正确析构动态对象

基类的析构函数为虚函数时,无论指针指的是同一类族的哪一个类对象,对象撤销时,系统会采用动态关联,调用相应的析构函数,完成该对象的清理工作。

习惯把析构函数声明为虚函数,即使基类并不需要析构函数,以确保撤销动态存储空间时能够得到正确的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Base
{ public:
Base( ){ cout << "Base" << endl; }
~Base( ){ cout << "Desconstruct Base" << endl; }
};
class Derv1: public Base
{ public:
Derv1( ){ cout << "Derv1" << endl; }
~Derv1( ){ cout << "Desconstruct Derv1" << endl; }
};
class Derv2: public Derv1
{ public:
Derv2( ){ cout << "Derv2" << endl; }
~Derv2( ){ cout << "Desconstruct Derv2" << endl; }
};
Base* pBase = new Base();
delete pBase;
Derv1* pDerv1 = new Derv1();
delete pDerv1;
pBase = new Derv2();
delete pBase;//析构由基类指针建立的派生类对象,没有调用派生类析构函数
class Base
{ public:
Base( ){ cout << "Base" << endl; }
virtual ~Base( ){ cout << "Desconstruct Base" << endl; }//后面默认都虚,基类指针也可做到连删
};
class Derv1: public Base
{ public:
Derv1( ){ cout << "Derv1" << endl; }
~Derv1( ){ cout << "Desconstruct Derv1" << endl; }
};
class Derv2: public Derv1
{ public:
Derv2( ){ cout << "Derv2" << endl; }
~Derv2( ){ cout << "Desconstruct Derv2" << endl; }
};

实现动态多态

• 基类声明虚函数

• 派生类重写虚函数

• 基类指针或引用调用

在许多情况下,在基类中不能给出有意义的虚函数定义,这时可把它说明成纯虚函数,把它的定义留给派生类来做
定义纯虚函数的一般形式为:

1
2
3
class 类名{
virtual 返回值类型 函数名(参数表) = 0
};

①纯虚函数没有函数体
②最后面的“=0” 不表示函数返回值为0
③这是一个声明语句

纯虚函数的作用
在基类中为其派生类保留一个函数的名字以便派生类根据需要对它进行定义, 否则无法实现多态性。

抽象类的概念

如果一个类中至少有一个纯虚函数,那么这个类被成为抽象类(abstract class**)**。

抽象类必须用作派生其他类的基类,不能作为返回或参数类型,可使用指向抽象类的指针支持运行时多态性。而不能用于直接创建对象实例

1
p->getArea(); (*p).draw( );//作指针做对象时的不同写法以区分

生类中应重写基类中的纯虚函数,否则派生类仍将被看作一个抽象类

1
2
3
4
5
6
7
void test1(TwoDimensionalShape & t){
t.show( ); t.draw( ); cout<<t.getArea()<<endl;
}
void test2(TwoDimensionalShape * p){
p->show( ); p->draw( );
cout<< (*p).getArea()<<endl; //???
}

如果二维图形又派生出了椭圆形,用于测试的test函数需要需改吗?这有何意义?

  1. test 函数是否需要修改?
    不需要修改
    椭圆形(Ellipse)TwoDimensionalShape的派生类,且正确重写了基类中的show()draw()getArea()虚函数(假设这三个函数在TwoDimensionalShape中是虚函数),则test1test2函数可以直接接收Ellipse对象(或指针 / 引用)正确调用派生类的实现。
  2. 意义:
    这体现了面向对象的多态性,具体意义如下:
    • 代码复用性:新增派生类(如椭圆形)时,无需修改已有的test1test2等通用函数,只需保证派生类遵循基类的接口规范(重写虚函数),即可直接使用这些函数进行测试。
    • 扩展性:系统可以轻松扩展新的二维图形类型(如椭圆形、三角形等),而不影响原有代码的逻辑,符合 “开闭原则”(对扩展开放,对修改关闭)。
    • 接口统一性:通过基类的引用或指针调用派生类的方法,屏蔽了不同派生类的实现差异,使代码更简洁、通用,降低了模块间的耦合度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class TwoDimensionalShape {
public:
virtual void show() = 0; // 纯虚函数:显示图形信息
virtual void draw() = 0; // 纯虚函数:绘制图形
virtual double getArea() = 0; // 纯虚函数:计算面积
virtual ~TwoDimensionalShape() {} // 虚析构函数
};

// 已有的派生类:圆形
class Circle : public TwoDimensionalShape {
// 实现show()、draw()、getArea()...
};

// 新增派生类:椭圆形
class Ellipse : public TwoDimensionalShape {
public:
void show() override { /* 实现 */ }
void draw() override { /* 实现 */ }
double getArea() override { /* 计算椭圆面积 */ }
};
int main() {
Ellipse e;
test1(e); // 传入Ellipse对象的引用,正确调用Ellipse的成员函数
test2(&e); // 传入Ellipse对象的指针,正确调用Ellipse的成员函数
return 0;
}

override是一个关键字,用于显式声明派生类中的成员函数重写(覆盖) 了基类中的虚函数(virtual函数)。它的主要作用是增强代码的可读性和安全性

(3) 在类的层次结构中,顶层或最上面的几层可以是抽象基类。

抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明

为什么引⼊多态
利⽤多态性可以设计和实现⼀个易于扩展的系统。增强代码的通⽤性

区别

对比维度 重载(Overload) 多态(Polymorphism)
定义 同一作用域内,多个函数名相同但参数列表(参数类型、个数、顺序)不同的函数。 基类与派生类中,派生类重写(override)基类的虚函数,通过基类指针 / 引用调用时,**根据对象实际类型执行(多种对象对应执行)**对应函数。
实现阶段 编译时确定(静态多态)。编译器根据函数参数列表匹配对应的函数。 运行时确定(动态多态)。程序运行时根据对象实际类型调用对应的函数。
作用范围 同一类中(或全局函数),函数名相同但参数不同(不完全等同于重写,属于新建,仅名字相同) 继承关系中,基类与派生类之间,函数名、参数列表、返回值完全相同(相同信息重写虚函数带来的不同处理响应)。
核心依赖 函数参数列表的差异(与返回值无关)。 基类虚函数、派生类重写、基类指针 / 引用能指向相应派生类对象并执行相应的函数。

二、联系

  1. 都是代码复用的手段
    • 重载允许同一功能(函数名)适配不同参数,避免为相似功能起不同名字(如printIntprintDouble)。
    • 多态允许通过统一接口(基类函数)操作不同派生类对象,简化代码逻辑(如Shape*统一管理圆形、方形等)。
  2. 都体现 “一个接口,多种实现” 的思想
    • 重载:同一函数名对应多种参数组合的实现。
    • 多态:同一虚函数接口(基类)对应派生类的多种重写实现
  3. 都依赖编译器的处理
    • 重载依赖编译器在编译时根据参数匹配函数(静态绑定)
    • 多态依赖编译器对虚函数表的处理,实现运行时动态绑定

三、总结

  • 重载是 “横向” 的函数扩展(同一类内,同名不同参),解决同一功能的不同参数适配问题,属于静态多态
  • 多态是 “纵向” 的函数扩展(继承体系中,重写虚函数),解决不同派生类对象的统一接口调用问题,属于动态多态

模板

模板可以实现类型参数化(包括新类型),C++模板包括 函数模板和类模板两种类型**。**

函数模板就解决函数重载中多次定义函数的问题。

类模板就是对一批仅仅成员数据类型不同的类的抽象

泛型编程(generic programming)
模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。

1
2
3
4
5
6
7
int square ( int x )
{ return x*x ; }
Complex square (Complex x )
{ return x*x ; }
template < typename T >
T square (T x )
{ return x*x; }

通过模板可以产生类或函数的集合,使它们操作不同的数据类型,避免需要为每种数据类型产生一个单独的类或函数。

1
2
3
template <class 类型参数名1 ,class 类型参数 2,…>
函数返回值类型 函数名(形式参数表)
{ 函数体 }

关键字class也可以使用关键字typename

在template语句与函数模板定义语句<返回类型>之间不允许有别的语句

函数模板允许使用多个类型参数,但在template定义部分的每个形参前必须有关键字typename或class

函数形式参数表中可以使用模板类型参数,也可以使用一般类型参数.

模板参数说明的每个类型参数必须在函数定义形参表中至少出现一次

类模板主要用于数据存储(容器)类表示和算法不受所包含的元素类型的影响。

一个类模板在类层次结构中
既可以是基类也可以是派生类:
Ø 类模板可以从模板类派生
Ø 类模板可以从非模板类派生
Ø 模板类可以从类模板派生
Ø 非模板类可以从类模板派生

EasyX 基础

  1. 在项目中创建一个.cpp源文件(右键源文件 -> 添加 -> 新建项 -> 设置文件名 first.cpp)

image-20250707160940619

EasyX 基本概念

绘图窗口与设备

initgraph 函数用于初始化绘图窗口

1
2
3
4
5
HWND initgraph(
int width,
int height,
int flag = NULL
);

示例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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <graphics.h>
int main()
{
initgraph(600, 600);
setorigin(300, 300); // 将绘图窗口的中心点作为坐标原点
circle(0, 0, 100);
setorigin(0, 0); // 将绘图窗口的左上角作为坐标原点
setaspectratio(2, 1); // x轴方向的缩放因子为2,y轴方向的缩放因子为1(默认值)
circle(100, 100, 100);
setorigin(0, 600); // 将绘图窗口的左下角作为坐标原点
setaspectratio(1, -1); // 缩放因子为负数,可以实现坐标轴的翻转,此行可使y轴向上为正
circle(100, 100, 100);
system("pause");
closegraph();
return 0;
}

颜色

EasyX 使用 24bit 真彩色,有四种表示颜色的方法:https://docs.easyx.cn/zh-cn/color,通过 setlinecolor 函数可以设置线条颜色

  1. 预定义常量表示颜色( 常量名要大写 )

  2. 16进制数字表示颜色( 0xBBGGRR ),注意颜色的顺序与RGB宏相反

  3. RGB 宏合成颜色( RGB(RRGGBB) )

  4. 用 HSLtoRGB、HSVtoRGB 转换其他色彩模型到 RGB 颜色

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <graphics.h>
int main()
{
initgraph(800, 600);
setfillcolor(BLUE); // 用预定义常量表示颜色
solidcircle(100, 200, 100);
setfillcolor(0xaa0000); // 用16进制数字表示颜色
solidcircle(300, 200, 100);
setfillcolor(RGB(0, 0, 170)); // 用RGB宏合成颜色
solidcircle(500, 200, 100);
setfillcolor(HSLtoRGB(240, 1, 0.33)); // 用 HSLtoRGB、HSVtoRGB 转换其他色彩模型到 RGB 颜色
solidcircle(700, 200, 100);
system("pause");
closegraph();
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//include <windows.h>
#include <graphics.h>

int main()
{
initgraph(640, 480);
BeginBatchDraw();

setlinecolor(WHITE);
setfillcolor(RED);
for (int i = 50; i < 600; i++)
{
DWORD beginTime = GetTickCount(); // 记录循环开始时间

cleardevice();
circle(i, 100, 40);
floodfill(i, 100, WHITE);
FlushBatchDraw();

DWORD endTime = GetTickCount(); // 记录循环结束时间
DWORD elapsedTime = endTime - beginTime; // 计算循环耗时
if (elapsedTime < 1000 / 60) // 按每秒60帧进行补时
Sleep(1000 / 60 - elapsedTime);
}

EndBatchDraw();
closegraph();
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
class IMAGE(int width = 0, int height = 0);

公有成员
int getwidth();
返回 IMAGE 对象的宽度,以像素为单位。

int getheight();
返回 IMAGE 对象的高度,以像素为单位。

operator =
实现IMAGE对象的直接赋值。该操作仅拷贝源图像的内容,不拷贝源图像的绘图环境。

在内存中保存图像信息。

loadimage 函数

范例1:loadimage 直接读取图片至绘图窗口

1
2
3
4
5
6
7
8
9
10
11
12
#include <graphics.h>

int main()
{
initgraph(1400, 600);

loadimage(NULL, _T("image\\background.jpg")); // 第一个参数为NULL时,直接读取图片至绘图窗口

system("pause");
closegraph();
return 0;
}

注:修改窗口大小,可以显示图片部分内容,但只能从绘图窗口的坐标原点(左上角)开始显示图片

范例2:loadimage 直接读取图片至绘图窗口并进行图片或窗口缩放

1
2
3
4
5
6
7
8
9
10
11
12
#include <graphics.h>

int main()
{
initgraph(700, 300);

loadimage(NULL, _T("image\\background.jpg"), 700, 300, false); // 将图像缩放为700*300在绘图窗口显示

system("pause");
closegraph();
return 0;
}

注1:图片缩放后的尺寸小于窗口尺寸,则窗口会有黑边;若大于窗口尺寸,则图片显示不全

注2:第五个参数若为 true,则会调整窗口以适应图片的大小

注3:从磁盘中读取大量图片显示的情况下,使用 loadimage 直接读取图片至绘图窗口性能较差

范例3:loadimage 读取本地图片文件,输出图片宽度和高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <graphics.h>
#include <stdio.h>

int main()
{
initgraph(1000, 600, SHOWCONSOLE); // 初始化绘图窗口并开启终端

IMAGE img; // 定义图像对象
loadimage(&img, _T("image\\background.jpg")); // 读取本地图片文件,存入图像对象
printf("width=%d, height=%d \n", img.getwidth(), img.getheight()); // 输出图像宽度和高度

system("pause");
closegraph();
return 0;
}

注:本例中的图片内容不会在窗口内显示

putimage 函数

范例1:putimage 在绘图窗口显示图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <graphics.h>

int main()
{
initgraph(1000, 600);

IMAGE img;
loadimage(&img, _T("image\\background.jpg"));
putimage(0, 0, &img); // 将IMAGE对象显示在绘图窗口的坐标(0,0)处

system("pause");
closegraph();
return 0;
}

范例2:putimage 截取图像部分内容进行显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <graphics.h>

int main()
{
initgraph(900, 600);

IMAGE img;
loadimage(&img, _T("image\\background.jpg"));
putimage(0, 0, 900, 600, &img, 115, 0); // 从图像的(115,0)坐标处截取宽900、高600的部分内容显示在窗口(0,0)处

system("pause");
closegraph();
return 0;
}

范例3:putimage 三元光栅操作码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <graphics.h>

int main()
{
initgraph(1000, 600);

IMAGE img;
loadimage(&img, _T("image\\background.jpg"));
putimage(0, 0, &img, NOTSRCCOPY); // 第四个参数是三元光栅操作码
system("pause");
closegraph();
return 0;
}

注:putimage 第四个参数是 三元光栅操作码 ,它定义了源图像与目标图像的位合并形式,默认值为 SRCCOPY 详见

https://docs.easyx.cn/zh-cn/putimage

透明贴图

范例1:通过PS制作原图掩码图前景图,再进行三元光栅操作叠加而成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <graphics.h>

int main()
{
IMAGE imgGuoqi, imgGuohui, imgGuohuiMask, imgGuohuiFg;
loadimage(&imgGuoqi, _T("image\\guoqi.jpg"), 1000, 600); // 加载国旗(背景图)
loadimage(&imgGuohui, _T("image\\guohui.jpg"), 200, 200); // 加载国徽原图(白色周边)
loadimage(&imgGuohuiMask, _T("image\\guohui_mask.jpg"), 200, 200); // 加载国徽掩码图(白色周边+黑色内容)
loadimage(&imgGuohuiFg, _T("image\\guohui_fg.jpg"), 200, 200); // 加载国徽前景图(黑色周边+待显示内容)

initgraph(1000, 600);

putimage(0, 0, &imgGuoqi); // 显示国旗
putimage(0, 0, &imgGuohui); // 显示国徽原图
putimage(0, 200, &imgGuohuiMask); // 显示国徽掩码图
putimage(0, 400, &imgGuohuiFg); // 显示国徽前景图

// 透明贴图
putimage(200, 0, &imgGuohuiMask, SRCAND); // 显示掩码图(SRCAND:按位与)
putimage(200, 0, &imgGuohuiFg, SRCPAINT); // 显示前景图(SRCPAINT:按位或)

system("pause");
closegraph();
return 0;
}

范例2:TransparentBlt 函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <graphics.h>
#pragma comment(lib, "MSIMG32.LIB") // 链接器在链接过程中包含指定的库文件

void putimage_alpha(IMAGE* dstImg, int x, int y, IMAGE* srcImg, UINT transparentColor)
{
HDC dstDC = GetImageHDC(dstImg);
HDC srcDC = GetImageHDC(srcImg);
int w = srcImg->getwidth();
int h = srcImg->getheight();
TransparentBlt(dstDC, x, y, w, h, srcDC, 0, 0, w, h, transparentColor);
}

int main()
{
initgraph(1000, 600);
IMAGE imgGuoqi, imgBaidu;
loadimage(&imgGuoqi, _T("image\\guoqi.jpg"), 1000, 600); // 加载国旗(背景图)
loadimage(&imgBaidu, _T("image\\baidu.png")); // 加载百度LOGO(PNG格式)
putimage(0, 0, &imgGuoqi); // 显示国旗
putimage(0, 0, &imgBaidu); // 显示百度LOGO
putimage_alpha(NULL, 0, 300, &imgBaidu, BLACK); // 显示百度LOGO(透明贴图)
system("pause");
closegraph();
return 0;
}

TransparentBlt 是 Windows GDI(Graphics Device Interface)中的一个函数,用于在绘制位图时支持透明效果。

函数说明:第1个参数为目标设备,第2、3个参数是输出目标矩形左上角坐标,第4、5个参数是目标矩形的宽和高,参数6-10与1-5类似,第11个参数是透明底色(若图片是透明图片,默认为BLACK)

注:此方法只支持 PNG 格式的图片

范例3:AlphaBlend 函数实现(推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <graphics.h>
#pragma comment(lib, "MSIMG32.LIB") // 链接器在链接过程中包含指定的库文件

void putimage_alpha(int x, int y, IMAGE* img)
{
int w = img->getwidth();
int h = img->getheight();
AlphaBlend(GetImageHDC(NULL), x, y, w, h, GetImageHDC(img), 0, 0, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

int main()
{
initgraph(1000, 600);

IMAGE imgGuoqi, imgBaidu;
loadimage(&imgGuoqi, _T("image\\guoqi.jpg"), 1000, 600); // 加载国旗(背景图)
loadimage(&imgBaidu, _T("image\\baidu.png")); // 加载百度LOGO(PNG格式)
putimage(0, 0, &imgGuoqi); // 显示国旗
putimage(0, 0, &imgBaidu); // 显示百度LOGO
putimage_alpha(0, 300, &imgBaidu); // 显示百度LOGO(透明贴图)

system("pause");
closegraph();
return 0;
}

AlphaBlend 是 Windows GDI 中用于实现 Alpha 混合(透明/半透明) 绘制的函数,比 TransparentBlt 更强大,支持 逐像素透明度(Alpha 通道)整体透明度(全局 Alpha)

注:此方法只支持 PNG 格式的图片

图片动画

图片动画的核心是一系列静态的图像(动画帧)。每一帧都是一张静态的图片,但它们之间略有不同,通常表现为物体的位置、形状或颜色的微小变化。这些帧按照特定的顺序排列,并以一定的速度连续播放,使得观者感受到运动的效果。

范例:角色动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <graphics.h>
#pragma comment(lib, "MSIMG32.LIB")

const int WINDOW_WIDTH = 1000; //窗口宽度
const int WINDOW_HEIGHT = 600; //窗口高度
const int FRAME = 60; //帧数
const int INTERVAL_MS = 15; //动画帧间隔
const int IMAGE_NUM = 13; //动画图片数

//显示透明图片
void putimage_alpha(int x, int y, IMAGE* img)
{
int w = img->getwidth();
int h = img->getheight();
AlphaBlend(GetImageHDC(NULL), x, y, w, h, GetImageHDC(img), 0, 0, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

int main()
{
bool running = true; //主循环控制
ExMessage msg; //键鼠消息
IMAGE imgBackground; //背景图片对象
IMAGE imgPEA[13]; //玩家动画图片
TCHAR imgPath[256]; //动画图片文件路径
int imgIndex = 0; //动画帧索引
static int timer = 0; //动画计时器

loadimage(&imgBackground, _T("image\\background.jpg")); //加载背景图片
for (int i = 0; i < IMAGE_NUM; i++) //加载动画图片
{
_stprintf_s(imgPath, _T("image\\pea\\%d.png"), i + 1); //动画图片路径(格式转换)
loadimage(&imgPEA[i], imgPath); //加载动画图片
}

initgraph(WINDOW_WIDTH, WINDOW_HEIGHT);
BeginBatchDraw();

//主循环
while (running)
{
DWORD beginTime = GetTickCount();

//消息处理
while (peekmessage(&msg))
{
}

//数据处理
timer += 5;
if (timer > INTERVAL_MS) //定时器超过预定的时间间隔时切换下一张图片
{
imgIndex = (imgIndex + 1) % IMAGE_NUM; //循环切换图片:索引值0-12
timer = 0; //重置计时器
}

//绘图
cleardevice();
putimage(0, 0, &imgBackground); //绘制背景图片
putimage_alpha(500, 300, &imgPEA[imgIndex]); //绘制豌豆图片
FlushBatchDraw();

//帧延时处理
DWORD endTime = GetTickCount();
DWORD elapsedTime = endTime - beginTime;
if (elapsedTime < 1000 / FRAME)
Sleep(1000 / FRAME - elapsedTime);
}

EndBatchDraw();
closegraph();
return 0;
}

Resize 函数

GetImageBuffer 函数

范例1:GetImageBuffer 通过直接操作显示缓冲区绘制渐变的蓝色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <graphics.h>

int main()
{
initgraph(600, 400);

DWORD* pMem = GetImageBuffer(); // 获取当前窗口所指图像缓冲区的指针
for (int i = 0; i < 600 * 400; i++)
pMem[i] = BGR(RGB(0, 0, i * 256 / (600 * 400))); // 直接对图像缓冲区每个坐标像素赋值(颜色)

system("pause");
closegraph();
return 0;
}

范例2:图像翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <graphics.h>

// 图像翻转
void flip_image(IMAGE* srcImg, IMAGE* dstImg)
{
int w = srcImg->getwidth(); // 获取源图像宽度
int h = srcImg->getheight(); // 获取源图像高度
Resize(dstImg, w, h); // 设置目标图像与源图像宽高一致
DWORD* src_buffer = GetImageBuffer(srcImg); // 获取源图像缓冲区指针
DWORD* dst_buffer = GetImageBuffer(dstImg); // 获取目标图像缓冲区指针

for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
int idx_src = y * w + x;
int idx_dst = y * w + (w - x - 1);
dst_buffer[idx_dst] = src_buffer[idx_src]; // 交换对应坐标像素的颜色值
}
}
}

int main()
{
initgraph(1400, 600);

IMAGE img1, img2;
loadimage(&img1, _T("image\\background.jpg"));
flip_image(&img1, &img2);
putimage(0, 0, &img2);

system("pause");
closegraph();
return 0;
}

消息处理

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct ExMessage
{
USHORT message; // 消息标识
union
{
// 鼠标消息的数据
struct
{
bool ctrl :1; // Ctrl 键是否按下
bool shift :1; // Shift 键是否按下
bool lbutton :1; // 鼠标左键是否按下
bool mbutton :1; // 鼠标中键是否按下
bool rbutton :1; // 鼠标右键
short x; // 鼠标的 x 坐标
short y; // 鼠标的 y 坐标
short wheel; // 鼠标滚轮滚动值,为 120 的倍数
};

// 按键消息的数据
struct
{
BYTE vkcode; // 按键的虚拟键码
BYTE scancode; // 按键的扫描码(依赖于 OEM)
bool extended :1; // 按键是否是扩展键
bool prevdown :1; // 按键的前一个状态是否按下
};

// 字符消息的数据
TCHAR ch;

// 窗口消息的数据
struct
{
WPARAM wParam;
LPARAM lParam;
};
};
};

message :可以分为四大类:EX_MOUSE(鼠标11项)、EX_KEY(键盘2项)、EX_CHAR(字符1项)、EX_WINDOW(窗口3项)

union :共用体中存储具体消息的数据

鼠标消息

范例:跟随鼠标移动的圆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <graphics.h>

class Ball {
private:
int posX;
int posY;
int radius;
public:
Ball(int x = 100, int y=100, int r=50) : posX(x), posY(y), radius(r) {} // 构造方法

int getX() const { return posX; }
int getY() const { return posY; }
int getRadius() const { return radius; }

void setX(int x) { posX = x; }
void setY(int y) { posY = y; }
void setRadius(int r) { radius = r; }
};

int main() {
bool running = true; // 主循环控制参数
ExMessage msg; // 消息对象
Ball ball(400, 300, 50); // 待绘制对象
//Ball ball;
initgraph(800, 600); // 初始化绘图窗口
BeginBatchDraw(); // 开启批量绘图

// 主循环
while (running) {
// 消息处理
while (peekmessage(&msg)) {
if (msg.message == WM_MOUSEMOVE) { // 圆的位置随鼠标位置变化
ball.setX(msg.x);
ball.setY(msg.y);
}
else if (msg.message == WM_LBUTTONDOWN) { // 左键按下圆变红色
setfillcolor(RED);
}
else if (msg.message == WM_LBUTTONUP) { // 左键松开圆变白色
setfillcolor(WHITE);
}
else if (msg.message == WM_RBUTTONDOWN) { // 右键按下结束主循环
running = false;
}
}
// 绘图
cleardevice(); // 清除屏幕
solidcircle(ball.getX(), ball.getY(), ball.getRadius()); // 绘制当前帧内容
FlushBatchDraw(); // 刷新批量绘图
}

EndBatchDraw(); // 关闭批量绘图
closegraph(); // 关闭绘图窗口
return 0;
}

键盘消息

范例1:用键盘控制小球

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <graphics.h>

// 用结构体封装小球属性
typedef struct Ball
{
int x; // 小球圆心坐标x
int y; // 小球圆心坐标y
int r; // 小球半径
int dx; // 小球在x轴方向移动的增量
int dy; // 小球在y轴方向移动的增量
COLORREF color; // 小球颜色
} Ball;

int main()
{
bool running = true;
ExMessage msg;
Ball ball = { 300, 300, 20, 5, 5, YELLOW }; // 创建小球并初始化
initgraph(600, 600);
BeginBatchDraw();

while (running)
{
while (peekmessage(&msg))
{
if (msg.message == WM_KEYDOWN)
{
switch (msg.vkcode) // 判断虚拟键代码
{
case 'w': // 上键:小球Y坐标减少
case 'W':
case VK_UP:
ball.y -= ball.dy;
break;

case 's':
case 'S':
case VK_DOWN: // 下键:小球Y坐标增加
ball.y += ball.dy;
break;

case 'a':
case 'A':
case VK_LEFT: // 右键:小球X坐标减少
ball.x -= ball.dx;
break;

case 'd':
case 'D':
case VK_RIGHT: // 右键:小球X坐标增加
ball.x += ball.dx;
break;

case VK_ESCAPE: // ESC键:结束主循环
running = false;
break;
}
}
}

cleardevice(); // 清除屏幕
setfillcolor(ball.color); // 设置填充颜色
solidcircle(ball.x, ball.y, ball.r); // 绘制无边框填充圆;
FlushBatchDraw();
}

EndBatchDraw();
closegraph();
return 0;
}

虚拟键代码 https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

优化后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <graphics.h>
#include <math.h>
#define WIN_WIDTH 600
#define WIN_HEIGHT 600

// 用结构体封装小球属性
typedef struct Ball
{
int x; // 小球圆心坐标x
int y; // 小球圆心坐标y
int r; // 小球半径
int dx; // 小球在x轴方向移动的增量
int dy; // 小球在y轴方向移动的增量
COLORREF color; // 小球颜色
bool isMoveUp = false; // 小球是否向四个方向移动
bool isMoveDown = false;
bool isMoveLeft = false;
bool isMoveRight = false;
} Ball;

int main()
{
bool running = true;
ExMessage msg;
Ball ball = { 300, 300, 20, 5, 5, YELLOW }; // 创建小球并初始化
initgraph(WIN_WIDTH, WIN_HEIGHT);
BeginBatchDraw();

while (running)
{
DWORD beginTime = GetTickCount(); // 记录循环开始时间

// 消息处理
while (peekmessage(&msg))
{
if (msg.message == WM_KEYDOWN) // 按下按键处理
{
switch (msg.vkcode)
{
case 'w':
case 'W':
case VK_UP:
ball.isMoveUp = true;
break;

case 's':
case 'S':
case VK_DOWN:
ball.isMoveDown = true;
break;

case 'a':
case 'A':
case VK_LEFT:
ball.isMoveLeft = true;
break;

case 'd':
case 'D':
case VK_RIGHT:
ball.isMoveRight = true;
break;

case VK_ESCAPE:
running = false;
break;
}
}
if (msg.message == WM_KEYUP) // 松开按键处理
{
switch (msg.vkcode)
{
case 'w':
case 'W':
case VK_UP:
ball.isMoveUp = false;
break;

case 's':
case 'S':
case VK_DOWN:
ball.isMoveDown = false;
break;

case 'a':
case 'A':
case VK_LEFT:
ball.isMoveLeft = false;
break;

case 'd':
case 'D':
case VK_RIGHT:
ball.isMoveRight = false;
break;
}
}
}

// 斜向移动:计算不同方向(包括同时)按下时的速度增量
int directX = ball.isMoveRight - ball.isMoveLeft;
int directY = ball.isMoveDown - ball.isMoveUp;
double directXY = sqrt(directX * directX + directY * directY);
if (directXY != 0)
{
double factorX = directX / directXY; //计算X、Y方向的标准化分量
double factorY = directY / directXY;
ball.x += (int)ball.dx * factorX; //小球坐标 = 方向增速 * 方向的标准化分量
ball.y += (int)ball.dy * factorY;
}

// 边缘检测
if (ball.y - ball.r <= 0) // 上
ball.y = ball.r;
if (ball.y + ball.r >= WIN_HEIGHT) // 下
ball.y = WIN_HEIGHT - ball.r - 1;
if (ball.x - ball.r <= 0) // 左
ball.x = ball.r;
if (ball.x + ball.r >= WIN_WIDTH) // 右
ball.x = WIN_WIDTH - ball.r - 1;

// 绘图
cleardevice(); // 清除屏幕
setfillcolor(ball.color); // 设置填充颜色
solidcircle(ball.x, ball.y, ball.r); // 绘制无边框填充圆;
FlushBatchDraw();

// 帧延时
DWORD endTime = GetTickCount(); // 记录循环结束时间
DWORD elapsedTime = endTime - beginTime; // 计算循环耗时
if (elapsedTime < 1000 / 60) // 按每秒60帧进行补时
Sleep(1000 / 60 - elapsedTime);
}

EndBatchDraw();
closegraph();
return 0;
}

其它函数

设置窗口标题

范例:使用 GetHWndSetWindowText 函数设置窗口标题

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <graphics.h>

int main()
{
initgraph(600, 600);

HWND hWnd = GetHWnd(); // 获得窗口句柄
SetWindowText(hWnd, _T("植物大战僵尸")); // 使用 Windows API 修改窗口名称

system("pause");
closegraph();
return 0;
}

弹窗消息

在Visual C++(VC)中,MessageBox 函数是一个常用的 Windows API 函数,用于显示一个模态对话框,其中包含文本、标题、图标和按钮等。以下是函数的详细用法:

1
2
3
4
5
6
int MessageBox(  
HWND hWnd, // 父窗口句柄。如果为NULL,则消息框没有父窗口
LPCTSTR lpText, // 要显示的消息文本
LPCTSTR lpCaption, // 消息框的标题
UINT uType // 指定消息框的内容和行为的标志
);

参数说明

  1. hWnd:指定消息框的父窗口句柄。如果此参数为NULL,则消息框没有父窗口,且作为顶级窗口显示。
  2. lpText:要在消息框中显示的文本。
  3. lpCaption:消息框的标题。如果此参数为NULL,则默认标题为“Error”。
  4. uType:用于指定消息框的内容和行为的标志。这可以是一个或多个以下常量的组合:
    • MB_OK:消息框包含一个“确定”按钮。
    • MB_OKCANCEL:消息框包含“确定”和“取消”按钮。
    • MB_YESNO:消息框包含“是”和“否”按钮。
    • MB_YESNOCANCEL:消息框包含“是”、“否”和“取消”按钮。
    • MB_ICONEXCLAMATION、MB_ICONWARNING、MB_ICONINFORMATION、MB_ICONQUESTION、MB_ICONERROR等:用于指定消息框中显示的图标。

返回值

函数返回一个整数值,表示用户点击的按钮。例如:

  • IDOK:用户点击了“确定”按钮。
  • IDCANCEL:用户点击了“取消”按钮。
  • IDYES:用户点击了“是”按钮。
  • IDNO:用户点击了“否”按钮。
1
2
3
4
5
6
7
8
9
10
11
12
#include <graphics.h>

int main()
{
initgraph(1000, 600);

HWND hWnd = GetHWnd();
MessageBox(hWnd, _T("你被僵尸吃掉了!"), _T("游戏结束"), MB_OK | MB_ICONERROR);

closegraph();
return 0;
}

注:在使用 MessageBox 函数之前,需要包含 windows.h 头文件(如果已经包含了 graphics.h 头文件则可以省略

播放音频

mciSendString 是 Windows API 中的一个函数,用于向媒体控制接口(Media Control Interface,MCI)设备发送命令字符串。这个函数常用于控制多媒体设备,如音频和视频播放,支持 MPEG, AVI, WAV, MP3 等多种格式。

范例:播放背景音乐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <graphics.h>
//#include <windows.h> // 此项在导入graphics.h头文件后可以省略
#pragma comment(lib, "winmm.lib") // 加载多媒体静态库

int main()
{
initgraph(1000, 600);

mciSendString(_T("open audio\\bg.mp3 alias BGM"), 0, 0, 0); // 打开音乐文件,alias指定别名
mciSendString(_T("play BGM repeat"), 0, 0, 0); // 使用别名播放音乐,repeat重复播放

IMAGE img;
loadimage(&img, _T("image\\background.jpg"));
putimage(0, 0, &img);

system("pause");
closegraph();
return 0;
}