第一部分:函数 function

1. 什么是函数

PDF 中提到,函数的意义是让程序更模块化,把代码按业务、按逻辑拆成一个个单元,提高复用性。第 8、9 页的图也很直观:原来 main 里一大段顺序代码,可以拆成 preprocess、inference、postprocess 这样的多个函数模块。

课堂理解

函数本质上就是:

  • 给一段代码起名字

  • 需要时直接调用

  • 避免重复写同样的逻辑

你这节课最典型的对应代码

1.func_demo.cpp 就是非常标准的“函数拆分业务”的例子:
circle_area() 负责和用户交互,输入半径;
calculate_area(double radius) 负责真正计算圆面积;
main 里只需要调用 circle_area()

这段代码体现出的思想

不是把“输入、计算、输出”都塞进 main,而是拆成:

  • 输入/输出层:circle_area()

  • 计算层:calculate_area()

这就是模块化。


2. 函数的定义

PDF 讲得很清楚,一个函数由四部分构成:

  • 返回类型

  • 函数名

  • 参数列表

  • 函数体

标准格式

返回类型 函数名(参数列表)
{
函数体
}

结合代码理解

1.func_demo.cpp 里:

double calculate_area(double radius){
return pi * pow(radius, 2);
}

这说明:

  • 返回类型:double

  • 函数名:calculate_area

  • 参数:double radius

  • 函数体:return pi * pow(radius, 2);

而另一个函数:

void circle_area(){
...
}

返回类型是 void,表示不返回值。它主要负责执行动作。

课堂总结

所以判断一个函数最重要的两个问题是:

  1. 它需要输入什么参数?

  2. 它要不要返回结果?


3. 函数调用

PDF 第 15、16 页展示了函数调用:定义一个 say_hello(),然后在 main() 中调用它,甚至还能在循环里反复调用。

对应到你的代码

1.func_demo.cpp 中 main 只有一句核心调用:

circle_area();

这就说明:函数定义好了,不会自动执行,必须被调用才执行。

复习重点

  • 定义函数:告诉编译器“这个功能怎么做”

  • 调用函数:真正执行这个功能


4. 函数原型 prototype

PDF 说得很重要:编译器要求,在使用函数之前,必须先“见过”这个函数。
小程序可以“先定义后调用”;但大型程序通常用**函数原型(前向声明)**告诉编译器函数长什么样,原型常放在开头或头文件里。

对应代码

2.func_prototype.cpp

void circle_area();
double calculate_area(double);

这里就是函数原型。然后 main 里先调用:

circle_area();

后面才给出函数定义。

为什么要这样做

因为编译器在读 main 的时候,如果还不知道 circle_area() 是什么,就无法检查调用是否合法。

课堂总结

函数原型的作用就是:

提前告诉编译器:有这么个函数,它的名字、参数类型、返回值类型分别是什么。


5. 参数与值传递 pass by value

PDF 第 22 页强调:函数默认是值传递。也就是调用函数时,传进去的是一份“副本”,函数内部改的是副本,不会影响外面的原变量。并区分了:

  • 形参:函数定义中的参数

  • 实参:函数调用时传进去的值

对应代码

3.func_params.cpp 非常典型。

例 1:int
void change_num(int num)
{
num = 100;
}

main 中:


int my_num {10};
change_num(my_num);

调用后,my_num 还是原来的值,不会变成 100。因为传进去的是拷贝。

例 2:string
void change_string(string str)
{
str = "World";
}

同理,main 里的 my_string 不会被改掉。

例 3:vector
void change_vector(vector<string> vec){
vec.clear();
}

虽然 vec.clear() 把函数里的 vector 清空了,但 main 里的 my_vector 不受影响,因为 vec 仍然是副本。

课堂结论

值传递的本质是:

函数收到的是“复制品”,不是原件。

注意

对于 vector<string> 这种对象,值传递虽然安全,但可能有性能开销,因为会发生拷贝。PDF 后面才引出为什么要用引用传递。


6. 函数重载 overloading

PDF 第 25、26 页指出:一组函数可以同名,但参数列表不同,这就叫重载。

对应代码

4.func_overloading.cpp

void demo_print(int);
void demo_print(double);
void demo_print(string);
void demo_print(string, string);
void demo_print(vector<string>);

它们名字都叫 demo_print,但参数不同。编译器会根据调用时传入的参数决定到底调用哪个版本。

代码中的几个关键现象

demo_print(100);

匹配 void demo_print(int)

demo_print(123.456);

匹配 void demo_print(double)

demo_print('A');

代码注释写得很清楚:字符 'A' 会被转换为整数 ASCII 码,所以会匹配 int 版本。

demo_print("C style string");

C 风格字符串会转换为 string,所以匹配 string 版本。

课堂总结

重载的核心不是“名字不同”,而是:

同一个功能名字,对不同类型、不同数量的参数提供不同实现。

复习时要记住

重载靠的是参数列表不同,不是返回值不同。


7. 数组作为函数参数

PDF 第 28~32 页重点讲了数组传参:

  • 数组元素本身不会整体复制

  • 数组名代表首元素地址(无法知道数组的长度,以及何时结束)

  • 实际上传进去的是地址

  • 函数内部不知道数组大小,所以要额外传 size

  • 如果不想修改数组,可以加 const

对应代码

5.func_pass_array.cpp

函数声明
void print_arr(const int arr[], size_t size);
void change_arr(int arr[], size_t size);

这里已经体现了两个重点:

  1. 数组参数通常搭配 size

  2. 只读数组要加 const

main 中
int student_scores [] {100,99,98,97};
print_arr(student_scores, 4);
change_arr(student_scores, 4);
print_arr(student_scores, 4);
观察地址

代码还特意打印了 main 里数组地址,以及函数内数组地址。它们是同一个地址,这说明数组传给函数时,本质上传的是首地址。

change_arr
for (size_t i {0}; i < size; i++){
arr[i] = 60;
}

调用后原数组被修改了,说明这里操作的就是原数组内存。

为什么 print_arr 里不能改数组

因为参数是:

const int arr[]

PDF 第 32 页也专门说了,const 表示只读。

student_scores 虽然看上去是一个“数组”,但当它作为参数传进 print_arr(student_scores, 4) 的时候,函数并没有把整个数组 {100, 99, 98, 97} 复制一份进去,而只是拿到了这个数组第一个元素的地址。你图里假设这个首地址是 3000,那函数里的 arr 实际上拿到的就是这个 3000size 拿到的是 4。所以在 print_arr 里面,arr[0] 其实就是去地址 3000 这个位置取值,于是得到 100arr[1] 就是往后找下一个 int,得到 99;后面的 9897 也是同样的道理。也就是说,函数里的 arr 和外面的 student_scores 实际上指向的是同一块内存,只是名字不一样而已,所以它能访问到原数组里的内容。这也是为什么数组传参时通常还要额外传一个 size,因为函数虽然拿到了首地址,但它并不知道这块数组到底有多长。你这张图本质上画的就是:左边是原数组在内存中的样子,右边是函数参数收到的“首地址 + 长度”,然后再通过这个地址去访问每一个元素。

这张图表达的意思是:student_scores 这组数组原来是 100、99、98、97,假设它的首地址还是 3000。当你调用 change_arr(student_scores, 4) 的时候,传进函数里的并不是一整份新数组,而仍然只是这个数组第一个元素的地址,也就是 3000,再加上长度 4。所以在 change_arr 里面,arr 实际上指向的就是外面那块原始数组内存。于是当函数里执行 arr[0] = 60arr[1] = 60arr[2] = 60arr[3] = 60 的时候,它改的不是副本,而是原数组本身,所以左边原来那四个值就都被覆盖成了 60。这也是为什么你这张图和前一张 print_arr 的图最大的区别在于:print_arr 只是“通过地址读取原数组”,而 change_arr 是“通过地址直接修改原数组”。本质上这说明数组传参时,函数和外部数组其实共享的是同一块内存,只要函数参数不是只读的,它就能把原数组内容改掉。

课堂总结

数组传参和普通 int/string 传参非常不一样:

  • 普通变量默认值传递,传副本

  • 数组名传进去时退化成地址,函数里改元素会影响原数组


8. 引用传递 pass by reference

PDF 第 34~39 页讲:有时我们希望函数内部能直接修改实参,这时可以用引用传递。引用就是实参的别名 alias。

对应代码

6.func_pass_ref.cpp

完整代码

//==========================================
//函数传参--引用传递
void pass_by_ref_1(int& num)
{
	num = 100;
}

void pass_by_ref_2(string& s)
{
	s = "changed";
}

void pass_by_ref_3(vector<string>& v)
{
	v.clear();//清空容器
}


void print_vector(const vector<string>& v)
{
    for (auto s : v) {
        cout << s << " ";
    }
    cout << endl;
}
int main()
{
    cout << "====================" << endl;
    int my_num{ 19 };
    cout << "修改前的值:" << my_num << endl; // 19
    pass_by_ref_1(my_num);  //在这个函数中 int& num是my_num的替身或者是个别名
    cout << "修改后的值:" << my_num << endl; // 100

    //cout << "====================" << endl;
    //string my_str{ "Hello" };
    //cout << "修改前的值:" << my_str << endl; // Hello
    //pass_by_ref_2(my_str);
    //cout << "修改后的值:" << my_str << endl; // Changed

    //cout << "====================" << endl;
    //vector<string> my_vec{ "apple", "banana", "orange" };
    //cout << "修改前的值:";
    //print_vector(my_vec); // apple banana orange
    //pass_by_ref_3(my_vec);
    //cout << "修改后的值:";
    //print_vector(my_vec); // 空



    return 0;

说明引用传递生效了

int 引用
void pass_by_ref_1(int &num)
{
num = 100;
}

调用后,main 中的 my_num 变成 100。

string 引用
void pass_by_ref_2(string &s)
{
s = "Changed";
}

调用后,原字符串被改掉。

vector 引用
void pass_by_ref_3(vector<string> &v)
{
v.clear();
}

调用后,原 vector 被清空。

const 引用
void print_vector(const vector<string> &v)

这表示:

  • 不拷贝整个 vector,更高效

  • 函数内部不允许修改 vector,更安全

课堂理解

引用传递 = 直接给原变量起了个别名。
所以函数里改引用,外面原变量也会变。

复习对比

  • int num:值传递,改不动外部

  • int &num:引用传递,可以改外部

  • const int &num:引用传递,但只读


9. 函数调用机制:栈、栈帧、递归

PDF 第 41、42 页讲了函数调用栈。每调用一次函数,就会创建新的活动记录(栈帧);函数结束后出栈。局部变量通常在栈上分配,栈空间有限,太深会栈溢出。

全局区:主要是来存放全局变量或者静态变量。

栈:栈上存储的是局部变量和参数等。

9.1

一、示例代码

#include <iostream>
using namespace std;

void func_2(int &x, int y, int z)
{
x += y + z;
}

int func_1(int a, int b)
{
int result {};
result = a + b;
func_2(result, a, b);
return result;
}

int main()
{
int x {20};
int y {30};
int z {};

z = func_1(x, y);

cout << "z = " << z << endl;

return 0;
}

二、本题核心知识点

这段代码主要考察以下几个知识点:

  1. 函数调用时的入栈和出栈过程
  2. 局部变量存放在各自函数的栈帧中
  3. 值传递和值拷贝
  4. 引用传递的本质
  5. 函数返回值如何影响主调函数中的变量

三、程序执行结果

最终输出结果是:

z = 100

四、整体执行流程分析

1. main() 先执行并入栈

程序开始运行后,首先进入 main() 函数。
此时会为 main() 创建一个栈帧,main() 中的局部变量放在这个栈帧里。

main() 中有三个局部变量:

  • x = 20
  • y = 30
  • z = 0

这里的 z {} 表示默认初始化为 0

所以此时 main() 栈帧中的变量情况是:

x = 20
y = 30
z = 0

2. 调用 func_1(x, y)func_1() 入栈

执行这句:

z = func_1(x, y);

会调用 func_1,于是 func_1() 入栈,并创建自己的栈帧。

函数定义是:

int func_1(int a, int b)

这里 ab值传递,所以会把 main() 中的 xy 的值拷贝一份传进去:

  • a = 20
  • b = 30

然后在 func_1() 中还定义了局部变量:

int result {};

所以初始时:

  • result = 0

接着执行:

result = a + b;

因此:

  • result = 20 + 30 = 50

此时 func_1() 栈帧中的变量情况是:

a = 20
b = 30
result = 50

3. 调用 func_2(result, a, b)func_2() 入栈

接下来执行:

func_2(result, a, b);

进入 func_2(),创建 func_2() 的栈帧。

函数定义是:

void func_2(int &x, int y, int z)

这三个参数中,最关键的是第一个:

int &x

这是引用传递,所以这里的 x 并不是一个新的独立变量,
它是 func_1()result 的别名。

也就是说:

x <-> result

它们本质上操作的是同一块数据。

另外两个参数:

  • y = a = 20
  • z = b = 30

这两个是普通值传递,是把值拷贝进来的。

所以进入 func_2() 后,可以理解为:

x -> 引用 func_1 中的 result(当前是 50)
y = 20
z = 30

4. 在 func_2() 中修改 x

执行:

x += y + z;

代入当前数值:

x += 20 + 30
x += 50

由于此时 x 引用的是 result,而 result 当前是 50,所以:

x = 100

因为 xresult 指向的是同一个值,所以同时也意味着:

result = 100

这一步是本题最关键的地方。


5. func_2() 执行结束,出栈

func_2() 执行完毕后,其栈帧销毁,函数出栈。

注意:

  • func_2() 中的普通形参 yz 会随着栈帧销毁而消失
  • 但是通过引用修改过的 result 属于 func_1() 栈帧,所以它的值会保留下来

因此回到 func_1() 时:

a = 20
b = 30
result = 100

6. func_1() 返回 result

接着执行:

return result;

此时返回的是 100

然后 func_1() 执行结束,栈帧销毁,函数出栈。


7. 返回到 main(),把返回值赋给 z

回到 main() 之后,执行:

z = func_1(x, y);

实际上就是把 func_1() 返回的 100 赋值给 z

所以这时 main() 中变量变成:


x = 20
y = 30
z = 100

注意这里:

  • main() 中的 xy 没有被改
  • 这是因为 func_1() 的参数 ab 是值传递
  • 被改的是 func_1() 内部的 result

8. 输出结果

最后执行:

cout << "z = " << z << endl;

输出:

z = 100


五、栈帧变化过程总结

可以按顺序概括成下面这样:

第一步:main() 入栈

栈帧中有:

x = 20
y = 30
z = 0

第二步:调用 func_1(x, y)func_1() 入栈

栈帧中有:

a = 20
b = 30
result = 50

第三步:调用 func_2(result, a, b)func_2() 入栈

栈帧中有:

x -> 引用 result
y = 20
z = 30

第四步:执行 x += y + z

计算后:

x = 100
result = 100

第五步:func_2() 出栈

回到 func_1(),此时:

result = 100

第六步:func_1() 返回 result

返回值为:

100

第七步:func_1() 出栈,回到 main()

此时:

z = 100

第八步:输出结果

输出:

z = 100


六、为什么 result 会变成 100?

因为在 func_2() 中:

void func_2(int &x, int y, int z)

参数 x引用,它绑定的是 func_1() 中的 result

所以:

x += y + z;

并不是改 func_2() 自己新建的某个变量,
而是直接改了 func_1() 里的 result

这就是引用传参的作用:可以直接修改实参对应的变量。


七、值传递与引用传递的区别

1. 值传递

func_1(int a, int b) 里的 ab,以及 func_2(int &x, int y, int z) 里的 yz

  • 传进去的是值的副本
  • 在函数内部修改它们,不会影响外部原变量

例如:

  • amain()x 的副本
  • bmain()y 的副本

所以无论 func_1() 里怎么改 ab,都不会影响 main() 中的 xy


2. 引用传递

func_2(int &x, ...) 中的 x

  • 不是新开一份独立数据
  • 而是原变量的别名
  • 修改 x 就等于修改它绑定的那个变量

在这道题里:

x 是 result 的别名

所以修改 xresult 也跟着变。


八、这道题最容易混淆的地方

1. func_2 里的 x 不是新的普通变量

它不是简单地“拷贝了 result 的值”,
而是直接引用 result 本身。

所以不能把它理解成:

x = 50

更准确地说应该理解为:

x 绑定 result


2. main() 里的 x 没有被修改

虽然 func_2 里也有个名字叫 x,但它引用的是 func_1 里的 result,不是 main() 里的 x

也就是说:

  • main() 中有一个变量叫 x
  • func_2() 中也有一个参数叫 x

它们不是同一个东西,只是名字相同而已。


3. z 的值来自返回值,不是被引用修改的

main() 里的 z 最终变成 100,是因为:

z = func_1(x, y);

也就是把 func_1() 的返回值赋给了 z
不是因为 func_2() 直接修改了 main() 中的 z


九、一句话总结

这段代码的执行本质是:

main()x=20y=30 传给 func_1()func_1() 先算出 result=50,再通过引用把 result 传给 func_2()func_2()result 改成 100,最后 func_1() 返回 100main() 中的 z


十、适合记忆的简版结论

你们可以把这题背成下面这个版本:

1. main入栈:x=20,y=30,z=0
2. 调用func_1:a=20,b=30,result=50
3. 调用func_2:x引用result,y=20,z=30
4. 执行x += y + z,即 result += 20 + 30
5. result从50变成100
6. func_2出栈
7. func_1返回result,即100
8. func_1出栈
9. main中z接收返回值100
10. 输出 z = 100

 9.2普通函数嵌套调用,入栈出栈

#include <iostream>
using namespace std;

unsigned long long factorial(unsigned long long n)
{
    if (n == 0)
        return 1; // 最终返回的是1
    return n * factorial(n - 1);  // 递归调用
}

int main()
{
    cout << factorial(3) << endl; // 6
    cout << factorial(8) << endl; // 40320
    cout << factorial(12) << endl; // 479001600
    cout << factorial(20) << endl; // 2432902008176640000
}

factorial(3) 的入栈过程就是:

main() 先入栈,执行到 factorial(3) 时,调用 factorial(3),于是 factorial(3) 入栈,参数 n = 3
因为 n != 0,所以它还不能返回,要继续调用 factorial(2),于是 factorial(2) 入栈,参数 n = 2
同理,factorial(2) 又调用 factorial(1),所以 factorial(1) 入栈,参数 n = 1
接着 factorial(1) 再调用 factorial(0),于是 factorial(0) 入栈,参数 n = 0
到这里为止,递归调用不断向下压栈,入栈过程结束。

可以直接记成:


main

factorial(3) n=3

factorial(2) n=2

factorial(1) n=1

factorial(0) n=0

核心就一句话:每一层都在等下一层的结果,所以会不断继续入栈,直到 n == 0 为止。

出栈过程其实就是:最里面那层先返回,然后外面一层一层拿着返回值继续算。

factorial(3) 来说,前面入栈已经到了:

main
factorial(3) n=3
factorial(2) n=2
factorial(1) n=1
factorial(0) n=0

这时开始出栈。

factorial(0) 先执行到:

if (n == 0)
return 1;

所以 factorial(0) 先返回 1,然后这一层出栈。

接着回到 factorial(1)
它原来停在这句:

return 1 * factorial(0);

现在 factorial(0) 已经返回了 1,所以这里就变成:

return 1 * 1;

因此 factorial(1) 返回 1,然后出栈。

再回到 factorial(2)
它原来停在:

return 2 * factorial(1);

现在 factorial(1) 返回的是 1,所以变成:

return 2 * 1;

因此 factorial(2) 返回 2,然后出栈。

再回到 factorial(3)
它原来停在:

return 3 * factorial(2);

现在 factorial(2) 返回的是 2,所以变成:

return 3 * 2;

因此 factorial(3) 返回 6,然后出栈。

最后回到 main(),输出 6

你们可以直接记成:

factorial(0) 返回 1
factorial(1) = 1 × 1 = 1
factorial(2) = 2 × 1 = 2
factorial(3) = 3 × 2 = 6

核心一句话就是:
出栈时,谁先到达 n==0 谁先返回,后面的每一层都用下一层的返回值继续计算。


第二部分:指针 pointer

  • 指针也是变量

  • 它保存的是地址

  • 可以指向另一个变量,也可以和函数、数组、堆内存等联系起来

  • 用途包括:访问作用域外数据、高效操作数组、动态分配内存等

一句话理解

普通变量存“值”,指针变量存“地址”。


11. 指针的声明、初始化、地址操作符 &

PDF 第 48~54 页依次讲了:

  • 怎么声明指针

  • 指针最好初始化为 nullptr

  • & 用来取地址

  • 指针类型必须和所指对象类型匹配

  • 指针本身也是变量,所以它的值可以改变

对应代码 1:最基础版

0.temp.cpp

int *ptr;
cout << "ptr的地址是: " << &ptr << endl;
cout << "ptr的值是: " << ptr << endl;

这里能区分两个概念:

  • &ptr:指针变量自己存放在内存中的地址

  • ptr:指针变量当前保存的“地址值”

但这个例子也提示一个风险:ptr 没初始化,值是不确定的。

对应代码 2:更完整演示

9.pointer_demo.cpp

普通变量
int my_num {10};
cout << &my_num << endl;

说明普通变量也有地址。

未初始化指针
int *num_ptr;
cout << num_ptr << endl;

未初始化时值不可靠。后面代码把它设为:

num_ptr = nullptr;

这才是安全写法。

不同类型的指针大小
int *p1 {nullptr};
double *p2 {nullptr};
long long *p3 {nullptr};
string *p4 {nullptr};
vector<string> *p5 {nullptr};

代码打印它们的 sizeof,在同一平台上通常一样大,因为它们本质上都只是地址。

类型匹配
int *score_ptr {nullptr};
score_ptr = &student_score;
// score_ptr = &high_temp; // 会报错

说明 int* 只能指向 int 类型对象的地址。

课堂总结

记住三个最基础的写法:

int num = 10; // 普通变量
int *ptr = &num; // 指针保存 num 的地址
ptr = nullptr; // 空指针

12. 指针的解引用 *

PDF 第 56~58 页说:解引用就是“通过指针拿到它指向的数据”。

对应代码

10.pointer_deref.cpp

int 例子
int student_score {100};
int *score_ptr {&student_score};
cout << *score_ptr << endl;
*score_ptr = 150;

这里 *score_ptr 就是“指针指向的那个 int 变量本身”,所以修改 *score_ptr 就等于修改 student_score

double 例子
double *temp_ptr {&high_temp};
temp_ptr = &low_temp;
cout << *temp_ptr << endl;

说明指针本身存的地址可以改,于是它能重新指向别的变量。

string 例子
string *str_ptr {&str};
cout << *str_ptr << endl;

说明指针不只用于基本类型,也能指向对象。

vector 例子
vector<string> *vector_ptr {&my_str_vec};
cout << (*vector_ptr).at(0) << endl;

这里要注意括号:*vector_ptr 先解引用得到 vector,再 .at(0) 访问元素。

课堂总结

  • ptr:地址

  • *ptr:地址里存放的数据


13. 动态内存分配:new / delete

PDF 第 60~63 页讲:堆内存由程序员自己申请与释放。

使用new关键字在堆上分配内存,并赋初值为10

C++会在堆上面分配一个容纳int类型的空间然后返回这个空间的地址,然后返回给int_ptr

此时如果输出int_ptr的值(地址),之后再解引用这个值就是10

  • new 分配

  • delete 释放

  • 数组要用 new[]delete[]

对应代码

11.pointer_allo.cpp

分配单个 int
int *int_ptr {nullptr};
int_ptr = new int;

在堆上开一块 int 大小的空间,把地址交给 int_ptr

赋值
*int_ptr = 100;

通过指针往那块堆内存里写值。

释放
delete int_ptr;

释放这块堆内存。

动态数组
temp_ptr = new double[size];
delete [] temp_ptr;

这就是动态数组。数组形式必须用 delete[]

课堂总结

必须成对记忆:

  • newdelete

  • new[]delete[]

代码区、全局区、栈、堆。

我们在堆上分配一个int大小的存储空间,会在做一个double类型的(是一段连续的存储空间),

声明一个指针指向int类型的 *int_ptr另一个 *double_ptr

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    int *int_ptr {nullptr};

    cout << "分配前的int_ptr的值是: " << int_ptr << endl; // 0x0
    int_ptr = new int; // 在heap堆上分配一个int类型的内存空间,返回该内存空间的地址
    cout << "分配后的int_ptr的值是: " << int_ptr << endl; // 0x7ffeeb5c9f7c
    cout << *int_ptr << endl; // 0

    *int_ptr = 100; // 通过指针修改内存空间的值
    cout << *int_ptr << endl; // 100

    delete int_ptr; // 释放内存空间
   

    size_t size {0};
    double *temp_ptr {nullptr};
    cout << "多少个温度值?";
    cin >> size;
    temp_ptr = new double[size]; // 在heap堆上分配size个double类型的内存空间,返回该内存空间的地址
    cout << "地址是: " << temp_ptr << endl;
    delete [] temp_ptr; // 释放内存空间


    return 0;
}

易错点

如果申请了堆内存却忘记释放,就会造成内存泄漏


14. 指针和数组的关系

PDF 第 65~70 页是这节课的重点之一:

  • 数组名的值是首元素地址

  • 如果类型一致,数组名和指针几乎等价

  • 下标访问和指针偏移访问可以互相转换

对应代码

12.pointer_array.cpp

int main()
{
    int student_scores [] {100, 98, 90};
    cout << "student_scores的值是: " << student_scores << endl; 

    int *score_ptr {student_scores}; // score_ptr指向student_scores数组的第一个元素
    cout << "score_ptr的值是: " << score_ptr << endl; 

    cout << "====== 数组名称,下标方式访问元素 ====== " << endl;
    cout << student_scores[0] << endl; // 100
    cout << student_scores[1] << endl; // 98
    cout << student_scores[2] << endl; // 90

    cout << "====== 指针名称,下标方式访问元素 ====== " << endl;
    cout << score_ptr[0] << endl; // 100
    cout << score_ptr[1] << endl; // 98
    cout << score_ptr[2] << endl; // 90

    cout << "====== 指针名称,指针运算符方式访问元素 ====== " << endl;
    cout << *score_ptr << endl; // 100
    cout << *(score_ptr + 1) << endl; // 98
    cout << *(score_ptr + 2) << endl; // 90

    cout << "====== 数组名称,指针运算符方式访问元素 ====== " << endl;
    cout << *student_scores << endl; // 100
    cout << *(student_scores + 1) << endl; // 98
    cout << *(student_scores + 2) << endl; // 90

    cout << "====== ++运算符 ====== " << endl;
    // 但需要注意++会改变指针的值,下次访问的就不是原来的位置了
    cout << *score_ptr++ << endl; // 100
    cout << *score_ptr++ << endl; // 98
    cout << *score_ptr << endl; // 90

    
    return 0;
}
数组名就是首地址
int student_scores [] {100, 98, 90};
cout << student_scores << endl;
指针指向数组首元素
int *score_ptr {student_scores};
cout << score_ptr << endl;

这两个输出是一样的首地址。

下标方式访问
student_scores[0]
score_ptr[0]

都能得到 100。

指针运算方式访问
*score_ptr
*(score_ptr + 1)
*(score_ptr + 2)

分别访问第 1、2、3 个元素。

数组名也能做偏移
*student_scores
*(student_scores + 1)
*(student_scores + 2)

这就是 PDF 第 70 页表格中的关系。

score_ptr++

代码还演示了:

cout << *score_ptr++ << endl;

这会访问当前元素后,再把指针移动到下一个位置。

课堂总结

重点公式一定要背:

arr[i] == *(arr + i)
ptr[i] == *(ptr + i)

15. 指针作为函数参数

PDF 第 76~78 页讲:函数形参可以是指针,这样就能通过地址修改外部数据。实参可以直接传地址,也可以传已有指针。

对应代码 1:修改单个变量

13.func_pass_pointer.cpp

void double_data(int *int_ptr)
{
*int_ptr *= 2;
}

main 中两种调用方式:

double_data(&value); // 直接传地址
double_data(int_ptr); // 传指针变量

最后 value 从 20 变 40,再变 80。

对应代码 2:交换两个数

14.swap_num.cpp

void swap_value(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}

调用:

swap_value(&x, &y);

交换后 x 和 y 的值真的互换了。

