C语言·指针

指针

1.指针

1.1 概念

内存中每个字节都有一个编号,这个编号叫做指针,也叫作地址。

专门用来存储这个编号的变量就叫做指针变量

只不过,平时交流的时候,都是称呼:

指针:指针变量

地址:内存编号

1.2 指针相关的操作

& :取地址符,获取变量的地址

对于多字节的变量取地址,取到的是编号最小的那个,我们称之为首地址

*:在定义指针时,只起到一个标识作用,标识定义的是一个指针变量

其他场景下,标识操作指针保存的地址里面的内容

1.3 指针和变量的关系

在这里插入图片描述

1.4 指针的基本使用

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//当程序执行到定义变量的语句时,
	//操作系统会根据变量的类型给变量分配对应大小的空间
	int a = 10;

	//通过变量名是可以直接操作对应的内存空间的
	a = 520;
	printf("a = %d\n", a);//520

	//通过 & 可以获取变量的地址  使用 %p 输出
	printf("&a = %p\n", &a);

	//可以定义一个指针来保存变量的地址
	//定义指针的格式  : 数据类型 *指针变量名;
	int *p;
	p = &a;	//p保存了a的地址  我们称之为 p只向a
	printf("p = %p\n", p);

	//当指针保存了变量的地址后
	//就可以通过指针访问该变量对应的内存空间中的内容了
	*p = 1314;
	printf("*p = %d,  a = %d\n", *p, a); //1314 1314

	//不能使用普通的变量来保存地址
	//long value = &a;//保存是可以的
	//printf("value = %#lx\n", value);
	//*value = 1234;//但是普通变量不能做 * 操作
	
	//指针只能保存已经分配了的地址 由操作系统分的或者malloc分的
	//int *p2 = 0x12345678;  //这是在给 p2 赋值 不是 给 *p2赋值
	//printf("p2 = %p\n", p2);
	//*p2 = 100;//即使保存了 操作也很危险 错误是不可预知的
	
	//指针的类型的作用:决定指针从保存的地址开始能访问几个字节
	//int * 指针能访问4个字节
	//char * 指针只能访问保存的那一个字节
	//一般情况下 指针的类型要和只向的变量的类型保持一致
	
	//可以在一行中定义多个指针 但是下面的用法是错误的
	//int *p3,p4; //这种写法 p3是指针 p4就是普通的int变量
	int *p3, *p4; //这种写法 两个才都是指针

	//定义指针如果没有初始化 里面也是随机值
	//也就是说只向了一个随机的地址,这种指针称之为 ----野指针
	//野指针是有害的
	//int *p5;
	//printf("p5 = %p\n", p5);
	//*p5 = 100;//对野指针取*操作 错误是不可预知的

	//如果定义指针时不知道指向谁 但是又想防止野指针
	//可以用 NULL 来给指针初始化   NULL的本质是   (void *)0
	int *p6 = NULL;	//叫做空指针
	*p6 = 1314; //对NULL取*操作一定是段错误

	return 0;
}

1.5 指针变量的大小

32位系统中 指针的大小是4字节

64位系统中 指针的大小是8字节

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char *p1;
	short *p2;
	int *p3;
	long *p4;
	long long *p5;
	float *p6;
	double *p7;
	printf("sizeof(char *) = %ld  sizeof(p1) = %ld\n", sizeof(char *), sizeof(p1));
	printf("sizeof(short *) = %ld  sizeof(p2) = %ld\n", sizeof(short *), sizeof(p2));
	printf("sizeof(int *) = %ld  sizeof(p3) = %ld\n", sizeof(int *), sizeof(p3));
	printf("sizeof(long *) = %ld  sizeof(p4) = %ld\n", sizeof(long *), sizeof(p4));
	printf("sizeof(long long *) = %ld  sizeof(p5) = %ld\n", sizeof(long long *), sizeof(p5));
	printf("sizeof(float *) = %ld  sizeof(p6) = %ld\n", sizeof(float *), sizeof(p6));
	printf("sizeof(double *) = %ld  sizeof(p7) = %ld\n", sizeof(double *), sizeof(p7));

	return 0;
}

