C指针及其运算

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C指针及其运算相关的知识,希望对你有一定的参考价值。


内存地址

以整型加法为例,我们来看看在计算机内部,CPU(中央处理器)是如何配合其他硬件进行计算的。

CPU与内存协同工作

CPU由三部分构成:

  • 算术、逻辑单元:对数据执行运算(例如加法、减法)的电路。
  • 控制单元:协调机器活动的电路。
  • 寄存器组 :数据临时存储。

数据几乎都存储在内存上,仅有当前正在处理的数据,才放到CPU的寄存器组上,等待CPU进行计算。CPU将数据计算完毕后,再存储到内存中。数据通过数据总线,在CPU和内存上进行传输。

C指针及其运算_c语言

访问内存中的“房间”

只有正在被处理的数据才会被放到CPU的寄存器上,等待CPU进行计算,CPU将数据计算完毕后,再存储到内存中。所以,内存才是数据的大本营。

我们知道,计算机通过晶体管的开关状态来记录数据。它们通常8个编为一组,我们称之为字节。既然内存需要存储数据,内存上必然有非常多的这种8个开关组成的编组。

我们可以把这种编组看作居住数据的“房间”,而为了方便地找到这些“房间”,每个“房间”均有一个编号。第一个“房间”编号从0开始,此后依次加1。

C指针及其运算_c语言_02


我们把“房间号”,称之为内存地址

基础数据类型怎样居住“房间”

以int为例,我们有以下两种表达方式:

  • 列举所有的房号:301,302,303,304。
  • 首房间及房间数:从301开始的4个房间。

计算机使用第二种方式记录一个数据对象在内存中的存储位置。
我们把第一个“房间”的“房间号”,称为这个数据对象的首地址。那么数据对象需要的房间数量,就是它所占用的存储空间大小。

因此,记录一个数据对象在内存中的存储位置,需要两个信息:

  • 数据对象的首地址。
  • 数据对象占用存储空间大小。

指针数据类型

取地址运算符 &

取地址运算符是一个一元运算符,写在一个数据对象的左边,可以获取一个数据对象的首地址和所需存储空间大小。

声明指针类型的变量

int n;
int* pn = &n;
char c;
char* c = &c;

​int* pn​​​声明一个保存了int类型的首地址和大小的变量。
​​​char* pc​​​声明一个保存了char类型的首地址和大小的变量。
变量pn存储了变量n的首地址与大小,变量pc存储了变量c的首地址与大小。通过pn和pc可以在内存中找到变量n和c。

定义

设一个数据对象为x,设另一个数据对象为p。p存储了x的首地址和所占空间大小。那么,p称之为x的指针,或者说p指向x
对于上面的代码:

  • pn被称作n的指针,或者说pn指向n。
  • pc被称作c的指针,或者说pc指向c。
int* pn; // 将空格放在变量旁
int *pn; // 将空格放在类型旁
int*pn; // 不用空格

另外,声明指针变量时,将空格放在变量旁或者将空格放在类型旁,甚至不用空格。这3种写法都是可以的

指针类型

指针类型通过值来保存目标数据对象的首地址,类型本身标记目标数据对象的空间大小。

首地址

指针类型的值就是目标数据对象的首地址

空间大小

C语言中通过不同的指针类型来标记目标数据对象的空间大小

使用指针

取值运算符 *

取值运算符是一个一元运算符,写在一个指针的左边,可以根据指针中存储的首地址和空间大小找到目标数据对象。
虽然它和乘法运算符很像,但是,它是一个一元运算符,仅需要一个操作对象。
除了通过指针访问所指向的数据对象,也可以通过指针修改所指向的数据对象

指针类型的大小

不同的编译器或编译配置可以让编译器生成32位或64位的程序,有时候被称作x86或x64。

  • 32位程序可以访问从0到2的32次方的地址范围。
  • 64位程序可以访问从0到2的64次方的地址范围。

如果指针类型的大小为4字节,使用%u作为printf的占位符是合适的。但是指针类型的大小为8字节时,使用%u有可能不能完全打印地址,可以使用长度指示符,将长度加长到8字节,例如%llu。
占位符%p是指针类型专用的占位符,32位或64位程序使用它均能保证打印的结果正确。不过,它通常是以十六进制显示的。

强制转换指针类型

#include <stdio.h>
int main()

int n = 1431655765;
int* pn = &n;
char* pc = (char*)pn;
printf("pn=%u\\n", pn);
printf("pc=%u\\n", pc);
printf("n=%d\\n", n);
printf("*pn=%d\\n", *pn);
printf("*pc=%d\\n", *pc);
return 0;

