初阶C语言(三)

四、数组

1.一维数组的创建和初始化

1.1数组的创建

数组是一组相同类型元素的集合
type_t arr_name [const_n]
//type_t 是指数组的元素类型
//arr_name 是数组的名字
//const_n 是一个常量表达式,用来指定数组的大小

在C99标准之前,数组的大小必须是常量或者常量表达式,在C99之后,数组的大小可以是变量,为了支持变长数组。

 

1.2数组的初始化

数组的初始化就是在创建数组的时候给数组一些合理的值

例如:
int arr[5] = { 1,2,3,4,5 };
我们创建了一个大小为5的数组,并在其中加入了1-5五个元素。
如果我们在此时只加入1,2,3三个元素(不完全初始化),剩余元素默认初始化为0.
在我们创建字符型数组时可以用字符型初始化:
char arr[3] = "12";
注意我们在用这种方法创建数组时,双引号里面的字符个数要小于指定的数组大小,因为字符型数组最后默认会有一个\0作为数组的结束标志。如果超过这个大小,此时再求数组长度会得到一个随机值。
另外我们也可以不指定数组大小直接输入元素,会默认根据我们的初始化内容来决定数组的大小。
int arr[] = {1,2,3};
char arr[] = "abc";
 

1.3一维数组的使用

一个数组在内存中是连续存储的,数组中每一个元素都会有一个下标,下标是从0开始id。我们可以通过下标直接引用元素。

printf("%d",arr[0]); //打印arr数组内第一个元素

我们可以通过sizeof函数来求一个数组的大小和长度。

            #include <stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5 };
    int n = sizeof(arr);
    int m = sizeof(arr) / sizeof(arr[0]);
    return 0;
}
            

上面代码我们定义一个数组并放入5个元素,因为一个int类型元素占用四个字节,所以n的大小为20,而sizeof(arr[0])求的是第一个元素的大小,
而每个元素的类型相同,故每个元素的大小相同,所以m为总大小除一个元素的大小得到元素长度。

 

2.二维数组的创建和初始化

2.1二维数组的创建

1 2 3 4
2 3 4 5
3 4 5 6

像上面就是一个二维数组,我们可以用C语言来实现。

int [3][4];

像这样我们就创建了一个三行四列的二维数组,前一个方括号表示多少行,后一个方括号表示多少列。

 

2.2二维数组的初始化

int arr[3][4] = {1,2,3,4,2,3,4,5,3,4,5,6,};

对二维数组初始化时我们可以不进行任何操作直接输入元素,因为我们定义了每行只有四个元素,所以到第四个元素时会自动换行,此时没有任何问题。当输入的元素不够时,剩余元素默认初始化为0.

如果我们想中间换行可以加一个分组

int arr[3][4] = {{1,2},{3,4},{5,6}};

此时1和2在同一行,3和4在同一行,5和6在同一行。

在创建一维数组的时候可以省路方框内的数组,但在二维数组中不能全部省略。我们只能省略多少行但不能省略多少列,因为如果规定了多少列我们输入数据时到了多少列以后会自动换行依次增加行数。但是如果省略了列数编译器不知道什么时候换行这时就成了一个一维数组。

 

2.3二维数组的使用

二维数组在引用的时候需要同时标明行数和列数,和一维数组一样行数和列数都是从0开始的。

遍历二维数组(三行四列)

