这个作业属于哪个班级 | C语言--网络2011/2012 |
---|---|
这个作业的地址 | C博客作业05--指针 |
这个作业的目标 | 学习指针相关内容。 |
姓名 | 喻文康 |
0. 展示PTA总分
1. 本章学习总结
1.1 指针定义、指针相关运算、指针做函数参数
指针定义:指针变量的定义格式为:类型说明符 *指针变量名,如int* p
指针相关运算:
- 指针减去一个指针,表示两个指针之间所差的内存单元或者元素个数,两个指针相加没有意义
- 关系运算,如果定义了两个指针变量p和q,并且都已经初始化了,
如果p==q,则表示p和q指向同一个地址
p>q,则表示p指向高地址而q指向低地址
也经常拿p和NULL做比较,用来表示指针当前的状态*
的优先级是一级,即最高级,但它的规律是从右向左,所以*p++
应当写作(*p)++
才对
int a=8,b;
int *p;
p=&a;
&a | 表示变量a的地址,&a=2000 |
---|---|
p | 指针变量p的值,p=2000=&a |
*p | 表示指针变量p指向的变量,*p=a=8 |
&*p | 相当于&(p),&(p)=&a=2000 |
*&a | 相当于(&a),(&a)=*p=a=8 |
&p | 表示指针变量p的地址,&p=2000 |
*&p | 相当于(&p),(&p)=2000 |
指针做函数参数:
在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,
并且这些数据不会随着函数的结束而被销毁。
像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,
只能传递它们的指针,在函数内部通过指针来影响这些数据集合。
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)不一样。具体如:
for(i = 0;str[i] != ' '; i++ )
{
printf("%c",str[i]);//常用借助字符串结束标志' '识别字符串的结束
}
scanf("%s",str);//表示读入一个字符串,直到遇到空白字符(空格、回车符、制表符)为止
//如果输入带有空格的字符串(比如人名)只会读到空格以前而空格以后不会读入,留在了输入缓冲区中
printf("%s",str);//表示输出一个字符串,直到遇到字符串结束标志' '为止(注意这里可以带有空格输出)
注意:scanf()
不能限制输入字符串的长度,很容易引起缓冲区溢出。当使用scanf()
和gets()
时,
确保输入字符串的长度不超过数组的大小,否则使用限制输入字符串长度的函数: fgets(name,sizeof(name),stdin)
④if(str1==str2)时合法语句,比较这两个地址的大小(作为指针,有不同的地址,一定为假),而不是比较字符串的大小,也不是比较字符串的内容。
字符串相关函数及函数代码原型理解:
- 字符串比较函数
int strcmp(const char* str1, const char* str2);
strcmp函数实际上是对字符的ASCII码进行比较 , 其中str1和str2可以是字符串常量或者字符串变量,返回值为整形,所以区分大小写。
① str1<str2,返回负值或者-1; ② str1=str2,返回0; ③ str1>str2,返回正值或者1;
2.字符串连接函数
int strcat(char *str1 , char const *str2);
将字符串str2连接在str1后,并且str1最后的结束字符NULL会被覆盖掉,并且连接后的字符串的尾部会再增加一个NULL.
注意:str1和str2所指的内存空间不能重叠,且str1要有足够的空间来容纳要复制的字符串。返回石str1字符串的首地址。在str1的末尾注意放结束标志' '。
3.字符串复制函数
char * strcpy( char *str1, char const *str2 ); //字符串复制函数
将str2所指的字符串复制到str1所指的字符串中。注意:src1和str2所指内存区域不可以重叠且str1必须有足够的空间来容纳str2的字符串。
另外,还有strncpy函数,与strcpy函数功能相似
语法:char *strncpy(char *destinin, char *source, int maxlen);
参数:
destinin
:表示复制的目标字符数组;
source
:表示复制的源字符数组;
maxlen
:表示复制的字符串长度。
复制字符串source中的内容(字符,数字、汉字....)到字符串destinin中,复制多少由maxlen的值决定。
如果source的前n个字符不含NULL字符,则结果不会以NULL字符结束。
如果n<source的长度,只是将source的前n个字符复制到destinin的前n个字符,不自动添加' ',也就是结果destinin不包括' ',需要再手动添加一个' '。
如果source的长度小于n个字节,则以NULL填充destinin直到复制完n个字节。source和destinin所指内存区域不可以重叠且destinin必须有足够的空间来容纳source的字符长度+' '。
strcpy与strncpy的区别:
strcpy只是复制字符串,但不限制复制的数量,很容易造成缓冲溢出。strncpy要安全一些。
strncpy能够选择一段字符输出,strcpy则不能。
4.字符串求长度
int strlen(char *s); //s为指定的字符串
5.字符串转数字
int atoi (const char * str);//转化成整形,头文件:#include <stdlib.h>
atoi()
函数会扫描参数 str
字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace()
函数来检测),
直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(' ')才结束转换,并将结果返回。
【返回值】返回转换后的整型数;如果 str
不能转换成 int
或者 str
为空字符串,那么将返回 0。
另外atol
(将字串转换成长型数);atof
(将字串转换成浮点型数) ;strtod
(将字符串转成double);
strtol(char *nptr,char **endptr,int base)
(转化为long int);strtoul
(将字串转换成无符号32位型数)
6.小写转大写
int toupper( int ch );//转大写
7.大写转小写
int tolower( int ch );//转小写
拓展:字符处理函数
函数原型 | 功能描述 |
---|---|
int isdigit(int c); |
判断是否为数字,返回真假值 |
int isalpha(int c); |
判断是否为字母,返回真假值 |
int isalnum(int c) |
判断是否为字母或者数字,返回真假值 |
int islower(int c) |
判断是否为小写字母,返回真假值 |
int isupper(int c) |
判断是否为大写字母,返回真假值 |
int isspace(int c) |
若c是空白符(换行符(’ ’)、空格符(’ ‘)、换页符(’f’)、回车符(’ ’)、水平(’ ’)、垂直制表符(’v’)),返回真值 |
1.3 指针做函数返回值
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个:
注意:用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,
函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
1.4 动态内存分配
动态内存是相对静态内存而言的。所谓动态和静态就是指内存的分配方式。动态内存是指在堆上分配的内存,而静态内存是指在栈上分配的内存。
前面所写的程序大多数都是在栈上分配的,比如局部变量、形参、函数调用等。栈上分配的内存是由系统分配和释放的,空间有限,在复合语句或函数运行结束后就会被系统自动释放。
而堆上分配的内存是由程序员通过编程自己手动分配和释放的,空间很大,存储自由。
malloc函数的使用
malloc 是一个系统函数,它是 memory allocate 的缩写。其中memory是“内存”的意思,allocate是“分配”的意思。
顾名思义 malloc 函数的功能就是“分配内存”。要调用它必须要包含头文件<stdlib.h>。它的原型为:
# include <stdlib.h>
void *malloc(unsigned long size);
例如如下代码:
double * ptd;
ptd = (double * ) malloc (30 * sizeof(double));
malloc 函数只有一个形参,并且是整型。该函数的功能是在内存的动态存储空间即堆中分配一个长度为size的连续空间。函数的返回值是一个指向所分配内存空间起始地址的指针,类型为 void*型。
简单的理解,malloc 函数的返回值是一个地址,这个地址就是动态分配的内存空间的起始地址。如果此函数未能成功地执行,如内存空间不足,则返回空指针 NULL。
注意:在定义指针数组后,一定要给它动态内存分配以及有指向,避免成为野指针。另外,在使用完分配的内存后要给与内存释放,具体如下
free函数的使用:
前面讲过,动态分配的内存空间是由程序员手动编程释放的。那么怎么释放呢?用 free 函数。
free 函数的原型是:
# include <stdlib.h>
void free(void *p);
free 函数无返回值,它的功能是释放指针变量 p 所指向的内存单元。此时 p 所指向的那块内存单元将会被释放并还给操作系统,不再归它使用。操作系统可以重新将它分配给其他变量使用。
需要注意的是,释放并不是指清空内存空间,而是指将该内存空间标记为“可用”状态,使操作系统在分配内存时可以将它重新分配给其他变量使用。
p 所指向的内存空间是一样的。所以释放后 p 所指向的仍然是那块内存空间。既然指向的仍然是那块内存空间,那么就仍然可以往里面写数据。可是释放后该内存空间已经不属于它了,该内存空间可能会被分配给其他变量使用。如果其他变量在里面存放了值,而你现在用 p 往里面写入数据就会把那个值给覆盖,这样就会造成其他程序错误。所以当指针变量被释放后,要立刻把它的指向改为 NULL。
1.5 指针数组及其应用
指针数组(也就是元素为指针类型的数组)常常作为二维数组的一种便捷替代方式。一般情况下,这种数组中的指针会指向动态分配的内存区域。
例如:
char *arr[4] = {"hello", "world", "shannxi", "xian"};
//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。
这样写往往可以避免内存浪费,那么我们来看看指针数组都有哪些应用
1. 指针数组在参数传递时的使用
如果是向子函数传参,这和传递一个普通数组的思想一样,不能传递整个数组过去,如果数组很大,这样内存利用率很低,所以应该传递数组的首地址,
用一个指针接收这个地址。因此,指针数组对应着二级指针,关于二级指针,后面会详细介绍。
2. 指针数组的排序
指针数组的排序非常有趣,因为这个数组中存放的是指针,通过比较指针指向的空间的大小,排序这些空间的地址。函数实现如下:
void sort(char **pa, int n)//冒泡排序
{
int i, j;
char *tmp = NULL;
for(i = 0; i < n-1; i++)
{
for(j = 0; j < n-1-i; j++)
{
if((strcmp(*(pa+j), *(pa+j+1)) > 0)
{
tmp = *(pa + j);
*(pa + j) = *(pa + j + 1);
*(pa + j + 1) = tmp;
}
}
}
}
3. 指针数组与二维数组的区别
在C语言中,一个字符串常量不可以直接赋给一个数组,比如:char s[10]; s="hello";这种写法是错误的。
但是一个字符串却可以直接赋给一个指针:char *p; p="hello";这仅限于将一个字符串常量的地址赋给指针p,
但如果char s[10]; char *p=s; *p="hello";这就是非法的,这就相当于直接把字符串常量赋给一个数组,
其实字符串常量赋给一个数组可以采用strcpy()函数。
1.6 二级指针
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:
例如:
int a=10;
int *p=&a; //取a的地址传给p
int *(*pp)=&p; //取p的地址传给pp
1.7 行指针、列指针
行指针:指的是一整行,不指向具体元素。
列指针:指的是一行中某个具体元素。
可以将列指针理解为行指针的具体元素,行指针理解为列指针的地址。
那么两个概念之间的具体转换是:
*行指针----列指针
&列指针----行指针
行指针 | 转换成:列指针 | 列指针等价表示 | 内容 | 内容等价表示 | 含义 |
---|---|---|---|---|---|
a或a+0 |
*a |
a[0] |
*a[0] |
*(*a) |
a[0][0] |
a+1 |
*(a+1) |
a[1] |
*a[1] |
*(*(a+1)) |
a[1][0] |
a+2 |
*(a+2) |
a[2] |
*a[2] |
*(*(a+2)) |
a[2][0] |
int a[3][4]={1,3,5,7,9,11,3,15,17,19,21,23};
表示形式 | 含义 | 指针类型 |
---|---|---|
a或者a+0 | 指向第0行 | 行指针 |
a+1 | 指向第1行 | 行指针 |
a+2 | 指向第2行 | 行指针 |
示例1:用列指针输出二维数组。
示例2:用行指针输出整个二维数组。
2. PTA实验作业
2.1 7-2 藏尾诗
2.1.1 伪代码:
定义一个二维数组str1[4][20],输入诗,注意没有输入的放' ',或者以指针数组的形式输入
定义一个一维数组str2[10],存放诗的尾
判断每一行诗的长度,将最后两个字符输入到str2中,注意下标要自增,如str2[j++] = str1[i][lens - 2];
最后在str2的末尾放结束标志符
输出str2
2.1.2 代码截图
2.2.3 找一份同学代码
学习点:使用了动态分配,使代码更健壮,因为用户输入的也可能是一首词,用数组的话就需要重新分配更大的内存,就容易造成内存浪费。
2.2 6-9 合并两个有序数组
伪代码:
因为一边排序一遍调整数组a会破坏数组a的数据,所以应先排序,放到另一个数组中,再复制数组到数组a即可
先定义新数组c,即各数组的小标aIndex,bIndex,cIndex并赋初值为0。
while (aIndex < m && bIndex < n)//读数组,当某一个数组结束时循环结束。
{
两个数组的值进行比较并排序
if (*(a + aIndex) < *(b + bIndex)a数组下标为aIndex的数存放到数组c中
else b数组小标为bIndex的数存放到c数组中
}
end while
while (aIndex < m)
将剩下的数据直接复制到c数组
end while
while (bIndex < n)
同上一个while
end while
复制数组c到数组a
2.2.2 代码截图
2.2.3 找一份同学代码
学习点:相较我的代码会显得很简洁,语句很精简,如num[j++]=num[k++]
;能够熟练掌握并运用二级指针。
2.3 7-4 说反话-加强版
2.3.1 伪代码
int main(void)
{
static char input[500001];存放输入的数据
int begin每个单词开头的下标, end每个单词结尾的下标,sign = 0判断一个单词是否读完,读完就要开始输出了;
int i = 0, length数组长度,含换行符, j;
length = strlen(input);
for (逆向遍历数组)
{
if (读到空格时)
{
if (一个单词读完,开始输出这个单词)
{
sign = 0;
for (输出从begin到end的字符)
if (不是第一个单词,则输出空格)
}
}
else确定begin和end的值
{
begin = i;
if (sign == 0)end = i;
sign = 1;
}
}
if (input[0] != ' ')第一个单词要单独输出,如果第一个单词前有空格,则输出已经在上面完成了
输出第一个单词
}
2.3.2 代码截图
2.3.3 请说明和超星视频做法区别,各自优缺点
超星视频做法:逆向遍历数组,每个单词计算其长度,读到空格时输出该单词,并清除单词长度len的值为0,循环下去直到读到第一个字符,如果第一个字符前无空格,则需加判断条件单独输出。
在看完超星视频的做法后,感觉自己的做法很麻烦,而且思路较为混乱、繁琐,而超星视频里还有很多值得学习的做法,如输出控制输出个数,逆向遍历数组的方法,对第一个单词单独处理的办法。