关于C语言中指针含义的详细解析

一、什么是指针

C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量;如果学习过汇编语言的小伙伴很容易理解。**所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。**指针说白了就是一块内存的地址,而指针变量就是用来保存这块地址,也就是说,指针变量里面存的是指针,就像整型变量里面存的是整数;举例说明,就如下图所示:
  在这里插入图片描述
指针变量就相当于图中路标,而指针就是路标上的地址,也就是需要的内存地址,通过它就可以找到需要的内存地址,就像图中一样,通过路标中的地址就可以找到房子,之后就可以找到房子中的东西了。

#include <stdio.h>
#include <stdlib.h>

int a = 2;
int* p = &a;//如果使用先定义后赋值需要将其放入主函数内执行,否则会报错

int main()
{
	printf("p = %d\n",p);
	printf("*p = %d\n", *p);
	printf("&p = %d\n", &p);
	printf("a = %d\n", a);
	printf("&a = %d\n", &a);
	system("pause");
}

如上面代码所示,a就是整型变量,其可以用来保存整型数据;顾名思义,p是一个指针变量,则它就是用来储存指针数据的,而指针就是一个地址,代码中将a的地址赋给了指针变量p,所以p就用来存储a的地址了,保存的指针就是&a,当然系统也会自动为指针变量分配一段内存地址,就是&p,就是上图中路标地址。

前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
 在这里插入图片描述
这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。

二、为什么要使用指针

在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:

1) 指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;

2) C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;

3) C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

三、如何声明一个指针

3.1 声明并初始化一个指针

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

int *p;        // 声明一个 int 类型的指针 p,它指向int型变量
char *p;        // 声明一个 char 类型的指针 p,它指向char型变量
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

<1>.malloc函数
函数原型为void *malloc(unsigned int size);
其作用是在内存的动态存储中分配一个长度为size的连续空间(size是一个无符号数)
此函数的返回值是一个指向分配域起始地址的指针(void)。
如果此函数未能成功地执行(例如内存空间不足),则返回Null.

<2>.calloc函数
函数原型是void *calloc (unsigned n,unsigned size)
其作用是在内存的存储区中分配n个长度为size的连续空间。
函数返回一个指向分配域起始地址的指针。
如果分配不成功,返回Null
用calloc函数可以为一维数组开辟动态存储空间,n为数组个数,每个元素长度为size

<3>.free函数
函数原型是void free(void *p)
其作用是释放由p指向的内存区,使这部分内存区能被其他区使用
p是最近一次调用calloc或者malloc函数的时候返回的值。

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的
1.深拷贝
深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。
就比如在我们的日常生活中,张三想要李四的资料,然后张三就在李四这里复制过去,这时张三和李四手里都有相同的一份资料,但这两份资料相互独立,不管是谁修改了他自己手里的内容都对另外的人手里的资料无影响!
2.浅拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。直接地址赋值,指针共享一片内存。
3.二者的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

浅拷贝

//C语言中的浅拷贝
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>


typedef struct _student {
	char name[30];
	char *title;
	int age;
}Student;


void main() {
	Student s1;
	Student s2;
	s1.age = 12;
	strcpy(s1.name, "小明");
	s1.title = (char *)malloc(sizeof(char) * 30);
	strcpy(s1.title, "总经理");
	s2 = s1;
	printf("s1的age=%d,s1的name=%s,s1的title=%s\n", s1.age, s1.name, s1.title);
	printf("s2的age=%d,s2的name=%s,s2的title=%s\n", s2.age, s2.name, s2.title);
	printf("s1.title的地址%x\n", s1.title);   
	printf("s2.title的地址%x\n", s2.title);  
	printf("&s1.title的地址%x\n", &s1.title);   
	printf("&s2.title的地址%x\n", &s2.title);  

	printf("s1.name的地址%x\n", s1.name);
	printf("s2.name的地址%x\n", s2.name);
	//printf("s1.name的地址%x\n", s1.name);   
	//printf("s2.name的地址%x\n", s2.name);  
	/*输出
	s1.title的地址756aa4f0
	s2.title的地址756aa4f0
	&s1.title的地址a2f4f8f0
	&s2.title的地址a2f4f920
	s1.name的地址a2f4f8d0
	s2.name的地址a2f4f900
	可以很明显看出s1和s2中成员char *title都指向了同一单元,但是他们本身的地址却不同,说明了他们都保存了同一个地址,
	即浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。直接地址赋值,指针共享一片内存。
	*/
	//这说明s1和s2中成员char *title;只是浅拷贝,两个指针指向同一块堆内存,
	//当释放free(s1.title);时,s2.title指向的内存空间也没释放了,所以再次释放会报错								
	if (s1.title != NULL)
	{
		free(s1.title);
	}
	//错误代码
	/*if (s2.title != NULL)
	{
	free(s2.title);
	}*/


	//要想实现深拷贝,那么必须给s2.title也分配一段内存空间,s2.title = (char *)malloc(sizeof(char) * 30);
	//然后通过strcpy()将s2.title指向的字符串复制到s2.title指向的内存空间内strcpy(s2.title, s2.title);

	//由此证明,结构体之间的赋值(s2 = s1;),是进行了结构体内部所有数据的拷贝,
	//如上s1.name的地址s2.name的地址不同,说明是把s1.name中的数据复制到了s2.name中
	system("pause");
}

深拷贝

//C语言中的深拷贝
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>


typedef struct _student {
	char name[30];
	char *title;
	int age;
}Student;


void main() {
	Student s1;
	Student s2;
	s1.age = 12;
	strcpy(s1.name, "小明");
	s1.title = (char *)malloc(sizeof(char) * 30);
	strcpy(s1.title, "总经理");
	s2 = s1;
	printf("s2.title的地址%x\n", s2.title);
	s2.title = (char *)malloc(sizeof(char) * 30);
	printf("s2.title的地址%x\n", s2.title);
	strcpy(s2.title, s1.title);
	printf("s1的age=%d,s1的name=%s,s1的title=%s\n", s1.age, s1.name, s1.title);
	printf("s2的age=%d,s2的name=%s,s2的title=%s\n", s2.age, s2.name, s2.title);
	printf("s1.title的地址%x\n", s1.title);
	printf("s2.title的地址%x\n", s2.title);
	printf("&s1.title的地址%x\n", &s1.title);
	printf("&s2.title的地址%x\n", &s2.title);

	printf("s1.name的地址%x\n", s1.name);
	printf("s2.name的地址%x\n", s2.name);
 
	/*输出
	s2.title的地址3f0a4c0
	s2.title的地址3f0a520
	s1的age=12,s1的name=小明,s1的title=总经理
	s2的age=12,s2的name=小明,s2的title=总经理
	s1.title的地址3f0a4c0
	s2.title的地址3f0a520
	&s1.title的地址c54ff770
	&s2.title的地址c54ff7a0
	s1.name的地址c54ff750
	s2.name的地址c54ff780
	可以很明显看出一开始浅拷贝完s1和s2中成员char *title都指向了同一单元,
	后通过分配一段内存空间,s2.title = (char *)malloc(sizeof(char) * 30);后,
	s2指向了先分配内存的入口地址,然后通过strcpy进行复制,
	就是将s1.title中的内容复制给了s2.titel所指向的内存地址,故属于深拷贝。
	*/
	//这说明s1和s2中成员char *title;是深拷贝,两个指针指向不同的内存,
	//当释放free(s1.title);时,s2.title指向的内存空间没有影响了,所以可以再次释放							
	if (s1.title != NULL)
	{
		free(s1.title);
	}
	
	if (s2.title != NULL)
	{
		free(s2.title);
	}
	system("pause");
}

3.2 未初始化和非法的指针

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

#include "stdio.h"

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

    return 0;  
}

这个程序可以编译通过,但是运行的话会报错,报错信息如下:

使用了未初始化的局部变量“p”

要想使这个程序运行起来,需要先对指针 p 进行初始化:

#include "stdio.h"

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

    return 0;  
}

对指针进行初始化后,指针指向了我们让它指向的变量,便可以正常对指针进行赋值了。,就如同修改变量x一样。

3.3 NULL指针

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

#include "stdio.h"
#include"stdlib.h"
int main() {
	int *p = NULL;
	printf("指针变量p保存的指针为%d\n", p);
	system("pause");
	return 0;
}

/***************
* 程序输出:
* 指针变量p保存的指针为0
***************/

可以看到指针指向内存地址0。在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是为操作系统保留的。但是,内存地址 0 有一个特别重要的意义,它表明该指针不指向一个可访问的内存位置。

3.4 指针的转换

首先先看下面两行代码的区别

char *p;    //声明一个char *型的变量p
(char *)p;  //p是一个已经声明过的变量,但类型知不一定是char *,这里用(char *)强制转换,把p看作是一个char *型指针.

第一行代码大家都会熟悉,就是普通的指针声明,第二行则是指针的强制转换,因为p已经是一个声明过的变量,这里将其转换为一个指向char型的指针,
具体用法参照下面代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void main() {

	int *p;
	int a = 2;
	p = &a;
	char *p1 = (char*)p;
	int b = *(int *)p1;

	printf("p = %p\n", p);
	printf("&p = %p\n", &p);
	printf("*p = %d\n", *p);
	
	printf("p1 = %p\n", p1);
	printf("&p1 = %p\n", &p1);
	printf("*p1 = %d\n", *p1);

	printf("&b = %p\n", &b);
	printf("b = %d\n", b);
	
	system("pause");
}
/*输出
p = 000000D8B732FC70
&p = 000000D8B732FC68
*p = 2
p1 = 000000D8B732FC70
&p1 = 000000D8B732FC60
*p1 = 2
&b = 000000D8B732FC74
b = 2
*/

从上面代码中可以看出指针p是指向int型的指针,通过强制转换为了p1则是指向char型的指针,而后又对p1进行转换,变成来整型变量b,通过地址可以看出p和p1虽然都是指针,但类型不同,且二者只是指向了同一地址单元,自身地址并不同,所以属于两个指针指向同一地址,是浅拷贝;而b则是使用了p1指向地址单元中的变量进行赋值,是深拷贝;

四、指针的运算

C 指针的算术运算只限于两种形式:

1) 指针 +/- 整数 :

可以对指针变量 p 进行 p++、p–、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。用一张图来说明一下:

在这里插入图片描述
在上图中,10000000等是内存地址的十六进制表示(数值是假定的),p 是一个 int 类型的指针,指向内存地址 0x10000008 处。则 p++ 将指向与 p 相邻的下一个内存地址,由于 int 型数据占 4 个字节,因此 p++ 所指的内存地址为 1000000b。其余类推。不过要注意的是,这种运算并不会改变指针变量 p 自身的地址,只是改变了它所指向的地址,即所保存的指针发生了变化。举个例子:

#include "stdio.h"
#include"stdlib.h"
int main() 
{
	int a = 10, *pa = &a, *paa = &a;
	double b = 99.9, *pb = &b;
	char c = '@', *pc = &c;
	//最初的值
	printf("&a=%#X,&b=%#X,&c=%#X\n", &a, &b, &c);
	printf("pa=%#X,pb=%#X,pc=%#X\n", pa, pb, pc);
	printf("&pa=%#X,&pb=%#X,&pc=%#X\n", &pa, &pb, &pc);
	//加法运算
	pa++; pb++; pc++;
	printf("pa=%#X,pb=%#X,pc=%#X\n", pa, pb, pc);
	printf("&pa=%#X,&pb=%#X,&pc=%#X\n", &pa, &pb, &pc);
	//减法运算
	pa -= 1; pb -= 1; pc -= 1;
	printf("pa=%#X,pb=%#X,pc=%#X\n", pa, pb, pc);
	printf("&pa=%#X,&pb=%#X,&pc=%#X\n", &pa, &pb, &pc);
	//printf("*paa = %d\n", *paa);
	//比较运算
	if (pa == paa) {
		printf("*paa = %d\n", *paa);//比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据.
	}
	else {
		printf("*pa = %d\n", *pa);
	}
	system("pause");
	return 0;
}
//运行结果:
/*
&a=0X8658FDEC,&b=0X8658FDF0,&c=0X8658FDE8
pa=0X8658FDEC,pb=0X8658FDF0,pc=0X8658FDE8
&pa=0X8658FDD0,&pb=0X8658FDE0,&pc=0X8658FDD8
pa=0X8658FDF0,pb=0X8658FDF8,pc=0X8658FDE9
&pa=0X8658FDD0,&pb=0X8658FDE0,&pc=0X8658FDD8
pa=0X8658FDEC,pb=0X8658FDF0,pc=0X8658FDE8
&pa=0X8658FDD0,&pb=0X8658FDE0,&pc=0X8658FDD8
*paa = 10
*/

从上面代码可以看到指针变量pa pb pc的自身地址 (&pa &pb &pc) 一直没有变化,只有其保存的指针,也就是保存的地址发生了偏移变化,使其指向发生了变化;同时也可以发现每个指针指向的变量也没有改变。

2)指针 - 指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。 两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。举个例子:

#include "stdio.h"
#include"stdlib.h"


int main() 
{
	int a[10] = { 10,20,30,40,50,60,70,80,90,0 };
	int sub;
	int *p1 = &a[2];
	int *p2 = &a[8];

	sub = p2 - p1;
	printf("(%d - %d)/4 = %d\n", p2, p1, sub);// 4是每个整型数据所占的字节数,输出结果为 6
	system("pause");
	return 0;
}

从以上代码可以看出两个指针相减最终得出的是两个指针在内存中的距离。

五、指针与数组

在C语言中,指针与数组之间的关系十分密切。实际上,许多可以用数组完成的工作都可以使用指针来完成。一般来说,用指针编写的程序比用数组编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。

5.1 指针与数组的关系

我们先声明一个数组:

int a[10];        // 声明一个int类型的数组,这个数组有10个元素

我们可以用 a[0]、a[1]、…、a[9] 来表示这个数组中的10个元素,这10个元素是存储在一段连续相邻的内存区域中的。
 接下来,我们再声明一个指针:

int *p;           // 声明一个int类型的指针变量

p 是一个指针变量,指向内存中的一个区域。如果我们对指针 p 做如下的初始化:

p = &a[0];        // 对指针进行初始化,p将指向数组 a 的第 1 个元素 a[0]

我们知道,对指针进行自增操作会让指针指向与当前元素相邻的下一个元素,即 *(p + 1) 将指向 a[1] ;同样的, *(p + i) 将指向 a[i] 。因此,我们可以使用该指针来遍历数组 a[10] 的所有元素。可以看到,数组下标与指针运算之间的关系是一一对应的。而根据定义,数组类型的变量或表达式的值是该数组第 1 个元素的地址,且数组名所代表的的就是该数组第 1 个元素的地址,故,上述赋值语句可以直接写成:

p = a;        // a 为数组名,代表该数组最开始的一个元素的地址 

很显然,一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现,这就是数组和指针的互通之处。但有一点要明确的是,数组和指针并不是完全等价,指针是一个变量,而数组名不是变量,它数组中第 1 个元素的地址,数组可以看做是一个用于保存变量的容器。更直接的方法,我们可以直接看二者的地址,并不一样:

#include "stdio.h"
#include"stdlib.h"                                                                      

int main() {
	int x[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int *p = x;
	printf("x的地址为:%p\n", x);
	printf("&x的地址为:%p\n", &x);
	printf("&x[0]的地址为:%p\n", &x[0]);
	printf("p指向的地址为:%p\n", p);// 打印指针 p 指向地址,就是数组名地址,也是数组第一个元素的地址
	printf("&p的地址为:%p\n", &p);// 打印指针 p 的地址,并不是指针所指向的地方的地址
	p += 2;
	printf("&x[2]的地址为:%p\n", &x[2]);
	printf("p指向的地址为:%p\n", p);// 打印指针 p 指向地址,此时p指向地址发生了变化
	printf("&p的地址为:%p\n", &p);// 打印指针 p 的地址,前面已经讲过指针变量自身地址不变
	printf("*(p+2)的值为:%d\n", *p);// 输出结果为 3,*(p+2)指向了 x[2]
	system("pause");
	return 0;
}
/*运行结果:
x的地址为:00000046FDAFFDE8
&x的地址为:00000046FDAFFDE8
&x[0]的地址为:00000046FDAFFDE8
p指向的地址为:00000046FDAFFDE8
&p的地址为:00000046FDAFFDE0
&x[2]的地址为:00000046FDAFFDF0
p指向的地址为:00000046FDAFFDF0
&p的地址为:00000046FDAFFDE0
*(p+2)的值为:3
*/
*/

可以看到, x 的值与 x[0] 的地址是一样的,也就是说数组名即为数组中第 1 个元素的地址。打印 &x 后发现,x 的地址也是这个值。而 x 的地址与指针变量 p 的地址是不一样的。故而数组和指针并不能完全等价。

5.2 指针数组

指针是一个变量,而数组是用于存储变量的容器,因此,指针也可以像其他变量一样存储在数组中,也就是指针数组。 指针数组是一个数组,数组中的每一个元素都是指针。声明一个指针数组的方法如下:

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

在上述声明中,由于 [ ] 的优先级比 * 高,故 p 先与 [ ] 结合,成为一个数组 p[ ];再由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针。数组的第 i 个元素是 *p[i],而 p[i] 是一个指针。由于指针数组中存放着多个指针,操作灵活,在一些需要操作大量数据的程序中使用,可以使程序更灵活快速。

#include "stdio.h"
#include"stdlib.h"                                                                      

int main() {
	int x[10] = { 10,20,30,40,50,60,70,80,90,0 };
	int y[10] = { 10,20,30,40,50,60,70,80,90,0 };
	int *p[10];//定义一个指针数组,数组中是10个指向整型变量的指针元素
	p[0] = x;//另指针数组中的第一个指针指向数组x的第一个元素
	p[1] = y;//另指针数组中的第二个指针指向数组y的第一个元素
	printf("p的地址为:%p\n", p);
	printf("&p[0]的地址为:%p\n", &p[0]);
	printf("&p[1]的地址为:%p\n", &p[1]);
	printf("\n");
	printf("x的地址为:%p\n", x);//输出数组地址也是数组第一个元素地址
	printf("p[0]的地址为:%p\n", p[0]);//输出指针p[0]地址
	printf("*p[0]的值为:%d\n", *p[0]);//输出指针p[0]指向地址中的变量
	printf("移动指针p[0]后的值为:%d\n", *++p[0]);

	printf("\n");
	printf("y的地址为:%p\n", y);
	printf("p[1]的地址为:%p\n", p[1]);
	printf("*p[1]的地址为:%d\n", *p[1]);
	system("pause");
	return 0;
}
/*运行结果:
p的地址为:000000321E53F7C0
&p[0]的地址为:000000321E53F7C0
&p[1]的地址为:000000321E53F7C8

x的地址为:000000321E53F770
p[0]的地址为:000000321E53F770
*p[0]的值为:10
移动指针p[0]后的值为:20

y的地址为:000000321E53F798
p[1]的地址为:000000321E53F798
*p[1]的地址为:10
*/

从代码中可以看到指针数值就是一个元素为指针的数组;数组中的指针变量自身地址都是连续存放的,每个指针变量都可以指向不同的数组或整型变量。同时以发现了一个问题,关于指针的移动问题,(p)++和(p++)和*p++的区别
(p++)究竟是谁++,是p的地址++,还是p所指的东西的地址++
第二级:!、~、++、–、-、(类型)、
、&、sizeof。这一级都是单目运算符号,这一级的结合方向是从右向左。比如出现p++,这时和++同级别,先算右边,再左边。所以p++等价于(p++),而不是(*p)++。

比如

#include "stdio.h"
#include"stdlib.h"                                                                      

int main() {
	int arr[5] = { 1,3,5,7,9 };
	int *p = arr;
	//前缀递增递减和*优先级相同,从右到左;  
	printf("*++p为 :%d\n", *++p); //*++p:p先自+,然后*p,最终为3
	*--p;//还原
	printf("++*p为 :%d\n", ++*p); //此时++*p:p先*p,然后+,最终为2
	--*p;//还原
	printf("\n");
	//后缀递增递减比前缀优先级高,从左到右。
	printf("*p++为 :%d\n", *p++); //由于是后缀加的特性,所以会先输出p指针之前指向的元素,即arr[0],也就是说会输出1;
	printf("*p为 :%d\n", *p); //*p++:p先自+,然后*p,会使得指针指向下一个元素地址,最终为*p=3;
	*p--;//还原

	printf("*(p++)为 :%d\n", *(p++));//运行过程与后缀一样;


	system("pause");
	return 0;
}
/*运行结果:
*++p为 :3
++*p为 :2
*p++为 :1
*p为 :3
*(p++)为 :1
*/

5.3 数组指针

数组指针是一个指针,它指向一个数组。声明一个数组指针的方法如下:

int (*p)[10];        // 声明一个数组指针 p ,该指针指向一个数组

由于 ( ) 的优先级最高,所以 p 是一个指针,指向一个 int 类型的一维数组,这个一维数组的长度是 10,这也是指针 p 的步长。也就是说,执行 p+1 时,p 要跨过 n 个 int 型数据的长度。数组指针与二维数组联系密切,可以用数组指针来指向一个二维数组,如下:

#include "stdio.h"
#include"stdlib.h"                                                                      
int main() 
{
	int arr[2][3] = { 1,2,3,4,5,6 };  // 定义一个二维数组并初始化
	int(*p)[3];                   // 定义一个数组指针,指针指向一个含有3个元素的一维数组

	p = arr;                  // 将二维数组的首地址赋给 p,此时 p 指向 arr[0] 或 &arr[0][0]
	printf("%d\n", (*p)[0]); // 输出结果为 1
	//下面两条可直接简化为printf("%d\n", (*++p)[0]);
	p++;  // 对 p 进行算术运算,此时 p 将指向二维数组的下一行的首地址,即 &arr[1][0]
	printf("%d\n", (*p)[0]);        // 输出结果为4;

	system("pause");
	return 0;
}
/*运行结果:
1
4
*/

六、指针与结构

6.1 简单介绍一下结构

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看做一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。声明一个结构的方式如下:

struct message{            // 声明一个结构 message
    char name[10];             // 成员
    int age;
    int score;  
};

typedef struct message s_message;     // 类型定义符 typedef

s_message mess = {"tongye",23,83};    // 声明一个 struct message 类型的变量 mess,并对其进行初始化 

--------------------------------------------------------------------------------------------------------------
/* 另一种更简便的声明方法 */
typedef struct{
  char name[10];
  int age;
  int score;
}message;

可以使用 结构名.成员 的方式来访问结构中的成员,如下:

#include "stdio.h"
#include"stdlib.h"                                                                      

struct message {// 声明一个结构 message
	char name[10];             // 成员
	int age;
	int score;
};
typedef struct message s_message; // 类型定义符 typedef
s_message mess = { "tongye",23,83 };// 声明一个 struct message 类型的变量 mess,并对其进行初始化 

int main() 
{
	printf("%s\n", mess.name);// 输出结果:tongye
	printf("%d\n", mess.age);// 输出结果:23
	system("pause");
	return 0;
}
/*运行结果:
tongye
23
*/

6.2 结构指针

结构指针是指向结构的指针,以上面的结构为例,可以这样定义一个结构指针:

s_message *p;        // 声明一个结构指针 p ,该指针指向一个 s_message 类型的结构
*p = &mess;      // 对结构指针的初始化与普通指针一样,也是使用取地址符 &

C语言中使用 -> 操作符来访问结构指针的成员,举个例子:

#include "stdio.h"
#include"stdlib.h"                                                                      

#include "stdio.h"

typedef struct {
	char name[10];
	int age;
	int score;
}message;

int main() {
	message mess = { "tongye",23,83 };
	message *p = &mess;

	printf("%s\n", p->name);          // 输出结果为:tongye
	printf("%d\n", p->score);         // 输出结果为:83

	system("pause");
	return 0;
}
/*运行结果:
tongye
83
*/

七、指针与函数

C语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。

7.1 指针作为函数的参数

传值调用的好处是是被调函数不会改变调用函数传过来的值,可以放心修改。但是有时候需要被调函数回传一个值给调用函数,这样的话,传值调用就无法做到。为了解决这个问题,可以使用传指针调用。指针参数使得被调函数能够访问和修改主调函数中对象的值。用一个例子来说明:

#include "stdio.h"
#include"stdlib.h"                                                                      


void swap1(int a, int b)// 参数为普通的 int 变量,这里的a,b与x,y没有关系
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}

