C++ primer plus笔记 --- 第14章、C++中的代码重用

14.1、包含对象成员的类

包含对象成员的类,也称为类嵌套或嵌套类。在C++中,可以在一个类中包含另一个类的对象,这个对象成为外部类的成员。

例如,一个汽车类可以包含一个发动机类的对象作为它的成员。在汽车类中,发动机类的对象可以具有其自己的数据成员和成员函数,同时它的行为也可以被汽车类控制。

对象成员的主要优点在于可以将类的细节隐藏在实现中,且不影响类的使用者。它还可以使代码更加组织化,方便地将相关代码组合在一起,以及减少代码的重复性。

在使用对象成员时,需要注意以下几点:

  1. 访问控制:外部类可以访问它的对象成员中公有的和受保护的成员,但无法访问对象成员中私有的成员。与任何类一样,对象成员的访问控制对保护数据和函数的访问非常有用。

  2. 构造函数和析构函数:如果对象成员需要进行复杂的初始化或清理,需要在外部类的构造函数和析构函数中分别对其进行初始化和清理。

  3. 存储和内存布局:对象成员存储在外部类的内存中,因此需要考虑它们如何占用外部类的内存空间以及它们在内存中相对于外部类的布局。

  4. 生命周期:对象成员的生命周期受外部类的生命周期限制,因此必须确保在外部类被销毁时,对象成员不会继续存在。

总之,使用对象成员可以提高代码的组织性、减少重复性和提高程序的可维护性。在使用对象成员时,需要考虑访问控制、构造函数和析构函数、存储和内存布局以及对象成员的生命周期等因素,以确保外部类的正确性和稳定性。

14.1.1、valarray类简介

valarray类是C++标准库中提供的一个通用数组类。它是一个模板类,能够存储任意类型的数据,并提供了很多操作数组的函数和运算符。使用这些函数和运算符可以方便地对数组进行数学运算和算法实现。

valarray类的主要特点如下:

  1. 支持常规数组的操作,例如访问特定元素,对所有元素应用任意函数,以及提取部分数组。

  2. 支持所有基本数学算法,例如对valarray对象进行运算,在valarray对象之间进行运算,并进行逐元素的数学函数应用。

  3. 支持逻辑运算符,并提供了很多逻辑运算符和逻辑函数,包括元素间的比较、逐位逻辑运算和逻辑函数应用。

  4. 具有高效的并行计算能力,可以利用并行算法进行大规模数据处理。

valarray类使得数组处理变得更加简单、可维护和高效。它还可用于多种模式识别和机器学习算法的实现,例如卷积神经网络和循环神经网络。在使用valarray类时,需要熟悉其各种函数和运算符,并灵活运用,以充分发挥其优势和实现复杂的算法。

14.1.2、Student类简介

Student类是一个常见的示例类,用于表示一名学生的基本信息。它通常包括学生的姓名、学号、年龄、性别等基本信息,以及一些与学生成绩相关的信息,例如考试成绩、平时成绩、总成绩等。

在实现Student类时,可以使用C++中的类模板来描述学生的基本属性和成绩等信息。例如,可以定义一个包含学生各项基本属性和成绩的类模板,其中包括姓名、学号、年龄、性别、考试成绩、平时成绩、总成绩等。在实际使用时,可以根据学生的具体需求来实例化这个类模板,例如创建一个表示某个特定学生的对象。

Student类可以包含各种成员函数,包括构造函数、析构函数、设置成员函数和获取成员函数等。它还可以包含一些其他的函数,例如计算平均成绩、判断学生成绩是否优秀等。通过使用这些函数,可以方便地访问和操作学生对象的属性和成绩,以便更好地管理学生信息。

总之,Student类是为了方便管理学生信息而创建的一个示例类,它包含有关学生基本属性和成绩的数据和函数。在实现Student类时,需要考虑设计合理的属性和函数,并通过合适的逻辑和方法来管理和操纵学生信息。

14.1.3、Student类示例

以下是一个简单的Student类示例,用于表示一名学生的基本信息和成绩:

#include <string>
using namespace std;

class Student {
public:
    // 构造函数
    Student(string name, int id, int age, char gender, double exam_score, double hw_score);

    // 获取学生姓名
    string getName() const;

    // 获取学生学号
    int getID() const;

    // 获取学生年龄
    int getAge() const;

    // 获取学生性别
    char getGender() const;

    // 获取学生考试成绩
    double getExamScore() const;

    // 获取学生平时成绩
    double getHWScore() const;

    // 获取学生总成绩
    double getTotalScore() const;

    // 判断学生是否成绩优秀
    bool isExcellent() const;

private:
    string name;        // 学生姓名
    int id;             // 学生学号
    int age;            // 学生年龄
    char gender;        // 学生性别
    double exam_score;  // 学生考试成绩
    double hw_score;    // 学生平时成绩

    // 计算学生总成绩
    double calculateTotalScore() const;
};

Student::Student(string name, int id, int age, char gender, double exam_score, double hw_score) {
    this->name = name;
    this->id = id;
    this->age = age;
    this->gender = gender;
    this->exam_score = exam_score;
    this->hw_score = hw_score;
}

string Student::getName() const {
    return name;
}

int Student::getID() const {
    return id;
}

int Student::getAge() const {
    return age;
}

char Student::getGender() const {
    return gender;
}

double Student::getExamScore() const {
    return exam_score;
}

double Student::getHWScore() const {
    return hw_score;
}

double Student::getTotalScore() const {
    return calculateTotalScore();
}

bool Student::isExcellent() const {
    return (calculateTotalScore() >= 90.0);
}

double Student::calculateTotalScore() const {
    // 计算总成绩,平时成绩占比30%,考试成绩占比70%
    return hw_score * 0.3 + exam_score * 0.7;
}