课堂理解

这里和“值传递”不同。虽然函数收到的是地址的副本,但这个副本仍然指向原变量,所以通过 *a*b 修改的还是原对象。


16. const 和指针

PDF 第 72~75 页把 const 指针分成三类:

  1. 指向常量的指针:不能通过指针改对象,但指针可改指向

  2. 常量指针:指针本身不能改指向,但能改对象

  3. 常量指针指向常量:两者都不能改

‘、

对应代码

15.const_pointer.cpp

情况一:函数参数 const vector<string> *const v
void display(const vector<string> *const v)

这表示:

  • const vector<string>:不能通过 v 修改 vector 内容

  • *const v:指针变量 v 自己也不能改指向

代码里两句注释掉的语句正好说明这两个限制:

// (*v).at(0) = "kiwi"; // 不允许
// v = nullptr; // 不允许
情况二:普通指针数组参数
void display(int *array, int sentinel)
{
while (*array != sentinel)
cout << *array++ << endl;
}

这里 array 可以移动,且可读写。

课堂记忆口诀

从右往左读最清楚:

  • const int *pp 指向的 int 不能改

  • int *const pp 自己不能改

  • const int *const p:两边都不能改


17. 函数返回指针

PDF 第 80~84 页讲得很关键:

函数在堆上动态分配内存空间,函数返回指针常见场景。

函数可以返回指针,但要注意返回的地址是否合法。
可以返回:

  • 动态分配的内存地址

  • 传入对象相关的合法地址

不能返回:

  • 局部变量的地址,因为函数结束后局部变量生命周期结束,地址失效

对应代码

16.return_pointer.cpp

正确示例:返回堆内存地址

int *create_array(size_t size, int initial_value = 0)
{
int *new_storage{nullptr};
new_storage = new int[size];
...
return new_storage;
}

这段代码在堆上分配数组并返回地址,是合法的。

调用方负责释放

new_arr = create_array(size, value);
display(new_arr, size);
delete[] new_arr;

这说明:谁最终持有这块堆内存,谁就要负责释放。

课堂重点

为什么不能返回局部变量地址?

因为局部变量在函数结束时就销毁了,它在栈上的空间已经无效,返回它的地址就成了“悬空指针”。PDF 第 84 页专门强调了这一点。


18. 引用 reference 回顾

 

PDF 第 86~88 页最后把引用和指针做了对比。引用是变量的别名,本质上可以看作一个“自动解引用、且自身不可改指向”的东西。
特点包括:

  • 声明时必须初始化

  • 不能为 null

  • 初始化后不能再改绑到别的变量

  • 常用于函数传参

对应代码

17.ref_demo.cpp

基本引用

int my_num {10};
int &my_ref {my_num};
my_ref = 100;

修改 my_refmy_num 也变成 100。

range-for 中的值拷贝

for (auto str:my_str)
str = "Hello";

这里只是改了拷贝,原 vector 不变。

range-for 中的引用

for (auto &str:my_str)
str = "Hello";

这里改的是原 vector 中元素,所以内容真的被改了。

常量引用

for (auto const &str:my_str)
cout << str << endl;

可读、不可改,也避免了拷贝。

课堂总结

这段代码非常适合你复习“值 / 引用 / const 引用”的区别。


第三部分:整节课的知识串联

