0%

C++11新标准

variadic template

就是参数个数随意的模板,定义任意个参数的模板,可以是任意类型
而且可以依据参数重载,关键字 typename… 也可以实现递归,下面的这两种给方式可以并存
相较来看,第一个定义比较特化,第二个比较泛化,所以就会,如果传入参数是 1 + x 那么就调用第一个,否则调用第二个

1
2
3
4
5
template <typename... types>
void func(size_t seed, types& ...args){return func(args...);}
template <typename... types>
void func(types& ...args){}

Space in Template Expressionn**ullptr & std::nullptr_t

左边是一个 object,就是0的空的指针,允许使用 null 代替0

auto

auto 表示不知道是什么类型的变量,可以把任意变量赋予 auto 变量
编译过程中,编译器会自动识别类型, 但是有时候不能使用 auto,使用 aut 必须初始化,不然不能使用(由于编译器不知道类型以及大小,不好分配空间)

1
2
3
verctor<string> v;
auto pos = v.begin();
auto l = [](int x)->bool{}; // 后面的参数表示 lambda 函数,无法确定类型就使用auto

lambda 函数

[] 开始的变量
[] 开头表示 lambda 函数,可以在函数内部定义,相当于一个仿函数

Uniform Initialization

一致性的初始化
一致初始化,一般发生在大括号,等于号
可以直接在声明变量之后加上大括号,里面放入初始化的值
编译器在看到{…}之后,就会做出一个 initializer_list<type>,关连着一个 arrary<type,n>, 调用函数时, arrary内的元素可以被编译器分解,逐一传给函数。但是如果该函数参数是一个 initializer_list<T>,调用者却不能够给予整个 T 参数,然后然后以为它们会被自动转化为一个 initializer_list<T> 传入

1
2
3
4
5
int values[] {1,2,3};  //-> 内部有一个arrary<int, 3>
verctor<int>v {1,2,3,4}; //-> 内部有一个arrary<int, 4>
verctor<string> cities {"Berlin","NewYork"}; // ->内部有一个arrary<string, 2>
complex<double> c {4.0,3.0}; // ->内部有一个arrary<float,2>
// 上面的代码都关连着一个arrary<type,n>

Initializer Lists

1
2
3
4
5
6
7
8
9
10
11
12
13
int i;    // 未定义初值
int j{}; // 定义为0
int *p; // 未定义初值
int *q{}; // 定义为 nullptr

int x1(5.3); // 可以把5.3转型为5
int x2 = 5.3; // 可以把5.3转型为5
int x3{5.3}; // error 大括号无法转型
int x4 = {5.3}; // error 大括号无法转型
char c1{7}; // ok
char c2{999999}; // 超出界限也不可以
std::vector<int> v1{1, 2, 3, 4, 5}; // OK
std::vector<int> v2{1, 2, 3, 4, 5.3}; // error 无法转型

initializer_list<T>
存在于仓库 <bits/stl_algo.h>中,是一种变量,就是定义时能够接收任意类型和数量的变量,但是所有变量类型都一样

1
initializer_list<T> a{x,x,x,x,x};

其实它的背后是一个 array<T, n> 一个数组

explicit
编译器在编译时,计算时会自动把数值转化为与结果相应的类型

1
2
3
4
5
6
7
8
9
10
11
12
class Complex
{
int real,imag;
// explicit
Complex(int re, int im = 0):real(re),imag(im){};// 单一形参
Complex operator+(const Complex& x)
{
return Complex((real+x.real), (imag + x.imag));
}
}
Complex c1(1,2);
Complex c2 = c1 + 5;

如果不加上 explicit 时,编译器就会把 5 转化为复数,但是只能调用单一形参的构造函数,然后相加,但是加上 explicit 之后,禁止调用Complex的构造函数转化,所以相加时就会报错

_iter_less_iter()
位于标准库 <bits/predefined_oops.h> 中,是一个比较大小的函数,返回一个结构体 _Iter_less_iter是一个有重载 () 的结构体,使用 < 比较大小

range-based for statement

存在于c++中的语法,类似于python

1
2
3
4
5
for(auto i :initlist)
{
cout << i;
}
cout << endl;

default & delete

C++11允许添加 =default 说明符到函数声明的末尾,以将该函数声明为显示默认构造函数。这就使得编译器为显示默认函数生成了默认实现,它比手动编程函数更加有效。
例如,每当我们声明一个有参构造函数时,编译器就不会创建默认构造函数。在这种情况下,我们可以使用default说明符来创建默认说明符。

默认函数需要用于特殊的成员函数(默认构造函数,复制构造函数,析构函数等),或者没有默认参数。例如,以下代码解释了非特殊成员函数不能默认

=default 就是定义函数为默认函数
=delete 就是定义函数已经被禁用,不能再使用
=0 是一种定义纯虚函数的方法,如果在父类结构体中声明函数=0,那么子类想要使用这个函数就必须重写

big-five

  1. Desconstructor 构造函数
  2. copy constructor 复制构造
  3. operator= 拷贝赋值函数
  4. copy constructor 移动构造函数 class_name(class_name && )
  5. Destructor 析构函数

