1:泛型编程
2:模板
2.1:函数模板
2.2:类模板
3:const成员函数与非const的区别
4:构造函数之初始化列表
4.1:初始化列表语法及其应用
4.2:explicit关键字
5:static成员变量,static成员函数
1:泛型编程
首先在提出泛型编程前我们先来看一下的代码(关于swap函数)。
void swap(int& x,int& y)
{
int tmp=x;
x=y;
y=tmp;
}
void swap(double& x,double& y)
{
double tmp=x;
x=y;
y=tmp;
}
void swap(char& x,char& y)
{
char tmp=x;
x=y;
y=tmp;
}
.......
我们知道因为c++支持函数重载所以我们能实现swap函数针对不同类型进行交换,但是上面不同swap函数的代码实现是非常相似的、冗余的除了类型不同,其他的代码实现逻辑是一样的。那么这样我们开发的时候效率可能会变低,代码的可维护性比较低,一个出错则其他的全部出错。
所以我们的c++提供了一种编程技术,泛型编程:编写与类型无关的通用代码,是代码复用的手段。模板是泛型编程的基础。
2:模板
对于模板的理解:我们可以这样理解,模板与我们生活中的模具类似,模板只需要提供一份代码就能让编译器自动推演生成对应的具体类型函数。
模板可以分为:函数模板与类模板
2.1:函数模板
函数模板代表一个函数的家族,函数模板与具体类型无关。在使用时会被编译器实例化成具体类型的函数版本。
语法:(关键字)template<typename T1,typename T2.......>
函数返回值 函数名(函数参数)
如下面的代码:
template<typename T1>
void Swap(T1& x,T1& y)
{
T1 tmp = x;
x = y;
y = tmp;
}
如上图我们所写的程序,我们相当于只写了一份函数模具,在编译器的处理下会生成两个不同版本的Swap函数,这两个函数调用时候的并不是同一个函数。
T1:被称为模板参数,定义的时候可以有多个模板参数,typename可以与class替换。
注意:一个模板参数只能被实例化成一种类型,通常被实例化第一次传参的类型。
class与typename可以相互使用。
函数模板的原理:函数模板不是函数,本质是我们写的模具,在使用的时候会被编译器生成不同的函数,本质是让编译器做的更多,我们只需要写一份就行。
如下图swap函数模板实例化被编译器
在编译器编译阶段,编译器需要根据传入实参的类型从而生成出对应类型的函数以供调用。
函数模板在使用的时候,模板参数必须被实例化。
函数模板实例化:用不同类型参数,生成不同的函数。
有两种方式:隐式实例化,显示实例化。
隐式实例化:编译器根据实参来自动实例化。
显示实例化:语法:函数名<类型>(实参),有些场景必须使用显示实例化,假如我们无参数的函数,或者有但是与模板参数无关。
swap<int>(x,y);
swap<double>(x,y);
被指定准确的类型后,如果变量不是相应的类型那么就会自动隐式类型转换成相应的类型。
比如说 x是double类型,y是int类型。那么调用swap<int>(a,b)的时候,a会隐式类型转化为int类型。
模板参数的匹配问题
一个具体的函数可以与模板函数同时存在比如说以下代码:
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
上面就是模板函数与非模板函数同时存在的咧子,匹配规则:
1:有匹配的就不用生成对应的模板函数了。
2:无匹配的并且满足对应的函数模板语法就会使用模板函数生成对应的类型函数。
模板函数不允许自动类型转化,但普通函数可以。
这里的意思是指假如我们没有匹配的非模板函数,那么编译器在生成模板函数的时候不会将我们的实参从一种类型转化成为其他类型。
2.2:类模板
语法:template<class T1,class T2,class T3.........>
class 类模板名 {类内定义}
比如我们所写的Stack类:
template<class T1>
class Stack
{
public:
Stack(int capacity =4)
{
_a=new T1[capacity];
_top=0;
_capacity=capacity;
}
~Stack()
{
delete[] _a;
_a = nullptr;
_top = 0;
_capacity = 0;
}
//
......
private:
T1 *_a;
int _top;
int_capacity;
}
在这里我们就实现了一个类模板了,能通过显示实例化定义不同的Stack对象。
类模板的实例化只能通过显示实例化进行。
如下图:
3:const成员函数与非const的区别
首先const成员函数是类里面特有的,将const修饰的成员函数称为const成员函数。
主要修饰的是对象创建后的函数,既构造函数不受这个const的影响
实际这个const修饰的是隐藏的this指针--->const Stack* this,表示的是this指针所指向的成员变量不能被修改。
const成员函数,是在类内部成员函数最后加一个const修饰的,这里的const给的是this指针-->const Stack* this
假设我们const是对象,此时就不能调用非const成员函数。因为权限不能放大 const--->非const 权限放大,非const-->const权限缩小,权限可以缩小/平移但是不能放大,因此上面的st2去掉用非const函数属于权限放大不能执行,所以编译就不通过。
因此我们对于const成员函数的定义规则如下:
1:对于那些需要修改成员变量内容的成员函数(构造函数除外)不能使用const。
2:能定义成const成员函数就尽量定义成const成员函数,这样非const对象也能调用const成员函数。
4:构造函数之初始化列表
我们前面已经学过了构造函数的语法了,我们知道我们在定义构造函数的语法,但是我们了解的还不够,所以我们还得学一个语法:构造函数的初始化列表。
首先我们知道:类在实例化(定义)的时候才可以开辟空间,而我们的初始化列表则是每个变量定义的地方,我们先来通过代码看看初始化列表的语法是怎么样的。
这样的就是初始化列表的写法:冒号开始,逗号分开,符号中间填成员变量初始化内容。
对于初始化列表的理解:
1:一些必须得定义的成员变量必须在初始化列表中进行初始化,函数体内不能进行初始化,比如 引用&,const 成员变量,自定义类型无默认构造函数(可以不需要传参数的)
2:我们的构造函数先走初始化列表在走函数体内初始化,我们尽量使用初始化列表初始化,因为自定义类型成员变量一定会先走初始化列表初始化。
3:不能完全放弃函数体内初始化,因为初始化列表有些功能是做不到的,比如说开空间失败的时候,我们就需要使用函数体内来判断是否开辟失败了。因此初始化列表与函数体初始化是可以混着用的。
4:初始化列表如果有没定义的成员变了,那么对于内置类型初始化列表会给缺省值,而自定义类型成员我们回去调用他的初始化列表。
特殊的语法:初始化列表初始化的顺序与成员变量的声明顺序相同。
这个代码的值分别是多少呢?
如果我们按照从上往下的走那么我们的答案因该是: 1 1
但是我们的答案确实 1 随机值
因为我们的_a2先声明的,所以我们也先走_a2(a1),而此时_a1是随机值所以我们就得到了_a2是随机值。
因此我们需要记住:初始化列表初始化的顺序与成员变量声明的顺序相同。
4.2 explicit关键字
在我们语法当中,我们的内置类型对象可以隐式类型转换成为自定义类型对象,而支持这样转化的语法是当我们构造函数只有单参数构造函数(支持传一个参数或多参数缺省值的也可以)发生这样的转化,如果我们不想要这个隐式类型转化发生那么我们就可以在构造函数前面加上explicit关键字,防止类型转化的发生。
如下面的代码:
那么为什么这种写法成立呢?
其实本质上是先用3去构造了一个A类型的临时对象,然后在用这个临时对象去拷贝构造a2.
这种隐式类型转化是非常广泛使用的,在后面的STL数据结构中会涉及。比如说push_back(1);
5:static成员变量与成员函数
static成员变量:属于类的成员变量,并不只属于某个对象,但是是每个对象可以调用他.
声明与定义是分离的,不能在类内定义,在类外也不需要static关键字,得在类外并且指定类域找到这个成员变量定义,静态成员变量不走初始化列表。
访问静态变量的时候: 类名::成员变量 对象.成员变量
计算的类的时候不需要计算static成员变量的大小。因为static成员变量的定义是在类外定义的。
static成员函数:静态成员函数无this指针,不能访问任意非静态成员。
总结:静态成员变量本质上就是受访问限定付与类域限制的全局变量,静态成员函数本质是受类域和访问限定符影响的全局函数无this指针。