变量pn为指向int类型的指针。
变量pc为指向char类型的指针。
虽然变量pn与pc不能在赋值时自动转换。但是,使用强制转换可以将pn转换为char*后赋值给pc。
赋值后,pn与pc均存储了n的首地址,均打印出了同样的首地址。但是,C语言使用不同的指针类型标识目标对象的空间大小。

pn=982514036
pc=982514036
n=1431655765
*pn=1431655765
*pc=85

pn为int*类型,pn表达式会从首地址开始处取sizoef(int)字节,其转换成int类型作为表达式结果。因此,结果为1431655765。
pc为char
类型,*pc表达式会从首地址开始处取sizoef(char)字节,将其转换成char类型作为表达式结果。因此,结果为85。

C指针及其运算_算法_03

指针运算

一个数据对象的内存位置有两个重要信息:

  • 数据对象的首地址。
  • 数据对象占用存储空间大小。

指针类型通过下面的方式,存储这两个重要信息。

  • 指针类型通过值来保存目标数据对象的首地址。
  • 类型本身标记目标数据对象的空间大小。

指针类型与整型进行加减

指针类型加 n 后。其首地址向后移动​​n*​​​步长 字节。
指针类型减 n 后。其首地址向前移动​​​n*​​步长 字节。

#include <stdio.h>
int main()

char* pc;
short* ps;
int* pn;
long* pl;
long long* pll;
float* pf;
double* pd;
// 现将整型转换为对应的指针类型再赋值
pc = (char*)100;
ps = (short*)100;
pn = (int*)100;
pl = (long*)100;
pll = (long long*)100;
pf = (float*)100;
pd = (double*)100;
pc = pc + 1;
ps = ps + 1;
pn = pn + 1;
pl = pl + 1;
pll = pll + 1;
pf = pf + 1;
pd = pd + 1;
printf("pc=%u\\n", pc);
printf("ps=%u\\n", ps);
printf("pn=%u\\n", pn);
printf("pl=%u\\n", pl);
printf("pll=%u\\n", pll);
printf("pf=%u\\n", pf);
printf("pd=%u\\n", pd);
return 0;

所有的指针内保存的首地址一开始均为100,加1后,现在变成了不同的值。
与初始值相比,分别移动了:1、2、4、4、8、4、8。
这些数值分别是对应的目标数据对象的空间大小。
指针类型加1后,将首地址向后移动了​​​sizeof(目标数据对象)​​字节。

应用指针类型与整型加减

#include <stdio.h>
int main()

int n1;
int n2;
int n3;
int n4;
printf("&n1=%d\\n", &n1);
printf("&n2=%d\\n", &n2);
printf("&n3=%d\\n", &n3);
printf("&n4=%d\\n", &n4);
putchar(\\n);
// 获取n1的首地址
int* p = &n1;
printf("p = %d\\n", p);
printf("p - 1 = %d\\n", p - 1);
printf("p - 2 = %d\\n", p - 2);
printf("p - 3 = %d\\n", p - 3);
putchar(\\n);
// 给n1到n4赋值
*p = 111; // p指向n1
*(p - 1) = 222; // p - 1后前后移动4字节,现在指向n2
*(p - 2) = 333; // p - 2后前后移动8字节,现在指向n3
*(p - 3) = 444; // p - 3后前后移动12字节,现在指向n4
printf("n1=%d\\n", n1);
printf("n2=%d\\n", n2);
printf("n3=%d\\n", n3);
printf("n4=%d\\n", n4);
return 0;
  • 表达式 p - 1 必须先被括号包裹,再使用取值运算符*。
    这是因为**取值运算符_的优先级高于算术运算符。_
    我们需要先让首地址移动,再进行取值操作。
    *若不使用括号, p 会先被取值,之后值再被加1。
  • 32位应使用%u,64位使用%llu
  • %d可能出现负号

这串代码是有问题的

函数内声明的变量在内存中不一定是首尾相接的。受到内存对齐的影响,它们有可能不连续,导致崩溃。
但是,数组可以保证各元素在内存中首尾相接,各元素连续分布在内存上。所以,C语言中可以使用指针与整型加减运算访问和修改数组元素。

同类型指针减法运算

sizeof(目标数据对象) 被称作步长。
指针类型与指针类型相减后,其结果为两首地址差值除以步长。

其他类型的指针运算

上面我们介绍了两种有指针类型参数的运算:

  • 指针类型与整型加减。
  • 同类型的指针相减。

