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字节的内存:
- 初始化语句时:int a[3]; 与sizeof结合时:sizeof(a)
- 与取址符&结合时:&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)无法计算原数组的大小,因此该程序无法正常执行。