【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 4316|回复: 8

游戏编程指南--第二章

[复制链接]
发表于 2006-1-6 11:44:10 | 显示全部楼层 |阅读模式
C++和C最大的区别在于C++是一种面向对象(object-oriented)的语言,即程序是以对象而不是函数为基础,所以严格说来,我们在第一章所讨论的还不是地道的C++程序。类(class)正是实现面向对象的关键,它是一种数据类型,是对事物的一种表达和抽象。类拥有各种成员,其中有的是数据,标识类的各种属性;有的是函数(类中的函数又叫方法),表示对类可进行的各种操作。举一个例子,我们可以建立一个“草”类,它可以有“高度”等各种属性和“割”、“浇水”等各种方法。 2.1 定义和使用类

让我们先看一个使用了类的程序:

//-----------------------------grass.h---------------------------------

class grass //定义grass类

{

private: //声明下面的成员为私有。类外的函数如果试图访问,编译器会告诉你发生错

//误并拒绝继续编译。缺省情况下类中的一切均为私有,所以这一行可以省略。

int height; //一般来说,类中的所有数据成员都应为私有

//不过本章后面的程序为了便于说明也拥有公有数据成员

public: //下面的成员为公有,谁都可以访问。

void cut( );

void water( );

int get_height( );

void set_height(int newh);

}; //这个分号不要漏了!

//-----------------------------grass.cpp-------------------------------

#include <iostream>

using namespace std;

#include "grass.h"

//下面对类的方法进行定义

void grass::cut( ) // "::"表示cut( )是grass的成员。

{

if (height>=10)

height-=10; //可自由访问grass中的任何成员。

}

void grass::water( )

{

height+=10;

}

int grass::get_height( ) //在类的外部不能直接访问height,所以要写这个函数

{

return height;

}

void grass::set_height(int newh) //同样我们写了这个函数

{

if (newh>=0)

height=newh;

}

void main( )

{

grass grass1,grass2; //其实这一句和"int a,b;"没什么区别,想一想!这一句语

//句被称为实例化。

grass1.set_height(20); //如果你用过VB一定会觉得很亲切。类以外的函数即使

//是访问类的公有部分也要用"."。

cout<<grass1.get_height( )<<endl;

grass1.set_height(-100); //因为set_height作了保护措施,所以这一句不会给

//height一个荒唐的值

cout<<grass1.get_height( )<<endl;

grass1.cut( );

cout<<grass1.get_height( )<<endl;

grass2=grass1; //同一种对象可直接互相赋值

cout<<grass2.get_height( )<<endl;

grass *grass3; //也可定义指向类的指针

grass3=new grass; //同样要new

grass3->set_height(40); //由于grass3是指针,这里要用"->"。其实也可以

//使用(*grass3).set_height(40); ("."操作符比"*"

//操作符执行时优先) ,不过这样写比较麻烦。

grass3->water( );

cout<<grass3->get_height( );

delete grass3; //释放指针

}

看了注释你应该可以读懂这个程序,现在我们可以看到类的第一个优点了:封装性。封装指的就是像上面这样似乎故弄玄虚地把height隐藏起来,并写几个好像很无聊的读取和改写height的函数。然而在程序中我们已经可以看到这样可以保护数据。而且在大型软件和多人协作中,由于私有成员可以隐藏类的核心部分,只是通过公有的接口与其它函数沟通,所以当我们修改类的数据结构时,只要再改一改接口函数,别的函数还是可以象以前一样调用类中的数据,这样就可以使一个类作为一个模块而出现,有利于大家的协作和减少错误。

有的人也许会认为写接口函数会减慢速度,那么你可以在定义前面加上"inline"使其成为内联函数。

类以外的函数其实也有办法直接访问类的私有部分,只要在类中声明类的方法时加入形如"friend int XXX (int xxx, int xxx) "这样的语句,类以外的"int XXX (int xxx, int xxx) "函数就可访问类的私有部分了。此时这个函数称为类的友元。

注意类中的函数最好不要返回类中的私有成员的引用或指针,否则我们将显然可以通过它强行访问类中的私有成员。

除了public和private两种权限外还有protected权限,平时是和private一样的,后面在讲类的继承时会进一步解释它的用途。

在类的定义中要注意定义成员数据时不能同时初始化(好像int a=0这样),且不能用extern说明成员数据。

一种类的对象可以作为另一种类的成员。例如:

class x

{

int a;

};

class y

