WindyWinter windy@ream.at C++ WindyWinter windy@ream.at
课程适用性 迫于时间所限,本课程只能面向C语言程序设计基础较好的同学。 希望经过短期训练,让大家能读懂WrightEagleBASE中涉及语言特性的部分。 合格的C++入门和参考书籍有C++ Primer、The C++ Programming Language和Thinking in C++。 大家都知道,学习编程最困难的部分不在于编程语言本身的特性,而在于建立用程序解决问题的思维模式。所幸大家都学过C语言,我就省了很多事儿了,又所幸C++是由C语言发展来的,所以我们就从C语言的几角旮旯开始,讲C++本身的东西。 C++ Primer最适合入门,The C++ Programming Language适合已经对另外一门面向对象的语言非常熟悉的同学。
C语言复习
整型 类型(C++) 下限 上限 short (int) -32767 32767 (long) int -2147483647 long long (int) -92233720368 54775807 9223372036854775807 类型(C) 下限 上限 (short) int -32767 32767 long (int) -2147483647 2147483647 long long (int) -92233720368 54775807 9223372036854775807 这里C语言和C++有一个不大不小的区别,C语言里的int通常是short int,而C++里通常是long int,这导致熟悉了C语言的人写C++程序没问题,但熟悉了C++的人倒回去写C程序肯定各种不适应。 当然这个是编译器相关的,不同的编译器有不同的默认设定。
Microsoft Engineering Excellence 浮点型 浮点数也是离散的。 浮点数的内部表示也是二进制,比如float,一共32个二进制位,所以不可能表示出超过2^32个数字。 IEEE 754 浮点数的比较 if (a == b) X if (fabs(a-b) < FLT_EPSILON) O if (fabs(a-b) < DBL_EPSILON) O FLT_EPSILON和DBL_EPSILON这两个常量是在float.h中定义的,它们的意义分别是float和double类型中,1.0和第一个比1.0大的数之间的差值。这两个值分别是1e-5和1e-9。 Microsoft 机密
Microsoft Engineering Excellence 其它类型 _Bool float _Complex / double _Complex 衍生类型 array、structure、union、function、pointer 类型限定符 const、volitile、restricted void类型 取值集合为空集的类型 C语言原本没有逻辑类型,用0表示假,用非0表示真, C99标准新规定了一个逻辑类型_Bool。即便有了逻辑类型,仍然沿用“用0表示假,用非0表示真”的做法。 _Bool和_Complex实际上是在C++先有了之后又加入C的,但在C++里的写法更好看一点。 加了类型限定符的类型叫qualified type,没加的叫unqualified type。 衍生类型里的array和structure常用,union的一个用处是转换变量类型,比如utilities.h里的一个sqrt函数就用上了,使用u.x时就把u占据的内存区当作IEEE 754标准的double类型变量,u.i就当作一个64位整数对待。 void类型只能用在函数返回值上,或者作为指针基类型,任何指针类型都可以隐式类型转换为void指针类型。 Microsoft 机密
Microsoft Engineering Excellence 其它类型 bool complex<float> / complex <double> 衍生类型 array、structure、union、function、pointer 类型限定符 const、volitile、restricted void类型 取值集合为空集的类型 C语言原本没有逻辑类型,用0表示假,用非0表示真, C99标准新规定了一个逻辑类型_Bool。即便有了逻辑类型,仍然沿用“用0表示假,用非0表示真”的做法。 _Bool和_Complex实际上是在C++先有了之后又加入C的,但在C++里的写法更好看一点。 加了类型限定符的类型叫qualified type,没加的叫unqualified type。 衍生类型里的array和structure常用,union的一个用处是转换变量类型,比如utilities.h里的一个sqrt函数就用上了,使用u.x时就把u占据的内存区当作IEEE 754标准的double类型变量,u.i就当作一个64位整数对待。 void类型只能用在函数返回值上,或者作为指针基类型,任何指针类型都可以隐式类型转换为void指针类型。 Microsoft 机密
Microsoft Engineering Excellence 指针 float * const float指针常量类型 const float * 常量float类型的指针类型 DecisionDataDerived ** 指针的指针 E1[E2] (*((E1)+(E2))) A[3] *(A+3) 3[A] *(3+A) = *(A+3) 方括号运算符在任何地方都这样展开。 Microsoft 机密
Microsoft Engineering Excellence const和#define #define属于预编译的范围,“符号替换”;const定义的是“不能修改的”变量。 #define只能定义有“字面值”的常量;const可以定义数组、结构体和union常量。 enum可以定义整数常量,一般用于互相关联的一组整数。 enum PlayMode{} #define的符号在编译出来的程序里是不存在的,编译器在预处理阶段就把他们替换掉了。 const变量跟非const变量一样都是在内存里的,const变量语义上不能被修改,实际上非要改也是可以的。 只有内置类型才有“字面值”。 除了#define和const以外还可以用enum定义常量。比如底层代码的types.h里面就有一大堆enum,定义的每个符号其实都是代表一个整数。 Microsoft 机密
预编译指令 #include 后面不是“字符串”,是用引号或者<>括起来的文件名。 条件编译 #define 符号 #undef 符号 #if / #elif 字面值常量表达式 #ifdef / #ifndef / #else 符号 不符合#if条件的代码段编译器不编译 引号括起来的编译器优先在源文件所在的目录下寻找,<>括起来的编译器先在系统目录下寻找。 #if后面的常量是指字面值,不是const变量 条件编译:types.h #ifdef _Debug
预编译指令 #define中“#”的用法 #x 将x变成字符串 x##y 将x与y连接起来 #define TeammateFormationTactic(TacticName) (*(FormationTactic##TacticName *)mFormation.GetTeammateTactic(FTT_##TacticName)) TeammateFormationTactic(KickOffPosition) *(FormationTacticKickOffPosition*)mFormation.GetTeammateTactic(FTT_KickOffPosition))
其它 sizeof运算符 inline函数 static变量 size_t fsize(int n) { char t[n+3]; return sizeof(t); } fsize(10) -> 13 inline函数 static变量 sizeof可以跟变量名,也可以跟类型名,返回该变量或该类型占据的内存字节数,size_t类型,32位或64位整数。 inline函数,utilities.h,提示编译器,在函数调用时,不必走函数调用的过程,可以直接做代码展开。决定权在编译器。 通常的局部变量、函数参数,都是放在栈上的,每次函数调用都会被初始化一次,函数结束销毁;加static的变量,放在静态存储区,整个程序运行过程中只初始化一次,程序结束时销毁。
C++标签
即时声明 C语言要求所有变量的声明必须在实意语句之前,也就是在所有{}的外面,或者是每对{}的最前面。 C++没有了这样的限制,变量只要遵循先声明后使用的原则就可以了,不再要求必须放在什么地方。我们可以在for语句头部塞上一个int i(0),for (int i(0); i < 10; ++i)。 “int i(0)”里的(0)是指将i初始化为0,作用相当于int i=0。
引用 引用(reference)是C++新定义的一种复合类型,其本意可以理解为变量的“别名(alternate name)”。 声明/定义一个引用: int a; int & r = a; r被定义为a的引用后,r和a可以被认为是同一个变量。 引用的主要用在函数形参中(作用与指针相仿): 避免传递规模巨大的实参; 将形参的值返回。 这里虽然有一个“=”,但却不是赋值运算符,而是“定义”。除非是函数形参列表中,否则引用的声明和定义必须在一起。 (作用与指针相仿)——形参与实参将共享同一个内存单元。
引用 void BehaviorAttackPlanner::Plan(std::list<ActiveBehavior> & behavior_list) PlayerState & player = const_cast<PlayerState &>(*mpWorldState->GetPlayerList()[i]); player.GetPos(); 每一个BehaviorXXXPlanner都有一个Plan函数,这个函数的形参是一个behavior list,是个链表,实参可能有几百K大,使用引用,就避免了复制,同时也使behavior list在Plan函数中能被“更改”。 有时候引用就只用来做“别名”,比如这个player引用的变量,非常非常长而且涉及指针、函数、数组、类型转换,给它设定一个引用就很合适。 因为指针和引用非常相似,C++里面基本上是混用的,但是用引用的多一点,因为引用用法跟普通的变量一样,指针则要使用解引用运算符*。
左值与右值 左值 右值 赋值运算符左边必须是左值 赋值运算符右边既可以是左值,又可以是右值 变量皆是左值 常量皆是右值 变量的引用是左值 常量的引用是右值 ++i是左值 i++是右值 函数、表达式可以返回左值——以引用的形式 函数、表达式可以返回右值——以值的形式 C++里出现了引用,就不得不提一个被强化的概念——左值和右值。这两个定义的来源是以赋值运算符为标准的。 左值既可以出现在赋值运算符的左边,也可以出现在右边;而右值只可以出现在右边。
类型转换 C++继承了原有的C语言的隐式类型转换; 所有的类型都可以隐式转换为该类型的引用: int => int &, int * => int * &, 所有的类型都可以隐式转换为该类型的常量; 强制类型转换在C++中有了另一类写法: (type) a xxx_cast<type> a; static_cast<type>实现与C中类型转换相同的功能; const_cast<type>去掉表达式的常量性; 另外还有reinterpret_cast和dynamic_cast 隐式类型转换就是自动的类型转换,不需要显式的写出来的。除了void指针隐式转换为其它指针之外。 C语言中一个带括号的类型名称后面跟一个表达式,表示将表达式的结果强制转换为指定的类型。 C++中对应的写法为xxx_cast<类型名>表达式。 前面的player引用中就有一个const_cast,注意const_cast应该慎用,后两种cast将不再涉及。
输入输出 std::cout << a << b << c << d <<std::endl; Logger::instance().GetTextLogger(“test”) << a << b << c << d << std::endl; 输出到Logfiles/WrightEagle-X-test.log中。 cin和cout是C++新定义的流输入输出方式,它们的用法是用“>>” 或“<<” (提取,插入)像串糖葫芦一样把变量串起来,被串起来的变量将按顺序被读入或输出。最后那个std::endl表示换行。
形参默认值 形参允许有默认值,即函数可以声明为如下形式: bool KickBall( Agent & agent, double angle, double speed_out, KickMode mode = KM_Hard, int *cycle_left = 0, bool is_shoot = false ); 有默认值的形参必须是该函数的最后一个或几个形参。
形参默认值 调用的时候可以不写有默认值的参数 都是合法的 没写的参数,实参值就是默认值。 KickBall(agent, 0.0, 3.0); KickBall(agent, 0.0, 3.0, KM_Quick); KickBall(agent, 0.0, 3.0, KM_Quick, &cycle); KickBall(agent, 0.0, 3.0, KM_Quick, &cycle, true); 都是合法的 没写的参数,实参值就是默认值。
函数重载 允许不同的函数有相同的函数名。 “不同的函数”是指形参的类型、数目或返回值的类型不同的函数。 bool GoToPoint(Agent & agent, Vector pos, double buffer = 0.5, double power = 100.0, bool can_inverse = true, bool turn_first = false); void GoToPoint(Agent & agent, AtomicAction & act, Vector pos, double buffer = 0.5, double power = 100.0, bool can_inverse = true, bool turn_first = false); 类型不同或数目不同均可,但若只有返回值类型不同则不行。 两个GoToPoint函数参数具有显著的不同,但名字一样,调用时由编译器根据传进的参数决定到底调用哪一个。
new和delete运算符 C语言用malloc和free。 C++用new和delete。 client = new Player; mWeight = new real**[mLayers]; delete[] mWeight; new和delete是C++引入的运算符,作用与C中的malloc和free相仿。
面向对象的C++
类 类是C++的新特性,为适应面向对象的程序设计而提出; 在C中,已经有了结构体的概念; 类与结构体的最大不同之处在于——不仅可以包含成员变量(常量),还可以包含成员函数。 当然,类还包括一些其他的特性: 成员变量、成员函数的访问权限; 构造函数; 析构函数; 拷贝构造函数; 隐式类型转换; …… 结构体在C中的实质是一堆变量的集合;在C++中,struct关键字仍然有效,但其意义已经改变——仅仅是另外一种声明一个类的方法。一个类应当被看作一种类型,这种类型声明的变量叫做对象,或者叫做这个类的实例。
一个著名的类 class BaseState { public: BaseState(); BaseState(const BaseState & o) {} void UpdatePos(const Vector & pos , int delay = 0, double conf = 1); const Vector & GetPos() const { return mPos.mValue; } int GetPosDelay() const { return mPos.mCycleDelay; } const double & GetPosConf() const { return mPos.mConf; } void UpdatePosEps(double eps) { mPosEps = eps;} const double & GetPosEps() const { return mPosEps;} private: StateValue<Vector> mPos; double mPosEps; }; 声明一个类,用class关键字,样式和声明一个结构体是差不多的。不同的是多了private和public这两个关键字,他们被称作成员的访问权限。声明在public之下的成员用法与结构中的一样,在任何地方都可以直接访问这个成员;声明在private之下的成员,则只能被本类中的成员函数访问,在类外部是不可见的。不过,这一点我们很快就会做出修正。除了这两种访问权限之外,还有一种protected,后面也会讲到他的意义,protected成员的访问权限与private成员一致。 如果声明一个成员之前没有出现访问权限的标识,则默认为private。struct也可以声明一个类,与class的唯一区别是——struct中没有访问权限的成员默认为public。这么做的原因是令C代码可以无障碍的迁移到C++。 BaseState类中,两个个成员变量是private的,在类之外不能访问。所有的成员函数都是public的,可以在类外部被访问。
成员函数的定义 成员函数可以直接在类定义里定义,也可以单独在外面定义。 void BaseState::UpdatePos(const Vector & pos, int delay, double conf) { mPos.mValue = pos; mPos.mCycleDelay = delay; mPos.mConf = conf; } 比如GetPos等函数比较简单,就直接在类定义的地方定义,UpdatePos稍微复杂一点,就单独拿到BaseState.cpp里面定义。 ::是“域运算符”,指明UpdatePos是BaseState的成员,前面有过std::cout,那里也是域运算符,是指明cout是std的成员。
this指针 每个类都有一个特殊的“成员”——this,表示对象自身; this只能在该类的内部使用,与不指明this没有区别: this->mPos mPos this->mPosConf mPosConf void UpdatePosEps(double mPosEps) { this->mPosEps = mPosEps;} 通常情况下,指明this和不指明this完全等价,不过存在特殊的情况,想必大家都遇到过,C语言里一个局部变量可以屏蔽全局变量,使得在局部变量的生存周期内被屏蔽的全局变量无效——某成员函数的形参恰好与类的成员变量同名,在该函数中,成员变量被形参屏蔽,访问时需要用this指针指明。比如把UpdatePosEps函数的形参改成mPosEps,函数里就只能用this指针访问mPosEps。
成员函数的const属性 const Vector & GetPos() const GetPos不能更改任何成员变量的值,在函数内部 this指针变成指向常量的指针; 任何成员变量被附加const属性。 这种声明主要用于指明该函数不会更改成员变量的值。 这个函数的声明比一般函数多了点东西,不仅前面有个const,而且后面还有一个const。前面那个const是返回值类型,常量vector引用,后面那个const是重点,加了这个const之后…… 成员变量被附加的const属性实际上来自于this指针的常量性,其道理很浅显——一个常量实例的任何成员都是右值。这也说明,this指针的存在,是成员函数可以访问成员变量的原因。 这样的声明,意在向别人说明,调用这个函数之后,成员变量不会发生任何变化。
构造函数 没有返回值类型,与类同名的函数被认为是构造函数。BaseState() 它的作用就是——构造一个对象。 BaseState() : mPosEps(10000); { mPos.mValue = Vector(10000 , 10000); } 冒号之后到括号之前,是构造函数特有的初始化列表,每一个成员变量用括号的形式初始化(只能用括号的形式),成员变量之间用逗号分隔。 不带任何参数的叫默认构造函数,如果定义对象时,既没有使用括号的形式初始化,也没有使用=的形式初始化,那么默认构造函数将被调用。
构造函数 BaseState(const BaseState & o) { mPos = o.mPos; } 如果将某个构造函数声明为private,则这个构造函数将无法使用。一般来说,这样做的目的是阻止编译器生成缺省的构造函数。 class Agent{ Agent(Agent &); …… }; 带同类型引用或常量引用为参数的,叫拷贝构造函数,定义对象时,如果用=形式初始化,拷贝构造函数将被调用; 带其他类型参数的,是一般的构造函数,定义对象时,用括号的形式初始化,实际上就是调用对应的构造函数。 当前两种构造函数缺省时,编译器将自动合成缺省的构造函数: 对于内置类型的成员变量,缺省默认构造函数不做任何事情,缺省拷贝构造函数复制其值; 对于类类型的成员变量,缺省默认构造函数将调用该成员变量的默认构造函数,缺省拷贝构造函数调用该成员的拷贝构造函数, 若该成员变量的默认构造函数或拷贝构造函数不可用(下面将会说明为什么不可用),那么将无法通过编译。 不能显式调用构造函数——即构造函数只会在对象定义时被调用一次,今后再无它途。
构造函数 class Line { public: Line(const Ray &r); …… }; 只带有一个参数的构造函数表明了一种可能的隐式类型转换。 这个构造函数表面Ray类型可以隐式转换为Line类型。
析构函数 没有返回值,名字是~<class name>,没有参数的函数是析构函数。构造函数可以有多个,析构函数只能有一个。 它的作用是销毁一个对象。 如果没有声明析构函数,编译器将合成默认析构函数 对于内置类型,释放其空间; 对于类类型,调用其析构函数。 实际上,上面两步是编译器附加在任何析构函数最后的两步。因为没有办法显式“释放空间”和调用析构函数。 当对象离开其生存空间,或者被delete时,析构函数将被调用。最常见的用法是,构造函数中用new创建了变量,析构函数中用delete擦屁股。
new和delete运算符 mallac/free和new/delete的区别在于 前者只涉及内存分配 后者不仅分配/释放空间,还要创建/销毁对象。 new运算符首先分配对象占据的空间,然后在其上调用构造函数;delete首先完成对象本身的销毁步骤,然后释放空间。 注意,不能用free释放new出来的对象占据的空间。 C++中malloc/free作用的存储区是自由存储区,new/delete作用的存储区是堆区。
静态成员 static关键字也可以修饰类的成员 class Dasher{ public: static Dasher & instance(); static Array<double, 8> DASH_DIR; static Array<int, 8> ANTI_DIR_IDX; static Array<double, 8> DIR_RATE; static int GetDashDirIdx(const AngleDeg & dir); }
静态成员 被修饰的成员叫做类的静态成员,是这个类的属性,不是某个对象的属性。 访问用:: Dasher::instance() Dasher::GetDashDirIdx() 不能用成员运算符.访问。不在构造函数中初始化,不在析构函数中被撤销。 在静态成员函数中,不存在this指针,不能访问类的非静态成员。 但是静态成员即使没有这个类的对象也可以使用。
运算符重载 C++不仅提供了对函数的重载,也提供了对运算符的重载。运算符可以视为特殊的函数。 双目运算符 单目运算符 Time Time::operator-(const int a); int Time::operator-(const Time &a); 单目运算符 _Tp & ObjectArray::operator[](const ObjectIndex & i) 特别的运算符重载:++、--。 虽然可以视为函数,但只能重载已经存在的运算符,不能自定义新的运算符。 不能重载内置类型之间的运算符。 自增、自减运算符特殊,是因为他们在表达式之前与表达式之后表示的意义并不完全相同。他们的重载方法请诸位自行查找相关资料,并不复杂,只是需要区分。 这里讲的运算符重载是非常简单的,最简单的情况。
运算符重载 还有一类特殊的运算符也可以被重载: opetator T() operator int(); operator xxx(); 这样的运算符必须是某个类的成员函数,它为这个类提供向特定类型的隐式类型转换。 更多的很多情况下,运算符重载是一个复杂的工程。在你真正掌握重载之前,请慎用。
继承与派生 class MobileState: public BaseState { void UpdateVel(const Vector & vel , int delay = 0, double conf = 1); const Vector & GetVel() const { return mVel.mValue; } int GetVelDelay() const { return mVel.mCycleDelay; } …… };
继承与派生 前面定义了BaseState类的一个派生类MobileState,BaseState是MobileState的基类。 它将获得BaseState类的一切成员,还另外附加了很多Vel相关的成员。 “一切成员”,不包括基类的构造函数、析构函数、new运算符和=运算符。但派生类中可以访问他们。 派生类对象可以隐式转换为基类类型; 派生类类型的指针可以隐式转换为基类类型的指针 BaseState * object = new MobileState; 与之相对的是,派生类与基类是不同的类,派生类获得基类的私有成员,但不能访问基类的私有成员。 public关键字表示继承的访问关系控制,类似还有private继承和protected继承。 在private继承中,基类的public成员和protected成员成为派生类的private成员; 在protected继承中,基类的public成员和protected成员成为派生类的protected成员; 在public继承中,基类的成员的访问权限不变。 说指针可以隐式转换是不太正确的,这里表现起来的确像是类型转换,但是object指针仍然保持了MobileState的特性。
虚函数与多态 在声明某个成员函数时加上virtual修饰符,表示允许派生类重载该函数;在声明析构函数时加上virtual修饰符,产生特殊效果。 class Client { virtual ~Client(); virtual void RunNormal(); …… }; 虚析构函数与销毁派生类中的指针的机制有关。
虚函数与多态 Client client = new Player; client->RunNormal(); virtual void Run() = 0; client虽然是Client类型指针,但调用的RunNormal()函数却是Player类的重载版本。这一特性叫做多态。 虚函数的“虚”是翻译的结果,他们是真实存在的函数,要想让它不存在,变成真正的“虚”函数,要在声明后面加“=0”,这样的虚函数叫纯虚函数,他只有一个声明,没有定义,不能被调用。带有纯虚函数的类不能用来定义对象,派生类如果不重载这个纯虚函数,一样不能定义对象。
友元 class Agent{ friend class Client; …… }; 友元是一个声明。友元不是类的成员。 有时我们需要在类外部访问类的private成员,这时需要声明一个类的友元。友元可以是另外一个类,可以是函数,可以是某个类的成员函数,也可以是运算符。被声明为友元的东西可以访问这个类的private成员。 友元的声明要按声明的完整格式写,还要附加friend修饰符。
Q&A