13.0.0 指针
13.1.0 指针的使用
13.1.1 变量的值域和变量的地址
复习一下变量在内存中的存储:
不同类型的变量在内存中占据不同的字节空间
int 4
double 8
float 4
char 1
内存中存储数据的最小基本单位是字节
每一个字节都有一个内存地址,这个地址是一个十六进制的数
声明一个变量,在内存中是从高字节向低字节分配连续的指定字节数的空间
任何数据在内存中都是以其二进制的补码形式存储的,低位存储在低字节,高位存储在高字节
变量的值:存储在变量中的数据,叫做变量的值
变量的地址:变量是由一个或者多个字节组成的,组成这个变量的低字节的地址就是这个变量的地址
如何取出变量的值:
直接写上变量的名字就可以取到变量中的值
如何取出变量的地址:
使用&运算符,&变量名;这个表达式的结果就是这个变量的地址
%p 格式控制符输出变量的地址
什么是指针?
变量的地址就是指针。
13.1.2 指针变量
指针变量就是专门用来存储地址的变量,专门用来存储另外一个变量的地址。
我们可以这么说,这个指针变量指向了那另一个变量。
使用指针变量的好处是什么?
可以通过指针变量找到这个指针变量指向的变量,通过指针变量就可以间接的访问这个指向的变量。
如何声明一个指针变量?
语法格式:数据类型* 指针变量的名称;
例如:int* p1;
这代表声明了1个指针变量,这个指针变量的名字是p1,这个指针变量的类型是int*,读作“int指针”。
这个*代表这个变量不是一个普通变量,而是一个用来专门存储地址的指针变量,这个p1指针变量中只能存储地址。
指针的类型:有哪些普通的类型就可以有哪些指针的类型。
的位置:可以和数据类型在一起,也可以和指针变量名放在一起,也可以单独写在中间。 int p1;
int *p1;
int * p1;
以上形式都是允许的。
指针变量是用来存储另外一个变量的地址,但是1个指针变量并不是可以存储任意类型的变量的地址,而是有规定的。
一个指针变量只能存储和这个指针类型相同的普通变量的地址。
如果不遵守这个规定,则会出现一些意想不到的问题。
13.1.3 指针变量的初始化
因为指针变量是用来存储另外一个变量的地址的,所以我们不能直接赋值1个非地址类型的常量数据给指针变量,也不可以直接赋值1个变量给指针变量。并且指针可以存储的另外一个变量的地址这个变量的类型是固定的。
所以:
正确的初始化步骤是:
1.先取出变量的地址:
使用取地址运算符& 就可以取出变量的的地址,要打印地址使用格式控制符%p
2.将取出来的变量的地址赋值给指针变量
int num =10;
int* p1 = #
这个p1指针变量的值就是num变量的地址,我们就说p1指针指向了num变量。
注意:
指针变量只能存储和指针变量类型相同的普通变量的地址,否则就会出问题
如果直接写变量名,操作的就是这个变量;如果加上&变量名;操作的就是这个变量的地址
指针变量在内存中也有一个地址,所以在指针变量的前面加上&也可以取出指针变量的地址。例如int* p1; &p1得到的就是p1这个指针的地址。
注意如果想要打印一个指针变量的值,应该使用%p占位符。
例如:printf(“%p\n”,p1); 这打印的是p1的值也就是p1指向的普通变量的地址
而printf(“%p\n”,&p1); 这打印的是p1这个指针变量自己的地址。
13.1.4 指针变量的使用
声明指针变量的好处就是可以间接的操作指针指向的变量。
如何使用指针间接操作指针指向的变量:
语法格式:*指针变量名; 代表这个指针指向的变量
例如:int num = 10;
int* p1 = #
p1 = 100; 这个时候p1完全等同于变量num,num被重新赋值为100。
这个时候我们就可以通过这种方式间接的为指针指向的变量赋值或者取值。
注意:
因为*指针变量名完全等同于这个指针指向的变量,所以通过这种方式为指针指向的变量赋值的时候,数据类型如果不同,会做自动类型转换。
13.1.5 使用指针变量的注意事项
指针变量也可以批量声明
int p1,p2,*p3;
注意不能写成int *p1,p2,p3; 这样的话p2和p3不是指针变量,会变成int类型的普通变量。
野指针
我们声明一个指针变量的时候,如果没有为其初始化,这个时候这个指针变量中是有值的,这个值是一个垃圾值,是一个随机地址,所指向的地址是不固定的。这个时候这种指针变量被称为野指针。
13.1.6 NULL值
我们声明一个指针变量,如果不为其初始化,那么这个指针就是一个野指针。在这个时候,如果通过这个指针取访问其指向的地址空间的时候,是相当危险的。可能会造成程序的崩溃。
所以,建议在声明一个指针变量以后,最好马上对其进行初始化,如果没有变量的地址可以初始化这个指针变量,那么就将其初始化为NULL值。
NULL值代表指针变量不指向内存中的任何地址。NULL和0可以完全等价,所以也可以将0赋值给指针变量。
如果一个指针变量的值是NULL,这个时候通过指针变量取访问指向的变量的时候100%报错。
13.1.7 多个指针指向同一个变量
int num = 10;
int* p1 = #
int* p2 = p1;
p1 = 100; 那么这个时候p2的值也是100。
p1和p2指针都指向了num变量,这个时候无论是通过p1还是p2都是在访问num变量。
这种形式叫做多个指针指向同一个变量。
13.2.0 指针的应用
13.2.1 指针作为函数的参数
指针也可以作为函数的参数。操作时可以直接将指针变量声明在函数的小括号内即可。
例如:void test(int* p1);
当我们调用一个函数的时候,如果函数的参数时一个指针变量,那么我们就必须要为这个指针传递一个和指针类型相同的普通变量的地址。
我们知道,一般普通变量作为形参的函数,在函数操作结束后,如果不返回结果值的话,函数内部的形参变量会被系统收回,不会改变实参的值。
但是,对于指针变量作为参数的函数,在函数的内部去访问参数指针指向的变量的时候,其实访问的就是实参变量。
所以这样做就可以做到在函数的内部修改实参变量的值。
那么什么时候要使用指针变量作为函数的参数呢?
我们知道,在C语言中,一个函数只能返回一个值,不能同时返回多个数据。
那么我们怎么做可以做到返回多个值呢?这个时候可以使用指针变量。
使用指针作为函数的参数,让调用者将自己的变量的地址传递到函数的内部,函数的内部通过指针就可以修改实参变量的值,达到返回多个值的效果。
例如:
通过写一个函数,得到一个数组的最大值和最小值。
void getMaxandMin(int arr[],int len,int* pMax,int* pMin)
{
int max = INT32_MIN;
int min = INT32_MAX;
for(int i = 0;i < 0;i++) { if(arr[i] > max)
{
max = arr[i];
}
if(arr[i] < min)
{
min = arr[i];
}
}
*pMax = max;
*pMin = min;
}
int main()
{
int arr[] = {20,56,23,41,14,74,89,100,120,235,41};
int max = 0;
int min = 0;
int len = sizeof(arr)/sizeof(arr[0]);
getMaxandMin(arr,len,&max,&min);
printf(“Max = %d\n Min = %d\n”,max,min);
}
这样做就可以做到同时将最大值和最小值返回给main函数中的普通变量。
13.2.2 指针为什么要分类型
指针变量也是一个变量,既然是变量那么肯定就在内存中占有字节数。
那么指针变量在内存中占据多少个字节呢?
通过sizeof函数计算,会发现无论指针是什么类型,在内存中都是占据8个字节。
既然指针都是占据8个字节,那么为什么指针还要分类型呢?
举个例子:
int num = 10;
int* p1 = #
p1指针变量存储的是num变量的地址,也就是num变量的低字节的地址。
通过这个p1指针其实只能找到这个地址的字节。
这个时候,通过p1指针找到这个字节,但是操作的时候指针能够操作多少个字节的空间呢?
操作多少个字节空间是根据指针的类型来决定的。
所以指针变量的类型,决定了通过这个指针找到地址的字节后,连续操作多少个字节空间。
int* 就连续操作4个字节
double* 就连续操作8个字节
float* 就连续操作4个字节
char* 就连续操作1个字节
如果定义了一个int类型的普通变量,而指向它的指针变量是char类型的话,那么指针变量所能操作的字节数只有1个字节,超出的部分无法正确使用,最后导致得到的普通变量的值就会发生意想不到的奇怪结果。
所以指针的类型如果不和指向的变量的类型相同的话,那么通过指针就无法正确的操作指向的变量。
13.2.3 多级指针
一级指针:一个指针变量存储的是一个普通变量的地址,这样的指针就叫做一级指针。
二级指针:一个指针变量存储的是一个一级指针变量的地址,这样的指针就叫做二级指针。
三级指针:一个指针变量存储的是一个二级指针变量的地址,这样的指针就叫做三级指针。
以此类推,还可以有四级指针等……
如何声明多级指针呢?
声明一级指针的语法是:数据类型* 指针名;
声明二级指针的语法是:数据类型** 指针名;
声明三级指针的语法是:数据类型*** 指针名;
以此类推……
注意:
一级指针只能存储普通变量的地址;
二级指针只能存储一级指针变量的地址;
三级指针只能存储二级指针变量的地址;
n级指针只能存储n-1级指针变量的地址。
否则就会出现问题。
初始化:
使用取地址符&,拿到对应的指针变量的地址,赋值给指针变量就可以了。
例子:
int num = 10;
int* p1 = #
int** p2 = &p1;
int*** p3 = &p2;
13.2.4 二级指针的使用
使用指针的时候,在指针变量名前面有几个*就代表其指向的几级变量。
例如:
*指针变量名 代表这个指针指向的变量
**指针变量名 这个指针至少要是一个二级指针,代表这个指针指向的指针指向的那个变量
也就是说:如果有这样几个多级指针:
int num = 10;
int* p1 = #
int** p2 = &p1;
int*** p3 = &p2;
在这里,
*p1 代表普通变量num
*p2 代表二级指针p2指向的一级指针p1
**p2 代表二级指针p2指向的一级指针p1指向的普通变量num
*p3 代表三级指针p3指向的二级指针p2
**p3 代表三级指针p3指向的二级指针p2指向的一级指针p1
***p3 代表三级指针p3指向的二级指针p2指向的一级指针p1指向的普通变量num
13.2.5 指针与整数之间的加减法
指针也可以和整数之间进行加减运算。
例如:一个指针变量 + 1;
结果并不是在指针地址的基础上加一个字节的地址,而是在这个指针地址的基础上加一个单位变量占用的字节数。
如果指针类型是int*,那么这个时候+1代表加4个字节地址;
如果指针类型是double*,那么这个时候+1代表加8个字节地址;
如果指针类型是float*,那么这个时候+1代表加4个字节地址;
如果指针类型是char*,那么这个时候+1代表加1个字节地址;
13.3.0 指针与数组
13.3.1 定义指针指向数组的元素
我们可以声明一个指针变量指向数组的元素,通过指针间接的操作数组的元素。
int arr[] = {10,20,30,40,50,60,70};
int* p1 = &arr[0]; 表示p1指针指向了数组中的第0个元素
数组的第0个元素的地址就是数组的地址,数组名就代表数组的地址,所以也可以这样定义:
int* p1 = arr;
这两句定义是完全相同的。
于是我们可以使用指针来遍历数组:
由于上一节我们说过一个指针变量+1可以得到这个指针变量的地址加一个单位变量占用的字节数。所以我们可以利用这个原理来遍历一个数组。
例如:
int arr[] = {10,20,30,40,50,60,70};
int* p1 = arr;
for(int i = 0;i < 7;i++)
{
printf(“%d\n”,(p1+i)); } 还可以这样写: int arr[] = {10,20,30,40,50,60,70}; for(int i = 0;i < 7;i++) { printf(“%d\n”,(arr+i));
}
还可以这样写:
int arr[] = {10,20,30,40,50,60,70};
int* p1 = arr;
for(int i = 0;i < 7;i++)
{
printf(“%d\n”,*(p1++));
}
但是这种方法需要注意:每次循环完了以后p1的值都会发生变化,当循环结束之后,p1的值会导致这个指针已经指向了数组之外的地址了。所以循环结束后p1已经不在指向数组中的任何元素了。
最后请注意:数组名代表数组的地址,而数组一旦创建,数组的地址就确定了,所以我们不能为数组名赋值,也不能修改数组名的值,但是可以利用数组名的值。
13.3.2 数组作为函数的参数的本质
当数组作为函数的参数的时候,它并不是去创建一个数组,而是创建了1个用来存储这个数组地址的指针变量。
其实如果我们为一个函数写了一个数组作为参数,在系统编译的时候,会自动将其转换成指针。所以,在声明参数的时候不是创建数组,而是创建1个存储数组地址的指针变量。所以这就是为什么在函数内部用sizeof计算数组的长度永远都是8。
从现在开始,如果函数带了1个参数,这个参数是一个数组,那么建议不用再写数组,而是写一个指针指向数组的第0个元素即可。
void test(int *arr,int len);
13.3.3 索引的本质
指针变量后面可以使用中括号,在中括号中写上下标来访问数据。
p1[n]; 前提是p1是1个指针变量
这个时候p1[n]完全等价于*(p1+n)
只要是指针都可以使用中括号下标,就相当于是去访问指针指向的变量。
因此,类似的在一个数组中:
int arr[5] = {0};
arr = 10; //这一句就相当于arr[0] = 10; arr[1] = 20; //这一句就相当于(arr+1) = 20;
arr[2] = 30; //这一句就相当于*(arr+2) = 30;
由此可以看出,操作数组我们虽然使用的中括号下标来操作,但实际上内部仍然使用的是指针来操作。
13.3.4 存储指针的数组
如果一个数组是用来存储指针类型的数据的话,那么这个数组就叫做存储指针的数组。
如何声明1个数组来存储多个指针数据呢?
语法格式:元素类型* 数组名[数组长度];
int* arr[3];
所以如果有多个指针数据,就可以将这多个指针数据存储到1个数组中。
例子:
int num1 = 10;
int num2 = 20;
int num3 = 30;
int* p1 = &num1;
int* p2 = &num2;
int* p3 = &num3;
int* arr[3] = {p1,p2,p3};
13.3.5 指针与指针之间的减法运算
指针与指针之间可以做减法运算,但结果是一个long类型的数据。
这个结果代表两个指针指向的变量之间相差多少个单位变量。
一般情况下,我们用来判断数组的两个元素之间相差多少个元素。
int arr[] = {10,20,30,40,50,60}; int* p1 = &arr[1]; int* p2 = &arr[5]; long res = p2 - p1;
我们可以发现res的结果是4。
当然,如果两个指针不指向同一个数组,计算出来的结果也就没有什么意义。
注意,指针与指针之间只能做减法运算。
13.3.6 指针与指针之间的比较运算
指针与指针之间可以做比较运算:
>
<
=
<=
==
!=
都可以作用于两个指针之间。
= < <= 可以判断两个指针指向的变量的地址,谁在高字节,谁在低字节。
== != 可以判断两个指针指向的变量是不是同一个变量。
13.4.0 指针与字符串
13.4.1 字符串的第二种存储方式
字符串数据在C语言中的存储有两种方式:
使用字符数组来存储
将字符串数据的每一个字符,存储到字符数组中,并追加1个’\0’代表存储结束。
char name[] = {‘j’,’a’,’c’,’k’,’\0’};
char name[] = {“jack”};
char name[] = “jack”;
使用字符指针来存储字符串数据
直接将1个字符串数据,初始化给1个字符指针。
char* name = “jack”;
虽然我们不知道这个的原理,但是有一点可以肯定。
这绝对不是将“jack”存储到指针变量name中,因为name是一个指针变量,只能用来存储地址。
13.4.2 内存中的五大区域
内存中分为五大区域:
栈
堆
BSS段
数据段/常量区
代码段
为什么要分五大区域,每个区域的作用是什么?
不同的数据存储在不同的区域,方便系统的管理。
栈 是专门用来存储局部变量的。
堆 允许程序员手动的从堆申请空间来使用
BSS段 是用来存储未初始化的全局变量和静态变量的。
数据段/常量区 是用来存储已经初始化的全局变量、静态变量,还有常量数据
代码段 是用来存储程序的代码/指令
13.4.3 存储字符串的两种方式的区别
存储字符串的两种方式分别是:
使用字符数组来存储
使用字符指针来存储
它们之间的区别:
当它们都是局部变量的时候:
char name1[]=“jack”;
char* name2=“rose”;
name1字符数组,是申请在栈区,字符串的每一个字符存储在这个字符数组的每一个元素中
name2指针变量,是申请在栈区的,而这个时候,字符串数据是存储在常量区中的
name2指针变量中,存储的是“rose”这个字符串在常量区的地址。
当它们都是全局变量的时候:
char name1[]=“jack”;
char* name2=“rose”;
name1字符数组,是申请在常量区的,字符串的每一个字符存储在这个字符数组的每一个元素中
name2指针变量,也是申请在常量区的,字符串数据也是以字符数组的形式存储在常量区
name2指针变量中,存储的是“rose”这个字符串在常量区的地址。
综上所述,这两种方式在内存中的存储的结构是不同的。
以字符数组存储:无论如何是一个字符数组,然后字符串的每一个字符存储在数组的元素中
以字符指针存储:无论如何首相有一个字符指针变量,字符串数据是以字符数组的形式存储在常量区的
另外,
以字符数组存储的字符串数据,可以修改字符数组的元素
以字符数组的形式存储字符串数据,不管是全局的还是局部的,都可以使用下标来修改字符数组的每一个元素
以字符指针的形式存储的字符串数据,这个时候字符指针指向的字符串数据是无法修改的
以字符指针的形式存储字符串数据,不管是全局的还是局部的,都不可以修改字符指针指向的字符串数据
13.4.4 字符串的恒定性
当我们以字符指针的形式存储字符串的时候,无论如何字符串数据是存储在常量区的,并且一旦存储到常量区中去后,这个字符串数据就无法更改。
当我们以字符指针的形式将字符串数据存储到常量区的时候,并不是直接将字符串存储到常量区,而是先检查常量区中是否有相同内容的字符串。
如果有,直接将这个字符串的地址拿过来返回;
如果没有,才会将这个字符串数据存储在常量区。
注意:当我们重新为字符指针初始化1个字符串的时候,并不是修改原来的字符串,而是重新的创建了1个字符串,把这个新的字符串的地址赋值给了它。
char *name = “jack”;
name = “rose”;
这样是可以的,但是这样做并不是把“jack”改成了“rose”,而是重新创建了1个“rose”,把“rose”的地址赋值给了name
char name[] = “jack”;
name = “rose”;
这样做是不行的,name是数组名代表数组的地址,不能为数组名赋值
要改成“rose”,可以这么做:
name[0] = ‘r’;
name[1] = ‘o’;
name[2] = ’s’;
name[3] = ‘e’;
name[4] = ‘\0’;
一道应用题:
有一个字符串svcnbdsenegifuiqidaisefeiginbvdaijfdiaeefewadwrefefsdfa
求这个字符串中e出现的次数。
代码如下:
int main(int argc, const char * argv[])
{
char *str = "svcnbdsenegifuiqidaisefeiginbvdaijfdiaeefewadwrefefsdfa";
int count = 0;
for (int i = 0; *(str+i) != '\0'; i++)
{
if (*(str+i) == 'e')
{
count++;
}
}
printf("count = %d\n",count);
return 0;
}
13.4.5 字符串数组
如果有多个字符串需要存储在系统中,如果用字符数组或指针都不合适,没有效率且输入繁琐,字符串长度也不容易管理。
这个时候可以引入二维字符数组或字符串数组来输入和管理多个字符串。
但是使用二维字符数组来存储多个字符串的时候每一个字符串的长度不能超过列数-1,如果超过就会发生错误,这造成了输入前必须计算好最大字符串长度的困扰。
例如:
char names[][10] =
{
“jack”,”rose”,”lily”
};
这里每个字符串的长度都不可以超过9,所以如何某一天想要更改其中一个字符串,而长度又超过了9的话,就需要同时修改这个二维数组的列数。
而一维的字符指针数组用来存储字符串就不存在这样的问题,一维字符指针数组又被称为字符串数组。
例如:
char* names[4] = {“jack”,”rose”,”lily”,”Alan”};
这其实是一个一维数组,每个元素的类型是char指针
这种方法的优点是:每一个字符串的长度没有限制。
13.4.6 字符串数组的排序
字符串数组的排序通常都是根据字符串首字母的顺序来排序。
而数组元素的排序通常都有两种方法:“选择排序”和“冒泡排序”
(详见11.8.2和11.8.3节)
例子:
将一些亚洲国家按照开头字母的顺序排列。
采用冒泡排序法,代码如下:
#include <stdio.h>
#include <string.h>
int main(int argc, const char * argv[])
{
char *countries[] =
{
"Nepal",
"Cambodia",
"Afghanistan",
"China",
"Singapore",
"Bangladesh",
"India",
"Maldives",
"South Korea",
"Bhutan",
"Japan",
"Sikkim",
"Sri Lanka",
"Burma",
"North Korea",
"Laos",
"Malaysia",
"Indonesia",
"Turkey",
"Mongolia",
"Pakistan",
"Philippines",
"Vietnam",
"Palestine"
};
int len = sizeof(countries) / 8;
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
int res = strcmp(countries[j], countries[j+1]);
if (res > 0)
{
char* temp = countries[j];
countries[j] = countries[j+1];
countries[j+1] = temp;
}
}
}
for (int i = 0; i < len; i++)
{
printf("%s\n",countries[i]);
}
return 0;
}
如果需要按照字符串长度来排序的话,采用选择排序法,代码如下:
#include <stdio.h>
#include <string.h>
int main(int argc, const char * argv[])
{
char *countries[] =
{
"Nepal",
"Cambodia",
"Afghanistan",
"China",
"Singapore",
"Bangladesh",
"India",
"Maldives",
"South Korea",
"Bhutan",
"Japan",
"Sikkim",
"Sri Lanka",
"Burma",
"North Korea",
"Laos",
"Malaysia",
"Indonesia",
"Turkey",
"Mongolia",
"Pakistan",
"Philippines",
"Vietnam",
"Palestine"
};
int len = sizeof(countries) / sizeof(countries[0]);
for (int i = 0; i < len - 1; i++)
{
for (int j = i + 1; j < len; j++)
{
unsigned long len1 = strlen(countries[i]);
unsigned long len2 = strlen(countries[j]);
if (len1 < len2)
{
char *temp = countries[i];
countries[i] = countries[j];
countries[j] = temp;
}
}
}
for (int i = 0; i < len; i++)
{
printf("%s\n",countries[i]);
}
return 0;
}
13.4.7 字符串函数补充
fputs函数
作用:将字符串数据输出到指定的流中。
流:标准输出流 -> 控制台
文件流 -> 磁盘上的文件
使用格式:
fputs(要输出的字符串,指定的流);
要使用fputs函数将字符串数据输出到标准输出流
标准输出流:控制台,stdout
将字符串存储到文件中
1)要先声明1个文件指针,指向磁盘上的文件
FILE* pFile = fopen(“/Users/alanc/Desktop/abc.txt”,”w”);
fopen函数可以创建1个指向文件的指针
fopen函数的两个参数:
第一个参数:文件的路径,代表创建的指针就指向这个文件
第二个参数:操作文件的模式,你要对这个文件采取什么操作。
“w” -> write 代表你要向这个文件写入内容
“r” -> read 代表你要从这个文件中读取数据
“a” -> apped 追加代表你向这个文件中追加数据
注意:
当操作模式是“w”的时候,如果文件不存在的时候就会创建这个文件;
如果这个文件存在,就会替换源文件。
当操作模式是“a”的时候,如果文件存在则追加内容,如果不存在就创建这个文件。
2)使用fputs函数将字符串写入到指定的文件流中
fputs(字符串,文件指针);
fputs(name,pFile);
3)写完之后,一定要记得使用fclose()函数,将这个文件关闭
fclose(pFile);
完整示例代码如下;
FILE* pFile = fopen(“/Users/alanc/Desktop/abc.txt,”w”);
char* name = “Hello,World!”;
fputs(name,pFile);
fclose(pFile);
fgets函数
作用:从指定的流中读取字符串。
这个流可以是标准输入流(控制台),也可以是文件流。
使用格式:
使用fgets函数从标准输入流(控制台)中读取数据
前面学习的scanf函数和gets函数也可以实现这个功能。
但是我们知道:
scanf函数有两个缺点,
不安全
输入的空格会被认为是结束信号
gets函数有不安全的缺点。
而fgets函数则没有这些缺点。我们说它是安全的。
语法:
fgets(要将字符串存储到的数组,字符串的长度+1,指定流);
stdin:代表标准输入流,也就是键盘流,控制台输入。
为什么我们说fgets函数是安全的?
如果我们输入的字符串的长度大于等于我们定义的数组的长度,函数只会按照我们定义的数组长度-1来接收数据,最后一个元素自动变成’\0’。这样系统就不会崩溃了。
如果我们输入的字符串的长度刚好等于我们定义的数组长度-1,那就是完美的。
如果输入的字符串长度小于我们定义的数组长度-1,那么就会将最后输入的换行字符’\n’一并接收,然后在后面添加’\0’结束符。
对于最后这种情况的解决方案是:输入完毕之后,判断字符数组中存储的字符串最后1个是不是’\n’,如果是那么就替换为’\0’
例如:
char input[5];
printf(“请输入:\n”);
fgets(input,5,stdin);
size_t len = strlen(input);
if(input[len-1] == ‘\n’)
{
input[len-1] = ‘\0’;
}
使用fgets函数从文件流中读取数据:
就是读取磁盘上文件的内容
1)先创建1个文件指针
FILE* pFile = fopen(“/Users/alanc/Desktop/abc.txt,”r”);
char content[50];
2)准备1个字符数组,准备存储读取到的字符串数据。
3)使用fgets函数从指定的文件流中读取
fgets(content,50,pFile);
4)输出数据
5)关闭文件流
fclose(pFile);
13.4.8 const关键字
const是一个关键字,是用来修饰变量的。
也就是说在声明变量的同时,可以使用const关键字来修饰。
const int num = 10;
一般情况下来说,被const修饰的变量具备一定程度上的不可变性。
const修饰基本数据类型的变量
基本数据类型:int、double、float、char
例如:const int num = 10;
这个时候num变量的值只能取值,不能修改。
还可以写成
int const num = 10;
const修饰数组
例如:const int arr[4] = {10,20,30,40};
数组的元素的值无法改变。
也可以写成
int const arr[4] = {10,20,30,40};
const修饰指针
例如:const int* p1 = #
这个时候,无法通过p1指针去修改指针指向的变量的值,但是如果直接操作变量是可以修改值的。即无法通过*p1 = 100;这样的语句修改num的值,但是可以使用num = 100;来修改num的值。
另外,这个时候指针变量的值是可以修改的,可以吧另外一个变量的地址赋值给这个指针。即p1 = &num2;这样的语句是允许的。
也可以写成
int const p1 = # 但是,如果写成int const p1 = # 这个时候就不一样了。
这个时候p1的值不能修改,不能修改p1所指向的变量的地址,但是却可以修改p1所指向的变量的值。即不可以使用p1 = &num2;这样的语句,但是可以使用*p1 = 100;这样修改num变量值的语句。
还有一种写法是:
int const * const p1 = #
这个时候既不能修改p1的值(指向的地址),也不能修改p1所指向的变量的值。
const的使用场景
const的特点:
被const修饰的变量,是只读变量,只能取值而不能改值,所以const变量的值,至始至终都不会发生变化。
所以,当某些数据是固定的,在整个程序运行期间都不会发生变化,并且你也不允许别人去修改,那么这个时候我们就可以使用const。
当函数的参数是一个指针的时候,函数的内部是有可能会修改实参变量的值的。当函数的编写者想要给调用者一个信息:“你放心大胆传递数据给我吧!我一定不会修改你的数据的。”
那么这个时候,就可以给参数加一个const。
所以,以后在调用函数的时候,如果看到了参数被const修饰,就可以放心的传递值给它,因为这样的函数只会使用我们的值,绝对修改不了我们的值。
13.5.0 内存管理
13.5.1 malloc函数
在13.4.2节中我们提到了内存中的五大区域,其中提到了“堆”是用来给程序员手动申请空间的。
这里需要注意的是:在堆中申请的字节空间,如果不主动释放,那么系统是不会自动释放的,除非程序结束。
所以在堆中申请字节空间的步骤一般都是三步:1. 申请;2. 使用;3. 释放。
那么如何在堆区申请指定字节数的空间呢?
有三个函数:
malloc()
calloc()
realloc()
这三个函数的声明都是放在stdlib的头文件中的,如果要使用主要需要事先导入这个头文件。
先来说说malloc()函数。
作用:向堆区申请指定字节的空间来使用。
括号内的参数只有一个,为size_t类型,也就是unsigned long类型。
参数代表的意义为:向堆区内存申请多少个连续的字节空间。
例如:malloc(4);
意思就是在堆区中申请4个连续的字节空间。
malloc函数的返回值为void *,这代表是没有类型的指针。
所以,它返回的是创建的空间的第一个字节的内存地址,这个地址是没有类型的。
那用什么类型的指针变量来接收malloc函数返回的地址呢?
那就要看想要用什么方式去操作这些字节空间,
如果想要1个字节1个字节的来操作,就用char指针;
如果想要4个字节4个字节的来操作,就用int指针;
如果想要8个字节8个字节的来操作,就用double指针;
如果想要4个字节4个字节的来操作,并且需要单精度浮点型来操作,就用float指针;
……
例如:
int *p1 = malloc(4);
*p1 = 100;
操作的时候是以4个字节为基本单位
char *p1 = malloc(8);
*p1 = 100;
操作的时候是以1个字节为基本单位
注意:
在堆区申请的字节空间是从低地址向高地址分配
不同批次申请的字节地址不一定是连续的,但申请的地址都是从0开始的
比如说:
int *p1 = malloc(4);
int *p2 = malloc(4);
int *p3 = malloc(4);
如采用printf(“%p\n,%p\n,%p\n”,p1,p2,p3);输出地址,会发现p1,p2,p3的地址不一定是连续的,但是地址的末位都是0。
但是同一批次申请的地址肯定是连续的
比如说:
int *p1 = malloc(4);
这申请的4个字节的空间地址一定是连续的。
在堆区申请的字节空间,里面是有值的,是垃圾值
在申请的时候可能会申请失败,如果申请失败,返回的指针就是NULL值
所以我们申请完空间以后,最好是判断一下是否申请成功:
int* p1 = malloc(4);
if(p1)
{
//申请成功时需要做的操作
}
申请的空间在使用完毕以后,一定要记得释放。释放的方法是:
free(指针);
如果没有释放空间,会直到程序结束才会释放,如果程序运行很久的话,会导致内存占用过多时长,造成越来越卡。
13.5.2 calloc函数与realloc函数
calloc函数
作用:向堆区申请指定字节数的空间
格式:
calloc(参数1,参数2);
参数1:多少个单位
参数2:每个单位的字节数
可以这么写:calloc(4,sizeof(int)); 表示申请4个int类型的空间。
注意:
calloc和malloc函数一样,也可能会申请失败,也必须在申请使用完以后释放其空间。
相对于malloc,calloc函数申请的字节空间,在申请完以后系统会将字节中的数据自动清零。
reallo函数
作用:扩容
注意:自从我们有了指针,几乎就可以操作内存上的每一个字节了。但是权力越大,越容易产生问题,所以不建议越出申请的空间来操作其他未申请的内存空间。
realloc函数可以在当我们发现我们原本申请的字节空间不够用的情况下为原本申请的空间扩容。
格式:
realloc(参数1,参数2);
参数1:想要扩容的指针
参数2:扩容后的字节空间单位
如果原来的空间后面还有剩余的空间,并且足够扩容,那么就直接扩容;
如果原来的空间后面不够扩容,那么就重新寻找空间扩容,并把原来的数据拷贝过来,原来的空间被释放。
13.6.0 推箱子游戏
推箱子游戏是在走出迷宫的基础上增加了一个箱子的进阶版,这里列出了两种代码方案,一种是在走出迷宫的基础上修改的。而优化版的则是引入了指针的应用,代码更加简洁。

13.6.1 普通代码
#include <stdio.h>
#include <stdlib.h>
#define rows 10
#define cols 11
char map[rows][cols] =
{
"##########",
"# #### #",
"# X#### #",
"# o #",
"###### #",
"# #### #",
"# #",
"# ######",
"# ",
"##########"
};
int personCurrentRow = 3;
int personCurrentCol = 2;
void showMap()
{
for (int i = 0; i < rows; i++)
{
printf("%s\n",map[i]);
}
}
char enterDirection()
{
printf("请输入小人的前进方向:W-上 S-下 A-左 D-右 Q-退出游戏\n");
printf("输入不区分大小写!\n");
char dir = 'a';
rewind(stdin);
scanf("%c",&dir);
return dir;
}
void moveToLeft(void);
void moveToUp(void);
void moveToRight(void);
void moveToDown(void);
int main()
{
while (map[8][9] != 'X')
{
system("clear");
showMap();
char personDir = enterDirection();
switch (personDir)
{
case 'a':
case 'A':
moveToLeft();
break;
case 'w':
case 'W':
moveToUp();
break;
case 'd':
case 'D':
moveToRight();
break;
case 's':
case 'S':
moveToDown();
break;
case 'q':
case 'Q':
printf("很遗憾,您退出了游戏!\n");
break;
}
}
printf("恭喜你,已经成功将箱子推出!\n");
return 0;
}
void moveToLeft(void)
{
int personNextRow = 0;
int personNextCol = 0;
personNextRow = personCurrentRow;
personNextCol = personCurrentCol - 1;
if (map[personNextRow][personNextCol] == ' ')
{
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
else if (map[personNextRow][personNextCol] == 'X')
{
int boxNextRow = 0;
int boxNextCol = 0;
boxNextRow = personNextRow;
boxNextCol = personNextCol - 1;
if (map[boxNextRow][boxNextCol] == ' ')
{
map[boxNextRow][boxNextCol] = 'X';
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
}
}
void moveToUp(void)
{
int personNextRow = 0;
int personNextCol = 0;
personNextRow = personCurrentRow - 1;
personNextCol = personCurrentCol;
if (map[personNextRow][personNextCol] == ' ')
{
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
else if (map[personNextRow][personNextCol] == 'X')
{
int boxNextRow = 0;
int boxNextCol = 0;
boxNextRow = personNextRow - 1;
boxNextCol = personNextCol;
if (map[boxNextRow][boxNextCol] == ' ')
{
map[boxNextRow][boxNextCol] = 'X';
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
}
}
void moveToRight(void)
{
int personNextRow = 0;
int personNextCol = 0;
personNextRow = personCurrentRow;
personNextCol = personCurrentCol + 1;
if (map[personNextRow][personNextCol] == ' ')
{
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
else if (map[personNextRow][personNextCol] == 'X')
{
int boxNextRow = 0;
int boxNextCol = 0;
boxNextRow = personNextRow;
boxNextCol = personNextCol + 1;
if (map[boxNextRow][boxNextCol] == ' ')
{
map[boxNextRow][boxNextCol] = 'X';
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
}
}
void moveToDown(void)
{
int personNextRow = 0;
int personNextCol = 0;
personNextRow = personCurrentRow + 1;
personNextCol = personCurrentCol;
if (map[personNextRow][personNextCol] == ' ')
{
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
else if (map[personNextRow][personNextCol] == 'X')
{
int boxNextRow = 0;
int boxNextCol = 0;
boxNextRow = personNextRow + 1;
boxNextCol = personNextCol;
if (map[boxNextRow][boxNextCol] == ' ')
{
map[boxNextRow][boxNextCol] = 'X';
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
}
}
13.6.2 优化版代码
#include <stdio.h>
#include <stdlib.h>
#define rows 10
#define cols 11
char map[rows][cols] =
{
"##########",
"# #### #",
"# X#### #",
"# o #",
"###### #",
"# #### #",
"# #",
"# ######",
"# ",
"##########"
};
int personCurrentRow = 3;
int personCurrentCol = 2;
void showMap()
{
for (int i = 0; i < rows; i++)
{
printf("%s\n",map[i]);
}
}
char enterDirection()
{
printf("请输入小人的前进方向:W-上 S-下 A-左 D-右 Q-退出游戏\n");
printf("输入不区分大小写!\n");
char dir = 'a';
rewind(stdin);
scanf("%c",&dir);
return dir;
}
void moveDirection(char personDir);
void getNextLocation(int personDir,int currentRow,int currentCol,int *pNextRow,int *pNextCol);
int main()
{
while (map[8][9] != 'X')
{
system("clear");
showMap();
char personDir = enterDirection();
if (personDir == 'q' || personDir == 'Q')
{
printf("很遗憾,您退出了游戏!\n");
return 0;
}
else
{
moveDirection(personDir);
}
}
printf("恭喜你!已成功推出箱子!\n");
return 0;
}
void moveDirection(char personDir)
{
int personNextRow = 0;
int personNextCol = 0;
getNextLocation(personDir, personCurrentRow, personCurrentCol, &personNextRow, &personNextCol);
if (map[personNextRow][personNextCol] == ' ')
{
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
else if (map[personNextRow][personNextCol] == 'X')
{
int boxNextRow = 0;
int boxNextCol = 0;
getNextLocation(personDir, personNextRow, personNextCol, &boxNextRow, &boxNextCol);
if (map[boxNextRow][boxNextCol] == ' ')
{
map[boxNextRow][boxNextCol] = 'X';
map[personNextRow][personNextCol] = 'o';
map[personCurrentRow][personCurrentCol] = ' ';
personCurrentRow = personNextRow;
personCurrentCol = personNextCol;
}
}
}
void getNextLocation(int personDir,int currentRow,int currentCol,int *pNextRow,int *pNextCol)
{
int nextRow = 0;
int nextCol = 0;
switch (personDir)
{
case 'a':
case 'A':
nextRow = currentRow;
nextCol = currentCol - 1;
break;
case 'w':
case 'W':
nextRow = currentRow - 1;
nextCol = currentCol;
break;
case 'd':
case 'D':
nextRow = currentRow;
nextCol = currentCol + 1;
break;
case 's':
case 'S':
nextRow = currentRow + 1;
nextCol = currentCol;
break;
}
*pNextRow = nextRow;
*pNextCol = nextCol;
}
下载购买完整本书,请点击如下图标: