那些年,C给我们留下的思考——换一种角度重释C语言

探索环境及工具

环境:VC6
工具:VC++6.0

探索内容

变量篇

全局变量和局部变量到底是什么,为什么全局变量可以不用初始化,而局部变量必须初始化

首先,我们需要回顾一下,什么是全局变量,什么是局部变量:

  • C语言允许在所有函数的外部定义变量,这样的变量称为全局变量(Global Variable)
  • 局部变量也称为内部变量,它是在函数内部定义的,其作用域仅限于函数内部

紧接着就是我们的探究部分,首先按照定义定义出全局变量和局部变量:

#include <stdio.h>

int x;		//全局变量x

int add(){

	int y;		//局部变量y
	y=0;
	return 0;
}

int main(){

	add();
	getchar();

}

然后在add()和getchar()处下断点调试如下:

查看汇编如下:
add()函数未调用前,如图中汇编视图可知,此时全局变量已有初值,由此可知全局变量在编译时,编译器就已经在全局变量区分配好了内存空间,且编译器也为其完成了相应初始化,而y由于函数add()还未被调用,未在栈空间分配内存,故显示not found:
在这里插入图片描述
add()函数调用之后,定义了局部变量y,此时编译器为变量y划分一个栈空间,而y的值为栈空间里的一个随机数据,未被初始化,不能使用(虽然vc6使用未被初始化的局部变量不会暴错,但使用无意义)
在这里插入图片描述
函数内执行到初始化语句后,y被成功初始化为0,数据初始化完成。
在这里插入图片描述
add函数执行结束后,编译器回收其对应栈空间,局部变量y,也重新回到not found 状态:
在这里插入图片描述
由上述的汇编探究,我们应该能更加深切的理解什么是局部变量,什么是全局变量,已经为什么我们可以不用初始化全局变量,而对于局部变量,必须初始化之后,才能使用,答案都在我们的探究过程中,总结如下:

  • 对于全局变量,编译器在编译的时候,就会为其在全局变量区分配好内存,并完成相应的初始化操作
  • 而对于局部变量,只有在函数调用的时候,才会为其分配栈空间,分配之后,局部变量的值也为栈空间的随机值,所以需要我们自己完成相应的初始化操作再使用

无符号和有符号到底是怎么辨别

在我接触计算机语言以来,无符号和有符号的使用一直挺模糊的,但是对于它的定义,我确记得很清楚,就是有符号数最高位就是符号位,为1就代表负数,反之代表正数,而对于无符号数,就没有符号位之说,数为多大就表示多大,这就是我之前对有符号数和无符号数的理解。

学而不思则罔,在学习的路上,我们都有各种不明白和不理解,而对于有符号和无符号,我也曾有过一个问题,既然有无符号数,也有有符号数,那么计算机到底怎么区分一个无符号数和有符号数,到底怎么存储无符号数和有符号数的呢?

下面就让我们从汇编的世界,来一探究竟,计算机的世界里有符号数和无符号数到底有什么不一样。

我们就以图中的值-1来探究,众所周知,计算机存储数是按照补码的方式来存储的,而-1的补码全为1,而int类型长度为4个字节,也就是32位,转为16进制表示就为0xFFFF_FFFF

#include <stdio.h>

int main(){

	int x = -1;     //默认 signed int
	unsigned int y = -1;
	printf("有符号:%d \n无符号:%u\n",x,y);
	

	printf("<-------------------------->\n");

	int a = 0xFFFFFFFF;     //默认 signed int
	unsigned int b = 0xFFFFFFFF;
	printf("有符号:%d \n无符号:%u\n",a,b);

}

打印结果如下:
在这里插入图片描述
通过汇编查看如下:
在这里插入图片描述
由上述汇编代码部分可知,有符号数和无符号数的存贮方式几乎一模一样,从汇编上看没有差异,若真如此,那我们换一种打印方式来验证一下,将有符号数,按照无符号数的方式打印,将无符号数,按照有符号数打印,其结果如下:

在这里插入图片描述
如图可知,果如我们猜测的那样,不管是有符号数,还是无符号数,都是我们人为的一种规定,计算机只知道存贮,实际上,计算机基本不区分有符号数和无符号数,这些都是人为的规定,数都是按照补码的形式存储在计算机中,判断一个数是有符号数还是无符号数,完全是人的主观行为,计算机本身并不区分,它只将数的补码存储,怎么读取,按照有符号的方式,还是无符号的方式,都是取决于使用它的人怎么看待。

但是无符号数是不是就和有符号数真的没有区别呢?在存储上确实如此,但是在类型转换和比较时,是有区别的,实例代码如下:

  • 类型转换时,有符号数需要根据符号位补0或者1,而无符号数直接补0即可
    在这里插入图片描述
    比较时,示例如下:
    在这里插入图片描述
    上述总结如下:
  • 有符号数和无符号数在存储上并无区别,都是以补码的形式存储在计算机内,计算机本身并不区分有符号还是无符号,它只负责将数按照补码的形式存储,至于存储的是有符号数还是无符号数,取决于使用的人怎么看待。
  • 但是有符号数在类型转换和比较的时候有区别,需要我们正确辨别理解

同样都是4个字节,为什么float比int存储的范围大那么多

在我们学习C语言数据类型的时候,我相信很多人都会有疑问,为什么int和float都是4个字节,存储数据的范围却差距那么大。这是为什么呢?

我们都知道,整数是按照补码的形式存储在计算机中,那么浮点型呢?
通过愈来愈深入的学习,我们知道浮点型数据和整数类型的数据是由于存储的方式不同,才导致产生如此的差别。

那么浮点型数据到底采用什么样的存储方式呢?浮点型数据在存储方式上都是遵从IEEE编码规范的,存储方式如图所示:
在这里插入图片描述
那具体是如何存储的呢?

首先,我们需要先将需要存储的十进制数转为二进制,整数转二进制很简单,是学计算机的基本功,我们平时用的很多,转的方式也有很多,最经典的就是除二取余法,但是小数转二进制,用的不是很多,笔者就示例一下小数转二进制的方式及一个现象。

例如小数0.25和0.4,转为二进制过程如下:
在这里插入图片描述
由上述转化过程可知,小数不一定能完整的转化为二进制,如0.4,这也就意味着,浮点型数据描述小数时,不是百分之百准确,是有一定的精确位数,也就是精度。

那将一个十进制数转化为二进制数后,又怎样按照IEEE编码规范存储呢?我以浮点数8.25,示例如下:

在这里插入图片描述
如上图所示,浮点数8.25按照IEEE编码规范存储值为0x41010000,通过汇编验证如图所示:
在这里插入图片描述
由上述汇编可知,浮点型数据确如我们所描述的那样,采用IEEE编码规范存储,接下来,我们再看看浮点数的精度:

  • float:2^23(尾数)=8388608,一共7位,这意味这最多只能有7位有效数字;
  • doubel:2^52=4503599627370496,一共16位,这意味着最多只能有16位有效数字

最后再来讨论我们的问题,也就是浮点数存储的值的范围,由于浮点数采用IEEE编码规范存储,故其存储值的范围取决于其指数范围,float的指数部分除去符号位为7位,也就是2^7,指数范围为-127~+128,故其存储范围为-3.40E+38 ~ +3.40E+38,而doubel指数范围为 -2^1023 ~ +2^1024,也即-1.79E+308 ~+1.79E+308。

C语言中的枚举类型是什么,为什么要有枚举类型

C语言中为什么要有一种枚举类型,直接使用宏定义不行么?

#define命令虽然能解决问题,但也带来了不小的副作用,导致宏名过多,代码松散,看起来总有点不舒服。C语言提供了一种枚举(Enum)类型, 枚举类型是预处理指令#define的替代,枚举与宏非常相似。 宏在预处理阶段用相应的值替换名称,枚举在编译阶段用相应的值替换名称。不仅可以解决宏定义带来的弊端,而且枚举类型还可以用来限制用户的输入,在很多开发场景中,我们都会用到枚举类型来进行限制。

探究如下:
在这里插入图片描述
通过汇编查看如下:

在这里插入图片描述
如上述所示,确如查阅资料所述,枚举在编译阶段用相应的值替换名称。

