友情链接:专栏地址
知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上
文章目录
- 🚀数组
- ⛳一、什么是数组
- ⛳二、一维数组
- 🎈(一)一维数组的定义
- 🎈(二)一维数组的初始化
- 🎈(三)一维数组元素的赋值和引用
- ⛳三、二维数组
- 🎈(一)二维数组的定义
- 🎈(二)二维数组的初始化
- 🎈(三)二维数组元素的赋值与访问
- ⛳四、其它多维数组
- ⛳五、变长数组(VLA)
- 🚀指针
- ⛳一、什么是指针
- 🎈(一)概念
- 🎈(二)&运算符
- 🎈(三)*间接运算符
- ⛳二、指针变量的定义与引用
- ⛳三、指针的初始化
- ⛳四、Void类型指针,空指针与坏指针
- ⛳五、常量指针和指针常量
- 🎈(一)常量指针
- 🎈(二)指针常量
- ⛳六、指针操作
- 🎈(一)指针的赋值、解引用
- 🎈(二)取值
- 🎈(三)指针与整数之间的加减运算
- 🎈(四)指针的自增、自减运算
- 🎈(五)指针与指针之间的加减运算
- 🎈(六)比较两个指针
- ⛳七、二级指针和多级指针
- 🚀动态内存分配与指向它的指针变量
- ⛳一、为什么要使用动态内存
- ⛳二、malloc()和free()
- ⛳三、free()的重要性,内存泄漏
- ⛳四、calloc()函数和memcpy()函数
- 🚀C语言字符串
- ⛳一、字符串的定义、初始化与字符串元素的引用
- ⛳二、字符串和字符串结束标志
- ⛳三、字符串的输入和输出
- 🎈(一)字符串输入
- 1.gets()函数
- 2.gets()的替代品
- (1)fgets()函数
- (2)gets_s()函数
- (3)scanf()函数
- 🎈(二)字符串输出
- 1.puts()函数
- 2.fputs()函数
- 3.printf()函数
- 🎈(三)自定义输入/输出函数
- ⛳四、字符串处理函数
- 🎈(一)strlen()函数
- 🎈(二)strcat()函数
- 🎈(三)strncat()函数
- 🎈(四)strcmp()函数
- 1.基本用法
- 2.strcmp()的返回值
- 3.strncmp()函数
- 🎈(五)strcpy()和strncpy()函数
- 🎈(六)sprintf()函数
🚀数组
⛳一、什么是数组
- 数组是一组有序数据的集合。数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。
- 用一个数组名(如s)和下标(如[15])来唯一地确定数组中的元素,数组元素的编号从0开始,例如candy[0]表示candy数组的第1个元素,candy[364]表示第365个元素
- 数组中的每一个元素都属于同一个数据类型。不能把不同类型的数据(如学生的成绩和学生的性别)放在同一个数组中。
由于计算机键盘只能输入有限的单个字符而无法表示上下标,C语言规定用方括号中的数字来表示下标,如s[15],将数组与循环结合起来,可以有效地处理大批量的数据,大大提高了工作效率,十分方便。
⛳二、一维数组
🎈(一)一维数组的定义
定义:
要使用数组,必须在程序中先定义数组,即通知计算机:由哪些数据组成数组,数组中有多少元素,属于哪个数据类型。否则计算机不会自动地把一批数据作为数组处理。
定义一维数组的一般形式为
类型说明符 数组名[常量表达式];
例如:
char name[40];
name后面的方括号表明这是一个数组,方括号中的40表明该数组中的元素数量。char表明每个元素的类型。
- 数组名的命名规则和变量名相同,遵循标识符命名规则。
- 在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。例如,指定a[10],表示a数组有10个元素。注意,下标是从0开始的,这10个元素是a[o],a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]。请特别注意,按上面的定义,不存在数组元素a[10]。
- 常量表达式中可以包括常量和符号常量,如"int a[3+5];”是合法的。不能包含变量,如“int a[n];”是不合法的。也就是说,C语言不允许对数组的大小作动态定义,即数组的大小不依赖于程序运行过程中变量的值。这是我们最常见的说法,不过在以下第二点拓展中可以看见有关VLA的不一样的点
- 在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。在C标准中,使用越界下标的结果是未定义的,使用越界的数组下标会导致程序改变其他变量的值。 不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。
拓展:
1.使用const声明数组,有时需要把数组设置为只读。这样,程序只能从数组中检索、引用值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组。
2.C++允许在声明数组大小时使用const整数,而C却不允许。
3.在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型常量,但是(与C++不同)const值不是。另外,表达式的值必须大于0:
int n = 5; int m = 8; float a1[5]; // 可以 float a2[5*2 + 1]; //可以 float a3[sizeof(int) + 1]; //可以 float a4[-4]; // 不可以,数组大小必须大于0 float a5[0]; // 不可以,数组大小必须大于0 float a6[2.5]; // 不可以,数组大小必须是整数 float a7[(int)2.5]; // 可以,已被强制转换为整型常量 float a8[n]; // C99之前不允许 float a9[m]; // C99之前不允许
以前支持C90标准的编译器不允许最后两种声明方式。而C99标准允许这样声明,这创建了一种新型数组,称为变长数组 (variable-length array)或简称 VLA(C11 放弃了这一创新的举措,把VLA 设定为可选,而不是语言必备的特性),但是注意一点:声明VLA时不能进行初始化
4.是否允许以下代码?
const int SZ = 80; ... double ar[SZ];
C90标准不允许(也可能允许)。数组的大小必须是给定的整型常量表达式,可以是整型常量组合,如20、sizeof表达式或其他不是const的内容。 由于C实现可以扩大整型常量表达式的范围,所以可能会允许使用const,但是这种代码可能无法移植。
🎈(二)一维数组的初始化
为了使程序简洁,常在定义数组的同时给各数组元素赋值,这称为数组的初始化。可以用“初始化列表”方法实现数组的初始化。使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前, 必须先给它们赋初值。
如果不初始化,各数组元素的值将是随机的(全局变量会初始化为 0,局部变量值随机),当我们引用数组元素时会有潜在错误,不过如果在第一次读之前给数组每个元素都赋值过就没事,但是对于复杂的程序,都不能这么假定,都必须初始化
-
在定义数组时对全部数组元素赋予初值。
-
可以只给数组中的一部分元素赋值:
如果在定义数值型数组时,指定了数组的长度并对之初始化,凡未被“初始化列表”指定初始化的数组元素,系统会自动把它们初始化为0(如果是字符型数组,则初始化为’\o’,如果是指针型数组,则初始化为NULL,即空指针)。
-
如果想使一个数组中全部元素值为0,可以写成
int a[10]={0,0,0,0,0,0,0,0,0,0}; 或 int a[10]={0}; //未赋值的部分元素自动设定为О
-
在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组长度,让编译器自动匹配数组大小和初始化列表中的项数,例如:
int a[5]={1,2,3,4,5}; //可以写成 int a[]={1,2,3,4,5};
拓展:
1.sizeof运算符
由于人工计算容易出错,所以让计算机来计算数组的大小。sizeof运算符给出它的运算对象的大小(以字节为单 位)。所以sizeof(days)是整个数组的大小(以字节为单位),sizeof(day[0])是数组中一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。
2.指定初始化器(C99)
C99 增加了一个新特性:指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。如:
int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
第一,如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段改为:[3] = 31,30,31,那么后面这些值将被用于初始化指定元素后面的元素。也就是说,在arr[3]被初始化为31后,arr[4]和arr[5]将分别被初始化为30和31。
第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。
第三,使用指定初始化器时为指定元素大小:编译器会把数组的大小设置为足够装得下初始化的值。
int stuff[] = {1, [6] = 23}; //stuff数组有7个元素,编号为0~6 int staff[] = {1, [6] = 4, 9, 10}; //而staff数组的元素比stuff数组多两个(即有9个元素)。
🎈(三)一维数组元素的赋值和引用
数组常与循环联系在一起
1.通过循环可以给数组的所有元素赋值
声明数组后,可以借助数组下标(或索引)给数组元素赋值,
/* 给数组的元素赋值 */
#include <stdio.h>
#define SIZE 50
int main(void)
{
int counter, evens[SIZE];
for (counter = 0; counter < SIZE; counter++)
evens[counter] = 2 * counter;
...
}
2.通过循环可以引用数组所有元素
在定义数组并对其中各元素赋值后﹐就可以引用数组中的元素。应注意:只能引用数组元素而不能一次整体调用整个数组全部元素的值。
//引用数组元素的表示形式为:
数组名[下标]
//与循环结合
...
for (counter = 0; counter < SIZE; counter++)
printf("%d",evens[counter]);
...
注意:C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。
#define SIZE 5 int main(void) { int oxen[SIZE] = {5,3,2,8}; /* 初始化没问题 */ int yaks[SIZE]; yaks = oxen; /* 不允许 */ yaks[SIZE] = oxen[SIZE]; /* 数组下标越界 */ yaks[SIZE] = {5,3,2,8}; /* 不起作用 */ }
⛳三、二维数组
二维数组,就是指含有多个数组的数组!如果把一维数组理解为一行数据,那么,二维数组可形象地表示为行列结构。
🎈(一)二维数组的定义
二维数组定义的一般形式为
类型说明符 数组名[常量表达式][常量表达式];
和一维数组一样,需要先定义,再使用。
//一行女兵 以一维数组的形式表示
int a[25] ;
//五行女兵
int a[5][25];
//定义了一个二维数组, //数组名是“a”, //包含 5 行 25 列,共 125 元素, //每个元素是 int
一维数组是按顺序存储的,二维数组呢? 同样也是,在逻辑上我们可以用矩阵形式(如3行4列形式)表示二维数组,能形象地表示出行列关系。但实际在内存中,各元素是连续存放的,不是二维的,是线性的。
🎈(二)二维数组的初始化
二维数组同样需要初始化,如果不初始化,它的值可能是随机的(全局变量会初始化为 0,局部变量值随机)
-
分行给二维数组赋初值。
int a[3][4]={ {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是这并不会影响其他行的初始化。
-
可以将所有数据写在一个花括号内,按数组元素在内存中的排列顺序对各元素赋初值。例如:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
-
可以对部分元素赋初值。例如:
int a[3][4]={{1},{5},{9}};
它的作用是对各行第1列(即序号为0的列)的元素赋初值,其余元素值自动为0。
-
如果对全部元素都赋初值(即提供全部初始数据),则定义数组时对第1维的长度可以不指定,但第⒉维的长度不能省。例如:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; //与下面的定义等价: //系统会根据数据总个数和第⒉维的长度算出第1维的长度。数组一共有12个元素,每行4列,显然可以确定行数为3。 int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12 };
在定义时也可以只对部分元素赋初值而省略第1维的长度,但应分行赋初值。例如:
int a[][4]={{0,0,3},{ },{0,10}};
🎈(三)二维数组元素的赋值与访问
二维数组元素的表示形式为
数组名[下标][下标]
二维数组同样使用循环来赋值与遍历访问每个元素,只不过与一维数组的区别是要使用两层循环
int a[6][6];
//赋值:
for(i=0;i<6;i++){ //行
for(j=0;j<6;j++){ //列
a[i][j] = (i/j)*(j/i);
}
}
//引用(遍历):
for(i=0;i<6;i++) {
for(j=0;j<6;j++) {
printf("%2d",a[i][j]); //对已经赋值的部分全部输出
}
printf("\n");
}
⛳四、其它多维数组
前面讨论的二维数组的相关内容都适用于三维数组或更多维的数组。可以这样声明一个三维数组:
int girl[3][8][5];
可以把一维数组想象成一排女兵,把二维数组想象成一个女兵方阵,把三维数组想象成多个女兵方阵。这样,当你要找其中的一个女兵时,你只要知道她在哪个方阵(从 0、1、2 中 选择),在哪一行(从 0-7)中选择,在哪一列(从 0-4 中选择)
而通常,处理三维数组要使用3重嵌套循环,处理四维数组要使用4重嵌套循环。对于其他多维数组,以此类推。
⛳五、变长数组(VLA)
C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。如下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // 一个变长数组(VLA)
- 变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern 存储类别说明符
- 不能在定义(声明)中初始化它们。最终, C11把变长数组作为一个可选特性,而不是必须强制实现的特性。
注意:变长数组不能改变大小,变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用 变量指定数组的维度。
拓展:
函数声明时可以省略形参名,而只写形参的类型,但是在这里必须用星号来代替省略的维度,并且在形参列表中必须在声明ar之前先声明这两个维度形参
int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序 int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名
C99/C11 标准允许在声明变长数组时使用 const 变量。所以该数组的定义必须是声明在块中的自动存储类别数组。
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小。普通 C 数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了。
🚀指针
⛳一、什么是指针
🎈(一)概念
指针(pointer)是 C 语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。前面使用的scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。打个比方,一个房间的门口挂了一个房间号2008,这个2008就是房间的地址,或者说,2008“指向”该房间。因此,将地址形象化地称为“指针”。意思是通过它能找到以它为地址的内存单元。
- 指针本身也是一个变量,称为指针变量
- 32 位系统中,int 整数占 4 个字节,指针同样占 4 个字节, 64 位系统中,int 整数占 4 个字节,指针同样占 8 个字节
- 指针变量的值是一个内存地址
- 指针是一个地址,而指针变量是存放地址的变量
- 简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。
C++程序员可能认为,既然C和 C++都使用指针变量,那么C应该也有引用变量。让他们失望了,C没有引用变量。
🎈(二)&运算符
在许多语言中,地址都归计算机管,对程序员隐藏。然而在 C 中,可以通过&运算符访问地址,通过*运算符获得地址上的值。例如,&barn表示变量barn的地址,使用*运算符即可获得储存在地址上的值。如果pbarn= &barn;,那么*pbarn表示的是储存在&barn地址上的值。
一元&运算符给出变量的存储地址。如果pooh是变量名,那么&pooh是变量的地址。可以把地址看作是变量在内存中的位置。
例如:
ptr = &pooh; // 把pooh的地址赋给ptr
对于这条语句,我们说ptr“指向”pooh。ptr和&pooh的区别是ptr是变量, 而&pooh是常量。或者,ptr是可修改的左值,而&pooh是右值。
&在C语言中有两种意思:
1.是取地址符,即本处所指
2.是位运算符,表示按位与,是双目运算符,在第一章中讲过
🎈(三)*间接运算符
使用间接运算符*(indirection operator)后跟一个指针名或地址时,*给出储存在指针指向地址上的值。该运算符有时也称为解引用运算符(dereferencing operator)。
不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。
例如:
val = *ptr; // 找出ptr指向的值
⛳二、指针变量的定义与引用
定义指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,
之前已经提到,在C primer plus一书中,将此称为声明,而大部分我们见到的都是叫定义变量
1.定义指针变量的一般形式为:
类型名 * 指针变量名;
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。
//定义指针变量
int *p; // int *p1, *p2;
或者
int* p; // int* p1,p2; //p1 是指针, p2 只是整形变量
或者
int * p;
或者
int*p;//不建议
1.指针本身的类型
p指向的值(*p)是int类型。p本身是什么类型?我们描述它的类型是“指向int类型的指针”。p 的值是一个地址,在大部分系统内部,该地址由一个无符号整数表示。但是,不要把指针认为是整数类型。一些处理整数的操作不能用来处理指针,反之亦然。例如,可以把两个整数相乘,但是不能把两个指针相乘。所以,指针实际上是一个新类型,不是整数类型。因此,如前所述,ANSI C专门为指针提供了%p格式的转换说明。
2.指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把 int 类型的值赋给double类型的变量,但是两个类型的指针不能这样做。
int n = 5; double x; int * p1 = &n; double * pd = &x; x = n; // 隐式类型转换 pd = p1; // 编译时错误
不兼容情况:
指向int类型等和指向数组是不兼容的
指向含不同数据元素个数的数组是不兼容的
指向指针的指针和指向数组的指针是不兼容的
把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针指向的数据,这样的行为是未定义的,在C++中不允许这样做
把非const指针赋给const指针没问题,前提是只进行一级解引用:
p2 = p1; // 有效 -- 把非const指针赋给const指针
但是进行两级解引用时,这样的赋值也不安全,例如,考虑下面的代码:
const int **pp2; int *p1; const int n = 13 pp2 = &p1; // 允许,但是这导致const限定符失效(根据第1行代码,不能通过*pp2修改它所指向的内容) *pp2 = &n; // 有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)
2.引用指针变量
即使用*运算符访问储存在指针指向地址上的值,前提是已执行,例如:“p=&a;” 即指针变量p指向了整型变量a,
//其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值。
printf("%d",*p);
⛳三、指针的初始化
int room = 2;
//定义两个指针变量指向 room
int *p1 = &room;
int *p2 = &room;
⛳四、Void类型指针,空指针与坏指针
Void类型指针:
空类型指针只存储地址的值,丢失类型,无法访问,要访问其值,我们必须对这个指针做出正确的类型转换,然后再间接引用指针。 所有其它类型的指针都可以隐式自动转换成 void 类型指针,反之需要强制转换
该类型的指针相当于一个“通用指针”,把指向 void 的指针赋给任意类型的指针完全不用考虑类型匹配的问题。强制转换一下即可
在C和C++中,都可以把任何类型的指针赋给void类型的指针,但是,C++要求在把void*指针赋给任 何类型的指针时必须进行强制类型转换。而C没有这样的要求。
void => 空类型
void* => 空类型指针
空指针:
空指针,简单来讲就是值为 0 的指针。(任何程序数据都不会存储在地址为 0 的内存块中,它是被操作系统预留的内存块。)例如:
int *p = 0;
//或者
int *p = NULL; //强烈推荐
空指针的使用:
-
当我们需要一个指针变量,但暂时不需要用到它,可以将指针初始化为空指针,目的就是避免访问非法数据。
int *select = NULL;
-
指针不再使用时,可以设置为空指针
int *select = &xiao_long_lv; //和小龙女约会 select = NULL;
-
表示这个指针还没有具体的指向,使用前进行合法性判断
int *p = NULL; // 。。。。 if (p) { //p 等同于 p!=NULL //指针不为空,对指针进行操作 }
坏指针:
坏指针指没有进行初始化的指针,或者是当前应用程序不可访问的地址值,不能对他们做解指针操作,例如:
int *select; //没有初始化
//情形一
printf("选择的房间是: %d\n", *select);
//情形二
select = 100;
printf("选择的房间是: %d\n", *select);
切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此, 在使用指针之前,必须先用已分配的地址初始化它。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。
⛳五、常量指针和指针常量
我们在前面使用const创建过变量:
const double PI = 3.14159;
虽然用#define指令可以创建类似功能的符号常量,但是const的用法更加灵活。可以创建const数组、const指针和指向const的指针。
🎈(一)常量指针
指向const的指针不能用于改变值,无论是使用指针表示法还是数组表示法,都不允许使用修改指针所指向数据的值。但是可以改变指针指向的地址
int wife = 24;
int girl = 18;
const int * zhi_nan = &wife; //第一种写法,建议用这种,更容易与指针常量分辨开来
int const * zhi_nan = &wife; // 第二种写法
//*zhi_nan = 26; 不允许修改
printf("直男老婆的年龄:%d\n", *zhi_nan);
zhi_nan = &girl; //允许修改指针的指向
printf("直男女朋友的年龄:%d\n", *zhi_nan);
//*zhi_nan = 20;
拓展:
1.指向 const 的指针通常用于函数形参中,表明该函数不会使用指针改变数据。
2.关于指针赋值和const需要注意一些规则。首先,把const数据或非const 数据的地址初始化为指向const的指针或为其赋值是合法的:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5}; const double locked[4] = {0.08, 0.075, 0.0725, 0.07}; const double * pc = rates; // 有效 pc = locked; //有效 pc = &rates[3]; //有效
但是只能把非const数据的地址赋给普通指针:
double * pnc = rates; // 有效 pnc = locked; // 无效 pnc = &rates[3]; // 有效
这个规则非常合理。否则,通过指针就能改变const数组中的数据。
🎈(二)指针常量
const还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,但是可以修改指针指向地址的值
int * const nuan_nan = &wife;
*nuan_nan = 26;
printf("暖男老婆的年龄:%d\n", wife);
//nuan_nan = &girl; //不允许指向别的地址
可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。
1.在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值:
const int * const super_nuan_nan = &wife; //不允许指向别的地址,不能修改指向变量的值 //*super_nuan_nan = 28; 不允许 //super_nuan_nan = &girl 不允许
2.总结:如何区分指针常量还是常量指针,看 const 离类型(int)近,还是离指针变量名近,离谁近,就修饰谁,谁就不能变
⛳六、指针操作
C提供了一些基本的指针操作,实例代码:
// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{
int urn[5] = { 100, 200, 300, 400, 500 };
int * ptr1, *ptr2, *ptr3;
ptr1 = urn; // 把一个地址赋给指针
ptr2 = &urn[2]; // 把一个地址赋给指针
// 解引用指针,以及获得指针的地址
printf("pointer value, dereferenced pointer, pointer address:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
// 指针加法
ptr3 = ptr1 + 4;
printf("\nadding an int to a pointer:\n");
printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
ptr1++; // 递增指针
printf("\nvalues after ptr1++:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
ptr2--; // 递减指针
printf("\nvalues after --ptr2:\n");
printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
--ptr1; // 恢复为初始值
++ptr2; // 恢复为初始值
printf("\nPointers reset to original values:\n");
printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
// 一个指针减去另一个指针
printf("\nsubtracting one pointer from another:\n");
printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n",
ptr2, ptr1, ptr2 - ptr1);
// 一个指针减去一个整数
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
//下面是我们的系统运行该程序后的输出:
pointer value, dereferenced pointer, pointer address:
ptr1 = 0x7fff5fbff8d0, *ptr1 =100, &ptr1 = 0x7fff5fbff8c8
adding an int to a pointer:
ptr1 + 4 = 0x7fff5fbff8e0, *(ptr1 + 4) = 500
values after ptr1++:
ptr1 = 0x7fff5fbff8d4, *ptr1 =200, &ptr1 = 0x7fff5fbff8c8
values after --ptr2:
ptr2 = 0x7fff5fbff8d4, *ptr2 = 200, &ptr2 = 0x7fff5fbff8c0
Pointers reset to original values:
ptr1 = 0x7fff5fbff8d0, ptr2 = 0x7fff5fbff8d8
subtracting one pointer from another:
ptr2 = 0x7fff5fbff8d8, ptr1 = 0x7fff5fbff8d0, ptr2 - ptr1 = 2
subtracting an int from a pointer:
ptr3 = 0x7fff5fbff8e0, ptr3 - 2 = 0x7fff5fbff8d8
🎈(一)指针的赋值、解引用
这个大部分在指针的定义与引用中已经讲到
赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的 变量名、另一个指针进行赋值。
在该例中,把urn数组的首地址赋给了ptr1, 该地址的编号恰好是0x7fff5fbff8d0。变量ptr2获得数组urn的第3个元素 (urn[2])的地址。
地址应该和指针类型兼容。也就是说,不能把 double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。 C99/C11已经强制不允许这样做。
解引用:*运算符给出指针指向地址上储存的值。
*ptr1的初值是 100,该值储存在编号为0x7fff5fbff8d0的地址上。
🎈(二)取值
和所有变量一样,指针变量也有自己的地址和值。对指针而言, &运算符给出指针本身的地址。
本例中,ptr1 储存在内存编号为 0x7fff5fbff8c8 的地址上,该存储单元储存的内容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向utn[0]的指针。
🎈(三)指针与整数之间的加减运算
指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位) 相乘,然后把结果与初始地址相加。因此ptr1 +4与&urn[4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与 &urn[2]等价,因为ptr3指向的是&arn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位 置,C保证该指针有效。
通用公式:
数据类型 *p;
p + n 实际指向的地址:p 基地址 + n * sizeof(数据类型)
p - n 实际指向的地址:p 基地址 - n * sizeof(数据类型)
🎈(四)指针的自增、自减运算
递增指向数组元素的指针可以让该指针移动至数组的下一个元素。递减则相反,前缀或后缀的递增和递减运算符都可以使用。
因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节), ptr1指向urn[1]。现在ptr1的值是 0x7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[1]的值)。注意,ptr1本身的地址仍是 0x7fff5fbff8c8。毕竟,变量不会因为值发生变化就移动位置。
总结: p++ 的概念是在 p 当前地址的基础上 ,自增 p 对应类型的大小, 也就是说 p = p+ 1*sizeof(类型),p–则相反
在递增或递减指针时还要注意一些问题。编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。另外,可以解引用指向数组任意元素的指针。但是,即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针。
🎈(五)指针与指针之间的加减运算
-
指针和指针可以做减法操作,但不适合做加法运算;
-
指针和指针做减法适用的场合:
-
两个指针都指向同一个数组,相减结果为两个指针之间的元素数目,而不是两个指针之间相差的字节数。即差值的单位与数组类型的单位相同。例如,ptr2 - ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。比如:
int int_array[4] = {12, 34, 56, 78}; int *p_int1 = &int_array[0]; int *p_int2 = &int_array[3]; // p_int2 - p_int1 的结果为 3,即是两个之间之间的元素数目为 3 个。如果两个指针不是指向同一个数组,它们相减就没有意义。
-
-
不同类型的指针不允许相减,比如以下相减是没有意义的
char *p1; int *p2; p2-p1;
🎈(六)比较两个指针
使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
两个指针变量之间比大小,其实是比较指向的地址谁大谁小以及为空或者不为空,而不是地址中保存的值,一般也只用于数组当中,指向一个数组前面元素的指针小于指向后面元素的指针,如果是单纯的两个不相关的指针进行比较,一般编译不通过
⛳七、二级指针和多级指针
二级指针也是一个普通的指针变量,只是它里面保存的值是另外一个一级指针的地址
定义:
int guizi1 = 888;
int *guizi2 = &guizi1; //1 级指针,保存 guizi1 的地址
int **liujian = &guizi2; //2 级指针,保存 guizi2 的地址,guizi2 本身是一个一级指针变
二级指针的用途:
-
普通指针可以将变量通过参数“带入”函数内部,但没办法将内部变量“带出”函数
-
二级指针不但可以将变量通过参数函数内部,也可以将函数内部变量 “带出”到函数外部
// demo 8-13.c #include <stdio.h> #include <stdlib.h> void swap(int *a,int *b){ int tmp =*a; *a= *b; *b= tmp; } void boy_home(int **meipo){ static int boy = 23; *meipo = &boy; } int main(void){ //int x=10, y=100; //swap(&x, &y); //printf("x=%d, y=%d\n", x, y); int *meipo = NULL; boy_home(&meipo); printf("boy: %d\n", *meipo); system("pause"); return 0; }
可以定义多级指针指向次一级指针
比如:
int guizi1 = 888;
int *guizi2 = &guizi1; //普通指针
int **guizi3 = &guizi2; //二级指向一级
int ***guizi4 = &guizi3; //三级指向二级
int guizi5 = &guizi4; //四级指向三级
// 有完没完。。。
🚀动态内存分配与指向它的指针变量
我们前面讨论的存储类别有一个共同之处:在确定用哪种存储类别后, 根据已制定好的内存管理规则,将自动选择其作用域和存储期。然而,还有更灵活地选择,即用库函数分配和管理内存。
⛳一、为什么要使用动态内存
-
按需分配,根据需要分配内存,不浪费
-
被调用函数之外需要使用被调用函数内部的指针对应的地址空间
-
突破栈区的限制,可以给程序分配更多的内存
⛳二、malloc()和free()
函数原型:
void *malloc(long NumBytes)
该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。(关于分配失败的原因,应该有多种,比如说空间不足就是一种。)
void free(void *FirstByte):
该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。
注意:
- 申请了内存空间后,必须检查是否分配成功。
- 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
- 这两个函数应该是配对。ree()函数的参数是之前malloc()返回的地址,如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
- 虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
- malloc()和free()的原型都在stdlib.h 头文件中。
现在,我们有3种创建数组的方法。
- 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内存创建这种数组。
- 声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度, 用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
- 声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
使用第2种和第3种方法可以创建动态数组(dynamic array)。这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。第3种比第二种更加灵活
到底从哪获得空间,释放的是什么:
1.malloc()到底从哪里得来了内存空间?
从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
2.free()到底释放了什么?
free()释放的是指针指向的内存!不是指针!指针并没有被释放,指针仍然指向原来的存储空间。指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容是未定义的,所以说是垃圾。因此,释放内存后把指针指向NULL,防止指针在后面不小心又被解引用了。
⛳三、free()的重要性,内存泄漏
静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非用 free()进行释放。例如:
int main()
{
double glad[2000];
int i;
...
for (i = 0; i < 1000; i++)
gobble(glad, 2000);
...
}
void gobble(double ar[], int n)
{
double * temp = (double *) malloc( n * sizeof(double));
.../* free(temp); // 假设忘记使用free() */
}
- 第1次调用gobble()时,它创建了指针temp,并调用malloc()分配了16000 字节的内存(假设double为8 字节)。假设如代码注释所示,遗漏了free()。 当函数结束时,作为自动变量的指针temp也会消失。但是它所指向的16000 字节的内存却仍然存在。由于temp指针已被销毁,所以无法访问这块内存,
- 它也不能被重复使用,因为代码中没有调用free()释放这块内存。
- 第2次调用gobble()时,它又创建了指针temp,并调用malloc()分配了 16000字节的内存。第1次分配的16000字节内存已不可用,所以malloc()分配 了另外一块16000字节的内存。当函数结束时,该内存块也无法被再访问和再使用。
- 循环要执行1000次,所以在循环结束时,内存池中有1600万字节被占 用。实际上,也许在循环结束之前就已耗尽所有的内存。这类问题被称为内存泄漏(memory leak)。在函数末尾处调用free()函数可避免这类问题发生。
⛳四、calloc()函数和memcpy()函数
1.分配内存还可以使用calloc(),典型的用法如下:
long * newmem;
newmem = (long *)calloc(100, sizeof (long))
calloc()函数接受两个无符号整数作为参数(ANSI规定是size_t类 型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。
在该例中,long为4字节,所以,前面的代码创建了100个4 字节的存储单元,总共400字节。free()函数也可用于释放calloc()分配的内存。
2.内存拷贝函数
void *memcpy(void *dest, const void *src, size_t n);
#include <string.h>
功能:从源 src 所指的内存地址的起始位置开始拷贝 n 个字节到目标 dest 所指的内存地址的起始位置中
🚀C语言字符串
由于字符数据的应用较广泛,尤其是作为字符串形式使用,有其自己的特点,C语言中没有字符串类型,字符串是存放在字符型数组中的,是以空字符(\0)结尾的char类型数组。
字符串常量:
在第一章的时候已经介绍过字符串常量,这里再补充几个知识点
- 字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在,即使函数被调用多次。
- 用双引号括起来的内容被视为指向该字符串储存位置的指针。这类似于把数组名作为指向该数组位置的指针。同样,字符串拷贝的时候也只拷贝一个地址,而不是整个数组
⛳一、字符串的定义、初始化与字符串元素的引用
定义字符串,采用定义字符数组的形式,用来存放字符数据的数组是字符数组。在字符数组中的一个元素内存放一个字符。定义字符数组的方法与定义数值型数组的方法类似。例如:
char c[10];
(一)定义字符串时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组储存字符串,在下面的声明中,用指定的字符串初始化数组 m1:
const char m1[40] = "Limit yourself to one line's worth.";
const表明不会更改这个字符串。
这种形式的初始化比标准的数组初始化形式简单得多:
const char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 's', 'e', 'l','f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 'n', 'e','\\', 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0' };
注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符数组。但是,用这种形式我们就很容易理解引用字符串中的某个字符,两种形式都是一样的:
m1[3]
就是字符 i ,在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为 了容纳空字符)。所有未被使用的元素都被自动初始化为0(这里的0指的是 char形式的空字符,不是数字字符0),例如:
(二)通常,让编译器确定数组的大小很方便。回忆一下,省略数组初始化声明中的大小,编译器会自动计算数组的大小:
const char m2[] = "If you can't think of anything, fake it.";
让编译器确定初始化字符数组的大小很合理。因为处理字符串的函数通常都不知道数组的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处结束。
让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。
声明数组时,数组大小必须是可求值的整数。在C99新增变长数组之前,数组的大小必须是整型常量,包括由整型常量组成的表达式。
int n = 8; char cookies[1]; // 有效 char cakes[2 + 5];// 有效,数组大小是整型常量表达式 char pies[2*sizeof(long double) + 1]; // 有效 char crumbs[n]; // 在C99标准之前无效,C99标准之后这种数组是变长数组
字符数组名和其他数组名一样,是该数组首元素的地址。
还可以使用指针表示法创建字符串:
const char * pt1 = "Something is pointing at me."; //等同于: const char ar1[] = "Something is pointing at me.";
以上两个声明表明,pt1和ar1都是该字符串的地址。在这两种情况下, 带双引号的字符串本身决定了预留给字符串的存储空间。尽管如此,这两种形式并不完全相同。
⛳二、字符串和字符串结束标志
在实际工作中,人们关心的往往是字符串的有效长度而不是字符数组的长度。例如,定义一个字符数组长度为100,而实际有效字符只有40个。为了测定字符串的实际长度,C语言规定了一个“字符串结束标志”,以字符’\0’作为结束标志。如果字符数组中存有若干字符,前面9个字符都不是空字符(‘\0’) ,而第10个字符是’\0’ ,则认为数组中有一个字符串,其有效字符为9个。也就是说,在遇到字符’\0’时,表示字符串结束,把它前面的字符组成一个字符串。
注意:C系统在用字符数组存储字符串常量时会自动加一个’\0’作为结束符。例如"C program"共有9个字符。字符串是存放在一维数组中的,在数组中它占10个字节,最后一个字节’0’是由系统自动加上的。
说明:'\0’代表ASCII码为0的字符,从ASCHI码表中可以查到,ASCII码为0的字符不是一个可以显示的字符,而是一个“空操作符”,即它什么也不做。用它来作为字符串结束标志不会产生附加的操作或增加有效字符,只起一个供辨别的标志。
说明:字符数组并不要求它的最一个字符为\0’,甚至可以不包含’\0’。像以下这样写完全是合法的:
char c[5]={'C' ,'h',' i' ,' n' ,'a'} ;
是否需要加’\0’,完全根据需要决定。由于系统在处理字符串常量存储时会自动加一个’0’,因此,为了使处理方法一致,便于测定字符串的实际长度,以及在程序中作相应的处理,在字符数组中也常常人为地加上一个’\0’。例如:
char c[6]={ 'C',' h' ,' i' ,'n',' a' ,'\o'};
这样做,便于引用字符数组中的字符串。
1.字符数组和字符串的区别:
字符数组就是字符串这种说法是错误的,两者在本质上的区别就在于“字符串结束标志”的使用,字符数组中的每个元素都可以存放任意的字符,并不要求最后一个字符必须是‘\0’。但作为字符串使用时,就必须以‘\0’结束,因为很多有关字符串的处理都要以‘\0’作为操作时的辨别标志,缺少这一标志,系统并不会报错,有时甚至可以得到看似正确的运行结果。但这种潜在的错误可能会导致严重的后果。因为在字符串处理过程中,系统在未遇到串结束标志之前,会一直向后访问,以致超出分配给字符串的内存空间二访问到其他数据所在的存储单元。
⛳三、字符串的输入和输出
🎈(一)字符串输入
1.gets()函数
在读取字符串时,scanf()和转换说明%s只能读取一个单词。可是在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。
gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。它经常和 puts()函数配对使用,
char words[5];
get(words);
put(words);
上一章介绍过,数组名会被转换成该数组首元素的地址,因此,gets()函数只知道数组的开始处,并不知道数组中有多少个元素。如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常中止;或者还有其他情况。
过去,有些人通过系统编程,利用gets()插入和运行一些破坏系统安全的代码。C 编程社区的许多人都建议在编程时摒弃 gets()。制定 C99 标准的委员会把这些建议放入了标准,承认了gets()的问题并建议不要再使用它。C11标准委员会采取了更强硬的态度,直接从标准中废除了 gets()函数。然而在实际应用中,编译器为了能兼容以前的代码,大部分都继续支持gets()函数。
2.gets()的替代品
(1)fgets()函数
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。其原型为:
char *fgets(char *str, int n, FILE *stream);
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。
-
fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
-
如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
-
fgets()函数的第3 个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中
-
如果函数读到文件结尾,它将返回一个特殊的指针:空指针(null pointer)
-
fgets()函数最容易使用,而且可以选择不同的处理方式。可以让程序继续使用输入行中超出的字符,也可以丢弃输入行的超出字符
继续使用:
/* fgets2.c -- 使用 fgets() 和 fputs() */ #include <stdio.h> #define STLEN 10 int main(void) { char words[STLEN]; puts("Enter strings (empty line to quit):"); while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n') fputs(words, stdout); puts("Done."); return 0; }
丢弃:
/* fgets3.c -- 使用 fgets() */ #include <stdio.h> #define STLEN 10 int main(void) { char words[STLEN]; int i; puts("Enter strings (empty line to quit):"); while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n') { i = 0; while (words[i] != '\n' && words[i] != '\0') i++; if (words[i] == '\n') words[i] = '\0'; else // 如果word[i] == '\0'则执行这部分代码 while (getchar() != '\n') continue; puts(words); } puts("done"); return 0; }
拓展:
1.fgets()函数读取整行输入并用空字符代替换行符,或者读取一部分输入,并丢弃其余部分。既然没有处理这种情况的标准函数,我们就创建一个:
char * s_gets(char * st, int n) { char * ret_val; int i = 0; ret_val = fgets(st, n, stdin); if (ret_val) // 即,ret_val != NULL { while (st[i] != '\n' && st[i] != '\0') i++; if (st[i] == '\n') st[i] = '\0'; else while (getchar() != '\n') //丢弃该输入行其余字符 continue; } return ret_val; }
为什么要丢弃过长输入行中的余下字符。这是因为,输入行中多出来的字符会被留在缓冲区中,成为下一次读取语句的输入。
(2)gets_s()函数
C11新增的gets_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。
- gets_s()只从标准输入中读取数据,所以不需要第3个参数。
- 如果gets_s()读到换行符,会丢弃它而不是储存它。
- 如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
总结:
如果输入行太长,使用gets()不安全,它会擦写现有数据,存在安全隐患。gets_s()函数很安全,但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。另外,如果打算让程序继续运行, gets_s()会丢弃该输入行的其余字符,无论你是否需要。
当输入与预期不符时,gets_s()完全没有fgets()函数方便、灵活。 也许这也是gets_s()只作为C库的可选扩展的原因之一。鉴于此,fgets()通常 是处理类似情况的最佳选择。
(3)scanf()函数
scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾: scanf()更像是“获取单词”函数,而不是“获取字符串”函数
- 如果预留的存储区装得下输入行,gets()和fgets()会读取第1个换行符之前所有的字符。
- scanf()函数有两种方法确定输入结束。无论哪种方法,都从第1个非空白字符作为字符串的开始。如果使用%s转换说明,以下一个空白字符(空行、 空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10 个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)
scanf()和gets()类似,也存在一些潜在的缺点。如果输入行的内容过长, scanf()也会导致数据溢出。不过,在%s转换说明中使用字段宽度可防止溢出。
🎈(二)字符串输出
C有3个标准库函数用于打印字符串:put()、fputs()和printf()。
1.puts()函数
puts()函数很容易使用,只需把字符串的地址作为参数传递给它即可。
char str1[80] = "An array was initialized to me.";
const char * str2 = "A pointer was initialized to me.";
puts("I'm an argument to puts().");
puts(str1);
puts(str2);
- 为puts()在显示字符串时会自动在其末尾添加一个换行符。
- 再次说明,用双引号括起来的内容是字符串常量,且被视为该字符串的地址。另外,储存字符串的数组名也被看作是地址。
- puts()如何知道在何处停止?该函数在遇到空字符时就停止输出,所以必须确保有空字符。
2.fputs()函数
fputs()函数是puts()针对文件定制的版本。
- fputs()函数的第 2 个参数指明要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为该参数。
- 与puts()不同,fputs()不会在输出的末尾添加换行符。
gets()丢弃输入中的换行符,但是puts()在输出中添加换行符。另一方面,fgets()保留输入中的换行符,fputs()不在输出中添加换行符。
3.printf()函数
和puts()一样,printf() 也把字符串的地址作为参数。printf()函数用起来没有puts()函数那么方便, 但是它更加多才多艺,因为它可以格式化不同的数据类型。
- 与puts()不同的是,printf()不会自动在每个字符串末尾加上一个换行符。因此,必须在格式字符串中指明应该在哪里使用换行符。
- 逐个字符输入输出。用格式符“%c”输入或输出一个字符
- 将整个字符串一次输入或输出。用“%s”格式符,意思是对字符串(string)的输入输出。输出项是字符数组名或者指针变量名,而不是数组元素名,指针变量不需要解引。scanf函数中的输入项如果是字符数组名,不要再加地址符&.,
🎈(三)自定义输入/输出函数
不一定非要使用C库中的标准函数,如果无法使用这些函数或者不想用它们,完全可以在getchar()和putchar()的基础上自定义所需的函数。假设需要一个类似puts()但是不会自动添加换行符的函数:
/* put1.c -- 打印字符串,不添加\n */
#include <stdio.h>
void put1(const char * string)/* 不会改变字符串 */
{
while (*string != '\0')
putchar(*string++);
}
假设要设计一个类似puts()的函数,而且该函数还给出待打印字符的个数:
/* put2.c -- 打印一个字符串,并统计打印的字符数 */
#include <stdio.h>
int put2(const char * string)
{
int count = 0;
while (*string) /* 常规用法 */
{
putchar(*string++);
count++;
}
putchar('\n'); /* 不统计换行符 */
return(count);
}
⛳四、字符串处理函数
C库提供了多个处理字符串的函数,ANSI C把这些函数的原型放在 string.h头文件中。其中最常用的函数有 strlen()、strcat()、strcmp()、 strncmp()、strcpy()和strncpy()。另外,还有sprintf()函数,其原型在stdio.h头文件中。
🎈(一)strlen()函数
strlen()函数用于统计字符串的长度。下面的函数可以缩短字符串的长度,其中用到了strlen():
void fit(char *string, unsigned int size)
{
if (strlen(string) > size)
string[size] = '\0';
}
- 该函数要改变字符串,所以函数头在声明形式参数string时没有使用 const限定符。
- 一些ANSI之前的系统使用strings.h头文件,而有些系统可能根本没有字符串头文件。
🎈(二)strcat()函数
strcat()(用于拼接字符串)函数接受两个字符串作为参数。该函数把第 2个字符串的备份附加在第1个字符串末尾,并把拼接后形成的新字符串作为 第1个字符串,第2个字符串不变。strcat()函数的类型是char *(即,指向char 的指针)。strcat()函数返回第1个参数,即拼接第2个字符串后的第1个字符串的地址。
/* str_cat.c -- 拼接两个字符串 */
#include <stdio.h>
#include <string.h> /* strcat()函数的原型在该头文件中 */
#define SIZE 80
char * s_gets(char * st, int n);
int main(void)
{
char flower[SIZE];
char addon [] = "s smell like old shoes.";
puts("What is your favorite flower?");
if (s_gets(flower, SIZE))
{
strcat(flower, addon);
puts(flower);
puts(addon);
}
else
puts("End of file encountered!");
puts("bye");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
🎈(三)strncat()函数
strcat()函数无法检查第1个数组是否能容纳第2个字符串。如果分配给第1个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。,可以用strlen()查看第1个数组的长度,注意, 要给拼接后的字符串长度加1才够空间存放末尾的空字符。
或者,用 strncat(),该函数的第3 个参数指定了最大添加字符数。例如,strncat(bugs, addon, 13)将把 addon字符串的内容附加给bugs,在加到第13个字符或遇到空字符时停止。因此,算上空字符(无论哪种情况都要添加空字符),bugs数 组应该足够大,以容纳原始字符串(不包含空字符)、添加原始字符串在后面的13个字符和末尾的空字符。
/* join_chk.c -- 拼接两个字符串,检查第1个数组的大小 */
#include <stdio.h>
#include <string.h>
#define SIZE 30
#define BUGSIZE 13
char * s_gets(char * st, int n);
int main(void)
{
char flower[SIZE];
char addon [] = "s smell like old shoes.";
char bug[BUGSIZE];
int available;
puts("What is your favorite flower?");
s_gets(flower, SIZE);
if ((strlen(addon) + strlen(flower) + 1) <= SIZE)
strcat(flower, addon);
puts(flower);
puts("What is your favorite bug?");
s_gets(bug, BUGSIZE);
available = BUGSIZE - strlen(bug) - 1;
strncat(bug, addon, available);
puts(bug);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
strcat()和 gets()类似,也会导致缓冲区溢出。为 什么 C11 标准不废弃strcat(),只留下strncat()?为何对gets()那么残忍?这也许是因为gets()造成的安全隐患来自于使用该程序的人,而strcat()暴露的问题是那些粗心的程序员造成的。无法控制用户会进行什么操作,但是,可以控制你的程序做什么。C语言相信程序员,因此程序员有责任确保strcat()的使用安全。
🎈(四)strcmp()函数
1.基本用法
可以使用C标准库中的strcmp()函数(用于字符串比较)。该函数通过比较运算符来比较字符串,就像比较数字一样。如果两个字符串参数相同,该函数就返回0,否则返回非零值。
#include <stdio.h>
#include <string.h> // strcmp()函数的原型在该头文件中
#define ANSWER "Grant"
#define SIZE 40
char * s_gets(char * st, int n);
int main(void)
{
char try[SIZE];
puts("Who is buried in Grant's tomb?");
s_gets(try, SIZE);
while (strcmp(try, ANSWER) != 0)
{
puts("No, that's wrong. Try again.");
s_gets(try, SIZE);
}
puts("That's right!");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
- strcmp()函数比较的是字符串,不是整个数组,这是非常好的功能。虽然数组try占用了40字节,而储存在其中的"Grant"只占用了6字节(还有一个用来放空字符)
- strcmp()函数比较的是字符串,不是字符,所以其参数应该是字符串 (如"apples"和"A"),而不是字符(如’A’)
2.strcmp()的返回值
如果在字母表中第1个字符串位于第2个字符串前面,strcmp()中就返回负数;反之,strcmp()则返回正数1。其他系统可能返回2
strcmp()比较"A"和本身,返回0;
比较"A"和"B",返回-1;
比较"B"和"A",返回1
-
如果两个字符串开始的几个字符都相同会怎样?一般而言,strcmp()会依次比较每个字符,直到发现第 1 对不同的字符为止。然后,返回相应的值。
"apples"和"apple"只有最后一对字符不同("apples"的s和"apple"的空字符)。 //由于空字符在ASCII中排第1。字符s一定在它后面,所以strcmp()返回一个正数。
-
strcmp()比较所有的字符,不只是字母。
3.strncmp()函数
strcmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第3个参数指定的字符数。
例如,要查找以"astro"开头的字符串,可以限定函数只查找这5 个字符:
/* starsrch.c -- 使用 strncmp() */
#include <stdio.h>
#include <string.h>
#define LISTSIZE 6
int main()
{
const char * list[LISTSIZE] =
{
"astronomy", "astounding",
"astrophysics", "ostracize",
"asterism", "astrophobia"
};
int count = 0;
int i;
for (i = 0; i < LISTSIZE; i++)
if (strncmp(list[i], "astro", 5) == 0)
{
printf("Found: %s\n", list[i]);
count++;
}
printf("The list contained %d words beginning"
" with astro.\n", count);
return 0;
}
🎈(五)strcpy()和strncpy()函数
如果pts1和pts2都是指向字符串的指针,那么下面语句拷贝的是字符串的地址而不是字符串本身:
pts2 = pts1;
-
如果希望拷贝整个字符串,要使用strcpy()函数。
-
strcpy()接受两个字符串指针作为参数,第2个参数指向的字符串被拷贝至第1个参数指向的数组中,拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串,可以把指向源字符串的第2个指针声明为指针、数组名或字符串常量;而指向源字符串副本的第1个指针应指向一个数据对象(如,数组),且该对象有足够的空间储存源字符串的副本。
注意,strcpy()把源字符串中的空字符也拷贝在内
-
strcpy()的返回类型是 char *, 该函数返回的是第 1个参数的值,即一个字符的地址。
-
第 1 个参数不必指向数组的开始。这个属性可用于拷贝数组的一部分。
记住,声明数组将分配储存数据的空间,而声明指针只分配储存一个地址的空间。
strncpy()函数:
strcpy()和 strcat()都有同样的问题,它们都不能检查目标空间是否能容纳源字符串的副本。拷贝字符串用 strncpy()更安全,该函数的第 3 个参数指明可拷贝的最大字符数。
但是,strncpy()拷贝字符串的长度不会超过第三个参数(n),如果拷贝到第n个字符时还未拷贝完整个源字符串,就不会拷贝空 字符。所以,拷贝的副本中不一定有空字符。鉴于此,程序一般把 n 设置为比目标数组大小少1,然后把数组最后一个元素设置为空字符:
strncpy(qwords[i], temp, TARGSIZE - 1);
qwords[i][TARGSIZE - 1] = '\0';
🎈(六)sprintf()函数
sprintf()函数声明在stdio.h中,而不是在string.h中。该函数和printf()类似,但是它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素组合成一个字符串。
sprintf()的第1个参数是目标字符串的地址。其余参数和printf()相同,即格式字符串和待写入项的列表。
/* format.c -- 格式化字符串 */
#include <stdio.h>
#define MAX 20
char * s_gets(char * st, int n);
int main(void)
{
char first[MAX];
char last[MAX];
char formal[2 * MAX + 10];
double prize;
puts("Enter your first name:");
s_gets(first, MAX);
puts("Enter your last name:");
s_gets(last, MAX);
puts("Enter your prize money:");
scanf("%lf", &prize);
sprintf(formal, "%s, %-19s: $%6.2f\n", last, first, prize);
puts(formal);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}