C++ primer plus学习笔记 --- 第8章、函数探幽

8.1、C++内联函数

C++ 内联函数是一种函数调用机制,在编译器的优化下颇为常见,其主要目的是因为函数调用在运行期间会存在一定的时间和空间开销。内联函数允许编译器将被调用的函数代码直接嵌入到调用该函数的位置,这样可以避免额外的函数调用开销和参数传递,从而提高程序的运行速度。

在 C++ 中,内联函数一般通过在函数定义前面加上 inline 关键字来声明。比如:

inline int add(int a, int b) {
  return a + b;
}

在上述代码中,add() 函数被声明为内联函数,通过 inline 关键字告诉编译器将该函数的代码展开到调用其的位置。

需要注意的是,内联函数并非在所有情况下都比普通函数更快。如果内联函数代码较大,或者内联函数中包含循环、递归等复杂的语句,内联函数展开反而会增加代码量,并可能导致程序的运行时间变慢。此外,在某些编译器中,将函数定义为内联函数可能会影响到调试器的调试能力。

一般来说,内联函数适用于以下情况:

  • 函数体非常短小,通常只有几行代码。
  • 函数被频繁调用,并且在调用过程中没涉及到递归或者循环等特殊控制流结构。
  • 函数的参数类型并不复杂,通常是基本数据类型或者指针。
  • 该函数在内存中被频繁访问,并且需要对性能进行高度优化的场合。

总的来说,内联函数的使用需要谨慎,需要在代码优化过程中进行权衡和评估,遵循“小而美”的原则,同时结合实际情况进行调整。

8.2、引用变量

C++ 中的引用变量是一种对已存在变量的别名,它是在变量名前加上 & 符号来定义的。引用变量可以看作是在原有变量名的基础上另外起了一个别名,即对同一变量的不同访问方式。

引用变量有以下几个特点:

  1. 引用变量必须在定义时进行初始化,即必须指向某一已存在的变量。
  2. 引用变量在定义之后一旦确定了其指向的变量,就不能再更改其指向了,其本质上是一个常量指针。
  3. 引用变量在使用时等价于其所指向的变量,即对引用变量的操作本质上就是对其所指向的变量的操作。
  4. 引用变量可以作为函数的参数或返回值,从而可以在函数间传递变量的别名,而不是变量的副本,更加高效。

引用变量的主要作用是简化代码的编写和阅读,同时还可以减少不必要的内存拷贝,提高程序的执行效率。引用变量通常用于以下场合:

  1. 作为函数参数传递,可以避免拷贝大的结构体和类,提高函数调用的效率;
  2. 作为函数返回值,可以直接返回函数内定义的局部变量而不必进行内存拷贝;
  3. 用于遍历数组和 STL 容器等数据结构时,可以避免对数组的元素或者容器内部对象进行不必要的拷贝;
  4. 用于修改函数外部的变量,可以改变变量的值而不必返回函数值。

但同时,也需要注意引用变量存在的一些问题,比如:

  1. 引用变量必须指向已存在的变量,否则会导致编译错误;
  2. 引用变量不支持空指针,即不能定义一个空引用;
  3. 引用变量只能在其作用域内有效,超出作用域就会被销毁;
  4. 引用变量不能作为数组的下标;
  5. 引用变量不能被 const 函数返回。

总之,引用变量在 C++ 中是很有用的一种特性,能够提高代码的效率和美观程度,但同时也需要谨慎使用,注意其存在的一些限制和问题。

8.2.1、创建引用变量

在 C++ 中,可以通过在变量名前面加上 & 符号来创建引用变量,如下所示:

int num = 10;
int &refNum = num; // 创建一个 int 类型的引用变量 refNum,并指向 num 变量

上面的代码中,我们在 num 变量名前加上 & 符号,创建了一个 refNum 引用变量,它指向了 num 变量。此时,refNum 和 num 变量实际上指向同一个内存地址,即对 refNum 的操作等效于对 num 的操作。

需要注意的是,创建引用变量时必须将其初始化为一个已经存在的变量。如果引用变量未初始化或初始化时指向了一个不存在的变量,程序将无法通过编译。

引用变量一般也必须在定义时进行初始化。如果试图在程序运行过程中将引用变量重新指向另一个变量,编译器将会报错。

需要注意的是,引用变量只是为变量起了一个别名,不会增加新的内存空间。因此,对引用变量的修改实际上也就是对其所指向的变量进行的修改,反过来同样成立。

