前言

在使用C的时候难免会碰到一些奇怪的用法或者令人困惑的语法等等,考虑到问题过于琐碎,就写于这个合集中,名为关于C语言的零碎思考

typedef 和 define 的区别

这种关键字的使用常见于对于某种类型的替换,例如下面的场景:

1
2
3
#define ElemType int

typedef int NewType

两者的区别在于:define是一种宏定义,本质上来说就是字符串替换,而typedef是一种类型封装

例如参考下面的代码,思考各个变量的类型:

1
2
3
4
5
#define ElemType int*

int main(){
ElemType a,b;
}

其中变量a的类型为int*,而变量b的类型为int类型,不难理解宏定义只是字符串替换。

1
2
3
4
5
typedef int* ElemType

int main(){
ElemType a,b;
}

其中**变量a和变量b的类型均为int***,可以理解为typedefint*封装成了一个新的类型。

typedef 和 struct 的使用

关于结构体的相关基础内容我已经在这篇文章中做过说明【9.0】C-结构体与共用体.

这里主要是梳理当typedefstruct关键词连用时的逻辑关系,代码示例:

1
2
3
4
struct{
int a;
float b;
}Test1;

如果我们使用上述代码来定义结构体,那么**Test1表示的被定义的变量**,而不是数据类型。

上述代码意味着:你将不可能在其他位置声明和Test1相同类型的变量,除非你继续在结构体声明的时候再添加其他变量。同样的,如果使用如下代码:

1
2
3
4
5
struct Test
{
int a;
float b;
}Test1;

此处的Test1是表示类型为Test的结构体变量,此代码与上述代码不同之处在于你可以通过使用struct Test关键词来在其他地方继续声明和Test1变量相同类型的其他变量

但是如果对struct关键词配合typedef关键词的话,其逻辑结构就不同了,代码示例:

1
2
3
4
typedef struct{
int a;
float b;
}Test1;

在使用typedef关键词后,如上代码中的Test1就不再是变量了,而是结构体类型名称,如果你使用Test1.a则编译器会报错。你可以通过Test1 变量名称来定义相同结构体类型的变量。

在了解如上代码的逻辑后,现在来判断如下代码中的Test1,Test2,Test是代表的什么?

1
2
3
4
typedef struct Test{
int a;
float b;
}Test1,Test2;
image-20220801205502564

答案是三者都表示同一种结构体类型,只是这种结构体类型的不同名称变体,其并非变量。不过不同的是,对于Test1Test2两者是相同的,与Test不同,如下是三者使用时声明的代码示例:

1
2
3
4
5
int main(){
Test1 a;
Test2 b;
struct Test c;
}

到这里你就会明白了,其实上面的代码,是下面代码的缩写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这是上面的代码
typedef struct Test{
int a;
float b;
}Test1,Test2;

//这是其拆解后的过程
//先声明结构体
struct Test{
int a;
float b;
}Test1,Test2;
//然后使用typedef函数改变其函数声明格式
typedef struct Test Test1,Test2;

Bool函数的使用问题

如果你使用GCC编译器,将其连接到Visual Code编辑器中使用,编译C语言文件。如果文件中使用了bool关键字,则会报错,需要引用#include <stdbool.h>来使用bool关键字

如果你使用Visual Studio安装的C/C++环境,使用bool关键词,通用需要引用#include <stdbool.h>,不过在引用后,IDE 依旧会报错,需要再引用#define _CRT_SECURE_NO_WARNINGS即可使用

取余运算/取模运算的算法规则

取模运算也就是取余运算,它的作用之一是可以将无线的集合,通过取余来映射到有限的集合里