void swap2(int *a, int *b)// 参数为指针,接受调用函数传递过来的变量地址作为参数,对所指地址处的内容进行操作,这里的a,b就是指向x,y的指针,会对x,y造成影响;
{
	int temp;            // 最终结果是,地址本身并没有改变,但是这一地址所对应的内存段中的内容发生了变化,即x,y的值发生了变化
	temp = *a;
	*a = *b;
	*b = temp;
}

int main()
{
	int x = 1, y = 2;
	printf("&x = %p\n&y = %p\n", &x, &y);
	swap1(x, y);                     // 将 x,y 的值本身作为参数传递给了被调函数
	printf("x = %d y = %d\n", x, y);    // 输出结果为:1 2传值调用被调函数不会改变调用函数传过来的值
	printf("&x = %p\n&y = %p\n", &x, &y);
	printf("\n");
	printf("&x = %p\n&y = %p\n", &x, &y);
	swap2(&x, &y);                    // 将 x,y 的地址作为参数传递给了被调函数,传递过去的也是一个值,与传值调用不冲突
	printf("x = %d y = %d\n", x, y);  // 输出结果为:2 1指针参数使得被调函数修改了主调函数中对象的值
	printf("&x = %p\n&y = %p\n", &x, &y);

	system("pause");
	return 0;
}
/*运行结果:
&x = 0000001F5BD7FDD4
&y = 0000001F5BD7FDD0
x = 1 y = 2
&x = 0000001F5BD7FDD4
&y = 0000001F5BD7FDD0

&x = 0000001F5BD7FDD4
&y = 0000001F5BD7FDD0
x = 2 y = 1
&x = 0000001F5BD7FDD4
&y = 0000001F5BD7FDD0
*/

7.2 指向函数的指针

在C语言中,函数本身不是变量,但是可以定义指向函数的指针,也称作函数指针,函数指针指向函数的入口地址。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。 声明一个函数指针的方法如下:

返回值类型 (* 指针变量名)([形参列表];
int (*pointer)(int *,int *);        // 声明一个函数指针

上述代码声明了一个函数指针 pointer ,该指针指向一个函数,函数具有两个 int * 类型的参数,且返回值类型为 int。下面的代码演示了函数指针的用法:

#include "stdio.h"
#include"stdlib.h"                                                                      
#include "string.h"

int str_comp(const char *m, const char *n);  // 声明一个函数 str_comp,该函数有两个 const char 类型的指针,函数的返回值为 int 类型
void comp(char *a, char *b, int(*prr)(const char *, const char*)); // 声明一个函数 comp ,注意该函数的第三个参数,是一个函数指针

int main()
{
	char str1[20];      // 声明一个字符数组
	char str2[20];
	int(*p)(const char *, const char *) = str_comp;// 声明并初始化一个函数指针,该指针所指向的函数有两个 const char 类型的指针,且返回值为 int 类型
	gets(str1);         // 使用 gets() 函数从 I/O 读取一行字符串
	gets(str2);
	comp(str1, str2, p);  // 函数指针 p 作为参数传给 comp 函数
	system("pause");
	return 0;
}


int str_comp(const char *m, const char *n)
{
	// 库函数 strcmp 用于比较两个字符串,其原型是: int strcmp(const char *s1,const char *s2);
	if (strcmp(m, n) == 0)
		return 0;
	else
		return 1;
}

/* 函数 comp 接受一个函数指针作为它的第三个参数 */
void comp(char *a, char *b, int(*prr)(const char *, const char*))
{
	if ((*prr)(a, b) == 0)
		printf("str1 = str2\n");
	else
		printf("str1 != str2\n");

}

这段代码的功能是从键盘读取两行字符串(长度不超过20),判断二者是否相等。
  注意,声明一个函数指针时,() 不能漏掉, 否则:

int *p(void *,void*);// 这表明 p 是一个函数,该函数返回一个指向 int 类型的指针。

正确类型为

int (*p)(void *,void*);//声明的函数指针指向的函数有两个指向任意类型指针参数且返回值为int型

参考文献:

1)C程序设计语言(第2版)

2)C和指针

参考:

C程序设计语言(第四版)
https://www.cnblogs.com/tongye/p/9650573.html

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值