C++知识点全面汇总

C++编译流程

预处理-》编译-》汇编-》链接

cpp-》预处理器-》编译器-》汇编程序-》目标程序-》链接器-》可执行程序

C++ 基础语法知识

向下兼容知识

各内置类型大小

64位下:

char 1 char* 8 short int 2 int 4 unsigned int 4 float 4 double 8 long 8

32位下:

与64位的区别在于char* 4

指针一般都是4字节的

size_t,size_type,int

int 不论在何种机器下都是4字节,为了更好地进行程序移植,引出了size_t,size_type

size_t 在32位下是4字节无符号整数(unsigned int),64下是8字节无符号整数(unsigned long),该变量含义表示,适用于计算内存中可容纳数据项目个数的无符号整数类型,因此常用于内存计数与数组管理函数中。(eg.sizeof)

size_type 一般是unsigned int 用来表示任意长度的string,vector等容器长度。

数组与指针的区别

数组名是一个指向一个连续已分配地址的指针,并且是一个常指针变量(const type*),无法赋值,无法自增自减操作,在函数传参的过程中会退化为一般指针。

char* tp=new char('1');
char str[10];//char* const str;
str++;//error
str=tp;//error

extra: strlen()计算字符串的长度,以’/0’为结尾,不包括/0

C风格字符串以及相关操作

使用char数组,注意最后一个字符是’/0’,因此n个大小的数组能够存储n-1个值。声明字符串如下

char strs[6]="hello";
char strs2[]="hello";

如果用sizeof计算会得到6,而用strlen计算会得到5

常用字符串操作:

strcpy(s1,s2);//将s2复制到s1
strcat(s1,s2);//将s1与s2连接
strcmp(s1,s2)//返回字典序的s1-s2,相等为0

**细节:**无论C/C++中,显示给出的字符串值都是const char*类型的

extern

表明变量或者函数定义在其他文件中,在其他文件中寻找这个变量(这两个文件必须链接起来)

在a.cpp中定义一个函数或者变量,

b.cpp中声明extern变量和函数,表明要使用的量定义在其他文件中,并通过将链接来使用。

注意

  1. a中变量或者函数不能是static 否则会被隐蔽掉
  2. 必须链接起来

C风格的输入输出

static

有以下个作用

  1. static用于修饰静态局部变量:用static修饰的变量会存在虚拟内存的全局数据区域,只初始化一次(首次声明时),全局数据区域内的数据离开作用域不会被销毁,它的生命周期和程序运行周期相关。

    void add()
    {
    	static int b=0;
    	b++;
    	return b;
    }//运行三次得到1,2,3,因为该区域的变量没有被销毁
    

    定义静态局部变量的好处是,变量属于函数,并且可以保留上次调用的值,并且不污染名称空间。但是作用域还是局部作用域,用其判定函数运行了多少次。

  2. 静态全局变量

    存储在全局数据区的量,但该变量只在本文件可见,其他文件无法通过extern来使用该变量,从而避免重命名问题

  3. 静态全局函数

    存储在全局数据区的量,但该函数只在本文件可见,其他文件无法通过extern来使用该函数,从而避免重命名问题

  4. 静态成员变量与成员函数详见OOP部分

结构体struct

C++中的结构体,可以拥有成员函数,可以拥有静态成员,可以有访问控制权限,可以有继承关系,可以初始化数据成员,甚至可以有虚函数等。区别在于:

默认访问权限是public

C++中有一个约定:struct往往用来定义数据结构,所操作数据

头文件

include"name"是在本地工作目录中寻找

include是在标准库中寻找

C++高级语法速览

OOP以下

宏定义

C++预处理命令之一,它是一个替换操作,不做计算和表达式求解,不占内存和编译时间

inline

向编译器请求为内联函数(编译器有权拒绝),可以在运行时调试,如果在类中定义成员函数会自动转换成内联函数,可以定义在头文件中。

与普通的函数区别在于,普通函数调用时需要记下返回地址,保存现场,执行跳转语句,再调用,而内联函数是通过函数体代码和实参直接代替函数调用语句,性能会得到提高很多。

虚函数,递归,迭代,内联函数内调用其他函数,一般不会内联。

auto

自动推断类型 类似C# var

可用于默认类型,自定义类型,可调用对象(函数,Lambda等)

不能使用引用, 会丢弃const属性,不能用于函数参数与普通成员变量

for_each

本质上是一个函数模板,其模板的实现大致如下

Function for_each(Iterator beg,Iterator end,Function f){
    while(beg!=end)
        f(*beg++);
}
//for_each(起点,终点,对每一项进行处理的函数)

一个用for_each遍历vector的例子

for_each(nums.begin(),nums.end(),[](int n){cout<<n<<" "});

decltype

可以得到表达式的类型,但并不进行计算,一共有如下这些使用方法

const int a=0;
int& b=a;
int* c=&a;

decltype(a);//得到const int
decltype(b);//int&
decltype((a));//int& 双括号得到引用
decltype(b+0);//得到int
decltype(c);//得到int*
decltype(*c)//得到int& c原本是指针,取指针最终得到的是所指对象 相当于引用

decltype(auto) 会根据表达式来替换auto后,在根据表达式得到对应的类型

const

常用用法

class MyClass{
public:
  void fun()const;
    //其中的this指针指向const成员变量, 等价于
    //const MyClass* const this; 也就是说无法改变类中除了mutable外的属性
};
const int a=0;//常量,必须初始化
const MyClass a1;//常量类对象,只能调用const成员函数
const int* a2;//指针本身不能变,但是其内容可以变
int* const a3;//指针本身可以变,但是其内容不能变
const int* const a4;//指针指向不能变,内容也不能变
const var& function()//不能使用返回值的引用来修改对象
const MyClass& obj;//万能引用类型,同时可以引用左值与右值

const定义的常量会进行类型检查,而define定义的常量不会

const还可以用来重载成员函数const void func() const;使得const对象也能调用对应函数

constexpr

常量表达式定义,在编译器既可以计算出表达式的值,且不能被改变

可以用其来修饰函数,这样函数必须要编译期间返回值,修饰函数的要求如下:

  1. 只有一个返回值
  2. 必须只能接受常量为实参
  3. 返回值必须是常量

别名定义

typedef using

两个关键字都用来进行别名定义,可以为默认类型,对象,结构体,数组,函数进行别名定义

//typedef用法
typedef int Array[10000];
Array a;//直接得到了一个数组
typedef struct {double x,double y} vector;//定义结构体
typedef int(*F)(int,int),int* INT;//typedef可以用逗号分割去定义多个别名

//using用法
using namespace std;//引用名称空间,注意头文件往往不会使用using
using INT = int*;//将右边的类型定义为左边的别名
using INT_Vector=int[50];//using是不能多个联用的
using Fa=int (*)(int,int);//注意两者定义委托的方式区别较大

指针与引用

指针和引用都间接操作对象

区别

指针是一个实体,但是引用是别名

指针可以多级指向,但是引用绑定了最初获得的对象后就无法变更,因此只能引用一级(哪怕这样在语法上是不会报错的)。

指针可以为空,引用不行

指针使用前可以不初始化,引用一定要初始化

sizeof运算结果不同,sizeof指针得到指针大小,sizeof引用得到所指对象大小

int a=5;
int b=6;
int& n1=a;
int& n2=b;
n1=n2;
n1=1;

上述代码虽然n1=n2,但n1仍然指向a,修改值后,a的值为1,b不变。

空指针,野指针,指针空悬

赋值为nullptr的指针为空指针

指向为无效区域的指针为指针空悬(往往出现在指向销毁内存区域中)

没有初始化的指针为野指针

智能指针

unique_ptr,shared_ptr,weak_ptr

shared_ptr

通过引用计数来实现的一种指针,如果当前指针其引用数为0,那么就会清空该指针所指向内存,常用方法如下:

shared_ptr<MyClass> p0;//不初始化的话为nullptr空指针
shared_ptr<int> p2(new int(10));//可以使用裸指针初始化,不推荐
shared_ptr<MyClass> p3=make_shared<MyClass>(10);
//用make_shared指定类型初始化,只要有满足所传参数的构造函数即可
shared_ptr<int> p3(p1);//可以由一个shared_ptr去初始化另外一个,并且增加引用计数
//总之shared_ptr的构造函数只能传递指针

*p1;//解指针
p1.get();//返回裸指针,如果清空这个裸指针会使得智能指针无法访问所指内存
p2=p1;//p1引用计数++,p2的--
p1.user_count();//返回引用计数

函数传值,返回值(非引用),将赋值给一个新的智能指针都会增加引用计数

离开作用域,被赋予了一个新的智能指针都会减少引用计数

一般来说智能指针会在清空时自定义调用delete,但是有些情况下需要自定义deleter,比如通过shared_ptr管理数组(一般不建议这么搞) shared_ptr没有自定义[]操作,需要通过get得到指针后显示访问数组。

shared_ptr<int> nums(new int[10],[](int *p){delete[] p;})

其他常用方法

nums.reset();
//根据当前引用计数情况
//1. 如果当前引用计数为1,那么就会调用删除其
//2. 否则直接置空当前指针

unique_ptr

不支持拷贝,不支持赋值,必须直接用裸指针初始化,与共享的shared_ptr不同,某一时刻只能有一个unique_ptr指向一个对象。

初始化方式

unique_ptr<int> ptr;
unique_ptr<int> p0(new int(10));

通过release可以让当前unique_str指针放弃所指向的内存,并返回其所指内存的指针(),通过这个函数可以转交内存权限,release返回的是裸指针

unique_ptr<int> ptr;
unique_ptr<int> p0(new int(10));
unique_ptr<int> p1=make_unique<int>(10);//C++14

ptr.reset(p0.release());//调用完release后p0就视为了nullptr
unique_ptr<int> 

自定义删除器

weak_ptr

辅助shared_ptr,指向其所指的对象,但是不控制其对象的生命周期(不会修改引用计数),也不会影响其释放对象。

动态分配数组

type* name=new type[const size];

delete[] name;

上述方法动态分配返回得到的是一个常量指针而不是一个数组类型

如果分配的是指针数组,在清空时要遍历整个数组清空每个指针后,再清空数组指针。

int** a=new int*[10];
for(int i=0;i<10;i++)
	delete[] a[i];
delete[] a;

智能指针分配数组

shared_ptr<int> nums(new int[10],[](int* p){delete[] p});//shared_ptr要自定义删除器,并通过get得到裸指针偏移来访问元素
unique_ptr<int[]> nums(new int[10]);//通过元素访问运算符[]即可访问元素

几种指针的区别

函数指针,指针数组,数组指针(行指针)

//函数指针
return_type (*function_name)(params...)
//接下来几个指针都与操作原生数组相关
int** p1;//用于动态生成二维数组,两个维度均未知
int* p2[10];//指针数组,每个元素都是指针,可以将其表示为int**
int (*p3)[10]; //行指针,每个指针都表示指向一行的元素,只能在多维数组当中使用,以这个例子为例,声明中的10表示二维数组当中第二维度的大小

//以一个二维数组为例来说明这三种指针的使用方法
int a[3][3]={
    {1,2,3},{4,5,6},{7,8,9}
};
//直接使用指针遍历
for(int i=0;i<3;i++)
{
    for(int j=0;j<3;j++)
    	cout<<*(*(a+i)+j)<<" ";
    cout<<endl;
}
//使用行指针遍历
//使用行指针必须要指明列维度,与int**不同,int (*p)只能指向一行,元素还是要下标取,往往使用行指针作为函数中二维数组的形参
void test(int (*p)[3],int len){...}//当然第二维度还是需要给出的,len表示第一维

左值引用

引用左值的引用,所有可以取&的值都是左值,不能取&的是右值

右值引用

左值往往是持久化的变量,右值往往是临时(即将要销毁)的数据(纯右值,将亡值)

右值引用指的是把变量绑定到右值

int&& i = 42;//右值引用
int& j=i;//左值引用
int&& k=i;//这条语句会报错,int&& 得到的仍然是变量视为左值,int&& 可能是左值 也可能是右值
void fun(int&& t);
fun(10)//t是右值
fun(i)//t是左值

往往可以用右值引用来接函数返回值来略微提高性能

右值引用本身自带了移动操作,右值引用量会引用右值地址,与左值开辟内存空间,复制并销毁内存的操作相比,避免了高昂的内存开销。函数返回值,类的构造函数,都可以使用右值引用。

完美转发:

函数模板中,完全依照模板的参数类型(即保持左右值特性),将参数传递给函数模板中的另一个函数。

Move

对变量进行移动,转移内存所有权,头文件utility

可以把一个左值转换成右值后,就无法再使用该左值,视其是无效的(可以新赋值或者销毁)。不是所有类型都可以移动,必须要有移动构造函数

int a=5;
int&& b=move(a);//左值该成右值,现在a,b使用同一块内存空间
a=10;//修改a=10后,a,b均为10,因此绝对不能修改被移动的遍历,应该视作其是无效的

移动与拷贝的区别:

拷贝对象相当于新开辟一块内存并把拷贝的数据复制过来

移动对象则是指新创建的对象会使用被移动内存空间,省下了内存开辟和收放的开销。

可调用运算符()

如果在类/结构体中重载该运算符,则会将类对象变成可调用类型,如下

struct cmp{ bool operator()(int a,int b){return a>b; }//仿函数
cmp()(a,b);//在生成对象后,可以直接调用这个对象

对于重载了可调用运算符的类/结构体,其生成的对象无法赋值给函数指针,但可以给function对象

C++函数多态

  1. 函数指针

    type (*function)(…type);声明一个函数指针量(不是类型),且只能接受函数(有些可调用对象比如重载了()运算符的类对象接受不了)

    bool (*Compare)(int,int);
    Compare=cmp;//可以声明函数指针变量(类似委托),将不同的函数赋值给它
    //注意如下的代码是错误的
    /*
    Compare c=cmp;函数指针是一个量,不是类型,如果要定义成类型可以使用using或者typedef
    */
    
  2. function模板

    需要头文件#include相当于把函数指针声明类型的两步变成一步

    function<return_type(…pars_type)> func;

    function<bool(int,int)> Cmp=cmp();
    

    function模板相当于C#中的委托,其可以接受函数,函数指针,可调用对象(仿函数),Lambda表达式

Lambda

可调用对象,匿名函数

//基本格式:[捕获列表](参数列表)->返回值类型{函数体}
//除了捕获列表与函数体其他均可以为省略(捕获列表可以为空)
//给出参数列表可以不给出返回值,但是给出了返回值一定要给出参数列表
//捕获列表是指所使用的局部变量,其他顾名思义
int a=1,b=2;
[a,b]{return a>b;}

//如果一个lambda表达式包含return之外的语句在未定义返回类型的情况下,其返回值都为void
//return a>b; if(a>b) return true; else return false; 两者返回可能是不同的
[a,b]()->bool{if(a>b)return true;else return false;}

//捕获列表中的局部变量也可以是引用或者指针
[&a]{a++;}
[&]{}//引用外部区域的所有变量
[=]{}//按值使用外部区域所有变量
[this]{}//lambda表达式拥有成员函数的权限

//指定参数
[](int a,int b)->bool{return a>b}//接受参数的匿名函数,这个例子中的返回值类型可以省略

bind关键字

头文件functional,placeholders,可以将固定参数传递给一个可调用对象并返回一个新的可调用对象

//bind(callable,...pars)
//其传递的参数需要严格与callable对应,一共有两类参数 _i(i=1,2,3..n)占位符与实际参数
//_i占位符相当于暂时占据了callable中参数的位置,新调用对象需要将参数传递给这些形参
//实际参数相当于已经赋值给了新调用对象

auto newcallable=bind([](string s,int n){return s.size()>=n;},_1,2);
newcallable("str2");//返回true
//上述的这个例子,bind将数值2绑定到了匿名函数的形参n,接下来的新调用对象只需传递实参给形参s即可。

auto newcallable=bind([](int a,int b,int c,int d){},_1,2,_2,2);
newcallable(2,2);
//上述的例子 根据占位符所占的位置是匿名函数的第1,3的位置,而实参已经传递给了其第2,4个位置,因此新调用对象使用时要传递第1,3个参数

异常处理

noexcept 用来修饰函数可以表示这个函数如果抛出异常就程序终止。直接终止程序相比异常处理可以减少一些开销。

头文件相关

OOP以上

OOP三原则

封装:使数据和加工该数据的方法封装成整体增加安全性

多态:同一个类型可以根据不同的消息做出不同的事件

继承:子类共享父类数据和方法的机制,增强复用性。

成员

数据成员必须是完全类型(变量等),指针成员,静态成员,引用成员可以是不完全类型

class ListNode
{
public:
	ListNode* next;//由于此时ListNode的成员尚未初始化,所以是不完全类型
	//ListNode next;因此这条语句是会报错的
}

构造函数

默认构造函数,在未定义构造函数情况下编译器会提供一个默认构造函数,一个函数如果定义了其他函数,编译器就不会自动提供,需要自行定义默认构造函数,尤其是当类中含有其他类型信息,指针,数组时

class MyClass{
public:
    MyClass()=default;//定义为默认构造函数
}
MyClass myc;

对于参数列表全部提供了默认值的构造函数也会视作默认构造函数

=delete可以把函数定义成删除的,也就是无法调用的,如果将拷贝控制函数(构造,复制,赋值,析构等)定义成删除的,会导致对象无法进行相应的操作。如果类中有成员是类类型的,其中存在=delete的拷贝控制,也会对当前类的对应功能造成=delete。

不能有多个默认构造函数,会有二义性

以下情况必须提供默认构造函数

  1. 声明类型数组,且未初始化
  2. 声明变量,且未指定初始化值
  3. 作为其他类的成员变量时

构造函数初始化列表

使用初始化列表的构造函数直接初始化对象,而通过赋值进行初始化的构造函数,会先对成员进行默认初始化后再进行赋值,这对于有些成员比如const或者引用是不能接受的。

拷贝构造函数

编译器会为我们提供默认的拷贝构造函数,逐元素进行拷贝操作(如果类类型,会用类类型的拷贝构造函数)

MyClass(const MyClass& ms)//形式化描述
{
    val=ms.val;
}

以下情况会使用拷贝构造函数:

传对象给非引用形参

返回对象为非引用类型

用花括号来初始化类型数组

拷贝初始化

用已给对象拷贝来进行对象初始化的本质是调用将已给对象传入拷贝构造函数创建了一个新对象

拷贝赋值运算符

编译器也会自动提供该成员,拷贝构造函数中为常量左值引用,可以接受左值,右值,常量左值,常量右值,左值引用,右值引用,常量左值引用,常量右值引用。如果是非常量左值引用,那么只能使用左值

MyClass& operator=(const MyClass& ms)
{
    this->val=ms.val;
    return *this;
}

返回当前对象的引用

析构函数

一个对象离开作用域,delete,容器被清空时等会调用析构函数,编译器会自行提供

~MyClass(){}

析构函数并不销毁成员,销毁成员时析构完毕后的析构阶段做的,析构函数的调用顺序与构造相反,是逆序调用的(后构造的先析构

自定义的析构往往会挂钩拷贝构造函数,拷贝赋值运算符重载

移动构造函数

移动构造函数使得对象创建时,会使用右值引用参数对应成员的空间,而不是像拷贝构造函数重新开辟空间后初始化。(内存处理是转移而不是拷贝

class MyClass{
public:
    int *val;
	MyClass(MyClass&& t)//右值引用只接受右值
    {
        if(this != &t)//先确保不是自赋值
        {
            if(t.val!=nullptr)
            {
                val=t.val;//使得所指空间相同
                t.val=nullptr;//使用移动构造函数需要使形参中指针为空,防止销毁时清空了调用对象的内存
            }   
        }
    }
}

直接赋予内存所有权会快很多

移动赋值运算符

类似做法

MyClass& operator=(const MyClass&& mc)

返回新建立对象的引用,这个函数必须给出右值来作为参数

两个移动函数不一定会由编译器自行创建

如果提供了移动构造函数/赋值运算符重载,那么编译器就不自动提供一般复制/赋值函数了

混合构造函数使用的例子

class vector2{
public:
	double x;
	double y;
    vector2(double X=0,double Y=0):x(X),y(Y=0);
    vector2(const vector2& v):x(v.x),y(v.y){}
    vector2& operator=(const vector2 v)
    {
        x=v.x;y=v.y;
        return *this;
    }
}
vector2 vcts[2];//首先调用2次默认构造函数,完成初始化
for(int i=0;j<2;i++)
    vcts[i]=vector2(i,i);//每次调用一次默认构造函数,赋值运算符函数,以及析构函数
//最后运行结束再调用两次析构函数

深浅复制问题

浅拷贝往往只拷贝了对象 而不是拷贝对象中的内容 这会导致如果销毁拷贝对象可能会影响源对象中的值

深拷贝是拷贝了对象的内容,使得拷贝对象与被拷贝对象间完全独立。

友元

在类中定义的非类成员函数,使得其获得与类成员函数一样的权限 关键字friend

class MyClass{
    friend istream& read(istream& is,MyClass m);
    private:
    	string name;
}
//能够访问private权限对象
istream& read(istream& is,MyClass m) { is>>m.name; return is;}

如果类定义在头文件中,需要为友元的函数在类外重新声明。

友元类

是指一个类A中定义为另一个类B的友元类后,可以在B的成员函数中访问A的私有对象。

class A{
	friend class B;//也可以只为成员函数定义友元friend void B::addA(int);
    int a;
}
class B{
public:
    A a;
    void addA(int n){ a.a+=n;}//由于定义了友元类 因此B可以访问到a
}

友元特性不能传递也不能继承,B是A的友元,C是B的友元,但是C不是A的友元,同理B是A的友元,C是A的派生类,B不能访问C中C的成员(但是可以访问C中A的成员

可变成员mutable

与const相反的一种成员变量,可以在const成员函数中进行修改。

运算符重载

//这两个函数相当于重载了>>,<<
istream& read(istream& is,mclass m);//read(cin,obj);
ostream& print(ostream& os,const myclass m);//print(cout,obj);

返回*this的成员函数

MyClass& Get(){ return *this; }//相当于返回该对象本身(引用),从而可以级联调用
myclass.Get().Other();//均在myclass对象上调用

MyClass Get(){ return *this; }//相当于返回该对象的副本

如果返回的是一个const引用的*this(成员函数本身是const,或者其返回值是const),那么最终会得到一个const对象,从而无法级联使用非const对象

类的隐式转换

对于提供单参数构造函数的类,可以直接把该参数类型的变量/值隐式转换成类对象,常常用在函数传递参数当中。

class Double{public:double db;Double(double v=0):db(v){}}
void func(Double db);
func(10);//会直接创建一个Double对象

可以通过explicit修饰单参数构造函数来避免隐式转换,该关键字只允许使用在类内部的构造函数中

静态成员

静态成员为类的所有对象所共享的成员static,可以通过类名::静态成员来调用

静态成员变量,静态成员变量的定义只能在类外定义

静态成员函数,不能使用this指针,不能定义为const

访问规则:非静态成员函数可以访问静态,非静态成员,静态成员函数只能访问静态成员

静态成员可以是不完全类型,也可以作为默认构造函数的参数

需要注意不论是静态成员函数还是静态成员变量都需要在类外再声明/定义一遍

继承

一个类作为另一个类的派生类

class base_class
{
static:
    int static_val;//派生结构中只会存在一个静态变量
protected:
    int a;//protected关键字表示保护类型,为派生类提供访问权限
}
class sub_class:public base_class
{
public:
	sub_class():base_class(){}//调用父类构造函数
}

父类指针,引用均可指向派生类的对象,从而实现多态。

必须通过指针或者引用指向派生类对象才能实现多态,仅仅父类对象指向派生类是无法实现多态的(与C#不同)

先进行继承结构中根类的构造再依次往下进行构造。(参数一层层的传递上去后,再一层层地进行构造)

final

类似sealed关键字,如果定义class为final,使得class无法被继承,如果定于函数为final表示其无法被覆盖

class MyClass final{}
class Base{
    void find() final{}
}

虚函数

声明虚函数,表示这个函数可以被派生类重写,并且产生动态绑定。虚函数可以有具体的定义,可以有默认参数,派生类如果重写相应虚函数,返回值与形参必须与基类中的虚函数一致。

//在base中定义为了virtual,则其所有派生类就都是virtual的了
virtual void func(){}
//在sub_base中
virtual void func() override { base::func();}//基类名称::虚函数 调用基类函数

Base* base = new SubBase;
base->Base::func();//强行调用基类函数,无视虚函数动态绑定

通过将子类对象赋给父类的指针或者引用,父类对象也只能使用子类重写父类的那些函数,而不能调用子类自己的函数。可以通过访问虚函数表的形式强行调用子类中新定义的那些虚函数,使用这个方法甚至可以访问定义为private和protected的虚函数

虚函数的实现

虚指针 虚函数表

运行期间的多态通过虚函数,虚函数表,虚函数指针来实现。

对于一个包含虚函数的类对象或者继承的父类中含有虚函数的对象来说,其内存中,首个位置存储的即是虚指针(指向虚函数表的一个指针),可以通过如下操作得到。

base b;
cout<<(int*)(&b)<<endl;//返回虚函数表地址

注意一个类的多个对象是共享一份虚函数表的,各个对象会通过他们本身的虚指针去指向虚函数表

在实现多态的过程中(通过基类指针调用派生类成员函数时),基类指针会通过虚指针得到虚函数表看是否存在满足条件可调用的函数。

单继承情况下的虚函数

如果不存在子类重写父类虚函数时,生成子类对象,其虚函数表中即会存在父类的虚函数,又会存在子类的虚函数,子类的虚函数排在父类的虚函数后面,可以通过如下代码验证:

typedef void (*func)();
class Base{
public:
	virtual void b_func(){cout<<"base"<<endl;}
}
class Subase:public Base{
public:
	virtual void s_func(){cout<<"subase"<<endl;}
}
Subase sb;
(func)(*(int*)*(int*)(&sb))();//(int*)*(int*)(&sb)为虚函数表的第一项
(func)(*((int*)*(int*)(&sb)+1))();

如果子类重写了父类的虚函数,那么此时子类对象虚函数表中原本排在前面的父类虚函数会被重写的子类虚函数代替,重写的这个函数不会出现再后面。当指向该子类对象的父类指针再调用对应函数的时候,遍历虚函数表首先得到的是重写过后的函数,从而调用这个函数,实现了多态。

多重继承下的虚函数表

与单继承类似,区别在于多重继承的类对象其内存位置的头几项会按照继承顺序给出对应基类的虚指针指向虚函数表,如果存在重写,那么重写函数会代替虚函数表中原本基类的函数,类自身的虚函数会添加到第一个虚函数表末尾中。

如果多重继承中存在有些父类有虚函数,有些父类没有虚函数,那么有虚函数的父类,其在子类对象的内存空间中永远会依照继承顺序放置在前面

安全性问题

可以通过访问内存的方式,在父类指针指向派生类对象的时候,访问其私有权限或者保护权限的虚函数。

抽象基类

纯虚函数,不能在类内给出定义,不能给出默认参数,定义了纯虚函数的类为抽象基类,抽象基类不能创建对象,继承了抽象基类的类必须给出纯虚函数的定义,否则依旧是抽象基类。但是抽象基类是可以定义指针与引用的

C++不像C#是没有abstract关键字的,只要有纯虚函数就是抽象基类

格式如下;

//定义了纯虚函数的类就是抽象基类
class abstract{
	virtual void func()=0;//virtual 函数定义 =0即为纯虚函数
}
//抽象基类中其他的函数是可以给出定义的

调用构造函数的顺序是从上至下调用

函数的调用与查找

调用类对象函数时,会从当前类的作用域(包含在其基类作用域内),从下往上寻找其基类的作用域,查找有无满足调用名称的函数,再根据其是一般函数调用还是虚函数调用进行检查。

因此,如果虚函数重写时函数定义不一致会导致找不到的情况。

继承中的拷贝控制

虚析构函数

对于一个继承结构来说,基类析构函数需要定义为虚析构函数,这是为了使基类指针指向派生类,delete时能销毁正确的类型。

构造函数

构造函数不能为虚拟构造函数,因为构造函数的调用顺序本身就是沿着继承结构自上而下的

如果一个派生类对象赋值/初始化一个基类对象会调用基类的赋值运算符/拷贝构造函数。

继承中的类型转换

RTTI:运行时类型识别,可以在运行的过程中,得到对象的类型。

**dynamic_cast:能够将基类指针或者引用,安全地转换成派生类。**比如基类A指向了B,创建一个新B变量,将A转换到B。只能转指针或者引用

A* a=new B;
B* b=dynamic_cast<B*>(a);//将a强制转换成b

static_cast: 与dynamic_cast相比,一种更低安全性的数据转换方式,使用该关键字作数据转换是不会进行类型安全性检查的。

typeid:返回类型对于数据的类型

类似C#中的typeof,返回type_info对象,用来判定对象是否是给定类型

int a;
typeid(a)==typeid(int);//返回true
//typeid(a).name()可以返回类型名称

typeid在oop中的使用

如果使用typeid用来计算类型变量,类型指针那么直接得到对应类型的类型/类型指针。

如果使用typeid用来计算基类的解指针,根据基类有无虚函数分为如下两种情况

  1. 基类含虚函数,那么解指针将得到指针具体所指向的对象(如果指向派生类就返回派生类类型)
  2. 基类不含虚函数,那么解指针就得到基类类型,哪怕指向的是派生类

引用同解指针

综上,基类最好保持一个虚函数,如果不存在需要显示定义的虚函数,那么最好给出一个虚析构函数

多继承基础

C++支持多继承,多继承的构造函数调用顺序是其继承列表的先后顺序,从根向下调用构造函数

析构函数正好相反。

class BaseA{public: BaseA(){}}; class BaseB{public: BaseB()};
class Subase:public BaseA,public BaseB
{
public:
	Subase():BaseA(),BaseB(){}
}

拷贝控制会按照构造函数调用的顺序依次调用对应函数的合成/自定义拷贝控制

虚继承

**使得虚继承类的派生类能够共享同一个基类,**用来解决多重继承中,间接继承同一个类导致重复生成对象的问题。保证继承体系中的基类不重复。

格式:class MyClass: public virtual Base

虚继承只影响继承虚继承类的类,不影响虚继承类本身

class DataStruct{}
class Tree:public virtual DataStruct{}//虚继承
class Graph:public virtual DataStruct{}
class RBT:public Tree,public Graph{}

多重继承容易出现二义性,因此不提倡多重继承,A,C继承D,B继承A,C。A,C,D中同时含有x,B对象使用x时就产生了二义性。

虚继承的构造顺序

虚继承中的虚基类会在最底层的对象中进行构造,随后的基类构造函数调用顺序与一般继承类似。如果不显示给出就调用虚基类的默认构造函数。以上述代码为例构造函数顺序如下:

DataStruct,Tree,Graph,RBT

如果存在多个虚基类,那么调用顺序按照继承列的顺序先调用虚基类构造函数

C++关键系统调用

文件输入输出

头文件fstream

涉及的三个类型fstream,istream,ostream,使用方式如下

//输入(是指输入进程)
istream streami("文件名称",fstream::in);//指明文件名称的构造函数自动调用了open函数
streami>>show;//如果打开.txt文件,那么输入的就一定是字符串,仅使用>>遇到空格就不读取了
streami.close();//如果切换打开文件需要先关闭文件流
streami.open("text.txt",fstream::in);//打开新文件
while(getline(streami,show)) //getline每次读取一行,遇到回车就下一行,直到文件末尾
    cout<<show<<endl;
streami.close();

//输出(是指从进程输出到文件)
ostream streamf("文件名称",fstream::app);
//输出的文件模式有out(会清空内容),app(向文件中添加内容),trunc(文件截断)
for(int i=0;i<n;i++)
{
    streamf<<out;
    //为方便文件读取,每次输入完后还跟一个'/n
    streamf<<'/n';
}
streamf.close();

内存管理

C++内存分区一共抽象为5个区域

代码区

全局数据区(全局,静态变量)

常量区

堆区(动态变量) 栈区(局部变量)

栈区与堆区的区别

栈上用来存储局部变量,包括函数传递的参数,返回值等等,由系统来释放内存。

堆是用来动态地给变量分配内存的,在C++中,动态分配内存以及释放内存需要程序员自行管理。

C++中不存在引用类型,值类型的区别,因为内存空间都是由程序员管理的

new,delete

new与malloc区别

  1. new是运算符 malloc是库函数
  2. new会调用构造函数,malloc只会申请内存
  3. new返回指定类型的指针,malloc返回void指针
  4. new可以自动计算所需空间大小,malloc需要手动设置空间
  5. new可以被重载

new->operator_new()->malloc()->构造函数

delete与free区别

  1. delete是运算符 free是库函数
  2. delete会调用析构函数,free会释放内存
  3. free要检查是否为空,delete则不会

delete->析构函数->operator_delete()->free()

delete只能删除动态分配的空间,在delete后,如果还需要使用该指针需要为其赋值nullptr,否则会变成野指针

NULL与nullptr区别

内存对齐

所谓内存对齐是指CPU指令操作的内存地址能够被其操作的内存大小整除。

对于内置类型来说对齐量就是其大小

对于自定义类型来说对齐量是其非静态成员变量中最大的那个变量,如果有些成员变量不满足对齐量要求,会进行空白填充。

class MyClass
{
public:
	int a;//占4字节
    char ch;//占1字节
	short s;//占2字节
};
//因此对齐量是4字节,对齐量必定是2的倍数
//a大小直接满足对齐要求占第1,2,3,4个字节
//c占第5个字节,为了满足对齐要求,空白6,7,8字节
//s同理,满足9,10字节后空白11,12字节
//因此最终大小是12

如果继承结构中存在虚函数,那么类对象还会中还有一个4字节的指向虚函数表的指针,所有函数本身都存放在代码区。

可以通过alignas来显示指定对齐量

class alignas(16) MyClass{
public:
	int a;//占4字节
    char ch;//占1字节
	short s;//占2字节
}
//这个对象最对齐量是16,直接把大小扩展到16

之所以要内存对齐是由于内存不对齐,CPU的性能可能会降低甚至报错

对齐量不能设置小于默认对齐

内存泄漏的原因

  1. 类的构造函数与析构函数中没有调用匹配的new,delete函数,导致对象被销毁后,成员指针所指向的空间没有被释放。
  2. 释放数组时没有使用方括号
  3. 释放指针对象数组时,仅仅使用方括号,销毁的是指针占用的空间,而没有销毁指针所指向对象占用的空间,需要循环处理
  4. 没有将基类的析构函数定义为虚函数

RAII

将对象的资源管理和对象的生命周期绑定。智能指针可以说是RAII的一种体现,在对象生命周期结束时同时释放资源,补足了C++本身容易导致内存泄漏的短板。

静态链接与动态链接

静态库函数的链接是放在编译时完成的

动态库把对一些库函数的链接载入推迟到程序运行时

两者都是共享代码程序复用的方式

静态链接优点:方便移植,程序运行时静态链接库以及已经完成遍历了,编写使用方便

动态链接优点:避免浪费内存空间,如果不同的应用程序使用相同的dll,那么内存中只需要有一份该共享库的实例。可以实现进程的资源共享,且方便升级。

C++常用异常类型

标准头文件为stdexcept,常用异常如下:

throw out_of_range(string str);//越界异常,并显示指定字符串

C++泛型编程

模板编程是泛型编程的基础,所谓泛型编程指的可以根据传递的类型作为参数来设计程序。主要用于设计函数与类类型,只有给出特定类型实参,编译器才会编译一个特定的函数/类实例。

模板的头文件即包括定义也包括声明

函数模板

对于函数模板,编译器可以根据所传输的参数,来推断模板参数,从而实例化一个模板,因此有些函数在调用时,可以不给出<>,前提,该模板函数形参不能为模板参数的左值引用

template<typename T,typename P>
T func(T t,P p){}

//一个自定义find模板类的函数
template<typename T,typename V>
bool find(T t,V v)
{
    for(auto it=t.begin();it!=t.end();it++)
        if(*it==v)
            return true;
    return false;
}

在typename中给出的模板形参可以用来作为函数的形参类型,返回值类型,或者局部变量定义类型。

非类型模板形参

模板中的参数不一定非得是类型,还可以是非类型,如下的非类型可以作为模板形参

  1. 整数
  2. 函数指针
  3. 对象指针/引用

但是当使用非类型模板形参时,只能传递字面值/静态值给该函数,来使用该模板,一个例子如下:

template<int N>
void showSize(const char (&p)[N]){
    cout<<N<<endl;
}
//使用时
showSize("Raven");

模板程序需要尽量减少对类型的要求

类模板

编译器无法进行模板类型推断,需要在<>中显示指定参数

//设计类类型
template<typename T>
class Array{
    typedef std::vector::size_type;
    private:
    	 len;
    	T* data;
    public:
    	static size_t count=0;
    	T& operator[](size_t i) noexcept;
}
//如果在类中定义成员函数,在类外给出声明时,需要指定其template和class的模板类型参数
//其成员只有在被使用时,才会被实例化
template<typename T>
T& Array<T>::operator[](size_t i) noexcept{
    return data[i];
}

//并且定义类模板中对象时,使用域解析前要给出<>中指明类模板的模板形参
template<typename T>
size_t Array<T>::count=0;

//可以使用typedef,using为给定了实参的模板进行别名定义
using array_int=Array<int>;
typedef Array<int> ints;

//甚至可以为类模板本身定义别名
template<T> using twin=pair<T,T>; 

当类模板中使用其他类模板时,同样需要显示指明模板参数,并且该参数往往就是这个类模板的模板形参。如果在模板类内使用该类本身,可以不指定模板参数

template<typename T>
class BlobPtr{
public:
	...
     BlobPtr& operator++();//重载的++运算符就可以不提供模板形参
}
//在类外实现该成员函数时就需要提供模板参数了
template<typename T>
BlobPtr<T> BlobPtr<T>::operator++()
{
    //由于在模板内部,因此可以不提供参数,默认使用模板参数T
    BlobPtr ret=*this;
    ++*this;
    return ret;
}

类模板的友元关系

需要涉及到前置声明问题,一共有如下几种友元策略

  1. 模板A是模板B特定实例的友元(往往在统一模板形参下的一对一友元)

    需要前置声明

    template<typename T> class Ptr;
    //当模板Ptr与模板Array使用相同模板形参时,Ptr是Array友元
    template<typename P>
    class Array{
        friend class Ptr<P>;
    }
    
  2. 模板A的所有实例/特定实例是非模板B的友元

    template<typename T>
    class Color;
    
    //Node的所有实例都是DataStruct的友元
    //Color的string参数的实例是DataStruct的友元
    class DataStruct{
        //如果要将所有实例作为友元,那么直接在友元的类内定义模板友元类
        template<typename T> friend class Node;
        //如果要设置特定实例为友元,那么必须前置声明需要友元的模板类
        friend class Color<string>;
    };
    
  3. 非模板类B是模板类A所有实例的友元

    这种设置方式与模板类A的所有实例是另一个类的友元类似,直接在A中定义友元类即可,无需前置声明。

    template<typename T>
    class A{
        friend class B;
    }
    
  4. 实例化模板的参数为模板的友元,使得该参数类型的量,可以访问模板类的私有成员(有什么用?)

    template<typename T>
    class Resolve {
    	friend T;
    };
    

模板类的静态成员

对于每个给定类型参数的模板类来说,其所有成员会共享一个static参数。

模板类形参域解析的二义性

对于一个模板类形参T,通过域解析符调用其成员时,编译器无法得到其调用的对象是一个类型还是一个静态成员,因此必须要使用typename显示指定其调用的是类型。

template<typename T>
class RBT{
public:
    //如果不用typename前置声明,那么编译器将无法得知下述语句做的是乘积运算,还是指针声明。
    T::Node *node;
}

一个使用typename显示指定类内类型的一个例子

template<typename T>
void showEach(T& docker)
{
	typedef typename T::size_type size_type;
	for (size_type i = 0; i < docker.size(); i++)
		i != docker.size() - 1 ? std::cout << docker[i] << " " : std::cout << docker[i] << std::endl;;
}

默认模板实参

对于函数来说,提供默认模板实参,直接在模板参数列表中给出类型即可

template<typename T,typename V>
void ForEach(T holder,function<void(V)> f=[](V v){cout<<v<<endl;})
{
    for(size_t t=0;t<holder.size();t++)
        f(holder[t]);
}
vector<int> holder={1,2,3,4,5,6};
ForEach<vector<int>,int>(holder);

类模板实参,与函数模板实参不同,实例化时依旧需要给出<>

template<typename T=int> class Person;
Person<> p;//需要给出空<>

成员模板

模板类中,模板函数为其成员,称为成员模板

普通类的成员模板

在类中声明模板,以及模板函数后,在类外定义时同样要给出模板标签

class Base{
public:
    template<typename T>
    static void show(T t);
}

template<typename T>
void Base::show(T t){
    cout<<t<<endl;
}

类模板的成员模板

在定义时,需要同时给出类的template以及函数的template,先给出类的template,再给出成员函数的template

template<typename T>
class Base {
public:
	template<typename V>
	static void show(T t,V v);
};
//通过decltype得到传入类型参数的具体类型,并通过typeid得到类型信息后,通过.name()返回类型字符串
template<typename T>
template<typename V>
void Base<T>::show(T t,V v)
{
	cout << "类模板实参:" << typeid(decltype(t)).name() << endl;
	cout << "函数模板实参:" << typeid(decltype(v)).name() << endl;
}

控制实例化

对于类模板来说,每次声明都会生成一个类模板代码,从而导致,在多个文件中如果使用同一个模板参数会生成多个重复的类代码,最终导致代码膨胀问题,因此可以通过extern关键字来使得同一个模板参数的类模板只生成一个类代码。(对于函数模板也一样)

//extern.h
template<typename T>
void show(T t){
    std::cout<<t<<std::endl;
}

//one.cpp
//显示实例化这个函数
template void show<int>(int t);

//main.cpp
//外部模板函数,会从链接的文件中找到显示实例化的对应函数并且使用,这样就不需要再重新生成代码了
extern template void show<int>(int t);

模板实参推断

模板函数类型转换

只有const<=>非const转换,以及数组或者函数指针的转换是可以的,其他都不行.

template<typename T>
void fun1(T,T);
template<typename T>
void fun2(T&,T&);

int a[10],b[20];
fun1(a,b);//可行,两者均被转换成了指针类型
fun2(a,b);//不可行,引用无法直接转换
fun1(12,2.5);//不可行,算术转换不支持,当然可以通过定义多个模板参数来解决类型不同问题
//stl中的max和min在使用过程中就会右上述问题

注:如果在函数模板中有默认参数类型,那么依旧能够允许转换。

显示模板函数参数

函数模板参数在定义过程中,<…>中后面的参数如果能够默认提供,那么就可以省略。这种往往用于未知函数类型返回值时。

template<typename T,typename A,typename B>
T Sum(A a,B b){...}
Sum<double>(1,2);//A,B都被初始化为了int,可以省略掉

尾置返回类型

专门用于返回类型较为复杂的情况,比如用来解决在模板编程中需要根据所传入的参数类型其推导来返回类型的情况,需要与auto联用,类似lamda中的返回值应用

//返回所传入容器中指定下标的值
template<typename T>
auto getElement(T t,int i)->decltype(t[i])
{
    return T[i];
}

一个复杂的例子:

如果一个函数要得到迭代器所指向类型的值类型,则光使用尾置返回类型还不够,要与remove_reference联合使用。

template<typename It>
auto getElement(It i)->typename remove_reference<decltype(*It)>::type
{
	return *i;
}
//通过decltype解指针得到引用,通过remove_reference再取引用,通过typename显示指明这是类型

函数指针来实参推断

函数指针可以用来推断模板参数,从而实例化一个模板

template<typename T> T Sum(T t1,T t2){return t1+t2;};
int (*s1)(int,int)=Sum;
//函数指针与函数模板在参数严格对应的情况下可以实例化函数模板
s1(2,3);//correct

模板实参推断与引用

模板函数中参数列表里的模板类型参数往往有如下三种

  1. 左值引用:只能接受左值

  2. 右值引用:可以接受左值与右值,(由于引用折叠机制),当其接受左值时,参数类型会被默认推导为左值引用

    一个很细节的Bug

    template<typename T>
    void G(T&& val)
    {
        vector<T> v;
    }
    int a=10;
    G(a);//会报错,传递左值给右值引用的模板函数,会被折叠成左值引用,T的类型是int&,而容器模板参数不持支引用(因为没法析构)
    

    右值形参接受左值只在类型为模板类型的情况下才能适用

  3. 常量左值引用:万能引用

引用折叠

引用是无法级联引用,即引用的引用,但是在模板参数或者类型别名定义中,可能会出现引用的引用,比如类型是int&,而模板参数是T&&,默认会推导出int& &&这种类型,从而产生引用折叠,引用折叠的规则如下:

  1. && &,& &&,& &都会被折叠成&
  2. && &&会被折叠成&&

再谈移动语义

由于左值无法直接赋给右值引用,所以引入了move语义,move语义本质上是一个模板函数,其核心实现如下:

template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    //通过static_cast强行把左值引用转换成右值引用
    return static_cast<typename remove_reference<T>::type&&>(t);
}

T&&使得move函数即可以接受左值,又可以接受右值。在其接受左值时,T会被转换成T&,而t的类型由于引用有折叠是T&,从而需要remove_reference去引用后再返回右值引用。

转发

在函数模板编程中,将参数列表中模板实参的全部特征完好地传递给在其中调用的其他函数。全部特征指左右值特征,是否是const属性。

基于引用折叠机制,在参数列表中,模板类型参数设置为右值引用时能够保留const与左/右值引用属性

一个很细节的bug:

void show(int &&a,int &b){cout<<a<<" "<<b<<endl;}
template<typename T>
void wrapper(T&& t1,T&& t2)
{
    show(t1,t2);
}
int b=10;
wrapper(10,b);//会报错,调用wrapper后,t1作为int类型,而t2作为int&类型,t1作为左值无法匹配show中右值参数a

上述的例子可知,10本身是纯右值,然而传递参数给t1后在传递给show时会丢失其右值特性,转发不完美。因此引入std::forward(),来保留转发过程中的实参属性。

template<typename T>
void wrapper(T&& t1,T&& t2)
{
    show(std::forward<T>(t1),std::forward<T>(t2));
}

当涉及保留实参特性时使用T&& 作为函数模板参数类型,而如果想要将其转发给另一函数,则配合forward(arg)使用,这个arg往往是参数列表里的右值引用。

综上,保持类型信息是一个两阶段的事情

  1. 参数中给出右值引用来保持值的常量,左右值信息
  2. 通过forward函数来完美转发给在其中调用的函数

模板与重载

模板函数匹配规则

定义函数模板会对调用函数时的匹配规则产生影响

  1. 如果存在同样好的非函数模板函数且能够匹配,则匹配该函数

  2. 如果仅存在模板函数,那么会在同样好的当中匹配最特例化的那个模板

    template<typename T> void show(const T& t);
    template<typename T> void show(T* t);
    string *str=new string("Lostemple");
    //本质上第一个show与第二个show都能接受str,但是由于选择最特例化的,因此会调用第二个show
    show(str);
    
  3. 引入函数模板后,匹配函数更有可能会二义性

  4. 如果可变参数版本与非可变参数版本同样最优匹配,那么会选择非可变参数版本

另一个例子

template<typename T> void f(T){}
template<typename T> void f(const T*){}
int *num=new int(3);
f(num);//调用的是f(T),虽然能同时匹配两个f,但是const涉及模板函数的类型转换,f(T)更加匹配,因此调用f(T)

模板函数重载

与一般函数重载类似,返回值,函数名相同,参数不同。但由于泛型严格的参数转换限制,这里参数不同除了一般重载因素外还指的是指针,引用(左值/右值),模板参数个数等因素的不同。

仅仅只改变参数的const属性是无法重载函数的,但是改变左右值属性是可以的

可变参数模板

C++11可以指定可变个数的模板参数,该可变个数模板参数还可用于模板函数的形参。语法如下:

template<typename T,typename... Args>
void foo(Args... reset){}
//这里的typename...表示Args的参数个数是可变的,在模板函数中Args...表示接受可变参数个数的实参

可以通过sizeof…来得到可变模板参数信息,如上例:

sizeof...(Args);//返回模板类型参数个数
sizeof...(reset);//返回可变模板参数类型所传递的实参个数

上述Args称为模板参数包,reset称为函数参数包

可变参数函数模板

往往用于函数实参个数与实参类型均未知的情况。并且往往与递归联合使用,从而依次获取函数参数包中的每个值(为了递归能退出,必须确保有一个非可变参数的函数)

一个例子

//这个单参数的函数必须要在可变参数之前
template<typename T>
void show(const T& t)
{
    cout<<t<<endl;
}
//如果要定义函数包的引用,格式为Args&...
template<typename T,typename... Args>
void show(const T& t,const Args&... reset)
{
    cout<<t<<endl;
    show(reset...);
}
show(1,2,3,4,5);

上述例子中,每次都会把当前递归层内的函数参数包中首个数据给变量t,剩余函数包参数再传给show(const T& t,const Args&… reset),由于第一个show只接受一个参数,所以只会匹配第二个函数。

一个任意数加法求和的例子

template<typename T>
auto Add(T&& t)->typename remove_reference<decltype(t)>::type{return t;}
//之所以必须提供一个单参数的,是为例Add(args...)的函数匹配
template<typename T,typename... Args>
auto Add(T&& t,Args&&... args)->typename remove_reference<decltype(t)>::type
{
	cout<<sizeof...(args)<<endl;
	if(sizeof...(args)==0)
		return t;
	else
		return t+Add(args...);
}
Add(1,2,3,3.5,4.5);//最终返回14,
/*
首次先得到t=1,args=2,3,3.5,4.5
再递归得t=2,args=3,3.5,4.5
t=3,args=3.5,4.5
t=3.5,args=4.5
t=4.5,再回溯递归
*/

包扩展

扩展操作是将包分解成构成它的元素,通过在包名后跟…(会按照指定格式扩展…前包中的每一项)

对于模板参数包来说,通过在函数模板中给出const Args&…表示接下来传入的每个默认推断类型都会是常量左值引用

对于函数参数包来说,通过给出args…,表示给出一个参数列表

转发参数

组合可变参数模板与forward完美转发来编写函数。通过右值引用保留函数参数包中的类型信息,再通过forward传递给所写函数中的其他函数。

template<typename... Args>
void fun(Args&&... args)
{
    work(forward<Args>(args)...);
}
//一个实例
int n1=10;
int& n2=n1;
const double&& n3=n1;
fun(n1,n2,n3);
//在work函数中三个forward会分别为
//forward<int>(n1)
//forward<int&>(n2)
//forward<const double&&>(n3)
//相当于同时对模板参数包与函数参数包进行了扩展

模板特例化

实例化:根据给定的模板参数去生成对应函数/类的过程

特例化:为原模板的特殊实例在给出模板参数的情况下给出定义。相当于自行完成了编译器完成的实例化过程

如果程序丢失特例化版本,可能会用原模板实例化,从而可能产生错误。因此,所有同名模板极其特例化版本应该放在同一个头文件中,并且特例化版本放在后面。

函数模板特例化

特例化不会影响函数名匹配,非模板函数重载可能会。函数特例化时,需要先前置声明函数模板,给出template<>,并且严格对应模板形参给出实参。

一个特例化的例子:处理字符指针。

template<typename T> void show(const T& str){}
template<size_t N> void show(const char (&)[N]){}
//特例化版本,T类型为const char*的第一个show模板
template<> void show(const char* const& p1){}
show("12345");//调用的是特例化版本
char *str="12345";
show(str);//调用的是常量模板参数版本

类模板特例化

与函数特例化类似,类模板特例化也需要前置声明原模板类,再特例化类模板,特例化格式如下:

template<>//按照顺序在类名后的<>中提供特例化模板实参
class Specialize<Type1,Type2>{...}

类模板部分特例化

类模板的特例化可以只特例化部分成员,在类名后需要给出特例化的模板实参,同时保留未特例化的参数。

template<typename T>
class PartSpecialize<T&&>{...}

特例化部分类成员

特例化的类成员会在当类模板实参为类成员特例化的实参时,调用特例化的对象。

template<typename T>
class Node{
    public:
    void work();
}
//非特例化版本
template<typename T>
void Node<T>::work(){...}

//针对T为string的特例化函数
template<>
void Node<string>::work(){...}
Node nd("node");
node.work();//会调用第二个版本
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值