第八章:函数探幽 | C++ Primer Plus 重点带看
内联函数
内联函数是编译器会将函数代码直接嵌入到调用处,避免函数调用的开销。普通函数需要压栈、跳转、返回等过程,内联函数直接把代码"复制粘贴"到调用处。
inline int add(int a, int b) {
return a + b;
}
int main() {
int x = add(1, 2); // 编译后等价于 int x = 1 + 2; 函数体直接展开到调用处
}
代价:
- 代码膨胀,每次调用都会复制一份代码。
注意事项:
- inline 关键字只是给编译器的建议,编译器可以选择忽略。
- 内联函数必须定义在头文件。因为内联函数需要在编译期展开,编译器必须能看到完整定义。如果定义在 .cpp 中,其他文件 #include 头文件时看不到定义,无法内联。
- 类的成员函数如果在类内定义是自动内联的,不会造成重复定义的错误。(普通/静态成员函数都是自动内联的)
- 静态成员变量以前不能在类内定义,C++17 起可以用 inline 在类内定义。
class MyClass {
public:
static inline int count = 0; // ✅ C++17,类内定义
static inline std::string name; // ✅ C++17
};
// 不需要类外定义了!inline 关键字告诉编译器:这个定义可以出现在多个源文件中,链接器会合并它们,不会报重复定义。
左值与右值
左值 = 可以取地址的表达式,代表一个持久的内存位置。
右值 = 不能取地址的表达式,代表临时值,用完即销毁。
左值引用
左值引用 = 左值的别名,不分配新内存,只是给已有变量起另一个名字。
注意事项:
- 必须初始化,引用必须绑定到一个已存在的左值,不能"空着"。
- 一旦绑定不能改变,引用不是指针,没有"指向"的概念,绑定后就是别名。
- 不能绑定右值。(但 const 左值引用可以绑定到右值,是一个特例)
引用与指针的区别
引用必须在声明引用变量的时候就完成初始化,不能通过赋值设置。int &a =b;相当于 int * const a = &b;
函数参数传递方式:按引用传递与按值传递
1、c 语言只有按值传递
- 传递基本类型时,形参是实参值的副本;
- 传递指针时,本质仍是按值传递——形参是指针变量的副本,只是这个副本存储的值是一个地址。
2、C++引用传参的绑定规则
- 非 const 左值引用(T&)只能绑定左值,适用于需要修改实参的场景;
- const 左值引用(const T&)可以绑定左值和右值,常用于只读访问并避免拷贝开销;
- 右值引用(T&&,C++11 引入)只能绑定右值,用于实现移动语义和资源转移。
void byValue(int x) { x = 10; } // 修改的是副本
void byPointer(int* p) { *p = 10; } // p 是指针的副本,但指向同一对象
void byRef(int& r) { r = 10; } // r 是实参的引用(别名)
int a = 5;
byValue(a); // a 仍为 5
byPointer(&a); // a 变为 10
byRef(a); // a 变为 10
返回引用
1、绝不能返回局部变量或临时对象的引用
int& wrong() {
int x = 10;
return x; // 错误:返回局部变量的引用
} // x 已销毁,返回的引用悬空
int& also_wrong() {
return 10; // 错误:返回临时对象的引用
}
2、只能返回生命周期超出函数作用域的对象的引用
3、返回引用避免拷贝,效率更高,常用于运算符重载和链式调用
// 1. 返回全局变量
int global = 10;
int& getGlobal() { return global; }
// 2. 返回静态变量
int& getStatic() {
static int s = 10;
return s;
}
// 3. 返回引用参数(最常见)
int& getElement(int arr[], int index) {
return arr[index]; // 实参数组生命周期在外部
}
// 4. 返回成员变量
class Person {
string name;
public:
string& getName() { return name; } // 成员生命周期随对象
};
// 5. 链式调用
class StringBuilder {
StringBuilder& append(const string& s) {
// ...
return *this; // 返回自身引用
}
};
函数的默认参数
1、默认参数只能在函数声明或定义的其中一处指定,不能同时指定。原因:编译器通过函数原型确定调用时需要传递的参数数目,定义时编译器已经知道要生成什么代码了。
// 正确:在声明中指定
void func(int a, int b = 10); // 函数原型
void func(int a, int b) { // 定义中不再指定
// ...
}
// 正确:没有声明时,在定义中指定
static void localFunc(int a, int b = 10) {
// ...
}
// 错误:重复指定
void func(int a, int b = 10);
void func(int a, int b = 10) { } // 错误!重复定义默认参数
2、必须从右向左指定
void func(int a, int b = 10, int c = 20); // 正确 // void func(int a = 1, int b, int c = 20); // 错误!中间不能断开
3、实参不能跳过中间的默认参数
void func(int a, int b = 10, int c = 20); func(1); // 正确:a=1, b=10, c=20 func(1, 2); // 正确:a=1, b=2, c=20 func(1, 2, 3); // 正确:a=1, b=2, c=3 // func(1, , 3); // 错误!不能跳过 b
函数重载(静态多态)
函数重载要求签名不同(参数个数、类型、顺序),返回类型不参与签名。编译器通过名称修饰为每个重载函数生成唯一内部名称,签名相同则修饰后冲突。注意 const 成员函数、引用限定符可构成重载,但数组和指针参数等价。
#include <iostream>
#include <string>
using namespace std;
class Data {
private:
string value;
public:
Data(const string& s) : value(s) {}
// 1. const 成员函数重载
string& get() {
cout << "非const版本: ";
return value; // 可修改
}
const string& get() const {
cout << "const版本: ";
return value; // 只读
}
// 2. 引用限定符重载(C++11)
void process() & {
cout << "左值对象调用: " << value << endl;
}
void process() && {
cout << "右值对象调用: " << value << endl;
// 可以安全地移动资源
}
};
int main() {
// const 成员函数重载
Data d1("hello");
const Data d2("world");
d1.get() = "modified"; // 非const对象 → 调用非const版本
cout << d1.get() << endl;
// d2.get() = "error"; // 错误:const对象返回const引用
cout << d2.get() << endl; // const对象 → 调用const版本
cout << "\n--- 引用限定符 ---\n";
// 引用限定符重载
Data d3("left");
d3.process(); // 左值对象 → 调用 & 版本
Data("right").process(); // 右值对象 → 调用 && 版本
std::move(d3).process(); // 转为右值 → 调用 && 版本
}
const 参数与重载
1、❌ 顶层 const 不能重载。值传递和指针本身的 const 是顶层 const,会被忽略。值传递时形参是副本,const 只影响函数内部,调用者无法区分。
void f(int a); void f(const int a); // 错误!重复定义,顶层const被忽略 void g(int* p); void g(int* const p); // 错误!重复定义,const修饰指针本身 // 原因:任何 int 值都能赋给 int 或 const int 变量,没有限制。 // int a = 5; // const int b = 10; // int x = a; // ✓ 正确 // int y = b; // ✓ 正确,const 被忽略 // const int z = a; // ✓ 正确 // const int w = b; // ✓ 正确
2、✓ 底层 const 可以重载。指针指向的对象或引用绑定的对象的 const 是底层 const。根本原因是:底层 const 决定了指针/引用能否修改实参内存,因此不同底层const的指针和引用能接受/绑定的实参范围不同,可构成重载。
void f(int* p); // 指向非const对象 void f(const int* p); // 指向const对象,可以重载 void g(int& r); // 绑定非const对象 void g(const int& r); // 绑定const对象,可以重载 // 原因:int& 和 const int& 能绑定的范围不同,它们是不同的类型。 // int a = 5; // const int b = 10; // int& r1 = a; // ✓ 正确 // int& r2 = b; // ✗ 错误!const int 不能绑定到 int& // const int& r3 = a; // ✓ 正确 // const int& r4 = b; // ✓ 正确
模板函数、模板函数的显示具体化、模板函数的显示实例化
优先级——非模板版本>显示具体化>显示实例化>模板函数(隐式实例化)。注:模板只是提供方案,并非真正的定义函数,定义要等到编译到具体的调用时才生成。
// 模板定义:提供通用方案
template<typename T>
void func(T a) { cout << "模板版本: " << a << endl; }
// 显式具体化:为特定类型提供专门实现
template<>
void func<int>(int a) { cout << "int具体化: " << a << endl; }
// 显式实例化:强制编译器生成特定类型的实例
template void func<double>(double a); // 声明式, 注意:显式实例化只是告诉编译器"请用模板生成这个类型的函数",不能单独给实现。
// 非模板版本:普通函数
void func(int a) { cout << "非模板版本: " << a << endl; }
func(10); // 调用非模板版本(最高优先级)
func(3.14); // 调用隐式实例化的模板版本
func<>(10); // 强制使用模板 → 调用int具体化
func<int>(10); // 显式指定模板参数 → 调用int具体化
返回类型后置
主要用于解决模板中返回类型依赖参数类型的情况。
template<typename T>
auto add(T a, T b) -> decltype(a + b) {
return a + b;
}
// 例如:a和b都是int,a+b是int,返回int
// 例如:a和b都是double,a+b是double,返回double
为什么需要后置?
// 错误:a 和 b 还未声明,编译器不知道它们是什么
template<typename T>
decltype(a + b) add(T a, T b) { return a + b; }
// 正确:后置返回类型,此时 a 和 b 已声明
template<typename T>
auto add(T a, T b) -> decltype(a + b) { return a + b; }
C++14 简化
// C++14起,可直接用auto推导返回类型
template<typename T>
auto add(T a, T b) {
return a + b; // 根据return语句自动推导
}
C++ Primer Plus 精读|从入门到面试,重点内容全程带看。 本专栏以《C++ Primer Plus》为蓝本,逐章提炼必考知识点、易错点、面试高频考点,跳过冗余示例,直击语法本质与工程实践,帮你高效吃透 C++ 基础,夯实底层开发必备能力。
