C++: 面向对象

封装

属性和行为

#include <iostream>
using namespace std;

// 面向对象三大特性:封装、继承、多态
// 封装指属性和行为的封装

/*
封装的语法:

class 类名 {
访问权限:
    属性(成员变量)
    行为(成员函数)
};
*/

class Hero {
    // 访问权限有3种:public, private, protected
public:
    // 属性:即声明变量
    int m_Id;  // m代表member成员
    int m_Hp;

    // 行为:即定义函数
    void addHp(int hp) {
        m_Hp += hp;
    }

    void subHp(int hp) {
        m_Hp -= hp;
    }

};

int main() {
    // 通过类来生成对象的过程,叫 实例化
    Hero h;
    // 访问对象的属性
    h.m_Id = 5;
    h.m_Hp = 100;
    h.addHp(100);
    cout << "Id 为" << h.m_Id << "的英雄,血量是" << h.m_Hp << endl;
    h.subHp(100);
    cout << "Id 为" << h.m_Id << "的英雄,血量是" << h.m_Hp << endl;

    return 0;
}

访问权限

#include <iostream>
using namespace std;

/*
访问权限:
公共权限    public      类内可以访问,类外也可以访问
保护权限    protected   类内可以访问,类外不可以访问  子类可以访问
私有权限    private     类内可以访问,类外不可以访问  子类不可以访问

B -> A

A 父类、基类     名字、房子、支付密码
B 子类、派生类    公有、保护、私有
*/

class People {
    // 公有权限
public:
    int m_Id;
    // 保护权限
protected:
    int m_HouseId;
    // 私有权限
private:
    int m_PayPass;

public:
    void work() {
        // 所有成员变量,类内均可访问
        m_Id = 1;
        m_HouseId = 2;
        m_PayPass = 1314;
    }
private:
    void work1() {
        // 所有成员变量,类内均可访问
        m_Id = 1;
        m_HouseId = 2;
        m_PayPass = 1314;
    }
};

class Son : public People {
    void func() {
        m_Id = 1;
        m_HouseId = 4;      // 保护成员,子类可以访问
        //m_PayPass = 123;  // 私有成员,子类无法访问
    }
};

int main() {
    // 实例化
    People p;
    p.m_Id = 1;       // 公有成员,类外可以访问
    //p.m_HouseId = 5;  // 保护成员,类外不可以访问
    //p.m_PayPass = 10; // 私有成员,类外不可以访问
    p.work();
    //p.work1();        // 私有成员函数,类外不可以访问

    return 0;
}

class和struct

#include <iostream>
using namespace std;

/*
struct 和 class 的区别

struct 的权限默认是公共的
class  的权限默认是私有的
*/

class C {
    int m_a;
};

struct S { // C++中的struct是可以定义成员函数的,C语言中的struct不行
    int m_a;
    void func() {
        m_a = 666;
    }
};

int main() {
    C c;
    S s;
    //c.m_a;     // 私有的
    s.m_a = 4; // 公有的
    s.func();
    cout << s.m_a << endl; // 666

    return 0;
}

属性私有化

#include <iostream>
#include <string>
using namespace std;

// 接口、方法、函数 是同一个概念
// 成员属性私有化好处:
// 1、可以控制读写权限
// 2、可以检测数据的有效性

class Hero {
public:
    void SetName(string name) {
        m_Name = name;
    }
    string GetName() {
        return m_Name;
    }
    int GetSkillCount() {
        return m_SkillCount;
    }
    void SetSpeed(int s) {
        if (s < 100 || s > 500) {
            cout << "速度设置不合法" << endl;
            return;
        }
        m_Speed = s;
    }
private:
    string m_Name;          // 可读,可写
    int m_SkillCount = 4;   // 只读
    int m_Speed;            // 只写
};
int main() {
    Hero h;
    /*
        h.m_Name = "123";
        h.m_SkillCount = 4;
        h.m_Speed = 10;
    */
    h.SetName("剑圣");
    cout << "英雄的名字叫:" << h.GetName() << endl;
    cout << "英雄的技能树是:" << h.GetSkillCount() << endl;
    h.SetSpeed(666);

    return 0;
}

对象特性

构造函数

对对象进行初始化。

如果程序员没有主动实现构造函数,编译器就会提供默认的构造函数,里面没有任何实现,是个空实现。

#include <iostream>
#include <string>
using namespace std;

/*
构造函数需要注意的点:

1、函数名称和类名保持一致
2、返回值类型 不需要写
3、构造函数可以有参数
*/
class Hero {
public:
    // 默认构造函数
    Hero() {
        m_Name = "";
        m_SkillCount = 4;
        m_Speed = 100;
        cout << "默认构造函数:Hero 构造完毕" << endl;
    }
    // 有参构造函数1
    Hero(string name) {
        m_Name = name;
        m_SkillCount = 4;
        m_Speed = 100;
        cout << "有参构造函数1:Hero 构造完毕" << endl;
    }
    // 有参构造函数2
    Hero(string name, int skillCount) {
        m_Name = name;
        m_SkillCount = skillCount;
        m_Speed = 100;
        cout << "有参构造函数2:Hero 构造完毕" << endl;
    }
private:
    string m_Name;
    int m_SkillCount;
    int m_Speed;
};
int main() {
    Hero h1;         // 调用默认的构造函数
    Hero h2("剑圣"); // 调用有参构造函数
    Hero h3();       // 这是个函数声明。int main(); 没有调用任何构造函数。这个并不是对象的实例化过程
    Hero h4{};       // 无参,调用默认的构造函数
    Hero h5 = Hero("剑圣"); // 调用有参构造函数.
    Hero h6("猴子", 4); // 调用有参构造函数

    return 0;
}

执行结果

默认构造函数:Hero 构造完毕
有参构造函数1:Hero 构造完毕
默认构造函数:Hero 构造完毕
有参构造函数1:Hero 构造完毕
有参构造函数2:Hero 构造完毕

析构函数

给对象进行反初始化。数据清理。

#include <iostream>
using namespace std;

/*
析构函数注意点:

1、函数名称号类名一致,并在最前面加上一个 ~ 波浪号
2、函数返回值不需要写
3、不能有参数

*/
class Hero {
public:
    // 构造函数
    Hero() {
        cout << "Hero 默认构造函数调用完毕!" << endl;
    }
    // 析构函数,在构造函数前加个~波浪号
    ~Hero() { // 析构函数中没有参数
        cout << "Hero 析构函数调用完毕!" << endl;
    }
};

void test() {
    Hero h;
}
int main() {
    test(); // 作用域调用完毕
    Hero h;
    int a;
    cin >> a;
    return 0;
}

执行结果

Hero 默认构造函数调用完毕!
Hero 析构函数调用完毕!
Hero 默认构造函数调用完毕!
6
Hero 析构函数调用完毕!

拷贝构造函数

拷贝构造函数也是一种构造函数。目的是用一种对象构造出另一个对象。

#include <iostream>
using namespace std;

/*
拷贝构造函数定义
类名(const 类名& 变量名){}

引用的目的是避免在调用时传参传进来时多一层拷贝
const目的是让传进来的参数不会被改变
*/
class Hero {
public:
    // 默认构造函数
    Hero() {
        m_Hp = 100;
        cout << "Hero 默认构造函数调用完毕" << endl;
    }
    // 有参构造函数
    Hero(int hp) {
        m_Hp = hp;
        cout << "Hero 有参构造函数调用完毕" << endl;
    }
    // 析构函数
    ~Hero() {
        cout << "Hero 析构函数调用完毕" << endl;
    }
    // 拷贝构造函数
    Hero(const Hero& h) {
        //h.m_Hp = 4; // 报错
        m_Hp = h.m_Hp;
        cout << "Hero 拷贝构造函数调用完毕" << endl;
    }
private:
    int m_Hp;
};

/*
拷贝构造函数的调用时机

1、用已经创建的对象来初始化对象
2、函数的传参
3、函数的返回值
*/

// 1、用已经创建的对象来初始化对象
void func1() {
    cout << "-----------func1----------" << endl;
    Hero h1(20); // 生成实例化对象
    Hero h2(h1); // 拷贝实例化对象 用h1生成h2的对象
}

// 2、函数的传参
void test1(Hero h) {

}
void test2(Hero* h) {

}
void func2() {
    cout << "-----------func2----------" << endl;
    Hero h1;
    //test1(h1); // 会调用拷贝构造函数
    test2(&h1);  // 没用调用拷贝构造函数。因为指针代表地址,并没有生成新的对象
}

// 3、函数的返回值
// IDE关闭优化,右键文件属性,在命令行中其它选项填写 /Zc:nrvo-
Hero test3() {
    Hero h(40);
    return h;
}
void func3() {
    cout << "-----------func3----------" << endl;
    Hero h = test3();
}

int main() {
    func1();
    func2();
    func3();
    return 0;
}

执行结果

-----------func1----------
Hero 有参构造函数调用完毕
Hero 拷贝构造函数调用完毕
Hero 析构函数调用完毕
Hero 析构函数调用完毕
-----------func2----------
Hero 默认构造函数调用完毕
Hero 析构函数调用完毕
-----------func3----------
Hero 有参构造函数调用完毕
Hero 拷贝构造函数调用完毕
Hero 析构函数调用完毕
Hero 析构函数调用完毕

初始化列表

#include <iostream>
#include <string>
using namespace std;

/*
初始化列表的语法

构造函数(传参1, 传参2): 成员变量1(传参1), 成员变量2(传参2) {}

*/
class Hero {
public:
    /*Hero(string name, int hp) {
        m_Name = name;
        m_Hp = hp;
    }*/
    Hero(string name, int hp, int speed) : m_Name(name), m_Hp(hp), m_Speed(speed) {
    }

    void Print() {
        cout << "英雄:" << m_Name << "的血量是" << m_Hp << ",速度是" << m_Speed << endl;
    }

private:
    string m_Name;
    int m_Hp;
    int m_Speed;
};
int main() {
    Hero h("剑圣", 100, 10);
    h.Print();

    return 0;
}

静态成员变量

在声明成员变量前,最前面加上 static.

#include <iostream>
#include <string>
using namespace std;

/*
静态成员变量特点:

1、所有的对象共享同一份数据
2、编译阶段分配内存
3、需要在类中进行声明,在类外进行初始化
*/
class Hero {
public:
    Hero() {
        m_Name = "英雄";
        m_Hp = 100;
    }

    ~Hero() {

    }

    // 3.1 声明
    static int m_HeroCount; // 类中声明静态成员变量
private:
    string m_Name;
    int m_Hp;
};

// 3.2 初始化
int Hero::m_HeroCount = 100; // 类外,静态变量需要初始化。作用域在Hero中,类名::变量

int main() {
    Hero h;
    cout << h.m_HeroCount << endl; // 100
    h.m_HeroCount = 101;
    cout << Hero::m_HeroCount << endl; // 101

    cout << &(h.m_HeroCount) << endl;    // 地址是一样的,说明是同一变量
    cout << &(Hero::m_HeroCount) << endl;
    return 0;
}

静态成员函数

也是所有成员对象共享一个函数,并且静态成员函数只能用到静态成员变量。

#include <iostream>
#include <string>
using namespace std;

/*
静态成员函数特点:
1、所有成员共享函数
2、静态成员函数只能使用静态成员变量,无法使用普通成员变量
*/

class Hero {
public:
    Hero() {
        m_Name = "英雄";
        m_Hp = 100;
    }
    ~Hero() {

    }

    static int m_HeroCount;

    static int GetHeroCount() {
        //m_Hp += 1; // 报错。只能操作静态成员变量
        return m_HeroCount;
    }
private:
    string m_Name;
    int m_Hp;

    static int GetHeroCount1() {
        //m_Hp += 1; // 报错。只能操作静态成员变量
        return m_HeroCount;
    }

};

int Hero::m_HeroCount = 100;

int main() {
    Hero h;
    cout << h.GetHeroCount() << endl; // 100
    cout << Hero::GetHeroCount() << endl; // 100
    //cout << h.GetHeroCount1() << endl; // 类外访问不到私有权限的静态成员函数

    return 0;
}

this指针

#include <iostream>
using namespace std;

/*
this指针
1、解决命名冲突。当构造函数构造时,传的变量与类中的成员变量一样的情况引用this指针
2、*this就可以获取到这个对象本身

this  &h
*this *(&h) == h
*/

class Hero {
public:
    Hero(int hp) { 
        //hp = hp; // 当前形参和成员变量一致时,成员变量就被隐藏掉了。形参把它覆盖了
        this->hp = hp;
        cout << this << endl;
        cout << (*this).hp << endl;
    }
    int hp;
};

int main() {
    Hero h(100);
    cout << h.hp << endl;
    cout << &h << endl;
    cout << (*(&h)).hp << endl; // 

    return 0;
}

const修饰成员函数

在函数参数列表后面加上关键字const。不能修改成员属性的值。

#include <iostream>
#include <vector>  // STL 是C++中的标准模板库. 如其中的函数 bool empty() cons
using namespace std;

// 常函数
// 常函数无法修改成员变量的值
class Hero {
public:
    Hero():m_Hp(0) {} // m_Hp初始化为0
    int getHp() const {
        // m_Hp = m_Hp + 1;
        return m_Hp;
    }
    void setHp(int hp) {
        m_Hp = hp;
    }
private:
    int m_Hp;
};
int main() {
    const Hero h;
    //h.setHp(100); // 报错。 对于常量对象, 只能调用常函数
    h.getHp();

    return 0;
}

mutable关键字

#include <iostream>
using namespace std;

// mutable 和 const 是相对的
// 希望在常函数中修改成员变量的值,就会用到mutable

class Hero {
public:
    Hero() :m_Hp(0), m_getHpCounter(0){}

    int getHp() const {
        m_getHpCounter++;
        return m_Hp;
    }

    void printCounter() const {
        cout << "Counter: " << m_getHpCounter << endl;
    }
private:
    int m_Hp;
    mutable int m_getHpCounter;
};
int main() {
    Hero h;
    h.getHp(), h.getHp(), h.getHp(), h.getHp(), h.getHp(), h.getHp();
    h.printCounter();

    return 0;
}

友元

友元的目的是,让一个函数或一个类能够访问另一个类的私有成员。

全局函数作为友元

#include <iostream>
#include <string>
using namespace std;

/*
友元的目的

让一个类或者函数
能够访问另一个类的私有成员

友元的关键字:friend

三种友元
1、全局函数作为友元
2、类作为友元
3、成员函数作为友元
*/

//1、全局函数作为友元
class People {
    friend void friendVisit(People* p);
public:
    People() {
        m_House = "别墅";
        m_Car = "跑车";
    }
public:
    string m_House;

private:
    string m_Car;
};

void friendVisit(People* p) {
    cout << "好朋友来访问你的" << p->m_House << endl;
    cout << "好朋友来访问你的" << p->m_Car << endl;
}

int main() {
    People p;
    friendVisit(&p);

    return 0;
}

类作为友元

#include <iostream>
using namespace std;

// 类作为友元
// 让一个类去访问另一个类的私有成员
class People; // 声明。方便PeopleFriend提前调用

class PeopleFriend {
public:
    PeopleFriend() {

    }

    void visit(People* p); // 函数声明
};

class People {
    friend class PeopleFriend;
public:
    People() {
        m_House = "别墅";
        m_Car = "跑车";
    }
public:
    string m_House;
private:
    string m_Car;
};

// 函数定义,指定作用域PeopleFriend
void PeopleFriend::visit(People* p) {
    cout << "好朋友来访问你的" << p->m_House << endl;
    cout << "好朋友来访问你的" << p->m_Car << endl;
}

int main() {
    People p;
    PeopleFriend pf;
    pf.visit(&p);

    return 0;
}

成员函数作为友元

#include <iostream>
#include <string>
using namespace std;

// 成员函数作为友元
// PeopleFriend 的某个函数能访问 Peope 的私有成员变量

class People;

class PeopleFriend {
public:
    PeopleFriend() {}

    void visitAll(People* p);
    void visitPub(People* p);
};

class People {
    //friend class PeopleFriend;
    friend void PeopleFriend::visitAll(People* p);
public:  // 表示这是一装饰,无论如何都能获取到
    People() {
        m_House = "别墅";
        m_Car = "跑车";
    }
public:
    string m_House;
private:
    string m_Car;
};

void PeopleFriend::visitAll(People* p) {
    cout << "好朋友访问了你的" << p->m_House << endl;
    cout << "好朋友访问了你的" << p->m_Car << endl;

}
void PeopleFriend::visitPub(People* p) {
    cout << "好朋友访问了你的" << p->m_House << endl;
    //cout << "好朋友访问了你的" << p->m_Car << endl; // 报错,没指定这个函数为类的友元
}

int main() {
    People p;
    PeopleFriend pf;
    pf.visitAll(&p);
    pf.visitPub(&p);

    return 0;
}

友元定义时类的声明顺序误区

  • PeopleFriend 要放在People之前,否则在People定义友元时,是不知道PeopleFriend有visitAll这个友元函数的。

运算符重载

运算符重载概念

运算符重载就是对已有的运算符重新定义,并且对不同的数据结构做不同的处理。

#include <iostream>
#include <string> // string 自带加法运算符
using namespace std;

/*
+
4 + 5 = 9

class A {};

A a;
A b;
a + b;
*/
int main() {
    // 1、加法运算符
    int a = 520;
    int b = 1314;
    cout << a + b << endl;

    // 2、字符串拼接
    string c = "520";
    string d = "1314";
    cout << c + d << endl; // 这里的加号用到了运算符重载技术

    return 0;
}

执行结果

1834
5201314

加号重载

#include <iostream>
using namespace std;

/*
+
*/
// 复数类
class Complex {
    friend Complex operator+(Complex& a, Complex& b);
    friend Complex operator-(Complex& a, Complex& b);
public:
    Complex() :real(0), image(0) {

    }
    Complex(int real, int image) {
        this->real = real;
        this->image = image;
    }

    /* 成员函数可以实现运算符重载
    //Complex add(Complex& other) {
    Complex operator+(Complex& other) {
        Complex ret;
        ret.real = this->real + other.real;
        ret.image = this->image + other.image;
        return ret;
    }
    */

    // a + bi
    void Print() {
        cout << real << "+" << image << 'i' << endl;
    }
private:
    int real;
    int image;
};

// 全局函数同样可以实现运算符重载
Complex operator+(Complex& a, Complex& b) {
    Complex ret;
    ret.real = a.real + b.real;
    ret.image = a.image + b.image;
    return ret;
}
Complex operator-(Complex& a, Complex& b) {
    Complex ret;
    ret.real = a.real - b.real;
    ret.image = a.image - b.image;
    return ret;
}

int main() {
    Complex a(10, 20);
    Complex b(5, 8);
    //Complex c = a.add(b);
    Complex c = a + b;
    Complex d = a - b;
    c.Print(); // 15+28i
    d.Print(); // 5+12i

    return 0;
}

左移重载

左移重载

#include <iostream>
using namespace std;

/*
<<

Complex c;
cout.operator<<(c) // 要进行输出的话,一直要调用cout左移c,即cout << c。
// operator<<是cout的成员函数。但我们希望是Complex的成员函数

// 成员函数重载
c.operator<<(cout) // 会变成 c << cout,这不是我们想要的,希望是cout << c
// 所以要把它从成员函数中抽出变成全局函数
*/
// 复数类
class Complex {
    friend Complex operator+(Complex& a, Complex& b);
    friend ostream& operator<<(ostream& cout, Complex a);
public:
    Complex() :real(0), image(0) {

    }
    Complex(int real, int image) {
        this->real = real;
        this->image = image;
    }

private:
    int real;
    int image;
};

Complex operator+(Complex& a, Complex& b) {
    Complex ret;
    ret.real = a.real + b.real;
    ret.image = a.image + b.image;
    return ret;
}
/*
void operator<<(ostream& cout, Complex a) {
    cout << a.real << '+' << a.image << 'i' << endl;
}
*/
// cout 是 std::ostream std::count
ostream& operator<<(ostream& cout, Complex a) {
    cout << a.real << '+' << a.image << 'i' << endl;
    return cout;
}
int main() {
    Complex a(10, 20);
    Complex b(5, 8);

    Complex c = a + b;
    cout << c << endl; // (cout << c) 得到的值再左移 endl;
    return 0;
}

左移重载补充

左移对象的重载主要是为了实现对象的输出。

cout是个ostream类型,如果不加引用就会调用拷贝构造函数

ostream& operator<<(ostream& cout, Complex a) {
    cout << a.real << '+' << a.image << 'i' << endl;
    return cout;
}

ostream源码中有个拷贝构造函数

_EXPORT_STD using ostream       = basic_ostream<char, char_traits<char>>;

class basic_ostream : virtual public basic_ios<_Elem, _Traits> { // control insertions into a stream buffer
  ...
public:
  __CLR_OR_THIS_CALL basic_ostream(const basic_ostream&)            = delete; // 无法调用拷贝构造函数。有delete关键字修饰后,任何地方都无法调用这个函数
  // 目的是为了全局只有一个 cout
  basic_ostream& __CLR_OR_THIS_CALL operator=(const basic_ostream&) = delete;
  ...

ostream加了引用就不会调用拷贝构造函数了。

但既然不想被外部调用,为什么不放私有属性里?原因是即不想外部调用也不想内部调用。

cout 必须加上引用

递增重载

递增重载

#include <iostream>
using namespace std;

/*
++

前置++
后置++
*/

class Complex {
    friend ostream& operator<<(ostream& c, Complex a);
public:
    Complex():real(0), image(0) {}
    Complex(int real, int image) {
        this->real = real;
        this->image = image;
    }

    // 前置++返回这个对象的引用
    Complex& operator++() {
        this->real += 1;
        return *this; // 返回自己
    }

    // 后置++的额外参数类型必须是int,返回原值
    Complex operator++(int) {
        Complex c = *this;
        this->real += 1;
        return c;
    }

private:
    int real;
    int image;
};

ostream& operator<<(ostream& c, Complex a) {
    c << a.real << '+' << a.image << 'i';
    return c;
}
int main() {
    int x = 1;
    cout << ++(++x) << endl; // 3
    cout << x << endl; // 3

    Complex a(10, 10);
    cout << a << endl; // 10+10i
    ++a;
    cout << a << endl; // 11+10i
    cout << ++a << endl; // 12+10i
    cout << ++(++a) << endl; // 14+10i
    cout << a << endl; // 14+10i

    cout << a++ << endl; // 14+10i
    cout << a << endl; // 15+10i

    cout << ((a++)++)++ << endl; // 14+10i
    cout << a << endl; // 15+10i

    int b = 5;
    // cout << ((b++)++)++ << endl; // 不能这么调用
    cout << b << endl; // 15+10i

    return 0;
}

递增重载补充

非常量引用初始值必须是左值

#include <iostream>
using namespace std;

/*
++

前置++
后置++
*/

class Complex {
    friend ostream& operator<<(ostream& c, const Complex& a);
public:
    Complex():real(0), image(0) {}
    Complex(int real, int image) {
        this->real = real;
        this->image = image;
    }

    // 前置++返回这个对象的引用
    Complex& operator++() {
        this->real += 1;
        return *this; // 返回自己
    }

    // 后置++的额外参数类型必须是int,返回原值
    Complex operator++(int) {
        Complex c = *this;
        this->real += 1;
        return c;
    }

private:
    int real;
    int image;
};

ostream& operator<<(ostream& c, const Complex& a) {
    c << a.real << '+' << a.image << 'i';
    return c;
}

class A {

};
A func1() {  // 模拟Complex operator++
    return A();
}

//void func2(A& a) { // 模拟 ostream& operator<<(ostream& c, Complex& a)
void func2(const A& a) {  // 改为常量引用
}

int main() {
    int x = 1;
    cout << ++(++x) << endl; // 3
    cout << x << endl; // 3

    Complex a(10, 10);
    cout << a << endl; // 10+10i
    ++a;
    cout << a << endl; // 11+10i
    cout << ++a << endl; // 12+10i
    cout << ++(++a) << endl; // 14+10i
    cout << a << endl; // 14+10i

    cout << a++ << endl; // 14+10i
    cout << a << endl; // 15+10i

    cout << ((a++)++)++ << endl; // 14+10i
    cout << a << endl; // 15+10i

    //func2(func1()); // 报错。非常量引用的初始值必须为左值

    int b = 5;
    // cout << ((b++)++)++ << endl; // 不能这么调用
    cout << b << endl; // 15+10i

    return 0;
}

赋值重载

#include <iostream>
using namespace std;

class Hero {
public:
    Hero() :m_Data(NULL) {}
    Hero(int data) {
        m_Data = new int;
        *m_Data = data;
    }
    // double free
    ~Hero() {
        if (m_Data) {
            delete m_Data;
            m_Data = NULL;
        }
    }
    int* m_Data;
};
int main() {
    Hero h1(1);
    Hero h2(2);
    cout << h1.m_Data << endl;
    cout << h2.m_Data << endl;
    h1 = h2; // h1中的m_Data被覆盖掉了。 经典问题:内存泄漏
    cout << h1.m_Data << endl;
    cout << h2.m_Data << endl;

    return 0;
}

赋值重载

#include <iostream>
using namespace std;

class Hero {
public:
    Hero() :m_Data(NULL) {}
    Hero(int data) {
        m_Data = new int;
        *m_Data = data;
    }
    // double free
    ~Hero() {
        if (m_Data) {
            delete m_Data;
            m_Data = NULL;
        }
    }

    Hero& operator=(Hero& h) {
        if (m_Data) {
            delete m_Data;
            m_Data = NULL;
        }
        m_Data = new int;
        *m_Data = *h.m_Data;
        return *this;
    }
    int* m_Data;
};
int main() {
    Hero h1(1);
    Hero h2(2);
    Hero h3(3);

    cout << h1.m_Data << endl;
    cout << h2.m_Data << endl;
    h1 = h2;
    cout << h1.m_Data << endl;
    cout << h2.m_Data << endl;

    h3 = (h2 = h3);
    return 0;
}

关系运算重载

#include <iostream>
using namespace std;

class Point {
public:
    Point(int x, int y) :m_x(x), m_y(y) {}

    bool operator==(const Point& other) const { // 常函数
        return m_x == other.m_x && m_y == other.m_y;
    }

