C++
2. 面向对象
2.1 构造与析构
2.1.1 构造函数
含义:使用一个类的对象时,为其分配完空间后,一般需要对创建的对象的属性进行初始化的操作,这个操作就可以在构造函数中完成。是对象生命周期的起点
函数定义:无返回值,无返回类型,与类名同名,可以有不同的重载。
// 定义
class Person{
public:
Person(){
cout << "无参构造" << endl;
}
Person(int age){
cout << "有1参构造" << endl;
}
Person(int age, int score){
cout << "有2参构造" << endl;
}
};
// 调用
int main(){
// 创建对象的时候调用构造函数
// 1.显式调用
Person xiaoming1 = Person(); // 无参构造
Person xiaoming2 = Person(18, 100); // 有2参构造
Person xiaoming3; //Person xiaoming3()不能加括号,否则会默认在定义函数
Person xiaoming4(18, 100);
// 2.隐式调用
Person xiaoming5 = {}; // 无参构造
Person xiaoming6 = 18; // 有1参构造,如果大括号内只有一个值{18},可以省略大括号
Person xiaoming7 = {18, 100}; // 有2参构造
// 使用new在堆上创建对象,如果调用无参构造函数,小括号可以省略不写
// 使用new的方式,在堆上创建对象,这个过程叫实例化
Person* xiaogming8 = new Person;
return 0;
}
explicit 关键字:修饰构造函数,写在构造函数之前,表示不能通过隐式调用构造函数。
explict Person(int age){
cout << "有1参构造" << endl;
}
注意:
- 如果在一个类中没有写任何的构造函数,系统会为其添加一个public权限的无参构造函数,可以让我们创建对象。
- 如果我们给一个类写了构造函数,系统不会再为我们提供任何默认的构造函数。
如果我们为一个类写了有参的构造函数,一般会同时写无参的构造函数。
// 构造使用
class Person{
public:
string name;
int age;
string gender;
int score;
// Person(){
// name = "";
// age = 0;
// dender = "";
// score = 0;
// }
Person(): name(""), age(0), gender(""), score(0){ } //初始化列表赋值
// Person(string n, int a, string g, int s){ n和name不能相同,a和age不能相同……,同名无法区分
// name = n;
// age = a;
// gender = g;
// score = s;
// }
person(string n = "", int a = 0, string g = "", int s = 0): name(n), age(a), gender(g), score(s){ }
person(string name, int age, string gender): name(name), age(age), gender(gender){ } //初始化列表赋值则可以同名,会自动区分
};
拷贝构造:根据一个对象,拷贝出另外一个对象,即开辟一块新内存给新对象,将一个对象的属性值拷贝到新对象中。
系统自动提供拷贝函数。
新对象与旧对象的内存地址不同,但是属性值相同。
class Person{
public:
string name;
int age;
Person(){}
Person(string name, int age) : name(name), age(age) { }
//自己定义拷贝函数,const常量,Person&p表示引用,引用传递,p是引用,p指向xiaoming,p和xiaoming指向同一个对象
Person(const Person& p){
cout << "调用了拷贝函数" << endl;
name = p.name;
age = p.age+1;
}
};
int main(){
Person xiaoming = Person("xiaoming", 18);
Person xiaohong = xiaoming;
cout << "小明的年龄" << xiaoming.age << endl;
cout << "小红的年龄" << xiaohong.age << endl;
return 0;
};
2.1.1 析构函数
- 析构函数是对象生命周期的终点,在对象空间被销毁之前调用。
- 在析构函数中,一般进行资源的释放,堆内存的销毁。
- 不能重载,一个类只有一个析构函数。
- 使用~开头,不能有参数。
class Person{
int a;
public:
Person() : a(0) {} // 初始化成员变量a为0
~Person(){
cout << "析构函数调用" << endl;
}
};
int main(){
Person p;
return 0;
}
2.2 浅拷贝与深拷贝
- 浅拷贝:在拷贝构造函数中,直接完成属性的值拷贝。即拷贝对象和被拷贝对象共享同一个内存空间,修改其中一个对象,另一个对象也会改变。
- 深拷贝:在拷贝构造函数中,创建出来新的空间,属性中的指针指向的是新的空间。
class Cat{};
class Person{
public:
int age;
Cat* pet;
Person() {
age = 0;
pet = new Cat; //其实存放的是堆内存中Cat的地址
}
//拷贝构造函数
Person(const Person& p){
//这里默认是浅拷贝
age = p.age;
pet = p.pet; //其实传递的是堆内存中Cat的地址,所以拷贝后pet指向的是同一个地址
}
~Person(){
if (pet != nullptr){
delete pet;
pet = nullptr;
}
}
};
int main(){
Person xiaoming;
Person xiaohong = xiaoming;
//栈空间里销毁内存是先进后出,所有是先释放xiaohong,再释放xiaoming
//先释放xiaohong的时候,会调用Cat的析构函数,pet被销毁(即存放Cat的堆内存被销毁)
//当释放xiaoming的时候,pet已经被销毁(即存放Cat的堆内存已经被销毁),程序就会出现异常
return 0;
}
//拷贝构造函数
Person(const Person& p){
age = p.age;
//深拷贝
pet = new Cat; //重新在堆内存中为申请一块空间Cat,将地址赋值给pet
pet->age = p.pet->age;
pet->name = p.pet->name;
}
2.3 this指针
- this指针是一个隐含的指针,它指向当前对象的地址。
- 当前对象:谁调用这个函数,this就指向谁。
class Person{
public:
int age;
int getAge(){
return this->age;
//绝大多数情况下this->age和age是一样的,且this->可以省略
//但是如果有局部变量与成员变量同名,为避免混淆,this->不可以省略,否则访问的是局部变量
}
};
int main(){
Person xiaoming{10};
Person xiaohong{18};
cout << xiaoming.getAge() << endl;
cout << xiaohong.getAge() << endl;
return 0;
}
this设计函数,返回对象本身
class MyNumber{
private:
int n;
public:
MyNumber():n(0){ }
MyNumber& add(int n){ // &引用
this->n += n;
return *this; //this是一个指针,需要返回对于内存*this
}
void display(){
cout << n << endl;
}
};
int main(){
MyNumber m;
m.add(1).add(2).display();
return 0;
}
空指针访问成员函数, 但是必须保证这个函数里面不能出现this进行空间访问。
class P {
public:
int age=0;
void fun1(){
cout << "fun1执行" << endl;
};
void fun2(){
if (this == nullptr){
cout << "this is nullptr" << endl;
return;
};
cout << "age=" << this->age << endl; //this是空指针,不能访问成员变量,如果运行这段,会出错
cout << "fun2执行" << endl;
}
};
int main(){
P *p = nullptr;
p->fun1();
p->fun2();
return 0;
}
2.4 const
2.4.1 常函数
- 使用const修饰,例如:
void fun1() const{ }
。- 常函数中不能修改成员变量的值。
- 常函数中不允许调用普通函数,只能调用其它函数。
2.4.2 常对象
- 使用const修饰对象,例如:
const Person p1 = Person("xiaoming", 18);
。- 常对象可以读取任意属性的值,但是不允许修改。
- 常对象只可以调用常函数。
2.4.3 mutable
- 用来修饰属性的,表示可变。
- mutable修饰的成员变量,在常函数中可以修改,也可以由常对象修改。
例如在对象中mutable int age;
2.5 友元
友元: 什么是友元?友元就是类A中的成员函数,可以访问类B中的私有成员。
假设客厅是public,都可以访问,但是卧室是private,只有自己可以访问,给予一个例外,让自己好朋友也可以访问,就是类A中的成员函数,可以访问类B中的私有成员。
2.5.1 全局函数做友元
class HOME {
friend void gotoBedRoom(HOME* home);
public:
string livingRoom = "客厅";
private:
string bedRoom = "卧室";
};
void gotoBedRoom(HOME* home) {
cout << home->livingRoom << endl;
cout << home->bedRoom << endl; //如果没有friend void gotoBedRoom(HOME* home);则无法访问。
}
int main(){
HOME home;
gotoBedRoom(&home);
return 0;
}
2.5.1 成员函数做友元
class HOME; //前置声明HOME类,后续才定义
class GoodFriend{
public:
HOME* home;
void visitBedHome(); //前置声明
};
class HOME {
friend void GoodFriend::visitBedHome();
public:
string livingRoom = "客厅";
private:
string bedRoom = "卧室";
};
void GoodFriend::visitBedHome() { //放在GoodFriend中声明无法访问home成员,因为没有定义
cout << home->livingRoom << endl;
cout << home->bedRoom << endl;
}
int main(){
HOME* home = new HOME();
GoodFriend* goodFriend = new GoodFriend();
goodFriend->home = home;
goodFriend->visitBedHome();
return 0;
}
2.5.3 类做友元
……
friend class Friend;
……
将类做成友元,友元类中的所有成员函数中,都可以访问私有部分。
2.6 运算符重载
运算符重载: 运算符重载,就是对已有运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
在C++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数名字由关键字operator及其紧跟的运算符组成。
语法:
定义重载运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数个数取决于两个因素。
运算符是一元(一个参数)的还是二元(两个参数)的;
运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元是this指针,对于二元是this指针加一个参数-此时该类的对象用作左耳参数)。
不能重载的运算符: . :: .* ?: sizeof
,除了这几种,几乎都可以重载。
重载+,全局函数定义
class Point{
public:
int x;
int y;
Point(): x(0), y(0){}
Point(int x, int y): x(x), y(y){}
};
// 重载+,相加结果也是Point
// const修饰,不能修改p1和p2的值
// &引用修饰,避免拷贝,提高效率
Point operator+(const Point &p1, const Point &p2){
return {p1.x + p2.x, p1.y + p2.y};
}
int main(){
Point p1(10 ,20);
Point p2(5, 15);
Point p3 = p1+p2;
cout << p3.x << " " << p3.y << endl;
return 0;
}
重载-,类内函数定义
class Point{
public:
int x;
int y;
Point(): x(0), y(0){}
Point(int x, int y): x(x), y(y){}
Point operator-(const Point& p){ // 参数:this,p
return {x - p.x, y - p.y};
}
};
int main(){
Point p1(10 ,20);
Point p2(5, 15);
Point p3 = p1 - p2;
cout << p3.x << " " << p3.y << endl;
return 0;
}
重载++,全局
// 前置运算++
Point operator++(Point& p){
p.x++;
p.y++;
return p;
}
// 后置运算++
Point operator++(Point& p, int){ // int占位,区分两个++
Point tem = p; //&引用,然后拷贝
p.x++;
p.y++;
return tem;
}
int main(){
Point p1(10 ,20);
Point p2(5, 15);
Point p3 = ++p1;
cout << p3.x << " " << p3.y << endl;
cout << p1.x << " " << p1.y << endl;
Point p4 = p1++;
cout << p4.x << " " << p4.y << endl;
cout << p1.x << " " << p1.y << endl;
return 0;
}
重载–,类内
class Point{
public:
int x;
int y;
Point(): x(0), y(0){}
Point(int x, int y): x(x), y(y){}
Point operator--(){
x--;
y--;
return *this;
}
Point operator--(int){
Point tem = *this;
x--;
y--;
return tem;
}
};
类外重载<<
class Person{
// 由于属性是私有的,所有需要给<<做友元
friend ostream& operator<<(ostream& out, const Person& person);
private:
string name;
int age;
string gender;
int* score;
public:
Person():name(""),age(0),gender(""),score(0){};
Person(string name, int age, string gender, int score):name(name),age(age),gender(gender),score(new int(score)){}
};
// 重载<<运算符
ostream& operator<<(ostream& out, const Person& person){
out << "name: " << person.name << ", age: " << person.age << ", gender: " << person.gender << ", score: " << *person.score;
return out;
}
int main(){
Person xiaoming{"xiaoming", 18, "male", 100};
cout << xiaoming << endl;
return 0;
}
2.7 封装
面向对象思想三大特性:封装、继承、多态。
- 广义上的封装,是指将一些功能相近的一些类放入一个模块。
- 狭义上封装是值通过对具体属性的封装实现,把对变量成员的访问进行私有化,然后通过公共接口实现间接访问。即就是将一些变量封装起来,对外隐藏,只对外提供get和set等方法。
- 进行侠义上的封装,提高代码的安全性、复用性和可读性。
2.8 继承
继承:继承是面向对象编程中一个重要的概念,它允许我们定义一个类,继承另一个类的所有属性和方法。
父类:被继承的类称为父类或基类、超类。
子类: 继承了父类的属性和方法,称为子类或派生类。
- 父类在所有的非静态成员,都可以继承给子类(排除构造函数和析构函数)。
- 一个类可以被多个类继承。
- 在C++中,一个类可以继承多个父类(多继承)。
- 一个类在继承了父类的同时,也可以被他的子类继承。
定义类中成员时:
public: 可以在任意的位置访问。
protected: 只能在当前类、子类中访问。
private: 只能在当前类中访问。
在C++中,继承有三种权限,公共继承,保护继承,私有继承。
- 公共继承public:继承到父类中的成员,保留原有的访问权限。
- 保护继承protected:继承到父类中的成员,超过protected权限部分,将降为protected。
- 私有继承private:继承到父类中的成员,超过private权限部分,将降为private。
class BaseClass {
public:
int publicField;
protected:
int protectedField;
private:
int privateField;
};
class A:public BaseClass {};
class B: protected BaseClass {};
class C: private BaseClass {};
继承中的构造和析构函数:
- 子类对象在创建时,会首先调用父类的构造函数,然后再调用子类的构造函数。默认调用无参构造函数。
- 父类中没有无参构造函数,或无参构造函数是私有的,需要给父类添加构造函数或修改构造函数的访问权限,或者在子类中显示调用父类的无参构造。
- 子类对象在销毁时,会首先调用子类的析构函数,然后再调用父类的析构函数。
如果父类和子类出现了同名的属性和方法,那么子类会隐藏父类的同名属性和方法。如下想要访问父类中的同名属性和方法,需要使用作用域运算符::进行显示指定。
class Animal{
public:
int age = 0;
void showAge(){
cout << "父类被调用,age: " << age << endl;
}
};
class Dog:public Animal{
public:
int age;
void showAge(){
cout << "子类被调用,age: " << age << endl;
}
};
int main(){
Dog dog;
dog.age = 10;
dog.showAge();
//访问父类中的同名属性和方法
dog.Animal::age = 20;
dog.Animal::showAge();
return 0;
}
多继承:一个类可以继承多个父类。
class C: public A, public B{};
如果A和B有同名成员,注意二义性,需要显示指定,如:
C.A::showAge(); C.B::showAge();
菱形继承(钻石继承):即一个类继承两个父类,且父类之间有相同的父类。
例如:骡子继承马和驴,马和驴都继承动物,那么就出现了菱形继承。
class Animal{
public:
int age=0;
};
class Horse: public Animal{
public:
int a=1;
};
class Donkey:public Animal{
public:
int b=2;
};
class Mule:public Horse, public Donkey {};
int main(){
Mule mule;
cout << mule.a << endl;
cout << mule.b << endl;
// cout << mule.age << endl;
// 不知道这个age是来自Horse的父类还是Donkey的父类,命名冲突,所以编译不通过。
// 可以通过指定父类解决这个问题,如:
cout << mule.Horse::age << endl;
cout << mule.Donkey::age << endl;
return 0;
}
虚继承:当一个类继承多个父类,且父类之间有相同的父类时,使用虚继承可以避免菱形继承。使得这个派生类中只保留一份相同的间接父类中的成员。
// 添加virtual关键字,即可使其虚继承
class Horse: public virtual Animal{
public:
int a=1;
};
class Donkey:public virtual Animal{
public:
int b=2;
};
// 此时下面代码不会报错
cout << mule.age << endl;
2.9 多态
生活中,多态是指客观的事物在人脑中的主观体现。例如,一只哈士奇,你可以看作是哈士奇,也可以看作是狗,也可以看作是动物。
在程序中,多态是指父类指针或引用指向子类对象,通过父类指针或引用调用子类中的方法。
C++支持编译时多态(静态多态)和运行时多态(动态多态)。静态多态和动态多态的区别就是函数地址是早绑定(静态绑定)还是晚绑定(动态绑定)。如果函数的调用,在编译阶段可以确定,并产生代码,那么就是静态多态。如果函数的调用,在运行阶段才能确定,那么就是动态多态。
对象转型:多态的前提,是指父类的引用或指针指向子类的对象。
class Animal{
public:
void bark(){
cout << "Animal Bark" << endl;
}
};
class Dog:public Animal{
public:
int age=0;
void bark(){
cout << "Dog Bark" << endl;
}
};
int main(){
// 父类的引用指向子类的对象
Dog dog;
Animal& animal = dog;
// 父类的指针指向子类的对象
Dog* xiaobai = new Dog();
Animal* xiaohei = xiaobai;
// 对象转型成父类的指针或引用后,将只能访问父类中的成员, 不能访问子类中的成员。
// animal.age; xiaohei->age; 报错,不能访问子类中的成员。
animal.bark(); // 两个类都有bark,但是调用的是父类的bark,所以输出Animal Bark
return 0;
}
问题原因:早绑定(在程序运行之前绑定),因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数的调用,由于调用函数的时候使用的是Animal类型,所有编译器确定了应该调用的bark是Animal::bark的,而不是传入对象Dog::bark。
解决方式:迟绑定(在程序运行时绑定,又叫动态绑定),通过虚函数实现。
虚函数:指允许在父类中声明的成员或函数,在子类中重写这个成员或函数。这种做法称为覆盖(override)或重写。C++要求在父类中声明这个函数的时候使用virtual关键字,这样才可以在子类中重写这个函数。
class Animal{
public:
// virtual表示虚函数,可被子类重写
virtual void bark(){
cout << "Animal Bark" << endl;
}
};
class Dog:public Animal{
public:
int age=0;
// 重写父类的虚函数,不加override不报错,但是建议加上表示重写
void bark() override{
cout << "Dog Bark" << endl;
}
};
int main(){
// 父类的引用指向子类的对象
Dog dog;
Animal& animal = dog;
// 父类的指针指向子类的对象
Dog* xiaobai = new Dog();
Animal* xiaohei = xiaobai;
// 此时结果是Dog Bark
animal.bark();
return 0;
}
注意:virtual只能修饰成员函数。构造函数不能为虚函数。
例子
class Student{
public:
virtual void show(){
cout<<"student"<<endl;
}
};
class GradeOne:public Student{
public:
void show() override{
cout<<"GradeOne"<<endl;
}
};
class GradeTwo:public Student{
public:
void show() override{
cout<<"GradeTwo"<<endl;
}
};
class GradeThree:public Student{
public:
void show() override{
cout<<"GradeThree"<<endl;
}
};
void showGrade(Student& grade){
// 这里不用再判断是哪个年级进行输出
// 传入的是Student类型的子类GradeOne,GradeTwo,GradeThree……
// 父类引用指向子类对象,所有调用的是传入子类中的show函数
// 需要增加GradeFour等,直接增加对应类即可,不用修改其余代码,符合编码的开闭原则
grade.show();
}
int main(){
GradeOne grade1;
GradeTwo grade2;
GradeThree grade3;
showGrade(grade1);
showGrade(grade2);
showGrade(grade3);
return 0;
}
在设计程序时,常常希望将基类仅仅作为派生类的一个接口。这就是说,仅想对基类进行向上类型的转换,使用它的接口,而不希望用户实际的创建以及基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
纯虚函数:纯虚函数是抽象类中的函数,它没有函数体,它只能被继承,不能被实例化。使用virtual来修饰一个函数,并且实现部分直接设置为0:virtual void test()=0;
如果一个虚函数的实现部分被设置为0,那么这样的函数就是纯虚函数。纯虚函数只有声明,没有实现。
抽象类:如果一个类中包含了纯虚函数,这个类就是抽象类。抽象类不能被实例化,即不能创建对象。
抽象类的使用:如果类继承自一个抽象函数,此时这个类中必须重写实现父类中所有的纯虚函数。否则这个类也是抽象类,无法创建对象。
接口类:接口类是抽象类的一种,接口类中只能包含纯虚函数,不能包含其他成员函数。下面代码就演示了接口类的使用:
class Cooker{
public:
virtual void buyFood()=0;
virtual void cook()=0;
virtual void eat()=0;
};
class Maid{
public:
virtual void cook()=0;
virtual void wash()=0;
virtual void clean()=0;
};
class Person:public Cooker,public Maid{
public:
void buyFood() override{
cout<<"买菜"<<endl;
}
void cook() override{ // 两个父类都有 cook()方法,但是只需要定义一个即可
// 注意:两个类的cook()方法,类型必须一致,否则会发生互斥,编译无法通过
cout<<"煮饭"<<endl;
}
void eat() override{
cout<<"吃菜"<<endl;
}
void wash() override{
cout<<"洗衣服"<<endl;
}
void clean() override{
cout<<"清理"<<endl;
}
};
int main(){
Person xiaobai;
xiaobai.buyFood();
return 0;
}
虚析构函数:如果Animal* animal = new Dog();
,那么animal指向的析构函数是Animal的析构函数,而不是Dog的析构函数。只执行了animal的析构函数,没有执行dog的析构函数。就需要虚析构函数来解决。
解决:将父类Animal的析构函数设置为虚函数,virtual ~Animal(){};
,然后在子类Dog中重写父类Animal的析构函数,~Dog() override {};
。这样当Animal* animal = new Dog();时,animal指向的析构函数是Dog的析构函数,并且Animal和Dog的虚构函数都会执行。
2.10 结构体
结构体也是一种自定义的数据类型,基本与类相同(在c中限制较多,在c++中与类差不多)。
结构体与类的区别:成员默认的访问权限不同,类中的成员默认是private,结构体中的成员默认是public。
// struct关键字创建结构体
struct Student{
string name;
int age;
int score;
Student(string name,int age,int score):name(name),age(age),score(score){
cout<<"构造函数"<<endl;
}
void study(){
cout<<"study"<<endl;
}
~Student(){
cout<<"析构函数"<<endl;
}
};
int main(){
// 创建对象实例的时候,struct可以省略不写
struct Student s1("huang",18,100);
Student s2("xiao",18,100);
Student *s3 = new Student("wang",18,100);
cout << s1.name << endl;
cout << s2.name << endl;
delete s3;
return 0;
}
2.11 模板
c++提供了函数模板和类模板,函数模板和类模板都是泛型编程,泛型编程就是指在定义函数或者类时,不预先指定具体的类型(使用虚拟的类型代替),而在使用时再指定类型的一种编程方式。
2.11.1 函数模板
函数模板也可重载。
template<typename T, typename U> // typename也可以写为class
void add(T a,U b){
cout<<a+b<<endl;
}
template<typename T, typename U, typename V>
V add(T a,U b){
return a+b;
}
int main(){
// 显示指定类型
add<int,double>(1,2.8);
// 也可以将返回值作为template,但是必须指定类型
cout << add<int, double, double>(1, 2.8) << endl;
// 可以根据实参自动推导
add(1,2.8);
add('a','b');
add<int>(1,2.8); // 从左往右给参数类型,这个int给T,其余的自动推导
return 0;
}
如果将返回值也做为template,那么可以将该返回值的template放在最前面,这样就只需要指定一个类型,其他类型自动推导。
template<typename V, typename T, typename U>
V add(T a,U b){
return (V) (a+b); // (V)可加可不加,做转换类型,不加也没有问题
}
add<int>(1,2.8); //返回3
函数模板和普通函数的区别:
- 普通函数调用,可以发生自动类型转换,函数模板调用,不可以发生自动类型转换。
- 函数调用时,优先匹配普通函数,再匹配函数模板。
模板使用案例:定义一个函数,实现将一个数组中的元素拼接成字符串返回
#include <iostream>
#include <sstream>
using namespace std;
template<typename T>
string toString(T* array, int len){
if (len==0 || array == nullptr){
return "[]";
}
ostringstream oss;
oss << "[";
for (int i=0;i<len-1;i++){
oss << array[i] << ",";
}
oss << array[len-1]<<"]";
return oss.str();
}
int main(){
int a[] = {1,2,3,4,5};
int len=sizeof(a)/sizeof(a[0]);
cout << toString(a,len) << endl;
return 0;
}
2.11.2 类模板
类模板和函数模板类似,但是类模板不能部分自动类型推导。
template<typename T1, typename T2>
class NumberCalculator{
private:
T1 n1;
T2 n2;
public:
NumberCalculator(){}
NumberCalculator(T1 n1, T2 n2):n1(n1), n2(n2){}
void showAdd(){
cout << n1 << " + " << n2 << " = " << n1 + n2 << endl;
}
void showMinus(){
cout << n1 << " - " << n2 << " = " << n1 - n2 << endl;
}
};
int main(){
NumberCalculator<int, double> nc1(10, 20.1);
nc1.showAdd();
nc1.showMinus();
NumberCalculator nc2(10, 20);
nc2.showAdd();
// NumberCalculator<int> nc3(10, 20); // 不能部分指定类型,要么全部指定,要么都不指定
return 0;
}
普通函数中,使用类模板作为参数,类模板必须要明确类型。
void userCalculator(NumberCalculator<T1,T2>& nc){}
函数模板中,使用类模板作为参数的时候,类模板可以明确类型,也可以使用函数模板中的虚拟类型,如下:
template<typename X, typename Y>
void userCalculator(NumberCalculator<T1,T2>& nc){}
void userCalculator2(NumberCalculator<X,Y>& nc){}
类模板继承
template <typename T>
class Animal{
public:
T arg;
};
// 普通的类继承模板,需要指定父类中的虚拟类型
class Dog: public Animal<int>{};
// 类模板继承类模板
template<typename E>
class Cat:public Animal<E>{};
int main(){
Dog xiaobai;
xiaobai.arg = 1;
Cat<string> xiaohei;
xiaohei.arg = "hello";
return 0;
}
需要注意:
类模板中的成员函数,是在调用函数的时候才创建的。
类模板外实现类模板函数:
// 类模板中函数的类外实现
template<typename T1, typename T2>
class NumberCalculator{
private:
T1 n1;
T2 n2;
public:
NumberCalculator();
NumberCalculator(T1 n1, T2 n2);
void add();
};
// 类外实现函数,需要定义模板
template<typename T1, typename T2>
NumberCalculator<T1, T2>::NumberCalculator() {
n1 = 0;
n2 = 0;
cout << "NumberCalculator的无参构造实现了" << endl;
};
template<typename T1, typename T2>
NumberCalculator<T1, T2>::NumberCalculator(T1 n1, T2 n2) {
this->n1 = n1;
this->n2 = n2;
cout << "NumberCalculator的有参构造实现了" << endl;
};
template<typename T1, typename T2>
void NumberCalculator<T1, T2>::add() {
cout << "n1 + n2 = " << n1 + n2 << endl;
}
int main(){
NumberCalculator<int, int> nc1(10, 20);
nc1.add();
return 0;
}
如果使用.h和.cpp定义类,其中.h里面申明类模板,在.cpp中实现类模板函数,那么在主函数中直接引入使用.h文件编译会报错。
原因:类模板中的成员函数,是在调用函数的时候才创建的。编译时先编译.h文件,然后编译对应的cpp文件,而.h文件只有申明,没有实现,所有在.h文件时就会报错,.cpp文件就无法继续编译。
解决:
- 引入.cpp文件,而不是.h文件。
- 将类模板的成员函数放在类模板中,而不是类模板外实现,即写成一个文件,一般为.hpp文件。