Sun,Chenye头像
C++零基础入门第4周:函数完全指南——参数传递、默认参数、函数重载与 constexpr 编译期计算(附20段可运行代码+避坑清单)封面图

C++零基础入门第4周:函数完全指南——参数传递、默认参数、函数重载与 constexpr 编译期计算(附20段可运行代码+避坑清单)

📚 本文是「C++ 全栈开发 30 周课程」阶段一第 4 周的详细讲义,也是阶段一的收官周。本周目标:掌握函数的声明/定义/调用、四种参数传递方式、默认参数、函数重载与编译期计算,并用函数模块化完成阶段一项目「学生成绩管理系统」。


📋 本周学习地图

第4周 · 函数

1. 函数基础:声明/定义/调用

2. 参数传递:值/引用/const引用/指针

3. 返回值与返回引用

4. 默认参数

5. 函数重载

6. 内联函数 inline

7. 编译期函数:constexpr / consteval

避坑:声明与定义不一致

避坑:大对象按值传参 / 悬空引用

避坑:返回局部变量引用

避坑:默认参数重复指定 / 必须靠右

避坑:重载歧义 / 仅返回值不同

避坑:inline 不等于一定内联

避坑:constexpr 函数里有运行期操作

阶段一项目:学生成绩管理系统


1️⃣ 函数基础:声明、定义、调用

1.1 三段式结构

#include <iostream>

// ① 函数声明(原型):告诉编译器"有这么个函数"
int add(int a, int b);

int main() {
    std::cout << add(3, 5) << "\n";   // ③ 调用
    return 0;
}

// ② 函数定义:给出具体实现
int add(int a, int b) {
    return a + b;
}

函数声明
int add(int, int);

编译器知道签名
可以编译调用处

函数定义
int add(int a,int b){...}

链接器把调用
和实现连起来

💡 为什么需要声明:C++ 是单遍编译,调用处必须先看到声明。多文件项目里声明放 .h,定义放 .cpp。

1.2 声明与定义必须一致

// ❌ 声明和定义参数类型不一致 → 编译/链接错误
int add(int a, int b);
double add(int a, int b) { return a + b; }   // error: 返回类型不匹配

1.3 函数名、参数、返回值三要素

要素说明示例
返回类型可以是 void、任意类型、引用、指针int、void、int&
函数名标识符,遵循命名规范add、getScore
参数列表类型 + 名字,可为空 () 或 (void)(int a, int b)

2️⃣ 参数传递:四种方式,选错就是坑

2.1 四种传递方式总览

方式写法是否拷贝能否修改实参典型场景
值传递void f(int x)✅ 拷贝❌小对象、不需要改原值
引用传递void f(int& x)❌ 零拷贝✅需要修改实参
const 引用void f(const int& x)❌ 零拷贝❌大对象只读(最常用)
指针传递void f(int* x)拷贝指针✅(解引用后)C 风格 API、可空参数

2.2 值传递:改的是副本

#include <iostream>

void byValue(int x) {
    x = 100;              // 改的是副本
}

int main() {
    int a = 10;
    byValue(a);
    std::cout << a << "\n";   // 10(没变)
}

2.3 引用传递:改的是原对象

void byRef(int& x) {
    x = 100;              // 改的是原对象
}

int main() {
    int a = 10;
    byRef(a);
    std::cout << a << "\n";   // 100(变了)
}

2.4 const 引用:零拷贝 + 只读(大对象首选)

#include <string>
#include <vector>

// ❌ 按值传大对象:每次都拷贝整个 string/vector
void printBad(std::string s) {
    std::cout << s << "\n";
}

// ✅ const 引用:零拷贝,且保证函数内不修改
void printGood(const std::string& s) {
    std::cout << s << "\n";
}

int main() {
    std::string big = "很长的字符串...";
    printBad(big);    // 拷贝一份
    printGood(big);   // 不拷贝
}

是

否

大

小(int/double/指针)

选择参数传递方式

需要修改实参?

