走向C++之路 WindyWinter windy@ream.at WindyWinter感谢诸位前来捧场。 #include <stdio.h> main(t ,_,a) char*a;{return t<1?main(*a,a[-t],"=a-1kj3gnm:q\ ebh_cf*<r.d>i^+?,()[?qzyrjuvcdefg\ h,!kbpolwxs'.t main(")&&a[-t]&&main (t-1,_,a):t/2?_==*a?putchar(32[a]) :_%115<36||main(t,_,a+1):main( 0,t,"+b:s?#mw{ty}t(x1{|~?\ y<#q?(*#{k)}rsh?vts){\ ?w*#yk<y,}w}z!w)v\ ~>u:!zym^t|x|\ |xtutu!uz\ |#}t") ;}
课程适用性 迫于时间所限,本课程只能面向C语言程序设计基础较好的同学, 并且希望经过短期训练,让你能读懂、写出合格的C++程序, 至少不再认为今后将面对的RoboCup 2D球队底层是天书, 以及改善面对超过一个文件的工程即手足无措的状况。 注意:本课程不会建立面向对象和泛型编程的思维模式,因此不能替代《面向对象程序设计》。 建议比较勤快的同学脱离本课程,用《C++ Primer》自学C++语言。 合格的C++入门和参考书籍有《C++ Primer》《The C++ Programming Language》和《Thinking in C++》 C++是C的超集。C99标准有550页,C++03标准有786页。即使你完全记得550页的C99标准说了什么,也还有200多页需要钻研,何况你我都清楚,能记得250页的就已经非常了得了。所以,培训的目标仅仅限定于编写联赛用的球队可能会使用到的C++的特性,而其他的地方,则能省就省。 《C++ Primer》适合入门,《Thinking in C++》适合对C非常熟悉的同学,《The C++ Programming Language》则适合已经对另外一门语言(面向对象的)非常熟悉的同学。
为你的程序贴上C++的标签 从这里开始 C++是C的超集,也就是说,大部分C程序都可以不经改变的直接被C++编译器编译通过(除非C程序中用到了C++的保留标识符)。但贴上C++的标签看起来更好一些。
一个著名的C程序 #include <stdio.h> void main() { int i; for (i=0; i<10; ++i) printf(“Hello World!\n”); } 为了给出一个直观的印象,我们先看一看C++程序跟C程序的相貌差别。 #include <stdio.h> main(t ,_,a) char*a;{return t<1?main(*a,a[-t],"=a-1kj3gnm:q\ ebh_cf*<r.d>i^+?,()[?qzyrjuvcdefg\ h,!kbpolwxs'.t main(")&&a[-t]&&main (t-1,_,a):t/2?_==*a?putchar(32[a]) :_%115<36||main(t,_,a+1):main( 0,t,"+b:s?#mw{ty}t(x1{|~?\ y<#q?(*#{k)}rsh?vts){\ ?w*#yk<y,}w}z!w)v\ ~>u:!zym^t|x|\ |xtutu!uz\ |#}t") ;}
贴上C++的标签 #include <iostream> using namespace std; int main() { for (int i(0); i<10; ++i) cout<<“Hello World!”<<endl; } return 0; 可以看出,C++特有的标签还是不少的,首先是标准库头文件没有了.h后缀,后面又有一个using namespace std的写法,main函数被强制规定为int类型,for循环首部可以新定义变量,初始化也有了新的写法,至于printf,则干脆变成了cout。这些改变我们将在后面一一讲明。 namespace叫做“命名空间”,C++的设计者认为,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,标识符重名的概率非常大,于是设计了 “命名空间” ,每个命名空间中的函数、变量等等互不干扰,重名也没关系。
C++与C的相貌差别 C的写法 C++写法 Glance #include<stdlib.h> _Bool scanf(“%d%d”,&a,&b); printf(“%d%d”,c,d); int *p = malloc(sizeof(int)); free(p); char str[]=“WindyWinter is talented.”; int a[10]; char b[20]; double c[30]; #include<cstdlib> bool int a,b; cin>>a>>b; cout<<c<<d; int *p = new int; delete p; string str(“WindyWinter is talented.”); vector<int> a; vector<char> b; vector<double> c; 所有C的标准库头文件都可以如此改换名称,在C++中使用(需要using namespace std);而且只有标准库头文件才可以如此改换名称,一般的头文件是不需要换的。其实“头文件”仅是一个约定的做法,#include是一个编译预处理指令,其意义仅在于指示编译器将被include的文件的内容粘贴到此处,所以即便你将头文件命名成xxx.ppt也能通过编译。 C语言原本没有逻辑类型,用0表示假,用非0表示真, C99标准新规定了一个逻辑类型_Bool,它在C++里面变成了bool。即便有了逻辑类型,在C++里面,仍然沿用“用0表示假,用非0表示真”的做法。 cin和cout是C++新定义的流输入输出方式,它们的用法是用“>>” 或“<<” (提取,插入)像串糖葫芦一样把变量串起来,被串起来的变量将按顺序被读入或输出,流输入输出方式将在最后做进一步的解释。 new和delete是C++引入的运算符,作用与C中的malloc和free相仿,不过到底是有区别的。 C中的字符串在C++中有了特性更完备的替代品string,C中的字符串在C++中被称作“C风格字符串”,仍然在发挥作用,不过很多时候是在与string配合使用。 vector是与C++中一个重要概念——容器的代表,vector可以看作长度可变的数组,很多时候代替了C数组。
即时声明和初始化方法 C语言要求所有变量的声明必须在实意语句之前,也就是在所有{}的外面,或者是每对{}的最前面。 This is the beginning. C语言要求所有变量的声明必须在实意语句之前,也就是在所有{}的外面,或者是每对{}的最前面。 C++没有了这样的限制,变量只要遵循先声明后使用的原则就可以了,不再要求必须放在什么地方。这就是为什么我们可以在for语句头部塞上一个int i(0)。 “int i(0)”里的(0)是指将i初始化为0,作用相当于int i=0。 但()的初始化方法不限于此: string a(“WindyWinter is talented.”); string b(a); string c = a; ()与=是否永远等价呢?对于内置类型是等价的,但对于类类型,一般是不等价的。 在所有{}外面的叫全局变量。 内置类型有bool, int, double等。 不等价性涉及到类的构造函数和拷贝构造函数,当讲完构造函数之后,我想大家就能完全明白()是什么意思了,但拷贝构造函数将被略过。
引用 引用(reference)是C++新定义的一种复合类型,其本意可以理解为变量的“别名(alternate name)”。 It’s something new. 引用(reference)是C++新定义的一种复合类型,其本意可以理解为变量的“别名(alternate name)”。 声明/定义一个引用: int a; int & r = a; r被定义为a的引用后,r和a可以被认为是同一个变量。 引用的主要用在函数形参中(作用与指针相仿): 避免传递规模巨大的实参; 将形参的值返回。 在C语言中我们已经见过一种很常用的复合类型——数组,任何类型,包括某个数组类型,加上一对方括号都可以构成一个新的数组类型;还有一个复合类型,是——指针。 注意,这里虽然有一个“=”,但却不是赋值运算符,而是“定义”。除非是函数形参列表中,否则引用的声明和定义必须在一起。 什么是声明?什么是定义? (作用与指针相仿)——形参与实参将共享同一个内存单元。 下面这个函数可以把a,b的值交换(注意这个函数写的不好,只是为了省地方才这么写)。 void swap(int & a, int & b) { a^=b^=a^=b; }
引用 指针 引用 The difference between reference and pointer 是一个变量(当然也可以加上const成为常量); 存在“空指针”; 取地址运算符,解引用运算符。 应用应当被看作一个“符号”,与const指针类似; 不存在“空引用”; 用法与正常变量一样。
左值与右值 左值 右值 Left or right, that is the question. 赋值运算符左边必须是左值; 变量皆是左值; 变量的引用是左值; string str是左值; ++i是左值; int *a = new int[10]; a是左值; 函数、表达式可以返回左值——以引用的形式。 赋值运算符右边既可以是左值,又可以是右值; 常量皆是右值; 常量的引用是右值; “WindyWinter is talented.”是右值; i++是右值; int a[10]; a是右值; 函数、表达式可以返回右值——以值的形式。 既然谈到了const和引用,就不得不提一个在C++中被强化的概念——左值和右值。这两个定义的来源是以赋值运算符为标准的。 左值既可以出现在赋值运算符的左边,也可以出现在右边;而右值只可以出现在右边。 注意左右两个a并不是同一个类型,左边是“指向整型的指针”,值是new运算符开辟的那个数组的首地址;右边是一个数组,数组本身并没有“值”的概念,但是在C和C++中,数组名可以转化为“指向数组的指针”,其值为数组的首地址。
函数 inline函数。 形参允许有默认值,即函数可以声明为如下形式: void func(int a, int b=0, int c=1); 如果在调用时没有给出b,c的值,则b=0,c=1,即调用时可以不写有默认值的参数——func(1), func(1,2), func(1,2,3)均是合法的。 允许不同的函数有相同的函数名(被称为函数重载)。 “不同的函数”是指形参的类型、数目或返回值的类型不同的函数,比如: int min(int a, int b); double min(double a, double b); int min(int a, int b, int c); double min(double a, double b, double c); 两两不同。 inline函数在C99标准里已有详细规定,内联函数的意义在于提示编译器,在调用该函数的地方可以直接把函数体粘贴过去,省去函数调用的开销;C++的inline函数与此基本相同。 有默认值的形参必须是该函数的最后一个或几个形参。 类型不同或数目不同均可,但若只有返回值类型不同则不行。
类型转换 C++继承了原有的C语言的隐式类型转换; We can change! – Change what? C++继承了原有的C语言的隐式类型转换; 所有的类型都可以隐式转换为该类型的引用: int => int &, int * => int * &, string => string &; 所有的类型都可以隐式转换为该类型的常量; C风格字符串可以隐式转换为const string; 强制类型转换在C++中有了另一类写法: (type) a xxx_cast<type> a; static_cast<type>实现与C中类型转换相同的功能; const_cast<type>去掉表达式的常量性; 另外还有reinterpret_cast和dynamic_cast 隐式类型转换就是自动的类型转换,不需要显式的写出来的。C语言的隐式类型转换有哪些?哪种转换不是隐式的? C语言中一个带括号的类型名称后面跟一个表达式,表示将表达式的结果强制转换为指定的类型。 C++中对应的写法为xxx_cast<类型名>表达式。 注意const_cast应该慎用,后两种cast将不再涉及。
Tags Cloud Keywords 课程适用性 cstdlib namespace int main() bool cin cout new delete string vector 即时声明 初始化方法() 引用 左值和右值 inline 函数重载 形参默认值 类型转换 强制类型转换 www.google.com www.cplusplus.com/reference 我们休息一下,这是第一段的tag。今天讲的内容是非常简略的,建议大家以此为关键词,搜索相关资料。下面的两个网站是搜索的常用地点——第一个相信大家都认得,第二个可以当作C++的reference。 Have a break.
面向对象的C++ C++为此而生 在软件工程学提出面向对象的程序设计方法后,C被迅速扩充为Objected-C,这些内容后来成为C++的一部分。 在这一节中,我们只关注现象。
类 类是C++的新特性,为适应面向对象的程序设计而提出; 在C中,已经有了结构体的概念; Good morning class. 类是C++的新特性,为适应面向对象的程序设计而提出; 在C中,已经有了结构体的概念; 类与结构体的最大不同之处在于——不仅可以包含成员变量(常量),还可以包含成员函数。 当然,类还包括一些其他的特性: 成员变量、成员函数的访问权限; 构造函数; 析构函数; 拷贝构造函数; 隐式类型转换; …… 结构体在C中的实质是一堆变量的集合;在C++中,struct关键字仍然有效,但其意义已经改变——仅仅是另外一种声明一个类的方法。一个类应当被看作一种类型,这种类型声明的变量叫做对象,或者叫做这个类的实例。
一个著名的类 class person { private: string m_name; int m_age; string m_email; void update(); public: person(); ~person(); string get_namecard(); }; person Cindy; 我们给出一个类声明的例子。 声明一个类,用class关键字,样式和声明一个结构体是差不多的。不同的是多了private和public这两个关键字,他们被称作成员的访问权限。声明在public之下的成员用法与结构中的一样,在任何地方都可以直接访问这个成员;声明在private之下的成员,则只能被本类中的成员函数访问,在类外部是不可见的。不过,这一点我们很快就会做出修正。除了这两种访问权限之外,还有一种protected,后面也会讲到他的意义,protected成员的访问权限与private成员一致。 如果声明一个成员之前没有出现访问权限的标识,则默认为private。struct也可以声明一个类,与class的唯一区别是——struct中没有访问权限的成员默认为public。这么做的原因是令C代码可以无障碍的迁移到C++。 person类中,三个成员变量与update()函数均是private的,在类之外不能访问。get_namecard()是public的成员函数,可以在类外部被访问。 名称为person,没有返回值类型的那个东西,也是一个函数,叫做类的构造函数;名称为~person (tilde person)的函数叫做类的析构函数,马上就会讲到他们的作用。最后我们定义了一个person类型的对象,或者叫person类的实例。C中定义结构体类型的变量,要在结构名前面加struct关键字,C++中不再有结构体的概念,被统一为类,前面说过,类应当被视为一种类型,一个整体,故而声明类类型的变量直接用类的名字就可以了。
域运算符:: string person::get_namecard() { return m_name+m_email; } Well, one is just not enough. string person::get_namecard() { return m_name+m_email; } #include <iostream> int main() std::cout<<“Cindy is a smart girl.”<<std::endl; return 0; person类中声明了一个成员函数get_namecard(),那么,这个函数该怎么定义?一种方法是在声明的地方定义,另一种方法就是将定义写在类声明的外面,用域运算符,就是这两个冒号,指明get_namecard()是person的成员函数。 域运算符也可以用来指明某个标识符是定义在某个namespace中的。这样,可以不写using namespace xxx而引用该namespace下的标识符。 C++标准库的所有内容都统一声明在namespace std中。
this指针和成员函数的const属性 每个类都有一个特殊的“成员”——this,表示对象自身; This is my … self. 每个类都有一个特殊的“成员”——this,表示对象自身; this只能在该类的内部使用,与不指明this没有区别: this->m_name m_name; this->update() update(); 如果修改get_namecard()的声明为: string get_namecard() const ; get_namecard()将不能更改任何成员变量的值,在函数内部: this指针变成指向常量的指针; 任何成员变量被附加const属性。 这种声明主要用于指明该函数不会更改成员变量的值。 通常情况下,指明this和不指明this完全等价,不过存在特殊的情况,想必大家都遇到过,C语言里一个局部变量可以屏蔽全局变量,使得在局部变量的生存周期内被屏蔽的全局变量无效,(提问,有没有人知道是什么特殊情况了?)——某成员函数的形参恰好与类的成员变量同名,在该函数中,成员变量被形参屏蔽,访问时需要用this指针指明。 成员变量被附加的const属性实际上来自于this指针的常量性,其道理很浅显——一个常量实例的任何成员都是右值。这也说明,this指针的存在,是成员函数可以访问成员变量的原因,所以,上面的“与不指明this没有区别”,应该改为“不指明this与指明this没有区别”。 这样的声明,意在向别人说明,调用这个函数之后,成员变量不会发生任何变化。
构造函数 没有返回值类型,与类同名的函数被认为是构造函数; 它的作用就是——构造一个对象。 如何构造? constructor 没有返回值类型,与类同名的函数被认为是构造函数; 它的作用就是——构造一个对象。 如何构造? person() : m_name(“Cindy”), m_age(0) { … } person(const person & t) : m_name(t.m_name), m_age(t.m_age), m_email(t.m_email) { … } person(string name, int age) : m_name(name), m_age(age) { … } 冒号之后到括号之前,是构造函数特有的初始化列表,每一个成员变量用括号的形式初始化(只能用括号的形式),成员变量之间用逗号分隔。 这三个构造函数分别是不同类型的构造函数: 不带任何参数的叫默认构造函数,如果定义对象时,既没有使用括号的形式初始化,也没有使用=的形式初始化,那么默认构造函数将被调用; 带同类型引用或常量引用为参数的,叫拷贝构造函数,定义对象时,如果用=形式初始化,拷贝构造函数将被调用; 带其他类型参数的,是一般的构造函数,定义对象时,用括号的形式初始化,实际上就是调用对应的构造函数。 当前两种构造函数缺省时,编译器将自动合成缺省的构造函数: 对于内置类型的成员变量,缺省默认构造函数不做任何事情,缺省拷贝构造函数复制其值; 对于类类型的成员变量,缺省默认构造函数将调用该成员变量的默认构造函数,缺省拷贝构造函数调用该成员的拷贝构造函数, 若该成员变量的默认构造函数或拷贝构造函数不可用(下面将会说明为什么不可用),那么将无法通过编译。 不能显式调用构造函数——即构造函数只会在对象定义时被调用一次,今后再无它途。
构造函数 如果将某个构造函数声明为private,则这个构造函数将无法使用。一般来说,这样做的目的是阻止编译器生成缺省的构造函数。 It’s something not that new. 如果将某个构造函数声明为private,则这个构造函数将无法使用。一般来说,这样做的目的是阻止编译器生成缺省的构造函数。 只带有一个参数的构造函数表明了一种可能的隐式类型转换: string(const char * s); new与malloc的区别在于:前者创建对象,后者分配空间。 编译器合成的缺省构造函数有时在编程上带来很多麻烦。 这个构造函数提供了一个C风格字符串到string的隐式类型转换。 分配空间后,该空间的内部结构仍然是不明确的,只有在上面调用过构造函数,一个对象才被真正的创建出来。new运算符将在分配出来的空间上调用构造函数,malloc却无此功能,因此,C++中一般不再使用malloc。
析构函数 没有返回值,名字是~<class name>,没有参数的函数是析构函数。构造函数可以有多个,析构函数只能有一个。 destructor 没有返回值,名字是~<class name>,没有参数的函数是析构函数。构造函数可以有多个,析构函数只能有一个。 它的作用是销毁一个对象。 如果没有声明析构函数,编译器将合成默认析构函数: 对于内置类型,释放其空间; 对于类类型,调用其析构函数。 实际上,上面两步是编译器附加在任何析构函数最后的两步。因为没有办法显式“释放空间”和调用析构函数。 析构函数只能在delete时和离开该对象的生存域时被自动调用。 也存在将析构函数声明为private的情况。 tilde 这也是delete和free的区别,delete将调用析构函数,销毁对象;free只是释放空间。注意,不能用free释放new出来的对象占据的空间。 在析构函数被声明为private的情况下,只能通过友元和静态成员函数来控制对象的生成销毁。所以,将析构函数声明为private诣在阻止用户控制对象的生存周期,封堵在栈上创建对象的途径。其标准用途是对象池。(这段话有人听懂了吗?听懂的举手?)没听懂是正常的,听懂也没什么用,不过我不会阻拦硬要听懂的同学。
静态成员 Steady, steady… static关键字也可以修饰类的成员: class person { … static int population; static int get_population(); }; 被修饰的成员叫做类的静态成员,是这个类的属性,不是某个对象的属性。 访问用:: int person::population = 0; person::get_population(); C里面的static修饰符表示变量被创建在静态存储区,生存周期为整个程序运行时。 静态存储区是什么?(C/C++的三大存储区——静态存储区、栈、堆) 不能用成员运算符.访问。不在构造函数中初始化,不在析构函数中被撤销。 在静态成员函数中,不存在this指针,不能访问类的非静态成员。这里再次说明了this指针的存在是成员函数可以访问成员的原因。
运算符重载 C++不仅提供了对函数的重载,也提供了对运算符的重载。运算符可以视为特殊的函数。一个简单的运算符重载如下: It’s fully operational. C++不仅提供了对函数的重载,也提供了对运算符的重载。运算符可以视为特殊的函数。一个简单的运算符重载如下: 单目运算符: T1 operator [] (T2 a); int & operator [] (int i); 双目运算符: T1 operator < (T2 a, T3 b); bool operator < (person & a, person & b); 特别的运算符重载:++、--。 虽然可以视为函数,但只能重载已经存在的运算符,不能自定义新的运算符。 不能重载内置类型之间的运算符。 自增、自减运算符特殊,是因为他们在表达式之前与表达式之后表示的意义并不完全相同。他们的重载方法请诸位自行查找相关资料,并不复杂,只是需要区分。 这里讲的运算符重载是非常简单的,最简单的情况。
运算符重载 还有一类特殊的运算符也可以被重载: opetator T() operator int(); operator xxx(); 这样的运算符必须是某个类的成员函数,它为这个类提供向特定类型的隐式类型转换。比如: class person { … string operator string(); } 更多的很多情况下,运算符重载是一个复杂的工程。在你真正掌握重载之前,请慎用。 更多的运算符重载,请查找专业教材。
继承与派生 class tallent : public person { … int IQ; }; Kim Jong-il chooses his third son as successor. class tallent : public person { … int IQ; }; 上面定义了person类的一个派生类tallent类,它将获得person类的一切成员,还另外附加了一个IQ成员。 上面的“一切成员”,不包括基类的构造函数、析构函数、new运算符和=运算符。但派生类中可以访问他们。 派生类对象可以隐式转换为基类类型; 派生类类型的指针可以隐式转换为基类类型的指针; person * WindyWinter = new tallent; person类被称为基类。 与之相对的是,派生类与基类是不同的类,派生类获得基类的私有成员,但不能访问基类的私有成员。 public关键字表示继承的访问关系控制,类似还有private继承和protected继承。 在private继承中,基类的public成员和protected成员成为派生类的private成员; 在protected继承中,基类的public成员和protected成员成为派生类的protected成员; 在public继承中,基类的成员的访问权限不变。 说指针可以隐式转换是不太正确的,这里表现起来的确像是类型转换,但事实上WindyWinter指针仍然保持了tallent的特性。
虚函数与多态 在声明某个成员函数时加上virtual修饰符,表示允许派生类重载该函数;在声明析构函数时加上virtual修饰符,产生特殊效果。 class person { … virtual string get_namecard(); }; WindyWinter->get_namecard(); virtual string get_namecard() = 0; 虚析构函数与销毁派生类中的指针的机制有关。 WindyWinter虽然是person类型指针,但调用的get_namecard()函数却是tallent类的重载版本。这一特性叫做多态。 虚函数的“虚”是翻译的结果,他们是真实存在的函数,要想让它不存在,变成真正的“虚”函数,要在声明后面加“=0”,这样的虚函数叫纯虚函数,他只有一个声明,没有定义,不能被调用。带有纯虚函数的类不能用来定义对象,派生类如果不重载这个纯虚函数,一样不能定义对象。
多继承 一个类可以继承于多个类,派生类获得所有基类的成员。 Well, one is just not enough. 一个类可以继承于多个类,派生类获得所有基类的成员。 class tallent : public person, public another_person { … int IQ; }; 基类之间用逗号分隔。 如果两个基类有同名成员,将在派生类中产生二义性,需要用::做访问控制。
友元 A friend in need is a friend indeed. class tallent { … friend smart_girl; friend bool pay(); friend bool dreamland::login(); friend tallent operator +(…); }; 友元需要声明在类的public段之下。 友元是一个声明。友元不是类的成员。 有时我们需要在类外部访问类的private成员,这时需要声明一个类的友元。友元可以是另外一个类,可以是函数,可以是某个类的成员函数,也可以是运算符。被声明为友元的东西可以访问这个类的private成员。 这意味着:1. 友元的声明要按声明的完整格式写,还要附加friend修饰符; 2. 友元不在声明友元的地方定义,在另外的地方定义。
Tags Cloud 类 域运算符 构造函数 析构函数 this指针 静态成员 静态成员函数 友元 Keywords 类 域运算符 构造函数 析构函数 this指针 静态成员 静态成员函数 友元 继承 派生 多继承 虚函数 纯虚函数 多态 运算符重载 类型转换 OOP 虚继承 我们休息一下,这是第二段的tag。今天讲的内容是非常简略的,建议大家以此为关键词,搜索相关资料。最后的两个关键词今天没有讲到。OOP的概念非常重要,不过这是一个非常理论化的东西,平时写程序未必需要理解;虚继承是较为特殊的一种继承。
作业 在这里结束 留一点作业,然后大家就解放了。
作业 Soli Deo gloria. USTC OnlineJudge 1000 1001 1003 1004 Homework 以上题目是锻炼编程语言熟练度的题目。不要求全部做完,也不检查作业。 今天就到这里吧,感谢大家捧场。Soli Deo gloria. 最后我给大家演示一下如何在USTC OnlineJudge上做题。如果对今天的内容还有疑议,请立刻提出。 Soli Deo gloria.