C语言:第14天笔记
内容提要
- 指针
- 变量指针与指针变量
- 指针变量做函数参数
- 指针变量指向数组元素
- 数组指针与指针数组
- 数组指针
- 变量指针与指针变量
回顾
变量指针与指针变量
变量指针:变量的地址值(首地址),本质是指针、地址
指针变量:存储指针的变量,本质是变量
指针操作的两个运算符
&
:取地址运算符,作用是获取指定对象的地址
*
:指针操作符,如果这个符号前面有数据类型,就被称作声明指针 ;如果没有,就被称作解引用
关于指针中指向的问题
c
int a = 10;
int *p = &a // 指针变量p指向对象a
指向:指针变量存储了谁的地址,这个指针变量就指向了谁!
使用间接操作,如何交换a和b的值
- 交换指向:指向发生改变,指向对象的数据不会改变
- 交换数据:指向不发生改变,指向对象的数据会改变
关于指针变量
指针变量本质上还是变量,只不过指针变量只能存储其他内存单元的地址,我们借助于指针变量,可以实现内存空间的共享。
关于共享
-
共享他人的空间
cint a = 10; int *p = &a; int *q = p; // p和q共享a的空间
-
共享自己的空间
cint a = 10; int *p = &a; // a和p共享a的空间
指针
变量指针与指针变量
指针变量做函数参数
指针变量做函数参数往往传递的是变量的首地址,借助于指针变量间接访问是可以修改实参变量数据的。
指针有一个作用就是,通过形参修改实参,我们将这样的参数称之为输出型参数。
案例
需求:有a,b两个变量,要求交换后输出,使用函数处理,用指针变量做函数的参数
-
方式1:交换指向(指针指向改变,指向对象的数据不变)
代码:
#include <stdio.h>
/**
-
方式1:交换指向
*/
void swap(int *p_a, int *p_b)
{
int *p_t;
// 交换
p_t = p_a;
p_a = p_b;
p_b = p_t;
printf("交换后:%d,%d\n",*p_a, *p_b); // 交换后:4,3
}
int main(int argc,char *argv[])
{
int a = 3, b = 4;
printf("交换前:%d,%d\n", a, b);// 交换前:3,4
swap(&a, &b); // 传参的过程可以理解:int *p_a = &a, int *p_b = &b; return 0;
}
-
-
方式2:交换数据(指针指向不变,指向对象的数据改变)
代码:
#include <stdio.h>
/**
-
方式2:交换数据
*/
void swap(int *p_a, int *p_b)
{
int temp;
// 交换
temp = *p_a;
*p_a = *p_b; // 将p_b指向对象的值赋给p_a指向的对象
*p_b = temp; // p_b:访问指针变量的空间,*p_b:访问指针指向对象的空间
printf("交换后:%d,%d\n",*p_a, *p_b); // 交换后:4,3
}
int main(int argc,char *argv[])
{
int a = 3, b = 4;
printf("交换前:%d,%d\n", a, b);// 交换前:3,4
swap(&a, &b); // 传参的过程可以理解:int *p_a = &a, int *p_b = &b; return 0;
}
-
指针变量指向数组元素【重难点】
数组元素的指针
- 数组的指针就是数组中第一个元素的地址,也就是数组的首地址。
- 数组元素的指针是指数组的首地址。因此,同样可以用指针变量来指向数组或者数组元素。
- 在C语言中,由于数组名代表数组的首地址,因此数组名实际上也是指针。访问数组名就是访问数组首地址。
范例:
c
#include <stdio.h>
int main(int argc,char *argv[])
{
// 创建一个数组
int arr[] = {11,22,33};
int *p1 = &arr[0]; // 指针变量指向数组arr第一个元素,指针的范围就是数组元素
int *p2 = arr; // 等价于上面写法,数组名默认就是一个指向首元素地址的指针,推荐
printf("%p,%p,%p\n", p1, p2, arr); // 0x7ffe33135e2c,0x7ffe33135e2c,0x7ffe33135e2c
return 0;
}
注意:虽然我们定义了一个指针变量接收了数组地址,但不能理解为指针变量指向了数组,而应该理解为指向来了数组的元素(默认为第1个元素)。
指针的运算
指针运算:前提是指针变量必须要指向数组的某个元素。(指针运算只能在同一数组内进行,并且只能是元素之间的偏移)
序号 | 指针运算 | 偏移量 | 说明 |
---|---|---|---|
1 | 自增:p++、++p、p+=1 |
sizeof(type) |
指向下一个元素的首地址 需边界检测,防止越界 |
2 | 自减:p--、--p、p-=1 |
sizeof(type) |
指向上一个元素的首地址 需边界检测,防止越界 |
3 | 加n个数:p+n |
n * sizeof(type) |
指向后面n个元素的首地址 需边界检测,防止越界 |
4 | 减n个数:p-n |
n * sizeof(type) |
指向前面n个元素的首地址 需边界检测,防止越界 |
5 | 指针相减:p1 - p2 |
` | (p1 - p2) |
6 | 指针比较:p1 < p2 |
逻辑值:真(1),假(0) | 前面的指针小于后面的指针 |
注意:
-
上面表格中的type,是指针指向数组的元素的类型
-
sizeof不支持运算,举例:
c#include <stdio.h> int main(int argc,char *argv[]) { int a = 10; printf("sizeof(a)=%lu,sizeof(int)=%lu,sizeof(++a)=%lu\n", sizeof(a), sizeof(int), sizeof(++a)); // sizeof(a)=4,sizeof(int)=4,sizeof(a++)=4 return 0; }
说明:
① 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的 下一个元素,p-1指向同一数组中的上一个元素。即p+1或p-1也表示地址。但要注意的是,虽然指针变量p中存放的是地址,但p+1并不表示该地址加1,而表示在原地址的基础上加了该数据类型所占的字节数d(d = sizeof(数据类型)) 。
② 如果p原来指向a[0],执行++p后p的值改变了,在p的原值基础上加d,这样p就指向数组的下一个元素a[1]。d是数组元素占的字节数。
③ 如果p的初值为&a[0]则p+i 和a+i 就是数组元素a[i]的地址,或者说,它们指向a数组的第 i 个元素 。
④
*(p+i)
或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。⑤ 如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是两个地址之差除以数组元素的长度d。
案例
c
#include <stdio.h>
#include <math.h>
int main(int argc,char *argv[])
{
// 创建一个用来实现指针运算的数组
int arr[] = {11,22,33,44,55};
int *p1 = arr + 4; // 55 等价于 arr[4]
int *p2 = arr + 1; // 22 等价于 arr[1]
size_t size = fabs(p2 - p1); // 3 = fabs(22对应的地址 - 55对应的地址) / int的字节数
printf("*p1=%d,*p2=%d,size=%lu,&arr[1]+2=%d\n", *p1, *p2, size, *(&arr[1]+2));
return 0;
}
运行结果:
案例
-
需求:通过下标法和指针法遍历数组
-
代码:
c#include <stdio.h> /** * 下标法遍历数组 */ void arr1(int arr[], int len) // 数组作为函数参数,传递的是数组的首地址(数组被降级为指针) { for (register int i = 0; i < len; i++) printf("%-4d", arr[i]); printf("\n"); } /** * 指针法遍历数组 */ void arr2(int arr[], int len) { // 创建一个指针变量,接收数组,此时实际上接收到的是数组中第一个元素的地址 int *p = arr; for (register int i = 0; i < len; i++) printf("%-4d",/* *(arr+i) 等价于*/ *(p+i)); printf("\n"); } /** * 指针法遍历数组 */ void arr3(int arr[], int len) { int *p = arr; for (register int i = 0; i < len; i++) { printf("%-4d", *p); p++; } printf("\n"); } /** * 指针法遍历数组 */ void arr4(int arr[], int len) { int *p = arr; for(; p < arr + len; p++) // 判断的时候不能写作 p + len,因为p在变化,而arr没有变化 { printf("%-4d", *p); } printf("\n"); } int main(int argc,char *argv[]) { int arr[] = {11,22,33,44,55}; int len = sizeof(arr) / sizeof(arr[0]); arr1(arr, len); arr2(arr, len); arr3(arr, len); arr4(arr, len); return 0; }
案例
-
需求:推导以下代码的运行结果
-
代码:
c#include <stdio.h> int arr2() { // 创建一个普通数组 int arr[] = {11,22,33,44,55,66,77,88}; int *p = arr; printf("%d\n", *p); // 11 p++; // 指针偏移 1 * sizeof(int) 指针移动到22这个位置 printf("%d\n", *p); // 22 int x = *p++; // 第1步:解引用p的值赋值给x, x = 22; 第2步:p++,指针移动到33这个位置 printf("%d,%d\n", x, *p);// 22,33 int y = *(++p);// 第1步:++p,指针偏移到44这个位置;第2步:对44这个地址解引用,得到44 printf("%d,%d\n", y, *p);// 44,44 (*p)++; // 第1步:对p解引用得到44;第2步:对44这个值+1,得到45 printf("%d\n",*p); // 45 }
※小贴士:
*p++:先解引用p,然后p这个指针自增(指针自增)
cint arr[] = {11,22,33}, *p = arr; int x = *p++; // x=11,*p=22 ① *p解引用,其实就是将指向的对象a的值赋值给x ② 指针p++,也就是指针偏移一位
(*p)++:先解引用p,然后使用解引用出来的数据自增(数值自增)
cint arr[] = {11,22,33}, *p = arr; int x = (*p)++; // x=11,*p=12 ① *p解引用,其实就是将指向的对象a的值赋值给x ② 解引用出来的对象数据自增
通过指针引用数组元素
引用一个数组元素,可以用:
① 下标法:如arr[i]
② 指针法:如*(arr + 1)
或者*(p+i)
。其中arr是数组名,p是指向数组元素的指针变量,其初始值:p = arr;
案例
需求:
-
下标法:(通过改变下标输出所有元素)
c#include <stdio.h> int main() { int arr[10], i; // 给数组元素赋值 for (i = 0; i < 10; i++) scanf("%d", &arr[i]); // 遍历数组元素 for (i = 0; i < 10; i++) printf("%-4d", arr[i]); printf("\n"); return 0; }
-
指针法(地址):(通过数组名计算出数组元素的地址,找出数组元素值)
c#include <stdio.h> int main() { int arr[10], i; // 给数组元素赋值 for (i = 0; i < 10; i++) scanf("%d", &arr[i]); // 遍历数组元素 for (i = 0; i < 10; i++) printf("%-4d", *(arr + i)); printf("\n"); return 0; }
-
指针法(指针变量):(用指针变量指向数组元素)
c#include <stdio.h> int main() { int arr[10], i, *p; // 给数组元素赋值 for (i = 0; i < 10; i++) scanf("%d", &arr[i]); // 遍历数组元素 for (p = arr; p < (arr + 10); p++) printf("%-4d", *p); printf("\n"); return 0; }
注意:数组一旦创建,就无法改变其值。
以上3种写法比较:
- 第①种写法和第②种写法执行效率相同。系统是将arr[i]转换为*(arr+i)处理的,即先计算出地址,因此比较费时。
- 第③种方法比第①②种方法快。用指针变量直接指向数组元素,不必每次都重新计算地址。(p++)能大大提高执行效率。
- 用第①种写法比较直观,而用地址法或者指针变量的方法难以很快判断出当前处理的元素。
使用指针变量指向数组元素时(上面第③种写法),注意以下前两点:
①
*(p--) 相当于arr[i--],先*p,再p--;
*(p++) 相当于arr[i++],先*p,再p++;
②
*(--p) 相当于arr[--i],先--p,再*
*(++p) 相当于arr[++i],先++p,再*;
③
*p++ 先*p,再p++
④
(*p)++ 先*p,再*p++
具体关系参照下面表格:
操作类型 指针表达式 数组下标等价 执行顺序 指针移动方向 是否改变指针地址 前置自减+取值 *(--p)
arr[--i]
1. 指针前移 2. 取新地址的值 向前(←) √ 前置自加+取值 *(++p)
arr[++i]
1. 指针后移 2. 取新地址的值 向后(→) √ 后置自减+取值 *(p--)
arr[i--]
1. 取原地址的值 2. 指针前移 向前(←) √ 后置自加+取值 *(p++)
arr[i++]
1. 取原地址的值 2. 指针后移 向后(→) √ 后置自减(简写) *p--
arr[i--]
1. 取原地址的值 2. 指针前移 向前(←) √ 后置自加(简写) *p++
arr[i++]
1. 取原地址的值 2. 指针后移 向后(→) √ 取值后值自减 (*p)--
arr[i]--
1. 取原地址的值 2. 值-1 不移动 × 取值后值自加 (*p)++
arr[i]++
1. 取原地址的值 2. 值+1 不移动 ×
数组名作函数参数
① 形参和实参都是数组名
c
// arr 数组 形参
void fun(int arr[], int len){..}
void main()
{
int arr[] = {11,22,33};
int len = sizeof(arr) / sizeof(arr[0]);
// arr 数组 实参
fun(arr, len);
}
② 实参用数组名,形参用指针变量
c
// arr 指针 形参
void fun(int *arr, int len){..}
void main()
{
int arr[] = {11,22,33};
int len = sizeof(arr) / sizeof(arr[0]);
// arr 数组 实参
fun(arr, len);
}
③ 实参和形参都用指针变量
c
// arr 指针 形参
void fun(int *arr, int len){..}
void main()
{
int arr[] = {11,22,33};
int len = sizeof(arr) / sizeof(arr[0]);
// arr 指针 实参
int *p = arr;
fun(p, len);
}
④ 实参用指针,形参用数组名
c
// arr 数组 形参
void fun(int arr[], int len){..}
void main()
{
int arr[] = {11,22,33};
int len = sizeof(arr) / sizeof(arr[0]);
// arr 指针 实参
int *p = arr;
fun(p, len);
}
案例:
需求:将数组a中的n个整数按相反顺序存放(数组反转)
分析:

代码:
c
#include <stdio.h>
/**
* 数组的反转:下标法
*/
void inv1(int arr[], int len)
{
// 反转思路:第0个和最后一个交换,第1个和倒数第二个交换...
// 定义循环变量和临时变量
register int i = 0, temp;
// 遍历数组
for (; i < len/2; i++)
{
temp = arr[i];
arr[i] = arr[len-1-i];
arr[len-1-i] = temp;
}
}
/**
* 数组的反转:指针法
*/
void inv2(int *p, int len)
{
// 反转思路:第0个和最后一个交换,第1个和倒数第二个交换...
// 定义循环变量和临时变量
int *i = p, *j = p + len - 1, temp;
// 遍历数组
for (; i < j; i++, j--)
{
temp = *i;
*i = *j;
*j = temp;
}
}
/**
* 遍历数组
*/
void list(const int *arr, int len) // const int *arr = arr;
{
const int *p = arr; // 添加const之后,指针指向对象的值不变,指针指向可以改变
for (; p < arr + len; p++) printf("%-4d", *p); printf("\n");
}
int main(int argc,char *argv[])
{
int arr[] = {11,12,13,14,15};
int len = sizeof(arr) / sizeof(arr[0]);
list(arr, len);
inv1(arr, len);
list(arr, len);
inv2(arr, len);
list(arr, len);
return 0;
}
数组指针与指针数组
数组指针
定义
**概念:**数组指针是指向数组的指针(指针变量),本质上还是指针。
指针变量指向数组元素和数组指针的区别?
特点:
① 先有数组,再有指针
② 它指向的是一个完整的数组
一维数组指针
语法:
c
数据类型 (*指针变量名)[容量];
案例:
c
#include <stdio.h>
int main(int argc,char *argv[])
{
// 一维数组指针
int arr[] = {100,200,300};
int len = sizeof(arr) / sizeof(arr[0]);
// 定义一个数组指针(一维数组指针)
int (*p)[len] = &arr; // arr默认指向数组元素,&arr指向整个数组,需要注意的的是,它们表示的范围不同,地址相同
// p++:此时不能p++,否则会越界
printf("&arr=%p,arr=%p,&arr[0]=%p\n", &arr, arr, &arr[0]); // arr 等价于 &arr[0]
// 如何访问数组指针
printf("%d\n", (*p)[2]); // 300
// 遍历数组指针
for (int i = 0; i < len; i++) printf("%-6d", (*p)[i]); printf("\n");
return 0;
}
我们之前所学的是指向数组元素的指针,本质上是指针变量;现在我们学的是指向数组的指针,叫作数组指针。
二维数组指针
语法:
c
数据类型 (*指针变量名)[行容量][列容量];
案例:
-
写法1:二维数组指针指向二维数组【不推荐】
c#include <stdio.h> int main(int argc,char *argv[]) { // 创建一个二维数组 int arr[][3] = {10,20,30,100,200,300,1000,2000,3000}; // 定义一个二维数组指针指向二维数组 int (*p)[][3] = &arr; // 遍历数组 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%-6d", (*p)[i][j]); } } printf("\n"); return 0; }
-
写法2:一维数组指针指向二维数组【推荐】
c#include <stdio.h> int main(int argc,char *argv[]) { // 创建一个二维数组 int arr[][3] = {10,20,30,100,200,300,1000,2000,3000}; // 定义一个一维数组指针指向二维数组,相当于指针指向的是二维数组的行 [行容量] int (*p)[3] = arr; // 等价于 &arr[0] (*p):指向数组的行 int arr[] = {100, 200, 300}; int *p = arr; 解引用p 得到第一个元素 // 遍历数组 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%-6d", p[i][j]); // printf("%-6d", *(*(p+i)+j)); // printf("%-6d", (*(p+i))[j]); // printf("%-6d", *(p[i]+j)); } } printf("\n"); return 0; }