引用传递 T&

对象大吗?
string/vector/自定义类

const T& ✅ 零拷贝只读

按值 T 即可

💡 口诀:要改就 T&,只读大对象 const T&,小对象直接按值。

2.5 指针传递:可空 + 可改

void byPointer(int* x) {
    if (x != nullptr) {
        *x = 100;         // 解引用后修改
    }
}

int main() {
    int a = 10;
    byPointer(&a);        // 传地址
    byPointer(nullptr);   // 合法,函数内判空
}

2.6 悬空引用陷阱

// ❌ 返回局部变量的引用 → 悬空引用,UB
int& badRef() {
    int local = 10;
    return local;         // local 在函数结束就销毁了
}

// ✅ 返回值,或返回静态/成员变量
int goodValue() {
    int local = 10;
    return local;         // 返回值拷贝,安全
}

3️⃣ 返回值与返回引用

3.1 返回值 vs 返回引用

#include <vector>

std::vector<int> g_data = {1, 2, 3};

// 返回值:调用者拿到副本
std::vector<int> getCopy() {
    return g_data;
}

// 返回引用:调用者拿到原对象的别名(零拷贝)
std::vector<int>& getRef() {
    return g_data;
}

int main() {
    auto copy = getCopy();    // 拷贝
    auto& ref = getRef();     // 零拷贝,改 ref 就是改 g_data
}

3.2 返回引用的三条铁律

规则说明
✅ 可以返回全局变量、静态变量、成员变量、参数引用
❌ 不能返回局部变量的引用/指针
⚠️ 慎用返回 const T& 时注意生命周期
// ❌ 局部变量
int& f1() { int x; return x; }

// ✅ 静态变量
int& f2() { static int x; return x; }

// ✅ 成员变量
class A { int x; public: int& get() { return x; } };

4️⃣ 默认参数

4.1 基本用法

#include <string>
#include <iostream>

void greet(const std::string& name,
           const std::string& prefix = "你好") {
    std::cout << prefix << ", " << name << "!\n";
}

int main() {
    greet("Alice");              // 你好, Alice!
    greet("Bob", "Hello");       // Hello, Bob!
}

4.2 两条硬规则

规则 1:默认参数必须从右往左连续指定

// ✅ 合法
void f(int a, int b = 2, int c = 3);

// ❌ 非法:b 没默认值,c 有
void g(int a, int b = 2, int c);

// ✅ 合法:跳过的必须靠右
void h(int a, int b, int c = 3);

规则 2:默认参数只能在声明或定义中出现一次

// ❌ 重复指定 → 编译错误
void f(int x = 1);
void f(int x = 1) { }

// ✅ 声明给默认值,定义不给
void f(int x = 1);
void f(int x) { }

4.3 默认参数 vs 重载:怎么选

场景推荐
只是"可选参数",逻辑相同默认参数
不同参数类型/数量,逻辑不同函数重载
默认参数会导致重载歧义改用重载
// ⚠️ 歧义示例
void f(int a);
void f(int a, int b = 0);

f(1);   // ❌ 两个都能匹配,编译错误

5️⃣ 函数重载

5.1 基本用法:同名不同参

#include <iostream>
#include <string>

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

std::string add(const std::string& a, const std::string& b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2) << "\n";              // 3(int 版本)
    std::cout << add(1.5, 2.5) << "\n";          // 4(double 版本)
    std::cout << add(std::string("A"), std::string("B")) << "\n";  // AB
}

int, int

double, double

string, string

调用 add(...)

实参类型

int add(int,int)

double add(double,double)

string add(string,string)

5.2 重载的判定依据

能构成重载不能构成重载
✅ 参数类型不同❌ 仅返回值不同
✅ 参数个数不同❌ 仅参数名字不同
✅ 参数顺序不同❌ 仅默认参数不同
// ❌ 仅返回值不同 → 编译错误
int f(int x);
double f(int x);

// ❌ 仅参数名不同 → 重复定义
int f(int a);
int f(int b);

5.3 重载解析:最佳匹配

void f(int x)    { std::cout << "int\n"; }
void f(double x) { std::cout << "double\n"; }

int main() {
    f(1);       // int(精确匹配)
    f(1.5);     // double(精确匹配)
    f(1.0f);    // ⚠️ float → 可能选 double(提升)也可能歧义
    f('a');     // ⚠️ char → int(整型提升)
}

💡 原则:重载要语义一致。add(int) 和 add(string) 是合理的;print(int) 和 deleteFile(string) 同名就是灾难。

5.4 重载 + 默认参数的歧义

void f(int a);
void f(int a, int b = 0);

f(1);   // ❌ 两个都匹配 → 歧义
f(1, 2); // ✅ 只有第二个匹配

6️⃣ 内联函数 inline

6.1 作用与写法

// 建议编译器在调用处展开函数体,减少函数调用开销
inline int square(int x) {
    return x * x;
}

int main() {
    int a = square(5);   // 可能被展开成 int a = 5 * 5;
}

6.2 三个关键认知

认知说明
inline 是建议不是命令编译器可以拒绝(函数太大、递归等)
主要用途头文件中定义函数,避免多重定义链接错误
现代编译器小函数即使不写 inline 也会自动内联(-O2)
// 头文件 utils.h
inline int max(int a, int b) {   // ✅ 多个 .cpp 包含也不会重复定义
    return a > b ? a : b;
}

💡 工程建议:小函数放头文件时加 inline;性能优化交给编译器 -O2,不要为了内联牺牲可读性。


7️⃣ 编译期函数:constexpr / consteval

7.1 constexpr(C++11):能编译期算就编译期算

#include <iostream>

// 编译期可求值的函数
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int f5 = factorial(5);   // ✅ 编译期算出 120
    int runtime = 5;
    int f_runtime = factorial(runtime); // ✅ 运行期也能调用
    std::cout << f5 << "\n";            // 120
}

是

否

constexpr 函数

实参是编译期常量?

编译期求值
结果进常量表

运行期求值
像普通函数一样调用

✅ 可用于数组大小、模板参数

✅ 灵活性保留

7.2 constexpr 的限制(C++11 → C++14 → C++20 逐步放宽)

标准函数体允许内容
C++11只能一条 return(或三目)
C++14允许局部变量、循环、多个 return
C++20允许 try、constexpr 虚函数、动态分配(有约束)
// C++14 起可以写循环
constexpr int sumTo(int n) {
    int total = 0;
    for (int i = 1; i <= n; ++i) {
        total += i;
    }
    return total;
}

constexpr int s = sumTo(100);   // 5050,编译期算出

7.3 consteval(C++20):强制编译期

// 必须编译期求值,运行期调用直接报错
consteval int mustBeCompileTime(int n) {
    return n * n;
}

int main() {
    constexpr int a = mustBeCompileTime(5);   // ✅ 25
    int x = 5;
    // int b = mustBeCompileTime(x);          // ❌ 编译错误:x 不是常量
}

7.4 三者对比

关键字标准语义运行期能否调用
constexprC++11能编译期算就算✅
constevalC++20必须编译期算❌
constinitC++20变量必须编译期初始化—(用于变量)

💡 选择:普通工具函数写 constexpr;明确"只允许编译期"的接口写 consteval。


8️⃣ 本周高频面试题(5 道)

#题目考察点参考答案要点
1值传递、引用传递、const 引用传递的区别?参数传递值拷贝、引用零拷贝可改、const 引用零拷贝只读;大对象只读用 const 引用
2函数重载的判定依据是什么?重载规则参数类型/个数/顺序不同;仅返回值不同不能重载
3默认参数有什么限制?默认参数必须从右往左连续;声明和定义只能出现一次
4constexpr 和 consteval 的区别?编译期计算constexpr 能算就算,运行期也可调用;consteval 必须编译期,运行期调用报错
5为什么不能返回局部变量的引用?生命周期函数结束局部变量销毁,引用悬空,访问是 UB

9️⃣ 阶段一项目验收:学生成绩管理系统(控制台版)

这是阶段一的收官项目,要求用本周学的函数知识把代码模块化,不再把所有逻辑堆在 main 里。

9.1 需求清单

功能要求用到的本周知识
添加学生输入姓名、成绩值/引用传参
删除学生按姓名删除const string& 传参
修改成绩按姓名改分引用传参修改
查询学生按姓名/学号查返回引用或副本
排序统计按成绩排序、算平均分函数返回 double
文件读写保存/加载函数返回 bool 表示成功

9.2 模块化设计(函数划分)

main()
菜单循环

addStudent()

removeStudent()

modifyScore()

queryStudent()

sortByScore()

calcAverage()

saveToFile()

loadFromFile()

Student 结构体
vector 容器

9.3 参考代码(关键函数签名)

#include <string>
#include <vector>
#include <iostream>
#include <fstream>
#include <algorithm>

struct Student {
    std::string name;
    double score;
};

// ✅ 大对象用 const 引用,零拷贝
void addStudent(std::vector<Student>& students,
                const std::string& name,
                double score);

// ✅ 需要修改容器,用非 const 引用
bool removeStudent(std::vector<Student>& students,
                   const std::string& name);

// ✅ 只读查询,返回 const 引用避免拷贝
const Student* findStudent(const std::vector<Student>& students,
                           const std::string& name);

// ✅ 返回统计值
double calcAverage(const std::vector<Student>& students);

// ✅ 返回 bool 表示成败
bool saveToFile(const std::vector<Student>& students,
                const std::string& filename);
bool loadFromFile(std::vector<Student>& students,
                  const std::string& filename);

// ✅ 编译期常量:最大学生数
constexpr int MAX_STUDENTS = 1000;

int main() {
    std::vector<Student> students;
    students.reserve(MAX_STUDENTS);

    // 菜单循环:do-while + switch(第3周知识复用)
    char choice;
    do {
        std::cout << "\n1.添加 2.删除 3.修改 4.查询 5.排序 6.统计 7.保存 8.加载 0.退出\n";
        std::cin >> choice;
        switch (choice) {
            case '1': { /* 调用 addStudent */ break; }
            case '2': { /* 调用 removeStudent */ break; }
            // ...
            case '0': break;
            default:  std::cout << "无效选项\n";
        }
    } while (choice != '0');

    return 0;
}

9.4 验收标准

  • main 函数不超过 50 行,只负责菜单调度
  • 每个功能独立成函数,函数名见名知义
  • 大对象参数一律用 const T&,需要修改用 T&
  • 查询函数返回 const Student*(找不到返回 nullptr)
  • 文件读写函数返回 bool,main 里检查返回值
  • 用 constexpr 定义最大容量等常量
  • 编译加 -Wall -Wextra 无警告
  • 所有 if/else、for 都带 {}

9.5 阶段一通关检查

阶段一通关

第1周:环境 + 编译调试

第2周:类型 + 运算符 + 转换

第3周:分支 + 循环 + 跳转

第4周:函数 + 模块化

学生成绩管理系统

✅ 进入阶段二:指针与内存管理


🔟 本周避坑速查表(贴墙版)

第4周避坑清单

参数传递

返回值

默认参数

重载

编译期函数

大对象 const T&
要改 T&
不返回局部引用

不返回局部变量引用
返回引用注意生命周期

默认参数靠右连续
声明定义只写一次

仅返回值不同不能重载
注意默认参数歧义

constexpr 能算就算
consteval 必须编译期


🎯 一句话总结:参数传递看"改不改 + 大不大",返回值警惕悬空引用,默认参数靠右写一次,重载只看参数不看返回值,constexpr 能算就算、consteval 必须算。 掌握这五条,加上把学生成绩管理系统写完,阶段一就正式通关,可以进入阶段二「指针与内存管理」了。


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_58662768/article/details/169951937

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--