    bool operator<(const Point& other) const { // 常函数
        int d = m_x * m_x + m_y * m_y; // 自己到原点的距离
        int otherd = other.m_x * other.m_x + other.m_y * other.m_y;
        return d < otherd;
    }
    bool operator>(const Point& other) const {
        if (*this == other) {
            return false;
        }
        if (*this < other) {
            return false;
        }
        return true;
    }
    // 其它关系运算符也是一样的,略
private:
    int m_x, m_y;
};
int main() {
    Point a(1, 6);
    Point b(2, 5);

    // 如果类没有定义判断关系,== 是不知道的,需要用操作符重载
    if (a == b) {
        cout << "a 和 b 相等" << endl;
    }
    if (a < b) {
        cout << "a 比 b 更加接近原点" << endl;
    }
    else if (a > b) {
        cout << "b 比 a 更加接近原点" << endl;

    }

    return 0;
}

函数调用重载

类当函数调用,并且可以存储状态信息。

#include <iostream>
using namespace std;

// ()
// add()  构建一个对象,这个对象可以当函数用

class AddFunctor {
public:
    AddFunctor() {
        m_acc = 0;
    }
    int operator()(int a, int b) {
        m_acc++;
        return a + b + m_acc;
    }
private:
    int m_acc;
};

int Add(int a, int b) {
    return a + b;
}

int main() {
    AddFunctor add;
    cout << add(5, 6) << endl; // 12
    cout << add(5, 6) << endl; // 13
    cout << add(5, 6) << endl; // 14

    cout << Add(5, 6) << endl; // 普通的函数是不存储状态信息的
    cout << Add(5, 6) << endl; // 11
    cout << Add(5, 6) << endl; // 11

    return 0;
}

继承

继承的语法

普通的类

#include <iostream>
using namespace std;

/*
*       动物
*      /    \
*    猫      狗
*
*/

class Cat {
public:
    void eat() {
        cout << "吃" << endl;
    }
    void sayHi() {
        cout << "喵~" << endl;
    }
};

class Dog {
public:
    void eat() {
        cout << "吃" << endl;
    }
    void sayHi() {
        cout << "汪汪汪~" << endl;
    }
};
int main() {
    Cat c;
    Dog d;

    c.eat();
    d.eat();

    c.sayHi();
    d.sayHi();

    return 0;
}

继承

#include <iostream>
using namespace std;

/*
*       动物
*      /    \
*    猫      狗
*
* 继承的语法
* class 子类 : 继承方式 父类 {}
* 子类 又叫 派生类
* 父类 又叫 基类
*/

// 抽象成一个动物类,有共性部分。省掉冗余代码
class Animal {
public:
    void eat() {
        cout << "吃" << endl;
    }
};

class Cat : public Animal {
public:
    void sayHi() {
        cout << "喵~" << endl;
    }
};

class Dog : public Animal {
public:
    void sayHi() {
        cout << "汪汪汪~" << endl;
    }
};
int main() {
    Cat c;
    Dog d;

    c.eat();
    d.eat();

    c.sayHi();
    d.sayHi();

    return 0;
}

继承方式

访问权限

子类继承方式 父类访问权限 父类 父类
  public protected private
public public protected 无法访问
protected protected protected 无法访问
private private private 无法访问
#include <iostream>
using namespace std;

/*
class 子类名 : 继承方式 父类名 {};

继承方式:
公共 public
保护 protected
私有 private

继承权限有3种、访问权限有3种
3 * 3 = 9

             |    public     |     protected      |   private
  public     |    public     |     protected      |   无法访问
  protected  |   protected   |     protected      |   无法访问
  private    |    private    |      private       |   无法访问

public:   类内可以访问,类外也可以访问
protected:  类内可以访问,类外不可访问,且子类可以访问
private:    类内可以访问,类外不可访问,且子类不可访问
*/

class Animal {
public:
    int m_pub;
protected:
    int m_pro;
private:
    int m_pri;
};

class Cat : public Animal {
public:
    Cat() {
        m_pub = 1;
        m_pro = 2; 
        //m_pri = 3; // 父类私有成员,子类公有继承,无法访问
    }
};

class BossCat : public Cat {
public:
    BossCat() {
        m_pro = 2; // 父类Cat中不是私有
    }
};

void testCat() {
    Cat c;
    c.m_pub = 1;
    //c.m_pro = 2; // 要么是私有,要么是保护成员
}

class Dog : protected Animal {
public:
    Dog() {
        m_pub = 1;
        m_pro = 2;
        //m_pri = 3; // 父类私有成员,子类保护继承,无法访问
    }
};

void testDog() {
    Dog d;
    //d.m_pub = 1;  // 要么是保护,要么是私有
    //c.m_pro = 2;  // 要么是保护,要么是私有
}

class PolicDog : public Dog {
public:
    PolicDog() {
        m_pub = 1; // 这个变量,在父类中Dog中一定不是私有成员
        m_pro = 2; // 这个变量,在父类中Dog中一定不是私有成员
    }
};

void testDog() {
    Dog d;
    // d.m_pub = 1;   // 要么是保护,要么是私有
    // d.m_pro = 2;   // 要么是保护,要么是私有
}

class Pig : private Animal {
public:
    Pig() {
        m_pub = 1;
        m_pro = 2;
        // m_pri = 3;   // 父类私有成员,子类私有继承,无法访问
    }
};

class WildPig : public Pig {
public:
    WildPig() {
        // m_pub = 1;   // 该变量在父类 Pig 中是私有的
        // m_pro = 2;   // 该变量在父类 Pig 中是私有的
    }
};

void testPig() {
    Pig p;
    // p.m_pub = 1;     // 要么是保护,要么是私有
    // p.m_pro = 2;     // 要么是保护,要么是私有
}

int main() {

    return 0;
}

构造和析构顺序

#include <iostream>
using namespace std;

/*
继承中,构造链里,先先构造的后析构

继承 d -> c -> b -> a

构造顺序为 a b c d 析构顺序为 d c b a
*/
class Animal {
public:
    Animal() {
        cout << "Animal 构造" << endl;
    }
    ~Animal() {
        cout << "Animal 析构" << endl;
    }
};

class Cat : public Animal {
public:
    Cat() {
        cout << "Cat 构造" << endl;
    }
    ~Cat() {
        cout << "Cat 析构" << endl;
    }
};

class BossCat : public Cat {
public:
    BossCat() {
        cout << "BossCat 构造" << endl;
    }
    ~BossCat() {
        cout << "BossCat 析构" << endl;
    }
};

void test() {
    //Animal a;
    //Cat c;
    BossCat bc;
}
int main() {
    test();

    return 0;
}

执行结果

Animal 构造
Cat 构造
BossCat 构造
BossCat 析构
Cat 析构
Animal 析构

同名属性访问

同名属性,父类和子类都有一个相同的变量。

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() {
        m_Data = 17981;
    }
    int m_Data;
};

class Cat : public Animal {
public:
    Cat() {
        m_Data = 29872;
    }
    int m_Data;
};

void test() {
    // 当子类继承父类,并且有同一个变量名时,并不会进行覆盖,需要用一个作用域进行访问就好了。
    Cat c;
    cout << c.m_Data << endl;
    cout << c.Animal::m_Data << endl;

    cout << &(c.m_Data) << endl;
    cout << &(c.Animal::m_Data) << endl;
}

int main() {
    test();

    return 0;
}

同名函数访问

同名函数,父类和子类有一个名字相同的函数。那么子类会不会覆盖掉父类函数?

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() {

    }
    void eat() {
        cout << "动物吃东西" << endl;
    }
};

class Cat : public Animal {
public:
    Cat() {

    }
    void eat() {
        Animal::eat(); // 子类中调父类函数
        cout << "猫吃东西" << endl;
    }
};

int main() {
    Cat c;
    c.eat();
    //c.Animal::eat(); // 可以调父类函数

    return 0;
}

执行结果

动物吃东西
猫吃东西

多继承

多继承,子类有一个类,它可以继承多个父类。

#include <iostream>
using namespace std;

class BaseA {
public:
    int m_A;
    int m_Base;

    BaseA():m_A(0), m_Base(520) {}
};

class BaseB {
public:
    int m_B;
    int m_Base;

    BaseB() :m_B(0), m_Base(1314) {}
};

class BaseC {
public:
    int m_C;

    BaseC() :m_C(0) {}
};

class Son : public BaseA, public BaseB, public BaseC {

};
int main() {
    Son s;
    s.m_A = 1;
    s.m_B = 2;
    s.m_C = 3;

    //s.m_Base = 8; // 报错 不明确,父类A和父类B有同名变量
    s.BaseA::m_Base = 8;
    s.BaseB::m_Base = 9;
    cout << &s.BaseA::m_Base << endl; // 不同的地址
    cout << &s.BaseB::m_Base << endl;
    cout << sizeof(s) << endl; // 20 Son继承了5个int 5*4=20

    return 0;
}

多态

多态分为:静态多态、动态多态

静态多态主要是函数重载和运算符重载,之前已了解过。

动态多态主要是利用派生类和虚函数来实现运行时多态。

所有的东西统一通过代码来理解,概念不用去记它。

多态的语法

#include <iostream>
using namespace std;

/*
多态实现:子类重写了父类的虚函数
*/
class Animal {
public:
    // 虚函数 关键字virtual
    virtual void eat() {
        cout << "动物在吃东西" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() {
        cout << "猫在吃东西" << endl;
    }
};

class Pig : public Animal {
public:
    void eat() {
        cout << "猪在吃东西" << endl;
    }
};

// 调用链 main -> test -> eat -> Animal::eat
// 函数传参是个动物,但是传入不同的动物会产生不同的行为,这就叫多态
void eat(Animal& a) {
    a.eat();
}

void test() {
    Cat c;
    Pig p;
    eat(c);
    eat(p);
}

int main() {
    test();

    return 0;
}

虚函数

多态原理:

  • 调用子类对象的虚函数时,会调用到子类自己的函数。
  • 一旦有虚函数时,会生成虚函数指针,它保存了这个类上所有的虚函数的地址。并且在子类继承时会覆盖掉虚函数。

虚函数:

#include <iostream>
using namespace std;

/*
多态实现:子类重写了父类的虚函数
*/
class Animal {
public:
    // 虚函数 关键字virtual
    virtual void eat() {
        cout << "动物在吃东西" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() {
        cout << "猫在吃东西" << endl;
    }
};

class Pig : public Animal {
public:
    void eat() {
        cout << "猪在吃东西" << endl;
    }
};

// 调用链 main -> test -> eat -> Animal::eat
// 函数传参是个动物,但是传入不同的动物会产生不同的行为,这就叫多态
void eat(Animal& a) {
    a.eat();
}

void test() {
    Cat c;
    Pig p;
    eat(c);
    eat(p);
}

int main() {
    test();

    return 0;
}

调试:

  • eat(c) 处打断点,F5调试,F11进入, F10
  • 监视中 输入a 和 c,观察
img_20251022_140000.png

纯虚函数和抽象类

有时候在写代码时,父类中的函数有可能没有任何实现,都需要子类实现(重写),这就是纯虚函数的目的。

#include <iostream>
using namespace std;

class Animal { // 只要有纯虚函数的类,叫抽象类。抽象类无法实例化对象,子类必须重写这个函数。
public:
    virtual void eat() = 0; // 纯虚函数
};

class Cat : public Animal {
public:
    void eat() {
        cout << "猫在吃东西" << endl;
    }
};

int main() {
    Cat c;
    c.eat();

    return 0;
}

虚析构和纯虚析构

虚析构主要解决内存泄漏问题。

比如,有个父类的指针指向派生类对象,对它进行销毁时,如果父类函数不是virtual就会掉用不到子类的析构函数,从而导致内存泄漏。

#include <iostream>
using namespace std;

class BaseA {
public:
    BaseA() {}
    ~BaseA() {
        cout << "BaseA 销毁了" << endl;
    }
};

class SonA : public BaseA {
public:
    SonA() : m_Value(NULL) {
        m_Value = new int(10);
    }
    ~SonA() {
        cout << "SonA 销毁了" << endl;
        delete m_Value;
    }

    int* m_Value;
};

class BaseB {
public:
    BaseB() {}
    /*
    virtual ~BaseB() { // 虚析构
        cout << "BaseB 销毁了" << endl;
    }
    */
    virtual ~BaseB() = 0; // 纯虚析构。需要在外部实现一下
};

BaseB::~BaseB() {
    cout << "BaseB 销毁了" << endl;
}

class SonB : public BaseB {
public:
    SonB() : m_Value(NULL) {
        m_Value = new int(10);
    }
    ~SonB() {
        cout << "SonB 销毁了" << endl;
        delete m_Value;
    }

    int* m_Value;
};

int main() {
    // 在堆上分配一个SonA对象,指向基类的指针
    BaseA* a = new SonA();
    delete a;  // 执行结果 BaseA 销毁了 。 但是SonA堆中内存m_Value new了但并没有被销毁,会导致内存泄漏
    // 加上virtual就可以了

    BaseB* b = new SonB();
    delete b;

    //BaseB c; // 抽象类,不能实例化

    return 0;
}

执行结果

BaseA 销毁了
SonB 销毁了
BaseB 销毁了

项目-C++游戏开发-扫雷

扫雷-面向对象编程

  • 资源 mine.png
  • 常量 const.h
  • 格子 Grid.h, Grid.cpp
  • 地图 Map.h, Map.cpp
  • 主函数 man.cpp

预计 100 行左右的代码,实际上可以更短,但是为了让代码相对较清晰,有些代码不能省,那我们现在开始。

环境安装

visual studio 2022 下载安装 https://visualstudio.microsoft.com/zh-hans

环境配置

首先要下载 SFML:https://www.sfml-dev.org/download/sfml/2.6.1/, Visual C++ 17(2022) 解压到本地放代码的文件夹。如D:\tmp\game\SFML-2.6.1

然后创建一个项目,项目创建完以后,在 main.cpp 中敲入第一句代码,Graphics.hpp 这个头文件通常包含了与图形绘制、窗口管理 等相关的函数声明 以及 类的定义。通过包含这个头文件,程序就能够使用 SFML 库提供的图形处理功能,具体哪些功能呢?比如 创建窗口、绘制图形、处理输入 等等操作。

minesweeper/main.cpp

#include <SFML/Graphics.hpp>

这时候我们发现,无法打开这个文件。因为需要进行简单的配置

  • 点击项目,右键属性,选择这里的 C/C++,选择【附加包含目录】,选择 SFML 的 include 目录,点击确定。
    • D:\tmp\game\SFML-2.6.1\include
  • 然后点击 链接器 -> 常规 -> 附加库目录,选择 SFML 的 lib 目录:
    • D:\tmp\game\SFML-2.6.1\lib
  • 再点击 链接器 ->输入,选择右边的【附加依赖项】,把这些内容拷贝进去:

(这里需要注意,对于 sfml-xxx-d.lib 是适用于 debug 模式的,sfml-xxx.lib 是适用于 release 模式的)

sfml-graphics-d.lib
sfml-window-d.lib
sfml-system-d.lib
sfml-audio-d.lib
opengl32.lib
freetype.lib
winmm.lib
gdi32.lib

最后点击应用,就配置完毕了。

然后再把 SFML-2.6.1\bin 目录下的 动态链接库文件 dll 都拷贝到项目目录下,即与main.cpp同级目录。

然后我们写个 main 函数来测试一下。

#include <SFML/Graphics.hpp>

int main() {
    sf::RenderWindow app(sf::VideoMode(816, 576), "MineSweeper");
    while (app.isOpen()) {

    }
    return 0;
}

这样一来,就可以显示一个基本的窗口了,接下来我们来解释解释这些代码的含义。

基础框架

命名空间

sf 是这个库的命名空间,基本上所有的接口都是从这里取的,利用两个冒号来获取相应的函数或者类,如果不想写这段前缀呢,我们可以和 std 一样,在代码前面写上这么一句话:

using namespace sf;

这样一来,这些前缀就都可以去掉了。

窗口创建

RenderWindow win(VideoMode(816, 576), "MineSweeper");

这段代码,就是实例化了一个窗口对象,RenderWindow 是一个类,win 是一个对象名,这一段就是有参构造函数的传参,我们可以 F12 进去看它的定义。

这里的 VideoMode 也是一个类,这两个参数是 VideoMode 构造函数的传参,分别代表了窗口的宽高。

字符集

VideoMode 中构造函数的第二个传参,是一个字符串,代表了窗口的标题,然后我们实现一个 while 循环。

while (win.isOpen()) {

}

并且循环条件是 win.isOpen() 为 True ,这个函数的含义就是当窗口 win 被打开的时候就返回真,那么一旦关闭,就会返回假,这时候程序就会结束掉,我们来运行一下。

我们看到窗口左上角有个标题,这时候我希望这个标题是中文的,可以改下这个字符串。

这时候就会发现,标题变成了乱码

为什么呢?我们来看下这个字符串的类型,是一个字符数组,它最初是为了处理英文字符而设计的,只能表示基本的 ASCII 字符集(一个字节表示一个字符)。而中文字符,通常需要多个字节来表示,不同的中文编码方式(如 GBK、UTF-8 等)占用的字节数可能不同。

所以如果使用char类型来存储中文字符时,如果编码方式不匹配,就容易出现乱码。例如,在某些编码方式下,一个中文字符可能占用 2 个或更多字节,如果按照单个字节来读取或处理,就会导致字符的不完整或错误解释。

这时候我们只需要在前面加上一个 L,再把鼠标移动上去,就会发现,它的类型变成了 wchar_t,这个叫宽字符,它一般占用 2 个或者以上字节。使用wchar_t可以处理包含中文字符的情况。

#include <SFML/Graphics.hpp>
using namespace sf;

int main() {
    RenderWindow app(VideoMode(816, 576), L"扫雷");
    while (app.isOpen()) {

    }
    return 0;
}

运行一下, 这样就没问题了。

关闭事件

这时候,当我们点击窗口的时候,会出现卡顿,并且未响应,这个原因是什么呢?

