C语言指针详解

一、什么是指针

C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量

前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
在这里插入图片描述
 这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址
我们只需要对两件事情感兴趣:
1.内存中的两个位置由一个独一无二的地址标识。
2.内存中的每个位置都包含一个值。

二、为什么要使用指针

在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:
1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

三、指针运算符&和*

*&是取地址运算符,是间接运算符

取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。

指针运算符(间接寻址符)*:与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。

#include "stdio.h"    

int main()
{
	int a=1;//声明了一个普通变量 a
	int *pa;//声明一个指针变量,指向变量 a 的地址
	pa = &a;//通过取地址符&,获取 a 的地址,赋值给指针变量
	
	printf("&a=%d\n", &a);//打印变量a的地址
	printf("pa=%d\n", pa);//其实还是打印pa的地址
	printf("&pa=%d\n", &pa);//打印生命的指针变量*pa的地址
	printf("a=%d\n", a);//打印变量a的值
	printf("*pa=%d\n", *pa);//通过间接寻址符,获取指针指向的内容

	return 0;
}

程序运行结果如下:
在这里插入图片描述
实际对应关系:
在这里插入图片描述
通俗的讲,指针赋值传递的是地址,使用指针找到的就是指向的地址变量的值,因此修改指针指向的数值可以直接修改对应地址的数据。

四、指针的用法

4.1 声明并初始化一个指针

指针其实就是一个变量,指针的声明方式与一般的变量声明方式没太大区别:

int *p;        // 声明一个 int 类型的指针 p
char *p        // 声明一个 char 类型的指针 p
int *arr[10]   // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
int **p;       // 声明一个指针 p ,该指针指向一个 int 类型的指针

指针的声明比普通变量的声明多了一个一元运算符 “”。运算符 “” 是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。在上述的声明中: p 是一个指针,保存着一个地址该地址指向内存中的一个变量; *p 则会访问这个地址所指向的变量

声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化或是使他指向现有的内存,或者给他动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题,稍后会讨论这个问题。初始化操作如下:

/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x;  // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址

/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10);    // malloc 函数用于动态分配内存
free(p);  // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h

指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿。

4.2、NULL指针

NULL 指针是一个特殊的指针变量,表示不指向任何东西。可以通过给一个指针赋一个零值来生成一个 NULL 指针。
举例:

#include "stdio.h"

int main(){
    int *p = NULL;
    printf("p的地址为%p\n",p);

    return 0;
}

程序执行结果:
在这里插入图片描述

4.3 未初始化和非法的指针

如果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,这时,程序会报错,在 Linux 上,错误类型是 Segmentation fault(core dumped),提醒我们段违例或内存错误。它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。举例如下:

#include "stdio.h"

int main(){
    int *p;
    *p = 1;
    printf("%d\n",*p);

    return 0;  
}

程序执行结果如下所示,虽然没有报错,但是程序并没有正常的运行。
在这里插入图片描述
警告:
但是这个p指向了哪里呢?我们声明了这个变量,但从未对它及进行初始化,所以我们没有办法预测1这个值存储于什么地方。从这一点看,指针变量和其他变量并无区别。如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化。无论是哪种情况,声明一个指向的整型的指针都不会“创建”用于存储整型值的内存空间。
所以,如果程序执行了这个操作,会发生什么情况呢?如果你运气好,a的初始值是个非法地址,这样的赋值语句将会出错,从而终止程序。在UNIX系统上,这个错误被称为“段违例”或“内存错误”,它提示程序试图访问一个并未分配给程序的内存位置。在一台运行windows的PC上,对未初始化或非法指针进行间接的访问操作是一般保护性异常的根源之一。
一个更为严重的情况是:这个指针偶尔可能包含了一个合法的地址。接下来的事情很简单:位于那个位置的值被修改,虽然你并无意去修改它。像这种类型的错误非常难以捕捉,因为引发错误的代码可能与原先用于操作那个值的代码完全不相干。所以,在你对指针进行间接访问之前,必须非常小心,确保它们已被初始化!

五、指针的指针

指针的指针也叫做双重指针,下面举一个例子说明指针的指针的用法。

int a=12;
int *b=&a;
int **c=&b;

它们在内存中的模样大致如下所示:
在这里插入图片描述
间接操作符具有从右向左的结合性,所以这个表达式相当于*(*c),我们必须从里向外逐层求值。第一个间接符指向的位置,就是变量b;第二个间接符访问这个位置所指向的位置,也就是变量a。如果表达式中出现了间接访问操作符,就随箭头访问它所指向的位置。

表达式相当的表达式
a12
b&a
*ba,12
c&b
*cb,&a
**c*b,a,12

举例:

#include "stdio.h"

int main()
{
	int a=12;
	int *b=&a;
	int **c=&b;
	
	printf("a=%d\n", a);
	
	printf("&a=%d\n", &a);
	printf("b=%d\n", b);
	
	printf("*b=%d\n",*b);
	printf("a=%d\n", a);
	
	printf("c=%d\n", c);
	printf("&b=%d\n", &b);
	
	printf("*c=%d\n", *c);
	printf("b=%d\n", b);
	printf("&a=%d\n", &a);
	
	printf("**c=%d\n", **c);
	printf("*b=%d\n", *b);
	printf("a=%d\n", a);
	
	
	return 0;
}

运行结果:
在这里插入图片描述

六、指针的运算

6.1 赋值运算

指针变量可以互相赋值,也可以赋值某个变量的地址,或者赋值一个具体的地址。

int *px, *py, *pz, x = 10;
//赋予某个变量的地址
px = &x;
//相互赋值
py = px;
//赋值具体的地址
pz = 4000;

6.2 指针与整数的加减运算

指针变量的自增自减运算。指针加 1 或减 1 运算,表示指针向前或向后移动一个单元(不同类型的指针,单元长度不同)。这个在数组中非常常用。

指针变量加上或减去一个整形数。和第一条类似,具体加几就是向前移动几个单元,减几就是向后移动几个单元。

举例:

#include "stdio.h"

int main(){
//定义一个一维数组
int a[3]={1,2,3};
//定义一个指针,指向 x
int *px = &a[0];

//打印数组的地址
printf("&a[0] = %d\n", &a[0]);
printf("&a[1] = %d\n", &a[1]);
printf("&a[2] = %d\n", &a[2]);
//打印指针的地址,看是否与数组地址一样
printf("px = %d\n", px);
printf("(px+1) = %d\n", (px+1));
printf("(px+2) = %d\n", (px+2));
//打印数组的值
printf("a[0] = %d\n", a[0]);
printf("a[1] = %d\n", a[1]);
printf("a[2] = %d\n", a[2]);
//打印指针的值,看是否与数组的值一样
printf("*px = %d\n", *px);
printf("*(px+1) = %d\n", *(px+1));
printf("*(px+2) = %d\n", *(px+2));

return 0;
}

运行结果:
在这里插入图片描述

6.3 关系运算

假设有指针变量 px、py。

px > py ——表示 px 指向的存储地址是否大于 py 指向的地址
px == py ——表示 px 和 py 是否指向同一个存储单元
px == 0 和 px != 0 ——表示 px 是否为空指针

举例:

#include "stdio.h"

int main(){
//定义一个数组,数组中相邻元素地址间隔一个单元
int num[2] = {1, 3};

//将数组中第一个元素地址和第二个元素的地址赋值给 px、py
int *px = &num[0], *py = &num[1];
int *pz = &num[0];
int *pn;

//则 py > px
if(py > px){
	printf("py 指向的存储地址大于 px 所指向的存储地址\n");
}

//pz 和 px 都指向 num[0]
if(pz == px){
	printf("px 和 pz 指向同一个地址\n");
}

//pn 没有初始化
if(pn == NULL || pn == 0){
	printf("pn 是一个空指针\n");
}

return 0;
}

运行结果:
在这里插入图片描述

七、数组和指针

7.1 一维数组

指针和数组并不是相等的,可以考虑下面的两个声明:

int a[3]={1,2,3};//声明个一维数组
int *b;//声明一个指针
b=a;//数组名a代表数组的第一个值的地址,将该值赋值给指针b
//b=&a[0];//也可以使用这种方式赋值
//int *b = &a[0];

机器地址和数值具体对应关系如下:
在这里插入图片描述

举例:

#include "stdio.h"

int main(){
	int a[3]={1,2,3};//声明个一维数组
	int *b;//声明一个指针
	b=a;//数组名a代表数组的第一个值的地址,将该值赋值给指针b
	//b=&a[0];//也可以使用这种方式赋值
	//int *b = &a[0];
	
	//打印数组的地址
	printf("打印数组的地址\n");
	printf("&a[0] = %d\n", &a[0]);
	printf("&a[1] = %d\n", &a[1]);
	printf("&a[2] = %d\n", &a[2]);
	
	//打印指针的地址,看是否与数组地址一样
	printf("打印指针的地址,看是否与数组地址一样\n");
	printf("b = %d\n", b);
	printf("(b+1) = %d\n", (b+1));
	printf("(b+2) = %d\n", (b+2));
	
	//打印数组的值
	printf("打印数组的值\n");
	printf("a[0] = %d\n", a[0]);
	printf("a[1] = %d\n", a[1]);
	printf("a[2] = %d\n", a[2]);
	
	//打印指针的值,看是否与数组的值一样
	printf("打印指针的值,看是否与数组的值一样\n");
	printf("*b = %d\n", *b);
	printf("*(b+1) = %d\n", *(b+1));
	printf("*(b+2) = %d\n", *(b+2));
	
	return 0;
}

运行结果:
在这里插入图片描述

7.2 二维数组

由于计算机的内存是一维的,多维数组的元素应排成线性序列后存入存储器。数组一般不做插入和删除操作,即结构中元素个数和元素间的关系不变。所以采用顺序存储方法表示数组。

1、 行优先存储——将数组元素按行向量排列,第i+1个行向量紧接在第i个行向量后面。
2、 列优先存储——将数组元素按列向量排列,第i+1个列向量紧接在第i个列向量后面。

声明一个二维数组和指针:

int a[2][3]={{1,2,3},{4,5,6}};//声明个二维数组
int *b;//声明一个指针
b=a[0];//数组名a代表数组的第一个值的地址,将该值赋值给指针b
//b=&a[0][0];//也可以使用这种方式赋值
//int *b = &a[0][0];

机器地址和数值具体对应关系如下:
在这里插入图片描述
举例:

#include "stdio.h"

int main(){
	int a[2][3]={{1,2,3},{4,5,6}};//声明个二维数组
	int *b;//声明一个指针
	b=a[0];//数组名a代表数组的第一个值的地址,将该值赋值给指针b
	//b=&a[0][0];//也可以使用这种方式赋值
	//int *b = &a[0][0];
	
	//打印数组的地址
	printf("打印数组的地址\n");
	printf("&a[0][0] = %d\n", &a[0][0]);
	printf("&a[0][1] = %d\n", &a[0][1]);
	printf("&a[0][2] = %d\n", &a[0][2]);
	printf("&a[1][0] = %d\n", &a[1][0]);
	printf("&a[1][1] = %d\n", &a[1][1]);
	printf("&a[1][2] = %d\n", &a[1][2]);
	
	//打印指针的地址,看是否与数组地址一样
	printf("打印指针的地址,看是否与数组地址一样\n");
	printf("b = %d\n", b);
	printf("(b+1) = %d\n", (b+1));
	printf("(b+2) = %d\n", (b+2));
	printf("(b+3) = %d\n", (b+3));
	printf("(b+4) = %d\n", (b+4));
	printf("(b+5) = %d\n", (b+5));
	
	//打印数组的值
	printf("打印数组的值\n");
	printf("a[0][0] = %d\n", a[0][0]);
	printf("a[0][1] = %d\n", a[0][1]);
	printf("a[0][2] = %d\n", a[0][2]);
	printf("a[1][0] = %d\n", a[1][0]);
	printf("a[1][1] = %d\n", a[1][1]);
	printf("a[1][2] = %d\n", a[1][2]);
	
	//打印指针的值,看是否与数组的值一样
	printf("打印指针的值,看是否与数组的值一样\n");
	printf("*b = %d\n", *b);
	printf("*(b+1) = %d\n", *(b+1));
	printf("*(b+2) = %d\n", *(b+2));
	printf("*(b+3) = %d\n", *(b+3));
	printf("*(b+4) = %d\n", *(b+4));
	printf("*(b+5) = %d\n", *(b+5));
	return 0;
}

