目录
模板
函数模板
类模板
非类型模板参数
模板的特化
函数模板特化
类模板的特化
为什么普通函数可以分离?
继承
继承概念
基类和派生类对象赋值转换(切割,切片)
隐藏
派生类的默认成员函数
.复杂的菱形继承及菱形虚拟继承
继承的总结和反思
模板
函数模板
函数模板的概念:
模板函数的语法格式:
template<typename T,typename V>
void Func(T t,V v)
{// ...
}
函数模板的原理:

类模板
类模板的概念:
类模板与函数模板一样代表了一个类家族,该类模板与类型无关,在使用时需要显示实例化。
类模板的定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类.
非类型模板参数
namespace bite
{// 定义一个模板类型的静态数组template<class T, size_t N = 10>class array{public:T& operator[](size_t index){return _array[index];}const T& operator[](size_t index)const{return _array[index];}size_t size()const{return _size;}bool empty()const{return 0 == _size;}private:T _array[N];size_t _size;};
}
模板的特化
概念:通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
函数模板特化
template<class T>//针对所有类型
bool Less(T left, T right)
{return left < right;
}template<> //对于int类型的特化,当传入int类型时,会执行该函数
bool Less<int>(int left,int right)
{return left<right
}
类模板的特化
1.全特化:将模板参数列表中所有的参数都确定化
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;
};
template<>
class Data<int, char>
{
public:Data() {cout<<"Data<int, char>" <<endl;}
private:int _d1;char _d2;
}
2.偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;
};
偏特化有两种表现方式:
1 部分特化,将模板参数类表中的一部分参数特化
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:Data() {cout<<"Data<T1, int>" <<endl;}
private:T1 _d1;int _d2;
};
2.参数跟进一步的限制
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}private:
T1 _d1;T2 _d2;
};
模板声明和定义不支持分离写道.h,和.cpp中
原因:编译器在编译时,是对每个文件进行独立编译,如果分开定义,我们在编译时由于是模板,没有实例化,编译器不知道该如何生成这段代码,所以不会编译这段代码,在调用时由于没有具体的函数地址,则无法被编译器所识别。
// add.h(声明,放头文件)
template <typename T>
T add(T a, T b); // 只声明,不定义// add.cpp(定义,放源文件)
#include "add.h"
template <typename T>
T add(T a, T b) { // 定义实现return a + b;
}// main.cpp(使用模板)
#include "add.h"
int main() {add(1, 2); // 尝试使用 add<int>return 0;
}
-
编译
main.cpp
时,编译器无法生成add<int>
:
main.cpp
只包含add.h
,只能看到模板的声明,看不到add.cpp
中的定义。没有完整定义,编译器不知道如何生成add<int>
的实际代码,只能暂时记下 “需要一个add<int>
函数”。 -
编译
add.cpp
时,编译器也不会生成add<int>
:
add.cpp
中有模板的完整定义,但编译器不知道main.cpp
会用int
类型实例化它。C++ 标准规定:编译器不会主动为模板生成所有可能的实例(因为类型是无限的,比如int
、double
、string
等),只会在看到具体实例化请求时才生成。 -
链接阶段报错:
链接器需要把main.cpp
中对add<int>
的引用和实际代码关联起来,但此时两个目标文件中都没有add<int>
的二进制指令,因此会报 “未定义的引用” 错误。
为什么普通函数可以分离?
普通函数(非模板)的声明和定义可以分离,因为:
- 编译器在编译
.cpp
时会为普通函数生成完整的二进制指令(比如int add(int a, int b)
的代码会直接生成在add.o
中)。 - 链接时,
main.o
中对add
的引用可以直接找到add.o
中的二进制代码。
而模板函数没有 “默认生成” 的代码,必须等待具体类型的实例化请求,这就要求实例化时必须能看到完整定义,所以我们建议将模板的声明和定义放到一个文件中
继承
继承概念
基类和派生类对象赋值转换(切割,切片)
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换
隐藏
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)且不构成重写
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员
派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
.复杂的菱形继承及菱形虚拟继承

class Person
{string _name;
}:class Student :public Person
{int _num
};class Teacher : public Person
{int _id;
};class Assitant: public Student,public Teacher
{stirng _majorCourse;
};void Test ()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
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 = "peter";
}
class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
下图是菱形继承的内存对象成员模型:可以发现有数据冗余问题
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
继承的总结和反思
1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
3. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。