程序开发过程中,所有的数据都需要放置到内存中才能进一步处理,尤其是游戏开发中渲染三维模型数据,这是消耗性能的关键部分。如果处理不当,很有可能造成内存溢出的情况发生,导致整个程序崩溃。防止内存溢出情况发生的关键点在于合理的分配和释放内存。Java和C#语言都是运行在虚拟机上的,虚拟机都有一种垃圾回收机制来释放数据持有的内存,这不需要开发人员人工干预。C++语言没有这种机制,因此需要开发人员手动释放某些数据占用的内存。这个时候,我就必须了解数据变量的作用域(生命周期)的概念。
C++程序运行过程中,系统需要分配内存供其运行。这些内存共分为四个区域,它们分别是代码区,全局区,堆区和栈区。代码区很容易理解,就是存放代码的。全局区主要存放全局变量和静态变量。这部分变量数据直到程序结束运行才会释放。栈区主要存放局部变量,也就是我们平时使用的数据变量。堆区也称为自由存储区,基本上是由开发人员使用new和delete关键字来分配和释放的。new一般配合指针使用,主要用于类的实例化(分配内存)。
// 栈区的使用
int w[] = {1,2,3,4,5};
cout << w[0] << endl;
// 堆区的使用
//int y[] = new int[5];
int* y = new int[5];
y[0] = 1;
cout << w[0] << endl;
// 手动释放
delete[] y;
局部变量定义在一个函数内部,在函数之外是不可访问的,函数的形参也属于局部变量。全局变量定义在所有函数之外,并且在其他的函数也可以访问。不同函数中定义的相同名称的局部变量互不干扰,局部变量会覆盖同名的全局变量!当我们需要在多个函数之间共享一些数据的时候,可以将这些数据定义成全局变量。局部变量和全局变量代码说明如下:
#include
#include
using namespace std;
// 声明函数
void test();
// 定义全局变量
int i = 100;
int j = 100;
// 入口函数
int main()
{
// 定义局部变量(覆盖同名全局变量)
int i = 10;
cout << "局部变量(main):" << i << endl; // 输出 10
// 输出全局变量
cout << "局部变量(main):" << j << endl; // 输出 100
// 调用函数
test();
}
// 定义函数
void test() {
// 定义局部变量
int i = 20;
cout << "局部变量(test):" << i << endl; // 输出 20
}
当局部变量被定义时,系统不会对其初始化,必须手动对其初始化。定义全局变量时,系统会自动初始化为0或空。每个变量只能在程序的一定范围内使用,此范围称为变量的作用域。作用域也就是变量的生命周期。变量进入它的作用域就会被创建,离开它的作用域,就会被销毁掉。局部变量会随着函数的调用结束而被释放,而全局变量会一直存活在整个程序结束。也就是说局部变量的作用域就是它所在的函数,全局变量的作用域就是整个程序。另外,main函数里面的变量也是局部变量,但是它要等到整个程序结束才能被释放。
我们还要理解存储类型的概念。存储类型是专门从变量的生命周期来划分的。变量的存储类型大致可分为静态存储方式和动态存储方式。对于动态存储变量,当程序运行到该变量处时才为其分配存储空间,当程序运行到该变量的作用域处时自动收回其存储空间,因此它的生命周期为所在作用域。它就是一个局部变量的概念。在程序开始就执行就为其分配存储空间,直到程序结束时,才收回变量的存储空间,这种变量称为静态存储空间,其生命周期为整个应用程序的运行过程。它其实就是一个全局变量的概念。C++中的存储类型包括auto(自动类型)、register(寄存器类型)、static(静态类型)和extern(外部类型)四种。
1. 自动类型auto,只能是局部类型的变量。属于动态存储方式。这是默认的方式。
2. 寄存器类型register,存储在cpu的寄存器中的变量,它比内存还有快,一般很少使用。
3. 静态类型static,可以声明全局/局部变量,属于静态存储方式。在函数内声明的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放,但不改变作用域(该变量不能被其他函数使用)。也就是说当该函数再一次被调用的时候,静态局部变量保留上一次操作执行后的值。我们经常使用静态局部变量。
// 声明函数
void test2();
// 定义函数2
void test2() {
static int k = 0;
k += 10;
cout << "局部静态变量(test2):" << k << endl;
}
// 调用静态变量函数
test2(); // 输出 10
test2(); // 输出 20
如果k是一个普通的局部变量,那么每次执行test2函数,它都应该输出10。但是我们将其声明为static类型,那么k就不会在函数执行完毕后释放,而是保留上次的数值,等再次执行test2函数的时候,k就是保留上次的数值,再次进行运算,也就是我们看到的20了。
4. 外部类型extern,大部分完整的程序都由不同模块组成,放在不同的文件中,分开编译成目标文件,最后进行链接。对于所有模块使用的全局变量,我们可以只在一个模块中定义全局变量,在其他模块中用extern说明这是一个“外来”的全局变量。全局变量默认是有外部链接性的。说白了,extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量。
类对象的生命周期遵循变量的作用域规则,类中的数据成员的生命周期跟随类的变量的生命周期。类中的数据成员的可见性是由访问控制符来决定的。类中成员可以使用static来修饰成为静态成员,包括静态数据成员和静态函数成员两种。静态成员的访问同样受到访问控制符的限制。静态成员属于类,不属于单个对象。当我们声明类的成员为静态时,无论创建多少个类的对象,静态成员都只有一个。静态成员在类的所有对象中是共享的。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来对它进行初始化。静态成员变量在程序启动时被分配内存,在程序结束时被释放,其生命周期为整个程序的运行过程。
// 定义包含静态成员的类
class Demo {
public:
// 静态数据成员
static int i;
// 普通数据成员
int j;
// 静态函数成员
static void showi() {
cout << "i = " << i << endl;
}
// 普通函数成员
void showj() {
cout << "j = " << j << endl;
}
};
// 初始化静态成员(必须)
int Demo::i = 0;
/**************** 以下代码放入main函数中执行 ****************************/
// 普通数据成员和函数的使用
Demo d;
d.j = 100;
d.showj();
// 普通对象可以调用静态成员
d.i = 100;
cout << "d.i = " << d.i << endl;
d.showi();
Demo f;
f.j = 200;
f.showj();
f.showi();
// 静态数据成员和函数的使用
Demo::i = 200;
Demo::showi();
类的静态函数仅使用类名加范围解析运算符 :: 就可以访问了。静态成员函数只能访问静态成员数据或者静态成员函数。若静态函数需要访问非静态成员,必须通过函数参数传递类的对象,通过对象才能访问非静态成员。普通成员函数有 this 指针,可以访问类中的任意成员,而静态成员函数没有 this 指针。
备注:如果使用全局变量存储类中所有对象所共享的数据,则由于全局变量能被所有对象和函数访问,致使类的封装性被破坏,数据安全性不能保证,并且类中的静态成员可以设置为私有,增加信息的隐秘性。比如我们使用一个静态整型成员变量来累计该类被实例化对象的数量,这就是一种非常不错的共享数据方法。
本课程的所有代码案例下载地址:
C++示例源代码(配合教学课程使用)-C/C++文档类资源-CSDN下载
备注:这是我们游戏开发系列教程的第一个课程,主要是编程语言的基础学习,优先学习C++编程语言,然后进行C#语言,最后才是Java语言,紧接着就是使用C++和DirectX来介绍游戏开发中的一些基础理论知识。我们游戏开发系列教程的第二个课程是Unity游戏引擎的学习。课程中如果有一些错误的地方,请大家留言指正,感激不尽!