C++面向对象

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;
    }

注意:

  1. 如果在一个类中没有写任何的构造函数,系统会为其添加一个public权限的无参构造函数,可以让我们创建对象。
  2. 如果我们给一个类写了构造函数,系统不会再为我们提供任何默认的构造函数。

如果我们为一个类写了有参的构造函数,一般会同时写无参的构造函数。

// 构造使用
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 析构函数

  1. 析构函数是对象生命周期的终点,在对象空间被销毁之前调用。
  2. 在析构函数中,一般进行资源的释放,堆内存的销毁。
  3. 不能重载,一个类只有一个析构函数。
  4. 使用~开头,不能有参数。
class Person{
    int a;
public:
    Person() : a(0) {}  // 初始化成员变量a为0
    ~Person(){
        cout << "析构函数调用" << endl;
    }
};
int main(){
    Person p;
    return 0;
}

2.2 浅拷贝与深拷贝

  1. 浅拷贝:在拷贝构造函数中,直接完成属性的值拷贝。即拷贝对象和被拷贝对象共享同一个内存空间,修改其中一个对象,另一个对象也会改变。
  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指针

  1. this指针是一个隐含的指针,它指向当前对象的地址。
  2. 当前对象:谁调用这个函数,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 常函数

  1. 使用const修饰,例如:void fun1() const{ }
  2. 常函数中不能修改成员变量的值。
  3. 常函数中不允许调用普通函数,只能调用其它函数。

2.4.2 常对象

  1. 使用const修饰对象,例如:const Person p1 = Person("xiaoming", 18);
  2. 常对象可以读取任意属性的值,但是不允许修改。
  3. 常对象只可以调用常函数。

2.4.3 mutable

  1. 用来修饰属性的,表示可变。
  2. 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 封装

面向对象思想三大特性:封装、继承、多态。

  1. 广义上的封装,是指将一些功能相近的一些类放入一个模块
  2. 狭义上封装是值通过对具体属性的封装实现,把对变量成员的访问进行私有化,然后通过公共接口实现间接访问。即就是将一些变量封装起来,对外隐藏,只对外提供get和set等方法
  3. 进行侠义上的封装,提高代码的安全性、复用性和可读性。

2.8 继承

继承:继承是面向对象编程中一个重要的概念,它允许我们定义一个类,继承另一个类的所有属性和方法。
父类:被继承的类称为父类或基类、超类。
子类: 继承了父类的属性和方法,称为子类或派生类。

  1. 父类在所有的非静态成员,都可以继承给子类(排除构造函数和析构函数)。
  2. 一个类可以被多个类继承。
  3. 在C++中,一个类可以继承多个父类(多继承)。
  4. 一个类在继承了父类的同时,也可以被他的子类继承。

定义类中成员时:

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文件就无法继续编译。
解决

  1. 引入.cpp文件,而不是.h文件。
  2. 将类模板的成员函数放在类模板中,而不是类模板外实现,即写成一个文件,一般为.hpp文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值