C语言中取余运算符为:%,其作用于两个整型数(正负皆可),运算结果是返回两数的余数(即返回值为整数),它遵循如下规定:(示例a%b

  • 运算结果的正负和被除数(a)符号一致
  • 被除数(a)小于除数(b)时,运算结果等于被除数(a)

单引号和双引号引发的问题

如果你学习过C#JAVA或者Python等现代高级语言的时候,你或许对于字符串和字符来说,都是通常来使用双引号("")来表示,例如:"这是一个字符串",这是一个"c"字符)。

这种写法在高级语言中是合法的,但是在C语言中,这种写法是不规范的,也是不完全合法的,例如下面的代码:

1
2
//声明一个char类型的数组str,声明其包含字符a,b,c
char str[10] = {"a", "b", "c"};

请思考如上写法是否合法?

image-20220809025647389

很不幸,这种写法并不合法,我们的编译器会给我们报错,如下所示:

image-20220809025317930

那么这是为什么?一共就三个字符a,b,c

image-20220809025404051

这是因为C语言在处理字符的时候,对于双引号("这是双引号的内容")的内容来说,它会被优先认为是字符串,对于单引号('这是单引号内容')会被优先认为是字符

那么问题来了,请思考如下写法编译器是否会报错?

1
char c = "b";

答案:会但是不完全会,我们的编辑器或者 IDE 并不会直接报错,它不会和上面的数组示例一样,在未编译的时候就报错,这种写法只会在编译的时候报错,但是仅仅是警告,而不是错误,也就是说,这种写法并不会中断程序的运行

在程序中,有句话叫做:“错误需要解决,警告可以不管,程序能跑就行”,但是这样真的可以让程序万无一失吗?

答案是并不能,我们在给字符c赋值字符串b的时候,编译器会将字符c的值变为ASCII码的第 40 号(()字符,这也就意味着程序并不能如我们所说的那样,警告可以不用管。

C中各种类型的问题

所占内存

类型 字节
int 4字节(Byte)
char 1字节(Byte)
float 4字节(Byte)
double 8字节(Byte)
long 4字节(Byte)

char类型

因为**char类型占一个字节,也就是说换算成数值类型,其可以表示 $2^8$ 个数值,也就是 $0 \sim 255$** 。但是实际上考虑到正负号的问题,所以它可以表示的实际数值为 $-128 \sim 127$

综上结果,我们是可以将int类型存储在char类型的变量中的,但是其大小只能被限制到如上的实际数值范围中。现在问题来了,思考如下代码:

1
char c = 128;

请问它是否合法?

image-20220809204325744

答:赋值合法。但是为什么?

这需要从char类型和int类型所占用的内存空间说起,如上表格所示,char类型在内存占用1 个字节,也就是8 个位,而int类型占用4 个字节,也就是32 个位。计算机本质是二进制的数值,也就是说对于计算机来说本来就没有字符这一个说法,这些字符的说法都是源于ASCII码的映射。

标准的ASCII码是采用1 个字节(8 位)来表示字符,但是实际上最高位用来做数据的奇偶校验位,用来验证数据完整。也就是说,**ASCII码实际能用的之后的后 7 位**,也就是会产生 $2^7=128$ 种情况。这 128 种情况对应标准ASCII码中的字符数字控制符等。

奇偶检验的相关内容详情查看《计算机组成原理》

通过ASCII码,我们建立了字符和数值的映射,这也就意味着字符本质还是数值。所以上述代码“合法”,但是它真的能用吗?请思考如下代码:

1
2
3
char a = 128;
printf("字符A的十进制表示为:%d\n",a);
printf("字符A的字符表示为:%c\n", a);

它们的运行结果是什么?

image-20220810001213562

答案:

  • 字符A的十进制表示为:-128
  • 字符A的字符表示为:€

为什么?明明给它赋值的 128 ,为什么他的数值结果确是 -128 ?关键是 -128 数值竟然还有对应的字符映射?

先解释第二个,之所以字符表示为,是因为前面说到标准的ASCII码表最高位用来做校验位,导致其容量只有 128 个映射。后来人将其最高位也算在数值映射内,也就是使用 8 位来映射字符,而形成了新的拓展ASCII码表,其拓展内容如下:

十进制 八进制 十六进制 二进制 符号 HTML 编号 HTML 名称 描述
128 200 80 10000000 欧元符号
129 201 81 10000001
130 202 82 10000010 单个低9引号
131 203 83 10000011 ƒ ƒ ƒ 拉丁小写字母f
132 204 84 10000100 双低9引号
133 205 85 10000101 水平省略号
134 206 86 10000110 匕首
135 207 87 10000111 双匕首
136 210 88 10001000 ˆ ˆ ˆ 修饰语字母抑扬音
137 211 89 10001001 千分号
138 212 8A 10001010 Š Š Š 拉丁大写字母S
139 213 8B 10001011 单左角引号
140 214 8C 10001100 Œ Œ Œ 拉丁字母连字OE
141 215 8D 10001101
142 216 8E 10001110 Ž Ž 拉丁大写字母Z
143 217 8F 10001111
144 220 90 10010000
145 221 91 10010001 左单引号
146 222 92 10010010 右单引号
147 223 93 10010011 左双引号
148 224 94 10010100 右双引号
149 225 95 10010101 子弹
150 226 96 10010110 破折号
151 227 97 10010111 破折号
152 230 98 10011000 ˜ ˜ ˜ 小波浪号
153 231 99 10011001 商标标志
154 232 9A 10011010 š š š 拉丁小写字母S
155 233 9B 10011011 单个右指向角引号
156 234 9C 10011100 œ œ œ 拉丁文小连字oe
157 235 9D 10011101
158 236 9E 10011110 ž ž 拉丁小写字母z
159 237 9F 10011111 Ÿ Ÿ Ÿ 拉丁大写字母Y
160 240 A0 10100000     不间断空间
161 241 A1 10100001 ¡ ¡ ¡ 倒感叹号
162 242 A2 10100010 ¢ ¢ ¢ 分号
163 243 A3 10100011 £ £ £ 英镑符号
164 244 A4 10100100 ¤ ¤ ¤ 货币符号
165 245 A5 10100101 ¥ ¥ ¥ 日元符号
166 246 A6 10100110 ¦ ¦ ¦ 管道,竖线损坏
167 247 A7 10100111 § § § 分区标志
168 250 A8 10101000 ¨ ¨ ¨ 间隔透析-umlaut
169 251 A9 10101001 © © © 版权标志
170 252 AA 10101010 ª ª ª 女性顺序指示器
171 253 AB 10101011 « « « 左双角引号
172 254 AC 10101100 ¬ ¬ ¬ 不签名
173 255 AD 10101101 ­ ­ ­ 软连字符
174 256 AE 10101110 ® ® ® 注册商标标志
175 257 AF 10101111 ¯ ¯ ¯ 间隔宏-上划线
176 260 B0 10110000 ° ° ° 学位标志
177 261 B1 10110001 ± ± ± 正负号
178 262 B2 10110010 ² ² ² 上标二平方
179 263 B3 10110011 ³ ³ ³ 上标三方
180 264 B4 10110100 ´ ´ ´ 急性口音-间隔锐
181 265 B5 10110101 µ µ µ 微标志
182 266 B6 10110110 稻草人标志-段落标志
183 267 B7 10110111 · · · 中间点-格鲁吉亚逗号
184 270 B8 10111000 ¸ ¸ ¸ 间距塞迪利亚
185 271 B9 10111001 ¹ ¹ ¹ 上标一
186 272 BA 10111010 º º º 男性顺序指示器
187 273 BB 10111011 » » » 右双角引号
188 274 BC 10111100 ¼ ¼ ¼ 分数的四分之一
189 275 BD 10111101 ½ ½ ½ 分数的一半
190 276 BE 10111110 ¾ ¾ ¾ 分数四分之三
191 277 BF 10111111 ¿ ¿ ¿ 倒问号
192 300 C0 11000000 À À À 拉丁大写字母A
193 301 C1 11000001 Á Á Á 拉丁大写字母A
194 302 C2 11000010 Â Â Â 拉丁大写字母A
195 303 C3 11000011 Ã Ã Ã 拉丁大写字母A
196 304 C4 11000100 Ä Ä Ä 拉丁大写字母A
197 305 C5 11000101 Å Å Å 拉丁大写字母A
198 306 C6 11000110 Æ Æ Æ 拉丁大写字母AE
199 307 C7 11000111 Ç Ç Ç 拉丁大写字母C
200 310 C8 11001000 È È È 拉丁大写字母E
201 311 C9 11001001 É É É 拉丁大写字母E
202 312 CA 11001010 Ê Ê Ê 拉丁大写字母E
203 313 CB 11001011 Ë Ë Ë 拉丁大写字母E
204 314 CC 11001100 Ì Ì Ì 拉丁大写字母I
205 315 CD 11001101 Í Í Í 拉丁大写字母I
206 316 CE 11001110 Î Î Î 拉丁大写字母I
207 317 CF 11001111 Ï Ï Ï 拉丁大写字母I
208 320 D0 11010000 Ð Ð Ð 拉丁大写字母ETH
209 321 D1 11010001 Ñ Ñ Ñ 拉丁大写字母N
210 322 D2 11010010 Ò Ò Ò 拉丁大写字母O
211 323 D3 11010011 Ó Ó Ó 拉丁大写字母O
212 324 D4 11010100 Ô Ô Ô 拉丁大写字母O
213 325 D5 11010101 Õ Õ Õ 拉丁大写字母O
214 326 D6 11010110 Ö Ö Ö 拉丁大写字母O
215 327 D7 11010111 × × × 乘法
216 330 D8 11011000 Ø Ø Ø 拉丁大写字母O
217 331 D9 11011001 Ù Ù Ù 拉丁大写字母U
218 332 DA 11011010 Ú Ú Ú 拉丁大写字母U
219 333 DB 11011011 Û Û Û 拉丁大写字母U
220 334 DC 11011100 Ü Ü Ü 拉丁大写字母U
221 335 DD 11011101 Ý Ý Ý 拉丁大写字母Y
222 336 DE 11011110 Þ Þ Þ 拉丁大写字母THORN
223 337 DF 11011111 ß ß ß 拉丁小写字母sharp s - ess-zed
224 340 E0 11100000 à à à 拉丁小写字母a
225 341 E1 11100001 á á á 拉丁小写字母a
226 342 E2 11100010 â â â 拉丁小写字母a
227 343 E3 11100011 ã ã ã 拉丁小写字母a
228 344 E4 11100100 ä ä ä 拉丁小写字母a
229 345 E5 11100101 å å å 拉丁小写字母a
230 346 E6 11100110 æ æ æ 拉丁小写字母a
231 347 E7 11100111 ç ç ç 拉丁小写字母c
232 350 E8 11101000 è è è 拉丁小写字母e
233 351 E9 11101001 é é é 拉丁小写字母e
234 352 EA 11101010 ê ê ê 拉丁小写字母e
235 353 EB 11101011 ë ë ë 拉丁小写字母e
236 354 EC 11101100 ì ì ì 拉丁小写字母i
237 355 ED 11101101 í í í 拉丁小写字母i
238 356 EE 11101110 î î î 拉丁小写字母i
239 357 EF 11101111 ï ï ï 拉丁小写字母i
240 360 F0 11110000 ð ð ð 拉丁小写字母eth
241 361 F1 11110001 ñ ñ ñ 拉丁小写字母n
242 362 F2 11110010 ò ò ò 拉丁小写字母o
243 363 F3 11110011 ó ó ó 拉丁小写字母o
244 364 F4 11110100 ô ô ô 拉丁小写字母o
245 365 F5 11110101 õ õ õ 拉丁小写字母o
246 366 F6 11110110 ö ö ö 拉丁小写字母o
247 367 F7 11110111 ÷ ÷ ÷ 除号
248 370 F8 11111000 ø ø ø 拉丁小写字母o
249 371 F9 11111001 ù ù ù 拉丁小写字母u
250 372 FA 11111010 ú ú ú 拉丁小写字母u
251 373 FB 11111011 û û û 拉丁小写字母u
252 374 FC 11111100 ü ü ü 拉丁小写字母u
253 375 FD 11111101 ý ý ý 拉丁小写字母y
254 376 FE 11111110 þ þ þ 拉丁小写字母thorn
255 377 FF 11111111 ÿ ÿ ÿ 拉丁小写字母y

你可以很清楚的发现,表中的对应关系,**十进制的 128 对应的字符为 **,这也就是为什么第二个输出为字符

那为什么第一个采用十进制输出就变成了 -128 了呢?

这就不得不提到数值是如何在计算机中存储的了,我在博客没搬家之前写过一篇《计算机的原码,反码,补码》的内容,不过搬到Hexo后感觉质量不是很好,遂没有腾过来。

计算机原码,反码,补码的详细内容可以参考(知乎)计算机补码运算背后的数学原理是什么?,知识出处《计算机组成原理》

我先说结论,数值在计算机中采用补码的形式存储,前面提到字符其实是数值十进制的映射,而十进制和二进制又有一层映射,所以本质来说,所有字符数值都是和二进制(当前情况是 8 位)的映射

我们在代码中(char a = 128)给变量a赋值 128 (十进制),其二进制原码为 1000 0000,很不幸的是,计算机采用 8 位,存储整型数值的时候,最高位用来表示符号位,也就是说计算机用 8 位表示数值的时候只能表示 $-128 \sim 127$ 这个范围。对于 128 的二进制源码其实它溢出到最高位了(最高位是符号位,只有 $2^7 =128$ 个数值了),127 的二进制原码为0111 1111,将它 $+1$ 也就是 128 ,它最后数值溢出到了最高位符号位上了,计算机会将它认为是负数,会按照 原码 -> 反码 -> 补码 的过程将其转换成补码存储。

当计算机需要读取它的十进制数值的时候,会再将 补码 -> 反码 -> 原码 -> 十进制呈现在屏幕上。这样就是为什么最后再输出十进制的时候会变成 -128 了。根据上述原理,下述代码同样的就是 -127 (十进制数值)

1
2
char a = 129;
printf("字符A的十进制表示为:%d\n",a);
image-20220810004238779

现在查看如下代码,请判断其是否合法?如果合法其输出结果是什么?

1
2
3
char a = '33';
printf("字符A的十进制表示为:%d\n",a);
printf("字符A的字符表示为:%c\n", a);
image-20220810004446624

答:“合法但是有问题”,上述代码会根据编译器的不同而不同,但是最多报个警报,并不会终端程序的运行,代码运行结果如下:

  • 字符A的十进制表示为:51
  • 字符A的字符表示为:3

看着这个输出结果,更加迷惑了,明明赋值的 33 输出一个 51 一个给我 3 .

image-20220810004905053

原因:在上面的单引号和双引号的部分我说过,单引号('')表示字符,也就是说,你赋值的'33'意味着你告诉计算机我要赋值给这个变量a一个字符33,计算机说:好的,字符'33'赋值给…..嗯?我这ASCII码表里可没有你说的字符'33',怎么办?它在这里做了一个模运算,伪代码应该是这样数值%10这里的模运算的结果是将无限的整型数值映射到了ASCII码表里有的 $0 \sim 9$ 字符

我们示例中的'33'经过模运算就是字符'3',查询ASCII码表可知,字符'3'对应的十进制整型数值为 51 。

类型转换带来的未定义行为

关于未定义行为(Undefined behavior)的解释如下:

In computer programming, undefined behavior (UB) is the result of executing a program whose behavior is prescribed to be unpredictable, in the language specification to which the computer code adheres. This is different from unspecified behavior, for which the language specification does not prescribe a result, and implementation-defined behavior that defers to the documentation of another component of the platform (such as the ABI or the translator documentation).

In the C community, undefined behavior may be humorously referred to as “nasal demons“, after a comp.std.c post that explained undefined behavior as allowing the compiler to do anything it chooses, even “to make demons fly out of your nose”.

内容来源:维基百科Undefined behavior

简述来说,未定义行为并不是错误,更多的来说是因为C的标准定义并没有对此进行详细的标准定义,以至于其对于不同的编译器可能会存在不同的处理方式,最终出现对于不同的编译器来说,运行结果不同的结果。

现在来尝试观察如下代码,分析其输出结果:

1
2
3
4
5
int main() {
float a = 1.5;
int b = 2;
printf("test: %d", (a+b) / 2);
}

上述代码运行结果:我不好说,因为它已经进入了未定义行为了

如果你了解隐式类型转换的话,对于上述运行的来说变量afloat类型, 和int类型的变量b进行加和的时候编译器会将b转换成float类型再参与运算,你发现了吗?其运算结果是float类型,也就意味着在printf()函数中使用%d输出float值就会出现未定义行为了

现在对上述代码进行微小改动,来参考如下代码,思考其运行结果:

1
2
3
4
5
6
int main() {
float a = 1.5;
int b = 2;
int c = a + b;
printf("test: %d", c / 2);
}

运行结果:test:1

你会发现**对于c=a+b来说,会将a+b其值的float类型转换为int**,虽然会丢失一定的精度,但是不至于因为%d而出现未定义行为。

现在来陈胜追击,查看一段奇怪的代码,思考其运行结果:

1
2
3
4
5
6
7
#include<stdio.h>                                              
int main(){
int i=10;
float x=43.2892f;
printf("i=%f x=%d \n",i,x);
return 0;
}

代码出自:【Stack Overfl0w】 Why are the int and float passed in printf going to the wrong positions in the format string?]

image-20220902204628036

根据一开始的代码可以得知,这段代码也会因为%f%d而出现未定义行为,其提问者的运行结果如下:

1
i=43.289200  x=10      

其中一个高赞回答:

What you’re doing invokes undefined behavior1, but looking at the resulting assembly using GCC on a platform with the System V AMD64 ABI we might formulate a hypothesis. The floating-point value is passed in the xmm0 register (an SSE register), while the integer is passed in the esi register (a general register). Presumably, your printf implementation expects floating-point numbers to be passed in SSE registers and integers to be passed in general registers, and simply picks the xmm0 register to read from when it encounters the first %f (and vice versa).

简述来说,就是如上代码的操作会引发未定义行为,在具有System V AMD64 ABI 的平台上使用 GCC 查看其汇编,推出的假设是:浮点值在寄存器(SSE 寄存器)中传递,而整数在寄存器(通用寄存器)中传递,因为调用关系问题,导致寄存器读取取反了。

回答作者:You

C函数问题

关于 Main 函数的有趣问题

如果你使用Visual Studio集成的 C/C++ 编译器,在编写main()函数的时候将其写为mian(),编译器并不会保mian()的错误,反而是报错如下所示:

image-20220902190945178 image-20220902191052794

Scanf函数读取问题

Scanf作为C语言入门级别函数,功能上来说学习过C语言的人都理解,现在来思考一下下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
printf("请输入字符\n");
char tempdata;
//读取字符
scanf("%c", &tempdata);
while (tempdata != '!')
{
//输出读取的字符
printf("输入的字符为:%c\n", tempdata);
scanf("%c", &tempdata);
}
}