NoCopy

在结构体中,所有的拷贝操作都被删除,不能进行拷贝,就是把编译器默认的拷贝函数给删除了

1
2
nocopy(const nocopy&) = delete;
nocopy &operator=(const nocopy&) = delete;

NoDtor

就是在结构体中的析构函数被删除,无法删除生成的变量

1
2
3
NoDtor() = default;
~NoDtor() = delete;
delete p; // 最后只能调用delete函数来删除比那两

PrivateCopy

此结构体中不可以被拷贝,但是可以被 friendmember 来拷贝,如果想要完全禁止拷贝操作,不但必须把拷贝函数放进 private,而且不可以定义它

Alias Template (template typedef)

不能对 Alias Template 进行特化,不能对这个重定义之后的名字做特化

1
2
3
4
5
6
7
template <typename T>
using vec = std::vector<T, MyAlloc<T>>;
vec<int> coll;
------------
#define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;
typedef template<typename T> std::vector<T, MyAlloc<T>> Vec<T>;
// 上面两种用 define 和 typedef 情况都达不到需求,而且,typedef 是不能接受参数的

template template parameter

模板的模板参数

1
2
3
4
5
6
7
8
9
template<typename T, template<typename>class Con>
class Stack
{
public:
void Push(T item);
T pop();
private:
Con<T> container_;
};

其实就是再定义一个结构体时,传入的模板参数是一个带有模板参数的模板参数,通常在使用时,这个参数 Con 就会用作 class 类型,而且在这个 class 里,可以定义多种的 Con

Type Alias

就是对原来存在的类型名称进行重新定义名字

1
typedef zhengshu int;

上述操作就是把 int 重命名为 zhengshu

throw

1
2
3
4
void excpt_func()
{
throw(int double);
}

这个例子就是说,在函数声明之后,定义了一个动态异常的声明 throw(int, double) ,指出了 excpt_func 可能抛出的异常的类型
在函数抛出异常时,就会导致函数被依次展开,并依帧调用本帧种已经构造的变量的析构函数,来终止程序运行
c++11 中被 noexcept 代替

noexcept

1
2
void except_func() noexcept;
void except_func() noexcept(/*常量表达式*/);

这个常量表达式会被转化为 bool 类型的,如果是 true 就不会抛出异常,否则就会抛出异常。noexcept 表示修饰的函数不会抛出异常,如果修饰的函数抛出了异常,那就会调用 std::terminate() 函数来终止程序的运行,比 throw() 效率更高,相当于是这个函数不能抛出异常

override

就是覆盖掉已经存在的一个方法,并且对其进行重写,从而达到不同的作用,通常在类的继承中搭配虚函数使用,一般就是子类对父类的虚函数进行重写,再次调用时就使用子类的该函数方法,对于普通的函数不能重写

  1. 纯虚函数(必须重写,否则不能使用该函数) 只有接口被继承
    virtual void func() = 0;
  2. 普通虚函数(可以重写)
    virtual void func();

final

就是一种禁用派生的关键字

1
2
3
4
5
// 1. 禁用重写 (就是该函数即使是虚函数也不能重写)
virtual void func() final;
// 2. 禁用继承
class fun final
{};

decltype

decltype 相当于 typeof(),可以使得编译器找到表达式的类型

1
2
template <typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);

就是获取数据的类型,充当返回类型,也可以用于 lambda 表达式,面对 lambda表达式 我们只有object, 没有类型,用 decltype 能获得表达式的类型

lambda 函数

  • 表达式把函数当作对象,把这个表达式当作对象使用
  • 表达式可以赋值给变量,也可以当作参数传给真正的函数
  • 在定义包含 lambda 函数的不定序的容器时或者定义一个排序准则,会用到 lambda 类型,这时候需要用到 decltype
1
[/*captures*/]/*<tparams>*/(/*param*/)-> /*retType*/{/*body*/};
  1. captures 捕获变量列表,就是在定义 lambda函数 的那个作用范围内的变量可以通过[]传入使用
    • [epsilon] 将参数 epsilon 按值传递
    • [&epsilon] 按引用传递
    • [&] 按引用捕获表达式中使用到的所有变量
    • [=] 按值捕获表达式中使用到的所有变量
    • [&,epsilon] 除了 epsilon 以外,其他都按引用传递
    • [=,&epsilon] 除了 epsilon 以外,其他都按值传递
    • 如果使用按值捕获,但是在 lambda 表达式中还想要修改拷贝的值,可以使用 mutable 关键字修饰,但是修改也不影响表达式之外的值
  2. tparams 模板参数列表,可以省略
  3. params 参数,传入的列表参数,就是形参,没有参数可以省略不写 ()
  4. retType 返回值类型,两种情况下可以不写 :
    • 无返回值
    • 返回值可以被编译器推导
    • 如果使用 lambda表达式 中含有多个返回值,可以指定返回值类型
  5. body 函数体,用于编写具体函数逻辑的地方

lambdas 函数

1
[] () mutable throwSpec -> retType {}
  1. 第一段 []lambdas表达式 的标志性符号,看见 此符号就应该知道,这是lambdas表达式。‘[]’内可以使用外部变量
  2. 第二段 () 里可以放入参数。
  3. mutable 关系着 [] 里的数据是否可以被改写。
  4. throwSpec 表示抛出异常。
  5. retType 是描述 [] 的返回类型。
  6. {} 是函数本体

标准库

Rvalue reference

右值实现了转移语义和精确传递,主要目的:

  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  2. 能够简洁明确的定义泛型函数
    判断左值还是右值时,看能不能对表达式取地址,如果能,那就是左值,否则就是右值,一般来说左值引用使用符号 & ,右值引用使用符号 &&
    std::move()作用就是将左值当作右值来处理,一旦使用 move 函数将左值变为右值就不能再次使用了,就是移动构造函数
    事实上我们是做了一个浅拷贝(shallow copy)。至于要将之前的指针置为空的原因在于,我们的类会在析构的时候 delete 掉我们的数组。那么我们浅拷贝出来的这个对象的成员变量(arr指针)就变成了一个悬挂指针(空指针)
    深拷贝
    当一个类里面有指针时,深拷贝就是不仅复制指针,而且指针所指向的内容也复制一份
    浅拷贝
    当一个类里面有指针时,深拷贝就是不仅复制指针,但是指针所指向的内容不会复制,两个指针指向同一个内容

perfect Forwarding

完美转发
std::forward<T>(),作用就是将数据的左右值类型保留

是C++11中引入的一种编程技巧,它允许在编写泛型函数时保留参数的类型和值类别,从而实现高效且准确地传递参数。这种技术通过使用右值引用和模板类型推导,使得在函数中以原始参数的形式传递参数给其他函数时,不会发生不必要的拷贝操作,从而提高性能

完美转发的一个关键概念是引用折叠,即一个声明的右值引用实际上是一个左值。这为参数转发(传递)提供了可能。例如,在C++中,我们可以使用 std::move 将左值转换为右值引用,从而在需要时将左值当作右值使用,避免不必要的拷贝操作。

在函数模板中,完美转发意味着完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数。例如,在模板函数 IamForwording 中,我们希望将参数按照传入时的类型传递给目标函数 IrunCodeActually ,而不产生额外的开销。为了实现这一点,我们需要传递的是一个引用类型,因为引用类型不会有额外的开销。同时,我们需要考虑转发函数对类型的接收能力,因为目标函数可能需要即接收左值引用,又接受右值引用。

总结来说,完美转发是一种在C++中实现高效参数传递的技术,它通过使用右值引用、模板类型推导和引用折叠等特性,确保参数传递时的性能优化和类型匹配。

universal reference

通用引用
构成通用引用有两个条件:

  1. 必须满足 T&& 这种形式
  2. 类型必须是能够通过编译器推断得到的

可以构成通用引用的情况:

  1. 函数模板参数

    1
    2
    3
    template<typename T>
    void func(T a);

  2. auto 声明

    1
    2
    auto a = ....;

  3. typedef 声明,相当于是重命名一样

    1
    typedef unsigned int u32; // 将无符号整型重命名为 u32
  4. decltype 声明

    autodecltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

    1
    2
    3
    auto varname = value;
    decltype(exp) varname = value;

    其中, varname 表示变量名, value 表示赋给变量的值, exp 表示一个表达式。

    auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。

    另外, auto 要求变量必须初始化,而 decltype 不要求,因为已知初始类型。

    主要依据就是引用类型合成

    1. T& & => T&
    2. T&& & => T&
    3. T& && => T&
    4. T&& && => T&&

tuple

就是组,一堆不同类型的东西可以储存到一起,储存为 tuple


面向对象的高级编程

C++ 98 (1.0)

C++ 03 (TR1, Tecgnical Report 1)

C++ 11 (2.0)

C++ 14

c++ 包括语言和标准库,程序对象就是包含数据和数据处理数据方法,c语言中的数据可以被任意函数处理,c++中的数据与可以处理这个数据的方法封装在一起,就是 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef _XXX_H_   // 防御式的声明,防止重复引入产生报错
#define _XXX_H_
#pragma once // 跟上两行的效果相同
// 前置声明
// 类声明
// 1. class head
// 2. class body 有些方法在函数内定义,有些在函数外定义
// inline 函数 : 在class内部定义,不在本体中定义但是想要作为inline函数需要加关键字inline,函数太复杂无法做成inline函数,是否编译为inline函数也不一定
// public:公开,所有的函数方法 private:私有,一般把数据封装,不可访问,但是可以调用class内部方法来改变 protected: 保护
// 构造函数: 函数名称与类的名称相同,可以有参数并且可以有默认值,只有构造函数才有的(初值列,初始列),不需要有返回类型,不需要有返回值,也可以重构构造函数
// singleton类 这种类的构造函数放进private中,创建时只能调用该类的函数方法创建
// const member functions 常量成员函数,定义函数时使用const关键字时表示该函数不能被改变
// return by value 和 return by reference
// 第一个是返回值,第二个是返回引用,返回值类型是xx&
// return by value 会产生临时物体。
// return by reference看起来象指针,但不会返回一个为NULL的引用,所以这点倒比指针安全,但也要注意引用对象的作用范围。返回对象的地址,但是不能返回局部变量的引用
complex(double r, double i):re(r),im(i){} // 这种方法只能在构造函数中使用,效率更高
// 类定义
#endif