1. 函数部分的主线

这节课函数部分的逻辑是这样的:

先学会把代码写成函数,再学会用函数原型解决“先调用后定义”的问题;然后理解默认参数传递是值传递;接着引出重载,让同名函数适应不同类型;随后进一步讨论数组和引用传参,最后用调用栈和递归说明函数在内存中是怎么运行的。

对应代码顺序建议

按下面顺序复习最顺:

  • 1.func_demo.cpp:函数定义和调用

  • 2.func_prototype.cpp:函数原型

  • 3.func_params.cpp:值传递

  • 4.func_overloading.cpp:重载

  • 5.func_pass_array.cpp:数组传参

  • 6.func_pass_ref.cpp:引用传参

  • 7.func_stack.cpp:函数调用链

  • 8.func_factorial.cpp:递归与栈


2. 指针部分的主线

指针部分的逻辑是:

先理解指针是“存地址的变量”,再通过解引用访问数据;接着学会在堆上动态申请空间;然后理解数组和指针几乎等价;之后进一步掌握 const 与指针的关系、指针传参、返回指针,并最终与引用进行对比。

对应代码顺序建议

  • 0.temp.cpp:最基础的指针变量观察

  • 9.pointer_demo.cpp:声明、初始化、地址、类型匹配

  • 10.pointer_deref.cpp:解引用

  • 11.pointer_allo.cpp:new / delete / 动态数组

  • 12.pointer_array.cpp:指针与数组

  • 13.func_pass_pointer.cpp:指针传参

  • 14.swap_num.cpp:双指针参数的经典应用

  • 15.const_pointer.cpp:const 和指针

  • 16.return_pointer.cpp:返回指针与动态内存

  • 17.ref_demo.cpp:引用与 const 引用


第四部分:考试 / 复习高频易错点

1. 值传递不会改外部变量

int numstring strvector<string> vec 这种默认写法,如果没有 &,通常都是拷贝。3.func_params.cpp 就是完整证明。

2. 数组传参不是整体拷贝

数组传入函数时,本质上是首地址,所以函数里修改元素会影响原数组。5.func_pass_array.cpp 是直接证据。

3. 引用传参能直接修改实参

int &numstring &svector<string> &v 都能改原变量。6.func_pass_ref.cpp 非常重要。

4. 指针一定尽量初始化

不要像 0.temp.cpp 里那样直接用未初始化指针。更推荐 nullptr9.pointer_demo.cpp 也在强调这一点。

5. ptr*ptr 别混

  • ptr 是地址

  • *ptr 是地址里存的数据
    10.pointer_deref.cpp 是最标准例子。

6. newdelete 必须配对

单个对象用 delete,数组用 delete[]11.pointer_allo.cpp16.return_pointer.cpp 都体现了。

7. 不要返回局部变量地址

函数结束,局部变量就销毁。PDF 第 84 页明确提醒不能这样做。

8. const 修饰谁要看位置

  • const int *p:不能改 *p

  • int *const p:不能改 p

  • const int *const p:都不能改
    结合 15.const_pointer.cpp 记忆效果最好。


第五部分:一页速记版

函数

  • 函数 = 返回类型 + 函数名 + 参数列表 + 函数体。

  • 定义后要调用才执行。

  • 使用前编译器必须先见过函数,可用函数原型。

  • 默认是值传递,函数里改的是副本。

  • 重载 = 同名函数 + 参数列表不同。

  • 数组传参本质上传地址,要额外传大小。

  • 引用传参可直接修改原变量。

  • 函数调用靠栈,递归层数太深会栈溢出。

指针

  • 指针是存地址的变量。

  • & 取地址,* 解引用。

  • 指针建议初始化为 nullptr

  • new 申请堆内存,delete 释放。数组要 new[] / delete[]

  • 数组名几乎等价于首元素地址。

  • arr[i] == *(arr+i)ptr[i] == *(ptr+i)

  • 指针传参可修改外部变量。

  • 返回指针时,不要返回局部变量地址。

  • 引用像“自动解引用且不可改绑的指针”。


第六部分:你的复习建议

你后面复习时,建议按这个方法来:

第一遍先只看 PDF,建立知识框架。
第二遍按代码顺序跑一遍,每个文件都自己预测输出。
第三遍重点区分下面三组最容易混淆的概念:

  • 值传递 vs 引用传递 vs 指针传参

  • 数组名 vs 指针变量

  • ptr vs *ptr vs &ptr

这样这节课就会非常扎实。

如果你愿意,我可以下一步把这份内容再整理成一版更像考试复习提纲的“精简背诵版”,或者整理成Markdown/PDF 风格的正式笔记版

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