C博客作业05--指针

这个作业属于哪个班级 C语言--网络2011/2012
这个作业的地址 C博客作业05--指针
这个作业的目标 学习指针相关内容

0.展示PTA总分


1.本章学习总结

1.1 指针

指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作

指针是一个占据存储空间的实体在这一段空间起始位置的相对距离值。在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。

指针变量的定义

一般形式:

类型名 *指针变量名;

  • 类型名指定指针变量所指向变量的类型(例如:int,float,char等)
  • 指针变量名时指针变量的名称,必须是一个合法的标识符。定义指针变量要使用指针声明符*,例如:int i,*p,则声明变量i是int型,变量p是指向int型变量的指针,指针值可以是特殊的地址0,也可以是一个代表机器地址的正整数。指针变量的类型不是指指针变量本身的类型,而是指它所指向的变量的数据类型,因此指针变量自身所占的内存空间大小和它所指向的变量数据类型无关
  • 指针声明符*在定义指针变量时被使用,说明被定义的那个变量是指针
int *p 定义一个指针变量p,指向整型变量
char *cp 定义一个指针变量cp,指向字符型变量
float *fp 定义一个指针变量fp,指向实型变量
double *dp1, *dp2 定义两个指针变量dp1,dp2 指向双精度实型变量
  • 定义多个指针变量时,每一个指针变量前面都必须加上*

定义指针变量时的注意事项:

  1. 指针变量名是一个标识符,要按照C标识符的命名规则对指针变量进行命名
  2. 指针变量的数据类型时它所指向的变量的类型,一般情况下一旦指针变量的类型被确定之后,它只能指向同种类型的变量
  3. 在定义指针变量时需要使用指针声明符*,但指针声明符并不是指针组成部分。例如,定义int *p;说明p是指针变量而不是 *p

指针基本运算

  • 取地址运算:通过变量名访问

    int  x = 20, y = 1, z = 155;
    printf("%d", x;)
    
  • 间接访问运算:通过地址访问存放地址的变量

int *p,a=3;
p=&a;
printf("%d", *p;)
&*p与&a相同,是地址

*&a与a相同,是变量

&表示取地址,*表示取内容

  • 赋值运算

一旦指针被定义并赋值后,就可以如同其他类型变量一样进行赋值运算

int a=3,*p1,*p2;//定义整型变量指针p1和p2
p1=&a;//使指针p1指向整型变量
p2=p1;

将变量a的地址赋给指针p1,再将p1的值赋给指针p2

  • *p++

    先将*p作为表达式的值,再将指针p的值加一,运算后,p不在指向变量a,即*p=*(p+1)。 
    表达式 *(p++)和 *p++等价
    
  • ++*p

    ++*p的意思是将p所指的变量的值加一,和 *p= *p+1,( *p)++表示的意思是等价的
    
  • 利用指针计算数组元素的个数和数组元素的存储个数

double *p,*q;
double a[2];
p=&a[0];
q=p+1;
printf("%d",q-p);//计算p,q之间的元素个数
printf("%d",(int)q-(int)p);//计算p,q之间的字节数

指针变量的初始化

int a;
int *p1=&a;//定义指针p1的同时给其赋值,使指针p1指向变量a
int *p2=p1;//定义指针p2的同时对其赋值,使p2和p1的值相同

初始化的注意事项:

  1. 指针变量先定义,赋值必须是地址
  2. 把一个变量的地址作为初始化赋值给指针变量时,该变量必须在此之前已经定义
  3. 可以用初始化了的指针变量给另一个指针变量给另一个指针变量作初始化值
  4. 不能用数组作为指针变量的初值,但可以将一个指针变量初始化为一个空格针int *p=0;

指针做函数参数

通过函数调用来改变主调函数中某个变量的值:

  1. 主调函数中,将该变量的地址或者指向该变量的指针作为实参
  2. 被调函数中,用指针类型形参接受该变量的地址
  3. 在被调函数中,改变形参所指向变量的值

指针做循环变量

sum = 0;
for (p = a ; p <  a+n ; p++)
     sum = sum + *p;

指针做循环变量,务必了解初始和结束地址!!

野指针

C语言中指针初始化是指给所定义的指针变量赋初值。指针变量在被创建后, 如果不被赋值, 他的缺省值是随机的 ,它的指向是不明确的, 这样的指针形象地称为“野指针”。野指针是很危险的, 容易造成程序出错, 且程序本身无法判断指针指向是否合法。

指针变量初始化时避免野指针的方法: 可以在指针定义后, 赋值NULL空值。

1.2指针做函数返回值

例题:

输入年和天数,输出对应的年、月、日。
例如:输入2000和61,输出2000-3-1。

优点:

  • 采用了leap变量和数组来判别闰年非闰年
  • 计算哪天那日时采用了减法的思路

注意事项:

返回指针的函数一般都返回全局数据对象或主凋函数中教据对象的地址,不能返回在函数内部定义的局部数据对象的地址。

1.3 字符指针

在应用标准库中的任何函数之前,必须要提供函数原型:#include <string.h>

  • 字符串的输入输出

    输入字符串:scanf ( )或fgets ( )
    输出字符串:printf ( )或puts ( )

  • 字符串和字符指针比较

字符数组/字符指针 区别
char a[] = "This is a string"; 有确定的地址,每个数组元素放字符串的一个字符;可以改变数组元素的内容
char *p = "This is a string"; 字符指针p只占用了一个可以存放地址的内容单元,存储字符串首字符的地址;直接改变指针的值,指向新的字符串
  • 字符串复制函数strcpy(s1,s2)(copy)

    把字符串s2复制到s1,直到遇到s2中的'\0'为止。s1必须是字符型数组基地址,s2可以是字符数组名或字符串常量

    int i;
    char s1[80],s2[80],from[80]="happy";
    strcpy(str1,from);
    strcpy(str2,"key");
    

    把from中的字符串复制给str2,“key”复制给str2后,数组str1存放了“happy”,str2存放了“key”

  • 字符串连接函数strcat(s1,s2)(connect)

​ 把字符串s2接到字符串s1后面,则s1原有的结束符被放在连接后的结束位置上

char str1[80]="hello",str2[80],t[80]="world";
strcat(str1,t);
strcpy(str2,str1);
strcat(str2,"!");

连接str1和t,结果放在str1中,再调用函数strcpy()将str1中的字符串赋给str2,最后连接str2和字符串常量“!”

  • 字符串比较函数strcmp(s1,s2)(compare)
    • 若s1和s2相等,返回0
    • 若s1大于s2,返回一个正数
    • 若s1小于s2,返回一个负数
例:strcmp("compute","compare")的值('u'-'a')是正数,则compute大于compare
  • 字符串长度函数strlen(s1)(length)

    不包括字符串结束符'\0'

    例:strlen("happy")的值为5
    

    注意:len=strlen(str);语句不要放循环内重复操作

  • 常用的字符串处理函数的例子

    char s1[]="beautiful big sky country" , s2[]="how now brown cow"

    函数(表达式)
    strlen(s1) 25
    strlen(s2+8) 9
    strcmp(s1,s2) negative integer
    语句 打印内容
    printf("%s",s1+10); big sky country
    strcpy(s1+10,s2+8); strcat(s1,"s!"); printf("%s",s1); beautiful brown cows!
  • const函数
    const 定义的是变量,但又相当于常量
    不允许给它重新赋值,即使是赋相同的值也不可以。
    只读变量。必须在定义的时候就给它赋初值

    可以节省内存空间

  • sizeof()函数

    sizeof() 是一种内存容量度量函数,功能是返回一个变量或者类型的大小(以字节为单位);在 C 语言中,sizeof() 是一个判断数据类型或者表达式长度的运算符。

注意事项:

  1. 定义字符指针,先赋值,后引用

    char *s, str[20];
    s = str;
    scanf (“%s”, s);
    
  2. 定义指针时,先将它的初值置为空
    char *s = NULL

1.4 动态内存分配

头文件: #include <stdlib.h>

  • 为什么要动态内存分配

    堆区申请的空间,想要多少申请多少
    数组要指定数组长度,空间浪费。
    栈区空间有限。

  • 堆区和栈区区别

    栈区(stack) 函数运行时分配,函数结束时释放。由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈。

    堆区(heap) 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收。分配方式类似于链表。

栈区 堆区
申请方式 由系统自动分配,例如:声明在函数中的一个局部变量 int b;系统自动在栈中为b开辟空间 程序员自己申请,并指明大小,在c中malloc函数,例如:p1=(char *)malloc(10);
申请效率 由系统自动分配,速度较快,程序员无法控制 由new分配的内存,一般速度较慢,而且容易导致内存碎片,但是用起来方便
存储内容 函数调用(返回值,各个参数,局部变量(静态变量不入栈)) 一般在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排

img

  • 动态内存分配相关函数及用法

    • 动态存储分配函数malloc()

    函数原型:void *malloc(unsigned size);

    功能:在内存的动态存储区中分配一连续空间,长度为size,此函数的返回值是分配区域的起始地址

    返回值:如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。

    • 计数动态存储分配函数calloc()

      函数原型: void calloc(unsigned n,unsigned size)

      功能:分配还把存储块里全部初始化为0

    malloc()对所分配的存储块不做任何事情,calloc()对整个区域进行初始化

    • 分配调整函数realloc()

      函数原型: void *realloc(void *ptr,unsigned size)

      功能:先判断当前的指针是否有足够的连续空间,如果空间不够更改以前的存储分配

    • 动态存储释放函数free()

      函数原型:void free(void *ptr)

      功能:释放!!!如果传递的参数是一个空指针,则不会执行任何动作。该函数不返回任何值

  • 举例为多个字符串做动态内存要如何分配。

1.5 指针数组及其应用

指针数组的定义

类型名 *数组名[数组长度]

指针数组是由指针变量构成的数组,在操作时,既可以直接对数组元素进行赋值和引用,也可以间接访问数组元素所指向的单元内容,改变或引用该单元的内容

二维字符数组与字符指针数组的区别

  • 二维数组:一旦定义,那么每个字符数组的字符串最大长度和首地址都不能改变

  • 指针数组:用于存放字符指针的 数组,仅能用来存放指针,所以它指向的每个字符串的首地址均可以改变,字符串的最大长度也可以改变,相比之下,字符指针数组比较灵活一点

字符数组 字符串指针
存储方式 分配一个存放元素的单元 分配一个存放元素地址的单元,地址指向元素的首地址,不是将整个字符串放到字符指针变量中
赋值方式 char str[16]= char *str="abc" / char *str; str="abc"
定义方式 char str[10] scanf(“%s”,str) char *str,a[10] str=a;scanf(“%s”,str) !!!str必须指向一个数组不然就是野指针
运算方面 字符数组名是常量,不能进行运算,不能改变值 指针变量的值是可以改变的,可以进行运算char *a=”abc” a=a+1

1.6 二级指针

首先任何值都有地址 ,一级指针的值虽然是地址,但这个地址做为一个值亦需要空间来存放,是空间就具有地址 ,这就是存放地址这一值的空间所具有的地址,二级指针就是为了获取这个地址。

二级指针的定义

类型名 * *变量名

例如:

int a=10;
int *p=&a;
int **pp=&p;

a,*p,**pp代表同一个单元,他们的值相同

1.7 行指针、列指针

行指针

  • 定义
int* a;//a是指向整形的指针
int* a[5];//一维指针数组(这里存放着5个指向整形的指针),a指向第一个元素的地址,a+1指向第二个......(a[5]是一个指针数组)
int (*a)[5];//指向数组(这里每个一维数组含5个元素)的指针,a是第一个一维数组的首元素地址,a+1指向第二个一维数组的首元素地址......(a是数组指针)
int (*a)();//a是指向函数的指针(函数指针)
int *a();//函数的返回类型是int *,a只是一个函数名
  • 主要用法
写法 解释 指针类型
a+0或&a[0] 指向第1个一维数组的地址(指向第1行) 行指针
a+1或&a[1] 指向第2个一维数组的地址(指向第2行) 行指针
a+2或&a[2] 指向第3个一维数组的地址(指向第3行) 行指针

行指针是向数组的指针,即上面几种指针类型中的 int (*a)[5];

当二维数组要被当做参数进行传递时,可以这样声明:

void fun(int (*p)[5],const int row); 或者 void fun(int p[][5],const int row);

列指针

  • 定义

对于一个二维数组:int a[3][5];

前面已经知道,a即是它的行指针,a+0表示第0行的地址,a+1表示第1行地址或者可以说成&a[0]表示第0行的地址,&a[1]表示第1行的地址,那么a[0]+0,a[0]+1…就表示第1行第1列的地址,第1行第2列地址。a[1]+0,a[1]+1就表示第2行第1列地址,第2行第2列地址…

  • 主要用法
写法 解释 指针类型
a[0]+0或&a[0] [0] 指向第1行第1列的地址 列指针
a[0]+1或&a[0] [1] 指向第1行第2列的地址 列指针
a[1]+0或&a[1] [0] 指向第2行第1列的地址 列指针
a[1]+1或&a[1] [1] 指向第2行第2列的地址 列指针

像上面的a[row]+col即列指针,列指针经过一次解引用就可以转化成二维数组中实际的值,列指针也是指向非常量的常量指针。

所以如果用列指针进行函数传参,可以直接声明如下:

void fun(int * const colPtr,const int row,const int col);

2.PTA实验作业

2.1 删除字符串中的子串

输入2个字符串S1和S2,要求删除字符串S1中出现的所有子串S2,即结果字符串中不能包含S2。

输入格式:

输入在2行中分别给出不超过80个字符长度的、以回车结束的2个非空字符串,对应S1和S2。

输出格式:

在一行中输出删除字符串S1中出现的所有子串S2后的结果字符串。

输入样例:

Tomcat is a male ccatat
cat

输出样例:

Tom is a male 

2.1.1 伪代码

定义字符数组s1[MAX],s2[MAX],temp[MAX]
定义指针*p//用于记录子串查找结果
输入字符数组S1,S2
while (p = strstr(s1, s2)) != NULL //找不到返回NULL
   *p= '\0';   //标记查找到子串的位置为结束符‘\0’
    strcpy(temp, p + j); //把出现子串之后的字符复制到temp中
    strcat(s1, temp);//把temp中字符拼接到s1
end while
输出字符串

2.1.2 代码截图

2.1.3 代码比较

1.使用字符串处理函数(我使用的方法)

解题思路:

  • 采用程序中自带的函数strcpy和strcat函数,将字符串进行复制拼接

  • 采用C语言库函数char *strstr(const char *s1, const char *s2)

    strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回 str1字符串从 str2第一次出现的位置开始到 str1结尾的字符串;否则,返回NULL。

2.基础算法

最闹人的情况是诸如 s1 为 paatt,s2 为 at 的情况。因为不单单是删除子串,删除子串之后在新的字符串中还要检查是否有子串。

解题思路:

  • 扫描,发现有子串时,删除子串(将后面的子串向前移),然后置 i = 0,又会开始扫描,扫描出口即是 s1[1] != NULL;
  • 处理中要注意如 s1 为 ccat,s2 为 cat 的情况。在扫描第二个 c 的时候,也就是说已经匹配了一部分字符后,之后不再匹配,要将当前的字符再与子串的第一个字符比较,这里第二个也是 c,与子串首字母相同,所以置 count = 1;
  • 注意处理要删除的子串在字符串末尾的情况

比较:

采用C语言库函数进行编写,代码比较简洁可观,思路也是比较清楚的,而如果不采用函数写,则需要分很多种情况进行讨论

2.2 合并两个有序数组

要求实现一个函数merge,将元素个数为m的升序数组a和长度为n的升序数组b合并到数组a,合并后的数组仍然按升序排列。假设数组a的长度足够大。

函数接口定义:

void printArray(int* arr, int arr_size);  /* 打印数组,细节不表 */
void merge(int* a, int m, int* b, int n); /* 合并a和b到a */

其中a和b是按升序排列的数组,m为数组a中元素的个数,n为数组b的长度;合并后的升序数组仍然存放在a中。

裁判测试程序样例:

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

void printArray(int* arr, int arr_size);  /* 打印数组,细节不表 */
void merge(int* a, int m, int* b, int n); /* 合并a和b到a */

int main(int argc, char const *argv[])
{
 int m, n, i;
 int *a, *b;

 scanf("%d %d", &m, &n);
 a = (int*)malloc((m + n) * sizeof(int));
 for (i = 0; i < m; i++) {
     scanf("%d", &a[i]);
 }

 b = (int*)malloc(n * sizeof(int));
 for (i = 0; i < n; i++) {
     scanf("%d", &b[i]);
 }
 merge(a, m, b, n);
 printArray(a, m + n);

 free(a); free(b);
 return 0;
}

/* 请在这里填写答案 */

输入样例:

输入包含三行。 第一行为两个整数m和n,分别为数组a和数组b中元素的个数。 第二行为包含m个整数的有序数组a。 第三行为包含n个整数的有序数组b。

7 11
1 2 14 25 33 73 84
5 6 17 27 68 68 74 79 80 85 87

输出样例:

输出为合并后按升序排列的数组。

1 2 5 6 14 17 25 27 33 68 68 73 74 79 80 84 85 87

