这个作业属于哪个班级 | C语言--网络2011/2012 |
---|---|
这个作业的地址 | C博客作业05--指针 |
这个作业的目标 | 学习指针相关内容 |
姓名 | 骆梦钒 |
1.本章学习总结(3分)
整理指针主要知识点,必须包含内容有:
1.1 指针定义、指针相关运算、指针做函数参数。
1.指针定义
什么是指针?
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。
指针变量的定义格式为:
类型说明符 *指针变量名 [=初值];
要同时定义两个指针变量:
int p1=NULL,p2=NULL;
(NULL是c语言中预定义的空指针关键字,它作为一个特殊的指针,表示不指向任何对象)
定义指针变量时要注意以下几点:
(1)类型说明符表示该指针变量所指向的变量的数据类型。
(2)定义指针变量时,指针变量名前必须有一个“ * ”号,表示定义的变量是指针变量。
(3)指针变量在定义时允许对其初始化
指针变量的使用
两个重要运算符:
(1)&:取地址运算符,如p=&a; 则p为变量a的地址。
(2):指针运算符,后面只能接指针变量。用于访问指针变量所指向的变量。如:*p表示访问指针变量p所指向的变量的内容
int a=8,b;
int *p;
p=&a;
对变量a有两种访问方式:
(1)直接访问。b=a;
(2)通过指针变量间接访问。b=*p;
注意:指针是一个地址,而指针变量是存放地址的变量。 |
2.指针运算
(1)指针和整数的加减运算:
可以通过指针与整数的加减运算来移动指针p,实现对不同数据单元的访问操作。对不同的数据类型,移动的单位长度不同。单位长度一般是指针所指向的变量的
数据类型长度。
格式一:p=p+n;(或p=p-n;)
格式二:p++;(或p–;)
格式三:++p;(或–p;)
注意:指针变量的值加1(或减1),并不是给地址加1(或减1),而是加上(或减去)1个数据类型长度,也就是指针所指向的变量在内存中所占的字节数。
(2)指针和指针的赋值运算:
int a=10,*p,*q;
p=&a;
q=p;
//p和q的值都是变量a的地址。
3.指针做函数参数
·实参和形参之间的数据传输是单向的“值传递”方式,也就是实参可以影响形参,而形参不能影响实参。指针变量作为参数
也不例外,但是可以改变实参指针变量所指向的变量的值。
·在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用bai指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
·像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些
数据集合。有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。
#include <stdio.h>
void swap1(int x,int y),swap2(int *px,int *py),swap3(int *px,int *py);
int main(void)
{
int a=1,b=2;
int *pa=&a,*pb=&b;
swap1(a,b);
printf("s1:a=%d,b=%d
",a,b);
a=1,b=2;
swap2(pa,pb);
printf("s2:a=%d,b=%d
",a,b);
a=1,b=2;
swap3(pa,pb);
printf("s3:a=%d,b=%d
",a,b);
return 0;
}
void swap1(int x,int y)
{
int t;
t=x;
x=y;
y=t;
}
void swap2(int *px,int *py)
{
int t;
t=*px;
*px=*py;
*py=t;
}
void swap3(int *px,int *py)
{
int *pt;
pt=px;
px=py;
py=pt;
}
第一个swap1函数运行结果是:x=2 y=1.但是第一个函数的形参是(int x,int y),实参是(int a,int b)。因为实参可以影响形参,而形参不可以影响实
参。所以a,b的值是不变的。
第二个swap2函数由图可知,pa和px都是a的地址,pb和py都是b的地址。此函数改变px,py的值。因为px和a在同一储存单元,py和b在同一储存单元。以改
变实参指针变量所指向的变量的值。所以pa和pb的值也改变了,最后输出结果也就改变了。
第三个swap3同样的道理,如上图直接改变了形参指针px和py的值,改变的只是地址,地址所对应的值没有改变,又因为形参是不会影响实参,所以pa和pb没有
改变。所以a,b值没有改变。
1.2 字符指针
包括指针如何指向字符串、字符串相关函数及函数代码原型的理解、字符串相关函数用法(扩展课堂未介绍内容)
定义:
指向字符型数据的指针变量。每个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。即将字符串的首地址赋值给字符指针,可让字符指针指
向一个字符串。
char *ptr = "Hello";//将保存在常量存储区的"Hello"的首地址赋值给ptr
与
char *ptr;
ptr = "Hello";//是等价的,注意不能理解为将字符串赋值给ptr
char str[10] = "Hello";
char *ptr = str;//数组名代表数组的首地址
//等价于
char *ptr;
ptr = str;//等价于ptr = &str[0];将数组的首地址赋给字符指针ptr
用法
·对于数组名str,不能使用str++操作使其指向字符串中的某个字符,因为数组名是一个地址常量,其值是不能被改变的。
(ptr+i):字符串中第i+1个字符,相当于(str+i),即str[i],也可以用ptr++,移动指针ptr,使ptr指向字符中的某个字符
·字符串的长度(指字符串所含的字符个数,但不包括最后的’ ’)与字符数组的大小(指字符串所包含的字符个数加上’ ’,故+1)不一样。
·scanf("%s",str);表示读入一个字符串,直到遇到空白字符(空格、回车符、制表符)为止
如果输入带有空格的字符串(比如人名)只会读到空格以前而空格以后不会读入,留在了输入缓冲区中
printf("%s",str);表示输出一个字符串,直到遇到字符串结束标志' '为止(注意这里可以带有空格输出)
·gets():可以输入带有空格的字符串。以回车符作为字符串的终止符,同时将回车符从输入缓冲区读走,但不作为字符串的一部分。而scanf()不读走回车符,
回车符仍留在输入缓冲区中。 gets()不能限制输入字符串的长度,很容易引起缓冲区溢出。同样即使scanf(“12s%”,name)也不能解决这个问题。当使用
scanf()和gets()时,确保输入字符串的长度不超过数组的大小,否则使用限制输入字符串长度的函数: fgets(name,sizeof(name),stdin)
puts():用于从括号内的参数给出的地址开始,依次输出存储单元中的字符,当遇到第一个’ ’时输出结束,并且自动输出一个换行符’ ’
#include <stdio.h>
#define N 12
int main()
{
char name[N];
char *ptrName = name;//声明一个指向数组name的字符指针ptrName
//注意这里,用字符指针输入字符串前提,必须保证字符指针事先已指向一个数组的首地址,如ptrName指向了数组name,如果只是单纯声明字符指针,编译会报错
printf("Enter your name:");
gets(ptrName);//输入字符串存入字符指针ptrName所指向的内存
//为提高程序安全性,也可以使用fgets(name,sizeof(name),stdin);
printf("Hello %s!
",ptrName);
return 0;
}
函数
·字符串比较函数
int strcmp(const char* str1, const char* str2);
字符串连接函数
int strcat(char *str1 , char const *str2);
char *MyStrcat(char *str1,char *str2)
{
char *pStr = str1;//定义一个字符指针并保存字符串str1的首地址
while(*str1! = ' ')//当没有读到str字符串的结束标志
{
str1++;//将指针移动到字符串str1的末尾
}//注意这里,在读到' '之前时,指针已经移到了这个位置,当读到时跳出循环不再指向下一个
所以复制str2的首地址是' '之前的位置
for(;*str2!=' ';str1++,str2++)
{
*str1 = *str2;//将字符串str2复制到字符串str1的后面
}
*str1 = ' ';//在连接后的字符串的末尾添加字符串结束标志
return pStr;//返回连接后的新的字符串str1的首地址
}
字符串复制函数
·char * strcpy( char *str1, char const *str2 );
void MyStrcpy(char *str1,char *str2)
{
while(*str2!=' ')//若当前str2所指字符不是字符串结束标志
{
*str1 = *str2;//复制字符
str2++;//使str2指向下一个字符
str1++;//使str1指向下一个存储单元
}
*str1 = ' ';//当复制循环结束时,在str1的末尾添加字符串结束标志
}
·字符串求长度
int strlen(char *s);
unsigned int MyStrlen(const char *pStr)//为防止实参在被调函数中被意外修改,在相应的形参前面加上类型限定符const
{
unsigned int len = 0;//计数器设置为0
for(;*pStr !=' ';pStr++)
{
len++;//循环统计不包括' '在内的字符个数
}
return len;
}
·字符串转数字
int atoi (const char * str);
·数字转字符串
int sprintf( char *buffer, const char *format [, argument] ... );
1.3 指针做函数返回值
具体格式是什么,注意事项。
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个:
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2)
{
if(strlen(str1) >= strlen(str2))
{
return str1;
}
else
{
return str2;
}
}
int main()
{
char str1[30], str2[30], *str;
gets(str1);
gets(str2);
str = strlong(str1, str2);
printf("Longer string: %s
", str);
return 0;
}
注意:函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机
制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
#include <stdio.h>
int *func()
{
int n = 100;
return &n;
}
int main()
{
int *p = func(), n;
n = *p;
printf("value = %d
", n);
return 0;
}
运行结果:
value = 100
修改后
#include <stdio.h>
int *func()
{
int n = 100;
return &n;
}
int main()
{
int *p = func(), n;
printf("c.biancheng.net
");
n = *p;
printf("value = %d
", n);
return 0;
}
运行结果:
c.biancheng.net
value = -2
第一个例子在调用其他函数之前使用 *p 抢先获得了 n 的值并将它保存起来,第二个例子显然没有抓住机会,有其他函数被调用后才使用 *p 获取数据,这个
时候已经晚了,内存已经被后来的函数覆盖了,而覆盖它的究竟是一份什么样的数据我们无从推断(一般是一个没有意义甚至有些怪异的值)。
上述提到的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例
子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据
就失去了意义。
1.4 动态内存分配
为什么要动态内存分配,堆区和栈区区别。动态内存分配相关函数及用法。举例为多个字符串做动态内存要如何分配。
为什么要动态内存分配
·因为内存太宝贵。
·如果全部是静止内存不能释放,对于小的程序可以运行完毕。但是对于大的程序,还没运行完,内存就要被占用完,此时就要发生内存泄露。
·给定一个占用内存可变大小的变量(假设是数组的长度len),给该变量通过函数动态分配内存后,分配内存的大小是根据数组的长度len决定的,假定用户输入len的大小是5,系统就会动态的给该数组分配长度为5的内存,该段代码运行结束后,系统调用free()函数释放分配的内存,然后接着运行剩下的程序。换句话说,动态分配内存可以根据需要去申请内存,用完后就还回去,让需要的程序用。
什么时候需要动态分配内存
当程序中有比较大的数据块需要使用内存的时候使用。原因:比较大的数据块如果使用了静态内存,在该数据块运行完毕后不能动态的释放该内存,直到整个程
序运行完才能释放,如果整个程序比较大,有可能因为内存不够而发生错误。
堆区和栈区区别
定义:
栈区:
存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的
指令集,效率很高,但是分配的内存量有限,比如iOS中栈区的大小是2M。
堆区:
就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说
的就是堆区。
栈区和堆区大小差异
栈区:由图中其实可以知道,栈区是向低地址扩展的,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,大小在进程
分配时是确定的,具体大小看编译器,操作系统。所需大小一般小于10M!太大没有意义,不符合栈是用来快速存取的目标。
堆区:堆区是向高地址扩展的,是不连续的内存区域(这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的是动态分配的),因为会手动进行分
配,会大一些,大小不固定。
栈区和堆区效率差异
栈区:由系统自动分配,速度较快。但程序员是无法控制的。(只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。)
堆区:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。(首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中)
1.5 指针数组及其应用
数组指针、指针数组、字符串指针
数组指针
类型说明符 * 变量名;//定义
例:
int num[] = {1,2,3};//数组不能先定义后初始化,定义后只能单个元素初始化
//即int num[];num[] = {1,2,3};(错误)
//int num[];num[0]=3;(正确)
int *p;
p = num;
指针数组
类型说明符 * 数组名[数组长度];//定义
类型说明符:表示指针所指向变量的数据类型
- 数组名:表示该数组为指针数组
例:
char *str[3] = { "one","two","three" };
printf("%s
", str[0]);
输出结果为:
one
在C语言和C++等语言中,数组元素全为指针变量的数组称为指针数组,指针数组中的元素都必须具有相同的存储类型、指向相同数据类型的指针变量。
指针数组比较适合用来指向若干个字符串,使字符串处理更加方便、灵活。
也可以作为函数的参量使用,使用方式与普通数组类似。
字符串指针
类型说明符 * 变量名;//定义
例:
char *str1 = "helloworld";
//上面代码等同于 char *str1;str1="helloworld";
//所以字符串指针相当于字符类型的指针,只不过初始化和定义一步完成
printf("%s
",str1+4);
输出结果为
oworld
多个字符串用二维数组表示和用指针数组表示区别
二维字符数组bai一旦定义,那么每个字du符串的zhi最大长度、首地址都不dao能改变了。
字符指针数组,顾名思义,它是存放字符指针的数组。由于它仅用来存放指针,所以它指向的每个字符串的首地址可改变,字符串最大长度也可以改变。
相比而言,字符指针数组更灵活一些。
二维数组的一般定义和赋值方式为:
char a[3][3];
int i;
for(i=0;i<3;i++)
{
gets(a[i]);
}
指针数组的一般定义形式和存储字符串的操作方法:
char *a[3],s[3];
int i;
for(i=0;i<3;i++)
{
gets(s);
a[i]=(char*)malloc(strlen(s)+1);
strcpy(a[i],s);
}
1.6 二级指针
定义
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:
int a =100;
int *p1 = &a;
int **p2 = &p1;
指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。
p1 是一级指针,指向普通类型的数据,定义时有一个;p2 是二级指针,指向一级指针 p1,定义时有两个。
1.7 行指针、列指针
定义格式、主要用法。
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
行指针
定义
行指针:指的是一整行,不指向具体元素。
a是这个“特殊的”一维数组的名称,也就是首地址,也就是第一个元素的地址,也就是第一行的首地址,是指首行一整行,并不是指某个具体元素。那么我们称之为“行指针”。
#include <stdio.h>
void main()
{
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int (*p)[4]= &a[0]; // 行指针定义法或者int (*p)[4]= a;
int i, j;
for(i = 0; i < 3; i++)
for(j = 0; j < 4; j++)
{
printf("%d ",*(*(p + i) + j));
}
return;
}
列指针
定义
列指针:指的是一行中某个具体元素。
我们来放大观看首行,首行的元素分别是:a[0][0],a[0][1],a[0][2],a[0][3]。将其看作一个独立的一维数组,那么 a[0]就是这个数组的名称,也就是这个数组的首地址,也就是第一个元素的地址,也就是a[0]+0。a[0]和a[0]+0都是指具体的元素,那么我们称之为“列指针”。
#include <stdio.h>
void main()
{
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int *p= a[0]; // 列指针的定义法
for(; p < a[0] + 12; p++)
{
printf("%d ",*p);
}
return;
}
行指针和列指针
可以将列指针理解为行指针的具体元素,行指针理解为列指针的地址。
那么两个概念之间的具体转换是:
*行指针----列指针
&列指针----行指针
对于元素a[1][2],其地址用列指针表示为a[1]+2,等价表示为(a+1)+2,那么内容是(*(a+1)+2);
2.PTA实验作业(7分)
2.1 题目名1(2分)
藏尾诗
2.1.1 伪代码
/*伪代码
for i=0 to 4 then
{
get//输入数组
strcpy//将str赋值到s[i]
}
for i=0 to 4 then
{
printf(s[i]+strlen(s[i])-2)//输出每句最后一个字
}*/
2.1.2 代码截图
2.1.3 找一份同学代码(尽量找思路和自己差距较大同学代码)比较,说明各自代码特点。
同学的:
通过s[i][m-2]来确定每句诗最后一个字的位置存入w数组中(其中汉子占两个字节,w[2i]和w[2i+1]设置巧妙,易懂明了
自己的:
运用动态内存分配,通过用两个数组输出时较为简便
2.2 题目名2(2分)
选择合并2个有序数组这题介绍做法。
2.2.1 伪代码
/*伪代码
while(i,j均大等于0)
{
if(a的最大值小于b的最大值)
a[k--]=b[j--]//存入b
else
存入a
}
while(j大等于0)//b未存完
存入b
2.2.2 代码截图
2.2.3 找一份同学代码(尽量找思路和自己差距较大同学代码)比较,说明各自代码特点。
展示同学代码,介绍做法具体区别。
同学的:
通过定义一个新数组s,将a数组先存放进去,在通过分多钟情况比较a与b的大小进行重新排序,
过程清晰易懂,但因为分多钟情况if句较多,效率较低。
自己的:
没有新定义数组,将总体分为两个情况:1、两个数组元素均大于等于0进入循环,比较a和b中最大值的大小,将更大的从后往前存入a中,2、当b中的元素未存放完,再写一个循环将b中剩余元素存入a中。
从后往前遍历两个数组,不需要分类讨论那么多情况
2.3 题目名3(3分)
选择说反话-加强版这题介绍字符指针操作如何操作字符串。并说明和超星视频做法区别。
2.3.1 伪代码
len=strlen(str)
for i= len-1 to 0 i-- then//逆向遍历
if(str[i]!=' ')
rear=i////指向单词的最后一个字母
for j=i-1 to 0 j-- then
if(str[j]==' ') break
front=j+1//指向第一个字母
if() printf(空格)
for k=front to rear k++ then
printf
i=j
2.3.2 代码截图
2.3.3 请说明和超星视频做法区别,各自优缺点。
超星:
分装了函数 通过用三个指针分别控制首、尾、逆向遍历指针,设计巧妙从后向前扫描到每个空格初再用printf("%.*s",长度len,地址p )输出,效率高且代码简洁。
自己的:
通过逆向遍历找到每个单词的首字母和末字母的位置,然后循环逐个逆向输出,没有用到指针,设置的变量较多,for循环中的代码繁琐
也有尝试过用函数和指针做
通过递归来实现反向输出但一直显示格式错误和运行时错误