Cpp复习 Chapter 1 内联函数、引用变量、函数重载、函数模板


Cpp系列笔记目录

【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板
【Cpp筑基】二、声明 vs 定义、头文件、存储持续性作用域和链接性、名称空间
【Cpp筑基】三、对象和类
【Cpp筑基】四、重载运算符、友元、类的转换函数
【Cpp筑基】五、类的继承、虚函数、抽象基类


【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板

1. 内联函数

C++提供了一种内联函数,在 C++ 中,内联函数(inline function)是一种特殊的函数,其定义使用 inline 关键字来提示编译器将函数调用直接替换为函数体,以减少函数调用的开销。内联函数通常用于简短、频繁调用的函数。

要使用内联函数,必须:

  • 在函数声明前加上关键字inline
  • 在函数定义前加上关键字inline

注意内联函数不能递归。内联函数的基本语法:

1
2
3
inline return_type function_name(parameters){
// 函数体
}

举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 定义一个内联函数
inline int add(int a, int b) {
return a + b;
}

int main() {
int result = add(3, 4);
cout << "Result: " << result << endl;
return 0;
}

输出结果
1
Result: 7

inline工具是C++新增的特性,C语言使用预处理器语句#define来提供宏——内联代码的原始实现。例如:

1
#define SQUARE(X) ((X)*(X))

inline#define的主要区别是:

  1. 实现机制:
    • 内联函数是由编译器在编译时展开的,是一种编译时的语言特性。编译器会在调用内联函数的地方直接替换函数体。
    • #define宏是在预处理阶段展开的,是一种文本替换的机制。预处理器会在编译前把宏展开。
  2. 类型检查:
    • 内联函数有类型检查,编译器会检查参数类型是否匹配。
    • #define宏是简单的文本替换,没有类型检查,容易出现类型错误。
  3. 时间和空间开销:
    • 内联函数在编译时展开,没有函数调用的开销,但会增加程序的大小,增加空间成本。
    • #define宏在预处理阶段展开,没有函数调用开销,但可能会导致代码膨胀。

2. 引用变量

C++新增了一种复合类型——引用变量,使用运算符&,引用变量的主要用途是作为函数参数列表中的形参。例如,创建一个引用变量

1
2
int rats;
int & rodents = rats;

==注意==:引用必须在声明时进行初始化,而不能像指针一样,先声明再赋值。引用一旦与变量进行关联,就一直指向这个变量。

C++11中新增了另一种引用——==右值引用==(rvalue reference),这种引用可以指向右值,使用&&进行声明。

在 C++ 中,“左值”(lvalue)和“右值”(rvalue)是用来描述表达式值类型的一对术语。理解它们的概念对于掌握 C++ 语言的赋值、引用、移动语义等方面的内容非常重要。

左值(lvalue,locatable value)是指能够定位的值,它表示存储在内存中的某个位置的对象。因此,左值是可以取地址的,可以出现在赋值操作的左侧。例如:

1
2
3
int x = 10;   // x 是左值
int *p = &x; // 可以取地址
x = 20; // 可以出现在赋值操作的左侧

右值(rvalue,readable value)是指不具有持久存储位置的临时值,它通常是表达式求值的结果。右值不能取地址,也不能出现在赋值操作的左侧。例如:

1
2
3
int y = 10;     // 10 是右值
int z = y + 5; // (y + 5) 是右值
int *p = &10; // 错误,不能对右值取地址

左值引用是对左值的引用,用于绑定左值。例如:

1
2
3
int a = 10;
int &ref = a; // 左值引用
ref = 20; // 可以通过引用修改 a 的值

右值引用是对右值的引用,用于绑定右值。这是 C++11 引入的特性,主要用于实现移动语义和完美转发,以提高性能。

1
2
int &&rref = 10;  // 右值引用
rref = 20; // 可以通过引用修改右值

新增右值引用主要是用于移动语义完美转发,理解左值和右值的区别是掌握 C++ 高级特性(如移动语义和完美转发)的基础。

==引用常用于函数的参数传递==,这样可以避免不必要的拷贝,并且允许函数修改传入的参数值。例如:

1
2
3
4
5
6
7
8
9
10
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

int main() {
int x = 10, y = 20;
swap(x, y); // x 和 y 的值被交换
}

注意,在使用引用进行函数的参数传递的时候,我们应该尽可能使用const,将引用参数声明为常量数据的引用的理由有三个:

  • 使用const可以避免无意中修改数据的编程错误
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const数据
  • 使用const引用使函数能够正确生成并使用临时变量

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

// 不使用 const 引用
void printString(std::string& s) {
std::cout << "Non-const reference: " << s << std::endl;
s = "modified";
}

// 使用 const 引用
void printConstString(const std::string& s) {
std::cout << "Const reference: " << s << std::endl;
// s = "modified"; // 错误,无法修改 const 引用
}

int main() {
std::string str = "hello";

// 使用非 const 引用
printString(str);
std::cout << "After printString(): " << str << std::endl;

// 使用 const 引用
printConstString(str);
std::cout << "After printConstString(): " << str << std::endl;

return 0;
}

输出如下:

1
2
3
4
Non-const reference: hello
After printString(): modified
Const reference: modified
After printConstString(): modified

什么时候使用引用和指针呢?

