跳转至

C++

1 数据类型

1.1 基本类型占字节数

基本数据类型

类型说明符 等价类型 按数据模型的位宽
C++ 标准 LP32 ILP32 LLP64 LP64
short
short int 至少 16 16 16 16 16
short int
signed short
signed short int
unsigned short
unsigned short int
unsigned short int
int
int 至少 16 16 32 32 32
signed
signed int
unsigned
unsigned int
unsigned int
long
long int 至少 32 32 32 32 64
long int
signed long
signed long int
unsigned long
unsigned long int
unsigned long int
long long
long long int
(C++11)
至少 64 64 64 64 64
long long int
signed long long
signed long long int
unsigned long long
unsigned long long int
(C++11)
unsigned long long int

1.2 POD(Plain Old Data)简单旧数据

1.3 TrivialType 平凡类型

1.4 聚合体

聚合体是下列类型之一:

  • 数组类型
  • 符合以下条件的类类型(常为 struct 或 union)
  • 没有私有或受保护的直接 (C++17 起) 非静态数据成员
  • 没有用户声明的构造函数 (C++11 前)
  • 没有用户提供的构造函数(允许显式预置或弃置的构造函数)(C++11 起)(C++17 前)
  • 没有用户提供、继承或 explicit 的构造函数(允许显式预置或弃置的构造函数)(C++17 起)(C++20 前)
  • 没有用户声明或继承的构造函数 (C++20 起)
  • 没有虚、私有或受保护 (C++17 起) 的基类
  • 没有虚成员函数
  • 没有默认成员初始化器 (C++11 起)(C++14 前)

1.5 位域

  • 参考: https://learn.microsoft.com/zh-cn/cpp/cpp/cpp-bit-fields?view=msvc-170
  • 定义:声明具有以位为单位的明确大小的类数据成员。相邻的位域成员可以打包成共享和跨过各个字节。
  • 语法:
  • 类型 标识符(可选) 属性(可选) : 大小
  • 类型 标识符(可选) 属性(可选) : 大小 花括号或等号初始化器 (C++20 起)
  • 规则:
  • 规则 1:位域的类型只能是整型或枚举类型。
  • 规则 2:位域不能是静态数据成员。
  • 分类
  • 有名位域:有标识符(变量名)
  • 无名位域:没有标识符(变量名),大小起到一个填充位的作用
  • 位域大小:只能是 0 或大于 0 的整数
  • 等于 0:只能用在无名位域中,表示下个位域应该在分配单元的边界开始。
  • 大于 0:表示位域将占有的位数
#include <iostream>

struct S1
{
    // 三位的无符号位域,允许的值有 0...7
    unsigned int b : 3;
};
struct S2
{
    // 通常会占用 2 个字节:
    // 3 位:b1 的值
    // 2 位:不使用
    // 6 位:b2 的值
    // 2 位:b3 的值
    // 3 位:不使用
    unsigned char b1 : 3, : 2, b2 : 6, b3 : 2;
};
struct S3
{
    // 通常会占用 2 个字节:
    // 3 位:b1 的值
    // 5 位:不使用
    // 6 位:b2 的值
    // 2 位:b3 的值
    unsigned char b1 : 3;
    unsigned int :0; // 开始新字节,边界大小4
    unsigned char b2 : 3;//从第5字节开始
    unsigned char :0; // 开始新字节,边界大小1
    unsigned char b3 : 1;//从第6字节开始
};
int main()
{
    S1 s = {0b00000111};
    std::cout << s.b << '\n';// 输出 7
    s.b=0b00001101; // 超过3bit的值存储不下
    std::cout << s.b << '\n'; // 输出5,(0b00000101)正式来说是由实现定义的行为

    std::cout << sizeof(S2) << '\n'; // 输出 2

    std::cout << sizeof(S3) << '\n'; // 输出 6
}

2 指针

2.1 指针概述

指针 p 是也是一个变量,变量 p 存的是一段地址数据(地址可能 4 位,也可能 8 位),*p 的作用是得到 p 的值(地址)所指向的数据,&p 可以获得变量 p 的地址。

  • 指针是变量:指针是一种特殊的变量,有自己的地址和值,与普通变量唯一不同是存储的值是地址。
  • 指针存的是地址
  string* str = new string("你好");
    cout << "new a string object :" << endl;
    cout << "指针str地址:"<<&str<<"指针str值:"<<str<<"指针str值指向的值:"<<*str<<endl;
  • 可能的地址值如下
