1.前文复习
案例复习
案例1:
cpp
//09-7---003--案例1
#include <stdio.h>
int a = 1;
main() {
int s = 3;
{
int a = 5;
s += ++a;
}
s += ++a;
printf("%d\n", s);
}
开始分析:外部的a是全局变量,主函数内部的s和复合语句里的a是局部变量。
++在前先加1再累加。因此,从主函数进入到定义完变量后执行复合语句的s += ++a;此时a是复合语句中的值,++a=6,再s+a再赋值给s,s=3+6=9。
出复合语句后执行s += ++a;此时的a指的是全局变量a=1,且s的值已被更新为9,再次++a再s+a赋值给s,此时s被更新为11。
因此最后应该输出11。d分析结束,我们上机看看最终结果:

案例2:
cpp
//09-7---004--案例2
#include <stdio.h>
int w = 3;
fun(int k) {
if (k == 0)return w;
return (fun(k - 1) * k);
}
main()
{
int w = 10;
printf("%d\n", fun(5) * w);
}
开始分析。定义了一个全局变量w,有一个子函数fun()和一个主函数main(),fun()是默认int类型。从主函数进入,首先定义了一个局部变量w=10,然后输出,输出的值来自于fun(5)*w,那么开始调用子函数fun(),单向传值,则k=5,进入fun(),即算出fun(5)就可以得到结果了。
开始调用fun(5),进入子函数,k=5,if语句判断为no,进入return语句,return fun(4)*5。那么接下来的任务就变成了求fun(4)。
开始调用fun(4),同样进入子函数,k=4,if语句判断为no,进入return语句,return fun(3)*4。那么接下来的任务就变成了求fun(3)。
开始调用fun(3),同样进入子函数,k=3,if语句判断为no,进入return语句,return fun(2)*3。那么接下来的任务就变成了求fun(2)。
开始调用fun(2),同样进入子函数,k=2,if语句判断为no,进入return语句,return fun(1)*2。那么接下来的任务就变成了求fun(1)。
开始调用fun(1),同样进入子函数,k=1,if语句判断为no,进入return语句,return fun(0)*1。那么接下来的任务就变成了求fun(0)。
开始调用fun(0),同样进入子函数,k=0,if语句判断为yes,进入if语句,执行return w,此时w是全局变量w=3。那么就返回3。
值3返回给fun(0),fun(0)=3,fun(0)*1=fun(1)=3,fun(1)*2=fun(2)=3*2=6,fun(2)*3=fun(3)=6*3=18,fun(3)*4=fun(4)=18*4=72,fun(4)*5=fun(5)=72*5=360。
因此fun(5)=360,返回值360,执行printf("%d\n", fun(5) * w);此时的w应该是局部变量w=10,因此最终应该输出3600。
分析结束,接下来我们上机看看最终效果:

本案例出现子函数自己调用自己的情况,即递归。函数允许嵌套调用,但不能嵌套定义。出现递归需要注意:递归的函数有几句?其次需要画图(见2.9)!上方写调用,下方写返回!!

如果将k的值更改为3,程序运行会有什么变化?
上机看看实际效果:

与前面不同,此时程序执行到k=3时,就带着w的值返回了。
案例3:
cpp
//09-7---004--案例3
#include <stdio.h>
void fun() {
a = 100;
b = 200;
}
main() {
int a = 5, b = 7;
fun();
printf("%d%d", a, b);
}
开始分析,可以发现,此时虽然调用了fun(),fun()内部也对a,b的值定义了,但此时fun()并没有返回任何值,因此主函数内的a,b仍然使用主函数内的值。因此输出57。光标落在7后。
分析结束,接下来我们上机看看最终效果:

但如果将主函数中的int a = 5,b = 7;删去,此时又输出多少呢?
此时再调用fun(),主函数内的a,b使用的就是fun()内用过的a和b的值了。此时返回100200,光标落在末尾的0后。
分析结束,接下来我们上机看看最终效果:

2.指针
指针是 C 语言区别于其他高级语言的核心语法特性,本质是内存地址的抽象封装。指针体系覆盖基础变量指针、数组指针、字符串指针、函数指针、指针数组、二级多级指针六大核心模块,同时配套地址传递、动态内存、多维数组寻址等工程应用。
2.1指针变量的定义格式
指针变量的定义格式是:类型名后面跟星号,再跟指针变量名,最后分号。(见3.1)
类型* 指针变量名;
跟之前学的普通变量定义相比,唯一的区别就是指针变量名前面多了一个星号。
这里有一个关键点:这个星号不是变量名的一部分 。变量名要遵循用户自定义标识符的命名规则,字母下划线开头,不能含星号。星号只是一个标志 ,告诉系统 "我不是普通的内容变量,而是指针变量"。它没有任何计算功能,纯粹是一个标记。
判断一个语句是不是定义语句,有一个口诀:前有类型,后有分号,就是定义语句;除此之外都是执行语句。
2.2如何区分普通变量和指针变量
区分方法很简单,就看变量名前面有没有星号。有星号的是地址变量,也就是指针变量;没有星号的是内容变量,也就是普通变量。
比如定义了int a, b, *p1, *p2,这里a和b前面没有星,是普通变量;p1和p2前面带*,是指针变量。
2.3指针变量的存储规则
内容变量存内容值,地址变量存地址值,不能乱存。(见3.4)
C 语言中直接写的常量,比如1000、2000、4000、7.8这些,系统永远当作内容值来处理。内容值只能存入普通变量,不能存入指针变量。指针变量里只能存地址值。
所以如果写 int a, * p; 然后a = 1000;这是对的,但若写p = 4000就是错的,因为4000是内容值,不能给地址变量。
2.4两个核心运算符
要知道地址长什么样,怎么获取地址,需要学习两个运算符:取地址运算符和指针运算符。(见3.2)
2.4.1取地址运算符 &
取地址运算符我们其实见过,在scanf里就用过。它的优先级是二级,属于单目运算符,结合性从右向左。它后面要放一个内容变量 ,功能是取出这个内容变量的地址值 。运算结果是一个地址值。
比如int a = 10; 那么&a就是内容变量a在内存中的地址。
2.4.2指针运算符 *
指针运算符也是二级单目运算符,结合性从右向左。它后面要放一个地址 ,功能是取出这个地址所对应内存单元的内容值 。运算结果是一个内容值。
比如 int a = 10, p = &a; 那么p就是取p中存的地址对应的内容,也就是a的值10。
这两个运算符是互逆的,一个得地址,一个得内容。
2.5星号的三个角色
星号在C语言中有三个角色,这是初学者最容易混淆的地方,必须分清。
第一个角色是乘运算符,双目运算,两边都要有操作数,比如7*8,有计算功能。
第二个角色是标志,在定义指针变量时使用,前面有类型,没有计算功能,可以理解为把星号 "盖掉",就是定义了一个指针变量。
第三个角色是指针运算符,单目,前面没有类型,发挥运算功能,后面放地址,结果是内容值。
判断方法很简单:两边都有数就是乘;前面有类型就是标志;都不是就是指针运算符。
2.6指针变量的使用口诀
指针变量的使用有一个口诀:有星为内容值,不是读就是写。放在赋值号左边是写,其他任何地方都是读。
比如a = * p,这里* p在赋值号右边,是读,读出p指向的内容赋给a。而* p = 20,* p在左边,是写,把20写入p指向的内存单元。
2.7指针变量的危险性
指针变量只定义不初始化时,里面的地址是随机的,可能指向用户区,也可能指向系统区。如果指向系统区,你去修改它的内容,就可能篡改操作系统的数据,导致死机或崩溃。(见3.4)
所以指针变量的使用原则是:定义且初始化后再使用。普通变量是先定义后使用,指针变量多了一步初始化。
比如int p; 然后直接写p = 200,这是极度危险的,p指向随机位置,可能造成系统崩溃。正确的做法是先让p指向一个合法变量,比如int a = 0; p = &a; 然后再写* p = 200。
2.8指针变量之间的赋值
两个指针变量之间有两种赋值方式。(见3.5)假设p1,p2都是指针变量。
第一种是地址赋值,p1 = p2,两边都是地址。执行后p1和p2里存的地址相同,指向同一个内存单元,就像两把钥匙开同一扇门。
第二种是内容赋值,*p1=*p2,两边都是内容值。执行后读出p2指向的内容,写入p1指向的单元,p1和p2的指向不变,但p1指向的内容被改变了。
思考:地址赋值为什么不写成&p1 = &p2?
此处p1,p2已经作为指针变量出现了,他们存的就是地址变量,如果写成&p1,意思是取p1这个变量自身存的地址。这两个的概念不一样!
理解(可以画图辅助):假设我现在有普通内容变量a,它的地址是1000,存a=42,且有指针变量p1 = 1000,p2 = 1000,而p1的地址假设是1004,p2的地址假设是1005。那么可以看到p1和p2本身的内容就是指向a的地址的(1000),如果再写成&p1和&p2,那么就意味着是取的p1,p2作为变量的地址(1004和1005)。
2.9指针题的画图方法
做指针题一定要画图。内容变量画房子,里面写值;指针变量画指向箭头,得谁的地址就指向谁,像钥匙开门一样。(见1.案例2、见2.8辅助理解)
C语言中有五类题必须画图:数组、静态变量、指针、链表、文件。养成画图的习惯,指针题就不会乱。
2.10指针等价表
假设 int a = 20; int * p = &a; 那么有以下等价关系:
* p 等价于 a,因为有星为内容值 。p等价于& a,因为无星为地址。*&a等价于a,因为先取地址再取内容,互逆抵消。&*p等价于p,因为先取内容再取地址,回到原地址。(见3.6)
还有自增自减的等价:(*p)++ 等价于 a++,(*p)-- 等价于 a--,++(*p) 等价于 ++a。
3.示例
下面通过几个完整的例子来巩固以上知识点,所有代码都可以在VS2019中直接编译运行。
3.1指针变量定义与非法赋值
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
// 定义:a、b是普通变量(内容变量),p是指针变量(地址变量)
int a, b, *p;
a = 1000; //内容值存入内容变量,合法
b = 2000; //合法
// p = 4000; //错误!4000是内容值,不能直接赋给指针变量
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
上机看实际运行效果:

3.2取地址运算符&与指针运算符*的基本用法
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a, b = 20, c = 30;
int d = 40;
int * p; // p是指针变量
p = &d; //&d取出d的地址,赋给指针变量p(地址给地址变量)
a = *p; //*p取出p指向的内容(即d的值40),赋给a
a = d; //直接赋值,效果与上一行相同
printf("d = %d\n", d); // 40
printf("*p = %d\n", *p); // 40(*p等价于d)
printf("a = %d\n", a); // 40
return 0;
}
上机看实际运行效果:

3.3定义时初始化vs先定义后赋值
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 10;
// ===== 写法1:定义指针变量时同时初始化(正确)=====
int * p1 = &a;
// 这里的 * 是"标志",前面有类型int,可以理解为:
// 定义了指针变量p1,并把a的地址赋给p1
printf("*p1 = %d\n", *p1); // 10
// ===== 写法2:先定义,再单独赋值(正确写法)=====
int * p2;
p2 = &a; //正确:p2是地址变量,直接赋地址
printf("*p2 = %d\n", *p2); // 10
// ===== 写法3:错误示范 =====
int * p3;
// *p3 = &a; //错误!
// 这里的 * 前面没有类型,是"指针运算符"
// *p3 表示取p3指向的内容(是个内容值),而 &a 是地址
// 内容值 = 地址,类型不匹配,编译报错
return 0;
}
上机看实际运行效果:

3.4野指针的危险性演示
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int * p; // 只定义,未初始化 → p中是随机地址(野指针)
// *p = 200; 这样写极度危险!
// p指向随机内存位置,如果恰好指向系统区,
// 写入200会篡改操作系统数据,可能导致死机/崩溃。
// VS2019中通常会触发运行时错误(访问违规)。
//正确做法:先让指针指向一个合法变量
int a = 0;
p = &a; // 初始化,p指向a
*p = 200; // 安全:把200写入a所在的内存单元
printf("a = %d\n", a); // 200
return 0;
}
上机看实际运行效果:

3.5指针之间的两种赋值(地址赋值vs内容赋值)
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int x = 10, y = 99;
int * p1 = &x;
int * p2 = &y;
printf("===== 初始状态 =====\n");
printf("x = %d, y = %d\n", x, y); // x=10, y=99
printf("*p1 = %d, *p2 = %d\n", *p1, *p2); // *p1=10, *p2=99
// ===== 地址赋值:p1 = p2 =====
p1 = p2; // 此时p1,p2都是指针变量,可以把p2里存的地址(y的地址)赋给p1
// 结果:p1和p2现在都指向y
printf("\n===== 执行 p1 = p2 后 =====\n");
printf("*p1 = %d, *p2 = %d\n", *p1, *p2); // *p1=99, *p2=99
printf("p1和p2现在都指向y\n");
// 恢复p1指向x
p1 = &x;
// ===== 内容赋值:*p1 = *p2 =====
*p1 = *p2; // 读出p2指向的内容(y的值99),写入p1指向的单元(x)
// 结果:x的值被改成99,p1仍然指向x,p2仍然指向y
printf("\n===== 执行 *p1 = *p2 后 =====\n");
printf("x = %d, y = %d\n", x, y); // x=99, y=99
printf("*p1 = %d, *p2 = %d\n", *p1, *p2); // *p1=99, *p2=99。可知p1仍指向x,p2仍指向y,但x的值被改成了99
return 0;
}
上机看实际运行效果:

3.6指针等价表综合验证
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 20;
int * p = &a;
printf("a = %d\n", a); // 20
printf("*p = %d\n", *p); // 20 → *p 等价于 a
printf("p = %p\n", (void*)p); // 打印a的地址
printf("&a = %p\n", (void*)&a); // 同上 → p 等价于 &a
// *&a 等价于 a
printf("*&a = %d\n", *&a); // 20
// &*p 等价于 p
printf("&*p = %p\n", (void*)(&*p)); // 与p相同
// (*p)++ 等价于 a++
(*p)++;
printf("(*p)++ 后,a = %d\n", a); // 21
// ++(*p) 等价于 ++a
++(*p);
printf("++(*p) 后,a = %d\n", a); // 22
return 0;
}
上机看实际运行效果:

学习过程有诸多不足之处,欢迎来私信与我一同交流学习,共同进步!
