data/method/一些思考/工作材料/071—指针(上)——指针初级.txt

528 lines
18 KiB
Plaintext
Raw Normal View History

2024-01-29 10:44:43 +08:00
一、指针是什么
指针是C语言的重要概念也是C语言及其扩展语言C++等的重要特色。指针可以使程序变得简洁、紧凑、高效。每一个学习C语言的人都应当深入学习和掌握指针。可以说没有掌握指针就是没有学会C语言。
指针的概念复杂也比较灵活,因此初学者经常会犯错。请同学们务必多思考、多练习、谨慎使用。
/********地址与指针的关系与区别*********/
在计算机内存中,每个存储单元都有一个特定的唯一的编号,称为地址。
在32位计算机中地址是由32位bit构成的2进制数占4字节byte通常由一个8位的16进制数来表示。
示例地址0x00110023(16进制)-------->0000 0000 0001 0001 0000 0000 0010 0011(2进制)
在C语言中专门用来保存地址的变量称为“指针变量”简称“指针”。在不影响理解的前提下“指针”、“指针变量”和“地址”不做区分统称“指针”。
/********地址与指针的关系与区别end******/
二、指针变量
从上文我们知道:存放地址的变量是指针变量,它可以指向另一个对象(如变量、数组、函数等)。
1、定义一个指针变量
用法:
<数据类型名> *<指针变量名>
说明:
1.<数据类型名>:指针指向的数据类型,是指针的目标的数据类型,而非指针本身的数据类型
2.*<指针变量名>:指针变量的变量名。前面加*代表这是一个指针变量
注意:定义指针时必须指定指针目标的数据类型名,这样才能够正确地按照数据类型取出数据。
/*******指针的数据类型*******/
在C语言的四类数据类型中指针是单独的一类数据类型。指针数据类型无法独立存在必须配合其他数据类型基本数据类型构造数据类型空类型一起使用表示指向该类型的指针。例如
int a;表示int类型变量a
int *p;表示该指针是指向int类型的指针
/*******指针的数据类型end****/
如:
char *s;//定义一个指向char类型数据的指针变量s
int *a;//定义一个指向int类型数据的指针变量a
float *f;//定义一个指向float类型数据的指针变量f
注意在C语言的官方文档内推荐定义指针时指针运算符*要紧贴指针变量名,而不要挨着数据类型名。
2、引用一个指针变量
在这里需要熟练掌握两个与指针有关的运算符:&和*
&:取地址运算符
*:指针运算符(或称“间接访问运算符”)
1给指针变量赋值
如果我们已经定义了一个int型变量a和一个int型指针p
int a;
int *p;
则我们可以使用&给指针变量赋值即让指针变量p指向变量a
p = &a;//将a的地址取出赋值给指针变量p
2引用指针变量所指向的内容
如果已执行p = &a表示指针p已指向变量a则我们可以使用指针p来引用变量a。如
printf("%d\n",*p);//*p表示将指针变量p的内容取出
或者可以通过指针对指向的变量进行操作。如:
*p = 1;//相当于a = 1
3打印指针变量
指针变量也是可以输出的,用格式控制%p
printf("%p",p);//打印指针p所存放的地址
4指针变量的初始化
我们可以在定义指针变量的时候对指针变量进行初始化。如:
int *p = &a;//初始化指针变量p指向变量a
示例输入a和b两个整数按先大后小的顺序输出这两个整数。要求使用指针。
#include<stdio.h>
int main()
{
int *p1,*p2,*tmp;
int a,b;
printf("请输入两个整数:\n");
scanf("%d%d",&a,&b);
p1 = &a;//让p1指向a
p2 = &b;//让p2指向b
if(*p1 < *p2)//相当于比较a<b
{
tmp = p1;
p1 = p2;
p2 = tmp;//交换指针而没有交换变量a和b
}
printf("较大的是%d\n较小的是%d\n",*p1,*p2);
return 0;
}
输入5 9
输出较大的是9 较小的是5
练习输入3个整数按由小到大的顺序输出这3个整数。要求使用指针。
答案1
//答案1是使用指针引用了指向内容并交换了变量的数值
//即程序执行后变量a、b、c的值可能发生改变
#include<stdio.h>
int main()
{
int a,b,c,tmp;
int *p1,*p2,*p3;
printf("请输入3个整数\n");
scanf("%d%d%d",&a,&b,&c);
p1 = &a;
p2 = &b;
p3 = &c;
if(*p1 > *p2 && *p1>*p3)
{
tmp = *p1;
*p1 = *p3;
*p3 = tmp;
}
if(*p2 > *p1 && *p2>*p3)
{
tmp = *p2;
*p2 = *p3;
*p3 = tmp;
}
//经过这两个判断语句后*p3即变量c中存的是3个数的最大值。再比较*p1即变量a和*p2即变量b的大小即可
if(*p1>*p2)
{
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
printf("%d %d %d\n",*p1,*p2,*p3);
return 0;
}
答案2
//答案2是比较指针中的引用内容并交换指针的指向对象而未改变变量中的数值
//即程序执行后变量a、b、c的值未发生改变而分别指向它们的指针可能发生了改变
#include<stdio.h>
int main()
{
int a,b,c;
int *p1,*p2,*p3,*tmp;
printf("请输入3个整数\n");
scanf("%d%d%d",&a,&b,&c);
p1 = &a;
p2 = &b;
p3 = &c;
if(*p1 > *p2 && *p1>*p3)
{
tmp = p1;
p1 = p3;
p3 = tmp;
}
if(*p2 > *p1 && *p2>*p3)
{
tmp = p2;
p2 = p3;
p3 = tmp;
}
//经过这两个判断语句后*p3中存的是3个数的最大值。再比较*p1和*p2的大小即可。注意此时指针p1、p2、p3的指向对象可能改变
if(*p1>*p2)
{
tmp = p1;
p1 = p2;
p2 = tmp;
}
printf("%d %d %d\n",*p1,*p2,*p3);
return 0;
}
5指针变量占内存空间大小
指针变量存储的是一个32位2进制数8位16进制数因此占内存4字节。
注意指针变量存储的是一个地址值而地址是4字节因此指针大小就是4字节。指针占内存空间大小与指针类型无关。
示例:
#include<stdio.h>
int main()
{
int a,*p;
char c,*q;
p = &a;
q = &c;
printf("sizeof(p) is %d\n",sizeof(p));//指针大小
printf("sizeof(*p) is %d\n",sizeof(*p));//指针指向的内容int类型大小
printf("sizeof(q) is %d\n",sizeof(q));//指针大小
printf("sizeof(*q) is %d\n",sizeof(*q));//指针指向的内容char类型大小
return 0;
}
输出结果4、4、4、1
三、使用指针变量
1、使用指针变量引用一般变量
上文已讲过,不再赘述
/***************大端序和小端序*******************/
思考若有int类型变量0x12345678能否使用char*类型指针指向该变量并使用指针取出该内存内存储的值?若可以,读取的值是什么?
int a;
char *p;
p = (char*)&a;
大端序:高地址存储数据低位,低地址存储数据高位
小端序:高地址存储数据高位,低地址存储数据低位
大端序常用于网络通信领域,小端序常用于操作系统领域
/***************大端序和小端序end****************/
2、使用指针变量引用数组
1数组元素的指针
数组存放在内存中,一个数组包含若干元素,每个数组元素都在内存中占据存储单元,都有相应的地址。我们可以使用指针来访问或操作一个数组。例如:
int a[10]={1,2,3,4,5,6,7,8,9,10};
int *p;
则让指针p指向数组首地址即数组下标为0的元素的地址
p = &a[0];
或可以直接写成:
p = a;//注意不要写成p = &a;
因为数组名即为数组的首地址即数组下标为0的元素的地址因此我们可以直接把这个地址赋值给指针。
同样对于指向数组的指针的初始化可以写成:
int *p = &a[0];
或:
int *p = a;
2对指向数组元素的指针进行运算
如果指针已经指向了数组元素,则该指针可以进行如下运算:
1.加一个整数(用+或+=。如p += 1;
2.减一个整数(用-或-=。如p -= 1;
3.自增。如p++;和++p;
4.自减。如p--;和--p;
5.两个指针相减。如p1-p2;只有p1和p2都指向同一数组元素才有意义
说明:
1.如果指针变量p已经指向数组中的一个元素则p+1指向这个元素的下一个元素p-1指向这个元素的上一个元素。p++与p--同理
注意C语言不检查数组越界的问题因此操作指针时要小心数组的边界不要越界
2.执行p+1时不是简单地将p的值所存储的地址加1而是加上一个数组元素所占的字节数。例如如果数组是int型或float型则p+1相当于地址+4因为int型和float型占4字节如果数组是char型则p+1相当于地址+1因为char型占1字节
示例:
#include<stdio.h>
int main()
{
int a[5]={1,2,3,4,5};
char c[]="Hello World";
int *p = a;
char *q = c;
printf("p的初始地址是%p\nq的初始地址是%p\n",p,q);
p++;
q++;
printf("p++后的地址是%p\nq++后的地址是%p\n",p,q);
return 0;
}
3.*p表示当前指针指向的元素的值*(p+i)代表指针在当前位置的第i个之后的元素的值。例如如果p现在指向数组首地址即a[0]),则*(p+5)就是a[5]的值。
思考:*(p+i)和*p+i的意思一样吗
4.如果指针p1和p2都指向同一数组如果执行p1-p2则表示两个指针的元素个数之差。这样的运算是有意义的可以得到这两个指针所指的元素间的相对位置。如
p1指向a[1]p2指向a[4],则
p2-p1=3即a[1]偏移3个地址可以找到a[4]
注意p1和p2不能相加两个指针相加是无意义运算。
3通过指针引用数组元素
如果一个指针已经指向了数组那么引用数组元素可以有2种方法
下标法使用数组下标的形式。如a[i]的形式
指针法:先让指针偏移到适当位置,然后使用指针运算符。如*(p+i)的形式
练习自定义一个整型数组有10个元素使用下标法和指针法分别输出这10个元素
答案:
//下标法
#include<stdio.h>
int main()
{
int a[10]={5,4,2,3,1,8,7,9,10,6};
int i;
for(i=0;i<10;i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
//指针法1
#include<stdio.h>
int main()
{
int a[10]={5,4,2,3,1,8,7,9,10,6};
int *p = a;
int i;
for(i=0;i<10;i++)
{
printf("%d ",*(p+i));
}
printf("\n");
return 0;
}
//指针法2
#include<stdio.h>
int main()
{
int a[10]={5,4,2,3,1,8,7,9,10,6};
int *p;
int i;
for(p=a;p<(a+10);p++)
{
printf("%d ",*p);
}
printf("\n");
return 0;
}
思考指针法1和指针法2的指针移动是否相同
/*************** *p++的用法****************/
有指针p是指向了数组a[]的指针。我们考虑这种用法:*p++
示例:
#include<stdio.h>
int main()
{
int a[10]={5,4,2,3,1,8,7,9,10,6};
int i = 1;
int *p = a;
while(i<=10)
{
printf("%d ",*p++);//在这里使用*p++
i++;
}
printf("\n");
return 0;
该程序的执行结果等同于上面的练习题答案。
对于*p++运算,因为*与++的运算优先级相同,则根据结合性,相当于*(p++)
对于*p++的运算C语言编译器做了4步操作
1.复制一个p的副本
2.将原始的p进行++运算
3.输出p的副本指向的内容值即计算*p副本
4.删除p的副本
实际上第3步与第4步是一起完成的。
初学者在使用*p++时一定要理解它的计算流程,谨慎使用
思考: *p++ 和 (*p)++两种用法结果相同吗?
/*************** *p++的用法end***************/
3、用指针代替数组名做函数参数
在“函数”部分的学习中我们已经接触过,将数组作为函数的形参和实参的写法:
void fun(int arr[],int n)//形参
{
……
}
int main()
{
int a[10];
fun(a,10);//实参
}
针对函数fun()的第一个形参,我们还可以使用指针的形式。即写成:
void fun(int *arr,int n)
这样在fun()函数中引用arr数组的第i个元素可以有2种形式
arr[i] 或 *(arr+i)
这样做的原因是因为无论形参是int arr[]还是int *arr编译器都把它当做指针处理即都视为int *arr这样来说int arr[]和int *arr在形参列表中二者是等价的。
练习1自定义一个int型数组将数组中所有元素反序存放。要求使用指针传参。
答案:
#include<stdio.h>
#define MAX 10
void reserve(int *a,int n)
{
int *p,*q;
int tmp;
p = a;//p指向数组第一个元素
q = a+(n-1);//q指向数组最后一个元素
while(p!=q && (p-1)!=q)//需考虑奇数个元素个数和偶数个元素个数不同的情况
{
tmp = *p;
*p = *q;
*q = tmp;
p++;
q--;
}
}
int main()
{
int a[MAX]={55,11,99,77,33,22,88,44,66,110};
int i;
reserve(a,MAX);
for(i=0;i<MAX;i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
练习2实现冒泡排序函数Bubble其中Bubble函数的数组的形参使用指针
答案:
#include<stdio.h>
#define MAX 10
void Bubble(int *a,int n)
{
int i,j,tmp;
for(i=0;i<n-1;i++)//外层循环控制循环次数为n-1次
{
for(j=0;j<n-i-1;j++)//内层循环控制当前的第j个元素与其身后的元素进行比较
{
if(a[j]>a[j+1])//如果前一个元素比后一个元素大,则向后移动
{
tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
}
}
}
}
int main()
{
int a[MAX]={55,11,99,77,33,22,88,44,66,110};
int i;
Bubble(a,MAX);
for(i=0;i<MAX;i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
4、通过指针引用多维数组以二维数组为例
假设定义了一个二维数组
int a[3][4];
则指向这个二维数组的指针的定义方法是
int (*p)[4];
注意二维数组的第二个下标不可省略。那么怎样使用指向二维数组的指针索引数组内数据呢?
使用数组下标索引数据:
a[i][j];//索引数组内第i行第j列的数据
使用指针索引数据的方法为:
*(*(p+i)+j)
/********** *(*(p+i)+j)的详细讨论************/
二维数组在内存中是以“大数组嵌套小数组”的形式存储的。例如我们有二维数组int a[3][4]
a代表数组内所有元素
a[0]代表a[0][0-3]的元素
a[1]代表a[1][0-3]的元素
a[2]代表a[2][0-3]的元素
当我们让指向二维数组的指针指向数组a即p=ap获得了数组名a代表的首地址实际上就是a[0][0]的地址。现在我们使用指针索引数组第i行第j列的元素
1.首先需要找到第i行执行运算p+i即可让指针p指向第i行
2.索引第i行内元素执行*(p+i)。此时会得到第i行存储的小数组
3.寻找第j列元素执行*(p+i)+j。此时指针指向了第i行第j列的元素
4.索引该元素,执行*(*(p+i)+j),取出该元素的值
//见附图
/********** *(*(p+i)+j)的详细讨论end*********/
示例有3个学生各学4门课课程成绩以二维数组的形式存储。计算总平均分。
#include<stdio.h>
#define ROW 3
#define COL 4
void average_total(int (*p)[COL],int n)//注意第二个下标不可省略
{
int i,j,sum = 0;
float avg = 0;
for(i=0;i<ROW;i++)
{
for(j=0;j<COL;j++)
{
sum += *(*(p+i)+j);//相当于数组p[i][j]的值
//sum += p[i][j];//与上面写法等价
}
}
avg = (float)sum/(ROW*COL);
printf("总平均分是%f\n",avg);
}
int main()
{
int score[ROW][COL]={{65,67,70,60},{80,87,90,81},{90,99,98,100}};
average_total(score,ROW*COL);
return 0;
}
注意示例程序中*(*(p+i)+j)的用法。因为p是指向二维数组的指针则*(p+i)相当于a[i]的地址,*(p+i)+j相当于a[i]+j的地址。
注意千万不要把*(*(p+i)+j)写成*(*p+i+j)。
5、通过指针引用字符串
之前我们学习过使用字符数组来操作字符串。实际上,我们可以直接使用字符型指针来指向一个字符串。例如:
char *p = "heloworld"
这样指针p指向的就是字符串的首地址即字符'h'的地址)。
示例1使用指针输出该字符串和该字符串的第8个字符
#include<stdio.h>
int main()
{
char *p = "I love China!";
printf("字符串是%s\n",p);
printf("字符是%c\n",*(p+7));//注意第8个字符是向后移动7位
return 0;
}
不过要注意,使用这种方法来引用字符串,字符串是只读的,不能修改。例如:
#include<stdio.h>
int main()
{
char *s1 = "Hello World!";
char *s2;
s2 = s1;
s2 += 2;
*s2 = 'L';//试图修改Hello World的第三个字符为'L',语法报错
printf("%s\n",s1);
return 0;
}
编译发现语法报错。
错误的原因:使用字符数组存放字符串,字符串(或者说字符数组)是存放在堆栈区内的,存放在堆栈区的数据是可读可写的(可修改)。而直接让指针指向一个字符串常量,字符串是存放在常量区内的。常量区所存放的数据是只读的(不可修改),因此试图修改只读数据,语法报错。
虽然让字符串存放在常量区可以节省部分堆栈区空间,但是这样做数据就不可修改。因此初学者一定要谨慎使用,选择合适的存放字符串的方式。
练习1自定义一个字符串不使用C库函数中的strcpy()函数,复制这个字符串。要求使用指针。
答案:
#include<stdio.h>
char *my_strcpy(char *dst,const char *src)
{
if((NULL==dst)||(NULL==src))
{
printf("数据无效!\n");
return NULL;
}
char *dstCopy = dst;
//第一种写法
/*int i=0;
while(src[i]!='\0')
{
dst[i] = src[i];
i++;
}
dst[i]='\0';*/
//第二种写法
/*while(*src!='\0')
{
*dst = *src;
dst++;
src++;
}
*dst='\0';*/
//第三种写法
/*while(*dst++=*src++);*/
return dstCopy;
}
int main()
{
char str1[]="Hello World!";
char str2[32];//足够大的空间
my_strcpy(str2,str1);
printf("str2 is %s\n",str2);
return 0;
}
练习2加密一个字符串。加密规则是字符串中所有字母都循环向后4个字母。如a--->e、b--->f……x--->b、y--->c、z--->d。A--->E、B--->F……X--->B、Y--->C、Z--->D。例如
"Hello World!"--->"Lipps Asvph!",注意非字母字符不转换。
其中函数的数组形参使用指针。
答案:
#include <stdio.h>
void change_word(char *p, int n)
{
int i;
for (i = 0; i < n; i++)
{
if ((p[i] >= 'a' && p[i] <= 'v') ||
(p[i] >= 'A' && p[i] <= 'V'))
{
p[i] += 4;
}
else if ((p[i] >= 'w' && p[i] <= 'z') ||
(p[i] >= 'W' && p[i] <= 'Z'))
{
*(p + i) = *(p + i) - 26 + 4;
}
}
}
int main()
{
char a[] = "Hello World!";
change_word(a, sizeof(a) / sizeof(a[0]));
printf("字符串a变成 %s\n", a);
return 0;
}