友元(friend)

在定义 class 中,函数声明前加 friend 关键字,那么这个函数就可以任意调用 private 中的值,也就是打破了封装,而不是使用 class 内的函数来拿到 private 中的值,这样的函数执行更快
同一个 class 里的各个 object 互为友元

  1. value 必须是 private 中的
  2. 参数尽可能的使用 reference 引用 来传递, (const)
  3. 返回值尽量用 reference 来传递,但是不能传递局部变量的引用
  4. class 中的函数要加 const
  5. 构造函数特殊语法

return by reference

表示返回值是引用的形式,return by reference 传出者无需知道接收者是以什么形式接收的,但是只用指针来传输,就必须知道传出的类型

1
2
3
4
5
6
7
8
int& compute(int* a) {
return *a;
}

int main(int argc, char const* argv[]) {
int b = 10;
int a = compute(&b);
}

操作符重载

对于操作符 (+=,-=,*=....) 重载时,引入参数时,第一个参数就是 this,第二个参数就是引用的另一个值,返回时可以用 return by reference

return by reference 传出者无需知道接收者是以什么形式接收的,但是只用指针来传输,就必须知道传出的类型

但是对于操作符 (+,-,...) 重载时,返回值必须是一个 local object,不能用 return by reference 传出数据

临时对象

typename() 创建临时对象,它的生命到下一行就结束了,没有名字,一般在 return 中使用

  1. 建立一个没有命名的非堆(non-heap)对象,也就是无名对象时,会产生临时对象
  2. 构造函数作为隐式类型转换函数时,会创建临时对象,用作实参传递给函数
  3. 函数返回一个对象时,会产生临时对象。以返回的对象最作为拷贝构造函数的实参构造一个临时对象

拷贝

浅拷贝只拷贝指针,深拷贝就是拷贝指针内的内容,但是得提前准备好内存地址,先准备一块空间,再把要拷贝的内容考进去

1
2
3
4
5
inline String::String(const String &str)
{
my_data = new char[strlen(str.my_data) + 1];
strcpy(my_data, str.my_data);
}

拷贝赋值

1
2
3
4
5
6
7
8
9
inline String &String::operator=(const String &str)
{
if (this == &str) // 目的是为了检测是否是自己赋值
return *this;
delete[] my_data;
my_data = new char[strlen(str.my_data) + 1];
strcpy(my_data, str.my_data);
return *this;
}

拷贝构造

1
2
3
4
5
6
7
8
class Complex
{
public:
double real, imag;
Complex(double &r, double &i) {
real= r; imag = i;
}
};

stack(栈) heap(堆)

stack 是存在于某个作用域的一个内存空间,当你调用函数时,函数本身就会形成一个 stack 来放置它接收到的参数,以及返回地址,在函数体内声明的任何变量其所使用的内存都取自栈

heap system heap 是指由操作系统提供的一块全局内存空间,程序可以动态分配,从其中获得若干块,但是,操作完之后必须释放掉,不然导致内存泄漏

stack objects / static local objects / global objects / heap objects

  1. stack objects 栈变量

    1
    2
    3
    4
    class Complex{};
    {
    Complex c1(1,2);
    }

    动态参数,生命周期只要离开作用域就会自动调用析构函数清理

  2. static local objects 静态本地变量

    1
    2
    3
    4
    5
    class Complex{};
    int main()
    {
    static Complex c2(1,2);
    }

    这就是所谓的静态参数,生命在作用域结束之后依然存在,直到整个程序结束

  3. global objects 全局变量

    1
    2
    3
    4
    5
    6
    class Complex{};
    Complex c3(1, 2);
    int main()
    {
    c3 = {1, -1}
    }

    其中 c3 就是一种全局变量,生命在整个程序结束之后结束,也可以看作是静态变量,不同之处在于可以被所有函数调用

  4. heap objects 指针变量

    1
    2
    3
    4
    5
    6
    class Complex{};
    ....
    {
    Complex *p = new Complex;
    delete p; // 重要
    }

    p 所指的就是 heap object,但是在程序结束时没有 delete 掉,那么指针就会被释放,但是指针所指的内存中的内容不会被删除,所以在 new 之后一定要 delete ,以免造成内存泄漏

new

先分配 memory 一块空间,再调用 ctor

1
2
3
4
5
6
Complex *p = new Complex;
// 编译器转化为 :
void* mem = operator new (sizeof(Complex)); // 分配类型
p = static_cast<Complex*>(mem); // 转型动作
p->Complex::Complex(1,2); // 构造函数,通过指针调用其构造函数
// 上面调用的一步实质上就是 : Complex::Complex(p, 1, 2);

delete

1
2
3
4
5
delete p;
// 在编译器转化为:
String::~String(p); // 先调用析构函数
operator delete(p); // 释放内存
// 释放内存函数的内部其实就是调用 free(p) 函数

VC中内存的分配

