深入解析C语言指针:从内存寻址到高效编程的核心

指针是C语言乃至整个系统编程领域的灵魂,它赋予了程序员直接与内存对话的能力。理解指针,不仅是掌握C语言的关键,更是通往底层系统、操作系统内核以及高性能编程的必经之路。本文将从内存的本质出发,层层递进,为你构建一个清晰、完整的指针知识体系,并探讨其在现代编程实践中的应用。

一、内存、地址与指针:理解计算机的“寻址”本质

要理解指针,必须先理解计算机内存是如何工作的。你可以将内存想象成一个巨大的、由无数小房间(字节)组成的线性酒店。每个房间都有一个唯一的门牌号,这就是内存地址。当我们声明一个变量,比如 int a = 10;,系统就会在内存中为它分配一个或多个连续的“房间”来存放这个整数值。

变量、数组:为了数据的存储

指针:为了高效访问数据,间接访问

内存:memony,程序运行起来后的一个数据暂存的场所,32bit 0~4G,C语言中,操作内存的最小单位是一个字节1byte,其中有 heap (堆  相对来说大一点的空间), map/share (库函数), stack (栈,局部变量,函数参数,返回地址)

地址:由于地址空间比较大,C语言中最小操作单位是1byte,为了方便访问内存,给每个内存都分配了一个编号,这个编号就叫内存地址

指针:指针变量,专门用来存储地址的变量

举例:查看指针的内容和大小

p的内容是变量a的地址,间接访问变量a的内容

指针变量本身也是一个变量,它特殊之处在于其存储的值不是普通数据,而是一个内存地址。通过这个地址,我们可以找到并操作目标数据,这就是间接访问。这种机制使得函数能够修改外部变量、动态管理内存以及高效处理数组和数据结构成为可能。无论是Java的引用、Python的对象标识,还是C++的智能指针,其思想根源都与此相关。

二、指针的声明、初始化与两大核心运算符

指针的声明遵循 基类型 *指针变量名; 的格式。这里的“基类型”至关重要,它决定了指针解引用时访问的内存大小和数据的解释方式。

  • 取地址运算符 &:获取变量的内存地址。例如,&a 返回变量 a 的地址。
  • 解引用运算符 *:访问指针所指向地址处存储的值。例如,*p 获取指针 p 指向的数据。

  • 只能给左值取地址 --> 只能给变量取
  • int a;    &a;    &a 是一个表达式,值表示的是a这个变量在内存空间的首地址,类型  int *

  • 只能是指针类型才能解引用
  • 解引用过程:先找到对应的内存地址,获得内存地址空间中的内容
  • 给人的感觉是取地址时 -->类型是 int * ,再解引用时 --> 类型是 int (把*扣掉)

指针必须初始化后才能安全使用。未初始化的指针被称为野指针,它指向一个随机的、可能非法的内存地址,对其进行操作是危险的,极易导致程序崩溃或数据损坏。安全的做法是将其初始化为 NULL(C语言中通常定义为 (void*)0),即空指针,表示它不指向任何有效对象。

int *p;
野指针:未经初始化的指针或者指针指向的内存空间是一个已经被释放的空间
野指针,p中存储的地址是一个随机数,从逻辑上来说这个指针不能读也不能写
int *p1=NULL;
空指针:NULL(宏定义),在stdio.h
空指针,NULL==((void*)0) 指针已经初始化,稍后才会关联一个变量的地址,在未关联变量地址前不能使用
int num = 123;//定义一个普通变量
p = # //让空指针p关联num的地址(赋值操作)

#include
int main()
{
	int a=10;
	//int *p = &a; //整形指针,& 获得a在内存中的地址
	printf("a addr %p\n",&a);
	//printf("p is %p\n",p);
	//printf("p size is %lu\n",sizeof(p));
	int *p5=NULL;
	int *p6=&a;
	printf("p6 addr %p\n",p6);
	printf("a addr %p\n",&a);
	printf("a value : %d\n",*p6);//通过指针,间接读取a变量的值
	*p6=50;
	printf("修改后 a value : %d\n",*p6);//间接访问
	printf("a value : %d\n",a);//直接访问
	return 0;
}

在64位操作系统中,无论指针的基类型是什么,指针变量本身的大小通常是8字节(64位),因为它需要存储一个64位的地址。这一点与Java或Go语言中的引用有显著区别。

基类型* 指针名

int* p1; // * 不是解引用 类型说明符 int *

char *p2; // char*

double * p3; //double *

float* p4; //float *

三、指针的算术运算:跨越内存的“步伐”

指针的加减运算并非普通的数学加减,而是以其基类型的大小为步长进行内存地址的移动。这是指针高效处理数组和缓冲区的基石。

如果指针需要+1,-1,一般配套数组操作的时候才会使用

p(存储一个变量的地址)+q(存储另一个变量的地址) --> 两个指针相加  可以加但没意义

当指针与数组相关联,此时,指针相减是有意义的

p-1 p-2; p应该指向一个数组,操作完后指针应该落在数组范围内

两个指针相减 --> 两个指针之间相差几个元素,这两个指针都要关联同一个数组

