一.仿函数
//仿函数——对象可以像函数一样使用
template<class T>
struct Less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
int main()
{
Less<int> less; //定义一个Less对象
cout << less(1, 2) << endl; //这里的less是一个对象,但是可以像函数一样使用
cout << sizeof(less); //而且大小为1
return 0;
}
二.反向迭代器
end,rend,begin,rebegin是对称的
namespace mst
{
template<class Iterator,class Ref,class Ptr>
struct ReverseIterator
{
typedef ReverseIterator<Iterator,Ref,Ptr> Self;
Iterator _cur;
ReverseIterator(Iterator it) //用正向迭代器构造反向迭代器
:_cur(it)
{}
Self& operator++()
{
--_cur;
return *this;
}
Self& operator--()
{
++_cur;
return *this;
}
bool operator!=(const Self& s)
{
return _cur != s._cur;
}
Ref operator*()
{
Iterator tmp = _cur; //返回前一个位置再解引用,就比如_cur=_head的时候
--tmp;
return *tmp;
}
};
template<class T> //用list来举列子
class list
{
typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
//list的实现...
};
}
三.模板进阶
1.非类型模板参数
非类型模板参数——整形常量(可缺省值,而且类型只能是整型常量,char也算整形,bool类型也可以)
template<class T, size_t N = 20 >
class Array
{
public:
private:
T _a[N];
};
int main()
{
Array<int, 10> a1;
Array<double, 20> a2;
return 0;
}
之前的都是类型模板参数,用T来表示int,double,string之类的
2.模板特化——不能只有特化
①全特化
//普通模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//模板特化——对某些类型进行特殊化处理
//函数模板特化写法 (其实不如直接写函数重载)
template<>
bool Less<Date*>(Date* left, Date* right) //这时候当T是Date*的时候走这条路,其他都走上面那条路
{
return *left < *right;
}
//函数重载
bool Less(Date* left, Date* right)
{
return *left < *right;
}
//类模板特化写法
struct less<Date*>
{
bool operator()(const Date* x, const Date* y)
{
return *x < *y;
}
};
②偏特化
//偏特化——扩大了范围
template<class T>
struct less<T*> //只要是指针,都走这条路,用指针指向的对象去比较
{
bool operator()(const T* l, const T* r) const
{
return *l < *r;
}
};
③半特化——对部分参数特化
template<class T>
class Date<T, int> //只有第二个是int就走这条线
{
date()
{
;
}
T _d1;
int _d2;
};
特化优先走最匹配的
3.模板的分离编译
①预处理,编译,汇编和链接
②分离编译
普通函数会被编译成一堆指令,所以在func.o中有fnc函数的地址,但是没有Add的地址,因为Add还没有实例化,没法确定T,不知道开多大。
先声明了,但是在链接的时候找不到了(因为没有实例化),call不到add函数
所以:模板不支持分离编译会报链接错误,最好解决方案就是在同一个文件里声明和定义,直接就可以实例化,编译时直接就有地址,不需要链接。
四.继承
1.基础知识(父类——共有的对象)
2.访问限定符/访问权限
private和protected 在当前类没有什么区别,但是继承的时候,private在子类(派生类)完全不可见(不能用),但是可以用公共的函数来使用,protected在子类是可见的。
3.赋值类型转换
//父类对象不能赋值给派生类对象,而派生类对象能赋值给父类对象
int main()
{
student s;
Person p = s; //发生了隐式类型转换
return 0;
}
引用,指针,都是取出了子类的切片(一部分)
4.隐藏/重定义
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况就叫隐藏(重定义)。只要函数名相同就构成隐藏。
与函数重载的最大区别:函数重载的两个函数必须在同一作用域里。
class Person
{
protected:
string _name = "mst";
int _age = 18;
};
class student :public Person
{
public:
void print()
{
cout << "name: " << _name;
cout << endl;
cout << "age: " << _age; //作用域是子类(派生类)优先
out << "age: " << Person::_age; //加上域作用限定符就可以访问父类的了
}
protected:
int _age = 22;
};
int main()
{
student s;
s.print();
return 0;
}
5.派生类中的默认成员
对于这六大函数,若派生类不写,会直接用父类里的
class Person
{
public:
Person(const char* name = "mst")
:_name(name)
{
cout << "调用了父类的构造函数" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "调用了父类的拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "调用了父类的==构造" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "调用了父类的析构函数" << endl;
}
protected:
string _name = "mst";
int _age = 18;
};
class Student :public Person
{
public:
Student(const char* name, int num)
:Person(name),_num(num) //如果用子类自己的函数:父类初始化父类的值(用了构造函数),子类初始化子类的值,两者分开执行
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); //注意这里构成隐藏,要写成“Person::”才会调用父类的拷贝构造
_num = s._num; //如果用子类自己的拷贝构造:父类拷贝父类的,子类处理子类的 ,两者互相分开工作,
}
return *this;
}
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}
/子类析构函数完成时,会自动调用父类析构函数,保证先析构子再析构父(构造时先构造父,再构造子)
protected:
int _num;
};
int main()
{
Student s("张三",18);
Student s2(s); //调用了父类的默认拷贝构造
Student s3 = s;
Person p = s;
s2 = s3;
return 0;
}
1.不写六大函数时,子类会自动调用父类的构造析构函数
2.在子类中初始化父类变量时,必须调父类的构造函数 ,不管写不写,只要是父类的,都得调用父类的构造函数来初始化
6.注意
①友元关系不会继承。
②静态变量属于整个类,不论子类还是父类都使用那同一个静态变量。
③不能被继承的类
:父类的构造/析构函数是私有。
7.多继承
菱形继承导致的问题:数据冗余和二义性
解决方法:用虚拟继承
class Person
{
public:
string _name;
};
class Student : virtual public Person //虚继承
{
protected:
int _num;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};
void test()
{
Assistant a;
a._name = "zhangsan";
a.Student::_name = "xx"; //用了虚继承,不论是a.Student::_name还是a.Teacher::_name,都指向了同一个_name
a.Teacher::_name = "yyy";
}
int main()
{
test();
return 0;
}
只有一份a,所以放到了公共位置,通过偏移量来找a。这里b和c存的地址指向的是其对应的偏移量。无论有多少个d变量(d1,d2...),所有的d变量的指针都是一样的(0x00aacd8c),都对应着相同的偏移量。
eg:
面试题:
初始化列表跟顺序无关,只跟声明的顺序有关,谁先声明,谁就先初始化。谁先被继承,谁就先被声明,因为D最后声明,所以D肯定在最后。
先执行A(s1),在执行B(),然后C(),然后D 。 因为A先被声明,D中B先被继承,C后被继承。
多继承的指针偏移:
P2会自己指到自己的区域
8.继承与组合
//公有继承
class A
{};
class B : public A
{};
//组合——组合也是一种复用 适配器就是一种组合
class C
{};
class D
{
private:
C _cc;
};
耦合度(项目之间的相互影响)越低越好
五.多态 ——某个行为针对不同对象有多种形态
1.多态的条件
①虚函数的重写——三同(函数名,参数,返回值)
特例1:当父类带virtual,子类不带virtual时,构成接口继承——接口继承: 接口(函数名)继承,函数体重写,所以可以不加virtual,注意,其中缺省的参数也继承了。
注意: 多态中父类和子类的析构函数也要加virtual
// 虚函数 把virtual加到一个函数的前面构成虚函数
class Person
{
public:
virtual void buy(int x) { cout << "成人票" << endl; }
};
class Student : public Person
{
public:
//如果加了virtual,而且函数名,参数,返回值都相同,那么会构成重写/覆盖 ,不加virtual就是隐藏
virtual void buy(int y) { cout << "学生票" << endl; }
};
void func(Person& p)
{
/不满足多态——看调用者的类型,调用这个类型的成员函数 (当p.buy()时,调用的都是person的,当s.buy()时就相反)
/满足多态——看父类指针指向的对象类型,调用这个被指向的类型的成员函数(指向(引用)父类调父类,指向子类调子类)
p.buy();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
特例2:返回值可以不同,但必须是父子关系的指针或者引用,构成协变
class Person
{
public:
virtual Person* buy()
{
cout << "成人票" << endl;
return this;
}
};
class Student : public Person
{
public:
//如果加了virtual,而且函数名,参数,返回值都相同,那么会构成重写/覆盖 ,不加virtual就是隐藏
virtual Student* buy()
{
cout << "学生票" << endl;
return this;
}
};
②用父类的指针或者引用去调用(传一个父类指针或引用过来) 必须是父类!!!
面试题:
这里的A* this 是隐藏的父类指针。
2.final和override关键字
①final:加载虚函数后面,表示该虚函数不能再被重写
class Person
{
public:
virtual void buy() final { cout << "成人票" << endl; }
};
②override:检查是否完成重写,没有完成就报错,完成了重写就无事发生
class Student : public Person
{
public:
virtual void buy() override { cout << "学生票" << endl; }
};
3.函数重载, 函数重写和函数重定义
4.抽象类
class Car
{
public:
//纯虚函数——>抽象类——>不能实例化出对象
virtual void Drive() = 0; //这是一个纯虚函数
};
5. 多态的原理
1.引入问题
Base的实际内存分布如下:
这里实际上隐藏了一个虚函数表指针(占4个字节)
2.虚函数表
注意:
1.跟虚拟继承一样,person1,person2,person3...同类的对象共用一个虚函数表(里面放着虚函数)。
2.虚函数表:本质是一个函数指针数组(虚函数指针数组),虚函数表的指针(虚表指针)指向它
3.子类虚函数重写时:先把父类的虚函数表拷贝过来,重写的虚函数才覆盖,不重写的不覆盖
3.为什么只能是父类的指针或引用
原因:子类对象切片拷贝时(赋值时)不会拷贝虚表,只拷贝成员 。
4.打印虚表的函数
//实际上应该是这个样: typedef void(* )() VF_PTR;
typedef void(*VF_PTR)(); //函数指针
void PrintVFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; i++) //只有vs环境下,虚表的最后一个才是nullptr(vs的改进)
{
printf("[%d]:%p\n", i, table[i]);
}
cout << endl;
}
int main()
{
Base b;
Derive d;
PrintVFTable((VF_PTR*)*(int*)&b); //只适用32位,64位用long long
//为了只去虚表的第一个指针(头四个字节),所以把b的地址转换成int*类型,之后在转回VF_PTR*(函数指针数组类型)
//如果是PrintVFTable(&b),那么解引用就是一整个虚表
PrintVFTable((VF_PTR*)*(int*)&d);
//PrintVFTable((VF_PTR*)&d); 结果是传不过去
PrintVFTable(*(VF_PTR**)&d); //这样也可以 先强转类型再接引用
return 0;
}
5.其他细节
1.虚表是什么阶段生成的——编译
2.对象中虚表指针是在什么时候初始化的——构造函数的初始化列表时
3.虚表存在哪里——代码段(常量区)
4. 多继承中派生类中新增加的虚函数放在第一张(先继承的那个)的虚表。
5.静态多态:编译时 一般是函数重载,根据传的参数去进行匹配
动态多态:运行时 运行起来之后去虚表里面去找(指针指向谁,调用谁)
总结题: