Always believe nothing is impossible and just try my best! ------ 谓之谦实。

the summury of pointer in C.

| 这个作业属于哪个班级 |


| ---- | ---- | ---- |
| 这个作业的地址 |
| 这个作业的目标 | 学习指针相关内容 |
| 姓名 | 骆锟宏 |

0.展示PTA总分

1.本章学习总结(3分)

1.1 指针定义、指针相关运算、指针做函数参数。

1.1.1 指针的定义:

int  *ptr; 整型指针,指向整型变量
double *fPtr;浮点型指针,指向浮点型变量
char *cPtr; 字符型指针,指向字符型变量 
  • 指针没有指向是危险的野指针。会发生段错误!!
    特别地,用'NULL'表示空指针;
  • 指针变量类型要和指向变量类型一致。
  • 指针变量占用空间和类型无关.不同类型指针变量占的内存空间大小是一样。
    eg. ptr = &integer

1.1.2 指针相关运算:

1.'&'表示取地址的运算符,运算对象是内容; 运算的值是地址,单目
2.''表示取内容的预算符,运算对象是地址; 运算的值是内容,单目
3.&
p 与 &a 相同,是地址
&a 与a相同,是变量
4. (
p)++ 等价于 a++,将 p 所指向的变量值加1
5. *p++ 等价于 *(p++),先对p(地址)进行向下移动,
再取移动后的地址的内容,此时p不再指向a。
eg,变量a = 3 ,则 &a 表示的就是 存放变量a的内容的地址
而 *(&a) 表示的就是对 地址(&a) 取内容,也就是变量a的内容
在这里也就是 3.

  • 需要特别注意的是,'*'会直接对内存单元进行操作,,影响会直接体现在内存上,
    只要内存还存在,数据也就存在,可以通过地址来获取。

1.1.3 指针做函数参数:

  • 形参用指针变量来定义;
  • 实参要传入变量的地址;
  • 优点:
    1.传入的是地址,数据量少,
    2.可以直接对地址操作,效果更好。
    3.可以改变多个变量值,比return返回更实用。
  • 实例:拆分实数的整数与小数部分
    代码如下:
void splitfloat(float x, int* intpart, float* fracpart)
{
    *intpart = (int)x;
    *fracpart = x - *intpart;
}

*可以返回实数部分和小数部分两个值。
4.可以用数组名直接充当函数的实参;
不过对于数值型数组,要记得传入相关的数组长度;
5.在函数中如果对指针进行移动的操作的话,
最好另外设一个指针变量来移动,因为指针
的首地址的保留很重要!

1.1.4 对指针变量的操作:

  1. 让指针进行移动,这种情况要注意指针跑到哪里去了;
  2. 用下标法来操作指针。类似于数组的使用,
    所以任何由数组下标来实现的操作都能用指针来完成
    3.用类似于下标法的偏移量法来表示指针。
    eg.假设有一个数组a[100],有一个指针变量*p且p指向a,
    则 p = a(数组名表示数组的首地址) = &a[0]
    p + 1 = &a[1] ;
    而 p[0] = a[0] = *p
    p[1] = a[1] = *(p+1)
    p[i] = a[i] = *(p+i)
    4.用指针来遍历数组:
    下标法:for(i=0;i<n;i++) a[i] or p[i]
    指针法:for (p = a; p <a+n; p++) *p
    5.其他:
    q-p:指针p和q之间元素的个数
    (int) q - (int) p:指针p和q之间的字节数
    6.指针之间的运算:
    q-p
    两个相同类型的指针相减,表示它们之间相隔的存储单元的数目
    p + 1 or p-1
    指向下一个存储单元 / 指向上一个存储单元
    注意指针不要越界
    p < q
    两个相同类型指针可以用关系运算符比较大小
    //常用于循环条件的设置中
    其他操作都是非法的:
    指针相加、相乘和相除,或指针加上和减去一个浮点数

1.2 字符指针

1.2.1 指针如何指向字符串

  • 直接指向字符数组的变量名(正确的赋值例子往下看:)
  • 字符指针和字符数组的区别:
    char sa[ ] = "This is a string"; 如果要改变数组sa所代表的字符串,只能改变数组元素的内容
    char *sp =sa; 如果要改变指针sp所代表的字符串,通常直接改变指针的值,让它指向新的字符串
  • 两个重点误区:
    数组名是常量,不能对它赋值,只能在定义时进行初始化(如上)
    sp是指针,只能指向,不能直接赋值(除非加上const)
    const char *sp = "This is a string";
    称为 只读变量。必须在定义的时候就给它赋初值
  • 代定义字符指针后,没有对它赋值,指针的值不确定,所以要先赋值后使用;

1.2.2 字符串相关函数及函数码原型的理解

*strcat(连接str2到str1后面)

char *strcat( char *str1, const char *str2 )
{
	char *tempstr;
	tempstr=str1;
	if(!str1||!str2) return NULL;//防止空指针传入。否则程序奔溃。
	while(*str1)  str1++;
	while(*str2) {
		*str1=*str2;
		str1++;str2++;
	}
	*str1='\0'; 
	return tempstr;  //连接后字符串的的首地址。
}

理解:
1.定义局部变量tempstr来保存首地址,然后对str1和str2直接操作;
2.因为这里是从字符的角度去操作的,所以重新生成的字符串,务必加上结束符!
3.考虑到放置空指针的输入。
4.首字符数组要足够大,否则会达不到效果。

*strcpy(将strSrc复制到strDest)

#include "assert.h"  
char *strcpy(char *strDest, const char *strSrc)  
{  
   assert((strDest != NULL) && (strSrc!= NULL)); 
    if (strDest == strSrc)  
        return strDest;  
    char *tempDest = strDest;  //保存首地址    
    while((*strDest++ = *strSrc++) != '\0');  //??
    *strDest=0;
    return tempDest;  
} 

理解:
1.需要对传入参数strDest和strSrc进行检查,禁止空指针传入。
2.仍然要注意保存好首地址
3.使用const来约束strSrc,提高程序的健壮性。
如果函数体内的语句试图改动strSrc的内容,编译器将指出错误。
4.首字符数组要足够大,否则会达不到效果。

  • 考虑到strcpy和strcat的问题,引入另外两个函数:
  1. strncpy() 函数。
    char *strncpy(char *dest, const char *src, size_t n)
    把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
  2. strncat() 函数
    char *strncat(char *dest, const char *src, size_t n)
    把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,追加最多n个字符
  • strcmp(用于比较两个字符串)
#include "assert.h"  
int strcmp(const char *str1,const char *str2)  
{    int len = 0;  
     assert((str1!= NULL) && (str2 != NULL)); 
    while(*str1 && *str2 && (*str1==*str2))  
    {  
        str1++;  
        str2++;  
    }  
    if((*str1-*str2)>0)     
        return 1;  
    else if((*str1-*str2)<0)  
        return -1;  
    else  
        return 0;  } 

理解:
1.检查,禁止空指针传入
2.分三种情况进行返回值

*strlen(计算字符串的有效长度)
*返回的值是整型,不包括结束标志,但是会包括换行符,在fgets输入中要注意。
*其他字符串函数:
<string.h>

  1. strpbrk(str1,str2)
    char *strpbrk(const char *str1, const char *str2)
    检索字符串 str1 中第一个匹配字符串str2 中字符的字符
  2. strrchr()
    char *strrchr(const char *str, int c)
    参数 str 所指向的字符串中搜索最后一次出现字符 c的位置。
  3. strstr()
    char *strstr(const char *haystack, const char *needle)
    字符串 haystack 中查找第一次出现字符串 needle 的位置
    <ctype.h> 书本P347
  4. islower():如果 c 有相对应的小写字母,则该函数返回 c 的小写字母,否则 c 保持不变。
  5. toupper():如果 c 有相对应的大写字母,则该函数返回 c 的大写字母,否则 c 保持不变。
  6. isdigit():判断c是否是数字字符。
    <stdlib.h> 书本P349
  7. atoi():该函数返回转换后的长整数,如果没有执行有效的转换,则返回零

常用字符串函数的网址:
http://www.runoob.com/cprogramming/c-standard-library-ctype-h.html
http://www.runoob.com/cprogramming/c-standard-library-string-h.html
https://blog.csdn.net/kl28978113/article/details/41549011

1.2.3 字符串相关函数用法(待拓展)

  • strcmp函数的用法:
    用于条件设定:(逐一比较元素字符的ASCLL码)
    if( strcmp(str1,str2) >0),
    if( strcmp(str1,str2) <0),
    if( strcmp(str1,str2) ==0),

  • strcpy函数的用法,比如在(字符串的冒泡排序)这道题目中:

#include <stdio.h>
#include <string.h>
int main()
{
    int K;
    int N;
    char ch[100][11];
    char temp[11];
    //由于字符串多了一个结束标志,所以记长度要加一
    
    int i, j;
    scanf("%d %d", &N, &K);
    for (i = 0; i < N; i++)
    {
        scanf("%s", ch[i]);
        //分别对字符数组的每行进行输入;
    }
    //冒泡排序法:
    for (i = 0; i < K; i++)
    {
        for (j = 0; j < N - 1 - i; j++)
        {
            if (strcmp(ch[j], ch[j + 1]) > 0)//ch[j]更大
            {
                strcpy(temp, ch[j]);
                strcpy(ch[j], ch[j+1]);
                strcpy(ch[j+1], temp);
            }
        }
    }
    for (i = 0; i < N; i++)
    {
        printf("%s\n", ch[i]);
        //分别对字符数组的每行进行输出;
    }
    return 0;
}

从这道题目来看,strcmp函数相当于字符串中的 条件运算符,
而strcpy相当于字符串中的 赋值运算符。

1.3 指针做函数返回值(课本278页)

  • 不能返回在函数内部定义的在栈区局部数据对象的地址
    这是因为所有的栈区局部数据对象在函数返回时就会消亡,
    其值不再有效。
  • 返回指针的函数一般都返回
    全局数据对象 //全局变量
    指向字符串常量指针 //字符串常量存储在静态存储区域,所以一直都存在
    堆区的指针在函数内动态申请的指针变量
    主调函数中数据对象的地址//传入的实参的地址

另外也可以定义静态变量,迫使即时是在函数内申请的局部变量
也存储在静态存储区域,使它不因函数进程的结束而消亡。

  • 例如题目:输出月份英文名
char* getmonth(int n)
{
    静态数组的方法:
    static char month[13][15] = { "","January","February","March","April","May","June"
        ,"July","August","September","October","November","December" };

        if (n >= 1 && n <= 12)//n是合法数字;
    {
        return month[n];
    }
    else
    {
        return NULL;
    }
}

1.4 动态内存分配

1.4.1 为什么要动态内存分配,堆区和栈区区别。

  • 因为局部变量申请大小是有限制的,由栈的剩余空间决定。
  • 栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数)
    如果申请的空间超过栈的剩余空间时,将会出现问题。
  • 而堆区可以到G级别。

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

<stdlib.h>
calloc函数(会自动初始化)
void calloc ( unsigned n, unsigned size)
在内存的动态存储区堆中分配n个连续空间,每一存储空间的长度为size,并且分配后还把存储块里全部初始化为0
若申请成功,则返回一个指向被分配内存空间的起始地址的指针
若申请内存空间不成功,则返回NULL
形式如下:int
p = (int ) calloc (n, sizeof(int);
malloc函数(不会自动初始化)
void malloc(unsigned size)
在内存的动态存储区中分配一连续空间,其长度为size
若申请成功,则返回一个指向所分配内存空间的起始地址的指针p,若申请不成功,则返回NULL
**返回值类型:void (
),赋给指针要强制转换。

结构体、链表用比较多
形式如下:int
p = (int )malloc(nsizeof(int));
*free函数:
void free (void *ptr)
释放由动态存储分配函数申请到的整块内存空间,ptr为指向要释放空间的首地址。

无论是使用malloc函数还是使用calloc函数进行动态申请,
在程序的最后都要记得进行free(p)进行内存空间的释放,
否则会产生内存碎片,影响程序的进程。

1.4.3为多个字符串做动态内存

  • 对二维数组进行动态内存的申请:
    用C++语法:记得保存C++文件。(具体的操作俺还没有代码基础)
    这里放上动态申请二维整型数组的例子:
int main()
{
   //int p[1024][1024];
    int (*p)[1024];
   int i,j;
   p=new int[1024][1024];
    for(i=0;i<1024;i++)
       for(j=0;j<1024;j++)
           p[i][j]=i+j;
    for(i=0;i<1024;i++)
        for(j=0;j<1024;j++)
           printf("i=%d,j=%d,p=%d\n", i,j,p[i][j]);    
    delete []p;
    return 0;
}

1.5 指针数组及其应用

1.5.1 多个字符串用二维数组表示和用指针数组表示区别?

*首先指针数组表示的是数组,它的元素是指针,每一个指针可以指向不同的地址
*每一个元素可以指向一个字符串的首地址,这样就可以通过操作指针变量来操作
字符串。
*而二维数组它也是数组,它的元素是一个一个的字符,只不过相当于把它的一整
行当作一条字符串。
*而不同的行当作不同条字符串来处理。
*可以用指针数组的不同元素分别指向二维数组的每一行。

  • 用指针数组处理多个字符串时:
    *用二维数组:
void sort(char a[][11],int n){//注意二维数组的形参的设置
	int i,j;
	char temp[11];
	for(i=0;i<n-1;i++){
	  for(j=0;j<n-1-i;j++){
	  if(strcmp(a[j],a[j+1])>0){
	    strcpy(temp,a[j]);
        strcpy(a[j],a[j+1]);
	    strcpy(a[j+1],temp);}
	}}}

*用指针数组:

void fsort(char *color[ ], int n)
{     int k, j;
      char *temp;
      for(k = 1; k < n; k++)
         for(j = 0; j < n-k; j++)  
if(strcmp(color[j],color[j+1])>0){
              temp = color[j];
              color[j] = color[j+1];
              color[j+1] = temp;
           }

理解:
*由于指针数组内的指针数组可以相互赋值,所以不需要在使用strcpy函数;

*采用动态申请的方式来处理多个字符串:
ppt内的例题:例11-6 输入一些有关颜色的单词,每行一个,以#作为输入结束标志,再按输入的相反次序输出这些单词。
其中单词数小于20,每个单词不超过15个字母(用动态分配内存的方法处理多个字符串的输入)。
样例代码:(不是俺自己写的)

#include <stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
    int i, n = 0;
    char* color[20], str[15];
    scanf("%s", str);
    while (str[0] != '#') {
        color[n] = (char*)malloc(sizeof(char) * (strlen(str) + 1)); /* 动态分配 */
        strcpy(color[n], str);     /* 将输入的字符串赋值给动态内存单元 */
        n++;
        scanf("%s", str);
    }

    for (i = n - 1; i >= 0; i--) {
        printf("%s  ", color[i]);
        free(color[i]);
    } /* 释放动态内存单元 */
    return 0;
}

1.5.2 指针数组的细节:

  • 指针数组操作时:
    //以下表示,color是指针数组;

指针数组元素可以互相赋值。
//这一点非常重要!!!是指针数组最为灵活的地方。
tmp=color[0];
也可以间接访问操作数组元素所指向的单元内容
printf (“%c”, *(color[0]+1) );
输出的是对应的二维数组第0行列下标为1的字符。

1.6 二级指针

  • 二级指针是指向指针的指针。
  • 定义为:类型名 **变量名
  • 一般用于指向指针数组。在函数中,
    一般用指针数组当实参,而二维指针
    充当形参,用来存放指着数组传入的
    二级地址。
    *样例:用指针数组处理多个字符串
    代码如下:(不是俺自己写的)
#include <string.h>
void main( )
{   int i;
    char  *pcolor[ ] = {“red”, ”blue”,  
       ”yellow”, ”green”,  ”black”}; 
    void fsort(char *color[ ], int n);
    fsort(pcolor, 5); 
    for(i = 0; i < 5; i++) 
        printf("%s ", pcolor[i]);
}  

void fsort(char **color, int n)
{     int k, j;
      char *temp;
      for(k = 1; k < n; k++)
         for(j = 0; j < n-k; j++)  
if(strcmp(color[j],color[j+1])>0){
              temp = color[j];
              color[j] = color[j+1];
              color[j+1] = temp;
           }
} 
  • 理解:
    函数部分和刚才的指针数组对于多个字符串的处理差不多,
    主要的变化就是说,以二维数组为形参相当于将其封装为
    一个函数,然后可以按指针数组的写法来表示二维指针。
    pc 相当于 color 相当于 &color[0]
    *pc 相当于 color[0]
    *(pc+i) 相当于 color[i]
    **pc 相当于 (pc) 相当于 *color[0]
  • 二维指指针和一维一样有下标表示法和类下标表示法和直接操作指针的方法。
  • 小结:
    二级地址:二维数组名a,int p;
    二级地址做运算后是一级地址
    a+i ≠ a[i],前者是二级地址,后者是一级地址
    (a+i)=a[i]:一级地址
    地址+数值=地址,
    (a+i)+j
    一级地址
    运算后就是内容
    eg:*a[i]=
    (a+i)=a[i][0]

1.7 行指针、列指针

1.7.1 行指针

形式:int (*p)[n]
含义:p为指向含有n个元素的一维数组的指针变量。二级指针。
行指针p是行地址性质的指针。
此时,p+i=a+i ≠ a[i]
(p+i)=(a+i) = a[i]

1.7.2 列指针

列指针是一级指针,可以直接把它当成
普通的指针来看就行。
不一样的地方在于它的指向是基于一个已存在的二维数组。
在初始化时,我们需要将它指向这个二维数组某一行
的首地址。
int *p;p=a[0];
*(p+i),表示离a[0][0]第i个位置的元素

2.PTA实验作业(7分)

2.1 查找子串(2分)

2.1.1 伪代码

while (*sPrt && *sPrt!='\n')//遍历fgets输入的主串;
{
      locPrt = sPrt;//标记可能的子串在主串中的位置;
      while (*sPrt == *tPrt)//找到相同的元素后开始同时遍历;
            sPrt++, tPrt++;
      如果找到(指子串指到了'\0'且没有指到'\n'),返回locPtr(地址)
      找不到(子串没有遍历到最后);归首子串,继续往下遍历主串;
      sPrt++;(遍历主串)
}
最后遍历完主串如果没有找到的话;(有找到会提前跳出函数)
返回NULL;

2.1.2 代码截图

  • 俺的代码的提交:

2.1.3 找一份同学代码(尽量找思路和自己差距较大同学代码)比较,说明各自代码特点。

  • 小林的代码:(林进源)

  • 小林的代码的提交:

  • 小林代码的特点:
    主要是运用了数组的特点来查找子串。

  • 小陈的代码:(陈宇杭)

  • 小陈代码的提交:

  • 小陈代码的特点:
    1.有点类似数组的思维,但是本质上使用的是
    标准的指针的写法。
    2.提供了一种思路,对指针或许没有必要让指针动起来,
    可以用一个数字变量来控制控制指针的地址的表达式的
    值,从而想控制数组的下标一样地去控制指针,而又不
    会像俺的去移动指针的那种方法,需要时刻注意指针的
    移动。
    3.合理运用了for语句的多重条件的写法。

  • 俺的代码:
    1.本质上是对超星视频学习的产物;
    2.有一个测试点没有过
    3.是采用让指针真实动起来的办法。

2.2 合并2个有序数组(2分)

2.2.1 伪代码

void merge(int* a, int m, int* b, int n)
{
先把b数组中的数字全部接到a数组尾部;
然后使用 选择法排序 将他们升序排序;
}

2.2.2 代码截图

  • 我自己的原始代码(有一个测试点没有过:运行超时):

  • 俺代码的提交:

2.2.3 找一份同学代码(尽量找思路和自己差距较大同学代码)比较,说明各自代码特点。

  • 小林的代码:

  • 小林代码的提交:

*如果遇到b中最小大于a中最大的话,那直接就相当于把b接到a后;
*有交错的情况,那按这个方法就是从大到下,从后到前进行输入,
特殊的地方是,由于a已经排好序,所以这样进行的过程中,a对a
的操作相当于就是在a数组内进行移动,不会额外侵占有效空间。

  • 小林代码的特点:很好地利用了数组a长度的特点,创新性地采用了从逆向
    载入数据的数组重构方式,这样做的话:
    1.不需要重新动态申请一个新的数组来分别存放,
    2.不需要多层循环嵌套,效率更高。

  • 谈谈俺的代码:
    1.能利用好学到的排序方法;
    2.没有利用好已知数组已经排好序的特点来展开
    3.无法避免在逻辑上使用排序法必须经历的循环
    嵌套带来的时间效率上的损失。

  • 这类题目特点的反思:
    在题目的已知条件中给出的题设如果含有已经排
    好序的特点的话,那比起粗暴地整合和重新排序
    更好的方法是对排好序的内容进行移动和重构,
    有点类似于数组的插入数据的思路。

2.3 选择说反话-加强版(3分)

2.3.1 伪代码

  • 我自己的版本(有一个测试点没有过):
动态申请一个大小为(500001*sizeof(char*))字符指针char* sentence用来存放句子;
动态申请一个大小为(250001 * sizeof(int*))整型指针int* loc用来存放大写字母的下标,
用每个大写字母下标位置来表示每个单词的位置;
输入的字符串的有效长度len=strlen(sentence);
从len-1开始逆向用下标法遍历sentence,并将找到的大写字母的下标
按顺序赋值给loc数组;
然后从0到j-1(loc的实际长度-1)遍历loc数组,用loc数组的内容充当sentence的下标,
然后让sentence数组从每个大写字母开始一个一个字符输出直到字符为空格或不是换行符
但是是'\0'为止;
输出完每个单词如果i != j-1(也就是说不是最后一个单词时),都要输出一个空格用来间隔。
  • 看完老师的视频后重新写的版本:
动态申请字符指针sentence和输入
void ReverseStr(char* startPtr)
char* endPtr = startPtr;//定义尾部指针;
while (*endPtr && *endPtr != '\n') endPtr++;
//让尾部指针指向尾部,并注意不要指到fgets自带的换行符;
定义用来遍历的指针并让它指向尾部指针减一;//也就是指向最后一个字母字符;
while (Ptr != startPtr)
{
    当字符不是空格时开始计算单词长度
        当字符遍历到空格时说明完成了一个单词的遍历
        用printf("%.*s", len, Ptr)来输出单词;//用一个状态变量
        来控制输出格式,即有空格隔开;
}
如果以空格开头,那第一个单词会在前面也就被输出,
如果没有的话,那就要另外输出第一个字符,
并且要注意不是空格开头并且只有一个单词的句子的情况;

2.3.2 代码截图

  • 我自己的版本(有一个测试点没有过):

  • 代码的提交:

  • 看完老师的视频后重新写的版本:

  • 代码的提交:

2.3.3 请说明和超星视频做法区别,各自优缺点。

  • 先谈谈俺的代码:
  • 优点是:
    1.利用了类似哈希数组的方法,
    2.巧妙利用循环变量的关系来控制格式,
    3.在查找上利用大写字母这一特点,快速定位了对象
    更倾向于面对对象,更高效。
  • 缺点是:
    1.有一个测试点没有过,而且到现在也没有想出来。
    2.以及相对来说,没有发挥出指针的特点,
    更多时借用了数组的思维。
  • 再谈谈超星视频的做法;
  • 优点是:
    1.巧妙利用指针进行逆向遍历
    2.使用了printf("%.*s", len, Ptr)这种操作字符串
    中的字符串的比较好的输出方法。
    3.用状态变量来解决格式问题
    4.对逆序过程分析地清晰,是很标准的面向过程的编程思想。
posted @ 2020-12-27 15:26  嘟嘟勒个嘟  阅读(215)  评论(0编辑  收藏  举报