最重要的事:一定要先从官方文档阅读
网上大部分教程都是某一个版本、或者是以自己的理解来讲解,但是工具是变化的,C++在不同的时期会有不同的特性,所以紧跟时代是重要的。
现代 C++
资源和智能指针
- C 样式编程的一个主要 bug 类型是内存泄漏。
泄漏通常是由于为分配的内存的调用失败引起的 delete new 。 现代 C++ 强调“资源获取即初始化”(RAII) 原则。 请尽可能使用智能指针管理堆内存。
std::string 和 std::string_view
- C 样式字符串是 bug 的另一个主要来源。
通过使用 std::string 和 std::wstring,,几乎可以消除与 C 样式字符串关联的所有错误。
std::vector 和其他标准库容器
容器总结
- 标准库容器都遵循 RAII 原则。 它们为安全遍历元素提供迭代器。
使用 vector 替代原始数组,来作为 C++ 中的序列容器。 使用 map(而不是 unordered_map),作为默认关联容器。 对于退化和多案例,使用 set、multimap 和 multiset。
vector<string> apples;
apples.push_back("Granny Smith");
map<string, string> apple_color;
apple_color["Granny Smith"] = "Green";
标准库算法
- 在假设需要为程序编写自定义算法之前,请先查看 C++ 标准库算法。
标准库包含许多常见操作(如搜索、排序、筛选和随机化)的算法分类,这些分类在不断增长。 for_each,默认遍历算法(以及基于范围的 for 循环)。 transform,用于对容器元素进行非就地修改 find_if,默认搜索算法。 sort、lower_bound 和其他默认的排序和搜索算法。
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();
auto i = m.begin();
基于范围的 for 循环
- 使用基于范围的 for 循环,此循环包含标准库容器和原始数组。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v {1,2,3};
for(int i = 0; i < v.size(); ++i)
{
std::cout << v[i];
}
for(auto& num : v)
{
std::cout << num;
}
}
用 constexpr 表达式替代宏
- C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。
在现代 C++ 中,应优先使用 constexpr 变量定义编译时常量。
#define SIZE 10
constexpr int size = 10;
#define toupper(a) ((a) >= 'a' && ((a) <= 'z') ? ((a)-('a'-'A')):(a))
const和define的区别以及const的优点
统一初始化
- 在现代 C++ 中,可以使用任何类型的括号初始化。
在初始化数组、矢量或其他容器时,这种初始化形式会非常方便。
#include <vector>
struct S
{
std::string name;
float num;
S(std::string s, float f) : name(s), num(f) {}
};
int main()
{
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);
std::vector<S> v2 {s1, s2, s3};
std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };
}
移动语义
- 现代 C++ 提供了移动语义,此功能可以避免进行不必要的内存复制。
在此语言的早期版本中,在某些情况下无法避免复制。 移动操作会将资源的所有权从一个对象转移到下一个对象,而不必再进行复制。 一些类拥有堆内存、文件句柄等资源。 实现资源所属的类时,可以定义此类的移动构造函数和移动赋值运算符。 在解析重载期间,如果不需要进行复制,编译器会选择这些特殊成员。 如果定义了移动构造函数,则标准库容器类型会在对象中调用此函数。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = string("h") + "e" + "ll" + "o";
cout << s << endl;
}
Lambda 表达式
- 在 C 样式编程中,可以通过使用函数指针将函数传递到另一个函数。
函数指针不便于维护和理解。 它们引用的函数可能是在源代码的其他位置中定义的,而不是从调用它的位置定义的。 此外,它们不是类型安全的。 现代 c + + 提供 函数对象、重写 运算符的类,从而使它们可以像函数一样进行调用。 创建函数对象的最简便方法是使用内联 lambda 表达式。
std::vector<int> 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; });
现代 C++ 处理异常和错误的最佳做法
- 现代 c + + 强调异常,而不是错误代码,作为报告和处理错误条件的最佳方式。
线程间通信机制
- 对线程间通信机制使用 C++ 标准库 std::atomic 结构和相关类型。
C++17 管理可变类型的内存位置 variant
- C 样式编程通常通过并集使不同类型的成员可以占用同一个内存位置,从而节省内存。
但是,并集不是类型安全的,并且容易导致编程错误。 C++17 引入了更加安全可靠的 std::variant 类,来作为并集的替代项。 可以使用 std::visit 函数以类型安全的方式访问 variant 类型的成员。
基本概念
左值和右值
每个 C++ 表达式都有一个类型,并属于 值类别。 值类别是编译器在表达式计算期间创建、复制和移动临时对象时必须遵循的规则的基础。
C++17 标准定义表达式值类别,如下所示:
- glvalue 是一个表达式,其计算确定对象、位字段或函数的标识。
- prvalue 是一个表达式,其计算初始化对象或位字段,或计算运算符的操作数的值,由其出现的上下文指定。
- xvalue 是一个 glvalue ,表示一个对象或位字段,该对象或位字段的资源可以重复使用 (通常是因为它接近其生存期的结尾) 。 示例:某些类型的表达式涉及 rvalue 引用 (8.3.2) 生成 xvalue,例如对返回类型为 rvalue 引用的函数的调用或对 rvalue 引用类型的强制转换。
- lvalue是一个不属于xvalue的glvalue。
- rvalue 是 prvalue 或 xvalue。
- lvalue 具有程序可以访问的地址。 lvalue 表达式的示例包括变量名称,包括 const 变量、数组元素、返回 lvalue 引用、位字段、联合和类成员的函数调用。
- prvalue 表达式没有程序可访问的地址。 prvalue 表达式的示例包括文本、返回非引用类型的函数调用以及表达式计算期间创建的临时对象,但只能由编译器访问。
- xvalue 表达式具有一个地址,该地址不再可供程序访问,但可用于初始化 rvalue 引用,该引用提供对表达式的访问权限。 示例包括返回 rvalue 引用的函数调用,以及数组下标、成员和指向数组或对象是 rvalue 引用的成员表达式的指针。
i = 7;
语法
引用
左值引用声明符 Lvalue & (C 风格 取地址运算符)
左值引用:只能绑定到不会被马上销毁的变量上(比如 已经定义的i)
-
不要将引用声明与使用 地址运算符混淆。 &当标识符前面有类型(例如int或char)标识符声明为对类型的引用时。 如果 &标识符 前面没有类型,则用法是地址运算符的用法。 -
引用类型函数自变量 向函数传递引用而非大型对象的效率通常更高。 这使编译器能够在保持已用于访问对象的语法的同时传递对象的地址。 -
引用类型函数返回值 返回的信息是一个返回引用比返回副本更有效的足够大的对象。 函数的类型必须为左值。 引用的对象在函数返回时不会超出范围。 -
指针的引用
BTree*
BTree&
BTree**
BTree*&
右值引用声明符 Rvalue &&
右值引用:只能绑定到一个将要销毁的对象上(比如 i * 42 表达式)
- 移动语义
管理内存缓冲区的 C++ 类 MemoryBlock 就是用移动语义编写出来的。 若要实现移动语义,通常为类提供 移动构造函数( 可选)和移动赋值运算符 (operator=) 。 其源是右值的复制和赋值操作随后会自动利用移动语义。 与默认复制构造函数不同,编译器不提供默认移动构造函数。
为什么使用移动语义? 若要更好地了解移动语义,请考虑将元素插入 vector 对象的示例。 如果超出 vector 对象的容量,则 vector 对象必须为其元素重新分配内存,然后将所有元素复制到其他内存位置,以便为插入的元素腾出空间。 当插入操作复制元素时,它首先创建一个新元素。 然后,它会调用复制构造函数将数据从上一个元素复制到新元素。 最后,它会销毁上一个元素。 使用移动语义可以直接移动对象,而无需进行昂贵的内存分配和复制操作。
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
std::cout << "In MemoryBlock(MemoryBlock&&). length = "
<< other._length << ". Moving resource." << std::endl;
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
MemoryBlock& operator=(MemoryBlock&& other) noexcept
{
std::cout << "In operator=(MemoryBlock&&). length = "
<< other._length << "." << std::endl;
if (this != &other)
{
delete[] _data;
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
return *this;
}
- 完美转发( 详细请看下面的模板 )
基于工厂设计模式 完美转发可减少对重载函数的需求,并有助于避免转发问题。 编写采用引用作为其参数的泛型函数时,可能会出现 转发问题 。 如果它将 (或 转发) 这些参数传递给另一个函数, 例如,如果它采用类型 const T&参数,则调用的函数无法修改该参数的值。 如果泛型函数采用类型 T&参数,则无法使用右值 ((如临时对象或整数文本) )调用该函数。 通常,若要解决此问题,则必须提供为其每个参数采用 T& 和 const T& 的重载版本的泛型函数。 因此,重载函数的数量将基于参数的数量呈指数方式增加。 使用 Rvalue 引用可以编写接受任意参数的函数的一个版本。 然后,该函数可以将它们转发到另一个函数,就像直接调用另一个函数一样。
模板
- 模板是 C++ 中的泛型编程的基础。
作为强类型语言,C++ 要求所有变量都具有特定类型,由程序员显式声明或编译器推断。 但是,无论它们运行哪种类型,许多数据结构和算法看起来都相同。 使用模板可以定义类或函数的操作,并让用户指定这些操作应处理的具体类型。 使用模板时的主要限制是类型参数必须支持应用于类型参数的任何操作。 void* 与 template 的优缺点
template <typename T>
T minimum(const T& lhs, const T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
完美转发
- 再次提到 完美转发
这个模板类只能用来处理Const。 通常,若要解决此问题,您必须为 factory 和 A& 的参数的每个组合创建一个重载版本的 const A& 函数。 利用右值引用,您可以编写一个版本的 factory 函数( 完美转发 ),如以下示例所示。
template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}
类型参数
- 请注意,在函数调用参数中使用类型参数 T 之前,不会以任何方式限定类型参数 T,在该参数中添加 const 和引用限定符。
template<typename... Arguments> class vtclass;
vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
非类型参数
- 与其他语言(如 C# 和 Java)中的泛型类型不同,C++ 模板支持 非类型参数,也称为值参数。
该值 size_t 在编译时作为模板参数传入,必须是 const 或 constexpr 表达式。
template<typename T, size_t L>
class MyArray
{
T arr[L];
public:
MyArray() { ... }
};
MyArray<MyClass*, 10> arr;
非类型模板参数的类型推理 c++17
- 编译器会推断使用auto方式声明的非类型模板参数的类型
template <auto x> constexpr auto constant = x;
auto v1 = constant<5>;
auto v2 = constant<true>;
auto v3 = constant<'a'>;
模板作为模板参数
- 模板可以是模板参数。
在此示例中,MyClass2 有两个模板参数:typename 参数 T 和模板参数 Arr
template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
T t;
Arr<T, 10> a;
U u;
};
默认模板参数
- 类和函数模板可以具有默认参数。 当模板具有默认参数时,可以在使用模板时将其保留为未指定。
template <class T, class Allocator = allocator<T>> class vector;
模板特殊化
- 类模板可以部分专用化,生成的类仍是模板。 在类似于下面的情况下,部分专用化允许为特定类型部分自定义模板代码:
[1]模板有多个类型,且只有一部分需要专用化。 结果是基于其余类型参数化的模板。 [2]模板只有一个类型,但指针、引用、指向成员的指针或函数指针类型需要专用化。 专用化本身仍是指向或引用的类型上的模板。
内联函数
- 类声明的主体中定义的函数是内联函数。
我们也可以在类声明外,声明一个函数是内联函数。 内联函数类似于宏,因为函数代码在编译时调用时扩展。 但是,内联函数由编译器分析,宏由预处理器扩展。 使用宏是不安全的 , 内联函数遵循对正常函数强制执行的所有类型安全协议。 内联函数使用与除函数声明中包含 inline 关键字以外的任何其他函数相同的语法指定。 计算一次作为内联函数的参数传递的表达式。 在某些情况下,作为宏的自变量传递的表达式可计算多次。
#include <stdio.h>
#include <conio.h>
#define toupper(a) ((a) >= 'a' && ((a) <= 'z') ? ((a)-('a'-'A')):(a))
int main() {
char ch;
printf_s("Enter a character: ");
ch = toupper(getc(stdin));
printf_s("%c", ch);
}
改用正常函数或者内联函数来写
#include <stdio.h>
#include <conio.h>
inline char toupper( char a ) {
return ((a >= 'a' && a <= 'z') ? a-('a'-'A') : a );
}
int main() {
printf_s("Enter a character: ");
char ch = toupper( getc(stdin) );
printf_s( "%c", ch );
}
在下面的类声明中,Account 构造函数是内联函数。 成员函数GetBalanceDeposit,Withdraw未指定为inline内联函数,但可以实现为内联函数。
class Account
{
public:
Account(double initial_balance) { balance = initial_balance; }
double GetBalance();
double Deposit( double Amount );
double Withdraw( double Amount );
private:
double balance;
};
inline double Account::GetBalance()
{
return balance;
}
inline double Account::Deposit( double Amount )
{
return ( balance += Amount );
}
inline double Account::Withdraw( double Amount )
{
return ( balance -= Amount );
}
int main()
{
}
Warning
C4018 有符号/无符号不匹配
容器.size() 是unsigned int 类型。 在遍历的时候,也要使用 unsigned int类型去遍历。
|