地址 变量名
0F001100 pstr 0F001110
0F001110 *pstr Hello World
&pstr 0F001100
0F001200 str Hello World

2.1.1 指针的指针 int** pp

2.1.2 delete 指针后指针赋值 null?

2.2 const 和指针的关系

注:以下举例以 int 作为类型, 且 new 的数据未作 delete 处理

2.2.1 速记技巧

const int* p;//const指向的是*p,*p就是p指向的数据,故指向数据不能改变
int const* p;//同上
int* const p=NULL;//const指向的是p,p是指针,故p不能改变,但p指向数据可以改变

2.2.2 类型一:const 和 int

//类型一
    const int i1 = 2;//【Ⅰ】
    // i1 = 3;//error

    int const i2 = 2;//【Ⅱ】等价于【Ⅰ】
    //i2 = 3;//error 

2.2.3 类型二:const 和 int 和 *

    int* tpi = new  int(1);
    int** tppi = new int* (new int(1));

    //类型二
    const int* pi_11{ new int(2) };//【Ⅲ】pi_11指向的值为常量
    //(*pi_11) = 3;//error
    pi_11 = tpi;//success

    int const* pi_12{ new int(2) };//【Ⅳ】等价于【Ⅲ】
    //(*pi_12) = 3;//error
    pi_12 = tpi;//success
    *tpi = 3;
    int* const pi_13{ new int(2) };//Ⅴ】pi_13指针为常量
    (*pi_13) = 3;//success
    //pi_13 = tpi;//error

    const int* const pi{ new int(2) };//【Ⅵ】pi指针为常量,且pi指向的值也为常量
    //pi = tpi;//error
    //(*pi) = 3;//error

2.2.4 类型三:const 和 int 和 **

    int* tpi = new  int(1);
    int** tppi = new int* (new int(1));
    const int* pi_11{ new int(2) };
    //类型三
    int* const* const ppi1{ new int* (new int(2)) };//【Ⅶ】ppi1指针为常量,且ppi1指针的指针为常量
    //ppi1 = tppi;//error
    //(*ppi1) = tpi;//error
    (*(*ppi1)) = 3;//success

    const int* const* const ppi2{ new int* (new int(2)) };//【Ⅷ】ppi2指针为常量,且ppi2指针的指针为常量,且ppi2指针的指针指向的值也为常量
    //ppi2 = tppi;//error
    //(*ppi2) = tpi;//error
    //(*(*ppi2)) = 3;//error
    int const * const* const ppi3{ new int* (new int(2)) };//【Ⅸ】等价于【Ⅷ】

    int** const ppi4{ new int* (new int(2)) };//【Ⅹ】ppi5二级指针为常量
    //ppi4 = tppi;//error
    (*ppi4) = tpi;//success
    (*(*ppi4)) = 2;//sucess

    const int** ppi5{ new const int* (new int(2)) };//【ⅩⅠ】ppi5指针的指针指向的值为常量
    ppi5 = &pi_11;//success
    (*ppi5) = pi_11;//success
    //(*(*ppi5)) = 2;//error

    int const** ppi6{ new const int* (new int(2)) };//【ⅩⅡ】等价于【ⅩⅠ】

2.3 c++ 引用和指针运用多态特性

2.3.1 指针方式

将子类对象赋给基类指针

class A
{
public:
    virtual void operator ()()
    {
        cout<< "A";
    };
};
class B:public A {
    virtual void operator ()()
    {
        cout << "B";
    };
};
int main()
{
    A* aP = new B();
    (*aP)();
    delete aP;
    aP = nullptr;
}

上面代码执行(*aP)();之后打印出 B,而不是 A, 说明是执行了子类方法。

2.3.2 引用方式

将子类对象赋给基类引用

class A
{
public:
    virtual void operator ()()
    {
        cout<< "A";
    };
};
class B:public A {
    virtual void operator ()()
    {
        cout << "B";
    };
};
int main()
{
    B b;
    A& aRef = b;
    aRef();
}

上面代码执行aRef ();之后打印出 B,而不是 A,说明是执行了子类方法。

2.4 指针引用

  • *& :指针的引用
  • **& :指针的指针的引用

3 数组

3.1 二维数组

二维数组可以从栈中创建

int m=10,n=5;
int arr[m][n];//创建10行5列数组

也可以从堆中创建

