这篇笔记主要是介绍C语言中的指针,包括指针的重要性、定义、使用等。阅读本文预计需要 10 分钟。
Day06 郝斌C语言自学视频之C语言的指针
指针的重要性
指针是继流程控制
和函数
之后,又一个重点。可以说指针是 C 语言的灵魂
。
指针的重要性具体表现
- 表示一些复杂的数据结构。
- 快递的传递数据,减少内存的耗用。【重点】
- 使函数返回一个以上的值。【重点】
- 能直接访问硬件。
- 能够方便处理字符串。
- 是理解面向对象语言中引用的基础。
指针的定义
在介绍指针定义时,我们先看一下地址的概念。
地址:地址是内存单元的编号。它是从 0 开始的非负整数。对于32位系统,它的范围是:4 G [0–(4G-1)]。4 G即2^32。
指针:指针就是地址,地址就是指针。
指针变量:指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量。
指针和指针变量是两个不同的概念。但是要注意,通常我们叙述时,会把指针变量简称为指针,实际他们含义并不一样。
指针的本质就是一个操作受限的非负整数
。
指针的使用
基本类型指针
格式:
指针变量类型 指针变量名
例:
/*
时间:2020年2月24日13:23:59
功能:
测试基本类型指针的使用
*/
# include <stdio.h>
int main(void)
{
int * p;
int i = 3;
int j;
p = &i; // OK
j = *p;
printf("i = %d, j= %d\n", i, j);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
i = 3, j= 3
--------------------------
*/
说明:
对于语句**int * p;
**:
p
是变量的名字,int *
表示p
变量存放的是int
类型变量的地址。int * p;
不表示定义了一个名字为*p
的变量。int * p;
应该这样理解:p
是变量名,p
变量的数据类型是int *
类型。所谓int *
类型,实际就是存放int
变量地址的类型。
对于语句**p = &i;
**:
p
保存了i
的地址,因此p
指向i
p
不是i
,i
也不是p
, 更准确的说,修改p
的值不影响i
的值,修改i
的值,也不影响p
的值。- 如果一个指针变量指向了某个普通变量,则
*指针变量
就完全等同于普通变量。解释:如果 p 是个指针变量,并且 p 存放普通变量 i 变量的地址,则 p 指向了普通变量 i。*p
就完全等同于i
。或者说:在所有出现*p
的地方都可以替换成i
,在所有出现i
的地方都可以替换成*p
。*p
就是以p
的内容为地址的变量。
对于语句**j = *p;
**,就相当于j = i;
* 的含义:
- 乘法。
- 定义指针变量。
int * p;
定义了一个名字叫p
的变量,int *
表示p
只能存放int
变量地址。 - 指针运算符。该运算符放在已经定义好的指针前面,如果
p
是一个已经定义好的指针变量,则*p
表示以p
的内容为地址的变量。
如何通过被调函数修改主调函数普通变量的值
- 实参必须为该普通变量的地址。
- 形参必须为指针变量。
- 在被调函数中通过
* 形参名 = XXX
的方式就可以修改主调函数相关的变量。
例:
/*
时间:2020年2月24日14:45:35
功能:
通过被调函数修改主调函数普通变量的值
总结:
1. 实参必须为该普通变量的地址。
2. 形参必须为指针变量。
3. 在被调函数中通过 * 形参名 = XXX 的方式就可以修改主调函数相关的变量。
*/
# include <stdio.h>
void huhuan_1(int, int);
void huhuan_2(int *, int *);
void huhuan_3(int *, int *);
// 不能完成互换功能
void huhuan_1(int a, int b)
{
int t;
t = a;
a = b;
b = t;
return;
}
// 不能完成互换功能
void huhuan_2(int * p, int * q)
{
int * t; // 如果要互换 p 和 q 的值,则 t 必须是 int *, 不能是 int, 否则会出错
t = p;
p = q;
q = t;
return;
}
// 可以完成互换功能
void huhuan_3(int * p, int * q)
{
int t; // 如果要互换 *p 和 *q 的值,则 t 必须定义成 int 不能定义成 int *, 否则语法错误
t = *p; // p 是 int *, *p 是 int
*p = *q;
*q = t;
return;
}
int main(void)
{
int a = 3;
int b = 5;
// huhuan_1(a, b);
// huhuan_2(&a, &b); // huhuan_2(*p, *q); 是错误的, huhuan_2(a, b); 也是错误的
huhuan_3(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
a = 5, b = 3
--------------------------
*/
指针和数组
指针和数组分为指针和一维数组以及指针和二维数组。这里主要介绍指针和一维数组。
一维数组名:一维数组名是个指针常量。它存放的是一维数组第一个元素
的地址。
下标和指针的关系:如果 p
是个指针变量,则 p[i]
永远等价于 *(p+i)
确定一个一维数组需要几个参数:需要两个参数。数组第一个元素的地址和数组的长度。即:如果一个函数要处理一个一维数组,则需要接收该数组的第一个元素的地址和数组的长度。
例 指针和一维数组
/*
时间:2020年2月24日16:35:50
目的:
确定一个数组需要的参数
总结:
如果一个函数要处理一个一维数组,
则需要接收该数组的第一个元素的地址和数组的长度。
*/
# include <stdio.h>
void f(int * pArr, int len)
{
int i;
for (i=0; i<len; ++i)
printf("%d ", *(pArr+i));
/*
*(pArr+i) 等价于 pArr[i]
也等价于 b[i]
也等价于 *(b+i)
*/
printf("\n");
}
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {-1, -2, -3, 4, 5, -6};
int c[100] = {1, 99, 22, 33};
f(b, 6);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
-1 -2 -3 4 5 -6
--------------------------
*/
指针变量的运算
指针变量不能相加,不能相乘,也不能相除。如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减。
例 指针的运算
/*
时间:2020年2月24日16:57:27
指针的运算
*/
# include <stdio.h>
int main(void)
{
int i = 5;
int j = 10;
int * p = &i;
int * q = &j;
int a[5];
p = &a[1];
q = &a[4];
printf("p和q所指向的单元相隔%d个单元\n", q-p);
// p-q 没有实际意义
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
p和q所指向的单元相隔3个单元
--------------------------
*/
一个指针变量到底占几个字节【非重点】
预备知识sizeof()
函数:
用法一:
sizeof(数据类型)
功能:返回值就是该数据类型所占的字节数。
例:
sizeof(int) = 4
sizeof(char) = 1
sizeof(double) = 8
用法二:
sizeof(变量名)
功能:返回值是该变量所占的字节数。
假设 p
指向 char
类型变量(1 个字节)。
假设 q
指向 int
类型变量(4 个字节)。
假设 r
指向 double
类型变量(8 个字节)。
p q r
本身所占的字符数是否一样?
答案: p q r
本身所占的字符数是一样的。
总结: 一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占 4
个字节。一个变量的地址是用该变量的首地址表示的。
多级指针
指针的指针就是多级指针了。对于多级指针需要明白, p
是指针变量,如果 q
是 p
的指针,则 *q = p
。
例 多级指针
/*
时间:2020年2月24日21:45:22
多级指针的示例
*/
# include <stdio.h>
int main(void)
{
int i = 10;
int * p = &i;
int ** q = &p;
int *** r = &q;
// r = &p; // error 因为 r 是 int *** 类型,r只能存放int ** 类型的变量的地址
printf("%d\n", ***r);
printf("%d\n", **q);
printf("%d\n", *p);
return 0;
}
/*
在VSCode中的输出结果是:
--------------------------
10
10
10
--------------------------
*/
对于指针和函数
以及指针和结构体
这里先不介绍。
动态内存分配【重点难点】
传统数组的缺点
传统数组也叫静态数组。
-
数组长度必须事先指定,且只能是常整数,不能是变量。如:
int a[5]; // Ok
int len = 5; int a[len]; // error
-
传统形式定义的数组,该数组的内存程序员无法手动释放。数组一旦定义,系统为该数组分配的存储空间就会一直存在,除非数组所在的函数运行结束。在一个函数运行期间,系统为该函数中所分配的空间会一直存在,直到该函数运行完毕时函数的空间才会被系统释放。
-
数组的长度一旦定义,其长度就不能再更改。数组的长度不能在函数运行的过程中动态的扩充或缩小。
-
A 函数定义的数组,在 A 函数运行期间可以被其他函数使用,但 A 函数运行完毕之后,A 函数中的数组将无法被其他函数使用。即:
传统定义的函数不能跨函数使用
。
为什么需要动态分配内存
动态数组的创造就是为了解决静态数组的 4 个缺陷。
静态内存 VS 动态内存的比较
区别 | 静态内存 | 动态内存 |
---|---|---|
内存分配 | 系统自动分配 | 程序员手动分配 |
内存释放 | 系统自动释放 | 程序员手动释放 |
内存分配位置 | 在栈 中分配 | 在堆 中分配 |
动态分配内存举例——动态数组的构造
假设需要动态构造一个 int
型一维数组。
int * p = (int *)malloc(int len);
-
本语句一共分配了两块内存,一块是动态分配的,总共
len
个字节,一个是静态分配的是 4 个字节,即变量p
本身所占的内存。 -
malloc()
只有一个int
型的形参,表示要求系统分配的字节数。 -
malloc()
函数的功能是请求系统分配len
个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回NULL
。
malloc()
函数只能返回第一个字节的地址,所以我们需要把这个无任何实际意义的第一个字节的地址(俗称干地址)转化成一个有实际意义的地址,因此 malloc
前面必须加 (数据类型 *)
,表示把这个无实际意义的第一个字节的地址转化为相应类型的地址。
如:
int * p = (int *)malloc(5);
表示将系统分配好的50个字节的第一个字节的地址转化为 int *
型的地址,更准确的说是把第一个字节的地址转化为 4 个字节的地址,这样 p
就指向了第一个的 4 个字节, p+1
就指向了第二个的 4 个字节,p+i
就指向了第 i+1
个的 4 个字节,p[0]
就是第一个元素, p[i]
就是第 i+1
个元素。
double * p = (double *)malloc(80);
表示将系统分配好的80个字节的第一个字节的地址转化为 double *
型的地址,更准确的说是把第一个字节的地址转化为 8 字节的地址,这样 p
就指向了第一个的 8 个字节, p+1
就指向了第二个的 8 个字节,p+i
就指向了第 i+1
个的 8 个字节,p[0]
就是第一个元素, p[i]
就是第 i+1
个元素。
freep(p);
表示将 p
所指向的内存给释放掉,p
本身的内存是静态的。不能由程序员手动释放,p
本身的内存只能在 p
变量所在的函数运行终止时由系统自动释放。
例 动态数组的构造
/*
时间:2020年2月26日21:15:46
动态数组的构造
*/
# include <stdio.h>
# include <malloc.h>
int main(void)
{
int a[5]; // 如果int 占4个字节的话,则本数组总共包含有20个字节,每四个字节被当做了一个int变量来使用
int len;
int * pArr;
int i;
printf("请输入你要存放元素的个数:");
scanf("%d", &len); //
pArr = (int *)malloc(4 * len); // 本行动态的构造了一个一维数组,该数组的长度是len,该数组的数组名是pArr,该数组的每个元素是int 整型。类似于 int pArr[len];
// 对一维数组进行操作, 如:对一维数组进行赋值
for (i=0; i<len; ++i)
scanf("%d", &pArr[i]);
// 对一维数组进行输出
printf("一维数组的内容是:\n");
for (i=0; i<len; ++i)
printf("%d\n", pArr[i]);
free(pArr); // 释放掉动态分配的数组
return 0;
}
跨函数使用内存的问题
静态内存不可以跨函数使用。或者说是:静态内存在函数执行期间可以被其他函数使用,但是在函数执行完毕后就不能再被其他函数使用了。
动态内存可以跨函数使用。动态内存在函数执行完毕之后仍然可以被其他函数使用。
【说明】
-
本学习笔记整理自B站郝斌老师的《郝斌C语言自学教程》片段P121-P150。
-
笔记中所有代码均在windows10操作系统,在VSCode编辑器中通过测试。具体VSCode C语言开发环境搭建方法请参照我的另一篇CSDN博客——Windows10下利用Visual Studio Code搭建C语言开发环境。
后记
如果对你有所帮助,欢迎关注我的公众号。这个公众号主要是慢慢分享和记录自己学习编程的笔记,比如:C,Python,Java等。后续也会分享自己面试以及在职场上的成长心得。