  • 因为我们没有处理事件,首先定义一个事件的类,然后调用 pollEvent 这个接口,就可以获取到事件队列里的最先来的那个事件,包括 鼠标事件、键盘事件 等等。

那么再学完数据结构以后,你就能理解队列的概念了,就是一种先进先出的数据结构。

可以通过获取事件的类型 type ,知道这是一个什么样的事件,比如 Event::Closed 代表窗口关闭事件,也就是你点了右上角的关闭按钮。当然,F12进去以后,可以看到更多事件的定义。

可以在控制台里输出一句话,来印证我们的观点,调用 win.close() 以后,win 的 isOpen 接口就会返回假,这样程序也就结束了。运行一下。

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

int main() {
    RenderWindow win(VideoMode(816, 576), L"扫雷");

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

资源加载

mine.png

接下来我们准备好这么一张图片,如果不会画图,可以直接用我的这张图,用画图工具打开以后,大概是宽为 1152,高为 96 的图片,每个小格子的大小是 96 x 96。

怎么把这张图片上的某一个格子,在屏幕上显示出来,就是我们这堂课要讲的内容了。

纹理对象

首先我们创建一个纹理对象,并且把这张图加载到内存中,纹理是游戏开发中一个比较重要的概念,可以理解成贴图, 2D游戏中,不同的对象,让人能够产生不同的视觉效果,就是利用不同的纹理实现的。

mine.png图片文件放到项目目录下。

Texture t;
t.loadFromFile("mine.png");

精灵

然后我们再实例化一个精灵,并且把刚才准备好的纹理对象,作为初始化参数,传给它。精灵可以这么去理解,我拿到一个纹理的某一个矩形区域,然后可以对它进行平移、缩放、旋转 等等变换,然后绘制到屏幕上的这么一个东西,我们叫它精灵。

Sprite s(t);

比如我可以这样:

在原先这张纹理贴图上,(96, 0) 的坐标上,取出一个 (96, 96) 的矩形,并且设置坐标为 (16, 16),然后把它的缩放值设置为原来的 1/2 ,这样就变成了一个 48 x 48 的矩形,然后调用 draw 接口绘制到 win 对象上面去,这时候其实屏幕上还没有东西,直到调用 display 以后,才会真正把它绘制到窗口上。

可以这么去理解,draw 调用完,实际上还没有真正的绘制到窗口上,只是把要画的内容,画到了一张画布上面,display 调用完,才会最终把这张画布的内容,一次性绘制到你的窗口上。

s.setTextureRect(IntRect(96, 0, 96, 96));
s.setPosition(16, 16);
s.setScale(Vector2f(0.5, 0.5));
win.draw(s);
win.display();

接下来我们来写这么一段话:

int r = rand() % 12;
s.setTextureRect(IntRect(96 * r, 0, 96, 96));

随机一个 0 到 11 的数字,然后让它乘上 96 ,去改变这个纹理矩形左上角的 x 坐标,来看看效果,你会发现每一帧,都在改变图片,而实际上 0 到 11 就代表了扫雷这个游戏中,每个会用到的格子。

整体代码 main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

int main() {
    RenderWindow win(VideoMode(816, 576), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    int r = rand() % 12;
    s.setTextureRect(IntRect(96 * r, 0, 96, 96));
    s.setPosition(16, 16);
    s.setScale(Vector2f(0.5, 0.5));
    win.draw(s);
    win.display();

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

初始地图

定义一个 showGrid 的二维数组,是一个 15 列 x 10 行 的地图,代表实际显示出来的地图元素,一开始都为 10, 10 就是这个图片,代表的是一个未知的元素。

int showGrid[16][11];
for (int i = 1; i <= 15; ++i) {
    for (int j = 1; j <= 10; ++j) {
        showGrid[i][j] = 10;
    }
}

然后在绘制的时候,遍历每一个地图元素,处理精灵的纹理、位置以及缩放,并且绘制到 win 这个对象上。

for (int i = 1; i <= 15; ++i) {
    for (int j = 1; j <= 10; ++j) {
        s.setTextureRect(IntRect(96 * showGrid[i][j], 0, 96, 96));
        s.setPosition(i * 48, j * 48);
        s.setScale(Vector2f(0.5, 0.5));
        win.draw(s);
    }
}

最终一次性展现到窗口上,运行。这样我们就得到了一张初始的地图。

整体代码 main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

int main() {
    RenderWindow win(VideoMode(816, 576), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    int r = rand() % 12;
    s.setTextureRect(IntRect(96 * r, 0, 96, 96));
    s.setPosition(16, 16);
    s.setScale(Vector2f(0.5, 0.5));
    win.draw(s);
    win.display();

    int showGrid[16][11];
    for (int i = 1; i <= 15; ++i) {
        for (int j = 1; j <= 10; ++j) {
            showGrid[i][j] = 10;
        }
    }
    for (int i = 1; i <= 15; ++i) {
        for (int j = 1; j <= 10; ++j) {
            s.setTextureRect(IntRect(96 * showGrid[i][j], 0, 96, 96));
            s.setPosition(i * 48, j * 48);
            s.setScale(Vector2f(0.5, 0.5));
            win.draw(s);
        }
    }
    win.display();

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

常量定义

这个时候我们发现,有太多数字了,这个我们叫它们 magic number,很难看,而且维护起来极其麻烦,所以我们想办法把数字变成常量。

首先引入第一个常量:

const int ORI_GRID_SIZE = 96;

它代表了在这张图片中,每个格子的像素大小,是 96。

const int GRID_SIZE = 48;

而 GRID_SIZE 呢,则代表显示到窗口的时候,每个格子实际的像素大小。

然后定义 MAP_COL 和 MAP_ROW ,分别代表这个扫雷地图,有多少列多少行:

const int MAP_COL = 15;
const int MAP_ROW = 10;

然后把之前 15 和 10 的地方,都替换掉(注意下面有个 10 是不能替换,因为含义不同):

int showGrid[MAP_COL+1][MAP_ROW+1];
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        showGrid[i][j] = 10;      // 这个10可不是 MAP_ROW
    }
}

遍历每个格子,进行渲染:

s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        s.setTextureRect(IntRect(ORI_GRID_SIZE * showGrid[i][j], 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
        s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
        win.draw(s);
    }
}

而对于整个窗口大小,可以是 格子数 乘上 (列数 + 2),左边加一列格子,右边加一列格子,上下也是一样的,所以窗口大小可以定义成这样的常量:

const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);

最后,showGrid 里面还有一个 10,这个我们可以用枚举来实现:

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

这样一来,代码就好看很多了。

整体代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    /*
    int r = rand() % 12;
    s.setTextureRect(IntRect(96 * r, 0, 96, 96));
    s.setPosition(16, 16);
    s.setScale(Vector2f(0.5, 0.5));
    win.draw(s);
    win.display();
    */

    int showGrid[MAP_COL+1][MAP_ROW+1];
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            showGrid[i][j] = GridType::GT_HIDE;
        }
    }
    s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            s.setTextureRect(IntRect(ORI_GRID_SIZE * showGrid[i][j], 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win.draw(s);
        }
    }
    win.display();

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

地图随机

showGrid 代表的是显示出来的格子类型,所以再定义一个 grid,代表真实的格子类型,并且利用随机函数,1/6 的概率是炸弹,5/6的概率是空。

GridType grid[MAP_COL + 1][MAP_ROW + 1];
GridType showGrid[MAP_COL+1][MAP_ROW+1];
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        showGrid[i][j] = GridType::GT_HIDE;
        if (rand() % 6 == 0) {
            grid[i][j] = GridType::GT_BOMB;
        }
        else {
            grid[i][j] = GridType::GT_EMPTY;
        }
    }
}

定义周围的八个方向

const int DIR[8][2] = {
    {-1, -1}, {-1, 0}, {-1, 1},
    {0, -1}, {0, 1},
    {1, -1}, {1, 0}, {1, 1},
};

并且统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型

for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        if (grid[i][j] == GridType::GT_EMPTY) {
            int cnt = 0;
            for (int k = 0; k < 8; ++k) {
                int ti = i + DIR[k][0];
                int tj = j + DIR[k][1];
                if (grid[ti][tj] == GridType::GT_BOMB) {
                    ++cnt;
                }
            }
            grid[i][j] = (GridType)cnt;
        }
    }
}

然后只需要在显示之前,把所有的实际格子内容,赋值给对应的显示格子,就相当于摊牌了。运行一下看看效果。

showGrid[i][j] = grid[i][j];

整个代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    /*
    int r = rand() % 12;
    s.setTextureRect(IntRect(96 * r, 0, 96, 96));
    s.setPosition(16, 16);
    s.setScale(Vector2f(0.5, 0.5));
    win.draw(s);
    win.display();
    */

    GridType showGrid[MAP_COL + 1][MAP_ROW + 1]; // 显示出来的格子类型
    GridType grid[MAP_COL + 1][MAP_ROW + 1]; // 真实的格子类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            showGrid[i][j] = GridType::GT_HIDE;
            if (rand() % 6 == 0) {
                grid[i][j] = GridType::GT_BOMB;
            }
            else {
                grid[i][j] = GridType::GT_EMPTY;
            }
        }
    }
    // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j] == GridType::GT_EMPTY) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj] == GridType::GT_BOMB) {
                        ++cnt;
                    }
                }
                grid[i][j] = (GridType)cnt;
            }
            showGrid[i][j] = grid[i][j];
        }
    }

    s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            s.setTextureRect(IntRect(ORI_GRID_SIZE * showGrid[i][j], 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win.draw(s);
        }
    }
    win.display();

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