int m=10,n=5;
int ** arr;
arr=new int* [m];//二维数组实质上是一个数组元素是另一个数组,而数组可用指针替换
for(int i=0;i<m;++i)
{
    arr[i]=new int[n];//每一个数组元素项都是一个数组
}

从堆中释放

for(int i=0;i<m;++i)
{
    delete[] arr[i];//删除内部数组
}
delete[] p;//删除外层数组

4 类

4.1 类成员变量

4.1.1 初始化

类成员变量有 3 中初始化方式: 1. 声明时初始化 2. 初始化列表 3. 构造函数初始化 - 示例

class A
{
public:
    // 1.声明时初始化
    int a = 1;
    // 2.初始化列表
    A(int a_):a(a_){}
    // 3.构造函数初始化
    A(int a_, bool b){ a = a_; }
};

4.1.2 位域

[[数据类型]] 位域是一种可以比 bit 级别的数据类型,只能又在类、结构体中,可以进占用某些 bit 位,而不是多个 byte。 - 声明规则:declarator:constant-expression

4.2 嵌套类

4.2.1 嵌套类声明定义

嵌套类有 2 中写法: 1. 在类中声明,类外定义 2. 在类中声明时定义 - 示例

class A
{
public:
    // 1.嵌套类在类中声明
    class A1;
    // 2.类中声明时定义
    class A2{};
};
// 1.嵌套类在类外定义
class A::A1
{
public:
    A();
    ~A(){}
};
// 类构造定义
A::A1::A1(){}

4.3 类设计陷阱

4.3.1 1. 类成员是一个基类指针,深拷贝避免丢失子类数据

class A
{
public:
    virtual ~A(){}
public:
    string str_A;
};
class B :public A
{
public:
    string str_B;
};
class C
{
public:
    C() :m_a(nullptr) 
    {

    }
    C(const C& in)
    {
        if (in.m_a != nullptr)
        {
            m_a = new A();
            *m_a = *in.m_a;
        }
        else
           m_a = nullptr; 

    }
    C& operator=(const C& in)
    {
        if (&in == this)
            return *this;
        if (m_a != nullptr)
            delete m_a;
        if (in.m_a != nullptr)
        {
            m_a = new A();
            *m_a = *in.m_a;
        }
        else
            m_a = nullptr; 
        return *this;
    }
public:
    A* m_a;
};
  • 问题分析:上面例子中看似很完美,对 C 中的指针 m_a 进行了深拷贝,然而 m_a 失去了对子类的数据拷贝。当 m_a 是 B 指针赋给 A 指针时,这时拷贝构造和赋值运算符中 *m_a = *in.m_a;只会去调 A 的赋值运算符,而不会去调 B 的,这样,m_a 中 B 数据就丢失了。
  • 解决方法:通过 static_cast 或 dynamic_cast 判断具体子类类型, 在对子类赋值(调用子类的赋值运算符)。
class C
{
public:
    C() :m_a(nullptr) 
    {

    }
    C(const C& in)
    {
        if (in.m_a != nullptr)
        {
            if (dynamic_cast<B*>(in.m_a))
            {
                B* pb = new B(*dynamic_cast<B*>(in.m_a));
                m_a = pb;
            }
            else
            {
                m_a = new A;
                *m_a = *in.m_a;
            }
            /*  m_a = new A();
              *m_a = *in.m_a;*/
        }
        else
            m_a = nullptr;

    }
    C& operator=(const C& in)
    {
        if (&in == this)
            return *this;
        if (m_a != nullptr)
            delete m_a;
        if (in.m_a != nullptr)
        {
            if (dynamic_cast<B*>(in.m_a))
            {
                B* pb = new B(*dynamic_cast<B*>(in.m_a));
                m_a = pb;
            }
            else
            {
                m_a = new A;
                *m_a = *in.m_a;
            }
            /* m_a = new A();
             *m_a = *in.m_a;*/
        }
        else
            m_a = nullptr;

        return *this;
    }
    ~C()
    {
        if (m_a != nullptr)
        {
            delete m_a;
            m_a = nullptr;
        }
    }
public:
    A* m_a;
};

4.3.2 2. 类成员指针变量设计成私有,避免疏忽导致的内存泄漏

class A
{

};
class B
{
public:
    B() :m_a(nullptr) {}
    ~B() 
    {
        if (m_a != nullptr)
        {
            delete m_a;
            m_a = nullptr;
        }
    }
public:
    A* m_a;
};
int main()
{
    B b;
    {
        A* a1 = new A;
        b.m_a = a1;
    }
    {
        A* a2 = new A;
        b.m_a = a2;
    }

}
  • 问题分析:上面 B 类定义看似完美,B 生命周期结束自动删除 m_a 内存, 但不正当的给 m_a 赋值导致了意外的内存泄漏。main 函数中 a1 首先赋给了b.m_a, 意图将 a1 的控制权交给 b,确实 b 接管了 a1,当 b 生命周期结束也可以通过析构删除 a1 指针, 当 a2 又赋给b.m_a 时,m_a 痛快的结束了 a2,从而放弃了 a1 的控制权, 使 a1 内存泄漏。
  • 解决方法:将成员指针设为私有,访问通过成员函数。
class A
{

};
class B
{
public:
    B() :m_a(nullptr) {}
    ~B()
    {
        if (m_a != nullptr)
        {
            delete m_a;
            m_a = nullptr;
        }
    }
    void setValue(A* a)
    {
        if (m_a != nullptr)
        {
            delete m_a;
            m_a = nullptr;
        }
        m_a = a;
    }
    A* getValue()
    {
        return m_a;
    }
private:
    A* m_a;
};
int main()
{
    B b;
    {
        A* a1 = new A;
        b.setValue(a1);
    }
    {
        A* a2 = new A;
        b.setValue(a2);
    }

}

4.3.3 类中成员变量为类自身

类中成员变量为类自身时怎么写?

4.3.3.1 1. 指针,智能指针可以,对象不可以

先看以下代码,一般成员声明情况

class A
{
public:
    A();
    ~A();
    A* m_pa; //正常
    A m_a;   //error:uses 'B', which is being define   
    shared_ptr<A> m_spa; //正常
};

解释: 1. A m_a; 不能通过编译,由错误信息可知,m_a 实例化时类 A 正在定义中(即还没有定义),编译器无法知道类 A 占多少空间,因此不能编译通过。 2. A* m_pa;和 shared_ptr<A> m_spa; 能正常通过编译,因为 m_pa 是指针存的是地址,只占 4 字节大小,可以明确确定。

4.3.3.2 2. 指针和智能指针怎么赋值?
  1. 不能在对象未生成时执行对象的赋值操作
class A
{
public:
    A(){m_pa = new A;}
    A(int){}
    A(char) { init(); }
    ~A();
    A* m_pa;
    shared_ptr<A> m_spa;
    void init() {
        m_pa = new A;
        m_spa.reset(m_pa);
    }
};

A a1; //error调用A();
A a2(2);//正常;
A a2('a');//error调用A(char);
a2.init();//正常

解释 2. A a1;会报错是因为 A 调用构造 A () 时,执行了语句 m_pa = new A; 而此时 A 的大小未知 new 多少内存无法确定,故报错; 3. A a2 (2); 正常执行,因为他执行了构造 A (int){},未对 m_pa 操作; 4. A a2 ('a'); 会报错是因为构造 A (char) 间接调用对 m_pa 的赋值操作,理由与 1 一样; 5. a2.init (); 正常执行是因为此时 A 的大小已知,可以执行赋值操作 6. 智能指针遵循指针一样的原则

总结:指针成员不能在类实例化阶段直接或间接进行操作

4.4 重载运算符

#include<iostream>
using namespace std;
struct S
{
    int val;
    S(const S& s):val(s.val){}
    S():val(0){}
    S(int i) :val(i) {}
    S& operator =(const S& s)
    {
        if (&s == this)
            return *this;
        val = s.val;
        return *this;
    }
    //++前置
    S operator ++(){
        printf("前置++");
        ++val;
        return *this;
    }
    //后置++
    S operator ++(int){
        printf("后置++");
        S s(*this);
        ++val;
        return s;
    }
    //转换函数
    operator int&()
    {
        return val;
    }//S对象转换int引用
    operator int()
    {
        return val;
    }//S对象转换int值
    operator int*()
    {
        return &val;
    }//S对象转换int*
    friend ostream& operator << (ostream& output,const S & s)
    {
        output << s.val;
        return output;
    }
    bool operator <(const S& s)
    {
        return val < s.val ? true:false;
    }
    void operator()(int i)//函数对象,参数,返回值任意
    {
        val = i;
    }
    int& operator[](int i)//此类不含数组类型,故设计直接返回了val成员
    {
        return val;
    }
    void* operator new(size_t t)// 注意函数的第一个参数和返回值都是固定的
    {
        //return malloc(t);
        return ::operator new(t);
    }
    void operator delete(void* ptr) // 重载了new就需要重载delete
    {
        //free(ptr);
        ::operator delete(ptr);
    }  
};

4.5 类设计常识

4.5.1 1. 类中默认 8 个函数

class A
{
public:
    // 默认构造函数;
    A();
    // 默认拷贝构造函数
    A(const A&);
    // 默认析构函数
    ~A();
    // 默认重载赋值运算符函数
    A& operator = (const A&);
    // 默认重载取址运算符函数
    A* operator & ();
    // 默认重载取址运算符const函数
    const A* operator & () const;
    // 默认移动构造函数
    A(A&&);
    // 默认重载移动赋值操作符
    A& operator = (const A&&);
};

4.5.2 1. 声明了任何构造函数,编译器不会提供默认构造函数

  • 分析:以下代码中自定义了 Box 的构造,将会使默认构造函数 Box(){} 失效, 因此代码 Box box3 将显式失败。
class Box {
public:
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height){}
private:
    int m_width;
    int m_length;
    int m_height;

};

int main(){

    Box box1(1, 2, 3);
    Box box2{ 2, 3, 4 };
    Box box3; // C2512: no appropriate default constructor available
}

4.5.3 提供 Init 成员函数

  1. 如果类中需要加载类外功能模块,需要定义一个 Init 成员函数,将初始化(加载)步骤在 Init 函数内实现,暴露 public,共使用时主动调用,这样,能根据 Init 函数的返回值判断结果;如果放在构造中,我们并不能知道这些模块是否加载成功。
  2. 构造函数里面只针对成员变量赋默认值(通常为 0 值、NULL 值)。

5 类型转换

  • static_cast :适用于例如 short 转 int、int 转 double 或 void*T* 转换
  • dynamic_cast :适用于含类继承关系的指针引用转换
  • reinterpret_cast :从二进制位重新解释类型的转换,可以将 2 个毫无关联的人类型进行转换
  • bit_cast :从二进制位重新解释类型的转换,但转换前后对象都含有相同的 bit 位,如(4 位 float 可以转 4 位 int,但不能转 8 位 long)
  • 示例
#include <bit>
int main()
{
    using namespace std;
    double f = 1.23;
    int i_s = static_cast<int>(f);//i=1
    //reinterpret_cast只能转&或*
    int i_r1 = reinterpret_cast<int&>(f);//i_r1=2061584302
    int i_r2 = (*reinterpret_cast<int*>(&f));//i_r2=2061584302

    //reinterpret_cast和bit_cast比较
    long long l_r = reinterpret_cast<long long&>(f); //l_r=4608218246714312622
    long long l_b = bit_cast<long long>(f);//l_b=4608218246714312622
    //double d = bit_cast<double>(i_r1);//error,4位int不能转8位double
}

6 初始化

  • 参考
  • 初始化语法
  • ( 表达式列表 ) : 括号中的以逗号分隔的含有任意表达式和花括号初始化器列表的列表
  • = 表达式 : 等号后面跟着一个表达式
  • { 初始化器列表 } : 花括号初始化器列表:以逗号分隔且可以为空的含有表达式和其他花括号初始化器列表的列表
  • 初始化类型有:
  • 值初始化,例如 std::string s{};
  • 直接初始化,例如 std::string s("hello");
  • 复制初始化,例如 std::string s = "hello";
  • 列表初始化,例如 std::string s{'a', 'b', 'c'};
  • 聚合初始化,例如 char a[3] = {'a', 'b'};
  • 引用初始化,例如 char& c = a[0];

6.1 值初始化 (C++03 起)

6.2 直接初始化、复制初始化

  • 直接初始化语法:
T 对象 ( 实参列表 );
T 对象 { 实参列表 };      (C++11 )
T ( 实参列表 )
static_cast< T >( 其他对象 )
new T(实参列表)
::() : 成员(实参列表) { ... }
[实参](){ ... }       (C++11 )
  • 复制初始化语法:
目标类型 对象 = 其他对象; 
目标类型 对象 = {其他对象} ;  (C++11 )
函数(其他对象)
return 其他对象;
throw 对象;
catch (目标类型 对象)
目标类型 数组[元素个数] = {其他对象序列};

6.3 列表初始化

  • 定义:从 {} 列表初始化对象
  • 直接列表初始化
T 对象 { 实参1, 实参2, ... }; (1) 
T { 实参1, 实参2, ... } (2) 
new T { 实参1, 实参2, ... } (3) 
类 { T 成员 { 实参1, 实参2, ... }; };  (4) 
类::类() : 成员{实参1, 实参2, ...} {... (5) 
  • 复制列表初始化
T 对象 = {实参1, 实参2, ...}; (6) 
函数( { 实参1, 实参2, ... } ) (7) 
return { 实参1, 实参2, ... } ;  (8) 
对象[ { 实参1, 实参2, ... } ] (9) 
对象 = { 实参1, 实参2, ... }  (10)    
U( { 实参1, 实参2, ... } )  (11)    
 { T 成员 = { 实参1, 实参2, ... }; };    (12)    

6.4 聚合初始化

  • 定义:聚合初始化对聚合体进行初始化
  • 语法
T 对象 = {实参1, 实参2, ...}; (1) 
T 对象 {实参1, 实参2, ... };  (2) (C++11 )
T 对象 = { .指派符 = 实参1 , .指派符 { 实参2 } ... };   (3) (C++20 )
T 对象 { .指派符 = 实参1 , .指派符 { 实参2 } ... }; (4) (C++20 )
T 对象 (实参1, 实参2, ...);   (5) (C++20 )

6.5 静态初始化和动态初始化

  • 参考
  • 静态初始化,适用于静态存储期的变量,在 main 函数之前就初始化了
  • 动态初始化:在运行时初始化。(静态初始化之外的全部都是动态初始化)

7 移动语义

7.1 类移动语义

  • 条件:
  • 实现移动构造
  • 重载移动赋值运算符
  • 示例
template<class T>
class A
{
public:
    A() = default;
    //1.1拷贝构造函数,有2种方式,同一类中只能存在一种
    A(const A& val) {//方式一
        cout << "拷贝构造: A(const A& val)" << endl;
    }
    //A(const A<T>& val) {//方式二
    //    cout << "拷贝构造: A(const A<T>& val)" << endl;
    //}

    //1.2模板拷贝构造函数
    template<class T1>
    A(const A<T1>& val) {
        cout << "模板拷贝构造: A(const A<T1>& val)" << endl;
    }

    //2.1重载赋值运算符,有4种方式,同一类中只能存在一种
    A& operator =(const A& val) {//方式一
        cout << "重载赋值运算符: A operator =(const A& val)" << endl;
        return *this;
    }
    //A& operator =(const A<T>& val) {//方式二
    //    cout << "重载赋值运算符: A& operator =(const A<T>& val)" << endl;
    //    return *this;
    //}
    //A<T>& operator =(const A& val) {//方式三
    //    cout << "重载赋值运算符:  A<T>& operator =(const A& val)" << endl;
    //    return *this;
    //}
    //A<T>& operator =(const A<T>& val) {//方式四
    //    cout << "重载赋值运算符:  A<T>& operator =(const A<T>& val)" << endl;
    //    return *this;
    //}

    //2.2模板重载赋值运算符
    template<class T1>
    A<T>& operator =(const A<T1>& val) {
        cout << "模板重载赋值运算符:  A<T>& operator =(const A<T1>& val)" << endl;
        return *this;
    }

    //3.1移动拷贝构造函数,有2种方式,同一类中只能存在一种
    A(A&& val) {//方式一
        cout << "移动拷贝构造: A(A&& val)" << endl;
    }
    //A(A<T>&& val) {//方式二
    //    cout << "移动拷贝构造: A(A<T>&& val)" << endl;
    //}

    //3.2 模板移动拷贝构造函数
    template<class T1>
    A(A<T1>&& val) {
        cout << "模板移动拷贝构造: A(A<T1>&& val)" << endl;
    }

    //4.1移动赋值运算符,有4种方式,同一类中只能存在一种
    A& operator =(A&& val) {
        cout << "移动赋值运算符: A& operator =(A&& val)" << endl;
        return *this;
    }
    /*A<T>& operator =(A&& val) {
        cout << "移动赋值运算符: A& operator =(A&& val)" << endl;
        return *this;
    }
    A& operator =(A<T>&& val) {
        cout << "移动赋值运算符: A& operator =(A&& val)" << endl;
        return *this;
    }
    A<T>& operator =(A<T>&& val) {
        cout << "移动赋值运算符: A& operator =(A&& val)" << endl;
        return *this;
    }*/

    //模板移动赋值运算符
    template<class T1>
    A& operator =(A<T1>&& val) {
        cout << "模板移动赋值运算符: A& operator =(A<T1>&& val)" << endl;
        return *this;
    }
private:
    vector<T> mv_value;
};

int main()
{
    A<int> ai1;
    A<int> ai2(ai1);
    ai1 = ai2;
    A<double> ad1(ai1);
    A<double> ad2 = ai1;
    ad2 = ai1;
    A<int> ai3(std::move(ai1));//ai1不再使用   
    A<double> ad3(std::move(ai3));//ai3不再使用
    ai3 = std::move(ai2);//ai2不再使用
    A<int> ai4;
    ad3 = std::move(ai4);//ai4不再使用
    ad3 = std::move(ad1);//ad1不再使用
}
//控制台输出结果:
拷贝构造: A(const A& val)
重载赋值运算符: A operator =(conSst A& val)
模板拷贝构造: A(const A<T1>& val)
模板拷贝构造: A(const A<T1>& val)
模板重载赋值运算符:  A<T>& operator =(const A<T1>& val)
移动拷贝构造: A(A&& val)
模板移动拷贝构造: A(A<T1>&& val)
移动赋值运算符: A& operator =(A&& val)
模板移动赋值运算符: A& operator =(A<T1>&& val)
移动赋值运算符: A& operator =(A&& val)

7.2 左值引用和右值引用调用规则

  • 非模板函数
  • 当仅实现 T& 时,只接受左值
  • 当只实现 T&& 时, 只接受右值
  • 当仅实现 const T& 时,可接受左值、常左值和右值
  • 当同时实现 T&T&&const T& 时,T& 优先接受左值,const T& 优先接受常左值,T&& 优先接受右值
  • 模板函数
  • 当仅实现 T& 时,只接受左值
  • 当只实现 T&& 时, 可接受左值、常左值和右值
  • 当仅实现 const T& 时,可接受左值、常左值和右值
  • 当同时实现 T&T&&const T& 时,T& 优先接受左值,const T& 优先接受常左值,T&& 接受右值
  • 示例
#if 0
template<class T>
void Test(const T&) {
    cout << "模板: const T&" << endl;
}
template<class T>
void Test(T&){
    cout << "模板: T&" << endl;
}
template<class T>
void Test(T&&) {
    cout << "模板: T&&" << endl;
}
#endif
#if 1
void Test(int&&){
    cout << "int&&" << endl;
}
void Test(const int&) {
    cout << "const int&" << endl;
}
void Test(int&) {
    cout << "int&" << endl;
}
#endif
int main()
{
    const int t1 = 1;
    int t2 = 2;
    Test(t1);
    Test(t2);
    Test(2);
    Test(std::move(t2));
}

8 可变参数

8.1 可变参数普通函数

  • 变参函数声明形式:return_type function_name(parameter_type format, ...);return_type function_name(parameter_type format ...);, 逗号可省略。
  • 变参函数列表不能是引用类型
  • 变参函数参数列表中的类型并不代表所有参数类型,只限制第一个参数类型。
  • 变参函数参数列表中给出的类型可以是任意类型,但必须能够提供足够信息让展开参数包时知道参数类型以及参数个数。
#include <stdio.h>
#include <stdarg.h>
 //这个num作用是提供出参数个数的作用,但无法提供参数类型作用
double average(int num,...)
{

    va_list valist;
    double sum = 0.0;
    int i;

    /* 为 num 个参数初始化 valist */
    va_start(valist, num);

    /* 访问所有赋给 valist 的参数 下一个参数类型是int型 */
    for (i = 0; i < num; i++)
    {
       sum += va_arg(valist, int);
    }
    /* 结束并清理为 valist 保留的内存 */
    va_end(valist);

    return sum/num;
}
//这个fmt能提供参数个数还能提供参数类型(fmt具有一定规则)
void simple_printf(const char* fmt...) // C 风格 "const char* fmt, ..." 亦合法
{
    va_list args;
    va_start(args, fmt);

    while (*fmt != '\0') {
        if (*fmt == 'd') {
            int i = va_arg(args, int);
            std::cout << i << '\n';
        } else if (*fmt == 'c') {
            // 注意自动转换到整数类型
            int c = va_arg(args, int);
            std::cout << static_cast<char>(c) << '\n';
        } else if (*fmt == 'f') {
            double d = va_arg(args, double);
            std::cout << d << '\n';
        }
        ++fmt;
    }

    va_end(args);
}

int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
   simple_printf("dcff", 3, 'a', 1.999, 42.5); 
}

8.2 可变参数模板函数

8.2.1 可变模板函数

  • 下面是一个最简单的变参模板函数,利用 sizeof... 运算符返回参数个数。
template<typename ...T>
size_t count(T...args)
{
    return sizeof...(args);
}
int main()
{
  auto num=count(1,"2",false);//num==3
}

8.2.2 递归展开参数包

  • 递归展开参数包的重点是提供一个递归终止函数。
#include<iostream>
using namespace std;

template <typename T>
void fun(const T& t) {
    const std::type_info& tpif = typeid(T);
    cout << "value:" << t << "的类型是" << tpif.name() << '\n';
}
template <typename T, typename ... Args>
void fun(const T& t, Args ... args) {
    const std::type_info& tpif = typeid(T);
    cout << "value:" << t << "的类型是" << tpif.name() << '\n';
    fun(args...);//递归解决,利用模板推导机制,每次取出第一个,缩短参数包的大小。
}
int main() {
    fun("1", false, 2);
}
//输出结果
//value:1的类型是char [2]
//value:0的类型是bool
//value:2的类型是int
  • 递归终止函数参数个数没有限制,可以无参,也可以多参。

注意:建议参数越少越好,当设计 0 参时,设计的变参函数才可以匹配 0 参输入,终止函数参数为 n 个,代表你函数输入的参数最少为 n 个

//0参实现递归终止函数
void fun() {

}
//多参实现递归终止函数
template <typename T1,typename T2>
void fun(const T1& t1,const T2& t2) {
    const std::type_info& tpif1 = typeid(T1);
    const std::type_info& tpif2 = typeid(T2);
    cout << "value:" << t1 << "的类型是" << tpif1.name() << '\n';
    cout << "value:" << t2 << "的类型是" << tpif2.name() << '\n';
}

8.2.3 逗号表达式展开参数包

#include<iostream>
using namespace std;
void print(T t)
{
    cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
    bool temp[] = { (print(args), true)... };
}
int main()
{
  expand(1,2,"1");
}

8.2.4 折叠表达式展开参数包

  • c++17 折叠表达式比逗号表达式方式更好
#include<iostream>
#include<type_traits>
using namespace std;
//折叠表达式 
template<typename ...T>
requires (is_arithmetic_v<T>&&...)
auto sum(T...args)->decltype((... + args))
{
    return (... + args);
}

//不建议逗号表达式
void sum_(double& all, const double val2)
{
    all += val2;
}
//c++14 开始auto推导返回类型不再需要尾随声明
template<typename T>
concept arithmetic = is_arithmetic_v<T>;
template<arithmetic...T>
auto sum1(T...args)
{
    double val = 0.0;
    bool temp[] = { (sum_(val, args), true)...};
    return val;
}
int main() {
    auto val = sum(1, 2.2); val被推导为double
  auto val1 = sum1(1, 2.3);
}

8.3 可变参数模板类

8.3.1 递归展开参数包

  • 递归展开一般需要 3 个类,一个前向声明,一个递归终止,一个基本定义
//前向声明
template<typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

//递归终止
template<typename Last>
struct Sum<Last>
{
    enum { value = sizeof (Last) };
};
int main()
{
    auto sz=Sum<int, float>::value;//sz==8
}

9 存储持续性

9.1 自动存储持续性

  • 函数类定义的局部变量

9.2 静态存储持续性

  • 在函数外定义的变量
  • static 修饰的变量

9.3 线程存储持续性

  • thread_local 修饰的变量

9.4 动态存储持续性

  • new 声明的变量

10 链接性

  • 示例
static int sta_i;//静态存储期,内部链接性
int ext_i;//静态存储期,外部链接性
void fun()
{
    static int l_a;//静态存储期,无链接性
    int l_b;
}
int main()
{
}

10.1 外部链接性

  • 特点:可被所有文件访问
  • 示例:
  • 全局变量
  • 全局函数

10.2 内部链接性

  • 特点:只能在本文件被访问
  • 示例:
  • static 修饰的变量、函数

10.3 无链接性

  • 特点: 只在本函数作用域被访问
  • 示例:
  • static 修饰的局部变量

10.4 函数与链接性

  • 外部链接性函数:函数默认链接性是外部的,可以在函数声明处使用 extern 修饰(可选),指明函数定义在其它地方,就如同 extern 修饰的变量一样。
  • 内部链接性函数:在函数的声明和定义都使用 static 修饰,表明函数只能在当前文件被使用,不能被其它文件链接。