2.2.1 伪代码

定义变量i,j//作为指针a,b的下标
定义变量k//作为指针num下标
定义指针*num//指向一个未使用的数组

动态内存分配给指针num 大小为(m+n)*sizeof(int)
while(   <m+n)//按照数字大小升序排列
   if(j>=n)//如果数组b的数存储完后存储a
     num[k++]=a[i++]
   else if(i>=m)//如果数组a的数存储完后存储b
     num[k++]=b[j++]
   else if(a[i]<b[j])//依题意需要存放a中,如果a的下标小于b的下标,则从a开始计算
     num[k++]=a[i++]
   else //比较数组a,b中的元素,将a,b中较小的数写入新的数组num中
     num[k++]=b[j++]
   end if
   end if
   end if
end while
for k=0 to m+n//将数组num写入数组a
   a[k]=num[k]
end for

2.2.2 代码截图

2.2.3 代码比较

1.我的代码

解题思路:

  • 采用动态内存分配给指针num 大小
  • 循环条件直到数组读取结束
  • 分别存储a结束后存b/b存完存a

2.同学的代码

解题思路:

  • 一开始从下标入手,定义合并后数组下标
  • 从最后一个数开始遍历
  • 比较大小,移动数的位置

比较:

两种思路都可取,我是从第一个数开始排列,而同学的代码是从最后一个数开始排列,并一个一个比较大小

2.3 说反话-加强版

给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。

输入格式:

测试输入包含一个测试用例,在一行内给出总长度不超过500 000的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用若干个空格分开。

输出格式:

每个测试用例的输出占一行,输出倒序后的句子,并且保证单词间只有1个空格。

输入样例:

Hello World   Here I Come

输出样例:

Come I Here World Hello

2.3.1 伪代码

定义字符型数组str[MAX]
定义变量i,j//数组下标
定义变量flag//标记该单词是否结束
定义变量l//标记输入字符的长度 l=strlen(str)

输入数组
for i=0 to l 
  if(空格)
     str[i]='\0'
   end if
end for

for 从最后开始
   if(str[l-1]=='\0'&&str[l]!='\0')
       flag=0
       
       for i=1 to 字符串结束
         输出
       end for
       
       for j=0 to l-1
         if(str[j]不为零)
            flag=1 
            跳出循环
       end for
       
       if(flag)
       输出空格
       end if
   end if
end for

2.3.2 代码截图

2.3.3 代码比较

解题思路:

  • 指针定位在字符串最后一个字符,往前扫描到第一个空格,即找到一个单词,则输出指定长度字符串
  • 若已经是第一个字符,则扫描结束
/*伪代码*/
void ReverseStr(char *begin)//逆转
{
  定义尾部指针endPtr=beginPtr//直到字符串的最后一个字符
  while(*endPtr)endPtr++//指针定位到字符串尾部
  遍历指针p=--endPtr//因为逆向扫描不需要考虑'\0'
  while(p!=beginPtr)//不等于起始地址
  {
	若*p不是空格,则统计单词长度len
	若*p不是空格但前一个字符是空格:
			   则找到单词,输出从p开始的len长度字符串,len=0//计算第二个单词要清空
	p--
  }
  输出第一个单词
 }

所涉及的知识点

定义指针指字符串 while(*endPtr&&endPtr!='\n')endPtr++
逆向扫描字符串while(p!=beginPtr){p--;}
怎么找字符串单词,即当前字符空格而前一个字符是空格while(p!=' '&&*(p-1)==' ')
怎么输出指定长度字符串 printf("%.*s",len,str)//*作为分配符

注意

  • 出现这种情况,是因为首先输入字符串逆向扫描时需要换行处理,且每轮单词输出完毕之后单词长度len需要清零

  • 在处理首字母时,需要单独考虑,因为首字母没有计算进去,则在输出是需要单词长度+1


3.课堂派测试

  • p=a的意思:将a的值赋给p指针指向的地址的值;
  • p=&a的意思是:将a的地址赋给指针p;
  • 区别:*p是一个值;p是一个地址;两者完全不相同。
  • 代表着p指向的地址的值,简单来说就是取值;&是取地址符号,取的是地址;p是指针,可以理解为所指向的值的地址,* p就是取p指针指向的地址的值,&a就是取a的地址。
posted @ 2020-12-27 13:37  GGGa-Yi  阅读(358)  评论(0编辑  收藏  举报