5. 函数:C++与QT中的核心抽象
函数这东西,说白了就是一段有名字的代码块。你给它输入,它给你输出。我刚开始学C++时,总觉得函数是个麻烦事——明明可以直接写代码,干嘛非要包一层?后来在QT项目里写了个两千行的main.cpp,改一个bug要找半小时...嗯,从那以后我再也不敢不拆函数了。
5.1 函数定义与调用
定义一个函数,你需要告诉编译器三件事:返回值类型、函数名、参数列表。看个最简单的例子:
// 函数定义
int add(int a, int b) {
return a + b;
}
// 函数调用
int result = add(3, 5); // result = 8
我个人习惯把函数声明放在头文件里,定义放在cpp文件里。这样别人看你的接口一目了然,不用翻实现代码。
5.2 参数传递:值、指针、引用
这是面试最爱问的问题,也是新手最容易踩坑的地方。三种传递方式,各有各的脾气。
| 传递方式 | 语法 | 特点 | 适用场景 |
|---|---|---|---|
| 值传递 | void func(int x) |
拷贝一份,原值不变 | 小对象、不需要修改原值 |
| 指针传递 | void func(int* x) |
传地址,可修改原值 | 需要修改、可能为空 |
| 引用传递 | void func(int& x) |
传别名,可修改原值 | 需要修改、保证不为空 |
我曾经在QT项目里犯过一个低级错误:用值传递传了一个QImage对象。你知道QImage有多大吗?一张1920x1080的图片,拷贝一次就是几MB。函数调用一次拷贝一次,循环里调了100次...界面直接卡死。后来改成const QImage&,问题瞬间解决。
QString、QList、QImage这些类,传参时一定要用const &,除非你明确需要修改副本。
5.3 函数重载
函数重载就是同一个函数名,参数不同。编译器根据你传的参数类型和个数,自动匹配对应的版本。
// 三个重载版本
void print(int value) {
qDebug() << "整数:" << value;
}
void print(double value) {
qDebug() << "浮点数:" << value;
}
void print(const QString& value) {
qDebug() << "字符串:" << value;
}
// 调用时自动匹配
print(42); // 调用第一个
print(3.14); // 调用第二个
print("hello"); // 调用第三个
你想想看,如果没有重载,你得写printInt、printDouble、printString三个名字,调用时还得记哪个是哪个。重载让代码更自然。
int getValue();
double getValue(); // 编译错误!
5.4 内联函数
内联函数用inline关键字修饰。它的作用是建议编译器:把函数调用处直接替换成函数体,省去函数调用的开销。
inline int max(int a, int b) {
return (a > b) ? a : b;
}
// 调用时,编译器可能直接替换成:
int result = (x > y) ? x : y;
说实话,现代编译器优化能力很强,很多时候你写不写inline区别不大。但有一种情况我建议你用:在头文件里定义的短小函数。比如QT的很多getter函数:
// QT源码中常见的写法
inline int width() const { return m_width; }
inline int height() const { return m_height; }
inline只是建议,不是命令。编译器觉得不合适,完全可以忽略你的inline。比如递归函数、循环体很大的函数,编译器通常不会内联。
5.5 默认参数
默认参数让你在定义函数时给参数一个默认值。调用时如果不传这个参数,就使用默认值。
void createWindow(int width = 800, int height = 600, const QString& title = "QT App") {
// 创建窗口...
}
// 多种调用方式
createWindow(); // 全部用默认值
createWindow(1024); // 只指定宽度
createWindow(1024, 768); // 指定宽高
createWindow(1024, 768, "我的应用"); // 全部指定
我个人习惯把默认参数写在声明里,定义里不写。这样别人看头文件就知道有哪些默认值:
// 头文件
void createWindow(int width = 800, int height = 600, const QString& title = "QT App");
// 实现文件
void createWindow(int width, int height, const QString& title) {
// 实现...
}
void func(int a = 1, int b); // 错误!
因为编译器不知道你调用func(2)时,2是给a还是给b。
5.6 函数模板
函数模板是C++泛型编程的基础。它让你写一次代码,支持多种类型。
template <typename T>
T maxValue(T a, T b) {
return (a > b) ? a : b;
}
// 使用
int m1 = maxValue(3, 5); // T被推导为int
double m2 = maxValue(3.14, 2.71); // T被推导为double
QString m3 = maxValue(QString("apple"), QString("banana")); // T被推导为QString
我在QT项目中经常用函数模板来写通用的数据处理函数。比如一个通用的序列化函数:
template <typename T>
QByteArray serialize(const T& data) {
QByteArray result;
QDataStream stream(&result, QIODevice::WriteOnly);
stream << data;
return result;
}
// 支持所有QT流操作符的类型
QByteArray bytes = serialize(42);
QByteArray strBytes = serialize(QString("hello"));
知识体系总览
下面这张图把本章的知识点串起来了。你可以看到函数从定义到调用的完整流程,以及各种特性的关系:
这张图把函数相关的知识点分成了三层。最上层是函数本身,中间是它的核心特性,底层是扩展特性。你看,参数传递又细分为三种方式,函数模板则衍生出泛型编程和类型推导。整个知识体系是层层递进的。
- 函数是代码复用的基本单元,一个函数只做一件事
- 参数传递选引用还是指针?能选引用就别选指针,除非可能为空
- 重载让接口更自然,但别滥用——参数个数和类型差异要明显
- 内联只适合短小函数,大函数内联反而可能降低性能
- 默认参数从右向左连续,这是语法规则
- 函数模板定义放头文件,这是实践准则
好了,函数这块的内容就这些。记住一点:写函数不是为了炫技,是为了让别人(包括未来的你)能看懂代码。我见过太多人把函数写得又长又臭,还美其名曰"封装"。嗯,封装不是把代码堆在一起,而是把逻辑拆得清晰。