{

x b;

};

如果我们把上面两个类的声明互调,那么由于执行x b;时x类还根本未被定义,编译器会报错。那么应如何解决呢?很简单,在最前面加一句class x;预先声明一下即可。

同一种类可以互相赋值。类可作为数组的元素。可以定义指向类的指针。总之类拥有普通的数据类型的性质。

只要定义一次类,就可以大批量地通过实例化建立一批对象,且建立的对象都有直观的属性和方法。这也是类的好处之一。

结构其实也是一种类,只不过结构的缺省访问权限是公有。定义结构时只需把"class"换为"struct"。 一般我们在仅描述数据时使用结构,在既要描述数据,又要描述对数据进行的操作时使用类。

最后介绍一下用什么办法可以得到一个变量是哪个类的对象:typeid(aaa).name( )能返回aaa变量所属类的名称,注意这是在程序运行期实现的,很酷吧,不过不要滥用它。

 楼主| 发表于 2006-1-6 11:44:43 | 显示全部楼层

2.2 类的构造函数>>

当我们将一个类实例化时,经常会希望能同时将它的一些成员初始化。为此,我们可以使用构造函数。构造函数是一个无返回值(void都不用写)且与类同名的函数,它将在类被实例化时自动执行。构造函数的使用就象下面这样:>>

#include <iostream>>>

using namespace std;>>

class grass>>

{

public:

int height;

grass(int height); //构造函数。当然,它需为public权限

//虽然在这个程序中它有参数,但并不必需

};

grass::grass(int height)

{

this->height=height; //对于任何一个对象的方法来说,this永远是一个指向这个

//对象的指针。所以这样写能使编译器知道是类中的height

}

void main( )

{

grass grass1(10); //普通对象实例化时就要给出初始化参数

//如果构造函数无参数就不需要写"(10)"

grass *grass2;

grass2=new grass(30); //指针此时要给出初始化参数

cout<<grass1.height;

cout<<grass2->height;

}

值得注意的是,当你使用grass grass1=grass2;或grass grass1(grass2);这样的方式来初始化对象时,构造函数将不会被执行,执行的将是所谓的拷贝构造函数。如果你偷懒没写它,系统会自动生成一个,它的行为将是逐字节拷贝grass2到grass1。这个行为看上去很正常,然而如果类中有指针型成员时它却存在着灾难性的后果。看看下面的一端代码片段:

grass grass1;

grass1.x=”hehe”; //假设x是grass类的一个char*类型成员

{

grass grass2=grass1; //此时grass2的x将和grass1的x指向同一个值!

} //grass2和它的x成员一起被销毁

//现在grass1.x也已无辜地失去意义

所以,当我们的类中有指针型成员时,我们必须像这样写一个自己的拷贝构造函数:

grass::grass(grass& grass1) //名称应与类相同,参数应为对同类数据的引用

//如果我们写成grass::grass(grass grass1),显然会陷入死循环,因为此时编译器需要

//调用拷贝构造函数来生成参数的复制品,所以我们必须使用实参

{

//在这里正确地拷贝各个数据

}

但是这实在挺麻烦的,有没有办法干脆禁止掉这种意义一般来说并不大的拷贝初始化呢?很简单,自己写一个只有声明没有定义的拷贝构造函数,并声明其为private权限,即可防止编译器自做聪明----编译时如果它发现grass grass1=grass2;这样的语句时会报错,不过你也别想用foo(grass a);这样的函数了,必须用foo(grass &a);……

构造函数还有一个用处就是可以进行类型转换。例如,我们定义了一个这样的构造函数:

grass::grass(int x)

{

height=x;

}

现在,如果我们定义了一个grass类的gg对象,以后就可以执行gg=5; 这样的语句了,也可将int类型的变量赋值给gg,因为这时实际上执行了gg=grass(5);这样的语句(如果我们使用了2.4节介绍的方法重载了=运算符,那么只会执行重载的=运算符)。

还有一种叫析构函数的东西,形如grass::~grass( ),在我们delete一个指向对象的指针时会自动调用,你应该在里面释放类的指针型成员。

 楼主| 发表于 2006-1-6 11:45:56 | 显示全部楼层

2.3 类的静态成员>>

类的静态数据成员和普通的静态变量含义不同,它的意思是:在每一个类实例化时并不分配存储空间,而是该类的每个对象共享一个存储空间,并且该类的所有对象都可以直接访问该存储空间。其实它就是一个专门供这个类的对象使用的变量----如果你把它声明为private权限的话。>>