#include <stdio.h>
int main()
{
    int arr[][4] = {1,2,3,4,2,3,4,5,3,4,5,6};
    int i = 0;
    int j = 0;
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
        {
            printf("%d  ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}
            

 

二维数组的存储

二维数组和一维数组一样也是在内存中连续存储的,即便有多行也是一块连续的内存。先存完第一行在存第二行……

 

3.数组越界

假设一个数组里面有n个元素,最大的下标为n-1,最小的下标为0,如果我们超出了这个范围就造成了越界。C语言本身不做数组下标的越界检查,编译器也不一定报错,所以编译器不报错不代表程序是对的。在写代码时要注意。

 

数组作为函数参数

在写代码时往往会将数组作为参数传个函数。

举个例子:冒泡排序

冒泡排序核心思想就是相邻元素比较。首先我们对第一个和第二个元素进行比较,然后将较大的元素往后移动,然后第二个元素和第三个元素进行比较,依次类推第一次排序我们就可以将最大的元素移到最后面。而比较次数为元素个数减一次。

在第二次排序时由于最大的元素已经放在最后面了,因此此次排序次数为上一次排序次数减一,排序方法和上次一样。

每次排序都会将一位最大的元素移到后面,因此需要排元素个数减一次。

#include <stdio.h>
void sort(int arr[],int sz)  //数组传参的时候有两种一种是指针一种是数组
{					  //这里用的是数组注意有[]
    int i = 0;
    int j = 0;
    int tmp = 0;
    for (i = 0; i < sz - 1; i++)  //排元素个数减一次
    {
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                tmp = arr[j];    //交换数据
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}
int main()
{
    int k = 0;
    int arr[] = { 9,8,7,6,5,4,3,2,1 };
    int sz = sizeof(arr) / sizeof(arr[0]);  //注意不能在函数内部求元素长度
    //冒泡排序对数组进行从小到大排序
    sort(arr,sz);
    for (k = 0; k < sz; k++)
    {
        printf("%d ", arr[k]);  //遍历数组
    }
    return 0;
}
            

上面代码中我们先定义一个数组,然后计算数组元素个数,将数组和元素个数传到函数中。之后进入第一个for循环,循环次数为n-1次,然后是第二个循环。第二个循环次数中sz-1的意思是所有元素两两比较需要比较sz-1次,后面-i的意思是每一次排序都会有一个最大值移到后面,而下一次排序便可以不用再去和这个元素比较,而i次代表i次循环也就意味着i个元素不再需要比较。进入第二个for循环就是比较相邻元素的两个值,如果前面的值大于后面的则交换。最后进入main主函数遍历数组。

 

5.数组名是什么

一维数组名

数组名可以表示首元素地址,但是有两个例外
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

验证:

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int l = sizeof(arr);
    printf("%p\n", arr);
    printf("%p\n", arr+1);
    printf("\n");
    printf("%p\n", &arr[0]);
    printf("%p\n", &arr[0] + 1);
    printf("\n");
    printf("%p\n", &arr);
    printf("%p\n", &arr + 1);
    printf("\n");
    printf("%d\n", l);
    return 0;
}
            

运行结果为

首先看第一组和第二组输出结果相同,并且加一后都加了四个字节,因此可以判断数组名可以表示数组首元素地址。在最后一组输出结果中我们看到sizeof数组的输出结果为40(整个数组的大小),所以数组名在这里表示整个数组的大小。在第三组数中的第一个输出结果和第一组第二组相同,但是第二个输出结果增加了一整个数组的长度,因此可以断定这里表示整个数组的大小。

 

二维数组名

二维数组的数组名和一维数组名同理,但是有一点不一样,二位数组虽然能表示首元素的地址,但是它表示的是第一行全部的地址。比如有一个三行四列的二维数组,他的数组名表示的就是第一行四个元素的地址。

因此可以有以下计算

计算有多少行

sizeof(arr)/sizeof(arr[0]) //后者表示第一行的大小

计算有多少列

sizeof(arr[0])/sizeof(arr[0][0]) //后组合表示0行0列的元素大小
 

五、指针

1.指针是什么

指针是内存中一个最小单元的编号,也就是地址

平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

内存

内存就是我们常说的运行空间(8G,16G),而内存的最小空间单元为1byte(字节),而内存中有很多很多字节,为了读取方便,计算机把每个字节都进行编号,这个编号就是指针。

 

而我们口语常说的指针为指针变量,就是用来存放指针的变量。

int* pa = &a;

上面代码就是把a的地址存到pa中,pa是一个指针变量。

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节.

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

 

2.指针和指针类型

C语言中有很多指针类型,像下面的代码输出结果全为8字节(X64环境,如果是X86-32位环境输出结果都为4字节。那为什么C语言不统一一个指针类型定义指针变量呢?

#include <stdio.h>
int main()
{
    char* ch = 0;
    int* num = 0;
    float* ac = 0;
    double* lc = 0;

    printf("%zu\n", sizeof(ch));  //%zu是专门打印sizeof的格式字符
    printf("%zu\n", sizeof(num));
    printf("%zu\n", sizeof(ac));
    printf("%zu\n", sizeof(lc));
    return 0;
}
    

我们尝试对不同的指针类型解引用操作尝试。

#include <stdio.h>
int main()
{
    int a = 0x11223344;  //定义一个整型变量a存储十六进制数字,由于十六进制一位数字相当于四位二进制数字,
                        //一共八位数字,三十二位二进制刚好三十二比特四字节
    int* pa = &a;       //定义一个整型指针变量存储a的地址
    *pa = 0;		//对pa解引用,将pa的值设为零
    return 0;			
}
    

对pa内存监视发现将pa解引用赋值以后,其四个字节内的数值全部变为0


 

现在将指针类型换成字符型指针类型

#include <stdio.h>
int main()
{
    int a = 0x11223344;  
    int* pa = &a;
    char* pc = (char*)pa;  //因为char和int的指针类型都是八个字节(x64)所以可以将int指针类型转化为char指针类型
                            //这里可以不用加(char*)强制转化,但为了严谨避免警告加上
    *pc = 0;		//对pa解引用,将pa的值设为零
    return 0;			
}
    

对pc内存监视为

这时我们发现只有第一个字节的数值变为了0.因此我们得到结论:

如果是int*指针,解引用访问4个字节

如果是char*指针,解引用访问1个字节

 

我们接着运行下面的代码。

#include <stdio.h>
int main()
{
    int a = 0x11223344;
    int* pa = &a;
    char* pc = (char*)pa;
    printf("%p\n", pa);
    printf("%p\n", pa + 1);
    printf("%p\n", pc);
    printf("%p\n", pc + 1);
    return 0;
}
    

输出结果为

这时发现int*指针加一后增加了四个字节,而char*指针加一后只增加了一个字节,因此可以得到第二个结论。

指针的类型决定了指针+-1操作的时候跳过几个字节。

 

int*指针和float*指针解引用虽然都是访问四个字节,+-1操作也都是跳过四个字节,但是不能通用,因为由他们定义的指针变量会认为自己存储的是定义他们指针的数据类型,并且对两者的指针变量解引用赋值存储方式也不同。

 

3.野指针

概念:|野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

3.1野指针成因

非法访问内存

int* p

1.p没有初始化就意味着没有明确的指向,一个局部变量不初始化放的是随机值。

*p = 10;

由于局部变量未初始化,对p解引用的地址是随机值,此时就构成了非法访问内存。

 

指针越界访问

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int* p = arr;    //数组名代表数组首元素地址
    int i = 0;
    for (i = 0; i <= 10; i++)
    {
        *p = i;
        p++;
    }
    return 0;
}
    

上面代码中定义一个长度为10的整型数组,然后将数组首元素地址的指针给p。进入for循环每次循环都把i的值赋给p的当前地址然后p跳到下一个元素的地址(因为int型每个元素大小为四个字节,int*指针+1也是跳过四个字节,所以可以刚好跳转到下一个元素的地址)。到最后p的值为10超出了元素的下标,此时就构成了指针的越界访问变成野指针。

 
int* p = NULL;

注意:当指针变量为空时(NULL),指针不能进行解引用操作。

指针指向的空间释放

#include <stdio.h>
int* test()
{
    int a = 10;
    int* b = &a;
    return b;
}
int main()
{
    int* p = test();
    return 0;
}
    

上面代码运行没有任何问题,但是p为野指针。首先建立一个指针变量,而指针变量的值为test函数的返回值,在test函数内部,定义一个整型变量a,然后将a的地址存到指针变量b中,再然后将b的值返回。现在p的值就是a的地址,但是a是在函数内部函数结束后a就被销毁了这个地址不能随意改动,所以此时的p为野指针。

 

4.指针运算

4.1指针+-整数

前面已经介绍过指针+-整数,再举个小“栗”子

#define N_VALUES 5    //定义一个常量的值为5
float value[N_VALUES];
float *vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
    *vp++ = 0;
}
    

首先定义一个值为五的常量,并根据常量定义一个数组,接着定义一个float*指针变量并且没有赋值(注意此时没有赋值指针变量里面存储的是随机内存,但是没有对其解引用,所以不是野指针)。进入循环,vp初始值为数组的首元素地址,循环条件为vp小于数组第五个下标的地址。由上图能看到,最大的下标为4,因此下标为5超出了数组的范围。进入循环内,先对vp解引用赋值0,然后vp自增1.最后结果为数组内部所有元素的值为0。

 

4.2指针-指针

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%d\n", &arr[9] - &arr[0]);
    return 0;
}
    

在上面代码中输出数组第九个下标的地址减去第零个下标的地址,输出结果为9.当两者交换时输出结果为-9,由此得到结论:

指针-指针得到的结果为元素的个数。(与数据类型等无关)指向同一块空间的两个指针才能相加减。

 

指针+指针相当于日历中的日期加日期,这没有什么意义,所以没有什么作用。

 

4.3指针的关系运算

在4.1的代码其实就是一个指针的关系运算

4.1代码中的for循坏就是用指针比较作为判断条件的。

注意:标准规定允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。


 

5.二级指针

二级指针就是存放一级指针地址的指针。

#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    return 0;
}
    

