这个作业属于哪个班级 | C语言--网络2012 |
---|---|
这个作业的地址 | C博客作业05--指针 |
这个作业的目标 | 学习指针相关内容 |
姓名 | 朱芳芳 |
指针目录
0.展示PTA总分(0----2)
1.本章学习总结(3分)
整理指针主要知识点,必须包含内容有:
1.1 指针定义、指针相关运算、指针做函数参数。
指针变量的定义
类型名 * 指针变量名
执政变量所指向的变量类型 指针声明符
int *ptr;
p是整型指针,指向整型变量
char *cPtr;
cp 是字符型指针,指向字符型变量
指针的基本运算
指针的值,是某个变量的地址。
int *p,a=3;
p=&a; & 取地址运算符
把a的地址赋给p,即p指向a,指针变量的类型为所指向的变量类型
- 间接访问运算符,访问指针所指向的变量
printf("%d",*p);输出p所指向的变量a的变量的值
指针取地址运算和间接访问运算
p=&a; 指针的赋值
p=10; 指针的内容的访问运算
p直接对内存单元操作,改变变量数据;
tips:
1)
&表示取地址,表示取内容
2)
&p与&a相同,是地址,&a与a相同,是变量
3)
(p)++ 等价于a++ 将p所指向的变量值加1
p++?等价于(p++),先取*p,然后再自加,此时p不再指向a
指针变量的初始化
1)
指针变量先定义,赋值必须是地址
int a;
int *p1;
p1=&a;
2)
在定义指针变量时,可以同时对它赋初值
int a;
int *p1=&a; *p1=&a是错误的!!
int *p2=p1;
3)
不能用数值作为指针变量的初值,但可以将一个指针变量初始化为一个空指针
int p=10000是错误的!!!
p=0;
p=NULL;
p=(int)1732; (int*)是将数字强转为指针类型
指针的赋值运算
int a=3,p1,p2;
printf("%d
",*p1);?
p1=&a;
p2=p1;
相同类型的指针才能互相赋值,没有指向的指针是危险的,会出现所谓的段错误!
指针作为函数参数
形参:指针变量 int *p
实参:&a,某个指针
eg:指针作为函数参数模拟角色互换
#include<stdio.h>
void swap(int *px,int *py);
int main(void)
{
int a = 1, b = 2;
int* pa = &a, * pb = &b;
swap(pa, pb);
printf("%d,%d",a,b);
return 0;
}
void swap(int* px, int* py)
{
int t;
t = *px;
*px = *py;
*py = t;
}
要通过函数调用来改变主函数中某一个变量的值:
1)主调函数中,将该变量的地址或者指向该变量的指针作为实参(实参是数组名)
2)被调函数中,用指针类型形参接受该变量的地址(形参是指针变量,可以写成数组形式)
3)在被调函数中,改变形参所指向变量的值
3)数值型数组传数组地址,数组长度
传地址的优点?
数据量少,直接对地址操作,效果更好,可以改变多个变量的值,比return返回更实用
#include<stdio.h>
void monthDay(int year, int yearday, int* monthPtr, int* dayPtr);
int main(void)//年,天数,月份指针,天数指针
{
int day=0, month=0, year=0, yearday=0;//初始化日期为零
scanf("%d %d",&year,&yearday);//输入年和天数
monthDay(year, yearday, &month, &day);//转换为月份日期的函数调用,返回为地址
printf("%d-%d-%d",year,month,day);
return 0;
}
void monthDay(int year, int yearday, int* monthPtr, int* dayPtr)
{
int k, leap;//k为具体天数
int tab[2][13] =//初始化二维数组
{
{0,31,28,31,30,31,30,31,31,30,31,30,31},{0,31,29,31,30,31,30,31,31,30,31,30,31},
};
/*建立闰年判别条件*/
leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
for (k = 1; yearday > tab[leap][k]; k++)
{
yearday = yearday - tab[leap][k];
*monthPtr = k;
*dayPtr = yearday;//逐渐减去每一个月的时间
}
}
数组和地址间的关系
int a[100];
数组名代表一个地址,他的值是数组首元素的地址,(基地址)a+i是数组a的及地址的第i个偏移量
指针+1,实际下移一个数据类型存储单元
注意:
移动的写法,a++!=*a++
注意运算符号的优先级
指针和数组的关系
任何由数组下标来实现的操作都能用指针来完成
例如:
&a[i] == a+i;
p+i == &p[i];
a[i] == *(a+i);
*(p+i) == p[i];
移动指针:p+1
地址加法:移动下一个数据单元
用指针完成对数组的操作
int a[100],*p;
p=a;
int sum=0;
for(p=a;p<a+n;p++)
{
sum=sum+*p;
}
指针做循环变量,务必了解初始和结束地址
遍历数组的方法:
下标法
for(int i=0;i<n;i++)
{
a[i];
}
指针法
for(p=a;p<a+n;p++)
{
*p;
}
使用指针计算数组元素个数和数组元素的存储单元数
p,q;
p=&a[0];
q=p+1;
printf("%d
",q-p);//指针p和 q之间元素的个数
printf("%
",(int)q-(int)p);//指针p和q之间的字节数,(int)q代表地址值
指针的算数运算和比较运算
double p,q;
q-p:两个相同类型的指针相减,表示他们之间的存储单元的数目
p+1/q-1:至此昂下一个存储单元,指向上一个存储单元
p<q:两个相同类型的指针关系可以用关系运算符比较大小
1.2 字符指针
包括指针如何指向字符串、字符串相关函数及函数代码原型的理解、字符串相关函数用法(扩展课堂未介绍内容)
字符数组和字符指针的重要区别
如果要改变数组所代表的字符串,只能改变数组元素的内容
如果要改变指针所代表的字符串,通常直接改变指针的值,让他直接指向新的字符串
char sa[]="this is a string";
const charsp="this is a string";
charsp="this is a string";错误❌
sp="hello";错误❌
sa="hello";错误❌
数组名是常量,不能对它赋值
sp是指针,只能指向,不能直接赋值
**关于const **
const是constant的缩写,意思是恒定不变的!
const定义的是变量,但又相当于常量
不允许给它重新赋值,即使是相同的值也不可以
只读变量,必须在定义的时候就给他赋初值
const char*sp="this is a string";错误❌
sp='a';
char sa[] ="this is a string";正确✔
charsp=sa;
字符串的输出
char sa[]="array";
const char*sp="point";
printf("%s",sa); array 数组
printf("%s",sp); point 指针
printf("%s","string"); string字符串
printf("%s",sa+2); ray 对数组进行操作
printf("%s",sp+3); int对指针进行操作
printf("%s","string"+1); tring 对字符串进行操作
即 数组名sa,指针sp和字符串string 的值都是地址
printf("",地址);
//%s:输出从指定地址开始,‘ ’结束的字符串
字符指针,先赋值,后引用
定义字符指针后,没有对他赋值,指针的值不确定,
chars;
scanf("%s",s);错误❌ 不要引用未赋值的指针
chars ,str[20];正确 ✔
s=str;
scanf("%s",s);
定义指针是,现将他的初值置为空
char*s=NULL;
1)指针如何指向字符串
C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "http://c.china.net";
int len = strlen(str), i;
//直接输出字符串
printf("%s
", str);//使用%s
//每次输出一个字符
for(i=0; i<len; i++){
printf("%c", str[i]);//使用%c
}
return 0;
}
字符数组当然是数组,利用指针对字符数组进行操作。
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "http://c.biancheng.net";
char *pstr = str;
int len = strlen(str), i;
//使用*(pstr+i)
for(i=0; i<len; i++){
printf("%c", *(pstr+i));
}
printf("
");
//使用pstr[i]
for(i=0; i<len; i++){
printf("%c", pstr[i]);
}
printf("
");
//使用*(str+i)
for(i=0; i<len; i++){
printf("%c", *(str+i));
}
printf("
");
return 0;
}
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正确
str[3] = 'P'; //错误
return 0;
}
这段代码能够正常编译和链接,但是在运行时会出现段错误(Segment Fault)或者写入错误。
第四行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符
课外拓展
** 字符数组和使用一个指针指向字符串是非常相似。区别是什么?
字符数组和指针字符串的区别是内存中的存储区域不一样,字符数组存储在全局数据区或栈区,指针字符串存储在常量区。
全局区和栈区的字符串(包括其它数据)有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。
内存权限不同导致一个明显的结果就是,字符数组在定义后可以读取和修改每个字符,而对于指针字符串来说,一旦被定义
后就只能读取而不能修改,任何对它的赋值都是错误的。
2)字符串相关函数及函数代码原型的理解
1.字符串的输入输出
输入: scanf()/可以接受空格/或者 fgets()/不能接受空格并且以' '结束/
输出:printf()或者 puts()
stdio.h
2.字符串的复制,连接,比较,求字符串长度
1、求长度
函数:strlen()
格式:strlen(字符串/字符串变量)
函数原型:
//求字符串长度函数strlen
#include <stdio.h>
int str_strlen(char *Str)
{
int l=0;
while(*Str!=' ')
{
*Str++;
l++;
}
return l;
}
注意:计算字符串的有效长度,不包括结束标志' ';
2、复制
函数:strcpy()
strcpy(字符串1(目标),字符串2(源))--将字符串2的内容复制给字符串1
注意:字符数组1的长度要能容纳字符串1+2的内容,
函数原型:
拷贝函数strcpy
#include <stdio.h>
char *str_strcpy(char *strDest,char *strSour)
{
while(*strSour!=' ')
{
*strDest=*strSour;
*strDest++;
*strSour++;
}
*strDest=' ';//直到最后一个给予结束标志符
return strDest;
}
注意:使用strDest,strSrc这样增强可读性的名字
对于传入参数的strDest,strSrc
进行检查,禁止空指针传入
使用const 来约束strSrc,提高程序的健壮性,如果函数体内的语
句试图改动strSrc的内容。编译器将指出错误
3、比较
函数:strcmp()
格式:strcmp(字符串1,字符串2)
若字符串1与字符串2相等返回--0 字符串1大于2 返回--1 小于返回--- -1
注意:strcmp函数实际上是对字符的ASCII码进行比较 , 其中str1和str2可以是字符串常量或者字符串变量,返回值为整形,所以区分大小写
函数原型:
//字符串比较函数strcmp,。
#include <stdio.h>
int str_strcmp(char *str1,char *str2)
{
while((*str1==*str2)&&(*str1!=' '))
{
*str1++;
*str2++;
}
if(*str1==' '&&*str2==' ')//结束位置相同,说明长度一样长,否则不一样长
return 1;
else
return -1;
}
4、合并
函数:strcat()
格式:strcat(字符串1,字符串2)--将字符串2的内容合并到字符串1
注意:字符数组1的长度要能容纳1+2的内容,且 str1=str1+str2是非法的·!❌
函数原型:
char *str_strcat(char *strDest,const char *strSour)
{
while(*strDest!=' ')//先将指针指向结尾的位置
strDest++;//移动到字符串末尾
while(*strSour!=' ')
{
*strDest=*strSour;//从字符串2的起始位置开始赋值到字符串1的结尾位置,再分别移动
strDest++;
strSour++;
}
*strDest=' ';//给予结束标志
return strDest;
}
或
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=' ';
return tempstr;//连接后字符串的首地址
//函数类型为指针,则返回地址,首地址不要改变
注意:将字符串str2连接在str1后,str1最后的结束字符NULL会被覆盖掉,并且连接后的字符串的尾部
会再增加一个NULL.注意:str1和str2所指的内存空间不能重叠,且str1要有足够的空间来容纳要复制
的字符串。返回是str1字符串的首地址。
字符串复制和合并函数存在的问题?
源字符串要足够大,否则会溢出导致系统崩溃
while((strDest++=strSrc++)!=' ');
如何解决?
strncpy函数
charstrncpy(chardest,const char*stc ,size_t n)
把src所指向的字符串复制到dest,最多复制n个字符,当长度小于n时,剩余部分将用控字节填充
strncat()函数
char*strncat(char*dest,const char*stc ,size_t n)
把src所指向的字符串追加到dest所指向的字符串的结尾,追加最多n个字符
字符串比较函数用来比较字符串的大小
if(str1>str2){}比较字符串首元素的地址 错误❌
if (strcmp(str1,str2)>0) 比较字符串内容 {}正确✔
3)字符串相关函数用法
1.3 指针做函数返回值
指针作为函数的返回值,函数的返回值类型需要定义为指针类型
一般定义格式为
数据类型*函数名称(形式参数列表)
注意事项:
一定要保证返回的指针是有效指针,一个常犯的错误是,返回局部变量的指针
1.4 动态内存分配
为什么要动态内存分配
堆区申请的空间,想要多少就申请多少
数组要指定数组长度,空间浪费
栈区空间有限
一般情况下,运行中的很多存储要求在写程序时无法确定,故需要动态内存分配
使用动态内存分配能有效使用内存
全局变量,静态局部变量,自动变量由编译器系统分配
课外拓展 堆区和栈区区别
一、区别
注:首先堆和栈可以分为两种,一种是数据结构,另一种是和内存的分配有关,这两种虽然都有栈和堆,但是两者关系并不大,
1、栈、堆是数据结构里面的叫法,注意:有时候有人喜欢这样说 "堆栈" 其实说的就是栈而不是堆。
2、堆区、栈区则是内存模型的叫法。
二、内存中的栈区和堆区
我们知道底层是C
而C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:
1、栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,
其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量
有限,局部变量申请大小是有限制的,由栈的剩余空间决定,栈的大小为2m,也有的说是1m,总之,是一个
编译时就会确定的常数,如果申请的空间超过了剩余空间,将会出现问题。
2、堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序
区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。
3、静态区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始
化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
4、常量区:常量存储在这里,不允许修改。eg:const
5、代码区:顾名思义,存放代码。
动态内存分配相关函数及用法
使用时申请:malloc,calloc
用完就释放:free
void*calloc (unsigned n,unsigned size)
在内存的动态存储区中分配连续n个连续空间,每一存储空间的长度为size,并且分配后还把存储块里全部初始化为0
若申请成功则返回地址,若不成功,则返回NULL;
void free (void *ptr)
释放有动态存储分配函数申请到的整块内存空间,ptr为指向要释放空间的首地址。
free(p);
void malloc(unsigned size)
在内存的动态存储区中分配连续的空间,其长度为size
eg:
p=(int )malloc(nsizeof(int))
若申请成功,则返回一个指向所分配内存空间的其实地址的指针,不成功则返回NULL;
malloc对所分配的存储区域不做任何事情
和free结合使用
举例为多个字符串做动态内存要如何分配
1.5 指针数组及其应用
多个字符串用二维数组表示和用指针数组表示区别?
1.6 二级指针
1.7 行指针、列指针
行指针:二级指针
1.形如 int(*p)[n]
指向有n个元素的一维数组
p+i=a+i;
(p+i)=(a+i)=a[i]
2.二维数组与指针
int a[3][4];
int (p)[4];
p=a;
(p)[0]=a[0][0];
(*(p+1))[0]=a[1][0];
((p+i)+j)=a[i][j];
列指针:一级指针
int a[3][4];
int * p;
p=a;
- (p+1)=a[0][1];
//移向下一个元素
注意区分行指针与列指针
行指针:p首先指向第0行,然后p+i定位到第i行,然后p+i进行解引用(*(p+i))把行地址转化为列地址,在得到第i行第0列地址后在加j得到第i行第j列地址,在进行解引用得到a[i][j]
列指针:p直接指向了第0行第0列,找出a[i][j]相对于a[0][0]的偏移量,i * n+j
2.PTA实验作业(7分)
2.1 7-3 字符串的冒泡排序 (2分)
我们已经知道了将N个整数按从小到大排序的冒泡排序法。本题要求将此方法用于字符串序列,并对任意给定的K(<N),输出扫描完第K遍后的中间结果序列。
输入格式:
输入在第1行中给出N和K(1≤K<N≤100),此后N行,每行包含一个长度不超过10的、仅由小写英文字母组成的非空字符串。
输出格式:
输出冒泡排序法扫描完第K遍后的中间结果序列,每行包含一个字符串。
2.1.1 伪代码
数据表达:
字符串存储:使用二维字符数组
输入二维数组:
外层循环for:扫描k遍
{
内层循环for:冒泡n-i-1次
{
if(后一个字符串的长度比前一个字符串的长度要长)
{
交换两个字符串的位置;
}
}end for;
}end for:
2.1.2 代码截图
2.1.3
//main.c
//author
//连续输入是个字符,以回车结束
#include "stdafx.h"
//输入10个数字
#define N 10
char min(char a, char b);
char max(char a, char b);
int main()
{
//int a[N] = { 10,9,8,7,6,5,4,3,2,1 };
int flag = N;
//指针的方法
char a[N] = { 0 };
char *p = a;
for (int i = 0; i < N; i++)
{
scanf_s("%c", p);
p++;
}
p = &a[0];
for (int i = 0; i < N; i++)
{
printf("输入的数为%c
", *p++);
}
p = a;
while (1)
{
for (int i = 0; i < flag - 1; i++)
{
char tmp1 = *(p + i);
char tmp2 = *(p + i + 1);
*(p + i) = min(tmp1, tmp2);
*(p + i + 1) = max(tmp1, tmp2);
}
if (flag == 2)break;
flag--;
}
p = a;
for (int i = 0; i < N; i++)
{
printf("%c ", *(p + i));
}
2.2 6-9 合并两个有序数组(2分)
选择合并2个有序数组这题介绍做法。
要求实现一个函数merge,将元素个数为m的升序数组a和长度为n的升序数组b合并到数组a,合并后的数组仍然按升序排列。假设数组a的长度足够大。
2.2.1 伪代码
合并两个有序数组有三种做法:
1:创建一个新数组c,把有序数组a和有序数组b中的元素放到新创建的数组c中,然后利用冒泡排序把数组c中的元素进行有序排序。
2:创建一个新的数组c,此数组的大小大于或等于已知两个数组之和。通过比较两个数组中的元素,谁小就把谁放到空数组中,知道其中一个数组为空,最后把剩下的数组全部放到新创建的始组中。
3:有两个有序数组a和b,其中数组a的末尾有足够的空间容纳数组b,将数组b容纳到数组a中
此题为第三种做法
考虑到a数组很大,可以直接在a数组上进行合并,但是要讲究效率。如果单纯从前往后合并,那么效率会非常低,因为a数组后面的数字需要不停的移动。换一种思路,采用从后往前合并,首先计算出总长度,设置一个指针从a数组最后往前移动
具体伪代码实现如下:
w计算合并后a数组的长度;
hile(b数组的长度递减大于零)
{
每一轮都要判断a的长度是否满足大于零并且递减的指针内容位置a和b谁比较大?a大就将a的该位置的值传入新数组:否则就是b;
}
void merge(int* a, int m, int* b, int n)
{/* 合并a和b到a */
int size = m-- + --n;
while (n >= 0) //b数组的长度递减大于零
{
*(a+(size--)) = m >= 0 && *(a+m) >*(b+n) ? *(a+(m--)) :*(b+(n--));//每一轮都要判断a的长度
}
}
2.2.2 代码截图
2.2.3
2.3 7-4 说反话-加强版(3分)
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int i;
int j = 0;
for (i = m; i < m + n; i++) {
nums1[i] = nums2[j];
j++;
}
for (i = 0; i < m + n - 1; i++) {
for (j = 0; j < m + n - 1 - i; j++) {
if (nums1[j] > nums1[j + 1]) {
int tmp = nums1[j];
nums1[j] = nums1[j + 1];
nums1[j + 1] = tmp;
}
}
}
}
本题做法使用数组下标或者使用指针都可,亮点是使用了三目运算符和自增自减运算符,以及使用数组重构,使代码更加简洁
给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。
输入格式:
测试输入包含一个测试用例,在一行内给出总长度不超过500 000的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用若干个空格分开。
输出格式:
每个测试用例的输出占一行,输出倒序后的句子,并且保证单词间只有1个空格。
2.3.1 伪代码
字符串存储(使用一维字符数组;
使用初始位置指针和结束位置指针来控制逆序;
while(没有到结束标志)
{
将结束位置指针指向字符数组末尾
}end while;
while(结束位置指针不等于初始位置指针)
{
逆向遍历字符数组;
利用指针所指向的内容是否为空格来判断单词;
输出单词,使用flag来控制输出格式;
}
2.3.2 代码截图
2.3.3 请说明和超星视频做法区别,各自优缺点。
借鉴超星的做法,
超星优点:
定义指针指向字符串,更能动态了解当前字符位置
while(endPtr&&endPtr!='
')
{
endPtr++;
}
逆向扫描字符串
while(p!=beginPtr)
{
p--;
}
怎么找字符串单词,即当前字符的前一个字符为空格
if(p!=' '&&(p-1)==' ')
{}