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

在这里插入图片描述

【1】数组名涵义

数组名有两个含义:

第一含义是:整个数组
第二含义是:首元素地址
当出现以下情形时,那么数组名就代表整个数组:

在数组定义中 在 sizeof 运算表达式中
在取址符&中
其他任何情形下,那么数组名就代表首元素地址。
即:此时数组名就是一个指向首元素的指针。

示例:

int a[3];                  // 此处,a 代表整个数组
printf("%d\n", sizeof(a)); // 此处,a 代表整个数组
printf("%p\n", &a);        // 此处,a 代表整个数组,此处为整个数组的地址

int *p = a;       // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
p = a + 1;        // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
function(a);      // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
scanf("%d\n", a); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]

C语言只有在第一含义的场合下表现为数组,其他大部分场合都表现为首元素的地址,当数组表现为首元素地址时,实际上就是一个指向其首元素的指针。数组运算实际上就是指针运算。

【2】数组下标

数组下标实际上是编译系统的一种简写.
其等价形式是: a[i] = 100; 等价于 *(a+i) = 100;
根据加法交换律,以下的所有的语句均是等价的:

  a[i] = 100;
*(a+i) = 100;
*(i+a) = 100;
  i[a] = 100;
数组运算,等价于指针运算。
例如:

int a[3];
// 此处的a等价于 &a[0],而a[0]是一个int,因此此处a的类型是 int (*)
int *p = a+1;

int a[2][3];
// 此处的a等价于 &a[0],而a[0]是一个int[3],因此此处a的类型是 int (*)[3]
int (*p)[3] = a+1;

int *a[3];//相当于二级指针,int **a;
// 此处的a等价于 &a[0],而a[0]是一个int *,因此此处a的类型是 int (*)*
int **p = a+1;

【3】字符串常量(匿名数组)

字符串常量在内存中的存储,实质是一个匿名数组 匿名数组,同样满足数组两种涵义的规定

示例:

printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组
printf("%p\n", &"abcd");        // 此处 "abcd" 代表整个数组

printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址
char *p1 = "abcd";         // 此处 "abcd" 代表匿名数组的首元素地址
char *p2 = "abcd" + 1;     // 此处 "abcd" 代表匿名数组的首元素地址

在这里插入图片描述

【4】零长数组(结构体空间扩充)

概念:长度为0的数组,比如 int data[0];

用途:放在结构体的末尾,作为可变长度数据的入口
示例:

struct node
{
    /* 结构体的其他成员 */
    // 成员1
    // 成员2
    // ... ...
    
    int   len;
    char *data[0];
};
// 给结构体额外分配 10 个字节的内存。
struct node *p = malloc(sizeof(struct node) + 10);
p->len = 10;

// 额外分配的内存可以通过 data 来使用
p->data[0] ~ p->data[9]

【5】变长数组

概念:定义时,使用变量作为元素个数的数组。
要点:变长数组仅仅指元素个数在定义时是变量,而绝非指数组的长度可长可短。实际上,不管是普通数组还是所谓的变长数组,数组一旦定义完毕,其长度则不可改变。

示例:

int len = 5;
int a[len];  // 数组元素个数 len 是变量,因此数组 a 是变长数组

int x = 2;
int y = 3;
int b[x][y]; // 数组元素个数 x、y 是变量,因此数组 b 是变长数组
int b[2][y]; // 数组元素个数 y 是变量,因此数组 b 是变长数组
int b[x][3]; // 数组元素个数 x 是变量,因此数组 b 是变长数组

语法:变长数组不可初始化,即以下代码是错误的:

int len = 5;
int a[len] = {1,2,3,4,5}; // 数组 a 不可初始化

【6】char型指针

char型指针实质上跟别的类型的指针并无本质区别,但由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。

定义:

char *p = "abcd";

【7】多级指针

如果一个指针变量 p1 存储的地址,是另一个普通变量 a 的地址,那么称 p1 为一级指针

如果一个指针变量 p2 存储的地址,是指针变量 p1 的地址,那么称 p2 为二级指针

如果一个指针变量 p3 存储的地址,是指针变量 p2 的地址,那么称 p3 为三级指针

以此类推,p2、p3等指针被称为多级指针
示例:

int a = 100;
int   *p1 = &a;  // 一级指针,指向普通变量
int  **p2 = &p1; // 二级指针,指向一级指针
int ***p3 = &p2; // 三级指针,指向二级指针

【8】指针万能拆解法

任意的指针,不管有多复杂,其定义都由两部分组成。
第1部分:指针所指向的数据类型,可以是任意的类型
第2部分:指针的名字

示例:

char   (*p1);      // 第2部分:*p1; 第1部分:char; 
char  *(*p2);      // 第2部分:*p2; 第1部分:char *; 
char **(*p3);      // 第2部分:*p3; 第1部分:char **; 
char   (*p4)[3];  // 第2部分:*p4; 第1部分:char [3]; 
char   (*p5)(int, float); // 第2部分:*p5; 第1部分:char (int, float); 

注解:

上述示例中,p1、p2、p3、p4、p5本质上并无区别,它们均是指针
上述示例中,p1、p2、p3、p4、p5唯一的不同,是它们所指向的数据类型不同
第1部分的声明语句,如果由多个单词组成,C语言规定需要将其拆散写到第2部分的两边

【9】void型指针

概念:无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针
要点: void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
void 型指针无法进行加减法运算

void关键字的三个作用:

修饰指针,表示指针指向一个类型未知的数据。 修饰函数参数列表,表示函数不接收任何参数。(预习)int main(void)
修饰函数返回类型,表示函数不返回任何数据。(预习)void func(int)

示例:

// 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
void *p = malloc(4);
// 1,将这 4 字节内存用来存储 int 型数据
*(int *)p = 100;
printf("%d\n", *(int *)p);
// 2,将这 4 字节内存用来存储 float 型数据
*(float *)p = 3.14;
printf("%f\n", *(float *)p);

【10】const 型指针

const型指针有两种形式:①常指针 ②常目标指针

常指针:const修饰指针本身,表示指针变量本身无法修改。
常目标指针:const修饰指针的目标,表示无法通过该指针修改其目标。 常指针在实际应用中不常见。 常目标指针在实际应用中广泛可见,用来限制指针的读写权限

示例:

int a = 100;
int b = 200;

// 第1中形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a; 

// 第2中形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;
const int *p2 = &a;

【11】函数指针巧用之处

概念:指向函数的指针,称为函数指针。
特点:函数指针跟普通指针本质上并无区别,只是在取址和索引时,取址符和星号均可省略

示例:

void   f (int); // 函数 f 的类型是: void (int)
void (*p)(int); // 指针 p 专门用于指向类型为 void (int) 的函数

p = &f; // p 指向 f(取址符&可以省略)
p =  f; // p 指向 f
// 以下三个式子是等价的:
  f (666); // 直接调用函数 f
(*p)(666); // 通过索引指针 p 的目标,间接调用函数 f
 p (666); // 函数指针在索引其目标时,星号可以省略

要点: 函数指针是一类专门用来指向某种类型函数的指针。 函数的类型不同,所需要的函数指针也不同。
函数的类型,与普通变量的类型判定一致,即去除声明语句中的标识符之后所剩的语句。

【12】面试题

(字节序)

【1】编写一个程序,测试当前平台的字节序。

解析: 此题有多种解法,在本次课中,我们可以使用指针来操作某个特定的字节。

参考代码

#include <stdio.h>

int main(void)
{
    // 定义一个4字节的整型数据
    int a = 0x12345678;

    // 定义一个char型指针指向最低地址
    char *p = &a;

    // 将最低字节数据打印出来
    // 如果是0x78,那就代表最低地址存储了低有效位,是小端序
    // 如果是0x12,那就代表最低地址存储了高有效位,是大端序
    printf("%#x\n", *p);
}

(数组下标运算、指针运算、内存操作)

【2】分析以下代码的输出结果。

#include <stdio.h>
int main(void)
{
    long  a[4] = {1, 2, 3, 4};
    long  *p1=(long *)(&a+1);
    long  *p2=(long *)((long)a+1);
 
    printf("%lx\n", p1[-1]);
    printf("%lx\n", *p2);
    
    return 0;
}

解析:

首先,&a是整个数组的地址,因此&a+1实际上是向后偏移了16个字节,因此 p1 指向了数组边界。然后输出 p1[-1] 等价于
*(p1-1),因为 p1 是 long 型指针,因此 p1-1 就向前偏移 sizeof(long) 个字节,也就是指向了 a[3]。

其次,(long)a+1 是一个纯整数运算(因为a被强转为long了),因此 p2 就指向了long型数据 a[0] 的第二个字节,最后打印
*p2 时,由于 p2 是一个 long 型指针,系统会从 a[0] 的第二个字节开始,取出 sizeof(long) 个字节出来作为 long 型数据来解释,因此最后输出的结果是 a[0] 的高位三字节和 a[1] 低位一字节的数据。

(复杂声明、多维数组操作)

【3】声明一个二维 int 型数组 a,再声明另一个一维数组指针数组 b,使该数组 b 的每一个指针分别指向二维数组 a
中的每一个元素(即每一个一维数组),然后利用数组 b 计算数组 a 的和。

解析
本题主要考察的是基本概念的理解:“指针数组”指的是装了很多指针的数组,
“数组指针”指的是指向数组的指针,“数组指针数组”指的是装了很多数组指针的数组。

参考代码
#include <stdio.h>

int main(void)
{
	int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
	int (*b[2])[3];

	b[0] = &a[0];
	b[1] = &a[1];

	int i, j, sum=0;
	for(i=0; i<2; ++i)
	{
		for(j=0; j<3; ++j)
		{
			sum += (*b[i])[j];
		}
	}

	printf("sum: %d\n", sum);
	return 0;
}

(数组操作)

【4】编写一个程序,求一个有N个元素的整型数组中子数组之和的最大值,子数组指的是一个数组中连续的若干个相邻的元素。 例如:

int a[7] = {-2, 5, -1, 6, -4, -8, 6}; 则子数组之和的最大值是 5+(-1)+6,即答案是 10。

解析: 本题是一道笔试题,考的是算法。因此不宜使用三重循环的暴力破解法。 参考代码:

#include <stdio.h>

int main(void)
{
	int a[100];
	int len = 0;

    printf("请输入系列整数,以#结束\n");
	while(scanf("%d", &a[len]) != 0)
		len++;

	int max=a[0], sum=0;

	int i;
	for(i=0; i<len; i++)
	{
		sum += a[i];

		if(sum > max)
			max = sum;

		else if(sum < 0)
			sum = 0;
	}

	printf("最大子数组之和: %d\n", max);
	return 0;
}

(变长数组:将二维数组a复制到二维数组b)

5】编写一个函数,它接收两个 m×n 的整型二维数组 a 和 b,
函数的功能是将数组 a 中的数据复制到数组 b 中。

参考代码:
#include <stdio.h>
#include <stdlib.h> // enable the function rand()

// 将数组source各个元素的值,赋给数组target
void assign(int row, int col,
		    double target[row][col], double source[row][col])
{
	int i, j;
	for(i=row; i>0; i--)
	{
		for(j=col; j>0; j--)
		{
			target[i-1][j-1] = source[i-1][j-1];
		}
	}
}

void show(int row, int col,
		   double target[row][col], double source[row][col])
{
	int i, j;
	printf("源数据:\n");

	for(i=row; i>0; i--)
	{
		for(j=col; j>0; j--)
		{
			printf("%f\t", source[i-1][j-1]);
		}
		printf("\n");
	}

	printf("目标数据:\n");
	for(i=row; i>0; i--)
	{
		for(j=col; j>0; j--)
		{
			printf("%f\t", target[i-1][j-1]);
		}
		printf("\n");
	}
}

int main(void)
{
	int i, j;
	int row, col;
	
	printf("请输入二维数组的行数和列数:\n");
	scanf("%d%d", &row, &col);

	double source[row][col];

	for(i=0; i<row; i++)
	{
		for(j=0; j<col; j++)
		{
            // 往源数组中写入随机数据
			source[i][j] = (double)rand() / (double)rand();
		}
	}

	double target[row][col];

	assign(row, col, target, source);
	show(row, col, target, source);

	return 0;
}

(字符数组:去除重复字符)

【6】编写一个程序,去掉给定字符串中重复的字符。 例如:

输入:google 输出:gole
参考代码:

#include <stdio.h>
#include <string.h>

void strip(char *str)
{
	if(str == NULL)
		return;

	int i, j;
	char *p = str;

    // 将所有重复的字符标记为-1
	for(i=1; str[i] != '\0'; i++)
	{
		for(j=0; j<i; j++)
		{
			if(p[j] == str[i])
			{
				str[i] = -1;
				break;
			}
		}
	}

    // 找到第一个-1的位置 redun
    // 找到紧随 redun 之后的正常字符的位置 common
	int redun = 0, common = 0;
	for(i=1; str[i] != '\0'; i++)
	{
		if(str[i] != REDUNDANT)
			continue;

		redun = i;
		while(str[i] == REDUNDANT)
			i++;

		common = i;
		break;
	}

    // 若没有重复的字符,则返回
	if(redun == 0)
		return;

    // 将所有正常字符往前移
	while(str[common] != '\0')
	{
		if(str[common] == -1)
		{
			common++;
			continue;
		}
		str[redun++] = str[common];
		str[common++] = -1;
	}
	str[redun] = '\0';
}

int main(void)
{
    // 输入待处理字符串
	char buf[64];
	fgets(buf, 64, stdin);

	strip(buf);
	printf("%s", buf);
	
	return 0;
}

在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Qt历险记

谢谢你的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值