事先说明

资料来源:

还有各处零散资料,不一一例举。同 C 语言差不多的地方,也不会写在本文。

编写规范

语言风格

Google 风格:

微软风格:同 C 语言一致。

注意事项

尽量使用现代 Cpp 写法

语言要求

main

任何 Cpp 程序必须从 main 函数开始,与 C 语言差不多。另外 Cpp 中构造函数也是在 main 函数之前执行。

C 头文件的迁移

有些 C 头文件被转换为 C++ 头文件,这些文件被重新命名,去掉了扩展后缀 h,并在文件名称前面加上前缀 c。表示来自 C 语言但是改造成了 C++ 的库。例如,C++ 版本的 math.hcmath

有时 C 头文件的 C 版本和 C++版本相同,而有时候新版本做了一些修改。所以建议用 C++ 风格的头文件

命名空间

命名空间是为了避免命名重复和污染而产生的。

定义:

// 命名空间可以定义变量,函数,类型。
namespace Jill{
    double bucket(double n){ ... }
    double fetch;
    int pal;
    struct Hill{ ... };
} 
// 可以命名同样名称的命名空间,但是最后会合并成同一个。
namespace Jill{ 
  void fuck;
}

// 可以嵌套
namespace outer{namespace inter{}} 
// 等效于上面
namespace outer::inter{} 

// 可匿名
namespace{}

// 全局变量真实写法
int g = 42;
void f();
// cpp 有个隐式的名称空间,被成为全局名称空间,同名字意思。等同于上面
namespace /* global */ { 
    int g = 42;
    void f();
}

int main(){
  ::f(); // 所以可以用:: 指是全局函数
}

使用:

namespace myspace {
  int a;
}

int main(){
  myspace::a = 1;          // 通过限定符调用内部内容
  using namespace myspace; // 直接展开该命名空间所有内容,不推荐
  using myspace::a;        // 展开该命名空间部分内容
}

类型

字面量

C++11 允许用户自定义字面量,可以理解为相应的字面量会按照用户的想法处理,但是这个必须符合 C++ 标准给出的形参格式。 一般来说这个可以用来对字面量做一些处理和符号重载。具体参考规定:https://cppreference.cn/w/cpp/language/user_literal

类型转化

运算符主要用途 / 语义检查时机风险与注意事项
static_cast编译期类型转换(“正常的”类型间转换)编译期仅做语义合理的转换,编译器会做基本检查。下行转换不做运行时检查,类型不匹配会产生未定义行为
dynamic_cast运行时带检查的多态类型转换(安全 downcast)运行期(RTTI)只能用于类的指针/引用,且类必须是多态类型(至少一个虚函数)。指针转换失败返回 nullptr,引用失败抛 std::bad_cast
reinterpret_cast底层“重解释”转换(仅重解释比特位)编译期(几乎不检查语义)极易导致未定义行为和不可移植代码,仅在非常清楚底层布局时使用,普通业务代码应尽量避免
const_cast改变表达式的 const / volatile 属性编译期“若原始对象本身是 const,通过去 const 后修改它”属于是未定义行为,只应用于“本体非 const,只是接口写成 const”的场景

普通类型

类别C++ 中的东西在 C 里的情况说明与典型用法
布尔类型bool(关键字),true / false(关键字)C99 起在 <stdbool.h> 中用宏:typedef _Bool bool;true/false 是宏,不是关键字在 C++ 中 bool 是内建基本类型,语法层面更“第一等”;在 C 中要 #include <stdbool.h> 才有 bool,底层是 _Bool
布尔字面量true / false(关键字)在 C99 里是宏:#define true 1 / #define false 0(通过 <stdbool.h> 提供)从语法上 C++ 支持 bool x = true; 不需要任何头文件;C 要 #include <stdbool.h> 才有。
宽字符类型wchar_t 是内建关键字类型C 中 wchar_t 是通过头文件引入的 typedef<wchar.h><stddef.h>),不是关键字用于宽字符(如 Unicode 宽字符)处理:wchar_t c = L'中';,搭配 L"宽字符串"wprintf 等。C 与 C++ 都有,但 C++ 把它当关键字处理,C 则当普通 typedef。
UTF 字符类型char16_tchar32_t(关键字类型)C11 中没有 char16_t/char32_t 关键字,只有 _Char16_t / _Char32_t 这样的保留标识用于实现内部;用户一般不用C++11 引入了专门的 UTF-16 / UTF-32 字符类型和字面量:u'a'u"..."U'a'U"..."。C11 也有 u"..." / U"..." 字面量,但没有对应的关键字类型供直接声明。
UTF 字符串字面量u"abc"char16_t const[]),U"abc"char32_t const[]C11 也支持 u"..." / U"..." 宽字符串字面量,但对应类型是实现相关的宽字符数组(常是 char16_t/char32_t 的内部等价),不直接暴露关键字类型区别主要在:C++ 把 char16_t/char32_t 作为独立基本类型公开;C 偏向通过宽字符串和库来处理。
原始字符串字面量R"(原始字符串\n不需要转义)"C 标准没有原始字符串字面量;GCC 等有 R"(...)" 作为 C 扩展,但不是标准 CC++11 标准引入的特性:不需要对反斜杠、引号做转义,适合正则表达式、路径等。C 里想要类似效果只能用多行字符串拼接或宏。
空指针字面量nullptr(关键字,类型为 std::nullptr_tC 没有 nullptr,只有宏 NULL(一般是 0(void*)0C++ 不允许直接将 void * 隐式转换到其他类型,从而 ((void*)0) 不是 NULL 的合法实现,而用 0 赋值又无法解决重载的混乱。故创造了 nullptr,可隐式转换到任意指针类型。
nullptr_t 类型std::nullptr_t(在 <cstddef> 中)C 无对应类型,只能用 void* 或整型表示“空指针值”便于模板与重载区分“真·空指针”与整数 0

复合结构

结构体

在 C++ 中,struct 等价于类 class

联合体

union 语法与类一致,如果添加一个有非平凡构造析构的类型,需要自己手动析构。

std::variantstd::visit

std::variant 是 C++17 引入的一个更类型安全型的联合体。std::visit 是一个相关的访问器,避免重复写一堆 std::get 来访问 std::variant

枚举

C++ 引入了 enum class 关键字,虽然仍然能用 enum,但是已经完全不推荐了。

enum 所有枚举成员名字都直接放在外层作用域中,而且本质上是 int,可以用隐式当整形用,容易混淆。而 enum class 解决了这些问题。

// 解决作用域问题
enum class Color { Red,Green };
enum class TrafficLight { Red,Green,Yellow };
Color c = Color::Red;
TrafficLight t = TrafficLight::Red;

// 解决隐式转化问题
Color c = Color::Red;
int x = c;          // 编译错误:不能隐式转成 int
c = 1;              // 编译错误:不能用整数给 enum class 赋值

// 指定底层类型
enum class Color : unsigned char {
    Red,
    Green,
    Blue
};

类型推断

auto

auto 只能初始化用,用于自动推断类型,编译期开始.

从 C++ 14 起,auto 能用于 lambda 表达式中的函数传参,而 C++ 20 起该功能推广到了一般的函数和模板返回值。但是 auto 还不能用于推导数组类型

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。用于处理表达式。例如:decltype (a+b) c = a + b;

所以 decltype 可以推断 const,引用等值类别,会严格保留相应表达式类别,而不只是类型。

C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置来推断模板返回值类型,不过 C++14 开始是可以直接让普通函数具备返回值推导。下例:

template<typename T,typename U>
auto add2(T x,U y) -> decltype(x+y){
    return x + y;
}

decltype(auto)

decltype(auto) 会像 decltype 一样分析表达式的类型和值类别,但语法上像 auto 一样不需要显式指定表达式。

左右值

凡是需要区分左值右值的,统一用通用引用来重载。第一代码更可读,否则每个都要写一个重载,太麻烦了; 第二对于某些量(比如字符串字面量),性能更好,可以避免一次构造,原因就是它是 const,数组形式不会退化。 凡是用通用引用的,统一用 forward 转发; 凡是用右值引用的,统一用 move 移动。

右值引用

左值,右值,将亡值

左值,顾名思义就是赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

右值,右边的值,是指表达式结束后就不再存在的临时对象。 而中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

纯右值,纯粹的右值,要么是纯粹的字面量,例如;要么是求值结果相当于字面量或匿名临时对象,例如。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、表达式都属于纯右值。但是字符串字面量不算右值,这是特例。

简单来说,左值,即可以用&取地址的值,右值就不可以,包括 return 返回的临时变量。

右值引用

使用 && 可以引用右值,这个引用本身是左值,因为可以取地址。比如 int &&a = 19,a 是左值,19 为右值。

常量引用 const T& 左值右值都可以,对于右值来说就是生成一个临时变量,延长声明周期,避免低效拷贝。

通用引用

在有类型推导的情况下,也就是类似下面情况:

template<typename T>
void f(T&& param);                  //param 是一个通用引用

auto&& var2 = var1;                 //var2 是一个通用引用

这种可能是左值也可能是右值的情况叫做通用引用。

引用坍缩规则

形参类型实参类型推导后最终类型说明
T&&(左值引用)T&左值引用 + 左值引用 → 左值引用
T&&&(右值引用)T&左值引用 + 右值引用 → 左值引用
T&&&(左值引用)T&右值引用 + 左值引用 → 左值引用
T&&&&(右值引用)T&&右值引用 + 右值引用 → 右值引用

移动语义

使用 std::move 可以把左值变成右值,实际上就是把资源所有权交出去了,这也叫做移动语义。源码上其实就是个转化,所以移动语义不移动。

注意如果类没有实现移动构造,默认调用还是拷贝构造,因为拷贝构造能接受右值(const)。

移动语义只有在移动对象实现移动操作,且值比较大(比如大于两个指针的大小),且进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为 noexcept,这三种情况都满足的时候,建议使用。

也不必如此严苛,日常尽量写移动,稍微注意一点这些情况就行。

完美转发

完美转发就是保持原本的参数类型,下面是例子

void process(const Widget& lvalArg);        //处理左值
void process(Widget&& rvalArg);             //处理右值

template<typename T>                        //用以转发 param 到 process 的模板
void logAndProcess(T&& param)
{
    auto now =                              //获取现在时间
        std::chrono::system_clock::now();
    
    makeLogEntry("Calling 'process'",now);
    process(std::forward<T>(param));
}

param 是通用引用,它指向的值会有相应类型,但是它本身是一个左值(把它当成指针)。但是我们通过 forward,就可以继续保留原有左右类型。

在以下情况会失败:

通用引用看来很完美,但是有个问题,如果写了通用引用的函数后,又写了一个重载函数,我们的目标数据有可能走的是通用引用而不是重载。比如说对于 1,short 明显比 int 更适合,而通用引用就可以做到这点,根据重载的规则,更能匹配的优先。

所以请不要对通用引用函数重载,或者你可以下面方法来解决:

智能指针

类别

智能指针所有权引用计数主要用途缺点
unique_ptr独占管理独占资源,替代原始指针不可拷贝,仅可移动。
shared_ptr共享多所有者共享资源有额外内存开销,可能循环引用
weak_ptr无(弱引用)不影响解决循环引用,临时访问资源不能直接访问对象,需通过 lock()

实际开发中,应根据资源的所有权关系选择合适的智能指针:优先使用 unique_ptr(效率最高),需共享时使用 shared_ptr,配合 weak_ptr 解决循环引用问题。

shared_ptr 需要避免循环引用(如两个 shared_ptr 互相指向对方),否则引用计数永远不会归零,导致内存泄漏(需配合 weak_ptr 解决)。

另外相比 new,make_xxx 更具有优越性。具体原因无需说明,单从概念即可看出,new 的话要构造两次。

使用

智能指针能如普通指针一样使用, 因为被重载了。

.get() 获取原始指针

.release 取消托管,需要自己手动释放

.reset(xxx) 重置指针托管的地址,即释放掉原本托管的内存,用参数智能代替,如 xxx 为空则是直接释放

自定义删除器

我们会发现智能指针设置得很自由,有些销毁方式不是单纯的 delete 能解决的,所以智能指针是有自定义删除器的功能的。

有三种可以作为删除器:

std::unique_ptr<T,DeleterType>std::shared_ptr<T> 不同,单从类型可以看出,如果 unique 要自定义删除器,要实例化的时候写上相应删除器类型。

自定义指针类型

通常,当我们写 std::unique_ptr<T,Deleter> 时,unique_ptr 内部会认为它管理的是一个类型为 T* 的指针。它提供的成员函数,比如 get(),就会返回一个 T* 类型。

例如,对于 std::unique_ptr<int>,它内部管理的指针类型是 int*

但是问题出现了,如果当 T 本身就是句柄或指针类型时怎么办,编译器会因为没有可以匹配的删除器报错的。

故而在 std::unique_ptr<T,Deleter> 会检查其删除器类型 Deleter 中是否存在一个名为 pointer 的嵌套类型,来告诉它 T 应该是什么。

下例:

struct HandleDeleter {
    // 告诉 unique_ptr,你管理的“指针”类型其实是 HANDLE
    using pointer = HANDLE;
    void operator()(HANDLE handle) const {
        if (handle && handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

// T 是 HANDLE,Deleter 是 HandleDeleter
// 因为 HandleDeleter:: pointer 存在,所以 UniqueHandle 内部的指针类型是 HANDLE,而不是 HANDLE*
// 否则编译不通过。
using UniqueHandle = std::unique_ptr<HANDLE,HandleDeleter>;

函数

返回值

返回值类型也可以使用 &,但是不能是局部变量,所以用处也不大。

const 修饰返回值类型,会抑制移动语义。

参数

在声明中使用 const 来表示这个参数不可修改。同时也禁止 const 的地址赋给非 const 指针。

在声明中使用 & 表示传递了值本身,而不是拷贝。

在声明中可以设置默认参数值,但必须从右向左添加默认值,这是 C++ 识别顺序导致的。实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。下例:

int harpo(int n,int m=4,int j=5); //VALID
int chico(int n,int m=6,int j); //INVALID
int groucho(int k=1,int m=2,int n=3); //VALID
beeps harpo(3,,8); // invalid,doesn't set m to 4

重载

重载即通过构造多个同名不同参数列表的函数,达到调用同名函数使用相应参数列表的效果。

在 C++ 中重载是通过编译器构造不同函数名字完成的。如果使用 C 库,务必使用 extern "C",因为 C 语言的函数没有改变名字,所以如果只是正常调用编译器也不知道。

重载只和参数列表(类型,个数,顺序)有关,和返回值无关。故而返回值不同但是参数列表一致的函数是无效重载。

函数容器

建议单一需求使用 Lambda,需要在一个容器内容纳多种函数存在的时候使用 std::function,不要用 std::bind

Lambda

底层实现是仿函数,也就是类对 () 的重载。其定义为:

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {/*函数体*/}

捕获列表:

需要注意的是,如果按引用捕获,要注意是否悬空捕获,比如说 lambda 把一个局部变量引用了,结果 lambda 又被一个全局容器使用了,那么就会出现这个问题。所以相比默认 &,显式地 &name 更为地好,至少会提醒你。

也许有人会想,那我默认按值引用就好了,但是如果这个值本身是个指针,后续被 delete 了,那也是有问题的。甚至即使你使用智能指针,也有可能。所以建议还是显式捕获

默认情况下,Lambda 表达式不能修改按值捕获的变量(捕获的副本被视为 const)。mutable 关键字的作用是解除这种限制,允许在 Lambda 内部修改按值捕获的变量副本。

在参数列表中运用 auto 的被称为泛型 lambda,因为编译器会生成一堆模板。

对于泛型 lambda 有个小问题,就是如果参数是按值引用,形参是左值,传进去是拷贝,效率会有点问题。可以用转发来实现,简单来说是,对 auto&& 形参使用 decltypestd::forward 它们。

std::functional

std::functional 是相比直接调用函数指针更安全(类型安全),更方便(可以容纳各种函数)的方式,换言之即函数的容器。下例:

#include <functional>
#include <iostream>

int foo(int para) {
    return para;
}

int main() {
    // std:: function 包装了一个返回值为 int,参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

由于其是用虚函数表实现,在性能上会有一定问题。

std::bind

std::bind 则是用来绑定函数调用的参数的,由于其各种问题,现在已经被抛弃了,不做介绍。

重载运算符

您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。

下面是不可重载的运算符列表:

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。下例:

Box operator+(const Box&);

面向对象

类的大部分情况和结构体差不多,相似东西不介绍。

成员函数

定义:

void Stock::updata(double price)

this

成员函数中可以使用被称为 this 的特殊指针。该指针指向用来调用成员函数的对象(this 被作为隐藏参数传递给方法)。如果需要引用整个调用对象,可以用 *this,在函数括号后面使用 constthis 限定为 const,这样就不能使用 this 修改对象函数。

友元函数

friend

const

const Stock & topval(const Stock &s) const;

返回值 const 代表调用者不能通过返回的引用修改该 Stock 对象

最后一个 const 就代表常量成员函数,带有这个 const 的成员函数可以访问不能修改调用对象的非静态不带 mutable 修饰数据成员,只能修改 static 成员变量。

常量对象只能调用常量函数(这就是为什么大多数模板要实现常量和非常量两个函数),对非常量对象没有限制。

mutable 不能修饰常量成员函数。

构造与析构

构造函数有以下几种,而且有些时候会根据有无存在相应构造函数来实现相关功能,需要严格按照其定义实现:

关于编译器生成默认移动构造函数这么多的要求原因为:

  1. 如果类中已经定义了一个移动构造函数(包括显式定义和删除的移动构造函数),编译器就不会再生成默认的移动构造函数。因为编译器认为用户已经对对象移动时的资源处理有了明确的规划
  2. 若定义了拷贝构造函数,编译器会认为用户对对象的复制操作有特定的要求,这种要求可能和移动操作存在关联,或者用户想对对象的复制和移动行为做统一控制,此时编译器不会生成默认移动构造函数。
  3. 拷贝赋值运算符用于将一个对象的值赋给另一个对象。当定义了拷贝赋值运算符,编译器会认为用户对对象之间的赋值操作有特殊需求,和移动操作可能存在冲突,就不会生成默认移动构造函数。
  4. 析构函数用于在对象生命周期结束时释放资源。如果定义了析构函数,意味着对象内部可能涉及到一些复杂的资源管理,编译器无法简单地确定移动操作的安全性和正确性,也就不会生成默认移动构造函数。
  5. 类的每个非静态成员变量类型都必须支持移动构造或者拷贝构造。如果存在不支持移动构造且也没有合适拷贝构造函数的成员变量类型,编译器就无法生成默认移动构造函数。例如,类中有一个成员变量是自定义的不支持移动构造的类类型,且没有定义合适的拷贝构造函数,编译器就不会生成默认移动构造函数。
  6. 如果类继承自其他类,基类也需要满足上述条件,即基类要么有默认移动构造函数,要么有用户自定义的合适的移动构造函数、拷贝构造函数等。

析构是销毁对象时候要用的,析构的名字是 ~对象名字()。有动态变量时有着重注意实现。

下面为构造和析构例子:

class Widget {
private:
    int* data;
    int  size;
    std::string label;

public:
    // 1. 普通构造函数(带参数)
    Widget(int* p, int n, const std::string& s)
        : data(p), size(n), label(s) {}

    // 2. 默认构造函数(无参)
    //    若类中未定义任何构造函数,编译器会自动生成一个(基本类型成员值不确定)
    //    此处显式定义,故编译器不再自动生成
    Widget() : data(nullptr), size(0), label("default") {}

    // 3. 拷贝构造函数
    //    使用已有同类型对象初始化新对象
    //    若未定义,编译器自动生成(逐成员浅拷贝)
    Widget(const Widget& other)
        : size(other.size), label(other.label)
    {
        data = new int[size];
        for (int i = 0; i < size; ++i)
            data[i] = other.data[i];
    }

    // 4. 移动构造函数(C++11)
    //    形参为非 const 右值引用,转移资源所有权
    //    若未定义且无自定义拷贝/析构/拷贝赋值,编译器可能自动生成
    Widget(Widget&& other) noexcept
        : data(other.data), size(other.size), label(std::move(other.label))
    {
        other.data = nullptr;  // 置空源对象,防止重复释放
        other.size = 0;
    }

    // 5. 委托构造函数
    //    调用同类的其他构造函数完成初始化,避免重复代码
    Widget(const std::string& s) : Widget()  // 委托给默认构造函数
    {
        label = s;  // 然后仅修改 label
    }

    Widget(int n) : Widget(new int[n]{}, n, "array") {}  // 委托给普通构造函数

    // 6. 转换构造函数
    //    单参数(或其他参数有默认值),支持隐式类型转换
    Widget(const std::string& s)  // 从 string 隐式转为 Widget
        : data(nullptr), size(0), label(s) {}
    // 用法: Widget w = std::string("hello");  或  Widget w("hello");

    // 若想禁止隐式转换,加 explicit:
    // explicit Widget(int n) : Widget(new int[n]{}, n, "array") {}

    ~Widget() { delete[] data; }
};

另外在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数。

这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。

并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。

C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。 例如:

class Magic {
    public:
    Magic() = default; // 显式声明使用编译器生成的构造
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    Magic(int magic_number);
} 

类的初始化方式

有以下几种:

class Point {
int main(){
  Point p;    // 默认初始化,调用默认构造函数
  Point p();  // 直接初始化
  Point p{};  // 值初始化
  Point p1 = p; // 拷贝初始化
  return 0;
}

类的继承

定义

class father{...};

class son:public father{
    ...
};
//father 为父类,public 代表继承关系,可以继承多个类.

继承关系:

类成员 / 继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public 成员派生类的 protected 成员派生类的 private 成员
基类的 protected 成员派生类的 protected 成员派生类的 protected 成员派生类的 private 成员
基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见

在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

子类会默认调用父类的相应成员函数,无需操心。

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例。

如果想要某个类不被继承,可以使用 final 关键字。

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

继承中的作用域

在继承体系中基类和派生类都有独立的作用域。

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。所以在实际中注意继承体系里面最好不要定义同名的成员。

切片

派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

当然,基类对象不能赋值给派生类对象。除非基类的指针或者引用通过强制类型转换赋值给派生类的指针或者引用。但是此处基类的指针必须是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(RunTime Type Information)的 dynamic_cast 来进行识别后进行安全转换。

继承后的同名函数会根据指针的类型决定调用哪个,特别是运用引用的情况下。

虚拟继承

如果有这种情况:

classDiagram
    direction RL
    class Base {
        +Base()
        #int base_data
        +void base_func()
    }
    
    class Derived1 {
        +Derived1()
        #int d1_data
        +void d1_func()
    }
    Derived1 --|> Base : 继承
    
    class Derived2 {
        +Derived2()
        #int d2_data
        +void d2_func()
    }
    Derived2 --|> Base : 继承
    
    class FinalDerived {
        +FinalDerived()
        #int d1_data 
        +void final_func()
        #int d2_data
    }
    FinalDerived --|> Derived1
    FinalDerived --|> Derived2
    
    note for FinalDerived "出现问题,最后的类对于相同的数据要实现两份"
    note for Base "基类:被多个派生类继承"

可以看到 d1_datad2_data 都被实现了.当然我们可以显式地访问相应数据,但是我们肯定不乐意这样做,所以有了 virtual,当 derived12 都虚拟继承 base 时,finalderived 直接继承两者,两个类型中一样的部分的指向改为同一个地址。

最好不要出现菱形继承关系,这样关系也很乱。

类的多态和 RTTI(runtime type identication,运行时多态)

override final 最好直接用,让编译器检查。

如果存在虚函数,无论实例化几个类,都会多出空间存放虚表的指针,但是虚表是同一张。

我对虚函数不太喜欢,所以不打算多介绍。

模板

类型模板参数

// `template` `typename` 都是必要的,除非用 `class` 替换 `typename`(两者等价),而且必须用尖括号
// 可带有默认类型
template <typename AnyType, typename T = int>
// 类也可以
void Swap(AnyType &a,AnyType &b){
    AnyType temp;
    temp = a;
    a = b;
    b = temp;
}
//template < 形参列表 > 函数声明

编译器遇到模板会马上实例化,导致多个文件实例化同一个类型,浪费时间。在 C++ 11 能 extern 模板,在一个地方统一实例化,来节省模板实例化时间。

变长参数模板

template<typename... Ts> class Magic;
// 个数不限,0 也可以
void printf(const std::string &str,Ts... args) {
  sizeof...(args); // 可获得函数中调用的参数个数
}
// Ts 是每个参数的类型,args是每个参数的值

可以通过递归解包,或者初始化列表,或者使用在 C++17 的展开语法,下面是两个例子:

// 递归
template<typename T>
void printf(T v) {
  std::cout << v << std::endl;
}
template<typename T, typename... Ts>
void printf(T v,Ts... args) {
  std::cout << v << std::endl;
  printf(args...);
}

// 初始化列表
template<typename T,typename... Ts>
auto printf(T v,Ts... args) {
    std::cout << v << std::endl;
    // 通过初始化列表,`(lambda 表达式,value)...` 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。 为了避免编译器警告,我们可以将 `std::initializer_list` 显式的转为 `void`。
    // 需要注意,这里之所以能够实现是因为 `…` 是作用与一个表达式的,或者说就是代表要展开参数。而 lambda 作为一个表达式就自然反复被调用。再通过 initializer_list 变成列表强制在编译期展开。
    (void) std::initializer_list<T>{([&args] {
        std::cout << args << std::endl;
    }(),v)...};
}

// C++ 17 展开语法
template<typename T,typename... Ts>
void printf2(T v,Ts... args) {
    std::cout << v << std::endl;
    //注意这是编译期的事情,所以不能简单用 if 解决,故而 17 才成功了
    if constexpr (sizeof...(args) > 0) printf2(args...); 
}

C++ 17 中将变长参数这种特性进一步带给了表达式,有四种类别: ( 形参包 运算符 … ) 一元右折叠 ( … 运算符 形参包 ) 一元左折叠 ( 形参包 运算符 … 运算符 初值 ) 二元右折叠 ( 初值 运算符 … 运算符 形参包 ) 二元左折叠

下例:

#include <iostream>

// 一元右折叠:(a + ...) 相当于 a1 + (a2 + (a3 + ...))
template<typename... Args>
auto sum_right(Args... args) {
    return (args + ...);
}

// 一元左折叠:(... * a) 相当于 (((a1 * a2) * a3) * ...)
template<typename... Args>
auto product_left(Args... args) {
    return (... * args);
}

// 二元右折叠:(a + ... + 10) 相当于 a1 + (a2 + (a3 + 10))
template<typename... Args>
auto sum_right_with_init(Args... args) {
    return (args + ... + 10);
}

// 二元左折叠:(100 - ... - a) 相当于 (((100 - a1) - a2) - a3)
template<typename... Args>
auto subtract_left_with_init(Args... args) {
    return (100 - ... - args);
}

int main() {
    std::cout << sum_right(1234) << std::endl;  // 1+(2+(3+4))= 10
    std::cout << product_left(1234) << std::endl;  // ((1 *2)* 3)*4 = 24
    std::cout << sum_right_with_init(123) << std::endl;  // 1+(2+(3+10))= 16
    std::cout << subtract_left_with_init(102030) << std::endl;  // ((100-10)-20)-30 = 40
    return 0;
}

非类型模板参数推导

让不同字面量成为模板参数,即非类型模板参数:

// 在 C++17 后可以使用 auto 在模板参数中推导
template <typename T,auto value, int BufSize>
class buffer_t {
public:
    T& alloc();
    void free(T& item);
private:
    T data[BufSize];
    T a = value;
}

buffer_t<int, 1, 100> buf; // value 被推导成 int,100 作为模板参数

特殊模板

类模板不是类,只有实例化类模板,编译器才能生成实际的类。变量模板同理,二者语法和函数模板差不多。

主要讲一下用户定义的推导指引,格式为 模板名称(类型a)->模板名称<想要让类型a被推导为的类型>。 如果涉及的是一类类型,那么就需要加上 template,然后使用它的模板形参。

下例:

template<typename T>
struct Test{...};

Test(int) -> Test<std::size_t>;
Test t(1);      // t 是 Test <size_t>

Test(T*) -> Test<T[]>;
Test t(p);      // t 是 Test <char[]>

// 变量模板,没啥用处
template<typename T>
T v; // 可以有各种修饰

待决名和SFINAE

待决名感觉是一种过于复杂语法,学了一遍感觉用处不大,不做说明

SFINAE 则完完全全是语言设计不佳导致的结果,没必要学习,如果有类似需求,直接使用 C++20 的约束。

约束和概念

C++ 20 引入了 conceptrequires

requires expresion

// 条件都满足返回true,不满足返回false
requires(Type1 param1, Type2 param2, ...){
  a + b; // 比如a和b是int,那么这个语句合法,这个条件通过
  {a*1} -> std::convertible_to<T>; // 这是复合要求,花括号里检查语句是否合法,箭头右边检查结果是否合法
  typename Type1::iterator // 类型要求,检查类型中是否有要求类型
  requires std::is_floating_point_v<T> // 可嵌套
}

requires clause

template <typename T>
requires xxx && xxx // 满足所有
requires xxx || xxx // 满足一个
concept a = xxx // 把条件作为一个概念
func...

模板推导规则

没必要记,看 lsp 的显示结果即可。

初始化

以下几种:

int n = 1; // 等号
int n(1)   // 括号
int n{1}   // 列表
int n;     // 默认

关于列表初始化需要注意这两点:

  1. auto 会将大括号初始化列表推断为 std::initializer_list 类型,而非预期的目标类型。(C++14 起修改后才合乎常理,但必须是单变量)
  2. C++ 的初始化列表必须在编译期完全展开,保证从左向右传入参数。
  3. C++11 后,对于类来说 T x{...}; → 列表直接初始化,而 T x = {...}; → 列表拷贝初始化

最推荐列表初始化

其余新语法

newdelete,字面意思

using 相较于 typedef 更简单,所以更推荐

typedef int (*process)(void *);//定义了一个函数指针类型 process
using NewProcess = int(*)(void *);//定义了一个函数指针类型 NewProcess

const 在 cpp 是默认内部链接。

const int *const p 这个变量的声明,这样去解析:

  1. p: 它名叫 P
  2. const p :p 是常量,它不可变。
  3. *const p : p 是常量指针,它不可变。
  4. int *const p :p 是常量指针,它不可变,它指向一个 int。
  5. const int *const p :p 是常量指针,它不可变,它指向一个常量 int。

constexprconsteval 一个是可以在编译期求值,一个是在 C++20 引入的必须在编译期求值

被这两个修饰的函数可以使用递归,在 C++17 后可以在 if 后面作为编译器判断

C++17 后,if/switch 可直接定义变量,下例

if (const std::vector<int>::iterator itr = std::find(vec.begin(),vec.end(),3);
    itr != vec.end()) {
    *itr = 4;
}

C++11 后,for 允许对数组类或容器类(vector,array 等等)的每个元素执行相同的操作:

double prices[5] {1.222.223.334.445.55};
// 如果要修改元素则需要 for(double &x : prices)
for (double x : prices)
    cout << x << std::endl;

还有基于列表和范围的初始化方式:

for(int x : {1,2,3})
    cout << x << "";
cout << endl;

异常处理

noexcept

C++11 后将异常的声明简化为以下两种情况:函数可能抛出任何异常; 函数不能抛出任何异常.

使用 noexcept(代表绝对不会抛出异常)修饰过的函数如果抛出异常,编译器会立即终止程序运行。未声明代表可能,需要用 try,catch,throw 来解决.

noexcept 还能使用表达式来判断.

在明确不会抛出异常,比如: 简单的数值计算函数,析构(析构已经隐式 noexcept 了),移动操作相关函数,频繁调用或者性能敏感的函数,接口(给调用方了解情况,这是必要的),最好使用.

如果有个函数调用其他函数,并且不知道调用的可否,那就是异常中立,就不要使用.

try,catch,throw

用到时候在搜索,这块 C++ 有一堆标准类型,没必要花太多精力

STL

容器

容器适配器

迭代器

迭代器类型缩写主要运算符典型用途
输入迭代器InIt* -> ++ == !=只读单向遍历,如从输入流读取
输出迭代器OutIt* ++只写单向输出,如写入容器或输出流
前向迭代器FwdIt* -> ++ == !=可多次单向遍历,读写皆可
双向迭代器BidIt* -> ++ -- == !=可前后移动遍历
随机访问迭代器RanIt* -> [] ++ -- + - += -= == != < > <= >=像指针一样随机跳转,支持下标和比较

除了上面五种基本迭代器外,STL还提供了一些迭代器的适配器,用于一些特殊的操作,如:

对于vectordeque以及basic_string容器类,与它们关联的迭代器类型为随机访问迭代器 对于listmap/multimap以及set/multiset容器类,与它们关联的迭代器类型为双向迭代器 queuestackpriority_queue容器类,不支持迭代器!