C语言学习!
目录
前言
一、指针是什么?
1.1 指针变量
1.2 取地址运算符和间接寻址运算符
二、指针变量的大小
三、指针和指针类型
四、指针和函数
五、野指针
5.1 野指针成因
1. 指针未初始化
2. 指针越界访问
3. 指针指向的空间释放
5.2 如何规避野指针
六、指针运算
6.1 指针+- 整数
6.2 指针-指针
6.3 指针的关系运算
七、指针和数组
7.1 指针与数组的关系
7.2 指针运算符和下标运算符
7.3 数组传参
八、二级指针
九、指针数组
9.1 一维数组
9.2 二维数组
总结
前言
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址。
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
一、指针是什么?
1.1 指针变量
整个内存空间是切割成一个个1byte(字节)大小的内存单元并编号来管理的,把内存单元的编号称为地址,地址也叫指针。存放指针(地址)的变量就是指针变量,通过指针变量里存放的地址,可以找到对应的内存单元。
就好比一个人国家划分成许多省,每个省都有邮政编码,根据邮政编码可以找到对应地址的省,而指针变量就像是记录邮政编码的小本子。
- 内存单元有编号,而这个编号其实就是地址,地址被称为指针。
- 存放指针(地址)的变量就是指针变量。
- 把内存单元的编号就称为指针。
- 指针其实就是地址,地址就是编号。
- 指针就是内存单元的编号。
总结:指针就是地址,口语中说的指针通常指的是指针变量。
代码示例:
#include <stdio.h>int main()
{int a = 10;//a是整型变量,占用四个字节的内存空间,存储10int* pa = &a;//pa是一个指针变量,用来存放地址的return 0;
}
调试窗口:
a变量里存放着10,a的地址是0x008ffd80,&a的意思是取得a的地址,运算符&的功能就是取得对象的地址。pa变量中存放的是a的地址0x008ffd80,而pa前面的int表示 pa 指向的对象是int类型的,*说明 pa 是指针变量。
这里注意:int 整形变量占4个字节的内存,&a取出的是a所占4个字节的第一个字节的地址存放在pa变量中,pa指针变量中存的就是a的首地址。
总结: 指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)
1.2 取地址运算符和间接寻址运算符
C语言为指针的使用提供了一对运算符。为了找到变量的地址,可以使用 & 取地址运算符。如果 a 是变量,那么 &a 就是 a 在内存中的地址。为了获得对指针所指向对象的访问,可以使用 * 间接寻址运算符。如果 p 是指针,那么 *p 表示 p 当前指向的对象。
只要 p 指向 a ,*p 就是 a 的别名。
代码示例:
#include <stdio.h>int main()
{int a = 0;int* p = &a;*p = 10;return 0;
}
运行结果:
*为解引用操作符,*p意思就是通过p中存放的地址,找到p所指向的对象(此时*p就是a)
&a:找到a的地址;*p找回对象a。
二、指针变量的大小
指针的大小是由硬件机器决定的:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
在64位的机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。
总结:
- 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
代码示例:
#include <stdio.h>
int main()
{char* pc = NULL;short* ps = NULL;int* pi = NULL;double* pd = NULL;printf("%zu\n", sizeof(pc));printf("%zu\n", sizeof(ps));printf("%zu\n", sizeof(pi));printf("%zu\n", sizeof(pd));return 0;
}
4
4
4
4
不管什么类型的指针都是在创建指针变量,指针变量是用来存放地址的。指针变量的大小取决于一个地址存放的时候需要多大空间。
三、指针和指针类型
指针的定义方式是: type + * 。
- char* 类型的指针是为了存放 char 类型变量的地址。
- short* 类型的指针是为了存放 short 类型变量的地址。
- int* 类型的指针是为了存放 int 类型变量的地址。
代码示例1:
#include <stdio.h>int main()
{int a = 0x11223344;int* pa = &a;*pa = 0;return 0;
}
调试窗口:
代码示例2:
#include <stdio.h>int main()
{int a = 0x11223344;char* pa = &a;*pa = 0;return 0;
}
调试窗口:
结论:
指针类型决定了指针在被解引用的时候访问几个字节。
- int*的指针,解引用访问4个字节
- char*的指针,解引用访问1个字节
代码示例:
#include <stdio.h>int main()
{int a = 0x11223344;int* pa = &a;char* pc = &a;printf("pa=%p\n", pa);printf("pa+1=%p\n", pa+1);printf("pc=%p\n", pa);printf("pc+1=%p\n", pc+1);return 0;
}
运行结果:
pa=008FF774
pa+1=008FF778
pc=008FF774
pc+1=008FF775
结论:
- 指针类型决定了指针+1或-1操作的时候,跳过几个字节。
- 指针的类型决定了指针的步长。
代码示例1:
#include <stdio.h>
int main()
{int a = 0;int* pi = &a;float* pf = &a;*pi = 100;return 0;
}
调试窗口:
代码示例2:
#include <stdio.h>
int main()
{int a = 0;int* pi = &a;float* pf = &a;*pf = 100.0;return 0;
}
调试窗口:
int* 和 float* 不能通用
- pi 解引用访问4个字节,pi+1 也是跳过4个字节。
- pf 解引用访问4个字节,pf+1 也是跳过4个字节。
但整型数字和浮点型小数在内存中的存储方式不同,所以在监视内存中的结果也不同,也论证 int* 和 float* 不能通用。
四、指针和函数
指针的一个重要作用就是作为函数参数使用。
代码示例:
#include <stdio.h>void text(int* y)
{*y = 20;
}
int main()
{int a = 0;text(&a);printf("a=%d", a);return 0;
}
运行结果:
a=20
通过函数调用表达式 text(&a) ,调用函数 text 时, text 函数中形参 y 被声明为指向 int 型变量的指针变量。函数被调用时,将 &a 复制到 y 中,指针 y 便指向了a。
由于在指针前加上指针运算符 * ,就可以显示该指针指向的对象。因此 *y 时 a 的别名。对 *y 赋值,也就是对 a 赋值,所以即使从 text 函数返回 main 函数,a 中保存的依然是180。
若要在函数中修改变量的值,就需要传入指向该变量的指针,即告诉程序:传入的是指针,请对该指针指向的对象进行处理。
只要在被调用的函数里的指针前写上指针运算符 * ,就能间接地处理该指针指向的对象。这也是 * 运算符又被称为间接访问运算符的原因。另外,通过在指针前写上指针运算符 * 来访问该指针指向的对象,称为解引用。
这里可以对照函数的传址调用加深理解 C语言 函数-CSDN博客
计算和与乘积
代码示例:
#include <stdio.h>void sum_mul(int x, int y, int* Sum, int* Mul)
{*Sum = x + y;*Mul = x * y;
}
int main()
{int a = 3;int b = 6;int sum = 0;int mul = 0;sum_mul(a, b, &sum, &mul);printf("sum=%d\n", sum);printf("mul=%d\n", mul);return 0;
}
运行结果:
sum=9
mul=18
调用函数 sum_mul 时,会将 sum 和 mul 的地址复制给形参 Sum 和 Mul 。因此 *Sum 就是 sum 的别名,*Mul 就是 mul 的别名。在函数体中,将求得的和赋值给 *Sum,将求得的乘积赋值给 *Mul 这就相当于给 sum 和 mul 进行赋值,因此从 sum_mul 函数返回到 main 函数之后,和与乘积也分别存储在 sum 和 mul 中了。
五、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
5.1 野指针成因
1. 指针未初始化
代码示例1:
#include <stdio.h>int main()
{int* p;*p = 10;return 0;
}
- p没有初始化,就意味着没有明确指向。
- 一个局部变量不初始化时,放的是随机值:0xcccccccc
- *p = 10; 这里非法访问内存了,这里的p就是野指针。
2. 指针越界访问
代码示例2:
#include <stdio.h>int main()
{int arr[10] = { 0 };int* p = &arr;int i = 0;for (i = 0; i <= 10; i++){*p = i;p++;}return 0;
}
当指针指向的范围超出数组arr的范围时,p就是野指针。
3. 指针指向的空间释放
代码示例3:
#include <stdio.h>int* text()
{int a = 10;return &a;
}
int main()
{int* p = text();return 0;
}
调用text函数,返回变量a的地址给指针p。
a是局部变量,他所占的空间一旦出了text函数就销毁了,就是将a所占的空间还给了操作系统。所以返回地址的同时,a所占空间销毁。
p在存a的地址的时候,地址所对应的空间已经销毁。p虽然可以通过地址找到对应空间,但是p不能访问和使用这块空间。此时p就是野指针。
5.2 如何规避野指针
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
代码示例:
#include <stdio.h>int main()
{int* p = NULL;int a = 10;p = &a;if (p != NULL){*p = 100;}return 0;
}
六、指针运算
6.1 指针+- 整数
如果 p 指向数组元素 arr[ i ] ,那么 p+j 指向 arr[ i+j ]。
如果 p 指向数组元素 arr[ i ] ,那么 p-j 指向 arr[ i-j ]。
代码示例1:
#include <stdio.h>int main()
{int arr[10] = { 0 };int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;for (i = 0; i < sz; i++){*p = 1;p++;}return 0;
}
调试窗口:
代码示例2:
#include <stdio.h>int main()
{int arr[10] = { 0 };int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;for (i = 0; i < sz; i++){*(p + i) = 1; }return 0;
}
调试窗口:
6.2 指针-指针
当两个指针相减时,结果为指针之间的距离,也就是两指针之间数组元素的个数。因此,若 p 指向 arr[ i ] 且 q 指向 arr[ j ],那么 p - q 就等于 i - j 。
代码示例:
#include <stdio.h>int main()
{int arr[10] = { 0 };printf("%d\n", &arr[9]-&arr[0]);return 0;
}
运行结果:
9
- 指针-指针的绝对值得到的是指针和指针之间元素个数。
- 不是所有指针都能相减,指向同一块空间的2个指针才能相减。
代码示例:
#include <stdio.h>int my_strlen(char* str)
{char* start = str;while (*str != '\0'){str++;}return (str - start);
}
int main()
{int len = my_strlen("abcdefg");printf("%d\n", len);return 0;
}
运行结果:
7
6.3 指针的关系运算
可以用关系运算符( < 、<= 、> 、>= )和判等运算符(== 、!=)进行指针比较。
代码示例:
#include <stdio.h>#define N_VALUES 10
int main()
{int values[N_VALUES] = { 0 };int* vp = NULL;for (vp = &values[N_VALUES]; vp > &values[0];){*--vp = 1;}return 0;
}
上面代码可以简化成下方代码吗?
#include <stdio.h>#define N_VALUES 10
int main()
{int values[N_VALUES] = { 0 };int* vp = NULL;for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--){*vp = 1;}return 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
七、指针和数组
7.1 指针与数组的关系
- 数组:一组相同类型元素的集合。
- 指针变量:是一个变量,存放的是地址。
- 数组可以通过指针访问。
arr 是数组,表达式 arr 的值就与 arr[0] 的地址,即 &arr[0] 一致,如果数组 arr 的元素类型为 int 型,那么不管元素个数是多少,表达式 arr 的类型就是 int* 型。
代码示例:
#include <stdio.h>int main()
{int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };int* p = arr;int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++){printf("&a[%d] = %p p+%d = %p\n", i, &arr[i], i, p + i);}return 0;
}
运行结果:
&a[0] = 00AFFEAC p+0 = 00AFFEAC
&a[1] = 00AFFEB0 p+1 = 00AFFEB0
&a[2] = 00AFFEB4 p+2 = 00AFFEB4
&a[3] = 00AFFEB8 p+3 = 00AFFEB8
&a[4] = 00AFFEBC p+4 = 00AFFEBC
&a[5] = 00AFFEC0 p+5 = 00AFFEC0
&a[6] = 00AFFEC4 p+6 = 00AFFEC4
&a[7] = 00AFFEC8 p+7 = 00AFFEC8
&a[8] = 00AFFECC p+8 = 00AFFECC
&a[9] = 00AFFED0 p+9 = 00AFFED0
代码声明了数组 arr 和指针 p ,指针 p 的初始值是 arr ,因为数组名 arr 会被解释为 &arr[0],所以存入 p 的值为 &arr[0] 的值,也就是指针 p 会被初始化为指向数组 arr 的起始元素 arr[0]。
由运行结果也可看出,指向个元素的指针 p+i 和 &arr[i] 是等价的。 &arr[i] 是指向元素 arr[i] 的指针,其值是 arr[i] 的地址。
指向数组元素的指针,有以下规则成立:
指针 p 指向数组中的元素 e 时
- p+i 为指向元素 e 后第 i 个元素的指针。
- p-i 为指向元素 e 前第 i 个元素的指针。
数组名在以下两种情况下不被视为指向起始元素的指针:
1.作为 sizeof 运算符的操作数出现时。
sizeof(数组名) 不会生成指向起始元素的指针的长度,而是生成数组整体的长度。
2.作为取地址运算符 & 的操作数出现时。
& 数组名 不是指向起始元素的指针的指针,而是指向数组整体的指针。
7.2 指针运算符和下标运算符
指向数组内部元素的指针 p+i 前加指针运算符 *
因为 p+i 是指向 p 所指元素后第 i 个元素的指针,所以在其前加上指针运算符后得到的 *(p+i) 就是该元素的别名。若 p 指向 a[0] ,那么表达式 *(p+i) 就表示 a[i] 本身。
指针 p 指向数组中的元素 e 时,
- 指向元素 e 后第 i 个元素的 *(p+i),可以写成 p[i]。
- 指向元素 e 前第 i 个元素的 *(p-i),可以写成 p[-i]。
代码示例:
#include <stdio.h>int main()
{int i = 0;int arr[5] = { 1,2,3,4,5 };int* p = arr;//arr是首元素地址int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){printf("arr[%d] = %d *(arr+%d) = %d p[%d] = %d *(p+%d) = %d\n", i, arr[i], i, *(arr + i), i, p[i], i, *(p + i));}for (i = 0; i < sz; i++){printf("&arr[%d] = %p arr+%d = %p &p[%d] = %p p+%d = %p\n", i, &arr[i], i, (arr + i), i, &p[i], i, (p + i));}return 0;
}
运行结果:
arr[0] = 1 *(arr+0) = 1 p[0] = 1 *(p+0) = 1
arr[1] = 2 *(arr+1) = 2 p[1] = 2 *(p+1) = 2
arr[2] = 3 *(arr+2) = 3 p[2] = 3 *(p+2) = 3
arr[3] = 4 *(arr+3) = 4 p[3] = 4 *(p+3) = 4
arr[4] = 5 *(arr+4) = 5 p[4] = 5 *(p+4) = 5
&arr[0] = 004FFD38 arr+0 = 004FFD38 &p[0] = 004FFD38 p+0 = 004FFD38
&arr[1] = 004FFD3C arr+1 = 004FFD3C &p[1] = 004FFD3C p+1 = 004FFD3C
&arr[2] = 004FFD40 arr+2 = 004FFD40 &p[2] = 004FFD40 p+2 = 004FFD40
&arr[3] = 004FFD44 arr+3 = 004FFD44 &p[3] = 004FFD44 p+3 = 004FFD44
&arr[4] = 004FFD48 arr+4 = 004FFD48 &p[4] = 004FFD48 p+4 = 004FFD48
- p+i 指向 arr[i] ,所以 *(p+i) 是 arr[i] 的别名。
- *(p+i) 可以写为 p[i] ,所以 p[i] 也是arr[i] 的别名。
- 数组名 arr 是指向起始元素 arr[0] 的指针,所以 arr+i 就是指向元素 arr[i] 的指针。
- 指针 arr+i 指向元素 arr[i] ,所以在其前写上指针运算符后得到的 *(arr+i) 就是 arr[i] 的别名。
访问各元素的4个表达式: arr[i] 、*(arr + i) 、 p[i] 、*(p + i) 。
指向各元素指针的4个表达式: &arr[i] 、arr + i 、&p[i] 、p + i 。
总结:
int* 型指针 p 指向 int 型数组 arr 的起始元素 arr[0] 时,指针 p 的行为就和数组 arr 本身一样。
这里的类型可以是 char 、double、int 等等。
7.3 数组传参
数组传参传的是首元素地址。
数组名传参代码示例:
#include <stdio.h>void text(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
int main()
{int arr[10] = { 0 };text(arr, 10);return 0;
}
运行结果:
0 0 0 0 0 0 0 0 0 0
指针传参代码示例:
#include <stdio.h>void text(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}int main()
{int arr[10] = { 0 };text(arr, 10);return 0;
}
运行结果:
0 0 0 0 0 0 0 0 0 0
上述两个代码等效。
八、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址也可以存放在指针里。 就是二级指针。
代码示例:
#include <stdio.h>int main()
{int a = 0;int* pa = &a;int** ppa = &pa;**ppa = 6;printf("%d", a);return 0;
}
运行结果:
6
pa是一个指针变量,一级指针变量。
ppa是一个二级指针变量。
对ppa解引用 *ppa ,找到 pa;再解引用 **ppa 才可以找到 a。
int* pa = &a;
pa前面的int表示 pa 指向的对象是int类型的,*说明 pa 是指针变量。
int** ppa = &pa;
int* 是说明 ppa 指向的对象是 int* 类型,后面的 * 是说明 ppa 是指针。
总结:
二级指针变量是用来存放一级指针变量的地址的。
九、指针数组
存放指针的数组就是指针数组。
9.1 一维数组
代码示例:
#include <stdio.h>int main()
{int arr[5] = { 0,1,2,3,4 };int* parr[5] = { &arr[0],&arr[1], &arr[2], &arr[3], &arr[4] };int i = 0;for (i = 0; i < 5; i++){printf("%d ", *(parr[i]));}return 0;
}
运行结果:
0 1 2 3 4
9.2 二维数组
普通二维数组的使用代码示例:
#include <stdio.h>int main()
{int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 4; j++){printf("%d ", arr[i][j]);}printf("\n");}return 0;
}
运行结果:
1 2 3 4
2 3 4 5
3 4 5 6
二维指针数组代码示例:
#include <stdio.h>int main()
{int arr1[4] = { 1,2,3,4 };int arr2[4] = { 2,3,4,5 };int arr3[4] = { 3,4,5,6 };int* parr[3] = { arr1,arr2,arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 4; j++){printf("%d ", parr[i][j]);}printf("\n");}return 0;
}
运行结果:
1 2 3 4
2 3 4 5
3 4 5 6
parr 指针数组指向3个数组 arr1、arr2、arr3 的数组首元素地址 arr1[0]、 arr2[0]、 arr3[0]。
parr 因为是指向 int 型数组的指针,所以 parr 类型为 int* 。
指针数组 parr 把三个独立的一维数组 arr1、arr2、arr3 串在一起管理。可以运行出上述普通二维数组的代码结果。
注:在 printf 函数打印数组时操作符 [ ] 就相当于解引用操作符 * ,arr[i] 等价于 *(arr+i) 。
总结
以上就是今天要讲的内容,本文介绍了指针的内容。