在该示例中,Student类包含了学生的姓名、学号、年龄、性别、考试成绩、平时成绩等属性,以及计算总成绩和判断是否成绩优秀的函数。这些成员函数也正是用于对对象进行初始化和操作的方法。可以通过实例化该类,创建新的Student对象,并使用对象的成员函数来获取和操纵对应的学生信息。

14.2、私有继承

私有继承(private inheritance)是C++面向对象编程中的一种继承方式。与公共继承和保护继承不同的是,私有继承派生类的对象无法访问基类的非公有成员。

具体来说,私有继承将基类的公有和保护成员转化为派生类的私有成员。这样,在派生类对象中,基类的公有和保护成员就不能直接访问了,只能通过派生类提供的公共接口(即成员函数)来间接访问。而基类的私有成员在派生类中是不可访问的。

私有继承的主要优点是可以隐藏基类的实现细节和公共接口,同时继承了基类的接口和实现。这样,可以在派生类中重新定义基类的接口,从而实现新的功能或更好地适应新的需求。

总之,私有继承是C++面向对象编程中的一种继承方式,可以隐藏基类的实现细节和公共接口,同时继承了基类的接口和实现。在使用私有继承时,需要注意访问权限的限制,以及重新定义基类接口带来的影响。

14.2.1、Student类简介(新版本)

以下是一个基于私有继承的Student类示例,相比之前的示例,这个新版本的Student类增加了一个基类Person,将Person类作为Student类的基类,Student类继承了Person类中的一些成员变量和成员函数。同时,将Person类的成员变量和成员函数设为私有,使它们不能在Student类外部被访问,从而实现了封装。

#include <string>
using namespace std;

class Person {
private:
    string name;
    int age;
    char gender;

public:
    Person(string name, int age, char gender) : name(name), age(age), gender(gender) {}
    string getName() const { return name; }
    int getAge() const { return age; }
    char getGender() const { return gender; }
};

class Student : private Person {
private:
    int id;
    double exam_score;
    double hw_score;

public:
    Student(string name, int age, char gender, int id, double exam_score, double hw_score)
        : Person(name, age, gender), id(id), exam_score(exam_score), hw_score(hw_score) {}

    using Person::getName;
    using Person::getAge;
    using Person::getGender;

    int getID() const { return id; }
    double getExamScore() const { return exam_score; }
    double getHWScore() const { return hw_score; }

    double getTotalScore() const {
        // 计算总成绩,平时成绩占比30%,考试成绩占比70%
        return hw_score * 0.3 + exam_score * 0.7;
    }

    bool isExcellent() const { return (getTotalScore() >= 90.0); }
};

在这个新版本的Student类中,Person类作为一个基类被继承,并且Person类中的成员变量和成员函数被设为了私有,使得它们只能在Person类或Person类的派生类中被访问。同时,为了保留基类Person类中的接口,新的Student类使用了using声明语句,将基类中的三个成员函数放置到Student类的public访问区域中,此时这些成员函数是公有的,并且只能通过Student类的对象来调用。而在派生类中新增了一个私有变量id和三个公有成员函数,它们可以被Student类的对象访问和调用。

总之,私有继承是最严格的继承形式,基类的所有成员对派生类都是私有的,并且基类的公共成员和构造函数只能通过派生类的成员函数进行访问。在这个新版本的Student类中,我们展示了如何使用私有继承来实现继承和封装,这将基类的公共接口和实现细节藏在私有区域中,同时保留了必要的公共接口,而私有变量则由派生类中新增的成员变量来扩展。

14.2.2、使用包含还是私有继承

在C++中,继承和包含都是实现类的复用的两种主要方式。在确定是否使用继承和包含时,需要考虑以下几个因素:

  1. 类之间的逻辑关系:如果派生类是基类的具体化,或者属于“is-a”关系,则应该使用继承;而如果派生类只是需要调用基类的某些功能,则应该使用包含。

  2. 可访问性:继承会将基类的公共接口暴露给派生类和其他类,而包含只有通过对象的接口才能访问。如果派生类需要访问基类的私有成员,应该使用私有继承;而如果不需要访问,则可以使用包含。

  3. 设计的灵活性:继承会将基类的设计限制透露给派生类,而包含则允许更灵活的设计。如果基类可能会发生变化,并且这个变化会影响到派生类,则应该考虑使用包含。

总之,对于需要复用其他类的功能的时候,可以通过继承和包含来实现,具体应该使用哪种方式,需要根据具体情况进行选择,综合考虑类之间的关系、可访问性和设计的灵活性等因素。

14.2.3、保护继承

保护继承(protected inheritance)是C++中的一种继承方式,与私有继承和公共继承不同,它将基类的共有和保护成员转化为派生类的保护成员,而将基类的私有成员仍保持为私有成员。这样,在派生类中,对于基类的共有和保护成员,外部不能直接访问,只能通过派生类的成员函数来访问。

保护继承通常用于实现继承和封装的共同作用。派生类通过保护继承,继承了基类的接口和实现,同时可以通过再定义方法来重新定义接口。这提供了更好的控制派生类对基类接口访问权限的能力,从而实现更安全的封装。

以下是一个简单的Person类和Student类的保护继承示例:

#include <string>
using namespace std;

class Person {
protected:
    string name;
    int age;
    char gender;

public:
    Person(string name, int age, char gender) : name(name), age(age), gender(gender) {}

    string getName() const { return name; }
    int getAge() const { return age; }
    char getGender() const { return gender; }
};

class Student : protected Person {
protected:
    int id;
    double examScore;
    double hwScore;

public:
    Student(string name, int age, char gender, int id, double examScore, double hwScore)
        : Person(name, age, gender), id(id), examScore(examScore), hwScore(hwScore) {}

