Download presentation
Presentation is loading. Please wait.
1
WindyWinter windy@ream.at
C++ WindyWinter
2
C++标签
3
命名空间 对于一个大型的C语言软件项目,给函数和全局变量起名不是一个容易的事情,因为必须考虑有没有可能与其它程序员写的代码冲突,多数的做法是对每个模块的一组函数名加个特定前缀。比如著名的PJSIP库,它选择了在每个函数和类型的前面加上前缀“pj_”,使得标识符都比较长,而且看起来有点儿冗余。
4
命名空间 所以C++引入了namespace的概念,把一些标识符以命名空间树结构的方式组织起来,使代码看起来更优雅。
namespace parser { inline double get_double(char **str_ptr); inline int get_int(char **str_ptr); } namespace sight { inline AngleDeg ViewAngle(ViewWidth view_width); }
5
命名空间 方式一,只在必要的时候通过域运算符::引用指定命名空间内的标识符
parser::get_double(……); std::cout<<…… 方式二,把parser命名空间中的全部标识符都引入到当前编译单元中 using namespace parser; get_double(……); 方式一适用于当前编译单元引用parser内的标识符不多,而且编译单元内使用这些标识符的次数也不多的情况。 使用了using namespace之后,parser所有的标识符对于当前编译单元都是可见的。 常见的using namespace std的写法就是将std命名空间中的全部标识符引入当前编译单元,std命名空间里面全是标准库函数。
6
命名空间 当声明命名空间时的名称为空时,则该命名空间为匿名命名空间(unnamed namespace)。匿名的空间是C++用于定义作用域为本编译单元的函数或变量的一种新的方式。由于匿名命名空间没有命名空间的名字,所以也无法在其它的编译单元内通过extern声明该变量或函数,于是该变量自然也只在本编译单元内可见。 编译器实现的时候,实际上是为每个匿名命名空间生成了一个唯一的内部名字。
7
命名空间 namespace { bool ret = BehaviorExecutable::AutoRegister<BehaviorDribbleExecuter>(); } 每一个BehaviorXXX.cpp中都有一个定义在匿名命名空间里的ret变量,该变量仅仅是为了实现BehaviorXXXExecuter的注册关系,在文件之外无任何意义。
8
string char * -> string #include <string> string s;
C++标准库提供了新的字符串实现方式string类,C的char*形式的字符串在C++中被称作“C风格字符串”。string与C风格字符串相比,不必担心内存是否足够、字符串长度等等,而且作为一个类出现,他集成的操作函数足以完成我们大多数情况下(甚至是100%)的需要。我们可以用 = 进行赋值操作,== 进行比较,+ 做连接。我们基本可以把它看成是C++的基本数据类型。 为了在我们的程序中使用string类型,我们必须包含头文件 <string>。 这样我们就声明了一个字符串变量s。上面的声明没有传入参数,所以就直接使用了string的默认的构造函数,这个函数所作的就是把s初始化为一个空字符串。
9
string string s; //生成一个空字符串s string s(str) //拷贝构造函数 生成str的复制品
string s(str, idx) //将字符串str内“始于位置idx”的部分当作字符串的初值 string s(str, idx, len) //将字符串str内“始于idx且长度顶多len”的部分作为字符串的初值 string s(“asdf”) //将C风格字符串作为s的初值 string s(c_str, len) //将C风格字符串c_str的前len个字符作为字符串s的初值。 string s(n, c) //生成一个字符串,包含n个c字符 string s(begin, end) //以区间[begin, end)内的字符作为字符串s的初值 string还有很多构造函数。 最后一个构造函数是为了与C++标准库中其它的一些东西保持统一,我们后面再说“区间”的问题。
10
string C++提供的由string得到对应的C风格字符串的方法有三种:
s.data(); //以字符数组的形式返回字符串内容,末尾没有\0。 s.c_str(); //返回一个以\0结尾的字符数组,也就是最标准的C风格字符串。 s.copy(c_str, n, pos); //把字符串的内容复制或写入既有的字符数组内。 data和c_str返回的均为常量,不能用这两个函数修改string的值,string的修改方法在后面。
11
string 访问string包含的字符——下标操作符[]
string s(“asdf”); s[5] //返回\0 s.assign(str, 1, 5); //如果str是”WrightEagle”就是把”right”赋给字符串s s += str; //追加个字符串 s += ”WrightEagle”; //追加个C风格字符串 s += ’a’; //追加个字符 虽然string不像C风格字符串一样存储\0,但用[]访问最后一个字符的下一个位置时,返回\0。
12
string 字符串长度 取子串 insert、erase、replace compare、find、rfind
s.length() 取子串 s.substr(6); //从索引6开始到结尾的子串 s.substr(1, 5); //从索引1开始5个字符 insert、erase、replace compare、find、rfind <、>、<=、>=
13
流输入输出 iostream scanf/printf fstream fscanf/fprintf
cin>>XXX; cout<<XXX; fstream fscanf/fprintf ifstream fin(“input.txt”); ofstream fout(“output.txt”); fin>>XXX; fout<<XXX; stringstream sscanf/sprintf stringstream cmd_str; cmd_str << “(dash ” << mCommandInfo.mPower << “ ” << mCommandInfo.mAngle << “)”; iostream 的设计初衷包括克服 C stdio 的缺点,提供一个高效的可扩展的类型安全的 IO 机制。但是现在iostream过于复杂了,导致效率非常差,所以大型的工程中都避免使用。 常用的反而是stringstream,但是大家用它也只是贪图方便的将数字转换成字符串。 WrightEagleBASE很少直接使用屏幕和文件输入输出,尽量用log输出——Logger::instance().GetTextLogger(“xxx”)<<……
14
模板与泛型编程
15
模板 实践中需要各种各样的min函数 如果还需要其他类型的min怎么办?
int min(int a, int b) { return a>b ? b : a; } double min(double a, double b) { return a>b ? b : a; } 如果还需要其他类型的min怎么办? char short long long-long float long-double ActiveBehavior AngleDeg 想象不止有这几个内置类型,还有很多很多很多类类型,那一个工程里面光min函数就得几十上百个;如果再加上max函数…… 这些min函数的函数体都是一模一样的,唯一不同的是参数和返回值的类型,如果能够把类型当作一个“变量”,就能大幅节省编码时间了。
16
模板 类型抽象——_Tp类型 Min(2, 3); Min(3.0, 4.0); Min(act_bhv1, act_bhv2);
template<typename _Tp> inline const _Tp& Min(const _Tp& x, const _Tp& y) { return std::min(x, y); } Min(2, 3); Min(3.0, 4.0); Min(act_bhv1, act_bhv2); 函数模板。 _Tp叫做模板参数,代表某个类型。函数模板不是函数,需要经过一个“实例化”的过程变成函数——在首次调用的时候,编译器推断_Tp的实际类型,生成对应类型的函数。
17
模板 Min(1, 2.0); //推断出错 Min(player1, player2); //编译出错
如果编译器无法推断出T类型是什么(推断的时候没有隐式类型转换),那么编译出错。 如果推断出来_Tp使得代码编译不过——比如_Tp类型不能比较大小(如PlayerState),那么编译出错。
18
模板 template<typename _Tp, std::size_t _Nm, bool _Zero = false> class Array { _Tp _M_instance[_Nm ? _Nm : 1]; public: Array() { if (_Zero) { bzero(); } } _Tp & operator[](const std::size_t & i) { return _M_instance[i]; } }; 类也可以有模板,比如这个WrightEagleBASE中定义的“安全数组”类。 定义一个数组类型的用途,或者是好处是什么? ——检查数组越界,数组越界是常见的错误,如果在operator[]里面加入对下标的检查,可以有效避免。成为“安全数组”。 ——方便数组复制,数组不能直接复制,即使是两个类型一样的数组也不行,但是定义成数组类型,就可以利用编译器合成的拷贝构造函数复制数组。
19
模板 Array<int, 11> numbers;
Array<PlayerState &, 11> teammates; template<typename _Tp, std::size_t _Nm, bool _Zero = false> 类模板经过实例化变成类。 实例化的方法,与函数不同,类模板实例化必须显式写出模板参数。 模板参数不同的模板实例是不同的类,彼此之间没有任何关系,不能互相访问私有成员。 模板参数不仅可以是类型名,还可以是数字、bool类型,模板参数还可以是任何可以转化为整数的东西。
20
标准模板库 C语言有一个小巧精干的标准库; C++在引入泛型编程的思想后,标准库得到了广泛的、革命性的变革——标准模板库。
vector、list、deque、queue、stack、priority_queue、set/map纷纷进入STL; STL中的函数不再限定参数的类型,而只做出基本要求: min、max、sort要求此种类型定义过”<“; binary_search要求传递进来的序列可以随机访问; …… vector——动态增长的数组、list——链表、deque——块状链表/双端队列,统称为容器; priority_queue——堆/优先队列,set/map——平衡二叉树,统称为容器适配器。 C标准库函数要求一个数组或序列时,常采用传递void指针+单个元素的体积的形式,STL受益于模板,不必再如此麻烦。
21
vector #include <vector> std::vector<PlayerState> players;
players.size(); //players的长度 players.push_back(player1); //将一个元素插入到vector的最后面 insert()、clear()、erase() vector是长度可变的数组,内部机制是数组。vector可以扩大和缩小,满了之后扩容为两倍,空了之后缩小为一半。 定义一个元素为PlayerState类型的vector。一个刚刚定义的vector一个元素也没有,容量是100。 []保持了与数组的一致性,取不存在元素会出现问题,但不会报错。 通常用push_back为vector增加新元素。
22
list #include <list>
std::list<ActiveBehavior> mActiveBehaviorList; ActiveBehavior dribble; mActiveBehaviorList.push_back(dribble); mActiveBehaviorList.sort(std::greater<ActiveBehavior>()); mActiveBehaviorList.front(); mActiveBehaviorList.empty(); list是链表,内部实现机制为双向链表。 list类型不用[]取元素。
23
迭代器 最简单的迭代器相当于指向容器中元素的指针;
for (vector<PlayerState>::iterator i = players.begin(); i < players.end(); ++i) { i->GetPos(); (*i). GetNeckDir(); } players.end() 表示“超出末端的位置”。 i相当于players里元素的指针。 vector的迭代器可以随机访问 --i; i += 2; i -= 6; 与iterator相仿的是reverse_iterator,对应有rbegin()、rend()。 players.begin()返回一个迭代器,指向players中的第一个元素。 与指针一样,对迭代器使用解引用运算符,可以得到迭代器指向的那个元素的引用。 看下面这个循环,i初始化为players的第一个元素的位置,每次++i,表示将i后挪一个位置,players.end()表示“超出末端的位置”,于是players中每个元素的位置都在players.end()之前,当i==players.end()时,说明i已经挪到了players之外,不再指向players中某个元素的位置了。于是这个循环遍历了players中的每一个元素。 表示前移1个位置、后挪2个位置,前移6个位置,若向后挪到了players之外,则i会与playres.end()相等,向前挪到players之外,则会自动挪到第一个位置。随机访问就是指可以跳跃性的访问任意一个元素,而不需要一个一个挪。与vector相对的是,list的迭代器只能用++/--,不能随机访问。 reverse_iterator的特点是,+表示向前挪,-表示向后挪;rbegin()是最后端,rend()是“超出前端”。
24
迭代器 迭代器的失效——如果一个迭代器指向的元素已经被删除,那么该迭代器失效,访问该迭代器后果不可预料。 迭代器的分类: 随机访问迭代器
前向迭代器 双向迭代器 输入迭代器 输出迭代器 正向迭代器 反向迭代器 常量迭代器 流迭代器 五种迭代器——Primer 356页。 常量迭代器用于当容器是个常量,带有const属性的时候,正常的迭代器默认元素是左值,可以更改,常量迭代器指定元素是右值,就像指向常量的指针一样。
25
sort #include<algorithm>
vector<int> a; sort(a.begin(), a.end()); vector<person> c; sort(c.rbegin(), c.rend()); int b[100]; sort(b, b+100); list<double> d; sort(d.begin(), d.end()); //X d.sort(); //O STL除了提供各种基本数据结构之外,也提供基本算法。sort是STL中提供的最常用的排序算法,在algorithm头文件中,其内部实现是冒泡排序和快速排序。 C标准库中有一个qsort函数,那个函数要求一段数组和一个比较函数。sort在这一点上与qsort很相似,它要求一对能随机访问的迭代器,一个指向序列开头,一个指向序列的“超出末端位置”, 并不要求这两个迭代器是正向的还是反向的,另外它要求被排序的序列中的元素带有<运算符,且<运算符必须被定义为严格小于——即表达式(x<x)必须返回false/0。 sort也可以用于排序数组,此时数组的头指针就是序列的开头,最后一个元素的位置+1就是“超出末端位置”。 下面,直接用sort去排序一个链表是不行的,因为sort要求随机访问迭代器,而链表的begin和end返回的迭代器不能随机访问。但链表提供了自己的sort函数,内部实现也是快速排序。 STL还提供了很多其它的小工具和小算法。
26
补充 C++于1983年在贝尔实验室诞生,迄今为止经历了三个发展阶段:
面向对象阶段(第一天课程),1979年~1995年。C++作为C语言的扩充,完善面向对象特性。 泛型编程阶段(第二天课程),1995年~2000年。C++的模板机制最早在1991年出现,之后HP写出了第一份标准模板库的实现,并于1994年底进入C++标准(ANSI)。到1998年C++的第一个ISO标准出现时,C++在全世界程序设计语言中的占有率达到了史无前例的76%。之后由于Java和C#等新兴语言的出现,C++不断受到冲击。该阶段到模板的集大成者——boost库出现为止。 模板元编程阶段,2000年~今。在不断尝试模板编程的过程中,人们完善C++的模板机制,直到它变成了图灵完备的(一个语言是图灵完备的,意味着该语言的计算能力与一个通用图灵机相当,这也是现代计算机语言所能拥有的最高能力)。C++的模板是所有编程语言的模板中第一个图灵完备的。于是boost库的一个分支mpl出现了。模板与泛型编程进入了模板元编程的时期,人们用模板写出了求阶乘、求素数,实现了链表,用C++做函数式编程。
27
Q&A http://cplusplus.com/reference/
Similar presentations