C++:现代C++

本文介绍了现代C++的主要特性,包括智能指针、std::string、std::vector、标准库算法、auto关键字、范围for循环、constexpr、统一初始化、移动语义、lambda表达式、异常处理和std::atomic等,旨在提升代码的安全性、简洁性和效率。推荐使用现代C++特性以降低C样式的编程风险和复杂性。

前言

自创建以来,C++ 即已成为世界上最常用的编程语言之一。 正确编写的 C++ 程序快速、高效。 相对于其他语言,该语言更加灵活:它可以在最高的抽象级别上运行,还可以在硅级低级别上运行。 C++ 提供高度优化的标准库。 它支持访问低级别硬件功能,从而最大限度地提高速度并最大程度地降低内存需求。 可以使用 C++ 创建各种应用。 游戏、设备驱动程序和高性能科学软件。 嵌入式程序。 Windows 客户端应用。 甚至用于其他编程语言的库和编译器也使用 C++ 编写。
C++ 的原始要求之一是与 C 语言向后兼容。 因此,C++ 始终允许 C 样式编程,其中包含原始指针、数组、以 null 结尾的字符串和其他功能。 它们可以实现良好的性能,但也可能会引发 bug 并增加复杂性。 C++ 的演变注重可显著降低 C 样式惯例使用需求的功能。 如果需要,你可以使用旧的 C 编程设施,但在使用现代 C++ 代码之时,对上述设施的需求应降低。 现代 C++ 代码更加简单、安全、美观,而且速度仍像以往一样快速。
下面几个部分概述了现代 C++ 的主要功能。 此处列出的功能在 C++11 及更高版本中可用,除非另有说明。 在 Microsoft C++ 编译器中,可以设置 /std 编译器选项,指定要用于项目的标准版本。

资源和智能指针

C 样式编程的一个主要 bug 类型是内存泄漏。 泄漏通常是由未能为使用 new 分配的内存调用 delete 导致的。 现代 C++ 强调“资源获取即初始化”(RAII) 原则。 其理念很简单。 资源(堆内存、文件句柄、套接字等)应由对象“拥有”。 该对象在其构造函数中创建或接收新分配的资源,并在其析构函数中将此资源删除。 RAII 原则可确保当所属对象超出范围时,所有资源都能正确返回到操作系统。
为了支持对 RAII 原则的简单采用,C++ 标准库提供了三种智能指针类型:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。 智能指针可处理对其拥有的内存的分配和删除。 下面的示例演示了一个类,其中包含一个数组成员,该成员是在调用 make_unique() 时在堆上分配的。 对 new 和 delete 的调用将由 unique_ptr 类封装。 当 widget 对象超出范围时,将调用 unique_ptr 析构函数,此函数将释放为数组分配的内存。

#include <memory>
class widget
{
private:
    std::unique_ptr<int> data;
public:
    widget(const int size) { data = std::make_unique<int>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);   // lifetime automatically tied to enclosing scope
                // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

在分配堆内存时,请尽可能地使用智能指针。 如果必须显式使用 new 和 delete 运算符,请遵循 RAII 原则。

std::string 和 std::string_view

C 样式字符串是 bug 的另一个主要来源。 通过使用 std::string 和 std::wstring,几乎可以消除与 C 样式字符串关联的所有错误。 还可以利用成员函数的优势进行搜索、追加和在前面追加等操作。 两者都对速度进行了高度优化。 将字符串传递到仅需要只读访问权限的函数时,在 C++17 中,可以使用 std::string_view,以便提高性能。

std::vector 和其他标准库容器

标准库容器都遵循 RAII 原则。 它们为安全遍历元素提供迭代器。 此外,它们对性能进行了高度优化,并且已充分测试正确性。 通过使用这些容器,可以消除自定义数据结构中可能引入的 bug 或低效问题。 使用 vector 替代原始数组,来作为 C++ 中的序列容器。

vector<string> apples;
apples.push_back("Granny Smith");

使用 map(而不是 unordered_map),作为默认关联容器。 对于退化和多案例,使用 set、multimap 和 multiset。

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

需要进行性能优化时,请考虑以下用法:

例如,当嵌入非常重要时,将 array 类型作为类成员。

使用无序的关联容器,例如 unordered_map。 它们的每个元素的开销较低,并且具有固定时间查找功能,但正确高效地使用它们的难度更高。

请勿使用 C 样式数组。 对于需要直接访问数据的旧 API,请改用 f(vec.data(), vec.size()); 等访问器方法。

标准库算法

在假设需要为程序编写自定义算法之前,请先查看 C++ 标准库算法。 标准库包含许多常见操作(如搜索、排序、筛选和随机化)的算法分类,这些分类在不断增长。 数学库的内容很广泛。 自 C++17 起,即提供了许多算法的并行版本。

以下是一些重要示例:

for_each,默认遍历算法(以及基于范围的 for 循环)。

transform,用于对容器元素进行非就地修改

find_if,默认搜索算法。

sort、lower_bound 和其他默认的排序和搜索算法。

若要编写比较运算符,请使用严格的 <,并尽可能使用命名 lambda。

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), comp );