1.6 指针的运算

指针里面存的都是地址编号,所以,指针的运算,本质就是地址作为运算量来参与运算。

既然是地址的运算,能做的操作就是有限的了。

只有相同类型的指针之间做运算,才是有意义的。

指针能做的运算

算数运算 + - ++ –

关系运算 > < == !=

赋值运算 =

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//指针+一个整数n  表示加上n个指针的数据类型的大小
	int s[5] = {10,20,30,40,50};
	int *p1 = &s[0];
	int *p2 = p1+2;
	printf("p1 = %p  p2 = %p\n", p1, p2);//相差8  2*sizeof(int)
	printf("*p1 = %d  *p2 = %d\n", *p1, *p2);//10  30

	//指针的减法
	//两个指针做差:结果是相差的数据类型的个数  并不是字节数!!!
	int *p3 = &s[0];
	int *p4 = &s[3];
	printf("p3 = %p  p4 = %p\n", p3, p4);//相差 12
	printf("p4-p3 = %ld\n", p4-p3); //得到的是 3  表示相差 3个int

	//指针的强转是安全的 因为指针的大小都是一样的
	short *p5 = (short *)&s[0];
	short *p6 = (short *)&s[3];
	printf("p5 = %p  p6 = %p\n", p5, p6);//相差 12
	printf("p6-p5 = %ld\n", p6-p5);//6 表示相差 6个short

	
	//要注意下面的用法
	//int s[5] = {10,20,30,40,50};
	int *p7 = &s[0];
	int value1 = *++p7; //先算 ++p7 相当于p7的指向向后移动了一个int 然后对新的p7取*
	printf("value1 = %d  p7 = %p\n", value1, p7);//20  &s[1]
	
	int *p8 = &s[0];
	int value2 = *p8++;  //先算p8++ 但是注意 取*是对 p8++这个表达式的结果取*
						//表达式的结果是  p8 加之前的值  也就是 s[0] 的地址
	printf("value2 = %d  p8 = %p\n", value2, p8);//10  &s[1]

	int *p9 = &s[0];
	int value3 = (*p9)++; //这种写法 等价与  int value3 = s[0]++;
	printf("value3 = %d  p9 = %p\n", value3, p9);//10  &s[0]
	printf("s[0] = %d\n", s[0]);// 11

	//指针的关系运算
	if(p3 < p4){
		printf("yes\n");
	}else if(p3 > p4){
		printf("no\n");
	}else if(p3 == p4){
		printf("p3 == p4\n");
	}
	

	//指针的赋值运算
	//指针变量本质也是变量 是变量 就允许进行赋值运算
	int a = 10;
	int b = 20;
	int *pp1 = &a;
	int *pp2 = &b;
	pp1 = pp2;

	return 0;
}

思考,下面的代码会输出什么?

int *p = NULL;
printf("%d %d %d\n", p+1, p, (p+1)-p); //4 0 1

编码

有一个以空格为分隔符的字符串。

“this is a book”

要求以单词为单位进行翻转。

“book a is this”

//思路
//1.先整体翻转
//2.再以单词为单位,每个单词翻转
#include <stdio.h>
int main(){
    char s[64] = "this is a book";
    printf("整体翻转前:[%s]\n", s);
    //1.先整体翻转
    int i = 0;
    while(s[i])i++;
    i--;
    int j = 0;
    char temp = 0;
    while(j < i){
        temp = s[i];
        s[i] = s[j];
        s[j] = temp;
		i--;
		j++;
    }
    printf("整体翻转后:[%s]\n", s);
    //2.再以单词为单位翻转
	i = 0;
	j = 0;
    int k = 0;
    while(1){
        while(s[i] != ' ' && s[i] != '\0'){
            i++;
        }
        k = i;//k用来记录当前单词的后一个字符
        i--;
        while(j < i){
            temp = s[j];
            s[j] = s[i];
            s[i] = temp;
            j++;
            i--;
        }
        if(s[k] == '\0'){
            break;
        }
        i = k+1;
        j = k+1; 
    }
    printf("完全翻转后:[%s]\n", s);
    
    return 0;
}