使用引用参数的主要原因有两个:

  • 程序员能够修改调用函数中的数据对象
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

对于使用传递的值而不做修改的函数:

  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为const

3. 函数重载

C++实现==多态==有两种方式,

  • 编译时多态Compile-time Polymorphism(通过函数重载和模板实现)
  • 运行时多态Runtime Polymorphism(通过继承和虚函数实现)

函数重载的关键是函数的参数列表(也称为特征标),如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是不重要的。C++允许定义相同名称的函数前提是他们的特征标不同。

举个例子,编译时多态可以通过函数重载或者是通过模板进行实现:

  1. 通过函数重载进行实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 使用函数重载实现多态
    #include <iostream>

    void print(int i) {
    std::cout << "Integer: " << i << std::endl;
    }

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

    void print(const std::string& str) {
    std::cout << "String: " << str << std::endl;
    }

    int main() {
    print(42); // 调用 void print(int)
    print(3.14); // 调用 void print(double)
    print("Hello"); // 调用 void print(const std::string&)
    return 0;
    }
  2. 通过函数模板进行实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>

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

    int main() {
    print(42); // T 被推断为 int
    print(3.14); // T 被推断为 double
    print("Hello"); // T 被推断为 const char*
    return 0;
    }

C++中的运行时多态(Runtime Polymorphism)是通过继承和虚函数(virtual functions)实现的。这种多态性允许在运行时根据对象的实际类型调用适当的方法,而不是在编译时决定调用哪个函数。运行时多态的核心是使用基类指针或引用来操作派生类对象。

运行时多态主要依赖以下的几个概念:

  • 继承(Inheritance):允许一个类继承另一个类的属性和方法。
  • 虚函数(Virtual Functions):在基类中声明为virtual的函数,可以在派生类中被重写。
  • 虚函数表(Virtual Table, vtable):编译器为包含虚函数的类生成的一个表,表中存储了类的虚函数指针。每个对象包含一个指向其类的虚函数表的指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

// 基类
class Shape {
public:
// 纯虚函数,定义接口
virtual void draw() const = 0; // = 0 表示纯虚函数,必须在派生类中实现
virtual ~Shape() {} // 虚析构函数,以确保派生类的析构函数被调用
};

// 派生类:Circle
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};

// 派生类:Rectangle
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Rectangle" << std::endl;
}
};

int main() {
// 创建基类指针数组,指向派生类对象
Shape* shapes[2];
shapes[0] = new Circle();
shapes[1] = new Rectangle();

// 遍历数组并调用虚函数
for (int i = 0; i < 2; ++i) {
shapes[i]->draw(); // 根据对象的实际类型调用相应的draw方法
}

// 释放内存
for (int i = 0; i < 2; ++i) {
delete shapes[i];
}

return 0;
}

4. 函数模板

函数模板有两种定义方式,第一种使用关键字templatetypename,例如

1
2
template<typename T>
void func (T & a); // 随便定义一个函数

第二种是使用关键字templateclass,例如

1
2
template<class T>
void func (T & a); // 随便定义一个函数

其中,template<typename T> 是模板头,表示这个函数是一个模板函数,T是一个类型参数,可以是任意合法的类型。T可以用在函数的返回类型、参数列表和函数体内。

  1. 模板类型可以有多种形式,不仅限于一个,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typename T1, typename T2>
    void print(T1 a, T2 b) {
    std::cout << a << " " << b << std::endl;
    }

    int main() {
    print(10, " apples"); // T1 被推断为 int,T2 被推断为 const char*
    print(3.14, 42); // T1 被推断为 double,T2 被推断为 int
    return 0;
    }
  2. 除了类型参数,模板还可以具有非类型参数,例如:

1
2
3
4
5
6
7
8
9
10
template<typename T, int N>
T getArrayElement(T (&arr)[N], int index) {
return arr[index];
}

int main() {
int arr[5] = {1, 2, 3, 4, 5};
std::cout << getArrayElement(arr, 2) << std::endl; // 输出 3
return 0;
}

在这个例子中,N是一个非类型模板参数,它表示数组的大小。

  1. 模板参数也可以有默认值,例如
1
2
3
4
5
6
7
8
9
10
template<typename T = int>
T multiply(T a, T b) {
return a * b;
}

int main() {
std::cout << multiply(3, 4) << std::endl; // 使用默认类型 int
std::cout << multiply(3.14, 2.0) << std::endl; // 显式指定类型为 double
return 0;
}
  1. 模板还允许提供具体化版本,即对特定的类型提供不同的版本,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>

// 模板函数定义
template<typename T>
T add(T a, T b) {
return a + b;
}

// 对 std::string 的完全特化
template<>
std::string add<std::string>(std::string a, std::string b) {
return a + " specialized " + b;
}

int main() {
std::cout << add(3, 4) << std::endl; // 输出 7
std::cout << add(3.5, 2.1) << std::endl; // 输出 5.6
std::cout << add<std::string>("Hello, ", "World!") << std::endl; // 输出 Hello, specialized World!
return 0;
}

函数模板具有显式具体化机制,显式具体化就是为特定的类型提供函数模板的特化版本,这里针对std::string类型提供了一个不同版本的add函数。

Reference

《C++ Prime Plus》