示例代码:

++    p++    :先获得当前地址,加上sizeof(基类型),计算结果写回指针变量本身

--       p--    :先获得当前地址,减去sizeof(基类型),计算结果写回指针变量本身

理解不同类型指针的步长差异是关键:

  • char *p; 执行 p+1,地址增加1字节。
  • int *p;(假设int为4字节)执行 p+1,地址增加4字节。
  • double *p;(8字节)执行 p+1,地址增加8字节。

  • char *  偏移量1个字节                                       sizoef(基类型)
  • int * 偏移量4个字节
  • float * 偏移量4个字节
  • double * 偏移量8个字节

解引用操作也依赖于基类型。指针不仅知道“去哪里”,还知道“取多少”以及“如何解释”取到的数据。

char *  执行解引用操作,从指针存储的地址开始往后(地址变大的方向)取1个字节的数据

int * 执行解引用操作,从指针存储的地址开始往后(地址变大的方向)取4个字节的数据

float * 执行解引用操作,从指针存储的地址开始往后(地址变大的方向)取4个字节的数据

double * 执行解引用操作,从指针存储的地址开始往后(地址变大的方向)取8个字节的数据

示例代码:指针与数组关联

务必分清修改指针本身(改变它的指向)和修改指针指向的内容。这是两个完全不同的操作。

四、指针与数组:密不可分的伙伴

在C语言中,数组名在大多数表达式中会被转换为指向其首元素的指针。这使得通过指针遍历和操作数组变得异常高效和自然。

7.1    数组的数组名

是一个指向数组中第一个元素的指针常量,a中存储的地址不能发生变化,数组名的本质就是一个指针常量(加了一些限制,关键字:const -->让指针中存储的地址不能改变)

7.2    指针和数组的区别

①  sizeof不同    int *,int [],int * ≈ int []

sizeof 数组  eg: int a[50];  sizeof(a)==sizeof(int)*50;

sizeof 指针  eg: int *p;  sizeof(*p)  永远是8byte

②   &    取地址用法不同

int*    执行&地址操作    int **    二级指针,存储地址的地址

int[]   执行&地址操作    int(*p)[]    数组指针,指向二维数组中的一行

7.3    使用指针访问数组元素    访问元素的方式

(在数组和指针已经关联好的情况下)a[n] --> *(p+n) --> *(a+n) --> p[n]

例题:以指针方式打印数组元素

是把地址扔进去,用能接地址的东西——指针来接

尽管关系紧密,但指针和数组名仍有区别:

  • sizeof 运算不同:对数组名使用 sizeof 得到的是整个数组的字节大小;对指针使用 sizeof 得到的是指针变量本身的大小(如8字节)。
  • & 运算符的语义不同&array 得到的是指向整个数组的指针(类型为 int(*)[N]),而 &pointer 只是获得指针变量自己的地址。

这种通过指针操作数组的模式,在C++的迭代器、Go语言的切片底层实现以及许多高性能库中都能看到其影子。[AFFILIATE_SLOT_1]

五、指针在函数中的应用:值传递 vs. 地址传递

这是指针最经典的应用场景之一,解决了C语言函数参数默认“值传递”的局限性。

  • 值传递:函数获得实参的一个副本,对副本的修改不影响原始数据。
  • 地址传递:函数获得实参的地址(指针),通过解引用可以直接修改原始内存中的数据。

形参是实参的复制品,只能读取实参的数据,在被调函数内部无法通过修改形参而改变实参

涉及类型有:基本数据类型变量,构造类型的变量

形参会接收到实参传递过来的地址,就可以在被调函数的内存,通过间接访问的方式,通过修改形参进而修改实参

涉及类型有:数组、指针

下面的示例清晰地展示了两种传递方式的区别:

#include 
void swap(int a,int b)
{
    int t = a;
    a = b;
    b =t;
}
void swap2(int *pa,int *pb)
{
    int t = *pa;
    *pa = *pb;
    *pb =t;
}
int main()
{
    int a = 10;
    int b =20;
    //swap(a,b);
    swap2(&a,&b);
    printf("a :%d b:%d\n",a,b);
    return 0;
}

在实现字符串操作函数(如 mystrlen, mystrcpy)时,使用指针不仅代码更简洁,而且效率更高,因为它避免了不必要的数组下标计算。

