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文件里。这样别人看你的接口一目了然,不用翻实现代码。

小技巧:QT项目中,如果你用Qt Creator,右键函数名选择"Refactor"可以快速提取函数。我写QT项目时经常用这个功能重构代码。

5.2 参数传递:值、指针、引用

这是面试最爱问的问题,也是新手最容易踩坑的地方。三种传递方式,各有各的脾气。

传递方式 语法 特点 适用场景
值传递 void func(int x) 拷贝一份,原值不变 小对象、不需要修改原值
指针传递 void func(int* x) 传地址,可修改原值 需要修改、可能为空
引用传递 void func(int& x) 传别名,可修改原值 需要修改、保证不为空

我曾经在QT项目里犯过一个低级错误:用值传递传了一个QImage对象。你知道QImage有多大吗?一张1920x1080的图片,拷贝一次就是几MB。函数调用一次拷贝一次,循环里调了100次...界面直接卡死。后来改成const QImage&,问题瞬间解决。

避坑指南:QT的隐式共享(Implicit Sharing)机制会让新手误以为拷贝不花钱。记住:QStringQListQImage这些类,传参时一定要用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");   // 调用第三个

你想想看,如果没有重载,你得写printIntprintDoubleprintString三个名字,调用时还得记哪个是哪个。重载让代码更自然。

注意:返回值类型不能作为重载的依据。编译器只看参数列表,不看返回值。所以下面这样是编译不过的:
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"));
经验之谈:函数模板的定义通常放在头文件里,因为编译器在实例化时需要看到完整的定义。这和普通函数不一样,普通函数声明和定义可以分开。

知识体系总览

下面这张图把本章的知识点串起来了。你可以看到函数从定义到调用的完整流程,以及各种特性的关系:

函数 定义与调用 参数传递 函数重载 内联函数 值传递 指针传递 引用传递 返回值类型 同名不同参 编译期展开 默认参数 函数模板 泛型编程 类型推导

这张图把函数相关的知识点分成了三层。最上层是函数本身,中间是它的核心特性,底层是扩展特性。你看,参数传递又细分为三种方式,函数模板则衍生出泛型编程和类型推导。整个知识体系是层层递进的。

核心要点:
  • 函数是代码复用的基本单元,一个函数只做一件事
  • 参数传递选引用还是指针?能选引用就别选指针,除非可能为空
  • 重载让接口更自然,但别滥用——参数个数和类型差异要明显
  • 内联只适合短小函数,大函数内联反而可能降低性能
  • 默认参数从右向左连续,这是语法规则
  • 函数模板定义放头文件,这是实践准则

好了,函数这块的内容就这些。记住一点:写函数不是为了炫技,是为了让别人(包括未来的你)能看懂代码。我见过太多人把函数写得又长又臭,还美其名曰"封装"。嗯,封装不是把代码堆在一起,而是把逻辑拆得清晰。