前言

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

typedef 和 define 的区别

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

#define ElemType int

typedef int NewType

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

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

#define ElemType int*

int main(){
	ElemType a,b;    
}

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

typedef int* ElemType

int main(){
    ElemType a,b;
}

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

typedef 和 struct 的使用

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

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

struct{
    int a;
    float b;
}Test1;

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

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

struct Test
{
    int a;
    float b;
}Test1;

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

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

typedef struct{
    int a;
    float b;
}Test1;

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

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

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

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

int main(){
    Test1 a;
    Test2 b;
    struct Test c;
}

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

//这是上面的代码
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语言中,这种写法是不规范的,也是不完全合法的,例如下面的代码:

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

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

image-20220809025647389

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

image-20220809025317930

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

image-20220809025404051

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

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

char c = "b";

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

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

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

C中各种类型的问题

所占内存

类型字节
int4字节(Byte)
char1字节(Byte)
float4字节(Byte)
double8字节(Byte)
long4字节(Byte)

char类型

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

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

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

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 名称描述
1282008010000000€欧元符号
1292018110000001
1302028210000010‚单个低9引号
1312038310000011ƒƒƒ拉丁小写字母f
1322048410000100„双低9引号
1332058510000101…水平省略号
1342068610000110†匕首
1352078710000111‡双匕首
1362108810001000ˆˆˆ修饰语字母抑扬音
1372118910001001‰千分号
1382128A10001010ЊŠ拉丁大写字母S
1392138B10001011‹单左角引号
1402148C10001100ŒŒŒ拉丁字母连字OE
1412158D10001101
1422168E10001110ŽŽ 拉丁大写字母Z
1432178F10001111
1442209010010000
1452219110010001‘左单引号
1462229210010010’右单引号
1472239310010011“左双引号
1482249410010100”右双引号
1492259510010101•子弹
1502269610010110–破折号
1512279710010111—破折号
1522309810011000˜˜˜小波浪号
1532319910011001™商标标志
1542329A10011010ššš拉丁小写字母S
1552339B10011011›单个右指向角引号
1562349C10011100œœœ拉丁文小连字oe
1572359D10011101
1582369E10011110žž 拉丁小写字母z
1592379F10011111ŸŸŸ拉丁大写字母Y
160240A010100000   不间断空间
161241A110100001¡¡¡倒感叹号
162242A210100010¢¢¢分号
163243A310100011£££英镑符号
164244A410100100¤¤¤货币符号
165245A510100101¥¥¥日元符号
166246A610100110¦¦¦管道,竖线损坏
167247A710100111§§§分区标志
168250A810101000¨¨¨间隔透析-umlaut
169251A910101001©©©版权标志
170252AA10101010ªªª女性顺序指示器
171253AB10101011«««左双角引号
172254AC10101100¬¬¬不签名
173255AD10101101­­­软连字符
174256AE10101110®®®注册商标标志
175257AF10101111¯¯¯间隔宏-上划线
176260B010110000°°°学位标志
177261B110110001±±±正负号
178262B210110010²²²上标二平方
179263B310110011³³³上标三方
180264B410110100´´´急性口音-间隔锐
181265B510110101µµµ微标志
182266B610110110稻草人标志-段落标志
183267B710110111···中间点-格鲁吉亚逗号
184270B810111000¸¸¸间距塞迪利亚
185271B910111001¹¹¹上标一
186272BA10111010ººº男性顺序指示器
187273BB10111011»»»右双角引号
188274BC10111100¼¼¼分数的四分之一
189275BD10111101½½½分数的一半
190276BE10111110¾¾¾分数四分之三
191277BF10111111¿¿¿倒问号
192300C011000000ÀÀÀ拉丁大写字母A
193301C111000001ÁÁÁ拉丁大写字母A
194302C211000010ÂÂÂ拉丁大写字母A
195303C311000011ÃÃÃ拉丁大写字母A
196304C411000100ÄÄÄ拉丁大写字母A
197305C511000101ÅÅÅ拉丁大写字母A
198306C611000110ÆÆÆ拉丁大写字母AE
199307C711000111ÇÇÇ拉丁大写字母C
200310C811001000ÈÈÈ拉丁大写字母E
201311C911001001ÉÉÉ拉丁大写字母E
202312CA11001010ÊÊÊ拉丁大写字母E
203313CB11001011ËËË拉丁大写字母E
204314CC11001100ÌÌÌ拉丁大写字母I
205315CD11001101ÍÍÍ拉丁大写字母I
206316CE11001110ÎÎÎ拉丁大写字母I
207317CF11001111ÏÏÏ拉丁大写字母I
208320D011010000ÐÐÐ拉丁大写字母ETH
209321D111010001ÑÑÑ拉丁大写字母N
210322D211010010ÒÒÒ拉丁大写字母O
211323D311010011ÓÓÓ拉丁大写字母O
212324D411010100ÔÔÔ拉丁大写字母O
213325D511010101ÕÕÕ拉丁大写字母O
214326D611010110ÖÖÖ拉丁大写字母O
215327D711010111×××乘法
216330D811011000ØØØ拉丁大写字母O
217331D911011001ÙÙÙ拉丁大写字母U
218332DA11011010ÚÚÚ拉丁大写字母U
219333DB11011011ÛÛÛ拉丁大写字母U
220334DC11011100ÜÜÜ拉丁大写字母U
221335DD11011101ÝÝÝ拉丁大写字母Y
222336DE11011110ÞÞÞ拉丁大写字母THORN
223337DF11011111ßßß拉丁小写字母sharp s - ess-zed
224340E011100000ààà拉丁小写字母a
225341E111100001ááá拉丁小写字母a
226342E211100010âââ拉丁小写字母a
227343E311100011ããã拉丁小写字母a
228344E411100100äää拉丁小写字母a
229345E511100101ååå拉丁小写字母a
230346E611100110æææ拉丁小写字母a
231347E711100111ççç拉丁小写字母c
232350E811101000èèè拉丁小写字母e
233351E911101001ééé拉丁小写字母e
234352EA11101010êêê拉丁小写字母e
235353EB11101011ëëë拉丁小写字母e
236354EC11101100ììì拉丁小写字母i
237355ED11101101ííí拉丁小写字母i
238356EE11101110îîî拉丁小写字母i
239357EF11101111ïïï拉丁小写字母i
240360F011110000ððð拉丁小写字母eth
241361F111110001ñññ拉丁小写字母n
242362F211110010òòò拉丁小写字母o
243363F311110011óóó拉丁小写字母o
244364F411110100ôôô拉丁小写字母o
245365F511110101õõõ拉丁小写字母o
246366F611110110ööö拉丁小写字母o
247367F711110111÷÷÷除号
248370F811111000øøø拉丁小写字母o
249371F911111001ùùù拉丁小写字母u
250372FA11111010úúú拉丁小写字母u
251373FB11111011ûûû拉丁小写字母u
252374FC11111100üüü拉丁小写字母u
253375FD11111101ýýý拉丁小写字母y
254376FE11111110þþþ拉丁小写字母thorn
255377FF11111111ÿÿÿ拉丁小写字母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 (十进制数值)

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

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

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的标准定义并没有对此进行详细的标准定义,以至于其对于不同的编译器可能会存在不同的处理方式,最终出现对于不同的编译器来说,运行结果不同的结果。

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

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

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

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

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

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而出现未定义行为。

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

#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而出现未定义行为,其提问者的运行结果如下:

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语言的人都理解,现在来思考一下下面一段代码:

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函数不应该不读取回车字符吗?

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

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读取回车字符的解决方案如下:

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

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

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

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

返回值数组

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

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

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

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]...来获取值,其本质原因是因为数组本身就是线性的,二维数组本质是上还是线性的存储,参考如下代码:

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]);	//遍历数组
		}
	}
}

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

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

Q.E.D.


赤脚踩在明媚的沙滩上,我看见了你闪耀的双眼,柔软的头发,我便心有所属