友情链接:专栏地址


文章目录


🚀数组

⛳一、什么是数组

  1. 数组是一组有序数据的集合。数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。
  2. 用一个数组名(如s)和下标(如[15])来唯一地确定数组中的元素,数组元素的编号从0开始,例如candy[0]表示candy数组的第1个元素,candy[364]表示第365个元素
  3. 数组中的每一个元素都属于同一个数据类型。不能把不同类型的数据(如学生的成绩和学生的性别)放在同一个数组中。

由于计算机键盘只能输入有限的单个字符而无法表示上下标,C语言规定用方括号中的数字来表示下标,如s[15],将数组与循环结合起来,可以有效地处理大批量的数据,大大提高了工作效率,十分方便。

⛳二、一维数组

🎈(一)一维数组的定义

定义

要使用数组,必须在程序中先定义数组,即通知计算机:由哪些数据组成数组,数组中有多少元素,属于哪个数据类型。否则计算机不会自动地把一批数据作为数组处理。

定义一维数组的一般形式为

类型说明符 数组名[常量表达式];

例如:

char name[40];

name后面的方括号表明这是一个数组,方括号中的40表明该数组中的元素数量。char表明每个元素的类型。

【跟着陈七一起学C语言】今天总结:C语言的数组和指针-LMLPHP

  1. 数组名的命名规则和变量名相同,遵循标识符命名规则。
  2. 在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。例如,指定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]。
  3. 常量表达式中可以包括常量和符号常量,如"int a[3+5];”是合法的。不能包含变量,如“int a[n];”是不合法的。也就是说,C语言不允许对数组的大小作动态定义,即数组的大小不依赖于程序运行过程中变量的值。这是我们最常见的说法,不过在以下第二点拓展中可以看见有关VLA的不一样的点
  4. 在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。在C标准中,使用越界下标的结果是未定义的,使用越界的数组下标会导致程序改变其他变量的值。 不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。

🎈(二)一维数组的初始化

为了使程序简洁,常在定义数组的同时给各数组元素赋值,这称为数组的初始化。可以用“初始化列表”方法实现数组的初始化。使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前, 必须先给它们赋初值。

  1. 在定义数组时对全部数组元素赋予初值。

  2. 可以只给数组中的一部分元素赋值:

    如果在定义数值型数组时,指定了数组的长度并对之初始化,凡未被“初始化列表”指定初始化的数组元素,系统会自动把它们初始化为0(如果是字符型数组,则初始化为’\o’,如果是指针型数组,则初始化为NULL,即空指针)。

  3. 如果想使一个数组中全部元素值为0,可以写成

    int a[10]={0,0,0,0,0,0,0,0,0,0};int a[10]={0};
    //未赋值的部分元素自动设定为О
    
  4. 在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组长度,让编译器自动匹配数组大小和初始化列表中的项数,例如:

    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列形式)表示二维数组,能形象地表示出行列关系。但实际在内存中,各元素是连续存放的,不是二维的,是线性的。

【跟着陈七一起学C语言】今天总结:C语言的数组和指针-LMLPHP

🎈(二)二维数组的初始化

二维数组同样需要初始化,如果不初始化,它的值可能是随机的(全局变量会初始化为 0,局部变量值随机)

  1. 分行给二维数组赋初值。

    int a[3][4]={
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    };
    

    如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是这并不会影响其他行的初始化。

  2. 可以将所有数据写在一个花括号内,按数组元素在内存中的排列顺序对各元素赋初值。例如:

    int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
    

【跟着陈七一起学C语言】今天总结:C语言的数组和指针-LMLPHP

  1. 可以对部分元素赋初值。例如:

    int a[3][4]={{1}{5}{9}};
    

    它的作用是对各行第1列(即序号为0的列)的元素赋初值,其余元素值自动为0。

  2. 如果对全部元素都赋初值(即提供全部初始数据),则定义数组时对第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重嵌套循环。对于其他多维数组,以此类推。

【跟着陈七一起学C语言】今天总结:C语言的数组和指针-LMLPHP

⛳五、变长数组(VLA)

C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。如下所示:

int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // 一个变长数组(VLA)
  1. 变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern 存储类别说明符
  2. 不能在定义(声明)中初始化它们。最终, C11把变长数组作为一个可选特性,而不是必须强制实现的特性。

注意:变长数组不能改变大小,变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用 变量指定数组的维度。

🚀指针

⛳一、什么是指针

🎈(一)概念

指针(pointer)是 C 语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。前面使用的scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。

由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。打个比方,一个房间的门口挂了一个房间号2008,这个2008就是房间的地址,或者说,2008“指向”该房间。因此,将地址形象化地称为“指针”。意思是通过它能找到以它为地址的内存单元。

  1. 指针本身也是一个变量,称为指针变量
  2. 32 位系统中,int 整数占 4 个字节,指针同样占 4 个字节, 64 位系统中,int 整数占 4 个字节,指针同样占 8 个字节
  3. 指针变量的值是一个内存地址
  4. 指针是一个地址,而指针变量是存放地址的变量
  5. 简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。

🎈(二)&运算符

一元&运算符给出变量的存储地址。如果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; //强烈推荐

空指针的使用:

  1. 当我们需要一个指针变量,但暂时不需要用到它,可以将指针初始化为空指针,目的就是避免访问非法数据。

     int *select = NULL; 
    
  2. 指针不再使用时,可以设置为空指针

    int *select = &xiao_long_lv; //和小龙女约会 
    select = NULL;
    
  3. 表示这个指针还没有具体的指向,使用前进行合法性判断

    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–则相反

🎈(五)指针与指针之间的加减运算

  1. 指针和指针可以做减法操作,但不适合做加法运算;

  2. 指针和指针做减法适用的场合:

    • 两个指针都指向同一个数组,相减结果为两个指针之间的元素数目,而不是两个指针之间相差的字节数。即差值的单位与数组类型的单位相同。例如,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 个。如果两个指针不是指向同一个数组,它们相减就没有意义。 
      
  3. 不同类型的指针不允许相减,比如以下相减是没有意义的

    char *p1; 
    int *p2; 
    p2-p1;
    

🎈(六)比较两个指针

使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

两个指针变量之间比大小,其实是比较指向的地址谁大谁小以及为空或者不为空,而不是地址中保存的值,一般也只用于数组当中,指向一个数组前面元素的指针小于指向后面元素的指针,如果是单纯的两个不相关的指针进行比较,一般编译不通过

⛳七、二级指针和多级指针

二级指针也是一个普通的指针变量,只是它里面保存的值是另外一个一级指针的地址

定义:

int guizi1 = 888; 
int *guizi2 = &guizi1; //1 级指针,保存 guizi1 的地址 
int **liujian = &guizi2; //2 级指针,保存 guizi2 的地址,guizi2 本身是一个一级指针变

【跟着陈七一起学C语言】今天总结:C语言的数组和指针-LMLPHP

二级指针的用途:

  1. 普通指针可以将变量通过参数“带入”函数内部,但没办法将内部变量“带出”函数

  2. 二级指针不但可以将变量通过参数函数内部,也可以将函数内部变量 “带出”到函数外部

    // 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; //四级指向三级 
// 有完没完。。。

🚀动态内存分配与指向它的指针变量

我们前面讨论的存储类别有一个共同之处:在确定用哪种存储类别后, 根据已制定好的内存管理规则,将自动选择其作用域和存储期。然而,还有更灵活地选择,即用库函数分配和管理内存。

⛳一、为什么要使用动态内存

  1. 按需分配,根据需要分配内存,不浪费

  2. 被调用函数之外需要使用被调用函数内部的指针对应的地址空间

  3. 突破栈区的限制,可以给程序分配更多的内存

⛳二、malloc()和free()

函数原型:

void *malloc(long NumBytes)

该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。(关于分配失败的原因,应该有多种,比如说空间不足就是一种。)

void free(void *FirstByte)

该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。

注意:

  1. 申请了内存空间后,必须检查是否分配成功。
  2. 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
  3. 这两个函数应该是配对。ree()函数的参数是之前malloc()返回的地址,如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
  4. 虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
  5. 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. 第1次调用gobble()时,它创建了指针temp,并调用malloc()分配了16000 字节的内存(假设double为8 字节)。假设如代码注释所示,遗漏了free()。 当函数结束时,作为自动变量的指针temp也会消失。但是它所指向的16000 字节的内存却仍然存在。由于temp指针已被销毁,所以无法访问这块内存,
  2. 它也不能被重复使用,因为代码中没有调用free()释放这块内存。
  3. 第2次调用gobble()时,它又创建了指针temp,并调用malloc()分配了 16000字节的内存。第1次分配的16000字节内存已不可用,所以malloc()分配 了另外一块16000字节的内存。当函数结束时,该内存块也无法被再访问和再使用。
  4. 循环要执行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';
}
  1. 该函数要改变字符串,所以函数头在声明形式参数string时没有使用 const限定符。
  2. 一些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;
}
  1. strcmp()函数比较的是字符串,不是整个数组,这是非常好的功能。虽然数组try占用了40字节,而储存在其中的"Grant"只占用了6字节(还有一个用来放空字符)
  2. strcmp()函数比较的是字符串,不是字符,所以其参数应该是字符串 (如"apples"和"A"),而不是字符(如’A’)

2.strcmp()的返回值

如果在字母表中第1个字符串位于第2个字符串前面,strcmp()中就返回负数;反之,strcmp()则返回正数1。其他系统可能返回2

strcmp()比较"A"和本身,返回0;
比较"A""B",返回-1;
比较"B""A",返回1
  1. 如果两个字符串开始的几个字符都相同会怎样?一般而言,strcmp()会依次比较每个字符,直到发现第 1 对不同的字符为止。然后,返回相应的值。

    "apples""apple"只有最后一对字符不同("apples"的s和"apple"的空字符)。
    //由于空字符在ASCII中排第1。字符s一定在它后面,所以strcmp()返回一个正数。
    
  2. 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;
  1. 如果希望拷贝整个字符串,要使用strcpy()函数。

  2. strcpy()接受两个字符串指针作为参数,第2个参数指向的字符串被拷贝至第1个参数指向的数组中,拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串,可以把指向源字符串的第2个指针声明为指针、数组名或字符串常量;而指向源字符串副本的第1个指针应指向一个数据对象(如,数组),且该对象有足够的空间储存源字符串的副本。

  3. strcpy()的返回类型是 char *, 该函数返回的是第 1个参数的值,即一个字符的地址。

  4. 第 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;
}


05-09 21:22