运行结果:
在这里插入图片描述

7.3 指针数组

指针数组——它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素。指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
在这里插入图片描述

7.4 数组指针

数组指针——它实际上是一个指针,该指针指向一个数组。数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
在这里插入图片描述

指针数组和数组指针举例:

#include "stdio.h"

int  main(){

	int *a[4]; //指针数组
	int a1[2][4]={{1,2,3,4},{5,6,7,8}};
	//将数组a1中元素赋给数组a
	for(int i=0;i<2;i++)
	{
		a[i]=a1[i];//这里int *a[4] 表示一个一维数组内存放着四个指针变量,分别是a[0]、a[1]、a[2]、a[3]
	}

	int (*b)[2]; //数组指针,该语句是定义一个数组指针,指向含4个元素的一维数组。
	int b1[4][2]={{1,2},{3,4},{5,6},{7,8}};
	b=b1;将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
	
	printf("*(a[0])=%d\n",*(a[0]));			//输出1就对
	printf("*(a[0]+1)=%d\n",*(a[0]+1));		//输出2就对
	printf("*(a[0]+2)=%d\n",*(a[0]+2));		//输出3就对
	printf("*(a[0]+3)=%d\n",*(a[0]+3));		//输出4就对
	//该语句表示a数组指向下一个数组元素。注:此数组每一个元素都是一个指针
	printf("*(a[1])=%d\n",*(a[1]));			//输出5就对
	printf("*(a[1]+1)=%d\n",*(a[1]+1));		//输出6就对
	printf("*(a[1]+2)=%d\n",*(a[1]+2));		//输出7就对
	printf("*(a[1]+3)=%d\n",*(a[1]+3));		//输出8就对

	printf("(*b)[0]=%d\n",(*b)[0]);			//输出3就对
	printf("(*b)[1]=%d\n",(*b)[1]);			//输出7就对
	 //该语句执行过后,也就是b=b+1;p跨过行b1[0][]指向了行b1[1][]
	printf("(*(b+1))[0]=%d\n",(*(b+1))[0]);	//输出3就对
	printf("(*(b+1))[1]=%d\n",(*(b+1))[1]);	//输出3就对
	
	printf("(*(b+2))[0]=%d\n",(*(b+2))[0]);	//输出3就对
	printf("(*(b+2))[1]=%d\n",(*(b+2))[1]);	//输出3就对

	printf("(*(b+3))[0]=%d\n",(*(b+3))[0]);	//输出3就对
	printf("(*(b+3))[1]=%d\n",(*(b+3))[1]);	//输出3就对

	return 0;
}

程序运行结果:
在这里插入图片描述
数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。

还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
比如要表示数组中i行j列一个元素:
(p[i]+j)、((p+i)+j)、((p+i))[j]、p[i][j]

八、结构体和指针

8.1 结构体的存储格式

struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如array、struct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对齐(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同

自然对齐(natural alignment)即默认对齐方式,是指按结构体的成员中(类型)size最大的成员作为基本的分配单元,而且与其顺序有这密切的联系。

结构体的存储:

1、结构体整体空间是占用空间最大的成员(的类型)所占字节数的整数倍。
2.、结构体的每个成员相对结构体首地址的偏移量(offset)都是最大基本类型成员字节大小的整数倍,如果不是编译器会自动补齐,

举例:

#include "stdio.h"
//定义一个结构体
typedef struct test{
	int a;
	float c;
	int b[3];

}test;

int  main(){

	test d;//结构体声明
	d.a=1;//结构体初始化
	for(int i=0;i<3;i++)
	{
		d.b[i]=2+i;
	}
	d.c=5.6;
	
	//打印结构体内的数值
	printf("d.a=%d\n",d.a);
	printf("d.c=%f\n",d.c);
	printf("d.b[0]=%d\n",d.b[0]);
	printf("d.b[1]=%d\n",d.b[1]);
	printf("d.b[2]=%d\n",d.b[2]);
	//打印结构体的地址
	printf("&d.a=%d\n",&d.a);
	printf("&d.c=%d\n",&d.c);
	printf("&d.b[0]=%d\n",&d.b[0]);
	printf("&d.b[1]=%d\n",&d.b[1]);
	printf("&d.b[2]=%d\n",&d.b[2]);
	//打印结构体所占的字节数
	printf("int占=%d\n",sizeof(int));
	printf("float占=%d\n",sizeof(float));
	printf("b[3]占=%d\n",sizeof(d.b));
	printf("sizeof(test)占=%d\n",sizeof(test));
	return 0;
}

程序运行结果:
在这里插入图片描述
存储格式:
在这里插入图片描述

8.2 结构体指针

喜欢使用指针的人一定很高兴能使用只想结构的指针。至少有4个理由可以解释为何要使用指向结构的指针。第一,就像指向数组的指针比数组本身更容易操作一样,指向结构的指针通常比结构本身更容易操作。第二,在一些早期的C实现中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。第三,即使能传递一个结构,传递指针通常更有效率。第四,一些用于表示数据的结构中包含只想其他结构的指针。
具体举例如下所示:

#include "stdio.h"
//定义一个新的结构体
typedef struct test{
	int a;
	float c;
}test;

int  main(){

	test d;//声明结构体
	d.a=1;//结构体初始化
	d.c=5.6;

	test *f;//声明结构体指针
	f=&d;//结构体指针初始化
	//以不同的方式打印结构体指针的变量
	printf("(*f).a=%d\n",f->a);//第一种方式打印结构体指针变量,使用“->”运算符
	printf("(*f).a=%d\n",(*f).a);//第二种方式打印结构体指针变量初始化
	printf("d.a=%d\n",d.a);

	printf("(*f).c=%f\n",f->c);
	printf("(*f).c=%f\n",(*f).c);
	printf("d.c=%f\n",d.c);

	return 0;
}

运行结果如下:
在这里插入图片描述
用指针访问成员变量,下面关系是恒成立的:

d.a==(*f).a==f->a

九、指针和函数

C和C++中经常会用到指针,和数据项一样,函数也是有地址的,函数的地址是存储其机器语言代码的内存的开始地址。

指针函数和函数指针经常会混淆,一个是返回值是指针,另一个是指向函数的地址,下面就分别解释指针函数和函数指针的区别。

9.1 指针函数

指针函数是 返回指针的函数主体是函数,返回值是一个指针
基本声明形式:返回数据类型 + * + 函数名 + (变量类型1,…);

int* fun(int,int);  
int * fun(int,int);
int *fun(int,int);

举例:

#include<stdio.h>
int* fun(int* x)    //传入指针  
{
	int* tmp = x;	  //指针tmp指向x
    return tmp;       //返回tmp指向的地址
}
int main()
{
    int b = 2;      
    int* p = &b;   //p指向b的地址
    printf("%d",*fun(p));//输出p指向的地址的值
    return 0;
}

运行结果:
在这里插入图片描述

9.2 函数指针

函数指针是 指向函数的指针 主体是指针 指向的是一个函数的地址
基本声明形式:返回数据类型 + (*函数名) + (变量类型1,…);
注意 * 和函数名要用括号括起来,否则因为运算符的优先级原因就变成指针函数了
函数指针的参数列表要和函数指针指向的函数的参数列表一致。

int (*fun) (int);

举例:

#include<stdio.h>
int add(int x,int y)
{
    return x + y;
}
int (*fun) (int,int);			//声明函数指针
int main()
{
    fun = &add;					//fun函数指针指向add函数
    printf("%d ",fun(3,5));		//使用函数指针时使用fun(3,5)和(*fun)(3,5)都可以
    printf("%d",(*fun)(4,2));
    return 0;
}

运行结果:
在这里插入图片描述

参考资料:
《C Primer Plus(第六版)中文版》
《C和指针》

  • 19
    点赞
  • 120
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值