简介
为什么每一名程序员都应该学习 C++?学习多种语言会使你在软件开发生涯中更加自信和娴熟。例如,如果你掌握了 Go 语言,你就会了解几个令人印象深刻的语言设计概念,提高自己的通用编程技能。C++ 是一种高级语言,它提供了比 C 语言更高级、对人更友好的抽象。但是,C++ 鼓励人们像 C 语言一样使用指针和手动管理内存。另外,C++ 标准库的设计注重计算机科学概念、性能和灵活性,而不是仅仅注重开发的便利性。因此,当你学习 C++ 时,会无意中学习到计算机科学的基础知识。每位开发人员都应该学习的 5 种编程语言。
像 Golang 这样符合现代潮流的编程语言,通过提供类似 C 语言的最小语法和自动内存管理(通过垃圾收集)与 C++ 竞争。但是,Go 会生成很大的二进制文件,所以它不适合于轻量级的场景。这就是为什么大多数程序员使用 Go 来构建高性能的云工具,因为对云环境来说,二进制文件的大小并不是问题。在高性能、轻量级的软件开发方面,程序员仍然喜欢用 C++ 而不是 Go。Go 通常是静态链接标准库实现,当我们导入 fmt 时,它会将一个 1.2MB 的 Go 最小二进制文件增加到 1.8MB。C++ 通常是动态链接,当我们包含 iostream 时,它会将一个 16.5KB 的最小二进制文件增加到 17.3KB。在 GNU/Linux 平台上,Go 二进制文件大小增加了 50%,而 C++ 二进制文件大小增加了不到 5%。
和 C 相比,C++ 向上做了更多的抽象和扩展。C++ 提供了 STL、类、模板、智能指针等一系列新特性,使得基于 C++ 进行的大型软件开发变得更加高效,而且进一步避免了使用原始 C 时容易出现的内存泄露等问题。
标准库
标准库三大要件:容器、算法、迭代器
auto n = std::count_if( // count_if算法计算元素的数量
begin(v), end(v), // begin()、end()获取容器的范围
[](auto x) { // 定义一个lambda表达式
return x > 2; // 判断条件
}
); // 大函数里面套了三个小函数
因为标准算法的名字实在是太普通、太常见了,所以建议你一定要显式写出“std::”名字空间限定,这样看起来更加醒目,也避免了无意的名字冲突。
多线程
在 C++ 语言里,线程就是一个能够独立运行的函数。
auto f = []() // 定义一个lambda表达式
{
cout << "tid=" <<
this_thread::get_id() << endl;
};
thread t(f); // 启动一个线程,运行函数f
多线程说它不难,是因为线程本身的概念是很简单的,只要规划好要做的工作,不与外部有过多的竞争读写,很容易就能避开“坑”,充分利用多线程,“跑满”CPU。说它难,则是因为现实的业务往往非常复杂,很难做到完美的解耦。一旦线程之间有共享数据的需求,麻烦就接踵而至,因为要考虑各种情况、用各种手段去同步数据。
thread_local
thread_local int n = 0; // 线程局部存储变量
auto f = [&](int x) // 在线程里运行的lambda表达式,捕获引用
{
n += x; // 使用线程局部变量,互不影响
cout << n; // 输出,验证结果
};
thread t1(f, 10); // 启动两个线程,运行函数f
thread t2(f, 20);
原子变量
using atomic_bool = std::atomic<bool>; // 原子化的bool
using atomic_int = std::atomic<int>; // 原子化的int
using atomic_long = std::atomic<long>; // 原子化的long
atomic_int x {0}; // 初始化,不能用=
atomic_long y {1000L}; // 初始化,只能用圆括号或者花括号
assert(++x == 1); // 自增运算
y += 200; // 加法运算
assert(y < 2000); // 比较运算
异步
auto task = [](auto x) // 在线程里运行的lambda表达式
{
this_thread::sleep_for( x * 1ms); // 线程睡眠
cout << "sleep for " << x << endl;
return x;
};
auto f = std::async(task, 10); // 启动一个异步任务
f.wait(); // 等待任务完成
assert(f.valid()); // 确实已经完成了任务
cout << f.get() << endl; // 获取任务的执行结果
async() 会返回一个 future 变量,可以认为是代表了执行结果的“期货”,如果任务有返回值,就可以用成员函数 get() 获取。不过要特别注意,get() 只能调一次,再次获取结果会发生错误,抛出异常 std::future_error。另外,这里还有一个很隐蔽的“坑”,如果你不显式获取 async() 的返回值(即 future 对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是“async”就变成了“sync”。所以,即使我们不关心返回值,也总要用 auto 来配合 async(),避免同步阻塞,就像下面的示例代码那样:
std::async(task, ...); // 没有显式获取future,被同步阻塞
auto f = std::async(task, ...); // 只有上一个任务完成后才能被执行
内存管理
RAII 是一种管理资源的惯用方法,在对象的构造函数中获取资源,在析构函数中释放资源。
当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数。由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。
C++从遗忘到入门 未读
面向对象
class Person {
public:
void doWork(); // 方法,类对外提供的一系列操作实例的函数
private:
std::string name; // 成员变量,封装到类中的属性,保存内部状态信息
int age;
};
C++ 里类的四大函数你一定知道吧,它们是构造函数、析构函数、拷贝构造函数、拷贝赋值函数。C++11 因为引入了右值(Rvalue)和转移(Move),又多出了两大函数:转移构造函数和转移赋值函数。所以,在现代 C++ 里,一个类总是会有六大基本函数:三个构造、两个赋值、一个析构。好在 C++ 编译器会自动为我们生成这些函数的默认实现,省去我们重复编写的时间和精力。但我建议,对于比较重要的构造函数和析构函数,应该用“= default”的形式,明确地告诉编译器(和代码阅读者):“应该实现这个函数,但我不想自己写。”这样编译器就得到了明确的指示,可以做更好的优化。
class DemoClass final
{
public:
DemoClass() = default; // 明确告诉编译器,使用默认实现
~DemoClass() = default; // 明确告诉编译器,使用默认实现
};
class DemoClass final
{
public:
DemoClass(const DemoClass&) = delete; // 禁止拷贝构造
DemoClass& operator=(const DemoClass&) = delete; // 禁止拷贝赋值
};
// 因为 C++ 有隐式构造和隐式转型的规则,如果你的类里有单参数的构造函数,或者是转型操作符函数,为了防止意外的类型转换,保证安全,就要使用“explicit”将这些函数标记为“显式”。
class DemoClass final
{
public:
explicit DemoClass(const string_type& str) // 显式单参构造函数
{ ... }
explicit operator bool() // 显式转型为bool
{ ... }
};