上面代码定义一个整型变量a,然后将a的地址存放到指针变量pa中,此时pa也叫做一级指针。因为pa也是个变量,所以也需要占用一定的空间,因此我们也可以将pa的地址存到一个指针变量中,这个变量就叫做二级指针。

#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;    //定义二级指针
    return 0;
}
    

对于二级指针进行一次解引用是a的地址,两次解引用才是a的值。

**pa = 10; //两次解引用对a赋值
 

int a = 10;
int* pa = &a;
int** ppa = &pa;

在上面一级指针,int和*要分开理解。*表示pa是个指针,int表示pa这个指针指向的a是个int型。

二级指针中第二个*表示ppa是指针,前面的int*表示ppa指向的pa是int*型。

 

6.指针数组

存放指针的数组就是指针数组

#include <stdio.h>
int main()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int arr[10] = { 1, 2, 3 };
    int* pa = &a;
    int* pb = &b;
    int* pc = &c;
    int* parr[10] = { &a, &b, &c };
    return 0;
}
    

在定义变量时我们可以用数组来批量定义,同样我们也可以用数组批量定义指针。在上面代码中我们用int* parr[10]定义了一个整型指针数组并且在里面存放了a,b,c三个变量的地址。指针数组和数组一样,里面存储的数据类型都是相同的。

同样我们可以对指针数组进行解引用操作访问指向元素的数值。

#include <stdio.h>
int main()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int arr[10] = { a, b, c };
    int* pa = &a;
    int* pb = &b;
    int* pc = &c;
    int* parr[10] = { &a, &b, &c };
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d\n", *parr[i]);
    }
    return 0;
}
    

上面代码中添加了一个for循环,并通过对数组的解引用操作遍历指针数组指向元素的数值。

 

指针数组还可以模拟二维数组

在学习二维数组时,用下面代码定义二维数组并且遍历二维数组。

#include <stdio.h>
int main()
{
    int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
    int i = 0;
    int j = 0;
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}
    

 

我们可以用指针数组达到类似的效果

#include <stdio.h>
int main()
{
    int arr1[4] = { 1,2,3,4 };
    int arr2[4] = { 2,3,4,5 };
    int arr3[4] = { 3,4,5,6 };

    int* parr[3] = { arr1,arr2,arr3 };
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 4; j++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}
    

首先定义三个数组,然后定义一个指针数组,将三个数组名存到指针数组中。因为数组名可以表示数组首元素的地址,所以代码可以正常运行。之后用for循环遍历数组。指针数组长度为3所以i<3,每个数组的长度为4所以j<4,打印元素时parr[i][j]中的i表示指向三个数组的那个数组,确认那个数组以后用j指向这个数组的某个元素。

 