2、大小端存储

练习:

定义一个变量a,类型为int,里面存储8888;

定义一个指针p,类型为int *,指向变量a,通过指针p将a的数据改成 0x12345678;

定义一个指针q,类型为 char *, 也指向变量a

分别使用 %#x 输出 *q *(q+1) *(q+2) *(q+3) 的值。

#include <stdio.h>
int main(){
    int a = 8888;
    int *p = &a;
    *p = 0x12345678;
    char *q = (char *)&a;
    printf("*(q+0) = %#x\n", *(q+0));
    printf("*(q+1) = %#x\n", *(q+1));
    printf("*(q+2) = %#x\n", *(q+2));
    printf("*(q+3) = %#x\n", *(q+3));
    
    return 0;
}

执行结果:

在这里插入图片描述

2.1 大小端存储的问题

根据CPU和操作系统的不同,对多字节的数据存储方式也不同。

小端存储:地址低位存储数据低位 地址高位存储数据高位

大端存储:地址低位存储数据高位 地址高位存储数据低位

在这里插入图片描述

请编写一个简单的程序,判断你使用的主机是大端存储还是小端存储?

#include <stdio.h>
int main(){
    int a = 0x12345678;
    char *p = (char *)&a;//&a 是地址地位
    if(0x78 == *p){
        printf("小端\n");
    }else if(0x12 == *p){
        printf("大端\n");
    }

    return 0;
}     

​ 思考:

在小端存储的主机下,下面的代码会输出什么?

int x = 0x41424344;
printf("%s\n", &x); // DCBA+不确定的值

2.2 指针和一维数组

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[5] = {10, 20, 30, 40, 50};
	//数组名就是数组的首地址 是一个地址常量
	//操作空间和数组的类型是一致的
	printf("s = %p\n", s);
	printf("s+1 = %p\n", s+1);//相差1个int

	//研究 数组名[下标] 访问成员的本质
	printf("s[0] = %d  *(s+0) = %d\n", s[0], *(s+0));//10 10
	printf("s[1] = %d  *(s+1) = %d\n", s[1], *(s+1));//20 20
	printf("s[2] = %d  *(s+2) = %d\n", s[2], *(s+2));//30 30
	//也就是说 数组名[下标] 方式访问成员 本质是
	//以数组首地址为基准 指针的偏移取数据
	//  s[i]  <==>  *(s+i)
	
	//可以定义一个指针指向一位数组
	//一般保证操作空间一致  我们都定义和数组类型相同的指针来保存地址
	//指针保存数组首地址有下面的写法
	int *p1 = s;  //数组名就是数组首地址  --最常用的写法
	int *p2 = &s[0]; //这样也可以
	int *p3 = &s;//这种写法 保存的也是数组的首地址
				//但是对数组名取地址 指针操作空间就变了 后面数组指针时再说
				//一般不使用这种写法
	printf("p1 = %p  p2 = %p  p3 = %p\n", p1, p2, p3);//一样的

	//当指针指向一维数组后 有下面的等价关系
	// s[i] <==> *(s+i)  <==> *(p+i)  <==> p[i]
	
	//一维数组的遍历
	int i = 0;
	for(i = 0; i < 5; i++){
		//printf("%d  ", s[i]);
		//printf("%d  ", *(s+i));
		//printf("%d  ", *(p1+i));
		printf("%d  ", p1[i]);
	}
	printf("\n");

	//指针和数组名的区别
	//数组名是常量 不能被赋值 也不能++
	//指针是变量  可以被重新赋值 也可以++
	
	//关于指针 ++ 操作的意义
	int *p4 = s;
	for(i = 0; i < 5; i++){
		printf("%d  ", *p4);
		p4++;
	}
	printf("\n");

	return 0;
}