用 auto 替代显式类型名称

C++11 引入了 auto 关键字,以便可将其用于变量、函数和模板声明中。 auto 会指示编译器推导对象的类型,这样你就无需显式键入类型。 当推导出的类型是嵌套模板时,auto 尤其有用:

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

基于范围的 for 循环

对数组和容器的 C 样式迭代容易引发索引错误,而且键入过程单调乏味。 若要消除这些错误,并提高代码的可读性,可使用基于范围的 for 循环,此循环包含标准库容器和原始数组。 有关详细信息,请参阅基于范围的 for 语句。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

用 constexpr 表达式替代宏

C 和 C++ 中的宏是指编译之前由预处理器处理的标记。 在编译文件之前,宏标记的每个实例都将替换为其定义的值或表达式。 C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。 在现代 C++ 中,应优先使用 constexpr 变量定义编译时常量:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

统一初始化

在现代 C++ 中,可以使用任何类型的括号初始化。 在初始化数组、矢量或其他容器时,这种初始化形式会非常方便。 在下面的示例中,使用三个 S 实例初始化 v2。 v3 将使用三个 S 实例进行初始化,这些实例使用括号初始化自身。 编译器基于 v3 声明的类型推断每个元素的类型。

#include <vector>
struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

移动语义

现代 C++ 提供了移动语义,此功能可以避免进行不必要的内存复制。 在此语言的早期版本中,在某些情况下无法避免复制。 移动操作会将资源的所有权从一个对象转移到下一个对象,而不必再进行复制。 一些类拥有堆内存、文件句柄等资源。 实现资源所属的类时,可以定义此类的移动构造函数和移动赋值运算符。 在解析重载期间,如果不需要进行复制,编译器会选择这些特殊成员。 如果定义了移动构造函数,则标准库容器类型会在对象中调用此函数。

Lambda 表达式

在 C 样式编程中,可以通过使用函数指针将函数传递到另一个函数。 函数指针不便于维护和理解。 它们引用的函数可能是在源代码的其他位置中定义的,而不是从调用它的位置定义的。 此外,它们不是类型安全的。 现代 C++ 提供了函数对象和重写 operator() 运算符的类,可以像调用函数一样调用它们。 创建函数对象的最简便方法是使用内联 lambda 表达式。 下面的示例演示如何使用 lambda 表达式传递函数对象,然后由 for_each 函数在矢量的每个元素中调用此函数对象:

std::vector v {1,2,3,4,5};
int x = 2;
int y = 4;
auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

可以将 lambda 表达式 [=](int i) { return i > x && i < y; } 可以读取为“采用类型 int 的单个参数并返回一个布尔值来表示此参数是否大于 x 并且小于 y 的函数”。 请注意,可在 lambda 中使用来自周围上下文的 x 和 y 变量。 [=] 会指定通过值捕获这些变量;换言之,对于这些值,lambda 表达式具有自己的值副本。

异常