在 vc 中,内存分配都是按照 16 的倍数来分配的,不足的补为16的倍数,而且在分配的区域块的第一个内存表示区块的大小,例如分配64位就会用 0x0040 来表示,一般最后一位 1 表示分配出去,0 表示未分配

arrary new 要搭配 arrary delete

不然就会导致头指针那一块内存被释放,但是剩下的内存无法释放并且找不到指针,就会造成内存泄漏

例如:

1
2
3
String* p = new String[3];
delete[] p; // 这样写会把所有都删掉,会调用3次析构函数,分别把动态分配的内存删掉
delete p; // 这样写就只会删除1个,只调用1次析构函数,只删除1次,就会有两个不被删除,造成内存泄漏

& 出现在 type 类型 之后表示引用,出现在 object变量 之前,表示取地址

static

在创建函数或者变量之前加上 static 表示创建静态的函数和变量
静态的函数只能处理静态的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
class Account
{
public:
static double m_rate;
static void set_rate(const double& x){m_rate = x;}
}
double Account::m_rate 8.0; // 要不要给初值都可以
// 调用static的方法:
// 1.
Account::set_rate(1.0);
// 2.
Account a;
a.set_rate(1.0);

类似于内存池:把 ctors 放进 private

1
2
3
4
5
6
7
8
9
10
class A
{
public:
static A& getInstances(return a;); // 外界可以通过这个函数得到 这个惟一的结构体 a
setup(){....}
private:
A(); // 把构造函数放进private中,表示外界不能创建
A(const A& rhs);
static A a; // 单建单体单立,只存在唯一的 a,但是没有使用的话就会显得浪费
}

下面这种创建单个唯一的a变量更好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public:
static A& getInstances();
setup(){....}
private:
A(); // 把构造函数放进private中,表示外界不能创建
A(const A& rhs);
}
A& A::getInstances()
{
static A a;
return a;
}

class template 类模板

需要在创建类时在之前声明 template<typename T> ,在类中要使用某种数据类型就可以用T代替

1
2
3
4
template<typename T> // 或者 template<class T> 都一样
class Complex{};
Complex<int>(1,2);
Complex<double>(1.0,2.0);

表示 T 还没有指定,可以创建不同类型的数据,会产生两段完全一样的代码,即代码膨胀,在编译时就会对传入的数据类型进行推导,然后把函数内的T全部改为指定的数据类型

namespace 命名空间

  1. using directive
    就是在函数开始之前声明 using namespace xx
  2. using declaration
    就是在函数前声明只使用某个命令 using std::cout

更多细节:

  1. operator type() const;
  2. explicit complex(…):initialization list{}
  3. pointer-like object
  4. function-like object
  5. Namespace
  6. template specialization
  7. Standard Library
  8. variadic template
  9. move ctor
  10. Rvalue reference
  11. auto
  12. lambda
  13. range-base for loop
  14. unordered contaioners

结构体的复合,继承,委托

  1. 结构体的复合

    其实就是对原来结构体的部分功能的重新使用,感觉像是结构体中套结构体,例如:

    1
    2
    3
    4
    5
    6
    7
    template <class T>
    class queue{
    protected:
    deque<T> c;
    public:
    bool empty() const {return c.empty();}
    }

    构造和析构函数
    构造由内到外:
    外部的结构体首先调用内部的结构体的构造函数,然后才执行外部的构造函数

    1
    2
    Container::Container(...):Component() {};

    析构由外到内:
    析构函数执行时首先调用外部结构体的析构函数,先把自己的任务做完,再调用内部函数的析构函数

    1
    2
    Container::~Container(...) {... ~Component();};

  2. 结构体的委托(Delegation)

    就是在结构体中定义一个另一个结构体的指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class StringRep;
    class String
    {
    public:
    ....
    private:
    StringRep* rep; // 记得析构函数 delete
    }

  3. 结构体的继承

    可以继承父类的方法,子类的对象有父类的成分,模板方法

    1
    2
    struct list_node{};
    struct listlink : public list_node{}; // public 继承

    构造由内而外,析构由外而内,父类的 dtor 必须是 virtual 虚函数,否则会出现未定义的行为。

虚函数
虚函数后面加 = 0,表示这个函数为纯虚函数,纯虚函数的一般形式: virtual 函数类型 函数名 (参数表列) =0
特点

  1. 纯虚函数没有函数体;
  2. 一个类里如果包含 =0 的纯虚函数,那么这个类就是一个抽象类
  3. 抽象类不能具体实例化(不能创建它的对象),而只能由它去派生子类
  4. 在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用。
    non-virtual(非虚)函数:不希望子类结构体重新定义它
    virtual(虚)函数:希望子类结构体重新定义,并且在父类中已有默认定义
    pure virtual(纯虚)函数:希望子类一定要重新定义它,并且在父类中没有默认定义,但是其实是可以有默认定义的
1
2
3
4
5
6
7
8
class shape
{
public:
int object() const; // non-virtual
virtual void error(const std::string& msg); // impure virtual
virtual void draw() const = 0; // pure-virtual
};
class rectange:public shape{...};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
document::onfileopen()
{
...
serialize();
...
}
class mydoc : public docunment
{
virtual void serialize(){};
}
int main()
{
mydoc mydoc;
mydoc.onfileopen(); // 其实是执行 document.onfileopen()
}

