C语言零基础入门级初阶数组+指针+面试题全讲解【系统学习第三天】

在这里插入图片描述

【1】C语言-》数组的基本概念

逻辑:一次性定义多个相同类型的变量,并存储到一片连续的内存中
示例:

int a[5];

语法释义:

a 是数组名,即这片连续内存的名称 [5] 代表这片连续内存总共分成5个相等的格子,每个格子称为数组的元素 int
代表每个元素的类型,可以是任意基本类型,也可以是组合类型,甚至可以是数组

初始化:在定义的时候赋值,称为初始化

// 正常初始化
int a[5] = {
    
    100,200,300,400,500};

int a[5] = {
    
    100,200,300,400,500,600}; // 错误,越界了
int a[ ] = {
    
    100,200,300}; // OK,自动根据初始化列表分配数组元素个数
int a[5] = {
    
    100,200,300}; // OK,只初始化数组元素的一部分

在这里插入图片描述

【2】C语言-》数组元素的引用

存储模式:一片连续的内存,按数据类型分割成若干相同大小的格子
元素下标:数组开头位置的偏移量
在这里插入图片描述
示例:

int a[5]; // 有效的下标范围是 0 ~ 4
a[0] = 1;
a[1] = 66;
a[2] = 21;
a[3] = 4;
a[4] = 934;

a[5] = 62; // 错误,越界了
a    = 10; // 错误,不可对数组名赋值 

【3】C语言-》字符数组的引用

概念:专门存放字符的数组,称为字符数组
初始化与元素引用

char s1[5] = {
    
    'a', 'b', 'c', 'd', 'e'};       // s1存放的是字符序列,非字符串
char s2[6] = {
    
    'a', 'b', 'c', 'd', 'e', '\0'}; // s2存放了一个字符串

char s[6] = {
    
    "abcde"}; // 使用字符串直接初始化字符数组
char s[6] =  "abcde" ; // 大括号可以省略

s[0] = 'A'; // 索引第一个元素,赋值为 'A'

【4】C语言-》多维数组的引用

概念:若数组元素类型也是数组,则该数组称为多维数组
示例:

int a[2][3];
// 代码释义:
// 1, a[2]   是数组的定义,表示该数组拥有两个元素
// 2, int [3]是元素的类型,表示该数组元素是一个具有三个元素的整型数组

在这里插入图片描述

多维数组的语法跟普通的一维数组语法完全一致

初始化

int a[2][3] = {
    
    {
    
    1,2,3}, {
    
    4,5,6}}; // 数组的元素是另一个数组

int a[2][3] = {
    
    {
    
    1,2,3}, {
    
    4,5,6}, {
    
    7,8,9}}; // 错误,越界了
int a[2][3] = {
    
    {
    
    1,2,3}, {
    
    4,5,6,7}};        // 错误,越界了

int a[ ][3] = {
    
    {
    
    1,2,3}, {
    
    4,5,6}}; // OK,自动根据初始化列表分配数组元素个数
int a[2][3] = {
    
    {
    
    1,2,3}};          // OK,只初始化数组元素的一部分

元素引用:

// a[0] 代表第一个元素,这个元素是一个具有 3 个元素的数组:{1,2,3}
// a[1] 代表第二个元素,这个元素也是一个具有 3 个元素的数组:{4,5,6}

printf("%d", a[0][0]); // 输出第一个数组的第一个元素,即1
printf("%d", a[1][2]); // 输出第二个数组的第三个元素,即6

在这里插入图片描述

【5】C语言-》数组万能拆解法

任意的数组,不管有多复杂,其定义都由两部分组成。
第1部分:说明元素的类型,可以是任意的类型(除了函数)
第1部分:说明数组名和元素个数

在这里插入图片描述
示例:

int   a[4];       // 第2部分:a[4]; 第1部分:int
int   b[3][4];    // 第2部分:b[3]; 第1部分:int [4]
int   c[2][3][4]; // 第2部分:c[2]; 第1部分:int [3][4]
//数组指针,叫法根据优先级。优先级  [] > *
int  *d[6];       // 第2部分:d[6]; 第1部分:int *
//指针数组函数  值是函数地址,就是数组的元素指向函数地址
int (*e[7])(int, float); // 第2部分:e[7]; 第1部分:int (*)(int, float)

注解:

上述示例中,a[4]、b[3]、c[2]、d[6]、e[7]本质上并无区别,它们均是数组
上述示例中,a[4]、b[3]、c[2]、d[6]、e[7]唯一的不同,是它们所存放的元素的不同
第1部分的声明语句,如果由多个单词组成,C语言规定需要将其拆散写到第2部分的两边

【6】C语言-》内存地址的引入

字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址

在这里插入图片描述

【7】C语言-》基地址的图讲解

单字节数据:对于单字节数据而言,其地址就是其字节编号。
多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址。

在这里插入图片描述

【8】C语言-》取址符&的引用

每个变量都是一块内存,都可以通过取址符 & 获取其地址
例如:

int a = 100;
printf("整型变量 a 的地址是: %p\n", &a);

char c = 'x';
printf("字符变量 c 的地址是: %p\n", &c);

double f = 3.14;
printf("浮点变量 f 的地址是: %p\n", &f);

注意:

虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸确实一样的。
不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。

【9】C语言-》指针概念的引入

指针的概念:

地址。比如 &a 是一个地址,也是一个指针,&a 指向变量 a。 专门用于存储地址的变量,又称指针变量。

指针的定义:

int    *p1; // 用于存储 int  型数据的地址,p1 被称为 int  型指针,或称整型指针
char   *p2; // 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
double *p3; // 用于存储double型数据的地址,p3 被称为 double 型指针
//指针的赋值:赋给指针的地址,类型需跟指针的类型相匹配。
int a = 100;
p1 = &a; // 将一个整型地址,赋值给整型指针p1

char c = 'x';
p2 = &c; // 将一个字符地址,赋值给字符指针p2

double f = 3.14;
p3 = &f; // 将一个浮点地址,赋值给浮点指针p3

指针的索引:通过指针,取得其指向的目标

*p1 = 200; // 将 p1 指向的目标(即a)修改为200,等价于 a = 200;
*p2 = 'y'; // 将 p2 指向的目标(即c)修改为'y',等价于 c = 'y';
*p3 = 6.6; // 将 p3 指向的目标(即f)修改为6.6,等价于 f = 6.6;

指针的尺寸

指针尺寸指的是: 指针所占内存的字节数 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关

【10】C语言-》野指针*的误区

概念:指向一块未知区域的指针,被称为野指针。野指针是危险的。
在这里插入图片描述

危害:

引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果

产生原因:

指针定义之后,未初始化 指针所指向的内存,被系统回收 指针越界

如何防止:

指针定义时,及时初始化 绝不引用已被系统回收的内存 确认所申请的内存边界,谨防越界

【11】C语言-》空指针*的讲解

很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。

在这里插入图片描述
概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
示例:

// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int  *p2 = NULL;

写代码建议这样做

// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3);               // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL;              // c. 让 p3 指向零地址

【12】C语言-》指针运算的引入

指针加法意味着地址向上移动若干个目标
指针减法意味着地址向下移动若干个目标

示例:

int  a = 100;
int *p = &a; // 指针 p 指向整型变量 a

int *k1 = p + 2; // 向上移动 2 个目标(2个int型数据)
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)

在这里插入图片描述

【13】C语言-》数组+指针 面试题讲解

(1.数组是不是就是地址)

答:有时候是,有时候不是。在C语言中非常重要的一点是:同一个符号,在不同场合,有不同的含义。 比如数组 int a[3];
当出现在以下三种情形中的时候,它代表的是一块12字节的内存:

  1. 初始化语句时:int a[3]; 与sizeof结合时:sizeof(a)
  2. 与取址符&结合时:&a
    只有在上述三种情形下,数组a代表一片连续的内存,占据12个字节,而在其他任何时候,数组a均会被一律视为其首元素的地址。
    因此,不能武断地说数组是不是地址,而要看它出现的场合。

(2.指针取地址)

指针不是地址码?为什么还可以取地址?地址的地址是什么意思?

答:你这个疑惑是典型的概念混淆。首先需要明确,指针通常指指针变量,是一块专用于装载地址的内存,因此指针跟别的普通变量没什么本质区别,别的变量可以取地址,那么指针变量当然也可以取地址。

(3.数组及数组元素地址)

【1】假如有如下定义:int a[3][5]; 完成如下要求:

用1种方法表示 a[2][3] 的地址。
用2种完全等价的方法表示 a[2][0] 的地址。
用3种完全等价的方法表示 a[0][0]的地址。
解析: 第一问直截了当,a[2][3]的地址:

1. &a[2][3]

第二问中,除了可以直接取a[2][0]的地址,子数组a[2]本质上也是其首元素a[2][0]的地址:

1. &a[2][0]
2. a[2]

第三问中,除了可以直接取a[0][0]的地址和子数组a[0]之外,还可以利用数组下标的运算规则,将下标运算符去掉:

1. &a[0][0]
2. a[0]
3. *a

提示:

a[0] 等价于 *(a+0) 等价于 *(a) 等价于 *a

(4.数组及指针定义+面试题常见)

【2】请写出符合以下要求的定义语句。

定义一个整型数 i
定义一个指向整型数的指针 p
定义一个指向整型指针的指针 k
定义一个有 3 个整型数的数组 a
定义一个有 3个整型指针的数组 b
定义一个指向有 3 个整型元素的数组的指针 q
定义一个指向函数的指针 r,该函数有一个整型参数并返回一个整型

参考代码:

1. int i;
2. int *p;//指向变量地址
3. int **k;//指向一级指针地址
4. int a[3];//保存变量值
5. int *b[3];//数组指针,指向变量地址
6. int (*q)[3];//指针数组  保存数组地址值
//返回值 int   参数:int
8. int (*r)(int);//函数指针  保存函数地址  以便调兵遣将

(5.数组下标运算、指针运算)

【3】分析下面的程序的执行结果。

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

解析 代码中的 p=a 是关键,该赋值语句让指针 p 指向了数组的首元素 1,然后指针 p 在每次循环之后自增
1,因此会不断指向后续元素。最后输出的结果是:

1 1
2 2
3 3
4 4

(6.数组与指针运算关系)

【4】阅读下面两段代码,分析程序的输出内容。

代码片段一:

int *p;
int a[2][2] = {
    
    1, 2, 3, 0};
p = a[0];
printf("%d, %d", *p, *(p+1)); // 输出什么?

解析

指针 p 指向子数组 a[0] 的首元素,即 p 指向 a[0][0],p+1 指向 a[0][1],因此程序输出1和2。

代码片段二:

int *p;
int a[2][2] = {
    
    {
    
    1, 0}, {
    
    2, 3}};
p= a[0];
printf("%d, %d", *p, *(p+1)); // 输出什么?

解析

指针 p 指向子数组 a[0] 的首元素,即 p 指向 a[0][0],p+1 指向 a[0][1],因此程序输出1和0。

(7.数组基本操作)

【5】编写一个函数,接收三个类型相同的整型数组 a、b 和 c,将 a 和 b 的各个元素的值相加,存放到数组 c 中。

参考代码

#include <stdio.h>

#define LIM 4

//这里不用返回值,数组名本就是指针=地址,改变的值会保留
void sumary(int array1[], int array2[],
	    int array3[], int size)
{
    
    
	int i;

	for(i=0; i<size; i++)
		array3[i] = array1[i] + array2[i];
}