它们的运算结果都在内存上拥有实际的意义。
还有另外几种运算:

  • 指针类型与整型进行乘除运算。
  • 同类型的指针相加。
  • 同类型的指针乘除。

这些运算结果都没有实际意义,在C语言中无法通过编译。


指针的原理——地址内存

指针

指针是什么

百度百科:
指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。
指针是一个占据存储空间的实体在这一段空间起始位置的相对距离值。在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。

由百度百科可知,指针的实质就是内存的地址,在计算机的内存中,每一个字节都有自己的地址,内存地址是一种用于软件及硬件等不同层级中的数据概念,是用来访问电脑主存中的数据。利用指针中存放的地址可以直接指向(points to)存在电脑存储器中另一个地方的数据。由于通过地址能找到所需的数据,就可以说,地址指向该数据。因此,将地址形象化的称为“指针”,意思是通过它能找到以它为地址的内存单元,并且可以访问内存其中的数据。
在这里插入图片描述

可能听起来比较晦涩,那我就来简单举个例子:如上图,我定义了一个整型变量a整型指针变量p,把a取地址赋值给p。输出p的值,可以看到是133fb5c,这就是变量a的地址,而指针p则是来存放a的地址的变量,也就是指针变量。在这里插入图片描述
在这里插入图片描述

而关于内存更详细的内容大家可以去看参考《程序员的自我修养》,其中第十章 内存中有更详细的内存布局。
我们可以通过指针p来找到a的内容,并且可以直接修改。我们就说p指向了a。指针变量的内容存储的是其指向的对象的首地址(如上),指向的对象可以是变量(指针变量也是变量,也就有了二极指针、多级指针,下面再说),数组,函数等占据存储空间的实体。

指针与指针类型

就和变量的数据类型一样,指针变量也有相应的类型。
在上面的例子中我们还可以发现,输出p+1时,地址增加了4个字节,而不是一个。指针向前走一步或者向后走一步需要走多大,是由指针变量的类型决定,因为指针pint型,所以p+1向下走了四个字节。但是无论指针变量的类型是啥,在32位的机器里指针变量的大小都是 4个字节

int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\\n", &n);//n的地址
	printf("%p\\n", pc);//变量pc的存储的值
	printf("%p\\n", pc + 1);//pc的下一个指针位置
	printf("%p\\n", pi);//变量pi的存储的值
	printf("%p\\n", pi + 1);//pi的下一个指针位置
	return  0;
}

运行结果
总结:指针的类型决定了向前移动或向后移动走一步要多大(几个字节)。

指针的解引用

int main() {
		int n = 0x11223344;
		char* pc = (char*)&n;
		int* pi = &n;
		*pc = 0;   
		*pi = 0;   
		return 0;
}

执行完 * pc=0;之后:
执行完*pc = 0;
执行完 * pi = 0; 之后
执行完*pi = 0;
总结:指针的类型决定了对指针解引用的时候有多大的权限(也就是能操作几个字节)。比如:char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

指针运算

指针±整数

重置数组为0:

float values[5];
float *p;
for (p = &values[0]; p < &values[5];)
{
     *p++ = 0;
}

但是有一点要注意,计算器有一种标准:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
假如这就是我们内存当中所存的数组元素,array[0]是数组的第一个元素,而array[3]则是数组中最后一个元素。这种情况下,指向内存位置的指针是可以和它后面这一块内存比较的,其实就可以理解为一个前开后闭的区间[start,end)。
在这里插入图片描述

所以我们不能这样写:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}

虽然这样写不会报错也可以运行,但是标准并不保证他可行,而且在不同的编译器下,它运行结果也不一样,所以结果没有意义。

指针-指针

得到字符串的长度

int my_strlen(char*s)
{
	char*p=s;//s的首地址赋值给p
	while(*p!='\\0')
	{
		p++;
	return p-s;//返回值为字符串的长度:空字符的地址减去s的首地址的长度
	}
}

指针与数组

点击链接查看

以上是关于C指针及其运算的主要内容,如果未能解决你的问题,请参考以下文章

C指针及其运算

C语言中16进制之间的加减乘除运算怎么做?

c语言问题 请问如何随机产生加减乘除任意一种运算符号呢

dev c语言 输入两数,输入符号进行加减乘除运算,结果不对,求大神改错

输入两个整数,进行加减乘除四则运算的c语言程序怎么写啊,拜托了~

c_cpp 用位运算实现四则运算之加减乘除的.c