与错误代码相比,现代 C++ 更注重异常,将其作为报告和处理错误条件的最佳方法。

std::atomic

对线程间通信机制使用 C++ 标准库 std::atomic 结构和相关类型。

std::variant (C++17)

C 样式编程通常通过并集使不同类型的成员可以占用同一个内存位置,从而节省内存。 但是,并集不是类型安全的,并且容易导致编程错误。 C++17 引入了更加安全可靠的 std::variant 类,来作为并集的替代项。 可以使用 std::visit 函数以类型安全的方式访问 variant 类型的成员。

该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处
本文章博客地址:https://blog.youkuaiyun.com/it_cplusplus/article/details/118089298

现代C++程序设计(原书第2版)》图文并茂,通俗易懂,真正做到寓教于乐,是一本难得的C++面向对象设计入门教材。 出版者的话 译者序 前言 第1章 C++概述与软件开发 1.1 什么是C语言和C++ 1.1.1 C和C++历史回顾 1.1.2 C/C++是一门编译语言 1.1.3 为什么许多程序员都选择C++ 1.2 什么是面向对象 1.2.1 C++程序并不一定是面向对象的 1.2.2 一个简单的面向对象程序示例 1.2.3 面向对象的软件更有优势 1.3 结构化设计与面向对象设计 1.3.1 ATM——结构化设计 1.3.2 采用面向对象方法的ATM——究竟是谁的任务 1.3.3 汽车维护——结构化设计 1.3.4 采用面向对象方法的汽车维护——究竟是谁的任务 1.4 软件开发技术概述 1.5 问题发现与解决 复习题 第2章 C++的入门知识 2.1 编程基础 2.1.1 算法设计 2.1.2 正确的软件开发步骤 2.2 专业术语及工程创建 2.3 C++程序的一般格式 2.3.1 “Hello World!”程序 2.3.2 “How’s the Weather?”程序 2.4 程序的数据及数据类型 2.4.1 C++的数据类型 2.4.2 容器=数据类型,标签=变量名 2.4.3 数据类型修饰符 2.4.4 问题分析:整型数据究竟有多大 2.5 C++中的变量声明 2.5.1 C++的命名规则 2.5.2 在哪里声明变量 2.6 C++中的运算符 2.6.1 计算路程的程序 2.6.2 从键盘输入程序所需数据 2.6.3 赋值运算符 2.6.4 运算符的优先级 2.6.5 数据类型及其存储的值 2.6.6 算术运算符 2.6.7 自增运算符和自减运算符 2.6.8 复合赋值运算符 2.7 #define、const和数据类型转换 2.7.1 #define预处理指令 2.7.2 const修饰符 2.7.3 const比#define好吗 2.7.4 数据类型转换 2.8 关于键盘输入和屏幕输出的更多内容 2.8.1 转义序列 2.8.2 ios格式标记 2.8.3 流的IO控制符 2.9 开始使用类和对象、C++string类 2.10 练习 复习题 第3章 控制语句和循环 3.1 关系运算符和逻辑运算符 3.2 if语句 3.2.1 if-else语句 3.2.2 问题分析:在if语句中使用大括号 3.2.3 if-else if-else语句 3.2.4 低效的编程方法 3.2.5 if-else程序示例 3.2.6 嵌套if-else语句 3.2.7 条件运算符“?” 3.3 switch语句 3.4 循环 3.4.1 括号的用法 3.4.2 无限循环 3.5 for循环 3.5.1 不要改变循环索引 3.5.2 for循环示例 3.6 while循环 3.7 do while循环 3.8 跳转语句 3.8.1 break语句 3.8.2 continue语句 3.9 问题发现与解决 3.9.1 五个常见错误 3.9.2 调试程序 3.10 C++类与vector类 3.11 总结 3.12 练习 复习题 第4章 函数一:基础 4.1 C++中的函数 4.1.1 只由一个main函数构成的程序 4.1.2 包含多个函数的程序 4.1.3 函数是个好东西 4.1.4 三个重要的问题 4.2 函数:基本格式 4.3 函数的编写要求 4.3.1 你想住在C++旅馆中吗 4.3.2 函数为先 4.3.3 函数声明或函数原型 4.3.4 函数定义、函数标题行与函数体 4.3.5 函数调用 4.3.6 传值调用 4.3.7 问题分析:未声明的标识符 4.4 重载函数 4.5 具有默认输入参数列表的函数 4.6 局部变量、全局变量和静态变量 4.6.1 局部变量 4.6.2 块范围 4.6.3 全局变量 4.6.4 危险的全局变量 4.6.5 问题分析:全局变量y0、y1与cmath 4.6.6 静态变量 4.7 C++stringstream类 4.8 总结 4.9 练习 复习题 第5章 函数二:变量地址、指针以及引用 5.1 数据变量和内存 5.1.1 sizeof运算符 5.1.2 预留内存 5.1.3 计算机内存和十六进制 5.2 取地址运算符& 5.3 指针 5.4 函数、指针以及间接运算符 5.4.1 解决思路 5.4.2 指针和函数 5.4.3 有效处理大型数据 5.5 函数和引用 5.5.1 复习:两种机制 5.5.2 为什么要强调指针的重要性 5.6 queue类 5.7 总结 5.8 练习 复习题 第6章 数组 6.1 使用单个数据变量 6.2 数组基础 6.2.1 数组的索引值从0开始 6.2.2 使用for循环和数组来实现的电话账单程序 6.2.3 数组的声明和初始化 6.2.4 数组越界==严重的问题 6.2.5 vector与数组的比较 6.3 数组和函数 6.3.1 每个数组都有一个指针 6.3.2 数组指针 6.3.3 向函数传递数组:最开始的引用调用 6.3.4 利用数组和函数生成随机数并进行排序 6.4 C字符串,也称为字符数组 6.4.1 字符数组的初始化 6.4.2 null字符 6.4.3 C字符串的输入 6.4.4 C++中提供的字符数组函数 6.5 多维数组 6.5.1 二维数组的初始化 6.5.2 嵌套的for循环和二维数组 6.5.3 利用二维数组来实现Bingo游戏 6.6 多维数组和函数 6.6.1 改进的Bingo卡片程序 6.6.2 白雪公主:利用二维数组来存储姓名 6.7 利用数据文件对数组赋值 6.8 总结 6.9 练习 复习题 第7章 类和对象 7.1 我们所了解的类和对象 7.2 编写自己的类 7.2.1 入门实例:自定义日期类 7.2.2 第一个C++类:Date类 7.2.3 揭开类的生命之谜 7.2.4 set和get函数的作用与VolumeCalc类 7.2.5 PICalculator类 7.3 作为类成员的对象 7.4 类的析构函数 7.5 对象数组 7.6 重载运算符与对象 7.7 指针、引用和类 7.7.1 指针和引用实例 7.7.2 处理日期和时间的程序实例 7.8 总结 7.9 练习 复习题 第8章 继承和虚函数 8.1 为什么继承如此重要 8.1.1 IceCreamDialog实例 8.1.2 Counter类实例 8.2 继承基础 8.2.1 Counter和DeluxeCounter实例 8.2.2 保护成员 8.2.3 员工、老板和CEO 8.3 访问控制符的规范和多继承 8.4 继承、构造和析构 8.4.1 构造函数和析构函数回顾 8.4.2 基类和派生类的默认构造函数——没有参数 8.4.3 在重载的构造函数中使用参数 8.4.4 基类和派生类的析构函数 8.4.5 医生也是人 8.4.6 关于派生类和基类构造函数的规则 8.5 多态和虚函数 8.5.1 多态——同一个接口,不同的行为 8.5.2 什么是虚函数 8.5.3 虚函数的作用 8.6 总结 8.7 练习 复习题 附录A 学习使用Visual C++2005Express Edition 附录B C++关键字表 附录C C++运算符 附录D ASCII码 附录E 位、字节、内存和十六进制表示 附录F 文件输入/输出 附录G 部分C++类 附录H 多文件程序 附录I Microsoft visual C++2005Express Edit
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值