上面的函数在运行的时候,子类结构体变量可以直接调用父类结构体的函数,函数中包含着虚函数,并且在子函数中有定义,就会跳到子函数中执行该函数(相当于执行命 mydoc.serialize()),再跳回父类的函数中

转换函数 conversion function

1
2
3
4
5
6
7
8
9
10
11
12
class Fraction  // 分数
{
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){}
operator double() const{return (double)(m_numerator / m_denominator);} // 分数转为小数
private:
int m_numerator; // 分子
int m_denominator; // 分母
}
Fraction f(3, 5);
double b = 4 + f; // 返回值就是4.6 Fration的double函数把f转化为double 类型的数据

non-explicit-one-arguement ctor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fraction  // 分数
{
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den){} // non-exciplict
Fraction operator+(const Fraction& f)
{
return Fraction(this->m_numerator, )
}
private:
int m_numerator; // 分子
int m_denominator; // 分母
}
Fraction f(3, 5);
Fraction b = f + 4; // 会调用 non-explicit ctor函数将4转化为Fration(4,1),然后调用operate+

上面的例子,如果函数 operate double 也存在的话,电脑会认为有两种方法,会报错

  1. 把f转化为 double,与4相加,再转化为 Fraction
  2. 把4转化为 Fraction,与f相加

这时就需要在构造函数之前加上 explict,这时候在加法中就不会把4转化为 Fraction,而是先把f转化为 double,再加法,在转化

智能指针 pointer-like classes

像指针的结构体,需要重载 *-> 符号

1
2
3
4
5
6
7
8
9
10
template <typename T>
class pointer
{
private:
T* ptr;
public:
~pointer(){delete ptr;}
void operator*(){return *ptr;}
void operator->(){return ptr;}
};

C++标准库中也有智能指针

1
2
3
#include <memory>
std::shared_ptr<T> p;
// get返回原指针

仿函数

  • — 其实就是一些小的 class 组合而成,而且这些 class 里面有对小括号的重载
1
2
3
4
5
6
class fillter
{
void operator()(int a){std::cout<<a;}
}
int b=0;
a(b);

模板:

模板的匹配规则:

  1. 最优化的优于次特化的,即模板参数最精确匹配的具有最高的优先权
  2. 非模板函数具有最高的优先权。如果不存在匹配的非模板函数的话,那么最匹配的和最特化的函数具有高优先权
    其实就是精确度越高优先权越高
  3. 类模板

    1
    2
    3
    4
    5
    6
    template <typename T>
    class A
    {
    T m;
    T getm(){return m;}
    }
  4. 函数模板

    1
    2
    3
    4
    5
    template <typename T>
    void getvalue(T a)
    {
    ...
    }
  5. 成员模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A
    {
    template <typename T>
    T getx(T x)
    {
    return x;
    }
    }

  6. 模板特化

    函数模板特化是在一个统一的函数模板不能在所有类型实例下正常工作时,需要定义类型参数在实例化为特定类型时函数模板的特定实现版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 类模板特化
    template <class T>
    class stack {};
    template < >
    class stack<bool> {};

    // 函数模板特化
    template <class T>
    T mymax(const T t1, const T t2)
    {
    return t1 < t2 ? t2 : t1;
    }
    template < >
    const char* mymax(const char* t1,const char* t2)
    {
    return (strcmp(t1,t2) < 0) ? t2 : t1;
    }

  7. 模板偏特化(局部特化)

    对于有多个模板参数的函数或者类,只对其中一个或几个模板参数进行特化处理,但是没有全部进行特化处理,这样就是模板偏特化

  8. 模板泛化

  9. 模板模板参数
    使用带模板参数的对象来作为模板

    1
    2
    3
    4
    5
    6
    template <typename N,template<typename T>container>
    class A
    {
    container<T> c;
    }

  10. 数量不定的模板参数

    1
    2
    3
    template <typename T,typename... Args>
    void abs(T& a, Args&... args);

vptr(虚指针) vtbl(虚函数表)

https://img-blog.csdnimg.cn/20210323151830567.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xfWV9GZWk=,size_16,color_FFFFFF,t_70#pic_center

虚指针是一个地址值,以该地址作为起始地址的一片单元格内部存放着各个虚函数地入口地址(这个函数地址也可以相当于是一个指针),这一片内存就是虚函数表,虚指针就是指向指针的指针
对于不同的子类所复写的虚函数也都存放在虚函数表中,这个虚函数表应当是自己的虚函数表,不是父类的虚函数表,对于不同类型的对象会调用不同的虚函数。但是如果子类没有重写父类的虚函数,并且父类的虚函数也不是纯虚函数,那么这个子类的虚函数表调用就是调用的是父类的虚函数表

https://img-blog.csdnimg.cn/90f3d3f0d7f34b2c87329fd00f4cbcf3.png

this(对象模型)