练习:

1.思考:在32位小端存储的主机上,下面的代码会输出什么?

int s[5] = {1,2,3,4,5};
int *p = (int *)((int)s+1);
printf("%x\n", *p);// 2000000

2.使用指针实现 strlen 函数的功能

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char s[32] = "hello world";
	int count = 0;
#if 0
	char *p = s;
	while(*p != '\0'){
		count++;
		p++;
	}
#endif
	
#if 0
	int i = 0;
	char *p = s;
	while(*(p+i)){
		count++;	
		i++;
	}
#endif

#if 0
	int i = 0;
	char *p = s;
	while(p[i]){
		count++;
		i++;
	}
#endif
	
	//char s[32] = "hello world";
	char *p = s;
	while(*p++) count++;
    //上面的循环结束时  p指向谁字符串s的'\0'的后面一位
	printf("count = %d\n", count);

	return 0;
}

3.使用指针实现 strcpy 函数的功能

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char s1[32] = "hello";
	char s2[32] = "beijing";
	char *p = s1;
	char *q = s2;
#if 0
	while(*q != '\0'){
		*p = *q;
		p++;
		q++;
	}
#endif
	while(*q){
		*p++ = *q++;
	}

	*p = *q;	// '\0'也得拷贝
	//因为前面的操作中 p和q的指向已经变了 不是指向s1和s2的首地址了
	//所以需要重置一下指向
	p = s1;
	q = s2;
	printf("%s\n", p);
	printf("%s\n", q);
	
	return 0;
}

4.使用指针实现 strcat 函数的功能

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char s1[32] = "hello";
	char s2[32] = "world";
	char *p = s1;
	char *q = s2;
	while(*p++);
	//上面循环结束时 p指向 s1的'\0'的后一位
	p--;  //让p指向s1的 '\0'

	//依次追加
	while(*q){
		*p++ = *q++;
	}
	*p = *q;//将 s2 的'\0'也追加给s1

	printf("s1 = [%s]\n", s1);
	printf("s2 = [%s]\n", s2);

	return 0;
}

2.3 指针和二维数组

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[3][4] = {{1,2,3,4},
					{5,6,7,8},
					{9,10,11,12}};
	//二维数组的数组名也是一个地址常量 是数组的首地址
	printf("s = %p\n", s);
	printf("s+1 = %p\n", s+1);//相差4个int 也就是一行元素
	//二维数组的数组名操作空间是一整行元素 叫做行指针
	
	//对行指针取一个*操作 相当于对指针的降维
	//把操作空间是一整行的指针降维成操作空间是一个元素的指针
	printf("*s = %p\n", *s);
	printf("*s+1 = %p\n", *s+1);//相差1个int

	//对降维后的指针再取*操作 才是操作数据
	printf("**s = %d\n", **s);//1
	printf("*(*s+1) = %d\n", *(*s+1));//2
	printf("*(*(s+1)+1) = %d\n", *(*(s+1)+1));//6
	printf("*(s[2]+2) = %d\n", *(s[2]+2));//11

	//也就是说 有如下的等价关系
	//s[i][j] <==> *(*(s+i)+j) <==>  *(s[i]+j)
	
	//注意:由于二维数组的数组名是一个行指针 操作空间已经超过基本类型了
	//所以不能使用普通的指针来保存二维数组的首地址
	//int *p = s;
	//printf("p = %p\n", p);
	//*(p+5) = 1314;
	//printf("%d\n",s[1][1]);
	//即使保存了 也只能按照单个元素操作 不能按照整行操作
	//因为 p 就是一个普通的 int * 指针
	//如果想保存二维数组的首地址 需要用到 数组指针

	//二维数组的遍历
	int i = 0;
	int j = 0;
	for(i = 0; i < 3; i++){
		for(j = 0; j < 4; j++){
			//printf("%d  ", s[i][j]);
			//printf("%d  ", *(s[i]+j));
			printf("%d  ", *(*(s+i)+j));
		}
		printf("\n");
	}

	return 0;
}