8.2.2、将引用用作函数参数

在 C++ 中,可以将引用作为函数的参数进行传递。这样可以直接传递变量的别名,而不是变量的副本,从而提高代码的执行效率,并且能够直接修改原始变量的值。

函数中声明引用参数的语法为:在参数类型前加 & 符号。例如:

void swap(int& a, int& b) {
  int temp = a;
  a = b;
  b = temp;
}

在上面的函数中,我们使用了两个整型引用参数 a 和 b,这样调用函数时将可以直接传递变量的引用,而不是变量的副本,从而可以高效地交换变量的值。

需要注意的是,当引用作为函数参数进行传递时,实际上传递的是变量的别名,而不是变量本身。因此,如果在函数内部修改引用变量的值,也就是修改了其所指向的变量的值。

引用参数一般用于以下场合:

  1. 当函数需要修改实参的值时,可以用引用作为参数进行传递,因为引用可以直接修改参数的值。
  2. 当函数需要传递一个大的数据结构(比如结构体或对象)时,使用引用可以避免复制该数据结构,从而提高效率。
  3. 当函数需要返回多个值时,可以将多个变量通过引用作为参数传递到函数中,然后在函数内部修改它们的值,这样就可以不用返回多个变量了。

需要注意的是,在函数中使用引用参数时,必须保证传入的参数是已经初始化的变量。如果调用函数时传入的是未初始化的变量,则会发生未定义的行为。

8.2.3、引用的属性和特别之处

C++ 中的引用是一种非常有用的特性,它具有以下几个属性和特别之处:

  1. 引用是一种别名,本质上是指向现有变量的指针,所以在内存上只有一份数据,引用变量是原始变量的同义替换,不会占用额外的内存空间。
  2. 引用必须在定义时初始化,并指向一个已存在的变量,因为引用的本质是在原有变量名的基础上添加了一个别名。
  3. 引用不能指向空值,即不能定义一个空引用。如果引用未初始化或者初始化时指向了一个不存在的变量,会导致程序崩溃或未定义的行为。
  4. 引用变量不能被修改指向其他变量,即引用是一种常量指针,其指向的变量不可改变。
  5. 引用可以作为函数参数传递和返回值,通过引用传参可以避免拷贝大对象,提高程序效率。同时通过引用返回值可以传递函数结果,同时也可以传递多个结果,比如通过引用参数修改多个变量的值。
  6. 引用不能被赋给另一个引用,即不能创建引用的引用。这是因为引用变量已经是指向变量的别名,再创建别名的别名没有意义。
  7. 引用和指针有相似之处,但也有不同之处。引用是一种非常安全的指针,可以避免指针相关的错误,但是没有指针的灵活性和通用性。
  8. 引用适用于简单数据类型,比如整数、浮点数等,以及小型的类、结构体等,不建议用于数组或者大型的容器等。

总之,引用是一种非常有用的特性,可以提高程序的效率和可读性,但也需要注意使用时引用的限制和特别之处,并合理使用引用来优化代码。

8.2.4、将引用用于结构

当引用用于结构时,需要注意以下几点:

  1. 引用的类型必须与结构的类型完全匹配,否则会编译错误。例如,一个指向 struct A 的引用不能被初始化为指向 struct B 的引用。
  2. 对引用的修改会直接反映在被引用的结构上,因此引用应该只用于对结构的成员进行访问,而不是修改。
  3. 在某些情况下,结构内的成员可能是引用类型。在这种情况下,我们必须小心避免出现循环引用,否则会导致内存泄漏和未定义的行为。
  4. 在使用引用作为函数返回值时,需要注意返回值的生存期是否正确,特别是在返回指向局部变量的引用时,需要小心处理。

下面是一个例子,演示了如何定义一个包含引用成员的结构:

#include <iostream>

using namespace std;

struct Coordinate {
  int& x; // 定义一个引用类型的成员 x
  int& y; // 定义一个引用类型的成员 y
  Coordinate(int& x_, int& y_): x(x_), y(y_) {} // 初始化引用成员
};

int main() {
  int x = 10, y = 5;
  Coordinate point(x, y);
  cout << point.x << ", " << point.y << endl; // 输出结果: 10, 5
  x = 2, y = 3;
  cout << point.x << ", " << point.y << endl; // 输出结果: 2, 3
  return 0;
}

在上面的例子中,我们定义了一个 Coordinate 结构,其中包含两个引用类型的成员 x 和 y。通过在结构体构造函数中初始化这些引用类型的成员,我们可以将引用指向其他变量,并且引用的值会随着对其他变量的修改而变化。

需要注意的是,在使用引用成员时,应该始终确保被引用的变量具有正确的生存周期,特别是在使用引用成员作为函数返回值时。如果引用成员指向被销毁的对象或者不再存在的对象,会导致未定义的行为和内存泄漏。

8.2.5、将引用用于类对象

当引用用于类对象时,需要注意以下几点:

  1. 引用的类型必须与类的类型完全匹配,否则会编译错误。例如,一个指向 class A 的引用不能被初始化为指向 class B 的引用。
  2. 引用可以用于访问类成员函数和变量,但是不能用于修改 const 成员变量或者调用 const 成员函数。
  3. 在使用引用作为函数参数时,应该使用引用来避免复制类对象,这样可以提高程序的效率并避免造成大量的内存开销。
  4. 在使用引用作为函数返回值时,需要注意返回值的生存期是否正确,特别是在返回指向局部变量的引用时,需要小心处理。
  5. 在将引用用作类成员时,需要确保它们指向的对象的生命周期足够长,以避免悬垂引用的问题。因此,在使用引用来管理类对象时,可能需要考虑一些基于引用计数、智能指针等管理方案。

下面是一个简单的示例代码,演示了如何在类中使用引用:

#include <iostream>
using namespace std;

class MyClass {
 public:
  MyClass(int& a_, int& b_): a(a_), b(b_) {}
  void print() const {
    cout << "MyClass: a = " << a << ", b = " << b << endl;
  }

 private:
  int& a;
  int& b;
};

int main() {
  int x = 10, y = 5;
  MyClass obj(x, y);
  obj.print(); // 输出结果: "MyClass: a = 10, b = 5"
  x = 2, y = 3;
  obj.print(); // 输出结果: "MyClass: a = 2, b = 3"
  return 0;
}

在上面的代码中,我们定义了一个名为 MyClass 的类,并在其中使用引用类型的私有成员 a 和 b,用于访问和修改外部的 x 和 y 变量。通过在构造函数中将 a 和 b 初始化为 x 和 y 的引用,我们可以随时更改 x 和 y 的值,并通过调用 MyClass 类的 print() 函数验证这一点。

需要注意的是,在使用引用成员时,应该始终确保被引用的变量具有正确的生存周期,特别是在使用引用成员作为函数返回值时。如果引用成员指向被销毁的对象或者不再存在的对象,会导致未定义的行为和内存泄漏。因此,在使用引用成员时,我们应该避免使用指向临时变量或函数参数的引用,或者使用更加健壮的引用计数、智能指针等管理方案来避免悬垂引用的问题。

8.2.6、对象、继承和引用

在 C++ 中,对象之间可以相互引用。当一个类创造对象时,它会创建一个完整的对象实例,该对象实例占用一定的内存。如果将一个对象作为另一个对象的成员,只需要分配额外的内存来存储引用,而不是整个对象

在继承方面,子类对象也可以引用其父类对象。C++ 中,可以使用多态性和虚函数来实现对父类对象的引用。多态性使得我们可以通过父类对象来访问子类对象的成员函数和变量。这个特性使得代码更加简洁和灵活,因为不需要直接操作特定的子类对象。

需要注意的是,当使用引用来引用父类对象时,可以通过 dynamic_cast 来检查引用是否指向了正确的对象。dynamic_cast 在运行时检查对象类型,如果类型匹配,则可以将引用转换为正确类型的引用。

下面是一个例子,演示了如何使用引用来引用父类对象:

#include <iostream>
using namespace std;

class Shape {
 public:
  virtual void draw() {
    cout << "Drawing a Shape..." << endl;
  }
};

class Circle : public Shape {
 public:
  void draw() override {
    cout << "Drawing a Circle..." << endl;
  }
};

void drawShapeByReference(Shape& shape) {
  shape.draw();
}

int main() {
  Shape shape;
  Circle circle;
  drawShapeByReference(shape); // 输出结果: Drawing a Shape...
  drawShapeByReference(circle); // 输出结果: Drawing a Circle...
  Shape& shapeRef = circle;
  shapeRef.draw(); // 输出结果: Drawing a Circle...
  if (Circle* pCircle = dynamic_cast<Circle*>(&shape)) {
    cout << "shapeRef is a Circle object." << endl;
    pCircle->draw();
  } else {
    cout << "shapeRef is not a Circle object." << endl;
  }
  return 0;
}

在上面的例子中,我们定义了一个名为 Shape 的基类和一个名为 Circle 的派生类,其中 Circle 类覆盖了 Shape 类的 draw 函数,以便绘制圆形。然后,我们定义了一个 drawShapeByReference 函数,它可以接受任何类型的 Shape 对象的引用,并调用其 draw 函数。

在 main 函数中,我们创建了一个 Shape 对象和一个 Circle 对象,并使用它们来调用 drawShapeByReference 函数。接下来,我们使用 Shape 类型的引用来引用了 Circle 对象,并调用了 draw 函数。最后,我们使用 dynamic_cast 来检查 shapeRef 是否指向了正确的 Circle 对象,并调用了 draw 函数。

总之,通过使用引用可以使代码更加简洁和高效,在继承和多态性方面发挥重要作用。但是,在使用引用时,需要特别注意生存期的管理和防止出现悬垂引用的问题。

8.2.7、何时使用引用参数

在 C++ 中,参数可以通过值传递、指针传递和引用传递三种方式传递给函数。其中,引用传递是一种常见的方式,但是在什么情况下使用引用参数是比较合适的呢?

1、函数需要修改参数并且需要返回修改后的结果

如果一个函数需要修改参数并且需要返回修改后的结果,那么使用引用参数是比较合适的,因为引用参数可以直接修改原始变量,而不需要复制一份副本。这样可以减少内存开销,并且能够提高函数的执行效率。例如:

void swap(int& a, int& b) {
  int temp = a;
  a = b;
  b = temp;
}

int main() {
  int x = 1;
  int y = 2;
  swap(x, y);
  cout << "x = " << x << ", y = " << y << endl; // 输出结果:x = 2, y = 1
  return 0;
}

在上面的例子中,我们定义了一个 swap 函数,它通过引用参数传递了两个整数。函数将它们的值交换了一下,然后修改了原始变量的值。这样,函数就避免了返回一个副本的开销。

2、函数需要访问大的数据结构

如果函数需要访问一个大的数据结构,例如一个大的数组或一个复杂的对象,那么使用引用参数是比较合适的,因为复制一个大的数据结构会带来很大的开销,而引用参数可以直接访问原始数据结构。例如:

void printArray(int (&arr)[5]) {
  for (int i = 0; i < 5; i++) {
    cout << arr[i] << endl;
  }
}

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

在上面的例子中,我们定义了一个 printArray 函数,它通过引用参数传递了一个整数数组。函数遍历整个数组,并将所有元素都打印到控制台。由于数组较大,复制数组会带来大量的开销,因此使用引用参数是比较合适的。

3、函数需要修改实参所在的数据结构

如果函数需要修改实参所在的数据结构,那么使用引用参数是比较合适的,因为引用参数可以直接修改原始数据结构,而不需要复制一份副本。例如:

struct Person {
  string name;
  int age;
};

void changeAge(Person& person, int newAge) {
  person.age = newAge;
}

int main() {
  Person john = {"John", 20};
  changeAge(john, 21);
  cout << john.age << endl; // 输出结果:21
  return 0;
}

在上面的例子中,我们定义了一个 Person 结构体,它包含一个名字和一个年龄。然后,我们定义了一个 changeAge 函数,它将修改一个 Person 对象的年龄。由于我们需要修改原始数据结构,使用引用参数是比较合适的。

总之,在使用引用参数时,需要遵循一些基本原则,例如:

  • 只有需要修改实参的时候才使用引用参数。
  • 如果函数需要返回修改后的结果,那么使用引用参数是比较合适的。
  • 如果函数需要访问大的数据结构,那么使用引用参数可以避免副本的开销。
  • 如果函数需要修改实参所在的数据结构,那么使用引用参数可以避免复制一份副本。

8.3、默认参数

C++ 允许函数参数拥有默认值,可以在函数定义时为参数提供默认值。这些参数被称为 默认参数,它们可以简化函数调用,并且可以增加代码的可读性。如果函数调用时没有提供对应的参数值,则使用默认值。

默认参数的语法格式如下:

void functionName(type parameterName = defaultValue);

其中,parameterName 表示函数参数的名称,而 defaultValue 表示该参数的默认值。注意,只有函数的最右边的参数可以设置默认值,因为如果默认值在中间,那么如果该参数没有被传递,编译器无法判断之后的参数应该赋给哪个变量。

下面是一个使用默认参数的函数示例:

#include <iostream>
using namespace std;

void printNum(int num = 1) {
  cout << num << endl;
}

int main() {
  printNum();    // 输出结果:1
  printNum(2);   // 输出结果:2
  return 0;
}

在上面的例子中,我们定义了一个 printNum 函数,它拥有一个默认参数 num = 1。当调用该函数时,如果没有提供 num 的值,那么该函数将打印默认值 1;如果提供了 num 的值,则打印指定的值。

当使用默认参数时,需要注意以下几点:

  • 可以将多个函数参数设置为默认值,但必须按照从右到左的顺序进行设置。
  • 默认值只能在函数的声明或定义中设置一次,也就是说,函数的各个声明中必须保持默认参数的一致性。
  • 在函数调用时,可以使用默认参数来跳过任意数量的参数,只需提供要覆盖的参数即可。

下面是一个使用多个默认参数的函数示例:

#include <iostream>
using namespace std;

void printInfo(string name, int age = 18, string gender = "male") {
  cout << "name: " << name << endl;
  cout << "age: " << age << endl;
  cout << "gender: " << gender << endl;
}

int main() {
  printInfo("John", 20, "male");          // 输出结果:name: John, age: 20, gender: male
  printInfo("Mary", 22);                  // 输出结果:name: Mary, age: 22, gender: male
  printInfo("Peter", "female");           // 输出结果:name: Peter, age: 18, gender: female
  printInfo("Tom");                       // 输出结果:name: Tom, age: 18, gender: male
  return 0;
}

在上面的例子中,我们定义了一个 printInfo 函数,它拥有三个参数:nameage 和 gender。其中,age 和 gender 都设置了默认值,可以不用在函数调用时指定。当函数调用时,如果不提供 age 和 gender 参数,则使用默认值。

需要注意的是,在函数声明时,需要在参数列表的声明和定义上都设置默认值。例如:

void functionName(int param1 = 1, int param2 = 2);

在函数实现时也需要指定默认值的参数,例如:

void functionName(int param1, int param2 /*= 2 */) {
  //...
}

需要注意,在函数实现时,可以不指定默认参数的值,这是因为默认参数的值已经在函数的声明中指定过了。但是,如果函数的声明被修改了,那么默认参数的值可能也需要相应地进行修改。

8.4、函数重载

C++ 允许在同一个作用域内定义多个同名的函数,只要它们的参数个数或参数类型不同,就可以形成函数重载。函数重载可以在代码中提高可读性和可维护性,允许同一个函数名在不同的上下文中执行不同的操作。

下面是一个使用函数重载的例子:

#include <iostream>
using namespace std;

// 重载的函数,用于求两个整数的和
int add(int a, int b) {
  return a + b;
}

// 重载的函数,用于求两个浮点数的和
double add(double a, double b) {
  return a + b;
}

int main() {
  int x = 1, y = 2;
  double f1 = 1.1, f2 = 2.2;

  cout << add(x, y) << endl;        // 输出结果:3
  cout << add(f1, f2) << endl;      // 输出结果:3.3

  return 0;
}

在上面的示例中,我们定义了两个函数 add,一个用于求两个整数的和,另一个用于求两个浮点数的和。这两个函数之间的区别在于它们的参数类型不同。在使用这两个函数时,根据传递给它们的参数类型的不同,编译器会自动选择调用哪个函数。

需要注意的是,在进行函数重载时,函数的返回类型不能用于区分函数,因为调用函数时只能根据参数列表来判断调用哪个函数。如果重载函数仅仅只是返回类型不同,那么在编译时就会产生错误。

下面是不能构成函数重载的例子:

int sum(int a, int b);
double sum(int a, int b);   // 编译错误,返回类型不能用来区分函数

另外,需要注意以下几点:

  • 函数名相同,参数列表不同的函数被视为不同的函数,包括参数类型、顺序和个数。
  • 默认参数不会影响函数重载,因为调用时可以省略默认参数。
  • 可以定义形参为 const 类型的。例如:const int x 和 int x 被视为是不同的参数。
  • C++ 支持函数模板,它们也可以看作是函数重载的一种形式。通过模板,可以定义一种通用的函数,其中某些变量或类型是未知的。这种函数可以为不同的类型和参数自动生成多个函数。

8.4.1、重载示例

下面是一个更具体的重载示例:

#include <iostream>
#include <string>
using namespace std;

// 重载的函数,用于输出 int 类型的值
void print(int x) {
  cout << "int: " << x << endl;
}

// 重载的函数,用于输出 double 类型的值
void print(double x) {
  cout << "double: " << x << endl;
}

// 重载的函数,用于输出字符串
void print(const char* str) {
  cout << "string: " << str << endl;
}

int main() {
  int a = 10;
  double b = 3.14;
  const char* c = "hello";

  print(a);   // 输出:int: 10
  print(b);   // 输出:double: 3.14
  print(c);   // 输出:string: hello

  return 0;
}

在上面的示例中,我们定义了三个重载的 print 函数,分别用于输出 int 类型的值、double 类型的值和字符串。在 main 函数中,我们分别调用了这三个函数,并传递不同类型的参数。根据参数类型的不同,编译器会自动选择调用哪个函数。

8.4.2、何时使用函数重装载

可以在以下情况下使用函数重载:

  1. 实现相似的功能:当需要实现一系列相似但不完全相同的函数时,可以使用函数重载。例如,可以定义多个 print 函数来输出不同类型的变量。

  2. 参数类型不同:当需要处理不同数据类型的参数时,可以使用函数重载。例如,可以定义一个函数来处理整数类型的参数,另一个函数来处理浮点数类型的参数。

  3. 函数返回类型不同:当需要根据不同的参数类型返回不同类型的结果时,可以使用函数重载。例如,可以定义一个函数来返回整数类型的值,另一个函数来返回浮点数类型的值。

  4. 参数个数不同:当需要处理不同数量的参数时,可以使用函数重载。例如,可以定义一个函数来处理一个参数,另一个函数来处理两个参数。

需要注意的是,在使用函数重载时,应该遵守以下规则:

  1. 函数名相同。

  2. 参数数量或类型不同。

  3. 函数返回类型可以相同,也可以不同。但不能仅仅因为返回类型不同就重载函数。

  4. 函数不同的类型或次序总是不同的。

  5. 如果两个函数的签名仅仅是返回类型不同,这属于重复定义,编译器将产生错误。

  6. 函数重载的作用域为当前所属的命名空间。

总之,函数重载提供了一种灵活的、方便的方式来处理不同类型和数量的参数,并且可以使代码更加清晰易读。

8.5、函数模板

函数模板是一种通用的函数定义,可用于创建多个功能类似的函数。函数模板定义了一种通用的函数类型,它有一个或多个类型参数。这些类型参数可以在调用函数模板时被具体化,从而生成实际的函数。

函数模板通常定义在头文件中,并使用 template 关键字指定一个或多个类型参数,如下所示:

template <typename T>
void myFunction(T arg)
{
  // 函数定义
}

模板参数在尖括号 <> 中定义,它可以是任何类型,包括基本类型和用户自定义类型。

当调用函数模板时,可以使用具体的类型来替换类型参数,如下所示:

myFunction<int>(5);   // 实例化 myFunction<int>(int arg)
myFunction<double>(3.14);   // 实例化 myFunction<double>(double arg)

在编译时,编译器将生成对应的函数实例,具体化的函数与函数模板具有相同的名称,但参数类型是具体化的类型。

函数模板还支持多个类型参数,可以使用逗号 , 分隔不同类型参数,如下所示:

template <typename T, typename U>
void myFunction(T arg1, U arg2)
{
  // 函数定义
}

使用多个类型参数时,必须在调用函数模板时为每个类型参数指定类型,如下所示:

myFunction<int, double>(5, 3.14);   
// 实例化 myFunction<int, double>(int arg1, double arg2)

函数模板使得代码更具通用性和可重用性,并且可以减少代码重复。

8.5.1、重载的模板

C++ 中,函数模板可以重载,即定义多个同名但具有不同模板参数的函数模板,这些函数模板可以处理不同类型和数量的参数,在调用函数模板时,编译器会根据参数类型和数量选择合适的函数模板进行调用。

以下是一个重载的模板示例,其中 max 函数模板用于寻找两个数中的最大值:

#include <iostream>
using namespace std;

// 第一个模板函数
template <typename T>
T max(T a, T b) {
  cout << "Template function1 called." << endl;
  return a > b ? a : b;
}

// 第二个模板函数
template <typename T, typename U>
T max(T a, U b) {
  cout << "Template function2 called." << endl;
  return a > b ? a : b;
}