在类中定义静态数据成员,只须在定义时在前面加上"static"。类的静态数据成员只能在类外进行初始化,若没有对其进行初始化,则自动被初始化为 0。在类外引用静态数据成员必须始终用类名::变量名的形式。静态数据成员可以用来统计创建了多少个这种对象。>>

举一个例子:>>

#include <iostream>>>

using namespace std;

class AA

{

private:

int a;

public:

static int count; //定义类的静态成员

AA(int aa=0) { a=aa; count++; }

//对于类的方法,如果是较简单的可以这样写以使程序紧凑

int get_a( ) { return a; }

};

int AA::count=0; //在类外初始化

void main()

{

cout<<"Count="<<AA::count<<endl;

AA x(10),y(20);

cout <<"x.a="<<x.get_a( )<<" Count="<<x.count<<endl;

cout <<"y.a="<<y.get_a( );

}

我们还可以定义静态的函数,只需在声明函数时在前面加上static即可,但定义函数时就不用加上static了。使用静态函数时也需要"类名::静态成员函数名(参数表);"的形式。

2.4 运算符重载

运算符重载可以使类变得非常直观和易用。比如说,我们定义了一个复数类(什么,你没学过复数?你读几年级?),然后再将加、减、乘、除等运算符重载,就可以自由地对复数对象好像整数一样进行这些运算了!可以想象,这将大大方便我们。

使用运算符重载很简单,我们就举一个复数类的例子来说明怎样使用:

#include <iostream>

using namespace std;

class complex

{

private:

double real;

double image;

public:

complex ( ); //缺省构造函数

complex (double r, double i); //顺便初始化值的构造函数

complex operator +(complex x); //计算A+B

complex operator ++( ); //计算++A

complex operator --(int); //计算A—

complex operator =(double x); //把一个double赋给一个complex时该怎么办

//系统还自动生成了一个complex operator=(complex);,它的实现是简单拷贝

//所以如果类中有指针成员,它会像默认的拷贝构造函数那样出问题L

//我们如果要重写它,还要注意检查自己赋给自己的情况

void print(); //输出复数

};

complex::complex( )

{

real=0.0f;

image=0.0f;

}

complex::complex(double r, double i)

{

real=r;

image=i;

}

complex complex:perator +(complex x)

{

complex c;

c.real=real+x.real;

c.image=image+x.image;

return c;

}

complex complex:perator ++( )

{

complex c;

++real;

c.real=real;

c.image=image;

return c;

}

complex complex:perator --(int)

{

complex c;

c.real=real;

c.image=image;

real--;

return c;

}

complex complex:perator =(double x)

{

real=x;

return *this; //按照C++的惯例,返回*this,以便实现链式表达式

}

void complex::print( )

{

cout<<real<<"+"<<image<<"i"<<endl;

}

void main( )

{

complex a(1,2);

complex b(4,5);

complex c=a+b;

complex d=++a;

complex e=b--;

//complex f=0.234; //这样写现在还不行,因为上面没写相应的拷贝构造函数

//你可以试着写一个

complex f;

f=a=0.234; //链式表达式

a.print( );

c.print( );

d.print( );

e.print( );

f.print( );

}

除了"."、 ".*"、 "::"、 "?:"四个运算符外,其它运算符(包括new、delete)都可被重载,cin和cout就是两个典型的例子。

对于双目运算符(即A?B),如加、减、乘、除等,可这样重载:

"complex operator ?(complex B);"运算时就好像调用A的这个方法一样

对于前置的单目运算符(即?A),如"-A"、 "--A"、"++A"等,可这样重载:

"complex complex:perator ?( );"

对于后置的单目运算符,如"A--"、 "A++",可这样重载:

"complex complex:perator ?(int);",其中参数表中的int不能省去。

下面出一道题让大家考虑考虑吧:创建一个字符串类并将+、-、=、==等运算符重载,使我们可以直观地操作字符串。

2.5 类的继承

OCK aspectratio="t" v:ext="edit">OCK>


可以继承是类的第二个优点,它使大型程序的结构变得严谨并减少了程序员的重复劳动。继承到底是什么呢?举个例子,比如说树和猫这两样东西,看起来好像毫不相干,但它们都有质量、体积等共有的属性和买卖、称量等共有的方法。所以我们可不可以先定义一个基类,它只包含两样事物共有的属性和方法,然后再从它派生出树和猫这两样事物,使它们继承基类的所有性质,以避免重复的定义呢?答案是肯定的。由于一个类可以同时继承多个类,一个类也可同时被多个类继承,我们可以建立起一个复杂的继承关系,就象这样:

2.1

我们可以看到,继承是很灵活样的。要说明继承是很简单的,只要像这样定义派生类即可:

class 派生类名 :派生性质 基类名1,派生性质 基类名2,...,派生性质 基类名n

{

这里面同定义普通类一样,不必再说明基类中的成员

};

关于这里的派生性质,有这样一张表可供参考:

 楼主| 发表于 2006-1-6 11:47:02 | 显示全部楼层

表2.1>>

派生性质>>

在基类中的访问权限>>

在派生类中的访问权限>>

public>>

public

public

protected

protected

private

不可访问

protected

public

protected

protected

protected

private

不可访问

private

public

private

protected

private

private

不可访问

 楼主| 发表于 2006-1-6 11:47:32 | 显示全部楼层
这张表中的后两栏意思是:当基类中设置了这种访问权限的成员被派生类继承时,它将等价于设置了什么访问权限的派生类的成员。>>

下面我们看一个例子:>>

#include <iostream>>>

using namespace std;>>

> >

class THING //定义基类

{

protected:

int mass,volume;

public:

THING(int m, int v);

int get_mass( );

void set_mass(int new_mass);

};

THING::THING(int m, int v)

{

mass=m;

volume=v;

}

int THING::get_mass( )

{

return mass;

}

void THING::set_mass(int new_mass)

{

if (new_mass>=0)

mass=new_mass;

}

class ANIMAL: public THING //定义派生类

{

private:

int life;

public:

ANIMAL(int x) : THING(10+x,7) { life=x; }; //定义派生类的构造函数时需要给

//出初始化基类的办法

//如有多个基类,用逗号隔开分别//提供参数

void set_life(int new_life) { if (new_life>=0) life=new_life; };

int get_life( ) { return life; };

void kill( ) { life=0; };

};

ANIMAL cat(50);

void main( )

{

cout<<cat.get_mass( )<<endl; //cat继承了THING类的方法

cout<<cat.get_life( )<<endl;

cat.set_life(100); //也有自己的方法

cat.kill( );

cout<<cat.get_life( )<<endl;

}

当某类同时继承了多个类而这些类又拥有相同名称的函数时,我们可以使用像这样的语句说明要使用的是哪一个类的方法:child->father::get_life();。

 楼主| 发表于 2006-1-6 11:48:07 | 显示全部楼层

2.6 虚函数和抽象类>>

虚函数体现了类的第三个优点:多态性(看上去好像很深奥)。 >>

有时候,在一个含有基类和派生类的程序中,我们需要在派生类中定义一个和基类的方法具有相同的函数名、返回类型和参数表,但函数的具体内容不同的方法。比如说,我们首先定义了一个"植物"类,然后又定义了一些它的派生类"松树"、"柳树"、"杨树"等等,然后在派生类中重载了"种植"方法,因为我们知道它的实现随着树种的不同而不同。但此时当一个"植物"类的指针指向一个"柳树"类的对象时(这是合法的),基类指针还是只能访问基类的"种植"方法,而不是在派生类中重新定义的方法!解决问题的办法是在基类中把这个方法定义为虚函数。>>

虚函数的定义方法是在基类声明成员函数时在最前加关键字"virtual"。>>

我们也举一个例子来说明虚函数的使用方法:>>

#include <iostream>

using namespace std;

class Base

{

public:

int a;

virtual int get_a( ) { return a; };

};

class Child: public Base

{

public:

int get_a( ) {return a*a; };

Child(int aa) {a=aa; };

};

Child child(10);

void main( )

{

Base *p;

p=&child;

cout<<p->get_a( );

}

从运行结果可以看到,调用的是Child类的get_a( )。你可以试一试删去virtual看看输出有什么变化。

值得注意的是基类的析构函数一定要是虚函数,否则在你通过基类的指针delete派生类的对象时显然将不会调用派生类的析构函数,这可不是我们希望看到的。另外,在派生类重载基类的函数是没有作用的,编译器只会根据指针的类型选择调用哪个函数。

有时候我们不需要用基类来定义对象,则可把基类的函数定义为纯虚函数,也不需再在基类中给出函数的实现。这时基类就被称为抽象类。还是上面的哪个植物的例子,由于我们这时显然不会定义一个"植物"类的对象,只会根据具体的树的不同而选择一个相应的类,所以我们完全可以把"植物"类中的虚函数全部定义为纯虚函数。定义纯虚函数的方法是在加了"virtual"后再去掉函数体并在声明后加上"=0"。就象这样:"virtual int get_a( )=0;"。

 楼主| 发表于 2006-1-6 11:50:11 | 显示全部楼层

2.7 模板>>

模板(template)是C++语言提供的一个有趣而有用的东西,它可以使我们快速地定义一系列相似的类或函数。下面我们先看看如何用使用模板来定义类:>>

#include <iostream>>>

using namespace std;>>

> >

template <class T> //这是一个所谓的prefix,即前缀。< >内为模板参数,在这

//里是一个类T。有了这个前缀,下面的半条语句就可以把T

//当作一个类的名称使用

class List

{

private:

T *a; //a是一个指向T类型的数据的指针

public:

int size;

List(int n);

T operator[ ](int i); //List[ ]的返回值为T类型的数据

~List();

};

template <class T> List<T>:ist( int n ) //哇!这是天书吗......

//其实很好懂,首先把前缀去掉,

//剩下的List<T>中的<T>是必须

//重复一次的参数表

{

a = new T[n]; //使a成为一个成员为T类型的数据的数组

for (int i=0; i<n; i++)

a=(T)(i+47); //a数组分配内容

size = n;

}

template <class T> List<T>::~List()

{

delete[] a;

}

template <class T> T List<T>:perator[ ](int i) //注意List<T>前的T

//这个函数的返回值的类型

{

return a+1; //和普通的[]有一点小小的区别J

}

void main()

{

List <char> c(10); //<char>给模板提供参数,说明Tchar

//我们完全可以把char看成是一个类

for (int i=0;i<c.size;i++)

cout<<c;

}

我们可以用类似的方法定义有多个参数的模板,比如:

#include <iostream>

using namespace std;

template <class T,int U> class List

{

private:

T *a;

public:

int size;

List();

T operator[ ](int i);

~List();

};

template <class T,int U> List<T,U>:ist()

{

a = new T[U];

for (int i=0; i<U; i++) a=(T)(i+48);

size = U;

}

template <class T,int U> T List<T,U>:perator[ ](int i)

{

return a+1;

}

template <class T> List<T>::~List()

{

delete[] a;

}

void main()

{

List <char,10> c; //注意,你只能把常数赋值给模板的"实际"参数

//因为模板的本质是直接替换!就像#define一样

//所以你不用担心它的效率J

for (int i=0;i<c.size;i++)

cout<<c;

}

下面我们再来看看如何用模板定义函数:

#include <iostream>

using namespace std;

template <class T> T print(T n); //和定义类时差不多,返回值为T类型,参数

//n也为T类型

template <class T> T print(T n)

{

cout<<n<<endl;

return n;

}

void main()

{

float x=3.14;

print(x);

char y='m';

cout<<print(y); //编译器会根据变量类型自动构造函数

}

大家是不是觉得有点像函数重载呢?不过不必浪费时间去写几乎完全一样的函数了。还记得1.3节所介绍的用#include定义的函数吗?它的好处是适用于所有数据类型,但现在,我们用模板也可以实现完全相同的功能了。注意编译器实现模板的办法实际上也是根据数据类型的多少创建一堆差不多的类或函数。

其实模板的引入就像当初类的引入一样有着重大的的意义,一种新的编程思想应运而生:Generic Programming (GP)。这种编程思想的核心是使算法抽象化,从而可以适用于一切数据类型。著名的STL(Standard Template Library)就是这种思想的应用成果。感兴趣的读者可以自己找一些这方面的书看看,对自己的编程水平的提高会有好处。

 楼主| 发表于 2006-1-6 11:50:29 | 显示全部楼层

2.8 优化程序>>

首先提醒大家一句,再好的语句上的优化也比不上算法上的优化所带来的巨大效益,所以我觉得对这方面不太熟悉的人都应该买本讲数据结构与算法的书来看看。在第八章讲述了几种常用的算法,如果你感兴趣可以看看。>>

下面就转入正题,讲一讲一般的优化技巧吧:>>

(1)使用内联函数。>>

> >

(2)展开循环。

for (i = 0; i < 100; i++)

{

do_stuff(i);

}

可以展开成:

for (i = 0; i < 100; )

{

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

}

(3)运算强度减弱。

例如如果有这样一段程序:

int x = w % 8;

int y = x * 33;

float z = x/5;

for (i = 0; i < 100; i++)

{

h = 14 * i;

cout<<h;

}

上面的程序这样改动可以大大加快速度:

int x = w & 7;

int y = (x << 5) + x; //<<+的运算优先级低!

float z = x*0.2;

for (i = h = 0; i < 100; i++)

{

cout<<h;

h += 14;

}

(4)查表。这种方法挺有用。比如说我们定义了一个函数f(x)可以返回x*x*x,其中x的范围是0~1,精度为0.001,那么我们可以建立一个数组a[1000],a[t*1000]存储的是预先计算好的t*t*t的值,以后调用函数就可以用查找数组代替了。

 楼主| 发表于 2006-1-6 11:50:44 | 显示全部楼层

2.9 调试程序>>

每个人都会犯错误,在编程中也如此。写完程序后第一次运行就直接通过的情况实在是不多的,偶尔出现一两次都是值得高兴的事。有错当然要改,但很多时候最难的并不是改正错误,而是找到错误,有时候写程序的时间还不如找错误的时间长。为了帮助大家节省一点时间,下面就讲一讲一点找错误的经验。>>

首先当然要说说常见的错误有哪些,最经常出现的是:漏分号、多分号、漏各种括号、多各种括号、"=="写成了"="(上面的错误看上去很弱智,不过也容易犯)、数组(指针)越界(最常见的错误之一!)、变量越界、指针使用前未赋初值、释放了指针之后继续使用它……等等。如果你的程序有时出错有时又不出错,很可能就是指针的问题。>>

有一点要注意的是VC.net显示出的出错的那一行有可能不是真正出错的位置!>>

常用的找错办法就是先确认你刚刚改动了哪些语句,然后用/*和*/把可能出错的语句屏障掉,如果运行后还不通过就再扩大范围。即使有一段程序你觉得不可能有什么问题或以前工作正常也要试试将它屏障,有时就是在似乎最不可能出错的地方出了问题。>>

还有一种大家都经常用的找错办法就是把一些变量的值显示在屏幕上,或是把程序运行的详细过程存入文件中,出什么问题一目了然。如果再像QuakeIII一样用一个"控制台"显示出来就很酷了。

象其它编译器一样,VC.net提供了变量观察(Watch)、单步执行(Step)等常规调试手段,当然你首先需要把工程设为Debug模式。然后设置好断点(在要设置断点的那一行左边的灰色区域按一下即可,会出现一个红圆,程序运行到此处会暂停),按F5就可以开始调试。此时会出现一个调试工具栏:

OCK aspectratio="t" v:ext="edit">OCK>

2.2 调试工具栏

图标的意义分别为:执行此语句,停止此语句的执行,停止调试,重新调试,显示即将执行的语句,调试入函数,跳过函数,调试出此{},用十六进制显示数据,显示断点情况。

大家还会注意到左下角出现了一个变量观察窗口,在这里可以非常方便地观察变量的值和改变情况。

我们还可以打开反汇编窗口、内存观察窗口和寄存器观察窗口,它们可是威力无比的,用起来非常爽。观察编译器生成的代码也是深入了解C++语言华丽的外表背后的真相的好办法。

VC++还提供了两条调试语句可以帮助你调试,第一条语句是assert。它的使用方法是assert(条件),你可以把它放到需要的地方,当条件不满足时就会显示一个对话框,说明在哪个程序哪一行出现了条件不满足,然后你可以选择停止,继续或是忽略。这条语句非常有用,因为直接执行程序时(而不是在VC++中调试)它也能工作。第二条语句是OutputDebugString (要输出的字符串),可以在屏幕下方编译窗口的调试那一栏显示这个字符串。

利用VC.net的开发环境调试是一项十分方便的事,只要你多调试(这不用刻意追求,因为它不可避免J),一定可以越来越熟练。

本书的C++语言部分到此可以告一段落了,但这里所讲述的只是C++语言的冰山一角,因为C++语言可被称为博大精深,而且它还在不断发展。希望大家在以后的日子里不要停止对C++语言的学习和研究,你一定会不断有新的感受和发现。最后推荐两本必读的好书:Scott Douglas Meyers的Effective C++和More Effective C++(其中不少内容我已经穿插到了前面的文字中J)。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-5-6 05:59

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表