int main(void)
{
    
    

	int array1[LIM] = {
    
    2, 4, 6, 8};
	int array2[LIM] = {
    
    1, 0, 3, 6};
	int array3[LIM];

	sumary(array1, array2, array3, LIM);
	
	int i;
	for(i=0; i<LIM; i++)
	{
    
    
		printf("%d\t", array3[i]);//打印
	}
	printf("\n");
	return 0;
}

(8.数组基本操作)

【6】编写一个程序,不使用格式控制符 %x 的情况下,将十进制数转换为十六进制。

解析
假定有一个十进制数为123,转换为十六进制的思路是将123对16进行短除法,每次取余数放入一个数组中,并将商作为新的十进制数,重复以上过程直到商为0。最后,将数组中的数据倒序输出就是结果。需要注意的地方有几点:第一,数组必须可以存储字母,因为十六进制数包含字母;第二,倒序输出结果。

参考代码

#include <stdio.h>
#include <stdbool.h>


int main(void)
{
    
    
    int decimal;
    bool negative = false;
    
    printf("请输入一个十进制数: ");
    scanf("%d", &decimal); // 为了突出重点,此处未进行输入合法性检测,望读者知悉
    
    // 判断并记录要转换的十进制数的正负号
    if(decimal < 0)
    {
    
    
        negative = true;
        decimal *= -1;
    }
    
    // 将该十进制数对16进行短除法,并将余数依次存入数组num中
    int i;
    char hex[10];
    for(i=0; i<10 && decimal!=0; i++)
    {
    
    
        switch(decimal % 16)
        {
    
    
        case 0 ... 9:
            hex[i] = decimal%16 + '0';
            break;
        case 10 ... 15:
            hex[i] = decimal%16 - 10 + 'A';
            break;
        }
        decimal /= 16;
    }

    if(i >= 10)
    {
    
    
        printf("数字太大,无法计算\n");
        return 0;
    }
    
    printf("转换成十六进制为: %c0x", negative?'-':' ');
    
    // 将数组num中的数字倒序输出
    int j;
    for(j=i-1; j>=0; j--)
    {
    
    
        printf("%c", hex[j]);
    }
    
    printf("\n");
    return 0;
}

(9.数组基本操作、指针基本操作)

【7】假设有如下声明:

float a[3];
float b[2][3];
float c = 2.2, *p;

则下列语句中那些是正确的,哪些是错误的?原因是什么?

a[2] = c;
a = c;
scanf("%f", &a);
printf("%f", a[3]);
b[1][2] = a[2];
b[1] = a; 
p = c;
p = a; 

解析

a[2] = c;           // 对数组某个元素赋值,正确
a = c;              // a 是数组,不可直接赋值
scanf("%f", &a);    // a 是数组,类型不匹配
printf("%f", a[3]); // a[3] 是float数据,类型不匹配
b[1][2] = a[2];     // 对数组某个元素赋值,正确
b[1] = a;           // b[1] 是数组,不可直接赋值
p = c;              // 类型不匹配
p = a;              // a 代表其首元素地址,等价于p=&a[0],正确

(10.数组参数变换、sizeof用法)

【8】分析下述代码,指出其不正确的地方。

#include <stdio.h>
#include <limits.h>
#include <ctype.h>

void upper_case(char str[])
{
    
    
    int step = 'a' - 'A';
    for(int i = 0; i<sizeof(str)/sizeof(str[0]); i++)
    {
    
    
        if(islower(str[i]))
            str[i] -= step;
    }
}
int main(void)
{
    
    
    char str[] = "abcdefghijklnmopqrstuvwxyz";
    printf("原数组:%s\n", str);
    upper_case(str);
    printf("转换后:%s\n", str);
}

解析

数组在除了定义和sizeof语句之外,均会被视为指向其首元素的指针,因此在上述代码中, upper_case(str)
中的str是一个指针,而非数组,等价于:

upper_case(&str[0]); 因此,在函数 void upper_case(char str[])
中,str由始至终都是指针,而非数组,因此sizeof(str)无法计算原数组的大小,因此该程序无法正常执行。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_45463480/article/details/124765718