《C++游戏服务器开发入门到掌握》深入学习C++

准备

  • 三大编译器: vs、gcc(gcc.gnu.org)、clang(www.llvm.org)
  • 安装 gcc: sudo apt-get install g++然后按两下talbe看看有哪些版本,选择最新的安装。
  • 增加 update 更新库: sudo add-apt-repository ppa:ubuntu-toolchain-r/test。g++ 安装失败有可能需要这样添加。
  • 查看 gcc 版本: gcc --version。注意:如果自己选择安装了与默认不同的版本,则需要输入gcc-8 --version查看。
  • 安装 make:sudo apt-get install make。相比于windows环境下 VS 的工程创建不一样,linux 下必须要自己 make。规则本身简单,但是要写得好的话比较繁锁。(.h 文件修改要保持 make 更新的话,就需要在 makefile 文件中添加大量的 .h)
  • 编写 makefile:hello:hello.o g++ -o hello hello.o hello.o:main.cpp g++ -c -o hello.o main.cpp四行拼起来。
  • **安装 cmake:**手写 makefile 太复杂,用一些更高级的包装软件。

关键字

  • 代替键盘上的字符: andand_epbitandbitorcomplnotnot_eqoror_eqxorxor_eq
  • constexpr: 编译期间能确定函数的返回值,直接用该值优化。
  • const_cast: const int j = 3; int *p = const_cast(&j); *p = 4;有坑不要往里跳,代码中出现这种情况,说明设计有一定的问题。
  • decltype: (declared type)decltype(a->x) y; // 类型和a->x一样。decltype((a->x)) z = y; // y的引用。表面看起来还没有auto好用。实际上是和auto一起用于模板编程中template auto add(T a, U b) -> decltype(a+b) { return a+ b; }后置返回类型。
  • dynamic_cast: 去除动态转换。指针转换失败可以用是否为空来判断,但引用转换失败则会抛出异常。在C类cast、static_cast、const_cast中效率最低,但正确性最高(在运行期会做检查)。
  • enum: 不受命名空间限制,容易冲突。所以以前的代码习惯性会在变量名中加自己的前缀。而且它的大小是根据实现决定的。
  • enum class: 增加了命名空间限制,使用起来就像类一样。还可以自定义取值范围。enum class NewColor : char { Red, Green, Blue };之前说的接口参数不建议用bool类型,此时就可以用enum class替代。
  • explicit: 增强明确性。个人见解,不要怕麻烦,能用则用。
  • export: 比较尴尬。只有一款不流行的编译器实现了这个功能。Java编译器开发者,两三个人就能完成。两个顶尖计算机科学家花了两年也没有实现。C++11明确指出export基本不用了。
  • friend: 很少用到。
  • goto: 功能过于强大,基本不用。
  • namespace: 未命名的namespace只能在本文件中使用,类似于static全局变量。
  • noexcept(C++11起): 承诺函数不抛出异常,便于编译器优化。后面还可带括号void f(int) noexcept(false) {}。和以前的void f(int) throw() {}一样。不太建议使用,需要像标准库一样严谨。
  • nullptr(C++11起): 是一种类型,用于模板编程中固定为指针类型,如果是传入0或者NULL,模板是识别不出来指针类型的,nullptr就省略了(int*)0强转这一步。
  • operator: 便于库的编写者用处比较大一些,对于普通C++程序员最好不用,带来的坏处比带来的好处还要多一些。
  • register: 定义的变量放到寄存器里(建议编译器),提高速度。现在基本不用。
  • reinterpret_cast: 几种cast总结:相对于C风格cast意图和分工更明确。除dynamic_cast以外,其他的都可以用C风格代替。建议使用C++版本的。
  • requires(概念TS): C++17可能会用到。
  • **static_assert:**编译时期确定。assert 是在运行时期确定的。