int main() {
  int x = 1, y = 2;
  double f1 = 1.1, f2 = 2.2;

  cout << max(x, y) << endl;                  // 输出结果:Template function1 called. 2
  cout << max<double>(f1, f2) << endl;        // 输出结果:Template function1 called. 2.2
  cout << max<double, int>(f1, y) << endl;    // 输出结果:Template function2 called. 2.2

  return 0;
}

在上面的示例中,我们定义了两个重载的 max 函数模板,分别处理同类型参数和不同类型参数的情况。在 main 函数中,我们分别调用了这两个函数模板,并传递了不同类型的参数。根据参数类型和数量的不同,编译器会自动选择调用哪个函数模板。

8.5.2、模板的局限性

尽管函数模板提供了强大的通用编程功能,但也有其局限性。下面是一些模板的局限性:

  1. 过于灵活:函数模板可以接受任何可赋给模板参数类型的形参类型,这使得模板代码过于灵活,可能导致更难以理解。

  2. 编译错误不直观:由于模板代码是在编译时实例化的,因此编译器有时可能难以推断出类型参数,这导致在编译时出现一些不直观的错误信息。

  3. 模板代码可能会膨胀:在编译时,编译器会为每个不同的模板实例生成一次实际的代码。如果有很多模板实例,这可能导致编译时间延长和程序的二进制文件大小增加。

  4. 不能类型推断:在使用模板函数时,必须显式指定模板参数,这可能对代码的易读性造成一定的影响。

  5. 不支持部分特化和默认模板参数:与类模板不同,函数模板不支持部分特化和默认模板参数。

综上所述,虽然函数模板提供了通用编程的强大功能,但在实践中需要谨慎使用,并且需要在代码的可读性和性能之间进行权衡。

8.5.3、显示具体化

显示具体化是一种特殊形式的模板,用于在特定情况下优化模板代码的性能或实现特定的行为。显示具体化是对特定类型的模板参数针对性的定义,允许编写针对该类型参数的特定模板实现。

以下是一个显示具体化的示例,使用显示具体化优化特定类型的 max 函数模板的实现:

#include <iostream>
using namespace std;

// 普通的 max 函数模板
template <typename T>
T max(T a, T b) {
  cout << "Template function1 called." << endl;
  return a > b ? a : b;
}

// 显示具体化,处理 char 类型
template<>
char max(char a, char b) {
  cout << "Char-specific template function2 called." << endl;
  return a > b ? a : b;
}

int main() {
  int x = 1, y = 2;
  double f1 = 1.1, f2 = 2.2;
  char c1 = 'a', c2 = 'b';

  cout << max(x, y) << endl;          // 输出结果:Template function1 called. 2
  cout << max(f1, f2) << endl;        // 输出结果:Template function1 called. 2.2
  cout << max(c1, c2) << endl;        // 输出结果:Char-specific template function2 called. b

  return 0;
}

在上述示例中,我们定义了一个普通的 max 函数模板,并针对特定的 char 类型进行了显示具体化。在调用函数时,如果参数是 char 类型,编译器会选择调用针对特定类型的模板实现,否则则使用普通的模板实现。

显示具体化可以显著提高针对特定类型的模板的性能,同时还可以实现特定的行为,因此在实践中使用起来非常有用。但是,由于其具有很强的优化能力,可能会导致代码可移植性差,因此需要谨慎使用。

8.5.4、实例化和具体化

实例化是指根据模板参数生成具体的函数实例的过程。当使用模板函数时,编译器会根据函数调用的参数类型来确定函数模板的参数类型,然后生成具体的函数实例。

具体化是指针对特定类型参数生成的针对性函数模板代码的过程。实际上,具体化是一种模板实例,它提供了特定类型参数的专门实现。具体化可以通过两种方式实现:显示具体化和隐式具体化。

显示具体化是针对特定类型参数的具有针对性的定义模板,而隐式具体化则是编译器根据需要自动为特定类型参数生成模板实例。

C++11 中引入了针对函数模板的显式实例化语法,这个语法可以在编译时生成特定类型参数的具体化版本,而避免了编译器自动推断类型,从而提高了编译速度和代码可读性。以下是一个使用显式实例化的例子:

#include <iostream>
using namespace std;

// 函数模板
template<typename T>
void foo(T t) {
  cout << "foo(T) is called." << endl;
}

// 显式实例化
template void foo<int>(int);

// 显式具体化
template<> void foo<char>(char ch) {
  cout << "foo(char) is called." << endl;
}

int main() {
  int i = 1;
  char c = 'a';
  foo(i);   // 调用已实例化的 foo<int>(T)
  foo(c);   // 调用显式具体化的 foo<char>(char)

  return 0;
}

在这个例子中,我们定义了一个函数模板 foo,然后使用显式实例化语法生成了 foo 的针对于 int 的具体实例。而对于 char 类型,我们使用显式具体化语法定义了一个具体实现。在 main 函数中,我们分别调用了 foo(i) 和 foo(c),输出对应的函数调用信息。

总体来说,实例化和具体化是函数模板的重要概念,对于函数模板的深入理解非常重要。

8.5.5、编译器选择使用哪个函数版本

当存在重载函数或模板函数时,编译器需要选择使用哪个函数版本。编译器将首先查找与被调用函数参数类型和数量最匹配的函数或函数模板,如果有多个匹配,则编译器将应用以下规则:

  1. 精确匹配优先级最高,即所有参数类型都精确匹配的函数或函数模板将被优先选择。

  2. 标准类型转换规则匹配优先于用户自定义类型转换。标准类型转换包括整型提升,浮点类型提升,指针类型转换等。

  3. 当两个函数或函数模板都可以通过标准类型转换将参数转换为匹配类型时,编译器将选择最特化函数版本。

  4. 如果有多个函数或函数模板都符合上述规则,且无法通过返回类型区分,编译器将报告二义性错误。

下面是一个示例,演示了编译器选择使用哪个函数版本:

#include <iostream>
using namespace std;

void print(int n) {
  cout << "int: " << n << endl;
}

void print(double d) {
  cout << "double: " << d << endl;
}

template<typename T>
void print(T t) {
  cout << "template: " << t << endl;
}

int main() {
  int x = 10;
  double y = 1.23;
  print(x);      // 输出 int: 10
  print(y);      // 输出 double: 1.23
  print("hello");  // 输出 template: hello

  return 0;
}

在上述示例中,我们定义了一个普通的 print 函数和一个模板函数 print,并针对不同的参数类型提供不同的实现。在调用 print 函数时,编译器将自动选择最合适的函数版本,以处理传递的参数。注意,虽然 print("hello") 传递了一个字符串字面值,但编译器将其转换为 const char* 类型并选择调用 print 模板函数。

8.5.6、模板函数的发展

模板函数是 C++ 中非常重要的一部分,可以说这一特性为许多 C++ 库的设计提供了强大的支持。随着 C++ 的发展和标准的更新迭代,模板函数的特性和用法也不断地得到改进和扩展。这里列举一些比较重要的特性和引入版本:

  1. 模板函数:C++98 标准引入了模板函数,它使得相似的函数可以通过使用模板来重用代码。

  2. 非类型模板参数:C++98 标准还引入了非类型模板参数的概念,它允许将一个非类型参数(如整数或指针)作为模板参数传递给模板函数或类模板。

  3. 显示实例化:C++11 引入了显式实例化,它提供了一种显式地要求编译器生成模板函数或类的具体实例的方法。这样,就可以显式地控制编译器实例化的方式,从而提高编译速度和代码可读性。

  4. 可变参数模板:C++11 还引入了可变参数模板的概念,它是一种可以接受任意数量和类型的参数的模板。可变参数模板在格式化输出、类型安全的 printf 等方面有着很好的应用。

  5. constexpr 函数:C++11 引入了 constexpr 函数,它是一种可以在编译期间计算值的函数。constexpr 函数的使用可以大大提高程序在编译时的执行效率。

  6. 模板别名:C++11 还引入了模板别名的概念,它允许给模板类型指定一个易于理解和使用的新名称,从而提高代码的可读性。

  7. 默认模板参数:C++11 引入了默认模板参数,它是一种指定模板参数默认值的方法。使用默认模板参数可以省去一些繁琐的代码,同时也有助于提高代码的可读性。

  8. 模板元编程:C++14 和 C++17 标准进一步扩展了模板的功能,引入了一些新的概念,如通用 lambda 表达式、编译期常量等,这些概念使得模板在编写元编程代码时更加强大和便捷。

总之,模板函数是 C++ 中非常重要和强大的特性,它为程序员提供了许多强大的编程能力。随着 C++ 标准的更新迭代,模板的功能和用法也在不断地得到改进和扩展,为程序员提供了更加便捷和灵活的编程体验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值