函数声明与定义
定义与作用
声明告诉编译器"这个名字存在",定义告诉编译器"这个东西是什么":
int add(int a, int b); // 声明:只有签名,没有函数体
int add(int a, int b) { // 定义:签名 + 函数体
return a + b;
}
函数是 C++ 程序的基本组织单元。理解声明与定义的分离、参数传递机制和函数指针是设计正确接口的基础。
核心原理
声明与定义的关系
参数传递方式
完整示例
示例一:参数传递六种方式的性能对比
场景说明:构造一个中大型数据结构,对比六种传递方式的性能差异和语义效果。
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
class Report {
public:
std::string title;
std::vector<int> data;
Report(std::string t, std::vector<int> d)
: title(std::move(t)), data(std::move(d)) {}
// 拷贝构造函数(展示拷贝开销)
Report(const Report& other)
: title(other.title), data(other.data) {
std::cout << " [拷贝了 Report: " << title << "]" << std::endl;
}
// 移动构造函数
Report(Report&& other) noexcept
: title(std::move(other.title)), data(std::move(other.data)) {
std::cout << " [移动了 Report: " << title << "]" << std::endl;
}
};
// 方式 1:传值(发生完整拷贝)
void process_by_value(Report r) {
std::cout << " 处理 Report: " << r.title
<< " (数据量: " << r.data.size() << ")" << std::endl;
}
// 方式 2:传引用(无拷贝,可修改)
void process_by_ref(Report& r) {
r.title += " [已处理]";
std::cout << " 修改 Report: " << r.title << std::endl;
}
// 方式 3:传 const 引用(无拷贝,不可修改)—— 推荐默认选择
void process_by_const_ref(const Report& r) {
std::cout << " 读取 Report: " << r.title << std::endl;
// r.title = "xxx"; // 编译错误
}
// 方式 4:传指针(可空)
void process_by_ptr(Report* r) {
if (r) {
std::cout << " 通过指针处理: " << r->title << std::endl;
} else {
std::cout << " 空指针,跳过" << std::endl;
}
}
// 方式 5:传右值引用(转移所有权)
void process_by_rvalue(Report&& r) {
Report local = std::move(r); // 窃取资源
std::cout << " 转移所有权: " << local.title << std::endl;
}
int main() {
std::cout << "===== 参数传递方式对比 =====" << std::endl;
Report rpt("季度报告", std::vector<int>(100000, 42));
std::cout << "\n1. 传值(会发生拷贝):" << std::endl;
process_by_value(rpt);
std::cout << "\n2. 传引用:" << std::endl;
process_by_ref(rpt);
std::cout << " 函数外标题变为: " << rpt.title << std::endl;
std::cout << "\n3. 传 const 引用:" << std::endl;
process_by_const_ref(rpt);
std::cout << "\n4. 传指针:" << std::endl;
process_by_ptr(&rpt);
process_by_ptr(nullptr);
std::cout << "\n5. 传右值引用(移动语义):" << std::endl;
process_by_rvalue(Report("临时报告", std::vector<int>(10, 1)));
return 0;
}
预期输出:
===== 参数传递方式对比 =====
1. 传值(会发生拷贝):
[拷贝了 Report: 季度报告]
处理 Report: 季度报告 (数据量: 100000)
2. 传引用:
修改 Report: 季度报告 [已处理]
函数外标题变为: 季度报告 [已处理]
3. 传 const 引用:
读取 Report: 季度报告 [已处理]
4. 传指针:
通过指针处理: 季度报告 [已处理]
空指针,跳过
5. 传右值引用(移动语义):
[移动了 Report: 临时报告]
转移所有权: 临时报告
逐段分析:
- 传值:对
rpt(含 10 万元素的 vector)产生完整深拷贝,开销巨大。验证了[拷贝了 Report]消息 - 传引用:零拷贝,但函数内部可修改原对象。
rpt.title被追加了[已处理] - 传 const 引用:零拷贝 + 防修改,是只读场景的最佳选择
- 传指针:结构体内存开销小,可表达"此参数可选"(nullptr)的语义
- 传右值引用:绑定到临时对象,可窃取其资源。
process_by_rvalue内部用std::move将资源转移到局部变量 - 默认选择:只读 →
const T&;输出参数(要修改) →T&;可选参数 →T*;转移所有权 →T&&
示例二:函数指针与回调机制
场景说明:实现一个成绩排序框架,通过函数指针注入比较策略。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
struct Student {
std::string name;
int id;
double gpa;
};
// 比较函数类型
using Comparator = bool (*)(const Student&, const Student&);
bool compare_by_id(const Student& a, const Student& b) {
return a.id < b.id;
}
bool compare_by_gpa(const Student& a, const Student& b) {
return a.gpa > b.gpa; // 降序
}
bool compare_by_name(const Student& a, const Student& b) {
return a.name < b.name;
}
// 通用的排序函数,通过函数指针注入策略
void sort_students(std::vector<Student>& students, Comparator cmp) {
std::sort(students.begin(), students.end(), cmp);
}
// 返回函数指针
Comparator get_comparator(const std::string& strategy) {
if (strategy == "id") return compare_by_id;
if (strategy == "gpa") return compare_by_gpa;
if (strategy == "name") return compare_by_name;
return nullptr; // 无效策略
}
int main() {
std::vector<Student> students = {
{"张三", 1003, 3.8},
{"李四", 1001, 3.5},
{"王五", 1002, 4.0}
};
// 直接传递函数指针
std::cout << "===== 按 ID 排序 =====" << std::endl;
sort_students(students, compare_by_id);
for (const auto& s : students) {
std::cout << s.id << " | " << s.name << " | GPA " << s.gpa << std::endl;
}
std::cout << "\n===== 按 GPA 排序 =====" << std::endl;
sort_students(students, compare_by_gpa);
for (const auto& s : students) {
std::cout << s.id << " | " << s.name << " | GPA " << s.gpa << std::endl;
}
// 通过函数返回函数指针
std::cout << "\n===== 策略选择 =====" << std::endl;
auto cmp = get_comparator("name");
if (cmp) {
sort_students(students, cmp);
for (const auto& s : students) {
std::cout << s.name << std::endl;
}
}
return 0;
}
预期输出:
===== 按 ID 排序 =====
1001 | 李四 | GPA 3.5
1002 | 王五 | GPA 4
1003 | 张三 | GPA 3.8
===== 按 GPA 排序 =====
1002 | 王五 | GPA 4
1003 | 张三 | GPA 3.8
1001 | 李四 | GPA 3.5
===== 策略选择 =====
李四
王五
张三
逐段分析:
using Comparator = bool (*)(const Student&, const Student&);定义函数指针类型别名sort_students通过Comparator参数接收策略,实现排序逻辑与比较逻辑的解耦get_comparator返回函数指针——是策略模式的函数式实现- 函数指针是轻量级回调,但不如
std::function灵活(后者可以包装 lambda 和有状态的函数对象)
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 只声明不定义就被调用 | 链接错误 undefined reference | 确保链接了所有目标文件 |
| 函数声明和定义的签名不一致 | 链接错误或意外重载 | 头文件中的声明与源文件定义逐字一致 |
| 返回局部变量的引用或指针 | 悬挂引用,UB | 返回值类型 |
| 大对象按值传递 | 不必要的深拷贝 | 使用 const T& |
| 函数指针为空时调用 | 段错误 | 调用前检查 if (fp) |
常见面试问题
声明和定义的区别?为什么需要头文件?——声明引入名称和类型;定义提供完整实现。头文件用于在多翻译单元间共享声明,避免重复定义。
f(T)、f(T&)、f(const T&)、f(T&&)各自的用途是什么?——传值用于小对象;T&用于输出参数;const T&是只读默认选择;T&&用于移动语义(窃取资源)。如何声明一个函数指针?
void (*fp)(int)各部分含义?——fp是指向"接收 int、返回 void"函数的指针。括号围绕*fp表示指针,否则会被解析为返回void*的函数声明。函数重载决议的基本规则?——建立候选集 → 筛选可行函数 → 选择最佳匹配。精确匹配优于提升优于标准转换。
std::function和函数指针的区别?——函数指针只能指向普通函数和静态成员函数;std::function是类型擦除的通用可调用对象包装器,可包装 lambda、bind 结果、函数对象等。
小结
- 声明放头文件,定义放源文件——C++ 项目组织的基本模式
- 只读参数默认用
const T&,输出参数用T&,可选参数用T* - 函数指针实现策略注入,适合编译期确定的回调;运行期变体多用
std::function - 永远不要返回局部对象的引用或指针