点击排雷

获取鼠标点击到的格子位置

Vector2i pos = Mouse::getPosition(win);
int x = pos.x / GRID_SIZE;
int y = pos.y / GRID_SIZE;

并且处理鼠标左键 和 鼠标右键 的 按下事件

if (e.type == Event::MouseButtonPressed) {
    if (e.key.code == Mouse::Left) {
        showGrid[x][y] = grid[x][y];
    }
    else if (e.key.code == Mouse::Right) {
        showGrid[x][y] = GridType::GT_FLAG;
    }
}

最后,如果当前的格子被确认是雷,那么所有格子都公开,游戏结束:

if( showGrid[x][y] == GridType::GT_BOMB) 
    showGrid[i][j] = grid[i][j];

整个代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    GridType showGrid[MAP_COL + 1][MAP_ROW + 1]; // 显示出来的格子类型
    GridType grid[MAP_COL + 1][MAP_ROW + 1]; // 真实的格子类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            showGrid[i][j] = GridType::GT_HIDE;
            if (rand() % 6 == 0) { // 1/6 的概率是炸弹,5/6的概率是空。
                grid[i][j] = GridType::GT_BOMB;
            }
            else {
                grid[i][j] = GridType::GT_EMPTY;
            }
        }
    }
    // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j] == GridType::GT_EMPTY) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj] == GridType::GT_BOMB) {
                        ++cnt;
                    }
                }
                grid[i][j] = (GridType)cnt;
            } 
        }
    }

    while (win.isOpen()) {
        Event e;
        // 获取鼠标点击到的格子位置
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;

        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }

            // 处理鼠标左键 和 鼠标右键 的 按下事件
            if (e.type == Event::MouseButtonPressed) {
                if (e.key.code == Mouse::Left) {
                    showGrid[x][y] = grid[x][y];
                }
                else if (e.key.code == Mouse::Right) {
                    showGrid[x][y] = GridType::GT_FLAG;
                }
            }
        }
        // 绘图
        s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (showGrid[x][y] == GridType::GT_BOMB) // 如果当前的格子被确认是雷,那么所有格子都公开
                    showGrid[i][j] = grid[i][j];
                //showGrid[i][j] = grid[i][j];

                s.setTextureRect(IntRect(ORI_GRID_SIZE * showGrid[i][j], 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win.draw(s);
            }
        }
        win.display();
    }
    return 0;
}

格子类化

接下来我们采用面向对象的思想,来改造下这个代码,首先是一个格子,目前用了两个数据来存储,一个是实际的格子类型,一个是显示的格子类型,我现在可以把它封装到一个类里面,定义两个私有成员变量。

分别用 m_realGridType 和 m_showGridType 来表示。

然后实现它们的 set 和 get 成员函数。并且把相关代码也进行替换。

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType realGType) {
        m_showGridType = realGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_showGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

改造下初始化地图的代码:

Grid grid[MAP_COL + 1][MAP_ROW + 1];
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        grid[i][j].SetShowGridType(GridType::GT_HIDE);
        if (rand() % 6 == 0) {
            grid[i][j].SetRealGridType(GridType::GT_BOMB);
        }
        else {
            grid[i][j].SetRealGridType(GridType::GT_EMPTY);
        }
    }
}
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        if (grid[i][j].IsEmpty()) {
            int cnt = 0;
            for (int k = 0; k < 8; ++k) {
                int ti = i + DIR[k][0];
                int tj = j + DIR[k][1];
                if (grid[ti][tj].IsRealBomb()) {
                    ++cnt;
                }
            }
            if (cnt > 0) {
                grid[i][j].SetRealGridType((GridType)cnt);
            }
        }
    }
}

改造下鼠标按键的代码:

if (e.key.code == Mouse::Left) {
    grid[x][y].ShowGrid();
}
else if (e.key.code == Mouse::Right) {
    grid[x][y].SetShowGridType(GridType::GT_FLAG);
}

改造下渲染的代码:

if( grid[x][y].IsShowBomb()) 
    grid[i][j].ShowGrid();
s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));

整个代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType showGType) {
        m_showGridType = showGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);

    Grid grid[MAP_COL + 1][MAP_ROW + 1];
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) { // 1/6 的概率是炸弹,5/6的概率是空。
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            } 
        }
    }

    while (win.isOpen()) {
        Event e;
        // 获取鼠标点击到的格子位置
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;

        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }

            // 处理鼠标左键 和 鼠标右键 的 按下事件
            if (e.type == Event::MouseButtonPressed) {
                if (e.key.code == Mouse::Left) {
                    grid[x][y].ShowGrid();
                }
                else if (e.key.code == Mouse::Right) {
                    grid[x][y].SetShowGridType(GridType::GT_FLAG);
                }
            }
        }
        // 绘图
        s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[x][y].IsShowBomb()) // 如果当前的格子被确认是雷,那么所有格子都公开
                    grid[i][j].ShowGrid();

                s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win.draw(s);
            }
        }
        win.display();
    }
    return 0;
}

地图类化

除了把格子用类来实现,整个地图也可以用类来实现。

class Map {
public:
    void init() {
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                grid[i][j].SetShowGridType(GridType::GT_HIDE);
                if (rand() % 6 == 0) {
                    grid[i][j].SetRealGridType(GridType::GT_BOMB);
                }
                else {
                    grid[i][j].SetRealGridType(GridType::GT_EMPTY);
                }
            }
        }
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[i][j].IsEmpty()) {
                    int cnt = 0;
                    for (int k = 0; k < 8; ++k) {
                        int ti = i + DIR[k][0];
                        int tj = j + DIR[k][1];
                        if (grid[ti][tj].IsRealBomb()) {
                            ++cnt;
                        }
                    }
                    if (cnt > 0) {
                        grid[i][j].SetRealGridType((GridType)cnt);
                    }
                }
            }
        }
    }

    void handleMouseEvent(Event& e, int x, int y) {
        if (e.type == Event::MouseButtonPressed) {
            if (e.key.code == Mouse::Left) {
                grid[x][y].ShowGrid();
            }
            else if (e.key.code == Mouse::Right) {
                grid[x][y].SetShowGridType(GridType::GT_FLAG);
            }
        }
    }

    void draw(RenderWindow& win, Sprite& s, int x, int y) {
        s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[x][y].IsShowBomb())
                    grid[i][j].ShowGrid();
                s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win.draw(s);
            }
        }
    }

private:
    Grid grid[MAP_COL + 1][MAP_ROW + 1];
};

这样实际游戏,调用的代码就可以很短了,所有的逻辑都封装到地图类里去了。

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);
    Map mp;
    mp.init();
    while (win.isOpen()) {
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
            mp.handleMouseEvent(e, x, y);
        }
        mp.draw(win, s, x, y);
        win.display();
    }
    return 0;
}

整个代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType showGType) {
        m_showGridType = showGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

class Map {
public:
    void init() {
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                grid[i][j].SetShowGridType(GridType::GT_HIDE);
                if (rand() % 6 == 0) { // 1/6 的概率是炸弹,5/6的概率是空。
                    grid[i][j].SetRealGridType(GridType::GT_BOMB);
                }
                else {
                    grid[i][j].SetRealGridType(GridType::GT_EMPTY);
                }
            }
        }
        // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[i][j].IsEmpty()) {
                    int cnt = 0;
                    for (int k = 0; k < 8; ++k) {
                        int ti = i + DIR[k][0];
                        int tj = j + DIR[k][1];
                        if (grid[ti][tj].IsRealBomb()) {
                            ++cnt;
                        }
                    }
                    if (cnt > 0) {
                        grid[i][j].SetRealGridType((GridType)cnt);
                    }
                }
            }
        }
    }
    void handleMouseEvent(Event& e, int x, int y) {
        if (e.type == Event::MouseButtonPressed) {
            if (e.key.code == Mouse::Left) {
                grid[x][y].ShowGrid();
            }
            else if (e.key.code == Mouse::Right) {
                grid[x][y].SetShowGridType(GridType::GT_FLAG);
            }
        }
    }
    void draw(RenderWindow& win, Sprite& s, int x, int y) {
        s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[x][y].IsShowBomb()) // 如果当前的格子被确认是雷,那么所有格子都公开
                    grid[i][j].ShowGrid();

                s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win.draw(s);
            }
        }
    }
private:
    Grid grid[MAP_COL + 1][MAP_ROW + 1];
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);
    Map mp;

    mp.init();
    while (win.isOpen()) {
        Event e;
        // 获取鼠标点击到的格子位置
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;

        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }

            // 处理鼠标左键 和 鼠标右键 的 按下事件
            mp.handleMouseEvent(e, x, y);
        }
        // 绘图
        mp.draw(win, s, x, y);
        win.display();
    }
    return 0;
}

接口优化

接下来我们来看,这个地图类对外提供的接口,只有三个了。一个是初始化,一个是处理事件,一个是渲染,并且渲染接口每次都把 窗口 和 精灵 传进去,实际上是没有必要的,因为在这个大循环里,这两个对象,是不变的。

所以这两个对象,实际上,可以作为 Map 类的成员变量,当然这里必须用指针。如果不传指针,就会通过拷贝构造函数,生成一个新的对象,这不是我们想要的,我需要它还是原来那个对象。

...
private:
    RenderWindow*    win;
    Sprite*          sprite;
    Grid             grid[MAP_COL + 1][MAP_ROW + 1];
};

然后修改 Map 类的初始化函数,如下:

void init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;
    ...
}


mp.init(&win, &s);

这里记住要传指针,所以把 win 和 s 的地址传进去就好了。最后修改 draw 函数(因为是指针,所以所有的 . 变成 -> 就好了):

void draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[x][y].IsShowBomb()) {
                grid[i][j].ShowGrid();
            }
            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}

sprite 作为 RenderWindow 的成员函数 draw 的参数,是一个对象,所以采用 * 进行解引用。

整个代码main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);
enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType showGType) {
        m_showGridType = showGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

class Map {
public:
    void init(RenderWindow* win, Sprite* sprite) {
        this->win = win;
        this->sprite = sprite;
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                grid[i][j].SetShowGridType(GridType::GT_HIDE);
                if (rand() % 6 == 0) { // 1/6 的概率是炸弹,5/6的概率是空。
                    grid[i][j].SetRealGridType(GridType::GT_BOMB);
                }
                else {
                    grid[i][j].SetRealGridType(GridType::GT_EMPTY);
                }
            }
        }
        // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[i][j].IsEmpty()) {
                    int cnt = 0;
                    for (int k = 0; k < 8; ++k) {
                        int ti = i + DIR[k][0];
                        int tj = j + DIR[k][1];
                        if (grid[ti][tj].IsRealBomb()) {
                            ++cnt;
                        }
                    }
                    if (cnt > 0) {
                        grid[i][j].SetRealGridType((GridType)cnt);
                    }
                }
            }
        }
    }
    void handleMouseEvent(Event& e, int x, int y) {
        if (e.type == Event::MouseButtonPressed) {
            if (e.key.code == Mouse::Left) {
                grid[x][y].ShowGrid();
            }
            else if (e.key.code == Mouse::Right) {
                grid[x][y].SetShowGridType(GridType::GT_FLAG);
            }
        }
    }
    void draw(int x, int y) {
        sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[x][y].IsShowBomb()) // 如果当前的格子被确认是雷,那么所有格子都公开
                    grid[i][j].ShowGrid();

                sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win->draw(*sprite);
            }
        }
    }
private:
    RenderWindow* win;
    Sprite* sprite;
    Grid grid[MAP_COL + 1][MAP_ROW + 1];
};

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);
    Map mp;

    mp.init(&win, &s);
    while (win.isOpen()) {
        Event e;
        // 获取鼠标点击到的格子位置
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;

        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }

            // 处理鼠标左键 和 鼠标右键 的 按下事件
            mp.handleMouseEvent(e, x, y);
        }
        // 绘图
        mp.draw(x, y);
        win.display();
    }
    return 0;
}

文件拆分

这个时候我们发现这个文件行数太多了,所以我们想办法把类拆到其它文件去。首先,先把 Grid 类的内容拆出去,新建一个 Grid.h 文件,实现如下:

#pragma once

const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType realGType) {
        m_showGridType = realGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_showGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

然后在 main.cpp 里面,写上这么一句话:

#include "Grid.h"

这时候,我们希望 .h 文件里面不要有函数的实现,只保留声明。然后在源文件里,新建一个 Grid.cpp 文件,把函数的实现写在这个文件里。

const.h

#pragma once
const int DIR[8][2] = {
    {-1,-1},{-1,0},{-1,1},
    {0,-1},{0,1},
    {1,-1},{1,0},{1,1},
};
const int ORI_GRID_SIZE = 96;
const int GRID_SIZE = 48;
const int MAP_COL = 15;
const int MAP_ROW = 10;
const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

Grid.h

#pragma once
#includeconst.h”

class Grid {
public:
    Grid();
    void SetRealGridType(GridType realGType);
    void SetShowGridType(GridType realGType);
    GridType GetShowGridType();
    void ShowGrid();
    bool IsEmpty() const;
    bool IsRealBomb() const;
    bool IsShowBomb() const;
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

Grid.cpp 如下:

#include "Grid.h"

Grid::Grid() {
    m_realGridType = GridType::GT_EMPTY;
    m_showGridType = GridType::GT_EMPTY;
}

void Grid::SetRealGridType(GridType realGType) {
    m_realGridType = realGType;
}
void Grid::SetShowGridType(GridType realGType) {
    m_showGridType = realGType;
}
GridType Grid::GetShowGridType() {
    return m_showGridType;
}
void Grid::ShowGrid() {
    m_showGridType = m_realGridType;
}
bool Grid::IsEmpty() const {
    return m_realGridType == GridType::GT_EMPTY;
}
bool Grid::IsRealBomb() const {
    return m_realGridType == GridType::GT_BOMB;
}
bool Grid::IsShowBomb() const {
    return m_showGridType == GridType::GT_BOMB;
}

同样,在实现一个 Map.h 和 Map.cpp。

Map.h

#pragma once

#include <SFML/Graphics.hpp>
#include "Grid.h"
using namespace sf;

class Map {
public:
    void init(RenderWindow* win, Sprite* sprite);
    void handleMouseEvent(Event& e, int x, int y);
    void draw(int x, int y);

private:
    RenderWindow* win;
    Sprite*    sprite;
    Grid      grid[MAP_COL + 1][MAP_ROW + 1];
};

Map.cpp

#include "Map.h"

void Map::init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;

    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) {
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            }
        }
    }
}

void Map::handleMouseEvent(Event& e, int x, int y) {
    if (e.type == Event::MouseButtonPressed) {
        if (e.key.code == Mouse::Left) {
            grid[x][y].ShowGrid();
        }
        else if (e.key.code == Mouse::Right) {
            grid[x][y].SetShowGridType(GridType::GT_FLAG);
        }
    }
}

void Map::draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[x][y].IsShowBomb()) {
                grid[i][j].ShowGrid();
            }
            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}

main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
#include "Grid.h"
#include "Map.h"
using namespace sf;

int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);
    Map mp;

    mp.init(&win, &s);
    while (win.isOpen()) {
        Event e;
        // 获取鼠标点击到的格子位置
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;

        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }

            // 处理鼠标左键 和 鼠标右键 的 按下事件
            mp.handleMouseEvent(e, x, y);
        }
        // 绘图
        mp.draw(x, y);
        win.display();
    }
    return 0;
}

这样一来,我们的 main 函数就变得非常清晰了。所以面向对象编程,会让代码变得简单。

游戏重开

然后一个游戏结束以后,我们希望它能够重开,在 Map 中,引入一个私有成员变量 isRunning,并且引入一个 initGame 的函数,围绕 isRuning 进行逻辑修改。

一旦按到一个雷,那么 isRuning 就从 true 变成 false,然后左键按下的时候,根据 isRunning 是 true 还是 false 做不同的处理,如果为 true,则保留原先的逻辑;如果为 false,则重新初始化游戏,开始新的一局。 Map.h

#pragma once
#include <SFML/Graphics.hpp>
#include "Grid.h"
using namespace sf;

class Map {
public:
    void init(RenderWindow* win, Sprite* sprite);
    void initGame();
    void handleMouseEvent(Event& e, int x, int y);
    void draw(int x, int y);
private:
    RenderWindow* win;
    Sprite* sprite;
    Grid grid[MAP_COL + 1][MAP_ROW + 1];
    bool isRunning;
};

Map.cpp

#include "Map.h"

void Map::init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;
    initGame();
}

void Map::initGame() {
    this->isRunning = true;
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) { // 1/6 的概率是炸弹,5/6的概率是空。
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    // 统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            }
        }
    }
}
void Map::handleMouseEvent(Event& e, int x, int y) {
    if (e.type == Event::MouseButtonPressed) {
        if (e.key.code == Mouse::Left) {
            if (isRunning) {
                grid[x][y].ShowGrid();
                if (grid[x][y].IsShowBomb()) {
                    isRunning = false;
                }
            }
            else {
                initGame();
            }
        }
        else if (e.key.code == Mouse::Right) {
            grid[x][y].SetShowGridType(GridType::GT_FLAG);
        }
    }
}
void Map::draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (!isRunning) // 如果当前的格子被确认是雷,那么所有格子都公开
                grid[i][j].ShowGrid();

            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}
emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike