耕耘和收获不是连贯的,中间还隔着很长一段时间,那就是坚持!
一:模板进阶
1.1:非类型模板参数
template<class T,size_t N>
class arr
{
private:
T _a[N];
};
这里的N就跟define一样,属于非类型模板参数。
1.2:array
int main()
{
array<int, 10> a1;
int arr[10];
cout << arr[11] << endl;
arr[11] += 1;
}
array是c++11提供的一个数组,如果使用c语言的数组
越界读,不检查
越界写,检查
int main()
{
array<int, 10> a1;
int arr[10];
cout << a1[11] << endl;
a1[11] += 1;
}
对于array来说
越界读,检查
越界写,检查
1.3:模板特化
1.3.1:函数模板的特化
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)const//const
{
return x < y;
}
昨天我们的这个仿函数实现了less的思路,但是我们以前实现过日期类
Date* p1 = &d1;
Date* p2 = &d2;
cout<<less(p1,p2);
如果这样的话,那我们就会把两个日期类的地址进行比较,显然结果会出错。
因此我们需要进行模板特化,特化即为特殊化。
函数模板特化的步骤:
有一个基础的函数模板
关键字template后面跟一对<>
函数名后跟一对<>,且<>中指定需要特化的类型
函数形参表:必须要和模板的基础参数类型完全相同。
对于日期类我们可以如下:
template<class T>
bool less(T left,T right>
{
return left<right;
}
template<>
bool less<Date*>(Date* left,Date* right)
{
return *(left)<*(right)
}
因为这样的形式和函数重载一样,函数名相同,形参的个数和顺序一样,只是类型不同。
因此可以直接给出函数,不用写关键字template<>。
1.3.2:类模板的特化
1.3.2.1:全特化
template<class T1,class T2>
class wjw
{
public:
wjw()
{
cout << "wjw<T1,T2>"<<endl;
}
private:
T1 _a;
T2 _b;
};
template<>
class wjw<int,char>
{
public:
wjw()
{
cout << "wjw<int,char>"<<endl;
}
private:
int _a;
char _b;
};
int main()
{
wjw<int, int> w1;
wjw<int, char> w2;
}
让wjw特化成参数为int和char类型的类模板。全特化就是将模板参数列表中所有的参数都确定化。
1.3.2.2:偏特化
1.3.2.2.1:部分参数特化
对于一个类模板
template<class T1,class T2>
class data
{
public:
data()
{
cout << "data<T1,T2>" << endl;
}
private:
T1 _m;
T2 _n;
};
如果只对其中一部分参数进行特化,就叫偏特化的一种。比如这里特化T2为double类型。
template<class T1>
class data<T1,double>
{
public:
data()
{
cout << "data<T1,double>" << endl;
}
private:
T1 _m;
double _n;
};
int main()
{
Data<int,int> d1;
Data<int,double> d2;
}
可以看到,d1用了默认的基础模板,d2调用了特化的模板,因为编译器会自动选择更好的模板,如果d2选用基础模板,T2还需要推导类型,所以直接走特化模板。
但是,这只是第一种偏特化:对部分模板参数进行特化。
1.3.2.2.2:对模板参数的类型限定
第二种偏特化是:对模板参数的类型进行限定。
template<class T1, class T2>
class Data<T1*,T2*>
{
public:
Data()
{
cout << "Data<T1*,T2*>" << endl;
}
};
Data<int*,char*> d3;
这里注意博主弄混淆了一个点:在类模板的特化中,我们这里的偏特化的第二种,是说限制模板参数的类型,比如把T变成T*,把T变成T&,我一开始在想为什么不直接用全特化呢,全特化是指让模板参数变为你需要的一个具体的类型,比如把T变为int*,int等等,他是改变T为一个具体的类型,偏特化是指把T本身的类型改变。
二:继承
2.1:继承概念
是面向对象程序使代码可以复用的最重要的手段,允许程序员在保持原有类的基础上进行拓展,增加功能,这样产生新的类,叫做派生类(子类),以前我们接触的复用都是函数的复用,继承属于类设计层次的复用。
2.2:继承定义
在类和对象我们学习过,访问限定符有public,protected,private,同样继承方式也是这三种。
基类继承派生类的成员访问方式关系如图:
不可见是指,派生类无法直接访问基类的private成员,但是可以通过调用继承过来且可以使用的基类的用于访问基类成员的函数来访问。
在类和对象我们学习过,即使加上private访问限定符,类外不能访问,但是类中可以访问,而这里的继承的不可见是类外类中都不能直接访问。
根据表格,如果不想在类外访问成员,但需要在派生类中访问到,就定义为protected,由此可见protected访问限定符是因为继承才出现的。
如果使用关键字class,默认继承方式是private,使用struct是public,不过最好显式写出继承方式。
总结可以发现,派生类中对基类成员的访问权限=min(基类中该成员访问权限,继承方式)
提倡使用public继承,因为用protected继承也只能在派生类中使用,维护性不强。
2.3:基类和派生类对象赋值转换
class person
{
public:
int _height;
int _weight;
};
class stu:public person
{
public:
int _stuid;//学号
int _major;//专业
};
int main()
{
stu s1;
person p1 = s1;
person* p1 = &s1;
person& p1 = s1;
}
派生类对象可以赋值给基类对象/基类指针/基类引用。形象的说法叫切片,切割。
基类对象不能赋值给派生类
实际上这里赋值的意思就是,把派生类中继承于基类的那一部分变量的赋值给基类。
2.3:继承中的作用域
在继承体系中,派生类和基类都有独自的作用域
派生类和基类有同名成员(变量/函数),子类将屏蔽父类对同名成员的访问,这种情况叫隐藏。
也叫重定义。
成员函数只要名称相同就构成隐藏。
最好不要在基类和派生类定义相同名的成员。
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
问B和A的fun构成什么关系?重载? 重写? 重定义/隐藏? 编译错误?
首先第一点也是最重要的一点,类和对象中我们学习过,函数重载的前提是在同一个作用域中,而这里B和A是2个不同的类,fun当然不会构成重载。
重写是在多态中提出,这里也不会是重写
显然这里是隐藏。
但是如果代码稍微改动一下:
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun();//改动了这里
};
改动了第21行代码,这时候就会出现编译错误,因为你不传参的时候,就是想调用A类的fun函数,但是没有指定类域,所以出错,如果想正确使用,应当是b.A::fun();
2.4:6大成员函数的继承
2.5:继承实战
class Person
{
public:
Person(const char* name)
:_name(name)//自定义类型
{
cout << "Person()构造" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)=重载" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()析构" << endl;
}
protected:
string _name;
};
class Stu : public Person
{
public:
//继承过来的父类成员调用父类的构造函数
Stu(const char* name,int num)
:_name(name)
,_num(num)
{}
protected:
int _num;//学号
};
这里在子类中的构造函数,如果我们想要给继承过来的父类对象进行初始化,我们直接用初始化列表不调用父类的构造函数初始化是不行的,这里规定是死的。
子类如果想要完成父类成员的初始化,必须调用父类的构造函数,如果父类没有提供默认构造,就要显式调用构造函数。这里我们就是显式调用父类的构造函数。改变第二行代码得到以下:也印证了2.4中的第一点。
Stu(const char* name,int num)
:Person(name)
,_num(num)
{}
Stu(const Stu& s)
:Person(s)
,_num(s._num)
{}
拷贝构造也是,需要调用父类的拷贝构造完成父类的拷贝初始化。这里的原理就是2.3的切片,相当于把s这个子类中继承父类的那些东西拷贝给父类。这里印证了2.4的第二点。
Stu& operator=(const Stu& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
operator=也必须调用父类的operator=完成父类的赋值重载。印证了2.6的第三点。
int main()
{
Stu s1("张三", 18);
Stu s2(s1);
Stu s3("李四", 19);
s1 = s3;//此处打断点
return 0;
}
可以看到调用成功,当我们越过第六行代码的断点时,
s1成功被s3赋值。
本篇文章到此结束!