六、结构体

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.结构体的声明

struct tag
{
    member - list;
}variable-list;
        

struct是结构体关键字,tag是一个标签,表示这个结构体叫什么名字(可以替换)。member – list是成员列表可以有多个,variable-list是结构变量,定义在结构的末尾,最后一个分号之前。

 

举个例子,用结构体描述一个人

struct Peo
{
    char name[20];    //定义一个名字,最大长度为20
    char tele[12];    //定义电话号码,最大长度为12
    char sex[5];    //定义性别,最大长度为5
    int high;    //定义身高,注意这和上面不同,上面是数组,这是变量,最大长度不超过四个字节。
}p1,p2;    //我们定义了一个结构类型,这时可以在创建好结构类型后直接定义这个类型的变量,若创建多个变量用“,”分隔。
        

p1和p2是两个结构体变量,注意是全局变量。

如果没有在创建结构体之后创建结构体变量,可以用下面语句创建。

struct Peo p1 = { 0 };
 

1.1结构成员的类型

结构成员可以是标量、数组、指针、甚至是其它结构体。

 

1.2结构体变量的初始化

#include <stdio.h>
int main()
{
    struct Peo p1 = {"张三","15055555555","男",181};
    return 0;
}
        

以上面创建的结构类型进行结构体变量初始化,初始化的大括号中按照结构体中成员列表的顺序依次赋值。前三个是char型数组用””引起来,后面是整型变量。

如果此时再加一个结构体并进行嵌套

struct Peo
{
    char name[20];    
    char tele[12];   
    char sex[5];    
    int high;    
};
struct Stu
{
    struct Peo p;
    int num;
};
        

此时如果想对Stu的结构类型变量初始化需要两个大括号,然后根据成员列表进行赋值。

struct Stu s = { {"张三","15055555555","男",181}, 5 }
 

2.结构体成员的访问

创建好结构体变量以后,下面进行打印操作。


printf("%s %s %s %d\n",p1.name, p1.tele, p1.sex, p1.high);
printf("%s %s %s %d %d\name",s.p.name, s.p.tele s.p.sex, s.p.high, s.num ); //嵌套结构类型变量打印

 

.结构体变量访问成员

->结构体指针访问成员

 

#include <stdio.h>
struct Peo
{
    char name[20];    
    char tele[12];   
    char sex[5];    
    int high;    
};
void print1(struct Peo p)
{
    printf("%s %s %s %d\n", p.name, p.tele, p.sex, p.high);
}
void print2(struct Peo* p)
{
    printf("%s %s %s %d\n", p->name, p->tele, p->sex, p->high);
}
int main()
{
    struct Peo p1 = { "张三","15055555555","男",181 };
    print1(p1);
    print2(&p1);
    return 0;
}
        

上面代码中,print1将结构体变量p1传到函数中的p,然后用.访问变量里面的成员依次打印。print2将结构体变量的地址传到函数内的p中,然后用->访问成员依次打印。

 

3.结构体传参

在上面代码中同样也是一个结构体传参的例子。创建好结构体变量以后有两种方式进行传参,第一种是将p拷贝到函数变量中,这种方法一个缺点是如果结构体变量内存很大会造成很大的内存浪费。第二种是将结构体变量的地址传到函数指针中,这种方法同样可以访问成员变量。

结构体传参的时候,尽量传结构体的地址。

评论

  1. 一桐
    已编辑
    10 月前
    2023-11-16 22:01:57

    受益很大

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