友情链接:专栏地址
🚀数组
⛳一、什么是数组
- 数组是一组有序数据的集合。数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。
- 用一个数组名(如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标准中,使用越界下标的结果是未定义的,使用越界的数组下标会导致程序改变其他变量的值。 不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。
🎈(二)一维数组的初始化
为了使程序简洁,常在定义数组的同时给各数组元素赋值,这称为数组的初始化。可以用“初始化列表”方法实现数组的初始化。使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前, 必须先给它们赋初值。
-
在定义数组时对全部数组元素赋予初值。
-
可以只给数组中的一部分元素赋值:
如果在定义数值型数组时,指定了数组的长度并对之初始化,凡未被“初始化列表”指定初始化的数组元素,系统会自动把它们初始化为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.通过循环可以给数组的所有元素赋值
声明数组后,可以借助数组下标(或索引)给数组元素赋值,
/* 给数组的元素赋值 */
#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]);
...
⛳三、二维数组
二维数组,就是指含有多个数组的数组!如果把一维数组理解为一行数据,那么,二维数组可形象地表示为行列结构。
🎈(一)二维数组的定义
二维数组定义的一般形式为
类型说明符 数组名[常量表达式][常量表达式];
和一维数组一样,需要先定义,再使用。
//一行女兵 以一维数组的形式表示
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把变长数组作为一个可选特性,而不是必须强制实现的特性。
注意:变长数组不能改变大小,变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用 变量指定数组的维度。
🚀指针
⛳一、什么是指针
🎈(一)概念
指针(pointer)是 C 语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。前面使用的scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。打个比方,一个房间的门口挂了一个房间号2008,这个2008就是房间的地址,或者说,2008“指向”该房间。因此,将地址形象化地称为“指针”。意思是通过它能找到以它为地址的内存单元。
- 指针本身也是一个变量,称为指针变量
- 32 位系统中,int 整数占 4 个字节,指针同样占 4 个字节, 64 位系统中,int 整数占 4 个字节,指针同样占 8 个字节
- 指针变量的值是一个内存地址
- 指针是一个地址,而指针变量是存放地址的变量
- 简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。
🎈(二)&运算符
一元&运算符给出变量的存储地址。如果pooh是变量名,那么&pooh是变量的地址。可以把地址看作是变量在内存中的位置。
例如:
ptr = &pooh; // 把pooh的地址赋给ptr
对于这条语句,我们说ptr“指向”pooh。ptr和&pooh的区别是ptr是变量, 而&pooh是常量。或者,ptr是可修改的左值,而&pooh是右值。
🎈(三)*间接运算符
使用间接运算符*(indirection operator)后跟一个指针名或地址时,*给出储存在指针指向地址上的值。该运算符有时也称为解引用运算符(dereferencing operator)。
例如:
val = *ptr; // 找出ptr指向的值
⛳二、指针变量的定义与引用
定义指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,
1.定义指针变量的一般形式为:
类型名 * 指针变量名;
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。
//定义指针变量
int *p; // int *p1, *p2;
或者
int* p; // int* p1,p2; //p1 是指针, p2 只是整形变量
或者
int * p;
或者
int*p;//不建议
2.引用指针变量
即使用*运算符访问储存在指针指向地址上的值,前提是已执行,例如:“p=&a;” 即指针变量p指向了整型变量a,
//其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值。
printf("%d",*p);
⛳三、指针的初始化
int room = 2;
//定义两个指针变量指向 room
int *p1 = &room;
int *p2 = &room;
⛳四、Void类型指针,空指针与坏指针
Void类型指针:
空类型指针只存储地址的值,丢失类型,无法访问,要访问其值,我们必须对这个指针做出正确的类型转换,然后再间接引用指针。 所有其它类型的指针都可以隐式自动转换成 void 类型指针,反之需要强制转换
该类型的指针相当于一个“通用指针”,把指向 void 的指针赋给任意类型的指针完全不用考虑类型匹配的问题。强制转换一下即可
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;
🎈(二)指针常量
const还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,但是可以修改指针指向地址的值
int * const nuan_nan = &wife;
*nuan_nan = 26;
printf("暖男老婆的年龄:%d\n", wife);
//nuan_nan = &girl; //不允许指向别的地址
可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。
⛳六、指针操作
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])的地址。
解引用:*运算符给出指针指向地址上储存的值。
*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–则相反
🎈(五)指针与指针之间的加减运算
-
指针和指针可以做减法操作,但不适合做加法运算;
-
指针和指针做减法适用的场合:
-
两个指针都指向同一个数组,相减结果为两个指针之间的元素数目,而不是两个指针之间相差的字节数。即差值的单位与数组类型的单位相同。例如,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 头文件中。
到底从哪获得空间,释放的是什么:
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类型数组。
⛳一、字符串的定义、初始化与字符串元素的引用
定义字符串,采用定义字符数组的形式,用来存放字符数据的数组是字符数组。在字符数组中的一个元素内存放一个字符。定义字符数组的方法与定义数值型数组的方法类似。例如:
char c[10];
(一)定义字符串时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组储存字符串,在下面的声明中,用指定的字符串初始化数组 m1:
const char m1[40] = "Limit yourself to one line's worth.";
(二)通常,让编译器确定数组的大小很方便。回忆一下,省略数组初始化声明中的大小,编译器会自动计算数组的大小:
const char m2[] = "If you can't think of anything, fake it.";
⛳二、字符串和字符串结束标志
在实际工作中,人们关心的往往是字符串的有效长度而不是字符数组的长度。例如,定义一个字符数组长度为100,而实际有效字符只有40个。为了测定字符串的实际长度,C语言规定了一个“字符串结束标志”,以字符’\0’作为结束标志。如果字符数组中存有若干字符,前面9个字符都不是空字符(‘\0’) ,而第10个字符是’\0’ ,则认为数组中有一个字符串,其有效字符为9个。也就是说,在遇到字符’\0’时,表示字符串结束,把它前面的字符组成一个字符串。
说明:字符数组并不要求它的最一个字符为\0’,甚至可以不包含’\0’。像以下这样写完全是合法的:
char c[5]={'C' ,'h',' i' ,' n' ,'a'} ;
是否需要加’\0’,完全根据需要决定。由于系统在处理字符串常量存储时会自动加一个’0’,因此,为了使处理方法一致,便于测定字符串的实际长度,以及在程序中作相应的处理,在字符数组中也常常人为地加上一个’\0’。例如:
char c[6]={ 'C',' h' ,' i' ,'n',' a' ,'\o'};
这样做,便于引用字符数组中的字符串。
⛳三、字符串的输入和输出
🎈(一)字符串输入
1.gets()函数
在读取字符串时,scanf()和转换说明%s只能读取一个单词。可是在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。
gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。它经常和 puts()函数配对使用,
char words[5];
get(words);
put(words);
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; }
(2)gets_s()函数
C11新增的gets_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。
- gets_s()只从标准输入中读取数据,所以不需要第3个参数。
- 如果gets_s()读到换行符,会丢弃它而不是储存它。
- 如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
(3)scanf()函数
scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾: scanf()更像是“获取单词”函数,而不是“获取字符串”函数
- 如果预留的存储区装得下输入行,gets()和fgets()会读取第1个换行符之前所有的字符。
- scanf()函数有两种方法确定输入结束。无论哪种方法,都从第1个非空白字符作为字符串的开始。如果使用%s转换说明,以下一个空白字符(空行、 空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10 个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)
🎈(二)字符串输出
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()不会在输出的末尾添加换行符。
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;
}
🎈(四)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()的返回类型是 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;
}