现在如果读取字符的时候,输入一个 A (或者任意一个单字符),猜一下输出结果是什么?会是输入的字符为:A吗?又或者是其他结果?

image-20220829055941223

很不幸,它的运行结果如下所示:

image-20220829060030262

这就奇了个怪了,scanf函数不应该不读取回车字符吗?

有了上面的运行结果,现在来看看下面的代码,猜一下运行结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
printf("请输入数值\n");
int tempdata;
//读取字符
scanf("%d", &tempdata);
while (tempdata != -1)
{
//输出读取的字符
printf("输入的数值为:%d\n", tempdata);
scanf("%d", &tempdata);
}
}

现在输入 2 (或者任意一个单数值),然后回车确认,猜猜运行结果是什么?运行结果如下:

image-20220829061227547

发现了,scanf对于%c即字符的处理和其他的处理是不一样的,我猜测是因为字符采用的是ASCII码表存储的,因为ASCII存在各种控制字符,包括“回车”和“空格”等控制字符,事实上对于一般除字符格式的字符都会在scanf前将空格等控制字符删除,而字符缺不会做这个处理,也就出现了上面的字符读取了回车字符的情况

关于上述scanf读取回车字符的解决方案如下:

1
scanf(" %c",&value);	//在%c前面加个空格就可以读取输入字符的时候避免回车和空格