2.4 数组指针

本质是一个指针,指向一个二维数组,也叫作行指针。

数组指针多用于二维数组作为函数的参数传递时。

格式:

   数据类型  (*数组指针名)[列宽];

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[3][4] = {{1,2,3,4},
					{5,6,7,8},
					{9,10,11,12}};
	
	//定义了一个数组指针p 指向二维数组s
	int (*p)[4] = s;

	//当指针保存了二维数组的首地址后 操作就和 数组名操作是一样的了
	//有下面的等价关系
	// s[i][j]  <==> *(s[i]+j)  <==>  *(*(s+i)+j)  <==>
	// p[i][j]  <==> *(p[i]+j)  <==>  *(*(p+i)+j)
	
	//二维数组的遍历
	int i = 0;
	int j = 0;
	for(i =0; i < 3; i++){
		for(j = 0; j < 4; j++){
			//printf("%d  ", s[i][j]);
			//printf("%d  ", *(s[i]+j));
			//printf("%d  ", *(*(s+i)+j));
			//printf("%d  ", p[i][j]);
			//printf("%d  ", *(p[i]+j));
			printf("%d  ", *(*(p+i)+j));
		}
		printf("\n");
	}

	//p和s的区别
	//p 是指针 是变量
	//s 是数组名 是常量

	return 0;
}

之所以不能对一维数组数组名取地址的原因:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[5] = {1,2,3,4,5};

	//对数组名s 取&操作 相当于 对指针的升维操作
	//把本来操作空间是一个元素的指针 升维 成操作空间是一行的指针
	//而我们的p就是一个普通的 int * 指针, 操作空间只能是一个元素
	//所以 编译时 类型不匹配 会报警告
	//int *p = &s;
	//printf("%d\n", p[3]); //4
	
	//可以使用数组指针来消除这个警告
	int (*p)[5] = &s;
	//但是注意 虽然警告消除了 但是此时的p就没有太大的意义了
	//因为p的操作空间是一行  而我们的一维数组 一共就只有1行
	//所以 p+1 就已经越界访问了
	//所以不要对 一维数组的数组名取地址 !!!

	return 0;
}

2.5 指针数组

本质是一个数组,数组中每个元素都是一个指针。

格式:

  数据类型 *指针数组名[下标];

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//可以使用二维数组来处理多个字符串
	char name[4][64] = {
			"zhangsan",
			"lisi",
			"fulajimier.fulajimiluoweiqi.pujing",
			"zhaoliu"
		};
	printf("%s\n", name[0]);
	printf("%s\n", name[1]);
	printf("%s\n", name[2]);
	printf("%s\n", name[3]);
	//但是这种处理方式不太好  因为需要以最长的字符串为准 会造成很多空间上的浪费
	
	printf("-------------------------------\n");

	//也可以使用指针数组来处理
	//定义了一个指针数组,数组名叫 p_name 
	//数组中共有4个元素 每个元素都是一个 char * 类型的指针
	char *p_name[4] = {NULL};
	//当取出数组的元素后 操作就和操作普通的 char * 指针 是一样的了
	p_name[0] = "zhangsan";
	p_name[1] = "lisi";
	p_name[2] = "fulajimier.fulajimiluoweiqi.pujing";
	p_name[3] = "zhaoliu";

	printf("%s\n", p_name[0]);
	printf("%s\n", p_name[1]);
	printf("%s\n", p_name[2]);
	printf("%s\n", p_name[3]);

	printf("sizeof(p_name) = %ld\n", sizeof(p_name));//32 == 4 * sizeof(char *)

	return 0;
}