在使用C语言枚举类型时需要注意以下几点:

  • 不能对枚举常量进行赋值操作(定义枚举类型时除外);
  • 枚举常量和枚举变量可以用于判断语句,实际用于判断的是其中实际包含的元素序号值;
  • 一个整数不能直接赋值给一个枚举变量,必须用该枚举变量所属的枚举类型进行强制类型转换;
  • 使用常规的手段无法输出枚举常量所对应的字符串,因为枚举常量为整型值;
  • 在使用枚举变量的时候,我们不关心其值的大小,只是关心其表示的状态,即其值的含义是什么。

函数篇

函数的参数到底是怎么传递的,值传递和地址传递到底是怎么一回事

首先,我们回顾一下,值传递和地址传递是怎么一回事.

  • 值传递:使用变量、常量、数组元素作为函数参数,实际是将实参的值复制到形参相应的存储单元中,即形参和实参分别占用不同的存储单元,这种传递方式称为“参数的值传递”或者“函数的传值调用”。
  • 地址传递:这种方式使用数组名或者指针作为函数参数,传递的是该数组的首地址或指针的值,而形参接收到的是地址,即指向实参的存储单元,形参和实参占用相同的存储单元,这种传递方式称为“参数的地址传递”。

之前,我们都是只是知道值传递和地址传递,理解和使用,并没有真正的通过汇编的角度来认识到底什么是值传递和地址传递,那么笔者从一个简单的交换示例,来从汇编的角度来真正理解何为值传递和地址传递。
示例如下:

#include <stdio.h>

void swap_value(int a ,int b){		//值传递

	printf("交换前(swap_value函数中):\n a=%d \n b=%d\n",a,b);
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
	printf("交换后(swap_value函数中):\n a=%d \n b=%d\n",a,b);

}
void swap_addr(int* a ,int* b){		//地址传递

	printf("交换前(swap_addr函数中):\n a=%d \n b=%d\n",*a,*b);
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
	printf("交换后(swap_addr函数中):\n a=%d \n b=%d\n",*a,*b);

}

int main(){

	int a=5;
	int b=10;
	printf("交换前(main函数中):\n a=%d \n b=%d\n",a,b);
	swap_value(a,b);
	printf("swap_value交换后(main函数中):\n a=%d \n b=%d\n",a,b);
	swap_addr(&a,&b);
	printf("swap_addr交换后(main函数中):\n a=%d \n b=%d\n",a,b);
	getchar();
	
}

运行结果如下:
在这里插入图片描述
从C语言代码执行结果我们也可以清晰的看出,值传递并不会影响真正的实参值,而地址传递传递,形参的变化会使实参也发生相应的变化。
那么接下来让我们从汇编的角度来一探究竟:
在这里插入图片描述
从汇编指令可以看出,值传递的时候使用的是move指令,直接将参数的值压入栈中传递,而地址传递的时候,是使用lea指令,将参数的地址压入栈中进行传递,故而,形参在取得实参地址之后,与实参共同拥有一段内存空间,形参的变化也就是实参的变化。
通过上述探究,总结如下:

  • 值传递的特点是单向传递,即主调函数调用时给形参分配存储单元,把实参的值传递给形参,在调用结束后,形参的存储单元被释放,而形参值的任何变化都不会影响到实参的值,实参的存储单元仍保留并维持数值不变。
  • 地址传递的特点是形参并不存在存储空间,编译系统不为形参数组分配内存。数组名或指针就是一组连续空间的首地址。因此在数组名或指针作函数参数时所进行的传送只是地址传送,形参在取得该首地址之后,与实参共同拥有一段内存空间,形参的变化也就是实参的变化。

函数中涉及的堆、栈到底又是什么

谈及堆栈,我们就会想到C语言中到底有多少内存模型,通过查阅资料,简单了解如下:

C语⾔的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:

  1. 栈区:存放函数的参数值、局部变量等,由编译器⾃动分配和释放,通常在函数执⾏完后就释放了,其操作⽅式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很⾼,但是分配的内存量有限,⽐如iOS中栈区的⼤⼩是2M。
  2. 堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放⼯作,需要⽤程序区释放。分配⽅式类似于数据结构中的链表。在iOS开发中所说的“内存泄漏”说的就是堆区的内存。
  3. 静态区:全局变量和静态变量(在iOS中就是⽤static修饰的局部变量或者是全局全局变量)的存储是放在⼀块的,初始化的全局变量和静态变量在⼀块区域,未初始化的全局变量和未初始化的静态变量在相邻的另⼀块区域。程序结束后,由系统释放。
  4. 常量区:常量存储在这⾥,不允许修改。
  5. 代码区:存放函数体的⼆进制代码。

接下来就让我们从汇编的世界,加深一下对堆栈的理解:

#include <stdio.h>
#include <malloc.h>

int add(int a ,int b){

	return a+b;

}

int main(){

	add(5,10);
	int* p=(int*)malloc(sizeof(int));
	if(p)
		printf("Request Merroy Error!");
	else
		printf("Memory Allocated at: %x/n",p);
	free(p);

	getchar();

}

(栈)汇编视角如下:
在这里插入图片描述
(堆)视角如下:
在这里插入图片描述
调用malloc从堆中申请内存:

  1. call malloc
  2. call _nh_malloc_dbg
  3. call _heap_alloc_dbg
    ……

堆中申请内存完成:
在这里插入图片描述
总结上述操作如下:

  • 栈区是在函数内分配的内存,存放函数的参数值、局部变量等,由编译器⾃动分配和释放,通常在函数执⾏完后就释放了;
  • 堆区就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放⼯作,需要⽤程序区释放。

分支语句篇

同样是分支语句,为什么条件多的时候使用Switch比使用if-else效率更高

if-else和switch是我们都很熟悉的分支语句,那么为什么条件很多时候,使用switch的效率更高呢,让我们从汇编的角度来深入理解一下:

#include <stdio.h>

void My_If(int x){
	
	if(x==1)
		printf("A\n");
	else if(x==2)
		printf("B\n");
	else
		printf("S\n");

}
void My_Switch(int x){
	
	switch(x){
	
		case 1: printf("a\n");break;
		case 2: printf("b\n");break;
		case 3: printf("c\n");break;
		default:printf("s\n");
	
	
	}

}

int main(){

	My_If(1);
	My_Switch(2);
	getchar();

}

(条件少)if-else汇编部分:
在这里插入图片描述
(条件多)if-else汇编部分:
在这里插入图片描述

(条件少)switch汇编部分:
在这里插入图片描述
(条件多)switch汇编部分:
在这里插入图片描述
通过上述汇编代码可知:

if-else语句条件多和少不会产生任何影响,都是使用cmp指令比较,而switch当条件很多的时候,会通过采用表的形式,在条件的多的时候,会产生一张虚拟表,可以通过查表的方式减少cmp指令的使用,提高了处理效率。故在条件多的时候,使用switch的方式效率更高。

循环语句篇

while do、do while和for的汇编有什么不一样

while-do、do-while、for是我们C语言种使用最多的循环语句,让我用一共简单的C程序来开启汇编的探索之旅:

#include <stdio.h>

void My_While_Do(int x){		//while-do循环
	
	while(x--){
		printf("While_do: \n x=%d\n",x);
	}

}

void My_Do_While(int x){	 //do-while循环
	
	do{
		printf("Do_While: \n x=%d\n",x);
	}while(x--);

}

void My_For(int x){		//for循环
	
	for(;x>0;x--){
	
		printf("For: \n x=%d\n",x);
		
	}

}

int main(){

	My_While_Do(3);
	My_Do_While(3);
	My_For(3);
	getchar();

}

汇编视角如下:

  1. while-do:
    在这里插入图片描述

  2. do-while:
    在这里插入图片描述

  3. for:
    在这里插入图片描述

通过上述汇编视角可知:
while-do、do-while和for汇编实现上并无多大区别,而while-do和do-while就是先执行在判断,还是先判断在执行的问题,着都是我们所熟知的,使用效率上并无差别,我们可以根据使用习惯和情况选择使用。

巧妙探究for循环的执行流程

对于for循环,是我们比较喜欢使用的一种循环,但是关于它的执行流程,一开始学的时候有点懵,现在,让我们通过一种巧妙的方式来证实我们那些年熟记的for循环执行顺序:

#include <stdio.h>

void R1(){
	
	printf("R1 \n");

}

int R2(){
	
	printf("R2 \n");
	return -1;

}

void R3(){
	
	printf("R3 \n");

}

void My_For_Route(){
	
	for(R1();R2();R3()){
	
		printf("R4 \n");
		
	}

}

int main(){

	My_For_Route();
	getchar();
}

在这里插入图片描述

如上述所示,证实了我们之前所用所学时候的调用判断流程,清晰而又简单,可以帮助我们更加直观的理解for循环的执行顺序。

数组篇

数组和指针的区别与联系

我们都知道,数组有时候使用起来和指针很相似,数组名就代表数组首地址,编译器把数组名标识为首地址元素地址的别名。

查阅资料可知,指针与数组最本质的区别就是:

指针是变量,而数组名是常量

既然数组名是常量,那么就会有以下四种区别:

  • 赋值问题
    在这里插入图片描述

  • 自增自减操作
    在这里插入图片描述

  • 内存分配问题
    在这里插入图片描述
    汇编角度:
    在这里插入图片描述

  • 锯齿数组问题
    在这里插入图片描述
    汇编视角:
    在这里插入图片描述

指针数组和数组指针汇编的理解

指针数组和数组指针,是我们很容易混淆的两个概念,从根本上来剖析容易混淆的原因,就是符号的优先级不熟悉,记不住"*“和”[]“的优先级哪一个更高,因而就对数组指针和指针数组的概念模糊不清。
在这里插入图片描述
知道了”*“和”[]"的优先级,理解数组指针和指针数组就很简单了,下面让我们从简单的示例以汇编的角度来进一步剖析:

#include <stdio.h>

int main(){
	
	int arr[6] = {1,2,3,4,5,6};

	
	int *p1[6]={	//因为"[]"的优先级比"*"高,故p1先与"[]"构成数组,即为指针数组
				&arr[0],&arr[1],&arr[2],&arr[3],&arr[4],&arr[5]
				};    

	int (*p2)[6]=&arr;	//"()"的优先级比"[]"高,故p2先与"()"构成指针,即为数组指针
	getchar();

}

汇编视角:
在这里插入图片描述
由上述分析探究可知:

对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在32 位系统下永远占 4 字节,至于它指向的数组占多少字节,需要视具体情况分析。

结构体篇

为什么结构体作为参数传递的时候,通常都是选择传递结构体指针

为什么我们开发编程的时候,一使用到结构体作为参数传递,为什么几乎哦都是使用其对应的结构体指针,不能像使用数组一样,直接传递结构体名字么?让我们用一个简单的示例来一探究竟。

#include <stdio.h>

struct Student{

	int id;
	char gender;
	double score;

}s1;

void Student_Print_Value(Student s){

	printf("Student信息如下:\n %d\n %c \n %f\n",s.id,s.gender,s.score);

}
void Student_Print_Addr(Student* s){

		printf("Student信息如下:\n %d\n %c \n %f\n",s->id,s->gender,s->score);
}

int main(){
	
	s1.id=1;
	s1.gender='m';
	s1.score=520;
	Student_Print_Value(s1);
	Student_Print_Addr(&s1);
	getchar();

}

汇编角度:
在这里插入图片描述
从上述汇编角度,我们可以清晰的发现:

当直接以结构体变量名字作为参数传递时,底层采用的是拷贝复制传递的方式,也就是把传递的结构体变量的值依次拷贝入栈存入一处新的内存空间,而采用结构体指针传递的方式,是采用的地址传递的方式,将实参的地址传递给形参,操作的是同一块内存,节约空间,故我们编程开发时,为了节约空间和灵活性的考虑,一般都是采用结构体指针的方式传递参数。

结构体的大小为什么老是和我想得不一样

结构体的大小为什么老是和我们想的不一样,学习结构体的时候,使用sizeof()函数输出结构体大小的时候,总是和我们预估的大小对不上,这又是怎么一回事呢?
通过深入的学习和查阅资料,我们知道了编译器在优化时,都会默认采用一种字节对齐的方式来进行优化,是一种以空间换时间的优化方法。关于字节对齐,网上有很多参考资料,笔者就以最通俗易懂的概括如下:

  • 字节对齐:一个变量占用n个字节,则该变量的起始地址必须是n的整数倍,即存放起始的地址%n=0.
  • 如果是结构体,那么结构体的起始地址是其最宽数据类型成员的整数倍。

那么结构体的成员也遵守字节对齐么?
让我们通过C与汇编结合的方式去探究。

#include <stdio.h>

struct Student{

	int id;								//int  4字节
	char gender;						//char  1字节

}s1;									//sizeof(Student)=4+1=5字节?

struct Teacher{

	_int64 id;							//_int64  8字节
	char gender;						//char  1字节

}t1;										//sizeof(Teacher)=8+1=9字节?


int main(){
	
	s1.gender=100;
	s1.gender='m';
	t1.id=101;
	t1.gender='m';
	printf("sizeof student:\n %d   %d\n",sizeof(Student),sizeof(s1));
	printf("sizeof Teacher:\n %d   %d\n",sizeof(Teacher),sizeof(t1));
	getchar();

}

输出结果如下:
在这里插入图片描述
汇编视角:
在这里插入图片描述
如上述汇编所示:

结构体种确实存在字节对齐的现象,起始地址是其最宽数据类型成员的整数倍,且结构体的总大小:N=Min(最大成员,对齐参数)是N的整数倍。

那这种字节对齐的方式,我们可以改变么?就是不想采用默认的以空间换取时间的字节对齐方式
当然可以,但我们对空间要求较高的时候,可以通过#pragma pack(n)来改变结构体成员的对齐方式,示例如下:
在这里插入图片描述
汇编视角:
在这里插入图片描述
我们可以发现:

使用上述pragma pack并未真正改变字节对齐的存储方式,只是告知编译器按照我们预设的n字节方式对齐,但是实际的存储方式,仍然按照默认的字节方式存储,只是一种表像。

指针篇

指针到底神奇好用在哪里

谈到C语言,最灵活,最复杂的莫过于指针了,它是C语言种最重要、最灵活的一种结构。
那么指针到底有何神奇之处,让所有学C的人为之着迷。
查阅资料总结如下:

  1. 指针允许你以更简洁的方式引用大的数据结构

    程序的数据结构从原子级别的数据结构:整型、浮点型、字符型、枚举型,到分子级别的数组、结构体(又称为“记录”),再到数据结构中的队列、栈、链表、树等,无论如何复杂,数据结构总是位于计算机的内存中,因此必有地址。利用指针就可以使用地址作为一个完整值的速记符号,因为一个内存地址在内部表示为一个整数。当数据结构本身很大时,这种策略能节约大量内存空间

  2. 指针使程序的不同部分能够共享数据

    类似于共享内存,如果将某一个数据值的地址从一个函数传递到另外一个函数,这两个函数就能使用同一数据。

  3. 利用指针,能在程序执行过程中预留新的内存空间

    大多数情况下,可以看到程序使用的内存是通过显式声明分配给变量的内存(也就是静态内存分配)。这一点对于节省计算机内存是有帮助的,因为计算机可以提前为需要的变量分配内存。但是在很多应用场合中,可能程序运行时不清楚到底需要多少内存,这时候可以使用指针,让程序在运行时获得新的内存空间(实际上应该就是动态内存分配),并让指针指向这一内存更为方便。

  4. 指针可以用来记录数据项之间的关系

    在高级程序设计应用中,指针被广泛应用于构造单个数据值之间的联系。比如,程序员通常在第一个数据的内部表示中包含指向下一个数据项的指针(实际上就是链表了),来说明这两个数据项之间有概念上的顺序关系。

其它网上相关使用指针可以带来如下的好处:

(1)可以提高程序的编译效率和执行速度,使程序更加简洁。

(2)通过指针被调用函数可以向调用函数处返回除正常的返回值之外的其他数据,从而实现两者间的双向通信。

(3)利用指针可以实现动态内存分配。

(4)指针还用于表示和实现各种复杂的数据结构,从而为编写出更加高质量的程序奠定基础。

(5)利用指针可以直接操纵内存地址,从而可以完成和汇编语言类似的工作。

(6)更容易实现函数的编写和调用

指针和字符串之间的关联探究

字符串也是C语言中使用较多的一种类型,那字符串和指针之间又有什么联系呢?常量区又与字符串之间又是怎么一回事呢?
让我们从汇编的角度来探究这些问题

#include <stdio.h>

int main(){
	
	
	char str[]={'A','B','C','D','\0'};
	char str1[] = "ABCD";
	char* str2 = "ABCD";
	printf(" %s\n %s\n %s\n",str,str1,str2);
	getchar();

}

汇编视角:
在这里插入图片描述
从上述汇编,我们可以看出:

当使用第一种方式定义字符串时,底层是之间将对应的Ascill码值放入数组存储的位置,而第二种方式,是将常量区的指定个数字符复制到对应数组存储位置,以便进行修改等相关操作,而第三种指针方式,是直接将常量区指定字符的地址存入指针,指向常量区的对应字符空间。

那么这样是不是意味这,我们在使用指针方式的时候,由于直接指向的是常量区的指定字符,故只能进行读操作,因为常量区默认是不能修改的,故下面进一步验证我们的判断。
在这里插入图片描述

上述使用汇编探究的时候,出现了内存访问错误,那这确实如我们所分析的那样,指针存储的是常量区的地址,故只能读,不能直接进行修改操作。

指针取值的两种方式

关于指针取值的方式,可以采用’*()‘和’[]',那么两者使用的时候有什么差异和联系呢?
让我们从汇编的角度来深入剖析理解一下。

#include <stdio.h>

int main(){
	
	
	int x = 5;
	int* p =&x;
	int** p1 = &p;
	int*** p2 =&p1;

	printf(" %d\n %d\n %d\n %d\n %d\n %d\n",p[0],*p,p1[0][0],**p1,p2[1][2][3],*(*(*(p2+1)+2)+3));
											//p1[0][0]<==>*(*(p+0)+0)==**p
	getchar();								//p2[1][2][3]和*(*(*(p2+1)+2)+3)只是为了测试从汇编观察

}

汇编视角如下:

在这里插入图片描述
从上述汇编视角可以看出:

'[]‘和’*()'取值方式完全一样,汇编上并无区别

调用约定是什么

首先,我们需要了解调用约定指的是什么,调用约定就是告诉编译器,怎么传递参数,怎么传递返回值,以及怎么平衡堆栈

常用的几种调用约定:

调用约定参数压栈顺序平衡堆栈
_cdecl从右至左入栈调用者清理栈
_stdcall从右至左入栈自身清理栈
_fastcallECX/EDX传送前两个参数,其余从右至左入栈自身清理栈

查看VC6.0默认采用的是第一种调用约定方式:
在这里插入图片描述
以下用简单的示例,以汇编的角度来深入理解剖析调用约定:

#include <stdio.h>

int add(int x, int y){

	return x+y;
}

int main(){
	
	int r = add(5,10);
											
	getchar();								

}

(默认_cdecl方式)汇编视角:
在这里插入图片描述
(_stdcall)视角:
在这里插入图片描述
(_stdcall)汇编视角:
在这里插入图片描述

在这里插入图片描述

(_fastcall)视角:
在这里插入图片描述

(_fastcall)汇编视角:

在这里插入图片描述
在这里插入图片描述
通过上述汇编过程探究,与查阅的资料所述一致,除了上述常见的三种调用约定,还有一些其他的调用约定,可以自行探究。

结语

C语言是我们很多大学生接触的第一门语言学科,那时的我们,学起来懵懵懂懂,很多东西理解不了,都是靠着记忆的方式记下来,但是,随着我们学习的深入,我们开始可以探究C语言种每条规则背后的奥秘,解开原来学习时留下的疑惑,这何尝不是一种深刻而又美妙的进步呢。

微语:即使能力有限,也要全力以赴,即使没有成功,也要比以前更强,愿你笑的灿烂、活得坦荡。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一问30

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值