三个基本法则

  • 资源管理: Java不需要手动释放只是说内存,除此之外还有文件句柄,网络连接。
  • 三法则: 如果需要自己管理资源。析构函数、复制构造函数、赋值运算符。如果确实不需要复制对象,(C98中将复制构造函数、赋值运算符声明为private,但不实现它,链接时检查)(C++11中可以在private声明中 = delete,编译时检查)(继承boost::noncopyable可以省去写代码)
  • C++诟病: 太多的复制。vector v = makeVector(); 创建了临时变量,有没有办法将它利用起来。(C++采取了右值引用)
  • 左值和右值: 名字、类型、值。能取地址的通常是左值。右值无法取地址。
  • 右值引用: 以前引用必须是可取地址的,int& la = 1;是错误的,但引入右值引用后int&& ra = 1;可以。
  • 新法则: 利用右值引用可以重载复制构造函数和赋值运算符。
  • std::move(b): 将括号中的元素变换为右值。目的是把部分资源从右边直接移动到左边来(C++11之前没有区别)。因为对于右值来说,通常是临时变量,移交所有控制权。移交后的对象是完整的,但资源被抽干了,可以使用,但是不建议。
  • 右值转换: 右值可以转换为const引用。print(const int& a); print(1);
  • delete规则: delete空指针没有问题,但是delete两次非空指针是会出问题的。

40、虚函数遇到构造析构就退化了

  • 面向对象: 1、数据的封装;2、类的继承;3、函数的多态。

41、重新审视 auto

  • 类型推导: 自动推导 = 号右边的数据类型,包括值和指针。
  • range for: 自动遍历。优点是书写简单,运行效率高。缺点是必须全部遍历,没办法选取一半。

42、左值引用和右值引用(不考虑模板)

  • 花括号构造: 更方便。但不允许有编译警告的值转换。比如 double 转 int 。
  • 类成员初始化: 可以直接在定义的地方赋值。书写简单,不容易漏掉。如果不需要特殊的初始化,也要习惯地用默认值初始化,现在就可以用int m_value {};即可。
  • 构造转发: 没有用到,所以没讲。
  • 右值引用: 值可以改变。int&& a = 10; a = 20;
  • 右值引用: 不能直接等于左值,需要借助std::move()

45、用 weak_ptr 打破循环引用

  • 初始化: 用智能指针对象初始化。
  • lock(): auto p = weakObj.lock(); 如果有其它引用技术,则增加一份引用技术,返回智能指针对象,否则返回空。
  • 重置: 当引用的对象调用 reset() 之后,就过期了。
  • expired(): 是否过期(不会生成引用技术)。

48、使用智能指针需要注意的几个“坑”

ObjectPtr obj3(new Object(2));	// 会调用两次new()
ObjectPtr obj4 = obj3;	// 拷贝操作非原子操作,比较复杂,尤其是在多线程编程中
ObjectPtr obj5 = std::make_shared<Object>(3);	// 保证只会调用一次new()
  • 优先考虑: 如果有可能,优先使用类的实例,其次使用std::unique_ptr,万不得已使用std::shared_ptr

49、lambda 函数

// 作为本地变量
auto local = [](int a, int b) {
	std::cout << "a " << a << ", b" << b << std::endl;
}
local(1, 2);
// 作为参数
template<typename Func>
void printUseFunc(Func func, int a, int b) {
	func(a, b);
}
printUseFunc([](int a, int b) {
	std::cout << "a " << a << ", b" << b << std::endl;
	},
	1, 2);
// 也可将外部参数传递进去
int a = 1;
int b = 2;
// [&a, &b]引用 or [=]全部采用 or [&]全部引用
auto local = [a, b]() {
	std::cout << "a " << a << ", b" << b << std::endl;
}
local();
// 这一切与Lua中的function功能相似
  • 优点: 本质是inline函数,效率高于普通函数调用。算法中作为算子;网络编程中使用。

STL之容器

50、概述

  • 内容: 算法、容器、迭代器。
  • 序列式容器: array/vector/deque/list/forward_list,实现方式数组(遍历最快)或者指针。
  • 关联式容器: set/map/multiset/multimap,实现方式二叉树(平衡二叉树(查找最差lgn)、红黑树)。
  • 无顺序容器: unordered_map/unordered_set/unordered_multimap/unordered_multiset,hash table(哈希表(查找遍历比map/set都快))。
  • 总结: 清楚实现方式可以在使用时更好的评估代价。
  • 比较重要的三种数据结构: stack栈,queue队列,priority_queue优先级队列。对以上的容器做封装、简化。
  • 还有一些容器: string字符串其实是对字符封装的容器。bitset对于表示对错效率更高。
  • 新鲜血液: regex正则表达式,匹配文本。rand/thread/async/future/time,写服务器要利用多核变得高效,就要经常用到这些东西。