#include
#include
void myshow(char *pstr)
{/*
	int i=0;
	for(i=0;'\0'!=*pstr;i++)
	{
		printf("%c",*pstr++);
	}
	*/
	while(*pstr)
	{
		printf("%c",*pstr);
		pstr++;
	}
	return;
}
int mystrlen(char *pstr)
{
	int len=0;
	while(*pstr)
	{
		len++;
		pstr++;
	}
	return len;
}
int mystrcpy(char*dst,char*src)
{
#if 0
	while(*dst=*src)
	{
		src++;
		dst++;
	}
#endif
	while(1)
	{
		*dst=*src;
		src++;
		if('\0'==*src)
		{
			break;
		}
		dst++;
	}
	//*(dst+1)='\0';
	*(++dst)+='\0';
	return 0;
}
int mystrcmp(char *str1,char*str2)
{
	while(*str1==*str2&&*str1)
	{
		str1++;
		str2++;
	}
	return *str1-*str2;
}
int mystrcat(char *str1,char *str2)
{
	while(*str1)
	{
		str1++;
	}
	while(*str2)
	{
		*(str1++)=*(str2++);
	}
	return 0;
}
int main()
{
	char str1[100]={0};
	char str2[100]={0};
//	char dst[100]={0};
	printf("input str1:\n");
	gets(str1);
	printf("input str2:\n");
	gets(str2);
/*	myshow(str);
	int len=mystrlen(str);
	printf("len:%d\n",len);
	mystrcpy(dst,str);
	printf("dst:%s\n",dst);
	int ret = mystrcmp(str1,str2);
	*/
/*	if(ret==0)
	{
		printf("same\n");
	}
	else
	{
		printf("no same\n");
	}
*/
	mystrcat(str1,str2);
	printf("str1: %s\n",str1);
	return 0;
}

int main()
{
    char str[100]="hello";
    char * p = str;
    /*
    while(*p)
    {
        printf("%c",*p);
        p++;//可以++,--操作,p本身被修改
    }
    */
    while(*str)
    {
        printf("%c",*str);
        str++;
     //数组名一个指针常量,本身不可以++,-。当数组定义好后,str 是一个地址标号,和开辟的内存空间的编号,终身绑定。
     }*/
    show(str)
}

掌握指针是理解现代编程语言内存管理模型的关键。无论是TypeScript/JavaScript中对对象的引用传递,还是Python中可变与不可变对象的行为差异,其底层逻辑都与指针概念相通。通过指针,我们能够编写出更高效、更灵活的代码,并为学习数据结构(链表、树)、操作系统和嵌入式系统开发打下坚实基础。[AFFILIATE_SLOT_2]

总结与进阶建议:指针的核心在于理解“地址”与“间接访问”。从区分指针变量和指针所指内容开始,熟练运用 &* 运算符,理解指针运算的步长逻辑,并掌握其在数组和函数传参中的经典用法。实践中,务必警惕野指针,善用空指针进行初始化检查。当你真正驾驭了指针,你便拥有了直接操控内存的能力,这是从应用层开发迈向系统层开发的标志性一步。

#include
int main()
{
	int a=20;
	int *p = &a; //整形指针,& 获得a在内存中的地址
	printf("a addr %p\n",&a);
	printf("p is %p\n",p);
	printf("p size is %lu\n",sizeof(p));
}
int num=0;
#  // 0x123456   类型为 int *
从这个地址开始数4个字节都是num的
int a=20;
int *p= &a;  //左边是定义一个指针,右边是把a的地址装进去
这块的*是类型说明符,说的是等会要装整型变量的地址
a;  //20  直接访问
*p;  //20  间接引用
这块的*才是解引用,这个值表示对应内存空间中的数据,类型  int
*&a;  值为20
* 与 & 为相反操作的运算符
int *p;  //p int --> int*
*p;  // int* --> int
*p=10;  //给a写入数据,当左值
int a=20;
int *p= &a;
int b=*p;  // 当右值,读取a中的数据
#include
int main()
{
	int *p1;   // *不是解引用 类型说明符  int*
	char *p2;  // char *
	double *p3;  // double *
	float *p4;  // float *
	// 所有的指针都是 8byte(62bit)
	printf("int * sizeof %lu\n",sizeof(p1));
	printf("char * sizeof %lu\n",sizeof(p2));
	printf("double * sizeof %lu\n",sizeof(p3));
	printf("float * sizeof %lu\n",sizeof(p4));
	return 0;
}
#include
int main()
{
	int a[10]={1,2,3,4,5,6,7,8,9,0};
	int *p=a+1;
	int *q=a+5;
	printf("q-p %ld\n",q-p);
	return 0;
}
#include
int main()
{
	int a[5]={1,2,3,4,5};
	printf("array addr %p\n",a);
	printf("a[0] %p\n",&a[0]);
	printf("a[1] %p\n",&a[1]);
	int *p=a;
	printf("a[0] %d\n",a[0]);
	printf("point *(p+0) %d\n",*(p+0));
	printf("*(a+0) %d\n",*(a+0));
	printf("point p[0] %d\n",p[0]);
	printf("a[0] addr %p\n",&a[0]);
	printf("point addr *(p+0) %p\n",&(*(p+0)));
	printf("*(a+0) addr %p\n",&(*(a+0)));
	printf("point addr p[0] %p\n",&p[0]);
/*
    printf("a[1] %d\n",a[1]);
    printf("point a[0] %d\n",*(p+1));
    printf("a[4] %d\n",a[4]);
    printf("point a[0] %d\n",*(p+4));
*/
	return 0;
}
#include 
void show_array(int * a,int len)
{
    printf("sizeof(a) is %lu\n",sizeof(a));
    int i = 0 ;
    for(i=0;i
posted on 2026-02-25 21:27  blfbuaa  阅读(11)  评论(0)    收藏  举报