    using Person::getName;
    using Person::getAge;
    using Person::getGender;

    int getId() const { return id; }
    double getExamScore() const { return examScore; }
    double getHWScore() const { return hwScore; }

    double getTotalScore() const {
        // 计算总成绩,平时成绩占比30%,考试成绩占比70%
        return hwScore * 0.3 + examScore * 0.7;
    }

    bool isExcellent() const { return (getTotalScore() >= 90.0); }
};

在这个示例中,Person类中的三个成员变量被定义为了保护成员,在Student类中可以被继承和访问,同时基类的构造函数被Student类所调用,Student类中增加了一个新的私有成员变量id。在派生类中,使用using声明语句使基类中的getName()、getAge()和getGender()函数可访问,从而避免了重新实现这些函数的重复劳动。Student类中也增加了自己的成员函数,用于获取学生的考试成绩、平时成绩、总成绩和判断是否优秀。

总之,保护继承是C++中的一种继承方式,将基类的共有和保护成员转化为派生类的保护成员,用于实现继承和封装的共同作用。保护继承提供了更好的控制派生类对基类接口访问权限的能力,同时可以通过再定义方法来重新定义接口。

14.2.4、使用using重新定义访问权限

在C++中,我们可以使用using关键字重新定义从基类继承而来的成员在派生类中的访问权限。使用using关键字,可以有效避免重复编写一些访问函数,同时又能避免派生类完全继承基类的访问权限的问题。

具体来说,在派生类中,使用using关键字可以将从基类继承而来的成员重新定义为公有、保护或私有。

以下是一个简单的例子,其中定义了一个基类Animal和一个派生类Dog:

#include <iostream>
using namespace std;

class Animal {
public:
    void eat() { cout << "Animal eats." << endl; }
    void sleep() { cout << "Animal sleeps." << endl; }
};

class Dog : private Animal {
public:
    using Animal::eat;  // 重新定义eat()访问权限为公有
    using Animal::sleep;  // 重新定义sleep()访问权限为公有
    void bark() { cout << "Dog barks." << endl; }
};

在这个例子中,Dog类使用私有继承继承了Animal类,同时通过using关键字将eat()和sleep()函数的访问权限重新定义为公有。这样,在Dog类的外部,就可以通过Dog类的对象调用eat()和sleep()函数了。

总之,使用using关键字可以重新定义从基类继承而来的成员在派生类中的访问权限。这样可以有效避免重复编写一些访问函数,同时又能避免派生类完全继承基类的访问权限的问题。

14.3、多重继承

C++支持多重继承,即从多个基类派生出一个派生类。 在多重继承中, 派生类会继承多个基类的成员函数和成员变量。

多重继承的语法与单一继承类似,只是在类名后面可以通过逗号分隔添加多个基类名。多继承的类具有多个父类,因此要考虑其访问控制,以确保派生类可以访问正确的基类成员。

例如,有一个基类Animal和一个基类Flyer,可以通过多重继承方式创建一个派生类Bird:

class Animal {
public:
   void eat() {}
   void sleep() {}
};

class Flyer {
public:
   void takeoff() {}
   void fly() {}
   void land() {}
};

class Bird : public Animal, public Flyer {
public:
   Bird(string name) : animalName(name) {}
   void chirp() {}
private:
   string animalName;
};

在这个例子中,Bird类从Animal和Flyer两个基类继承,可以调用它们的成员函数takeoff()、fly()、land()、eat()和sleep()。Bird类也添加了自己的成员函数chirp()。

需要注意的是,多个基类中如果出现成员函数或成员变量名称相同,就会出现命名冲突的问题。 解决方法是使用作用域解析运算符(::)或者在派生类中覆盖基类中的成员函数(也可以使用using关键字)。

在使用多重继承时,需要慎重考虑继承关系的合理性,并合理控制访问权限以避免潜在的问题。

14.3.1、有多少Worker

这个问题的主要意思是已知一组派生类,如何统计出当前系统中有多少个派生类的实例。可以使用静态成员对象和构造函数技术来实现这个功能。

具体来说,需要在基类中定义一个静态成员变量,每当一个派生类对象被创建时,就将这个静态成员变量的值加1。这样就可以在运行时记录当前系统中有多少个派生类的实例。

例如,我们可以使用一个名为Worker的基类,然后在每个派生类的构造函数中将一个名为count的静态数据成员加1。最后,可以使用一个名为Worker::getCount()的静态成员函数来获取系统中Worker类的子类的实例数。

#include <iostream>
using namespace std;

class Worker {
public:
    static int getCount() { return count; }
    Worker() { ++count; }
    virtual ~Worker() { --count; }
private:
    static int count;
};

int Worker::count = 0;

class Manager : public Worker {
public:
    Manager() {}
};

class Engineer : public Worker {
public:
    Engineer() {}
};

class Salesman : public Worker {
public:
    Salesman() {}
};

int main() {
    Manager m1, m2, m3;
    Engineer e1, e2;
    Salesman s1, s2, s3, s4;

    cout << "Number of workers: " << Worker::getCount() << endl;

    return 0;
}

在这个例子中,使用静态成员变量count来记录Worker类的子类的实例数,并在每个派生类的构造函数中将count加1。使用类似的方式,可以实现其他派生类的创建和计数。最后,使用Worker::getCount()获取系统中Worker类的子类的实例数。

总之,可以使用静态成员对象和构造函数技术来统计派生类的实例数。 静态成员变量用于记录实例数,每当一个对象被创建时,计数器加1;构造函数用于初始化计数器,析构函数用于清理计数器。

14.3.2、哪个方法

这个问题的主要意思是让我们在多重继承的情况下,如何处理两个基类中具有同名函数的情况。

在多重继承中,如果两个基类中存在同名的成员函数,当派生类调用这个函数时,编译器就会产生二义性问题,并无法确定使用哪个基类中的成员函数。解决这个问题的方法主要有以下两种:

1、使用作用域解析运算符

可以使用作用域解析运算符(::)明确指明使用哪个基类中的成员函数。具体来说,在调用同名函数时,在函数名前加上基类名和作用域解析运算符即可。例如:

class Base1 {
public:
    void foo() { cout << "Base1::foo()" << endl; }
};

class Base2 {
public:
    void foo() { cout << "Base2::foo()" << endl; }
};

class Derived : public Base1, public Base2 {
public:
    void test() {
        // 调用Base1类中的foo()函数
        Base1::foo();

        // 调用Base2类中的foo()函数
        Base2::foo();
    }
};

在这个例子中,Derived类同时从Base1和Base2中继承了函数foo(),在派生类中通过作用域解析运算符来明确指明使用哪个基类中的函数。

2、在派生类中覆盖同名函数

对于同名函数,也可以在派生类中覆盖基类中的函数,从而实现对基类函数的重定义。当派生类中存在同名函数时,编译器会优先选择派生类中的函数。例如:

class Base1 {
public:
    void foo() { cout << "Base1::foo()" << endl; }
};

class Base2 {
public:
    void foo() { cout << "Base2::foo()" << endl; }
};

class Derived : public Base1, public Base2 {
public:
    void foo() { cout << "Derived::foo()" << endl; }
    void test() {
        // 调用Derived类中的foo()函数
        foo();
    }
};

在这个例子中,Derived类中定义了同名函数foo(),此时编译器会优先选择派生类中的函数。在调用foo()函数时,会选择派生类中的foo()函数。

总之,当多个基类中存在同名函数时,可以使用作用域解析运算符来明确指明使用哪个基类中的函数,或者在派生类中覆盖同名函数。

14.3.3、MI小结

多重继承(MI)是C++中的一个特性,允许一个派生类从多个基类中继承。MI的语法与单一继承类似,只是在类名后面可以通过逗号分隔添加多个基类名。

在MI中,如果多个基类中存在同名的成员函数或者成员变量,编译器就会产生二义性问题,需要通过作用域解析运算符或者在派生类中覆盖同名函数的方式来解决。

使用多重继承时,需要慎重考虑继承关系的合理性和访问权限的控制,以确保派生类可以正确地访问基类成员。此外,在多重继承中,需要注意继承顺序的影响,因为不同的继承顺序可能会影响到虚函数的调用和成员变量的布局。

总之,多重继承是C++中的一个强大特性,可以为程序提供更灵活、更可扩展的设计方式。但是,使用多重继承也需要慎重考虑,避免出现潜在的问题。

14.4、类模板

在 C++ 中,类模板可以让我们写出可以处理多种数据类型的通用类。类模板定义了一个通用的类模板,其中涉及的数据类型可以用作参数进行推导,以最终生成特定的类或函数。

类模板的定义方式与函数模板类似,使用 template 关键字加上类型参数列表来定义。类型参数列表中可以有多个类型参数,每个参数可以用于指定可以用在类中的不同类型。

在类模板中,类型参数也可以用作默认模板参数,它们的默认值代表着可以在使用类时忽略的参数。例如:

template <typename T, typename U = int>
class MyClass {
public:
    T data1;
    U data2;
    MyClass() {}
    MyClass(T d1, U d2) : data1(d1), data2(d2) {}
    void print() {
        cout << "data1=" << data1 << ", data2=" << data2 << endl;
    }
};

在这个例子中,类模板 MyClass 有两个类型参数 T 和 U。类型参数 U 是一个带有默认参数值的参数,类型为 int。MyClass 类有两个公共成员变量 data1 和 data2,成员函数 print() 可以打印出这两个变量的值。

可以通过在 MyClass<> 后面传递类型参数来创建特定类型的对象:

MyClass<int> obj1(10, 20);
MyClass<double, float> obj2(1.2, 3.4f);

可以看到,类型参数 T 被传递为 int 和 double,类型参数 U 被传递为 int 和 float。这样,编译器就能够根据传递的类型参数生成两个不同的 MyClass 类。

类模板除了可以定义成员变量和成员函数外,还可以定义静态成员变量和静态成员函数。可以使用模板参数作为静态成员变量的类型或返回值类型。

例如,以下示例代码定义了一个类模板 Stack,在该模板中使用一个模板参数 T 来指定堆栈内部使用的元素类型:

template<typename T>
class Stack {
private:
    vector<T> elems;    // 堆栈元素

public:
    void push(T const&);  // 元素入栈
    void pop();           // 元素出栈
    T top() const;        // 访问栈顶元素
    bool empty() const { // 如果为空则返回真。
        return elems.empty();
    }
};

template<typename T>
void Stack<T>::push(T const& elem)
{
    // 追加传入元素的副本
    elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop()
{
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    // 删除最后一个元素
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const
{
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    // 返回最后一个元素的副本
    return elems.back();
}

这个类使用了模板参数 T 来指定堆栈内部元素的类型,在实例化此模板时,T 将被替换为实际类型。示例代码使用 vector 容器维护元素,但 vector 的类型不属于 T,它是一个具体的类型。

总之,C++ 中的类模板允许我们定义具有通用性的类,这些类可以在实例化时使用一个或多个模板参数。类模板的语法和函数模板类似,有助于写出更具通用性的代码,提高代码的可重用性和扩展性。

14.4.1、定义类模板

在 C++ 中,定义类模板与定义函数模板类似,都是使用 template 关键字和类型参数列表来实现的。类模板定义的一般形式如下:

template<typename T1, typename T2, ...>
class MyClass {
  // 成员声明和定义
};

其中,typename 关键字可以换成 class 关键字,两者意义相同,用来声明类型参数。MyClass 是定义的类名,T1T2、… 是类型参数列表,用逗号分隔。

在类模板中,可以使用类的成员函数、成员变量等成员组成泛型,如下所示:

template<typename T>
class MyClass {
public:
    void func(T& arg) {
        std::cout << "arg = " << arg << std::endl;
    }

    T val;
};

这里的 func 成员函数和 val 成员变量都使用了泛型类型 T,可以处理多个不同类型的数据。使用时需要为 T 提供具体的数据类型,比如:

MyClass<int> obj;
obj.func(123);  // 输出 "arg = 123"
obj.val = 456;

这里的 MyClass<int> 就是使用 int 类型实例化的类模板,将 T 替换成了 int。可以看到,func 函数可以处理任何类型的参数,不过 val 变量只能使用 int 类型。

除了使用 typename/class 关键字来声明类型参数之外,还可以使用模板常量来声明参数,如下所示:

template<size_t N>
class MyArray {
public:
    int arr[N];
};

这里的 N 就是一个模板常量,是一个编译时常量,可以用于指定数组的大小,使用时需要传递一个编译时常量,比如:

MyArray<5> arr;

这里的 MyArray<5> 就是使用常量 5 实例化的类模板,将 N 替换成了 5。这样就可以定义一个大小为 5 的数组。注意,模板常量一般使用非类型模板参数的语法进行声明,即用 typename/ class T, int N 来定义。

总之,类模板是一种强大的工具,可以创建泛型类,提高代码的可读性和代码复用程度。在定义和使用类模板的过程中要注意语法和细节,才能按照期望的方式实现功能。

14.4.2、使用模板类

在 C++ 中使用模板类的方式与使用普通类基本相同,只需要在类名后面加上 <> 并指定类型参数即可。例如:

MyClass<int> obj;

这条语句创建了一个 MyClass 类的对象 obj,并将 int 作为类型参数传递给它。这意味着 MyClass 将被实例化成一个处理 int 类型数据的类。

在使用模板类的成员函数或数据成员时,也需要注意类型参数的传递。例如:

MyClass<int> obj;
obj.func(123);

这里的 func 函数将处理 int 类型数据,因为在创建 obj 对象时指定了 int 作为类型参数。

在一些情况下,编译器可以自动推断模板类的类型参数,就像调用普通函数时一样。例如:

template<typename T>
T get_max(T a, T b) {
    return a > b ? a : b;
}

int a = 10;
int b = 20;
int max_val = get_max(a, b);  // 编译器自动推断类型参数为 int

这里的 get_max 使用了类型参数 T,编译器会根据传入的 a 和 b 的类型,自动推断出 T 的类型为 int,从而实例化一个处理 int 类型数据的函数。

需要注意的是,模板类一般定义在头文件中。因为在编译阶段,编译器需要访问模板类的源代码,才能实例化出具体的类。如果将模板类的声明和定义分别放在不同的文件中,编译器将无法访问到定义文件,也就无法完成实例化,从而导致链接错误。

总之,使用模板类时需要注意类型参数的传递,并在需要时为模板类提供具体的类型参数。同时,也要将模板类定义在头文件中,以便编译器在编译时能够正确访问模板类的定义。

14.4.3、深入探讨

在深入探讨模板类时,有几个常见的问题需要注意。

1、懒汉式 VS 饿汉式:模板类的静态成员变量

在模板类中,静态成员变量和静态成员函数是由所有实例化的模板类对象所共享的。但是,在如何实现静态成员变量上,有两种常见的模式:懒汉式和饿汉式。

懒汉式指的是,在需要使用静态成员变量时才进行初始化。这样可以减少变量初始化的开销,但是需要加锁来保证线程安全。

template<typename T>
class MyClass {
public:
    static int n;
};

template<typename T>
int MyClass<T>::n = 0;

在使用 MyClass 的静态成员变量时,需要先创建一个 MyClass 对象,然后通过对象来访问静态成员变量。

MyClass<int> obj1;
MyClass<int> obj2;
obj1.n = 10;
cout << obj2.n << endl;  // 输出 10

饿汉式则是在编译期间就进行变量的初始化。这样可以避免加锁,但是会增加程序的启动时间和占用空间。

template<typename T>
class MyClass {
public:
    static int n;
};

template<typename T>
int MyClass<T>::n = 0;
// 模板类静态变量在编译期间已经初始化完成

// 在 main 函数之前调用静态成员函数
template<class T>
class Global {
public:
    Global() {
        MyClass<T>::n = 10;
    }
};

Global<int> global_var;  // 在 main 函数之前调用构造函数

在使用 MyClass 的静态成员变量时,同样需要先创建一个 MyClass 对象,然后通过对象来访问静态成员变量。

MyClass<int> obj1;
cout << obj1.n << endl;  // 输出 10

2、模板类的特化

特化是指当模板类的某些情况下需要特别处理,就可以定义一个特化版本。模板类的特化形式有两种:全特化和偏特化。

全特化是指当模板类的某个类型参数为特定类型时,需要特别处理,可以定义一个全特化版本。如下所示:

template<typename T>
class MyClass {
public:
    void func() {
        cout << "general version" << endl;
    }
};

template<>
class MyClass<int> {
public:
    void func() {
        cout << "specialized version for int" << endl;
    }
};

在使用 MyClass 时,对于 int 类型的模板参数,将会使用全特化版本的成员函数。

MyClass<float> obj1;
MyClass<int> obj2;
obj1.func();  // 输出 "general version"
obj2.func();  // 输出 "specialized version for int"

偏特化是指定义一个模板类的部分类型参数,而非全部类型参数。一般来说,偏特化只针对有限组组特定的类型参数组合。

template<typename T, typename U>
class MyClass {
public:
    void func() {
        cout << "general version" << endl;
    }
};

template<typename T>
class MyClass<T, float> {
public:
    void func() {
        cout << "partial specialization for <T, float>" << endl;
    }
};

template<>
class MyClass<int, double> {
public:
    void func() {
        cout << "full specialization for <int, double>" << endl;
    }
};

在使用 MyClass 时,如果其中一个类型参数为 float,将会使用偏特化版本的成员函数。

MyClass<int, int> obj1;
MyClass<int, float> obj2;
obj1.func();  // 输出 "general version"
obj2.func();  // 输出 "partial specialization for <T, float>"

当两个类型参数分别为 int 和 double 时,将会使用全特化版本的成员函数。

MyClass<int, double> obj3;
obj3.func();  // 输出 "full specialization for <int, double>"

3、模板类的约束和概念

模板类的一个问题是可能有太多的实例化版本,而某些类型组合可能不会正常工作。因此,C++20 引入了概念和模板约束来解决这个问题。

概念是一个名称,它描述了一组约束,这些约束说明了模板参数必须满足的要求。例如,可以创建用于约束类型 T 的概念:

template<typename T>
concept MyConcept = requires(T x) {
    x.func();  // 要求 T 类型必须有 func 成员函数
};

然后,在定义模板类时,可以使用 requires 关键字来约束它的模板参数必须满足哪些概念:

template<MyConcept T>
class MyClass {
    // ...
};

如果模板实参没有满足必需的概念,则编译器会给出错误提示。

在使用 C++20 标准时,可以使用模板约束来约束模板参数必须满足什么限制。模板约束是一个可以测试模板参数的布尔表达式,它在模板参数实例化之前进行计算。例如:

template<typename T>
requires std::is_integral_v<T>  // 要求 T 类型必须是整数类型
class MyClass {
    // ...
};

如果模板参数类型不是整数类型,则编译器会给出错误提示。

概念和模板约束都是用于约束模板参数必须满足什么要求,从而减少不必要的模板实例化。这在使用模板库(如 STL)时非常有用,因为可以确保只使用正确和经过优化的实例化函数。

14.4.4、数组模板示例和非类型参数

在模板类中,也可以定义非类型参数,非类型参数指的是模板参数不是类型,而是常量表达式,它可以是整数、枚举、指针或引用。非类型参数可以在编译时进行计算,并使用它来定义模板类的行为。

例如,可以使用非类型参数定义一个数组,如下所示:

template<typename T, int N>
class MyArray {
public:
    T data[N];  // 定义一个大小为 N 的数组
    // ...
};

MyArray<int, 10> arr;  // 定义一个包含 10 个 int 类型的数组
MyArray<char, 20> str;  // 定义一个包含 20 个 char 类型的数组

在使用 MyArray 时,可以定义一个大小为 N 的数组,其中 N 是非类型参数。

MyArray<int, 10> arr;  // 定义一个包含 10 个 int 类型的数组
MyArray<char, 20> str;  // 定义一个包含 20 个 char 类型的数组

非类型参数可以在模板中使用,例如可以使用非类型参数定义模板类的行为。例如,可以使用非类型参数在编译时定义模板类的大小:

template<typename T, int N>
class MyFixedSizeArray {
public:
    T data[N];
    int size() const {
        return N;
    }
};

MyFixedSizeArray<int, 10> arr;
cout << arr.size() << endl;  // 输出 10

在使用 MyFixedSizeArray 时,可以定义一个大小为 N 的数组,并在定义时就指定数组的大小。

除了整数参数之外,还可以使用枚举或指针等类型作为非类型参数。例如,可以使用枚举定义一个模板类的默认行为,如下所示:

enum class MyOption {
    Option1,
    Option2,
    Option3
};

template<MyOption option = MyOption::Option1>
class MyTemplateClass {
public:
    void func() {
        if constexpr (option == MyOption::Option1) {
            // ...
        }
        else if constexpr (option == MyOption::Option2) {
            // ...
        }
        else if constexpr (option == MyOption::Option3) {
            // ...
        }
    }
};

MyTemplateClass<MyOption::Option1> obj1;  // 创建一个模板对象,使用选项1
MyTemplateClass<MyOption::Option2> obj2;  // 创建一个模板对象,使用选项2
obj1.func();  // 使用选项1 的行为
obj2.func();  // 使用选项2 的行为

在使用 MyTemplateClass 时,可以使用枚举为非类型参数,指定模板对象的默认行为。

总之,使用数组模板示例和非类型参数,可以根据需要定义一个高度可定制化的模板类,从而更好地满足用户需求。

14.4.5、模板多功能性

模板具有很强的多功能性,因为它们可以适用于许多不同的数据类型,并且还可以用于不同的实现方式和算法。模板的多功能性使得可以编写更通用、可重用的代码,从而提高代码效率和可维护性。

以下是模板多功能性的一些示例:

  1. 容器类:可以使用模板来定义通用的容器类,例如 vector、list、set 等,它们都可以容纳不同类型的数据,通过模板可以对这些类型进行抽象,使得代码可以更加通用和可维护。

  2. 函数模板:可以使用函数模板来定义通用的函数,例如 max、min、sort 等,它们可以处理多种数据类型,从而提高代码效率和可重用性。

  3. 类模板:可以使用模板来定义通用的类,例如 stack、queue、map 等,它们可以支持不同的数据类型和算法,从而提高代码的通用性和可维护性。

  4. 模板元编程:在 C++ 中,还可以使用模板元编程技术,通过编写模板来实现一些计算机科学中的算法和技术,例如递归、二分查找、排序和树等,这些算法和技术可以应用于许多不同类型的问题。

总之,模板的多功能性是 C++ 的一个重要特点,可以提高代码的效率和可维护性,并使代码更加通用、可重用。

14.4.6、模板的具体化

模板可以有显式具体化(explicit specialization)和偏特化(partial specialization)两种形式。

1、显式具体化:对于某个特定的类型,可以通过显式具体化来定义模板。显式具体化的语法格式为:

template<> 
class 类名<实参列表> {
   // 类或函数定义 
};

例如,可以对模板类 Sort 进行显式具体化来处理指针类型的数组:

template <>
class Sort<int *> {
public:
    static void sort(int *arr, int len) {
        // 对指针类型的数组进行排序
    }
};

2、偏特化:偏特化是指对特定类型或特定条件下的模板进行重新定义。偏特化的语法格式为:

template<typename T1, typename T2>
class 类名<T1*, T2*> {
   // 类的定义
};

例如,可以对模板类 Pair 进行偏特化,来处理指针类型的成员变量:

template<typename T>
class Pair<T*> {
public:
    T *first;
    T *second;
    Pair(T *a, T *b) {
        first = a;
        second = b;
    }
};

总之,模板的具体化可以用于特定类型或特定条件下的模板定义,有助于提高代码的复用性和可读性。在具体化时需要注意避免与模板的通用形式重叠,以免出现意料之外的问题。

14.4.7、成员模板

成员模板(member template)指的是定义在类或结构体中的模板函数或模板类,可以随着类或结构体的实例化而实例化。

成员模板的语法格式为:

template<typename T>
class 类名 {
public:
    template<typename U>
    void function(U u);
    template<typename V>
    class InnerClass {
        // class definition
    };
};

template<typename T>
template<typename U>
void 类名<T>::function(U u) {
    // function body
}

template<typename T>
template<typename V>
class 类名<T>::InnerClass {
    // class definition
};

其中,function 和 InnerClass 都是成员模板,分别是一个模板函数和一个模板类。在模板参数列表中,T 和 U 或者 T 和 V 分别代表外层类和其中的成员模板参数类型。

通过成员模板,可以在类或结构体中方便地定义泛型算法和泛型数据结构,从而提高代码复用性。使用成员模板时需要注意如下几点:

  1. 在定义之外实现模板成员函数时,需要使用类名<T>::的限定符。

  2. 模板类的定义要放在类声明之内。

  3. 成员模板不允许被继承或者虚继承。

  4. 类或结构体的模板参数列表中的形参不能与成员模板参数列表中的形参同名,否则会发生错误。

总之,成员模板是 C++ 中非常有用的泛型编程特性,在编写复杂的数据结构和算法时非常方便,同时也提高了代码的可读性和可维护性。

14.4.8、将模板用作参数

在 C++ 中,可以使用模板作为函数或类的参数,这种使用方式被称为“模板作为参数”。

使用模板作为参数,可以将模板作为算法中的通用组件,提高代码的可重用性和可维护性。以下是两个示例:

1、使用函数模板作为参数:

template<typename T>
void printArray(T arr[], int size) {
    for (int i = 0; i < size; i++) {
        cout << arr[i] << ' ';
    }
    cout << endl;
}

template<typename T>
void applyFunction(T arr[], int size, void (*func)(T&)) {
    for (int i = 0; i < size; i++) {
        func(arr[i]);
    }
}

void increment(int& n) {
    n++;
}

int main() {
    int arr[] = {1, 2, 3};
    applyFunction(arr, 3, increment);
    printArray(arr, 3);
    return 0;
}

在上面的示例中,我们定义了一个函数模板 printArray,用于输出数组元素。我们还定义了另一个函数模板 applyFunction,用于将一个函数应用于数组元素。在主函数中,我们定义了一个整数类型的数组 arr,并调用了 applyFunction 函数,将 increment 函数应用于数组元素。最后,我们输出了数组 arr 的元素。

2、使用类模板作为参数:

template<typename T>
class Array {
public:
    Array(int size) {
        // ...
    }

    T& operator[](int index) {
        // ...
    }

    const T& operator[](int index) const {
        // ...
    }
};

template<typename T, typename Container>
class Stack {
private:
    Container storage;
public:
    void push(const T& value) {
        storage.push(value);
    }

    void pop() {
        storage.pop();
    }

    const T& top() const {
        return storage.top();
    }
};

int main() {
    Array<int> arr(5);
    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }

    Stack<int, Array<int>> s;
    for (int i = 0; i < 5; i++) {
        s.push(arr[i]);
    }

    while (!s.empty()) {
        cout << s.top() << ' ';
        s.pop();
    }
    cout << endl;

    return 0;
}

在上面的示例中,我们定义了一个类模板 Array,用于表示数组类型。我们还定义了另一个类模板 Stack,用于表示栈类型。在 Stack 类模板中,我们使用了一个模板参数 Container,用于指定底层存储容器的类型。在主函数中,我们使用了 Array 类模板作为 Stack 类模板的底层存储容器类型,并测试了 Stack 类模板的 pushpop 和 top 成员函数。

总之,使用模板作为参数,可以将模板作为算法中的通用组件,提高代码的可重用性和可维护性。在 C++ 中,模板也是一种非常有用的泛型编程特性。

14.4.9、模板类和友元

在 C++ 中,模板类和友元之间存在一定的关系,对于模板类的友元声明需要特别注意。

当我们在模板类中声明一个友元时,需要将友元函数或类的声明放在模板类的前面,同时需要使用 template 关键字和类模板参数列表来声明友元,以指明友元函数或类也是一个模板。下面是一个简单的示例:

template<typename T>
class Foo {
public:
    Foo(T _data) : data(_data) {}
    template<typename U>
    friend class Bar;
private:
    T data;
};

template<typename U>
class Bar {
public:
    void print(const Foo<U>& foo) {
        cout << foo.data << endl;
    }
};

int main() {
    Foo<int> foo(42);
    Bar<int> bar;
    bar.print(foo);
    return 0;
}

在上面的示例中,我们定义了一个模板类 Foo,以及一个模板类 Bar。在 Foo 类中,我们声明了 Bar 类为 Foo 类的友元。为了正确声明 Bar 类为模板类,我们使用了 template 关键字和 typename U 类模板参数列表。

在主函数中,我们定义了一个 Foo<int> 类型的 foo 对象,并调用了 Bar<int> 类的 print 成员函数输出 foo 对象的 data 成员变量。

总之,当我们需要在模板类中声明一个友元时,需要注意友元的声明顺序以及使用 template 关键字和类模板参数列表。使用正确的友元可以提高代码的灵活性和可维护性,使得模板类可以更加通用、可重用。

14.4.10、模板别名(C++11)

模板别名是 C++11 中引入的一种特性,可以用来为模板类型定义别名,方便进行编程。

使用 using 关键字来定义模板别名,语法格式为:

template <typename T>
using MyVector = std::vector<T, MyAllocator<T>>;

在上面的示例中,我们使用 using 关键字定义了一个模板别名 MyVector,用来代替 std::vector<T, MyAllocator<T>>。这样,我们就可以使用 MyVector 来定义一个向量类型,而不用每次都写出完整的模板类型名称。例如:

MyVector<int> v; // 等价于 std::vector<int, MyAllocator<int>> v;

模板别名不仅可以用于模板类,还可以用于模板函数和函数指针类型的模板别名。例如:

template <typename T>
using MyFunc = void (*)(T);

template <typename T>
void myFunc(T t) {
    // ...
}

MyFunc<int> f = myFunc; // 等价于 void (*f)(int) = myFunc;

在上面的示例中,我们使用 using 关键字定义了一个模板函数指针类型的模板别名 MyFunc,用来代替 void (*)(T) 类型的函数指针。这样,我们就可以使用 MyFunc 来定义一个函数指针类型,而不用每次都写出完整的函数指针类型名称。例如,我们将 MyFunc<int> 定义为指向 myFunc 函数的指针,并将其赋值给 f 变量。这样,f 变量就成为了一个名为 myFunc 的函数的指针,其参数类型为 int

总之,模板别名是 C++11 中非常有用的模板特性,可以使代码更加简洁和易读,增强代码的可维护性和可重用性。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: "C Primer Plus" 是一本经典的 C 语言教程书籍,它旨在为初学者提供一种深入浅出、易于理解的方式来学习 C 语言的基础知识和应用技巧。这本书结构清晰,内容详实,深入浅出地讲解了 C 语言的各个方面,包括变量、数据类型、运算符、流程控制、函数、指针、结构体、文件操作等。通过逐步增加难度的方式,读者可以逐步领会 C 语言的精髓,从初学者成长为具备一定经验和技能的程序员。 与其他 C 语言教程不同的是,"C Primer Plus" 强调实战教学,让读者通过不断练习和编写实际程序来理解和掌握 C 语言的各种应用,从而在实际工作更加得心应手。此外,书也提供了大量的练习题和实例代码,方便读者自行探索和实践。 总之,"C Primer Plus" 是一本教授 C 语言的经典著作,它基于深入的理论知识,提供了大量的实战编程练习机会,适合想要深入学习 C 语言的初学者和有一定经验的程序员。 ### 回答2: 《C Primer文版是一本经典的C语言教程,原版为《C Primer Plus》,是一本聚焦于C语言程序设计入门、进阶和实践的杰出著作。本书全面覆盖了C语言语法、函数、指针、数组、字符串、文件操作、内存管理等核心内容,包含丰富的示例和练习,让学习者逐步了解和掌握C语言的核心要素。 《C Primer文版相比于英文原版,更方便国学生阅读和理解。该书的内容体系完整,难度逐渐递增,因此对于C语言零基础的读者来说,也能循序渐进地学习C语言。该书知识点分类清晰,讲解详细,涵盖了C语言绝大部分特性和工具,有助于读者构建系统的学习框架,能够从小白逐步走向合格C程序员的道路上。 《C Primer文版是学习C语言必备的参考书籍之一,它不仅适用于学生自学,也适用于教授C程序设计的教师作为教材使用。它具有深入浅出、通俗易懂和良好的示范性质。对于只有一定编程经验的读者,该书也是适合挑战的好书。无论你是初学者还是进阶爱好者,它都能帮助你打下坚实的编程基础,快速提升编程实践能力,成为一个有成就的程序员。 ### 回答3: C Primer Plus 文版是一本通俗易懂、系统完整的C语言教材,由Stephen Prata编写。其最新版为第六版,内容涵盖了从C语言的基础概念到高级特性的知识点,详细介绍了函数、指针、数组、结构体、文件操作、动态内存分配等重要概念和技术应用。全书分为26,每都配有大量的例子和练习题,使读者能够深入了解C语言的应用和开发方法,同时提供了大量的修改和调试技巧。 C Primer Plus 文版不仅适用于初学C语言的新手,也可以作为已有C语言基础但需要提高的人员的参考书。此外,该书还涵盖了C语言编程的规范要求和工程实践,非常适合从事C语言编程的软件开发人员阅读和学习。总之,C Primer Plus 文版是学习C语言的入门必备教材之一,无论是基础学习和提高深入,都具有重要的学习价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值