0701——指针(C语言高级)

一、指针是什么

指针是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 *p;//定义一个指向int类型数据的指针变量p

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)

    {

        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=a)后,p获得了数组名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*********/

/***********语法糖*********************/

语法糖(Syntactic sugar)是由英国计算机学家彼得·约翰·兰达(Peter J. Landin)(Lambda运算的发明人,创立函数式编程)提出的概念。指的是计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。

例如在指针操作数组的事例中,我们使用指针操作数组元素的方式

一维数组:*(a+i) 二维数组:*(*(a+i)+j)

是编译器操作数组成员的方式,而我们可以使用

一维数组:a[i] 二维数组:a[i][j]

的方式操作数组成员。该方式与上面的方式完全等价,且更加方便使用。那么我们就可以称这种使用方式为“语法糖”。

//某种程度上,for()循环可以理解为while()循环的语法糖

注意:加糖后的代码应与加糖前的代码保持一致,更重要的是,糖在不改变其语法结构的前提下实现了运行时等价。即,加糖前与加糖后的代码在编译后是一模一样的。

/***********语法糖end******************/

示例:有3个学生,各学4门课,课程成绩以二维数组的形式存储。计算总平均分。

#include<stdio.h>

#define ROW 3

#define COL 4

void average_total(int (*p)[COL])//注意第二个下标不可省略

{

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);

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 = "helloworld"

这样指针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;

}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一章 绪论作业答案(共50分) 一、分析如下程序中 (1)~ (10)各语句的频度。(每个1分,共10分) Ex( ) { int i , j , t ; (1) for( i=1 ; i<10 ; i++) //n = (2) printf(“\n %d” , i ); //n = (3) for(i=1; i<=2; i++) //n = (4) printf(“\n”); //n = (5) for(i=1; i<=9; i++) //n = { (6) for(j=1; j <= i ; j++) //n = { (7) t = i * j ; //n = (8) printf(“]”,t); //n = } (9) for(j=1; j 0) { if(x > 100) {x -= 10 ; y -- ;} else x ++ ; } 问if 语句执行了多少次?(2分) y--执行了多少次?(2分) x ++执行了多少次?(2分) 三、回答问题(共25分) 书中16页的起泡排序如下: void bubble_sort(int a[],int n){ //将a中整数序列重新排列成自小至大有序的整数序列。 for(i=n-1,change=TRUE;i>=1&&change;--i){ change=FALSE; for(j=0;ja[j+1]{a[j]<-->a[j+1];change=TRUE; } } }//bubble_sort 1.(共15分)分析该算法的最佳情况 ,最坏情况和平均情况下各自的时间复杂度(给出分析思路与过程)。 (1) 最佳情况的时间复杂度分析(5分): (2) 最坏情况的时间复杂度分析(5分): (3) 平均情况的时间复杂度分析(5分): 2.(共10分)比较与C语言书中的起泡排序异同,并从时空效率角度说明谁更优。 四、完成如下选择题(每3分,共9分)。 1. 设f为原操作,则如下算法的时间复杂度是( )。 for (i = 1; i*i=1;i--) for(j=1;jA[j+1]) A[j]与A[j+1]对换; 其中n为正整数,则算法在最坏情况下的时间复杂度为( )。 A.O(n) B.O(nlog2n) C. O(n3) D. O(n2)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值