当然你也可以选择使用其他的读取输入的函数,例如:sscanf等等来解决上述问题。

题外话:对于读取输入函数scanf来说,其实它会将键盘的输入读取到缓存区,你可以把它理解为队列,读取的字符压入队列,先入先出,对于每次输入结束都会在其末尾加上回车符(编译器自动),但是对于字符来说,编译器并没有给我的缓存区加上回车符,而是我们输入的时候把回车符输入了,由于字符的特殊性,导致读取字符的时候编译器并没有给我们删除前面的控制字符,使得我们读取到了回车字符。

因为缓存区的存在,所以对于上述输入来说,例如:我们希望输入ABCD四个字符,其实可以不需要A,回车,B,回车,这样输入,其实我们可以直接在第一次输入就输入ABCD,程序会依次将第一次输入存储到缓存区,然后挨个读取。

返回值数组

C不允许函数返回一个数组,但是可以通过返回一个类型指针来代替(顺序表)。

关于数组指针,需要注意的是它是线性的

数组名本身其实是一块地址的首地址,创建一个数组例如int a[10]类型的数组,编译器会根据我们传入的数组包含的数组个数(10),来开辟一块大小为 $10 \times 4byte$ 大小空间,并将空间的首地址返回给所其的数组名称a,所以本质上来说,数组名其实是一块首地址的指针;至于我们根据数组下标的访问数组,其实是根据首地址的加和计算出来的。所以参考如下代码:

1
2
3
4
5
6
7
8
int main() {
int a[2] = { 1,2 }; //创建一个一维数组
int* b = a; //声明并定义 int 类型指针,其初始值为数组 a 的“首地址”
for (int i = 0; i < 2; i++)
{
printf("其值为:%d\n", b[i]); //输出其值
}
}

其运行结果:

image-20220901074210064

对于二维及以上的数组来说,如果采用数组指针的方式要获取其值,不可以采用常规的a[n][n]...来获取值,其本质原因是因为数组本身就是线性的,二维数组本质是上还是线性的存储,参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
int main() {
int a[2][2] = { {1,2},{3,4} }; //声明并定义二维数组
int* b = a; //int 类型的指针
for (int j = 0; j < 2; j++)
{
for (int i = 0; i < 2; i++)
{
printf("其值为:%d\n", b[j][i]); //遍历数组
}
}
}

如果你运行上述代码,会发现它是无法运行的,原因就是其本质是线性存储的,如果你希望正常读取二维数组的内容,可以修改成如下:

1
2
3
4
5
6
7
8
int main() {
int a[2][2] = { {1,2},{3,4} }; //声明并定义二维数组
int* b = a; //int 类型的指针
for (int j = 0; j < 4; j++)
{
printf("其值为:%d\n", b[j]); //遍历数组
}
}

上述代码也印证了数组名是个地址,而且二维数组是线性存储的,其运行结果如下:

image-20220901074633848