C++ 学习指南

最重要的事:一定要先从官方文档阅读

网上大部分教程都是某一个版本、或者是以自己的理解来讲解,但是工具是变化的,C++在不同的时期会有不同的特性,所以紧跟时代是重要的。

现代 C++

资源和智能指针

  • C 样式编程的一个主要 bug 类型是内存泄漏。
    泄漏通常是由于为分配的内存的调用失败引起的 delete new 。 现代 C++ 强调“资源获取即初始化”(RAII) 原则。
    请尽可能使用智能指针管理堆内存。

智能指针实质是一个对象,行为表现的却像一个指针。

  • unique_ptr 对于同一块内存只能有一个持有者, 不允许赋值操作,也就是不能放在等号的左边。
typedef boost::shared_ptr<Socket> SocketPtr;
SocketPtr accept();
  • shared_ptr 是目前工程内使用最多最广泛的智能指针,它使用引用计数实现对同一块内存的多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和 unique_ptr 最大的区别。
    引用计数型的智能指针在Python中就有点像垃圾回收机制。
    这样外部就可以用智能指针去接收,那么何时析构?当然是引用计数为 0。
    和python一样需要避免循环引用导致的内存泄露。

Python的GC算法很简单,恰恰是使用单一的引用计数,这一点无法避免循环引用,这个是和C++11的shared_ptr是很类似的。

  • weak_ptr 并不拥有其指向的对象,也就是说,让 weak_ptr 指向 shared_ptr 所指向对象,对象的引用计数并不会增加。

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(); // C-style
auto i = m.begin(); // modern 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++ 中,应优先使用 constexpr 变量定义编译时常量。
#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

//附加内容
#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()
{
    // 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++ 提供了移动语义,此功能可以避免进行不必要的内存复制。
    在此语言的早期版本中,在某些情况下无法避免复制。
    移动操作会将资源的所有权从一个对象转移到下一个对象,而不必再进行复制。
    一些类拥有堆内存、文件句柄等资源。 实现资源所属的类时,可以定义此类的移动构造函数和移动赋值运算符。 在解析重载期间,如果不需要进行复制,编译器会选择这些特殊成员。 如果定义了移动构造函数,则标准库容器类型会在对象中调用此函数。
// string_concatenation.cpp
// compile with: /EHsc
#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 引用的成员表达式的指针。

在这里插入图片描述

// Correct usage: the variable i is an lvalue and the literal 7 is a prvalue.
i = 7;

语法

引用

左值引用声明符 Lvalue & (C 风格 取地址运算符)

左值引用:只能绑定到不会被马上销毁的变量上(比如 已经定义的i)

  • 不要将引用声明与使用 地址运算符混淆。
    &当标识符前面有类型(例如int或char)标识符声明为对类型的引用时。
    如果 &标识符 前面没有类型,则用法是地址运算符的用法。

  • 引用类型函数自变量
    向函数传递引用而非大型对象的效率通常更高。 这使编译器能够在保持已用于访问对象的语法的同时传递对象的地址。

  • 引用类型函数返回值
    返回的信息是一个返回引用比返回副本更有效的足够大的对象。
    函数的类型必须为左值。
    引用的对象在函数返回时不会超出范围。

  • 指针的引用

BTree*  //指针
BTree&  //引用
BTree** //二重指针
BTree*& //指针的引用

右值引用声明符 Rvalue &&

右值引用:只能绑定到一个将要销毁的对象上(比如 i * 42 表达式)

  • 移动语义
    管理内存缓冲区的 C++ 类 MemoryBlock 就是用移动语义编写出来的。
    若要实现移动语义,通常为类提供 移动构造函数( 可选)和移动赋值运算符 (operator=) 。
    其源是右值的复制和赋值操作随后会自动利用移动语义。
    与默认复制构造函数不同,编译器不提供默认移动构造函数。

为什么使用移动语义?
若要更好地了解移动语义,请考虑将元素插入 vector 对象的示例。 如果超出 vector 对象的容量,则 vector 对象必须为其元素重新分配内存,然后将所有元素复制到其他内存位置,以便为插入的元素腾出空间。 当插入操作复制元素时,它首先创建一个新元素。 然后,它会调用复制构造函数将数据从上一个元素复制到新元素。 最后,它会销毁上一个元素。 使用移动语义可以直接移动对象,而无需进行昂贵的内存分配和复制操作。

// Move constructor.
// 定义一个空的构造函数方法,该方法采用一个对类类型的右值引用作为参数
MemoryBlock(MemoryBlock&& other) noexcept
   : _data(nullptr)
   , _length(0)
{
   std::cout << "In MemoryBlock(MemoryBlock&&). length = "
             << other._length << ". Moving resource." << std::endl;

   // Copy the data pointer and its length from the
   // source object.
   _data = other._data;
   _length = other._length;

   // Release the data pointer from the source object so that
   // the destructor does not free the memory multiple times.
   other._data = nullptr;
   other._length = 0;
}

// Move assignment operator.
// 定义一个赋值运算符,该运算符采用一个对类类型的右值引用作为参数并返回一个对类类型的引用
MemoryBlock& operator=(MemoryBlock&& other) noexcept 
{
   std::cout << "In operator=(MemoryBlock&&). length = "
             << other._length << "." << std::endl;

   if (this != &other)
   {
      // Free the existing resource.
      delete[] _data;

      // Copy the data pointer and its length from the
      // source object.
      _data = other._data;
      _length = other._length;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      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>;      // v1 == 5, decltype(v1) is int
auto v2 = constant<true>;   // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>;    // v3 == 'a', decltype(v3) is char

模板作为模板参数

  • 模板可以是模板参数。
    在此示例中,MyClass2 有两个模板参数:typename 参数 T 和模板参数 Arr
template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
    U u; //Error. U not in scope
};

默认模板参数

  • 类和函数模板可以具有默认参数。 当模板具有默认参数时,可以在使用模板时将其保留为未指定。
template <class T, class Allocator = allocator<T>> class vector;

模板特殊化

  • 类模板可以部分专用化,生成的类仍是模板。 在类似于下面的情况下,部分专用化允许为特定类型部分自定义模板代码:
    [1]模板有多个类型,且只有一部分需要专用化。 结果是基于其余类型参数化的模板。
    [2]模板只有一个类型,但指针、引用、指向成员的指针或函数指针类型需要专用化。 专用化本身仍是指向或引用的类型上的模板。

内联函数

  • 类声明的主体中定义的函数是内联函数。
    我们也可以在类声明外,声明一个函数是内联函数。
    内联函数类似于宏,因为函数代码在编译时调用时扩展。
    但是,内联函数由编译器分析,宏由预处理器扩展。
    使用宏是不安全的
    内联函数遵循对正常函数强制执行的所有类型安全协议。
    内联函数使用与除函数声明中包含 inline 关键字以外的任何其他函数相同的语法指定。
    计算一次作为内联函数的参数传递的表达式。 在某些情况下,作为宏的自变量传递的表达式可计算多次。
// inline_functions_macro.c
#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);
}
//  Sample Input:  xyz
// Sample Output:  Z

改用正常函数或者内联函数来写

// inline_functions_inline.cpp
#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 );
}
//  Sample Input: a
// Sample Output: A

在下面的类声明中,Account 构造函数是内联函数。 成员函数GetBalanceDeposit,Withdraw未指定为inline内联函数,但可以实现为内联函数。

// Inline_Member_Functions.cpp
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类型去遍历。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 前 言6 第1 章 文件结构 1.1 版权和版本的声明. 1.2 头文件的结构. 1.3 定义文件的结构. 1.4 头文件的作用. 1.5 目录结构. 第2 章 程序的版式 2.1 空行. 2.2 代码行. 2.3 代码行内的空格. 2.4 对齐. 2.5 长行拆分. 2.6 修饰符的位置. 2.7 注释. 2.8 类的版式. 第3 章 命名规则 3.1 共性规则. 3.2 简单的WINDOWS 应用程序命名规则. 3.3 简单的UNIX 应用程序命名规则 第4 章 表达式和基本语句 4.1 运算符的优先级. 4.2 复合表达式. 4.3 IF 语句 4.4 循环语句的效率. 4.5 FOR 语句的循环控制变量. 4.6 SWITCH 语句. 4.7 GOTO 语句. 第5 章 常量 5.1 为什么需要常量. 5.2 CONST 与 #DEFINE 的比较. 5.3 常量定义规则. 5.4 类中的常量. 第6 章 函数设计 高质量C++/C 编程指南,v 1.0 2001 Page 4 of 101 6.1 参数的规则. 6.2 返回值的规则. 6.3 函数内部实现的规则. 6.4 其它建议. 6.5 使用断言. 6.6 引用与指针的比较. 第7 章 内存管理 7.1 内存分配方式 7.2 常见的内存错误及其对策 7.3 指针与数组的对比 7.4 指针参数是如何传递内存的? 7.5 FREE 和DELETE 把指针怎么啦? 7.6 动态内存会被自动释放吗?. 7.7 杜绝“野指针”. 7.8 有了MALLOC/FREE 为什么还要NEW/DELETE ?. 7.9 内存耗尽怎么办?. 7.10 MALLOC/FREE 的使用要点 7.11 NEW/DELETE 的使用要点. 7.12 一些心得体会 第8 章 C++函数的高级特性 8.1 函数重载的概念. 8.2 成员函数的重载、覆盖与隐藏. 8.3 参数的缺省值. 8.4 运算符重载. 8.5 函数内联. 8.6 一些心得体会. 第9 章 类的构造函数、析构函数与赋值函数 9.1 构造函数与析构函数的起源. 9.2 构造函数的初始化表. 9.3 构造和析构的次序. 9.4 示例:类STRING 的构造函数与析构函数 9.5 不要轻视拷贝构造函数与赋值函数. 9.6 示例:类STRING 的拷贝构造函数与赋值函数 9.7 偷懒的办法处理拷贝构造函数与赋值函数. 9.8 如何在派生类中实现类的基本函数. 9.9 一些心得体会. 第10 章 类的继承与组合. 高质量C++/C 编程指南,v 1.0 2001 Page 5 of 101 10.1 继承 10.2 组合 第11 章 其它编程经验. 11.1 使用CONST 提高函数的健壮性 11.2 提高程序的效率 11.3 一些有益的建议 参考文献 附录A :C++/C 代码审查表. 附录B :C++/C 试题. 附录C :C++/C 试题的答案与评分标准.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值