【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板
1. 内联函数
C++提供了一种内联函数,在 C++ 中,内联函数(inline function)是一种特殊的函数,其定义使用 inline
关键字来提示编译器将函数调用直接替换为函数体,以减少函数调用的开销。内联函数通常用于简短、频繁调用的函数。
要使用内联函数,必须:
- 在函数声明前加上关键字
inline
- 在函数定义前加上关键字
inline
注意内联函数不能递归。内联函数的基本语法:
inline return_type function_name(parameters){
// 函数体
}
举个例子:
#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;
}
输出结果
Result: 7
inline
工具是C++新增的特性,C语言使用预处理器语句#define
来提供宏——内联代码的原始实现。例如:
#define SQUARE(X) ((X)*(X))
inline
和#define
的主要区别是:
- 实现机制:
- 内联函数是由编译器在编译时展开的,是一种编译时的语言特性。编译器会在调用内联函数的地方直接替换函数体。
- 而
#define
宏是在预处理阶段展开的,是一种文本替换的机制。预处理器会在编译前把宏展开。
- 类型检查:
- 内联函数有类型检查,编译器会检查参数类型是否匹配。
#define
宏是简单的文本替换,没有类型检查,容易出现类型错误。
- 时间和空间开销:
- 内联函数在编译时展开,没有函数调用的开销,但会增加程序的大小,增加空间成本。
#define
宏在预处理阶段展开,没有函数调用开销,但可能会导致代码膨胀。
2. 引用变量
C++新增了一种复合类型——引用变量,使用运算符&
,引用变量的主要用途是作为函数参数列表中的形参。例如,创建一个引用变量
int rats;
int & rodents = rats;
注意:引用必须在声明时进行初始化,而不能像指针一样,先声明再赋值。引用一旦与变量进行关联,就一直指向这个变量。
C++11中新增了另一种引用——右值引用(rvalue reference),这种引用可以指向右值,使用&&
进行声明。
在 C++ 中,“左值”(lvalue)和“右值”(rvalue)是用来描述表达式值类型的一对术语。理解它们的概念对于掌握 C++ 语言的赋值、引用、移动语义等方面的内容非常重要。
左值(lvalue,locatable value)是指能够定位的值,它表示存储在内存中的某个位置的对象。因此,左值是可以取地址的,可以出现在赋值操作的左侧。例如:
int x = 10; // x 是左值
int *p = &x; // 可以取地址
x = 20; // 可以出现在赋值操作的左侧
右值(rvalue,readable value)是指不具有持久存储位置的临时值,它通常是表达式求值的结果。右值不能取地址,也不能出现在赋值操作的左侧。例如:
int y = 10; // 10 是右值
int z = y + 5; // (y + 5) 是右值
int *p = &10; // 错误,不能对右值取地址
左值引用是对左值的引用,用于绑定左值。例如:
int a = 10;
int &ref = a; // 左值引用
ref = 20; // 可以通过引用修改 a 的值
右值引用是对右值的引用,用于绑定右值。这是 C++11 引入的特性,主要用于实现移动语义和完美转发,以提高性能。
int &&rref = 10; // 右值引用
rref = 20; // 可以通过引用修改右值
新增右值引用主要是用于移动语义和完美转发,理解左值和右值的区别是掌握 C++ 高级特性(如移动语义和完美转发)的基础。
引用常用于函数的参数传递,这样可以避免不必要的拷贝,并且允许函数修改传入的参数值。例如:
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
引用使函数能够正确生成并使用临时变量
举个例子:
#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;
}
输出如下:
Non-const reference: hello
After printString(): modified
Const reference: modified
After printConstString(): modified
什么时候使用引用和指针呢?
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
对于使用传递的值而不做修改的函数:
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为
const
3. 函数重载
C++实现多态有两种方式,
- 编译时多态Compile-time Polymorphism(通过函数重载和模板实现)
- 运行时多态Runtime Polymorphism(通过继承和虚函数实现)
函数重载的关键是函数的参数列表(也称为特征标),如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是不重要的。C++允许定义相同名称的函数前提是他们的特征标不同。
举个例子,编译时多态可以通过函数重载或者是通过模板进行实现:
- 通过函数重载进行实现
// 使用函数重载实现多态
#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;
}
- 通过函数模板进行实现
#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):编译器为包含虚函数的类生成的一个表,表中存储了类的虚函数指针。每个对象包含一个指向其类的虚函数表的指针。
#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. 函数模板
函数模板有两种定义方式,第一种使用关键字template
和typename
,例如
template<typename T>
void func (T & a); // 随便定义一个函数
第二种是使用关键字template
和class
,例如
template<class T>
void func (T & a); // 随便定义一个函数
其中,template<typename T>
是模板头,表示这个函数是一个模板函数,T
是一个类型参数,可以是任意合法的类型。T
可以用在函数的返回类型、参数列表和函数体内。
- 模板类型可以有多种形式,不仅限于一个,例如:
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;
}
- 除了类型参数,模板还可以具有非类型参数,例如:
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
是一个非类型模板参数,它表示数组的大小。
- 模板参数也可以有默认值,例如
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;
}
- 模板还允许提供具体化版本,即对特定的类型提供不同的版本,例如:
#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》