C与指针
各位好我是栖夜。今天为大家介绍一下C语言里的指针
指针
内存地址与存储空间
- 存储空间里的内存单元是宿舍楼里的一个个小房间,房间是专门用来存储数据的
- 地址如果房间编号,根据这个编号我们可以找到对应的宿舍房间
变量地址
变量地址 是系统分配给变量的内存单元的起始地址
不同数据类型字节长度不同,因此可以理解为 从某个变量的起始地址到尽头所占的内存单元个数不同,比如:
int a = 1; // int 占用4个字节
char b = 'x'; // char 占用1个字节
double c = 1.1; // double 占用8个字节
// ...
指针
- 计算机中所有数据都存储在内存单元中,每个内存单元都有一个对应的地址,只要通过这个地址就能够找到对应单元中存储的数据,就像你通过1102房间号 (指针) 就能找到宿舍楼中的我 (数据) 一样
- 由于通过地址就能找到所需的变量单元,所以该地址指向了该变量单元,这个起了指路作用的地址被称为指针。因此指针就是存储地址的变量
- 内存单元的指针(所指向的地址)和内存单元里面存的内容是两个不同的概念,1102门牌号和1102里面住的我是不同的概念
指针变量及初始化
C语言中,允许用一个变量来存放其他变量的地址,它们被称为指针变量。指针变量首先是变量,并且它的存储内容是一个指针。
我们定义指针变量时需要确定它的指针类型。某种类型的变量需要用相同类型的指针存储其地址,因为类型说明符表示了这个指针变量所指向的变量的数据类型
// 定义一个指针变量p并且把变量a的地址赋值给p
int a = 1;
int *p;
p = &a; // 先定义指针变量后初始化
char b = 'x';
char *q = &b; // 定义指针变量的同时初始化
注意:int *p
中,指针变量名是p,而不是*p
这是两种不合法的初始化:
int *p;
p = 1; // 指针变量只能存储地址,不能存储其他类型
int *q;
*q = &a; // 指针变量赋值时前面加*是非法的
注意:
-
允许有多个指针变量指向同一个地址
-
指针指向的地址允许被修改
int a = 1,
b = 2;
int *p = &a;
p = &b; // 该指针指向的地址被修改了
- void* (指针变量) 在64位系统中字长为8,但是具体某个指针变量占多少个字节的存储空间要看其被声明时的指针类型,也就是指针变量需要它所指向的数据类型告诉他要访问多少个字节的存储空间。比如
int *p = &a;
中,由于是int类型的指针,所以编译器知道要从保存的地址开始取4个字节
&和*
- &:该单目运算符用来取操作对象的地址。取址操作不涉及内存。另外由于常量表达式和寄存器变量(register)存储在存储器中,没有地址,所以&无法取这两者的地址
- *:间接寻址符,作用是通过操作对象的地址,获取其存储的内容,也就是访问指针所指向的存储空间
- 因此,*与&互为逆运算
用代码查看它们的实现过程:
int a = 1
int *p = &a; // 通过&获取a的地址并赋值给声明的指针变量p
printf("%d", *p); // p中存的已经是a的地址,因此通过间接寻址符能够获取指针p指向的内容
&和*都是右结合的,这意味着*&p
的含义是:先取p的地址、再取p的地址中存的内容,因此p == *&p
这是一个小程序:输入两个整数ab,然后将其中较大的值赋值给a,较小的值赋值给b
void main()
{
int a, b;
int *p = &a, *q = &b;
scanf("%d, %d\n", &a, &b); // 输入两个整型,分别赋值给a和b
int temp;
if(*p < *q) // if (p指向的地址的内容 > q指向的地址的内容)
{
temp = *p;
*p = *q;
*q = temp;
}
printf("a = %d, b = %d\n", *p, *q); // 输出p指向的值和q指向的值
}
// 假如我输入45,48 会打印 a=48, b=45
另外,指针变量也可以被赋值NULL或0,如果赋值0,此时0的含义是NULL的字符码值
野指针
指针没有初始化时,里面是一个垃圾值,我们称这是一个野指针。野指针可能导致程序崩溃,也会访问你不该访问的数据
为了避免野指针的出现,指针必须初始化才可以访问其所指向的区域
int *p;
和int a;
一样,都会产生一些垃圾:
指针运算
- 赋值运算
int *pa, *pb, *pc;
int x = 1;
pa = &x; // 赋予某个变量的地址
pb = pa; // 指针变量相互赋值
pc = 0ff13; // 赋值具体的地址
- 指针与整数加减运算
指针加/减n,表示指针向前/后移动n个单元 (不同类型的指针单元长度不同)
- 关系运算
px > py : px指向的存储地址是否大于py指向的存储地址
px == py:px和py是否指向同一个存储单元
px == 0 / px != 0:px是否为空指针
多级指针
如果一个指针变量存放的内容是另一个指针变量的地址,则这个指针变量被称为二级指针。依次叠加,形成了多级指针
二级指针定义:数据类型 **二级指针名;
int *pa, a = 1;
int **ppa;
pa = &a;
ppa = &pa;
// 我们可以直接用二级指针做普通指针的操作
printf("i = %d\n", **ppa);
printf("the address of i = %d\n", *ppa); // &**ppa == *ppa
注意: 在初始化二级指针时,不能直接 ppa = &&a,因为&i获取的是一个具体的数值,而具体数值是没有指针的
数组指针
指针访问数组
以前我们通过下标访问数组元素,现在可以通过指针访问数组元素
-
数组中每个元素有相应的地址,指针变量也可以保存数组元素的地址,这样的指针是数组元素指针
-
数组名不代表整个数组,数组名只代表该数组首个元素的地址
int a[3] = {1, 2, 3}; int *p; p = &a[0]; // 等价于 p=a // p=a 的作用是 把a数组的首元素的地址赋给指针变量p
操作数组指针
-
*p = 1
: 赋值操作 将指针指向的存储空间赋值为1。假如p指向数组arr的第1个元素,则此操作为arr[0] = 1
-
p + 1
或p++
: 指针与整数相加减 此时p+1指向arr[0]的下一个元素arr[1]。p++、++p等自增自减操作原理类似。**通过 p + 整数 可以移动到想要操作的元素,p + i
即指向arr[i]。注意: p + 整数的操作要考虑边界的问题,即如果arr.length=2,p+3对于数组操作来说没有意义
因此,访问数组元素可以用下面两种方法
- 下表法 如
arr[i]
- 指针法 如
*(p+i)
注意:数组名虽然是数组的首地址,但数组名所保存的数组的首地址是不可更改的。因为数组名不等价于指针变量,指针变量能进行p++和&操作,而这些操作对于数组名是非法的。数组名在编译时是确定的,在程序运行期间算一个常量
int a[3];
a++; // 非法
int *p = a;
p++; // 合法
demo: 用指针访问数组元素
int nums[5] = {4, 2, 7, 9, 13};
int *p = nums; // 将数组nums的首地址赋给p
printf("nums[0] = %d, nums[1] = %d\n", *p, *(p + 1)); // 访问数组nums的第一个元素和第二个元素
int i;
for(i=0; i<5; i++) {
printf("nums[%d] = %d", i, *(p + i)); //遍历数组并输出
}
字符数组与字符指针
C语言中没有提供字符串的数据类型,但是可以通过字符数组和字符指针的方式存储字符串
字符数组方式:
char str[] = "LeonardoSya";
printf("%s", str);
字符指针方式:
char *str = "LeonardoSya";
printf("%s", str); // 从str指向的起始地址一直打印到'\0'的地址
printf("%c", str[3]); // 输出n 通过下标取字符
printf("%c", *(str + 3); // 输出n 通过字符指针取字符
printf("%d", strlen(str)); // 获取字符串长度
(字符串方法详见笔者的上篇博客https://blog.csdn.net/m0_74861839/article/details/129552384?spm=1001.2014.3001.5501)
遍历字符串:
char *str = "LeonardoSya";
int i;
for(i=0; i<strlen(str); i++) {
printf("%c", *(str+i)); // 输出LeonardoSya
}
// 当然 这种方法与直接 printf("%s", str); 输出结果一致
注意:
-
字符数组不能通过数组名自增操作,而字符指针可以自增操作
-
字符指针不可修改字符串内容
*(str+2) = 'y'
是非法的。因为使用字符数组来保存的字符串是保存栈里的,保存栈里的数据可读写,所以可以修改字符串中的内容;而使用字符指针来保存字符串时,它保存的是字符串常量地址,常量区是只读的,所以用字符指针对字符串中的字符修改非法
-
字符指针不能够直接接收键盘输入。请看下面这个test
char *str; scanf("%s", str); // 报错
报错的原因: str是一个野指针,它没有指向某一块内存空间,因此字符指针直接接收键入字符是非法的。如果给str分配了空间,则可接收键入
指针数组
(注意区分数组指针和指针数组)
指针数组
指针变量和普通变量一样,也能组成数组
// 指针数组定义: type *name[length];
int arr[5] = {2, 5, 7, 1, 3};
int *p[5],
**pp,
i;
for(i=0; i<5; i++) {
p[i] = &arr[i]; // 遍历数组给指针赋值
}
pp = p; // p[]指针数组的首地址赋给二级指针pp 指针数组的数组名为首个指针的指针
// pp == &p[0] == &&arr[0] , **pp == *p[0] == arr[0]
for(i=0; i<5; i++) {
printf("%d", **pp); // 输出257
pp++;
}
指针与多维数组
多维数组和二维数组没有本质区别,但复杂度大幅提高
多维数组的地址
三维数组的实际存储形式如下:
实际存储内容为最内层纬度,且为连续的。a的跨度为4个单元,a[0]的跨度为2个单元,以此类推
易得:a == a[0] == a[0][0] == &a[0][0][0]
(仅数值相等)
多维数组的指针
用指针表示一维数组:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
用指针操作多维数组:
int arr[2][2] = {
{1, 2},
{3, 4}
};
int *p[2] = {arr[0], arr[1]}; // arr[0]\arr[1]各为一个数组
printf("arr[0][0] = %d", **p); // *p == arr[0] **p = arr[0][0]
printf("arr[1][0] = %d", **(p+1)); // *(p+1) == arr[1]
printf("arr[0][1] = %d", *(*p+1)); // *p+1 == &arr[0][1]
指针函数
函数在内存中也要占据部分存储空间,它也有一个起始地址。因此我们可以用一个指针指向一个函数,其中,函数名就代表函数的地址。
指针函数能够起到调用函数、将函数作为参数在函数间传递的作用
指针作为 函数的参数
// 交换两个变量的内容
#include <stdio.h>
void fn(int *x, int *y); // 指针是fn的形参
void main()
{
int x = 1, y = 2;
fn(&x, &y);
printf("x = %d, y = %d", x, y); // x=2, y=1
}
void fn(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp; // 实现了两个指针各自指向的值的交换(地址没改 改的是地址上的值)
}
指针作为 函数的返回值
返回值为指针的函数声明:type *name(f1, f2…) { }
int i;
int *sum1(int x, int y) { // 声明一个返回值为指针的函数
i = x + y;
return &i;
}
数组不能拷贝,所以函数不能返回数组,但函数可以返回数组指针
指向函数的指针
C语言规定,函数不能嵌套定义,也不能将函数作为参数传递。但函数名是该函数的入口地址,因此我们可以定义一个指针指向该地址,将指针作为参数传递
函数指针定义:type (*name)();
- 函数指针在进行 * 操作时,可以理解为执行该函数
- 和数组指针不同的是,函数指针不能进行 + 整数 的操作
// demo
#include <string.h>
/* 定义一个方法,传入两个字符型和一个函数指针p,用p操作字符串 */
void check(char *x, char *y, int (*p)());
void main()
{
/* 字符串转换成ASCII码后比较函数 返回0/1/-1 是string.h库中的字符串函数 使用前需声明 */
int strcmp();
char x[] = "js";
char y[] = "vue";
int (*p)() = strcmp; // 定义了一个函数指针
check(x, y, p); // 调用方法check
}
void check(char *x, char *y, int (*p)())
{
if(!(*p)(x, y))
printf("==");
else
printf("!=");
}
更多有关指针的内容将会在结构体中介绍,笔者尽量在学业之余尽快更新~
感谢您的阅读
💓