51、容器保存的是什么

  • 容器内元素的条件: 1、必须可以复制(copy)或者搬移(move);2、元素必须可以被赋值操作开复制或者搬移。=赋值操作符;3、元素可以被销毁。
  • 不同的容器特殊的要求: 1、序列容器要求元素必须有默认构造函数;2、对于某些操作,元素需要定义==,比如std::find;3、关联式容器需要定义<;4、无顺序容器要提供一个hash函数。
  • stl 容器里面存的是元素的值而不是引用: 对于我们自己定义的类,里面到底存的什么东西呢?
  • stl 设计原则: 效率优先、安全为次,并且基本没有处理异常(只有两个函数)。(错误检查是需要消耗性能的。)

52、容器的通用接口

  • 通过对接口进行编程: 1、类的派生和继承;2、通过模板(STL主要就是采取这种方式)。
  • size(): forward_list不支持。
  • clear(): std::array不支持。
  • <: unordered系列不支持。
  • max_size(): 很少用到。
  • e.swap(g)/swap(e,g): 交换容器内容。std::array比较特殊,耗时是线性增加的,其他的都很快O1。
  • cbegin(): 返回const &迭代器。(begin()返回二者皆可能)
  • 总结: forward_list很少用到,std::array几乎可以用vector代替。

53、std::array

  • 特点: 可以和C的接口做对接。
  • 获取元素: arr[i];越界直接崩溃;arr.at(i);越界抛出异常;std::get(arr);编译前检查是否越界(tuple的用法)。
  • 其他函数: arr.front();arr.back();
  • tuple: C++11一个非常强大的数据结构,比以前常用的如pair,vector等都要强大很多。

54、std::verctor

  • 特点: 可以和C的接口做对接。
  • front()、back(): 调用前需要检查empty(),因为为空时是未定义的。array不会有这样的问题。
  • pop_back(): maybe_wrong,要判断是否为空。并且多线程下要注意。
  • clear(): 清理所有元素,但不会降低内存。
  • shink_to_fit(): C++11才有的。建议capacity()根据size()降低使用内存。
  • emplace(): 和copy/move操作相关。
  • 特殊: 绝对不要存bool值std::vector,感兴趣可以自己查文档。

55、std::deque

  • 特点: 随机访问元素,末端和头部添加删除元素效率高,中间低。元素的访问和迭代比vector要慢(内存是分开一块一块的),迭代器不是普通的指针(可理解为智能指针)。不能和C的接口做对接。
  • 不同点: 没有capacity()reserve(100),但是有shrink_to_fit(),其他都与vector相同。
// 存储世界聊天内容
using Buffer = std::vector<char>;
using Group = std::deque<Buffer>;

56、std::list

  • 特点: 不支持随机访问,访问头部尾部元素速度快。对于异常支持好。
  • 不同点: 和vector相比,没有capacity()reserve(100)shrink_to_fit()
  • 不能随机访问: 不支持list[0]list.at(0)。deque的存储方式是一块一块的,list的存储方式是一个一个的。
// 找到中间元素
auto iterBegin = a.begin();
// 初学者方法
for (int i = 0; i < 4; ++i)
	++iterBegin;
// 高级一点的方法
std::advance(iterBegin, 4);
auto iter5 = std::next(iterBegin, 4);
// 但最好不要出现这样的情况,如果频繁访问中间元素,而不是插入删除,可能用别的container会好一些。
// 算法
b.remove(1.0f); // 值等于1.0f的全部删除
b.remove_if([](auto v) { return v > 100.0f; }); // 传入比较条件,满足的全部删除。
b.reverse(); // 反转列表
b.sort(); // 默认以"<"排序。// stl的std::sort(a.begin(), a.end());对于list编译出错,所以list自带sort()方法。
g.sort();
b.merge(g); // 合并两个排好序的列表。
c.unique(); // 去重排好序的列表。没排好序的也能去重,只不过结果不对。1 1 2 2 1 1 3 4 -> 1 2 1 3 4
c.splice(c.begin(), b); // 在c的某个位置插入整个b。

总结: 在使用list和算法的时候,优先考虑list自身的算法,更高效。

57、std::forward_list

  • 特点: C++11引入。不支持随机访问,访问头部元素速度快。对于异常支持好。有些接口看起来和list一样,但却有着细微的差别。
  • 没有size(): forward_list和自己手写的c-style singly linked list相比无异,没有任何时间和空间上的额外开销,任何性质如果和这个目标抵触,我们放弃该特征。
  • before_begin(): 返回头指针的前一位置。只能用于自身的一些算法,不能用于std通用算法。
  • erase_after(): 删除传入迭代器之后的元素,返回void(list返回下一指向的迭代器)。
  • insert_after(): 在传入的迭代器之后插入元素,返回指向插入元素的迭代器。
  • splice_after(): c.splice_after(c.before_begin(), b);

58、智能指针的一个陷阱

// 单次消耗时间对比(纳秒)
int		1.00
int*		1.23
SharedPtr	1.49
weakPtr	14.64

第三方程序: celero::Run(argc, argv);
总结: 频繁使用、循环遍历(玩家或怪物列表),不建议使用weak_ptr。

59、std::set

  • 特点: 耗时O(logn),适合频繁插入删除查找元素,并且按照顺序排列的场景。和list一样,有自己的保存方式,需要消耗额外内存。(父节点+左节点+右节点+前置节点+后置节点)五个指针 * 8 = 40Byte。
  • pair: 其实就是个含有first、second的数据结构。
  • 成员算法: a.count(1.0f);a.find(1.0f);a.lower_bound(3); // 返回第一个大于等于该值的迭代器位置a.upper_bound(3); // 返回第一个大于该值的迭代器位置a.equal_range(3); // 返回lower和upper的pair
  • insert(): 对于set返回pair,multiset肯定是成功的。

60、std::set(第二部分)

  • 用起来不方便: 1、迭代器获取到的值是const,不能改变值(set根据值排序,想想如果值改变了,顺序是不是就乱了);2、查找也是通过值来查找,值满足条件即可,不能保证除此以外的符合条件(Person类有age和name,创建时要传入第二个参数决定排序的依据,所以查找时也是依据这个)。
  • 坑: 即使重载了==运算符,也只有std::find(线性耗时)能找到,set本身的find(logn耗时)还是通过构造传入的比较类(第二个参数)进行查找。
  • 应用场景: 一般用来存指针或者智能指针,或者基本类型的值。用的非常少,等待网络信息的socket(指针)存入set,主逻辑用的非常少。

61、std::map

  • 特点: 使用频率仅次于vector,有的场景甚至排第一。内部是pair这点和set一样,只不过封装的接口不一样。
  • 注意: find()返回的iterator实际为std::pair&,如果将此赋值给std::pair的对象或者引用对象又或者const引用对象,都会额外生成一个临时变量,很废。
  • insert(): 对于map返回pair,multimap肯定是成功的。
  • 插入方式变化: 以前的insert()方式有限,推荐用C++11新特性emplace(),方法更多效率更高,键入字母也少。(讲解类型推导有点深奥)。
  • 第三种插入方式[]: 因为没有的会创建,所以要求value的类型有默认构造函数。表达式等同于返回value的引用(没有就新建)。
  • 第四种插入方式at(): C++11引入的,有则返回引用,没有就抛出异常。
  • 总结: 实际生产中建议使用find()。虽然有点繁琐,但可以包装一下。
auto findIter = b.at(10);
if (findIter != std::end(b) /* b.end() */) {
	auto& v = (*findIter).second;
} else {

}
// 包装
template <class Map>
typename Map::mapped_type get_default(
	const Map &map, const typename Map::key_type &key,
	const typename Map::mapped_type &dflt = typename Map::mapped_type()
) {
	auto pos = map.find(key);
	return (pos != map.end() ? pos->second : dflt);
}
auto info = get_default(b, 10);
if (info.empty()) {

} else {

}

62、std::unordered set/map

  • 优点: C++11引入的查找速度比set/map的O(logn)还要快。
  • 缺点: 1、没有排序(对于明确要求排序的场景不适用);2、平摊下来的速度是常量的,但也有极端情况(未讲);3、美国测试:一千万以下unordered优于set,一千万以上就反过来了(数量多hash算法会重复,然后会重新排列结构)(不过对于我们日常开发,不会到达千万级)。
  • 模板参数: 可传5个参数,好在后面3个有默认值。在某本书上说过,你写的模板类每多一个模板参数,就会让潜在的用户减少一半。第4个参数主要用于处理不同key但hash值一样的情况。
  • 模板类特化: 对于已经存在的模板类,指定其中一个模板参数的实现方式,成为模板类特化,实际用的很少。这里举例实现hash用到。
  • hash评测: 一个好的hash算法,就是冲突尽可能地少,例子中只是简单的实现,比较挫。第二个例子会好很多,但是你无法证明冲突是最少的。第三个例子是经过数学证明的一种非常好的方法(来自boost库,具体原理讲师也不知道)。

参考自《C++游戏服务器开发入门到掌握》教学视频。

如有侵权,请联系本人删除。

你可能感兴趣的:(C++游戏服务器开发入门到掌握)