2.6 指针和字符串

虚拟内存分区

在这里插入图片描述

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//可以定义一个指针 直接指向字符串常量
	char *p1 = "hello world";	//字符串常量在字符串常量区
							//是只读的 不能修改的
	printf("p1 = %s\n", p1);//读取没问题
	//*p1 = 'H';	//错误的 修改就段错误
	
	char *p2 = "hello world";
	printf("p1 = %p  p2 = %p\n", p1, p2);//不管定义多少个指针
			//只要指向同一个字符串常量 那么地址就是一样的
	
	printf("---------------------------------\n");

	//也可以定义一个数组来保存字符串
	//s1是局部的 在栈区 由操作系统分配空间
	//相当于用字符串常量 "hello world" 来初始化数组
	char s1[32] = "hello world";
	//栈区的内容是允许修改的
	printf("s1 = %s\n", s1);//hello world
	*s1 = 'H';
	printf("s1 = %s\n", s1);//Hello world

	char s2[32] = "hello world";
	printf("s1 = %p  s2 = %p\n", s1, s2);//不一样

	return 0;
}

2.7 二级指针

二级指针是用来保存一级指针的地址

二级指针多用于将一级指针作为函数的参数传递时。

变量、一级指针、二级指针的关系图:

在这里插入图片描述

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int *p = &a;
	int **q = &p;
	
	//上面三步执行完 有如下的等价关系
	// a  <==> *p  <==> **q
	// &a <==> p   <==> *q
	// &p <==> q
	
	printf("a = %d  *p = %d  **q = %d\n", a, *p, **q);//一样的
	printf("&a = %p  p = %p  *q = %p\n", &a, p, *q);//一样的
	printf("&p = %p q = %p\n", &p, q);//一样的
	**q = 520;//正确的 给a赋值
	printf("a = %d  *p = %d  **q = %d\n", a, *p, **q);//一样的

	//不能使用一级指针来保存一级指针的地址
	//int *temp = &p;
	//printf("temp = %p\n", temp);
	//**temp = 1314;//即使保存了 也没有用 因为一级指针不能取 ** 操作

	return 0;
}

编码:

自己编写代码实现 atoi() 函数的功能 ---- 字符串转整型

//思路
//"1234"  --> 1234
//((1*10+2)*10+3)*10+4

#include <stdio.h>

int main(){
    char s[10] = "4399";
    int num = 0;
    char *p = s;
    while(*p != '\0'){
        num *= 10;
        num += (*p-'0');
        p++;
    }
    printf("%d\n", num);
    return 0;
}
//上面三步执行完 有如下的等价关系
// a  <==> *p  <==> **q
// &a <==> p   <==> *q
// &p <==> q

printf("a = %d  *p = %d  **q = %d\n", a, *p, **q);//一样的
printf("&a = %p  p = %p  *q = %p\n", &a, p, *q);//一样的
printf("&p = %p q = %p\n", &p, q);//一样的
**q = 520;//正确的 给a赋值
printf("a = %d  *p = %d  **q = %d\n", a, *p, **q);//一样的

//不能使用一级指针来保存一级指针的地址
//int *temp = &p;
//printf("temp = %p\n", temp);
//**temp = 1314;//即使保存了 也没有用 因为一级指针不能取 ** 操作

return 0;

}


编码:

自己编写代码实现 atoi() 函数的功能 ---- 字符串转整型

//思路
//“1234” --> 1234
//((1*10+2)*10+3)*10+4

#include <stdio.h>

int main(){
char s[10] = “4399”;
int num = 0;
char *p = s;
while(*p != ‘\0’){
num *= 10;
num += (*p-‘0’);
p++;
}
printf(“%d\n”, num);
return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值