首先,对于类的开辟空间,如果一个空的类,系统也会默认给类分配为1个字(32位)的空间,而且成员函数的定义不影响类的内存大小
this是类中指向这个调用非静态成员函数的对象,this指针不需要自己手动声明或定义,它是被系统定义的
作用

  1. 用于区分形参和成员变量名
  2. 用于返回类的本身
  3. 如果我们创建一个类的空指针,我们依然可以调用不涉及成员变量的类的函数,但是不能访问带有成员变量的成员函数,成员变量与具体的对象有关

常函数
就是类里面的函数不能修改成员变量的特殊函数,除非成员变量之前加上关键字 mutable ,常函数本质上是限制了 this 指针,将指针由 typename *const this 修改为 const typename *const this 让其不能对指向的内容进行修改,常用语法是 返回类型+函数名()+const

常对象
常对象就是在对象前加 const,常对象不能对成员变量进行修改,除非成员变量前加关键字 mutable,并且常对象只能调用常函数,因为通过非常函数调用有可能改变成员变量,常变量语法是 const+类型名+变量名

const

当成员函数的 constnon-const 两个版本都存在,那么 const object 只会调用 const 版本, non-const object 只能调用 non-const 版本

const object(data members 不变) non-const object(data members 可变动)
const member function(保证不更改data members) true true
non-const member function(不保证不更改data members) false true
函数形参使用const来修饰的话,既可以引入常量也可以引入非常量
1
2
const String str("hello world");
str.print(); // print 不是const函数,在定义的时候可以定义为const 成员函数

mutable

是可变的,易变的,与 const 相反,这个关键字是为了突破 const 的限制而设计的,被 mutable 所修饰的变量,将永远处于可变的状态,即使在 const 函数中也是可变的

1
2
3
4
5
6
7
class A {
mutable int num;
public:
void increase() const {
num++;
}
};

new delete

new 先分配内存 再调用构造函数
delete 先调用析构函数 再释放内存
可以重载 newdelete 函数

对于函数重载

1
2
3
4
template <typename... types>
void func(size_t seed, types& ...args){}
template <typename... types>
void func(types& ...args){}

第一种比较特化,如果传入参数的话,优先考虑第一种情况

nullptr and std::nullptr_t

nullptr 表示0或者空指针,nullptr 的类型是 std::nullptr_t

auto 变量

auto 表示不知道是什么类型的变量,可以把任意变量赋予 auto 变量,编译过程中,编译器会自动识别类型

1
2
3
verctor<string> v;
auto pos = v.begin();
auto l = [](int x)->bool{}; // 后面的参数表示 lambda 匿名函数,无法确定类型就使用auto

auto keywiord

auto 关键字来定义函数的返回值

uniform initializtion

一致初始化,一般发生在大括号,等于号,可以直接在声明变量之后加上大括号,里面放入初始化的值,编译器在看到 {...} 之后,就会做出一个 initializer_list<type> ,关连着一个 arrary<type,n>, 调用函数时, arrary 内的元素可以被编译器分解,逐一传给函数。但是如果该函数参数是一个 initializer_list<T> ,调用者却不能够给予整个 T 参数,然后然后以为它们会被自动转化为一个 initializer_list<T> 传入

1
2
3
4
5
int values[] {1,2,3};  -> arrary<int, 3>
verctor<int>v {1,2,3,4}; ->arrary<int, 4>
verctor<string> cities {"Berlin","NewYork"};->arrary<string, 2>
complex<double> c {4.0,3.0};
// 上面的代码都关连着一个arrary<type,n>

函数的参数为 initializer_list<T>

对于 {"Berlin","NewYork"} 会形成一个 initializer_list<string> ,背后会有一个 arrary<string, n>

调用 vectir<string>ctors 时,编译器找到了一个 vector<string>ctor 接受 initializer_list<string>。 所有的容器都如此

函数的参数为单个的变量

对于 complex<double> c {4.0,3.0} ,会把 arrary<type> 中的每一个参数分开,以此执行 complex<type> ctor

complex<type> 并没有任何 ctor 接受 initializer_list<type>

initializer_list

1
2
3
4
5
6
7
8
9
10
11
12
13
int i;    // 未定义初值
int j{}; // 定义为0
int *p; // 未定义初值
int *q{}; // 定义为 nullptr

int x1(5.3); // 可以把5.3转型为5
int x2 = 5.3; // 可以把5.3转型为5
int x3{5.3}; // error 大括号无法转型
int x4 = {5.3}; // error 大括号无法转型
char c1{7}; // ok
char c2{999999}; // 超出界限也不可以
std::vector<int> v1{1, 2, 3, 4, 5}; // OK
std::vector<int> v2{1, 2, 3, 4, 5.3}; // error 无法转型
1
2
3
4
5
6
7
8
void print(std::initializer_list<int> vals)
{
for(auto p = vals.begin(); p!=vals.end();p++)
{
std::cout << "*p" << "\\n";
}
}
print({1,2,3,4}); // 依次打印输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class P
{
public:
p(int a,int b)
{
cout << a << b <<endl;
}
p(initializer_list<T>)
{
cout << "p(initializer_list)";
for(suto i :initlist)
{
cout << i;
}
cout << endl;
}
}
P p(1,2); // 1
P q{1,2}; // 2,参数为整个包,如果2不在了,就会拆解为单个的,调用1
P r{1,2,3}; // 2
P s = {1,2}; // 2

对于只包含少量的参数的函数,可以用{…args}传入多个参数

1
2
max({1,2,3,4,5});
min({1,2,3,4,5});

explicit 明确的 不允许转化类型

默认的编译器在编译时,计算时会自动把数值转化为与结果相应的类型

1
2
3
4
5
6
7
8
9
10
11
12
class Complex
{
int real,imag;
// explicit
Complex(int re, int im = 0):real(re),imag(im){};// 单一形参
Complex operator+(const Complex& x)
{
return Complex((real+x.real), (imag + x.imag));
}
}
Complex c1(1,2);
Complex c2 = c1 + 5;

如果不加上 explicit 时,编译器就会把 5 转化为复数,但是只能调用单一形参的构造函数,然后相加
但是加上 explicit 之后,禁止调用 Complex 的构造函数转化,所以相加时就会报错,这个关键字加在构造函数中

default & delete

C++11 允许添加 =default 说明符到函数声明的末尾,以将该函数声明为显示默认构造函数。这就使得编译器为显示默认函数生成了默认实现,它比手动编程函数更加有效。
例如,每当我们声明一个有参构造函数时,编译器就不会创建默认构造函数。在这种情况下,我们可以使用 default 说明符来创建默认说明符。
默认函数需要用于特殊的成员函数(默认构造函数,复制构造函数,析构函数等),或者没有默认参数。例如,以下代码解释了非特殊成员函数不能默认
default 就是默认函数,只针对特殊的函数,例如构造函数与析构函数
delete 就是禁用函数,可以用在任意函数上,与=0不同,=0只能用于虚函数中

big-three(big-five)

  1. Desconstructor 构造函数
  2. copy constructor 复制构造
  3. operator = 拷贝赋值函数
  4. copy constructor 移动构造函数 class_name(class_name && )
  5. Destructor 析构函数

NoCopy

在结构体中,所有的拷贝操作都被删除,不能进行拷贝,就是把编译器默认的拷贝函数给删除了

1
2
nocopy(const nocopy&) = delete; // 拷贝函数
nocopy &operator=(const nocopy&) = delete; // 拷贝赋值函数

NoDtor

就是在结构体中的析构函数被删除,无法删除生成的类的变量,而且这个变量依然在栈中,需要我们手动删除

1
2
3
NoDtor() = default;
~NoDtor() = delete;
delete p; // 最后只能调用delete函数来删除变量

PrivateCopy

此结构体中不可以被拷贝,但是可以被friendmember来拷贝,如果想要完全禁止,不但必须把拷贝函数放进private,而且不可以定义它

Alias Template (template typedef)

不能对 Alias Template 进行特化,不能对这个重定义之后的名字做特化

1
2
3
4
5
6
7
template <typename T>
using vec = std::vector<T, MyAlloc<T>>;
vec<int> coll;// 相当于 std::vector<T,MyAlloc<T>> coll
------------
#define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;
typedef template<typename T> std::vector<T, MyAlloc<T>> Vec<T>;
// 上面两种情况都达不到需求,而且,typedef 是不接受参数的

排序函数

排序函数一般都会规定排序方法,这个排序方法有多种形式

  1. 函数
  2. lambda 函数
  3. 类,前提是这个类里面必须有重载()的函数

引用&

相当于是一个常量指针

  1. 用于定义变量,必须在定义时初始化,并且之后不再改变(常量指针)
  2. 用于函数参数的类型限定,相当于是传入一个指针,可以更快并且内存也占用更少
  3. 可以作为函数返回值,返回一个常量指针

volatile 关键字

用于修饰变量,作用是告诉编译器不要对这个变量做过度的优化,并且这个变量是易变的,每次读取使用这个变量时,是从变量的地址直接读取的。

1
2
3
4
5
6
7
8
9
10
const volatile int a;
// 1. 本程序段中不能对a作修改,任何修改都是非法的,或者至少是粗心,编译器应该报错,防止这种粗心;
// 2. 另一个程序段则完全有可能修改,因此编译器最好不要做太激进的优化。
// “const”含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”。
// “volatile”的含义是“请不要做没谱的优化,这个值可能变掉的”,而并非“你可以修改这个值”。
// const和volatile这两个类型限定符不矛盾。
// const表示(运行时)常量语义:被const修饰的对象在所在的作用域无法进行修改操作,编译器对于试图直接修改const对象的表达式会产生编译错误。
// volatile表示“易变的”,即在运行期对象可能在当前程序上下文的控制流以外被修改(例如多线程中被其它线程修改;对象所在的存储器可能被多个硬件设备随机修改等情况)
// 被volatile修饰的对象,编译器不会对这个对象的操作进行优化。
// 一个对象可以同时被const和volatile修饰,表明这个对象体现常量语义,但同时可能被当前对象所在程序上下文意外的情况修改。

constexpr 关键字

修饰的变量在编译阶段就可以运算结束,程序中不能更改

数据可以存在只读区.嵌入式开发感知更强.

可以用在需要完整常量表达式的场合. 还能做一下简单计算.