C语言---Xiyou_Linux2023纳新题解 -----详细版

前言

 🐼个人简介:一名在校的大一新生。

 🎉个人wechat:lufei13308378206。

🚀本文由秋落风声,首发CSDN,此篇参考了本校Linux实验室的同学们的题解,在此表示感谢。

🔥如果有知识性的错误,或者逻辑上不清楚,欢迎加我的WeChat,来共同探讨,也期待您提供的宝贵建议。CSDN后台私信我也可以哦。

——————————————————————————————

今日(24_4_18)找书时偶然翻到这份面试题(还有其它实验室的),我能看懂了(大概)。我认为这值得写一份清晰的题解。以下仅供个人解答

这套题考察了大量C语言基础知识,重点囊括二进制,数组,函数,结构体和指针,只能作为C语言的巩固复习有限参考。比如这里不(很少)涉及文件,预处理,编译 链接的有关知识。

适用对象

1. 想扎实巩固C语言的朋友们,这套题可以有效检验你们的学习成果。

2.想面试本校的Linux实验室的新大一同学们,也许你从0开始,不要畏难,也许题解你看得很吃力。我会把每道题考点罗列一遍,请抓紧时间学习。加油吧!

我使用的是windows系统下的vs2022,

可能与Linux系统中的编译器gcc有差别。

🔥

① 本题目只作为西邮 Linux 兴趣小组 2023 纳新面试的有限参考。
② 为节省版面,本试题的程序源码省去了 #include 指令。
③ 本试题中的程序源码仅用于考察 C 语言基础,不应当作为 C 语言「代码风格」的范例。
④ 所有题目编译并运行于 x86_64 GNU/Linux 环境。

0.鼠鼠我啊,要被祸害了

1000 瓶水,其中有 一瓶 有毒,小白鼠只要尝一点带毒的水, 24 小时后就会准时死亡。
至少要多少只小白鼠才能在 24 小时内鉴别出哪瓶水有毒?
知道二进制吧?二进制由1,0组成。
1表示 turn on(打开) 
0表示 turn off(关闭)
理解一:
0和1表示两种相反的状态,即这里0可以表示死掉了,1表示活着。

对于一只小白鼠 喝完水,它有两种状态 即0(死了) 1(活了),那么这种结果就可以说明几瓶水有毒?

2瓶,很好理解是吧,小鼠喝了其中一瓶,根据最后的状态,可以确定两瓶总有一瓶有毒。

现在有两只小白鼠,对应4种结果 即00 ,01,10,11。两只都死了(00),两只都活着(11),一死一活(10 01)。可以判断几瓶呢?

4瓶,嗯,还是写一下理解吧。

设四瓶水 分别为A ,B ,C, D。小鼠a,b。

a:A,B ;

b:B,C;

D瓶都不喝。

🆗相信你懂了。

三只小鼠可以证明8瓶水有毒。4只小鼠可以证明16……巴拉巴拉。

因此其实这道题就是解方程 2^n=1000;由于2^9=512,所以实际要至少10只小鼠,最多可以检测1024瓶水。

理解二:

看不明白?呃,这样吧。

1000瓶水,二进制中0表示没喝,1表示喝了。要表示1000中结果,最少要10个二进制位。2^10=1024>1000。每小鼠按照二进制编号来喝水,根据最后结果死掉的小鼠都喝的就是有毒的。

  

可怜的小白鼠。

考察二进制

1.先预测一下~

按照函数要求输入自己的姓名试试~
char* welcome() {
	// 请你返回自己的姓名
}
int main(void) {
	char* a = welcome();
	printf("Hi, 我相信 %s 可以面试成功!\n", a);
	return 0;
}

都说让你返回姓名了,就返回一个字符串呗。

char* welcome() {
	return "eren";//后面有解释
}
int main(void) {
	char* a = welcome();//创建char* 的指针变量接受返回值
	printf("Hi, 我相信 %s 可以面试成功!\n", a);//打印字符串。
	return 0;
}

要解释一下吗?

welcome 的返回类型是字符指针(char* ),意思是要return 地址。

这里return "eren" ,看起来return 的不是字符串吗?

这个其实是常量字符串,常量字符串是字符数组,可以看作首元素的地址。这里return的就是其首元素的地址。

可以这样写吗?

char* welcome() {
	char* arr = "eren";//创建临时变量存储常量字符串的地址
	return arr;//返回这个指针。
}
int main(void) {
	char* a = welcome();
	printf("Hi, 我相信 %s 可以面试成功!\n", a);
	return 0;
}

 试运行两种代码,看一下结果!

char* welcome() {
	char arr[] = "eren";//字符数组
	return arr;//返回数组名,数组名即首元素的地址
}
int main(void) {
	char* a = welcome();
	printf("Hi, 我相信 %s 可以面试成功!\n", a);
	return 0;
}

 第一种方法结果是对的,但我不建议。

 第二种写法是错的。

我说得简单一点,你认为两个“eren”一样吗?

也许你听过栈,堆,静态区。

无论是char arr[],char* arr都是welcome函数内部创建的,为局部变量,存储在栈区,函数结束就销毁了。

常量字符串存储在静态区,变量常量在程序运行期间会一直存在,不会释放,且变量常量在其中只有一份拷贝,不会出现相同的变量和常量的不同拷贝。

一之所以对,只不过是把常量字符串地址传回去,常量字符串一直都在就能打印。

二不对,字符数组在栈区上,这个“eren"拷贝了一份在数组上。出函数,内存回收,数据就不在了。

不妨自己调试观察一下。

前面变量加static修饰,或者改成全局变量就可以了。

仅展示第二种运行结果。

 

2.欢迎来到 Linux 兴趣小组

有趣的输出,为什么会这样子呢~

nt main(void) {
	char* ptr0 = "Welcome to Xiyou Linux!";
	char ptr1[] = "Welcome to Xiyou Linux!";
	if (*ptr0 == *ptr1) {
		printf("%d\n", printf("Hello, Linux Group - 2%d", printf("")));
	}
	int diff = ptr0 - ptr1;
	printf("Pointer Difference: %d\n", diff);
}

 1.常量字符串和字符数组中字符串在内存中存储。

2.指针解引用

3.函数递归。

4.printf的返回类型

 解释一下原因。


自上而下,解读每一行代码

第一个把常量字符串首元素的地址存储在字符指针,存储在静态区。

第二个是字符数组,这里是字符串,存储在栈区

if语句内指针解引用 *ptr0指向常量字符串的‘w'。*ptr1,(一维)数组数组名即是首元素的地址,*ptr1是字符数组(字符串)的‘w',都是字符’w',执行if下面的语句。

最外面的printf的第一个参数是格式字符串,里面占位符%d , 换行符\n,第二个参数是另一个printf的语句,这个表达式的返回结果是什么呢?

printf的返回值是打印的字符总数。返回类型是int。

继续看第二个printf("Hello, Linux Group - 2%d", printf("")),格式字符串,和又一个printf。

再看第三个printf,总算简单了。printf("")先打印一下空字符串(打印个寂寞),返回值为0。(printf打印字符串打印\0之前的字符,并返回\0的字符总数)。自己查一下printf。

第二个等价于printf("Hello, Linux Group - 2%d",0),先打印,结果为Hello, Linux Group - 20

并返回打印字符个数23个。

第一个等价于printf("%d",23),打印23,返回值被忽略。

按照打印顺序  Hello, Linux Group - 2023  就是打印结果啦。

从递归来看,先递推到终点,再逐步回归。

其实栈的应用,栈的特点:先进后出,后进先出。

最后两个指针(地址)进行减法,不可以这样操作,因为它们不是在同一块空间,内存块都不同,结果为随机值(每次程序运行结果不同)。不是0,不是0,不是0!

🆗

那么今年就是2024了,懂我意思吧,哈哈 !

3.一切都翻倍了吗

① 请尝试解释一下程序的输出。
② 请谈谈对 sizeof () strlen () 的理解吧。
③ 什么是 sprintf () ,它的参数以及返回值又是什么呢?
int main(void) {
	char arr[] = { 'L', 'i', 'n', 'u', 'x', '\0', '!' }, str[20];
	short num = 520;
	int num2 = 1314;
	printf("%zu\t%zu\t%zu\n", sizeof(*&arr), sizeof(arr + 0),
	sizeof(num = num2 + 4));
	printf("%d\n", sprintf(str, "0x%x", num) == num);
	printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0));
}

解释一下程序输出,那先看看程序输出吧。

1. sizeof()计算数据类型的大小(单位字节),sizeof(int)计算的是int数据类型的大小。

sizeof(变量)本质也是计算变量类型的大小。

sizeof(表达式),计算表达式结果的类型,表达式不运算。

sizeof(函数调用表达式),函数都不调用,计算的是函数返回类型的大小。

2.strlen是专门计算字符串的大小,计算的是字符串'\0'前面的字符个数

它只能计算字符串吗?

字符数组也适用,但用结束标志'\0'。

前面的考点就是这些。

自上而下分析每个printf和它的对应参数。

1.%zu是针对size_t的占位符,这是一种无符号整型,不了解详情自行查资料。

sizeof(*&arr),这里我提供两种理解,其一,* &均为指针运算符,互为可逆运算,即*&抵消了。等价于sizeof(arr),计算的是整个字符数组的大小,这里是7,(自己数一下)。

其二,先看&arr的类型是什么,是指针数组(char * []),再解引用是字符数组,数组元素是七个,就是(char [7])这个类型的大小。

sizeof(arr+0),arr+0,这里arr是首元素的地址,arr+0还是首元素的地址,类型是(char*),大小为4字节

sizeof(num = num2 + 4),表达式不进行运算,表达式结果类型是什么?是short类型,注意这里num是short,num2是int。结果为2字节。

2.sprintf函数。

sprintf - C++ Reference (cplusplus.com),如果看不明白下面的,自己网站找。

printf知道吧。参数分别是格式字符串和待打印项。格式字符串有n个占位符,printf就有n+1个参数。

那么sprintf有什么区别呢?

sprintf第一个参数是char* str,其余参数跟printf一模一样,功能是指定格式化后的字符串,拷贝到str所在的空间。(不打印了)

返回类型是int,返回写入数组的字符总数。

分析此处,先格式化字符串,以%x(16进制)将520转换到字符串,520(10进制)=208(16进制),字符串"0x208"拷贝到str数组上。返回字符串的字符总数('\0'之前),返回值是5,那个判断真假的式子为假(0),所以打印结果为0。

3.第三个printf,strlen(&str[0] + 1),从str[1]开始到'\0'之前的字符个数。前面分析了str存了

"0x208",这里就是从'x'开始到'\0'之前的(已标注),这里一共4个,与打印结果相符。

strlen(arr+0),从arr数组首元素开始到'\0'之前的元素。char arr[] = { 'L', 'i', 'n', 'u', 'x', '\0', '!' };即Linux,结果为5。

考察

1.sizeof运算符的理解

2.strlen函数的参数,返回类型,功能;

3.sprintf函数参数功能返回类型;

4.进制转换(10进制转二进制转16进制)

补充:为什么不把sizeof这个运算符设计成函数?

1.从代码上来看更加简洁。

2.sizeof可以作用于类型,比函数灵活。

3.sizeof可以计算复杂类型的大小,比函数灵活。

4.函数调用会在栈上申请空间(函数栈帧),运行时会加大开销。

4.奇怪的输出

程序的输出结果是什么?解释一下为什么出现该结果吧~
int main(void) {
 char a = 64 & 127;
 char b = 64 ^ 127;
 char c = -64 >> 6;
 char ch = a + b - c;
 printf("a = %d b = %d c = %d\n", a, b, c);
 printf("ch = %d\n", ch);
}

又要看输出结果分析吗?,先上结果吧。 

 

掌握一下位运算就搞定了,还有进制转换。

64,127在默认为整型(int),先转二进制补码,正整数二进制原码=反码=补码。

  64:00000000 00000000 00000000 01000000

127:00000000 00000000 00000000 01111111

按位与(&)运算,任何位*0为0,乘一就为它本身。

这里相应有0变成0,没0的变成1.

64&127  =64

按位异或 (^)相同为0,相异为一

结果为 00000000 00000000 00000000 00111111(63的二进制)

64^127  =63;

移位操作符(这里☞右移操作符

c=-64>>6,由于大多编译器采用的是算术右移,-64>>6=-1

所以前面以%d(10进制有符号整数(signed int))打印的结果就理解了。

因为字符类型本质是整型,如果把右边当成整型即64+63-(-1);则ch是128整型。

但由于char为有符号的char,取值范围为-128~127,取不到128的整型。见图

 127-(-1)后,是-128的整型,这里规定10000000是-128的补码。所以ch的打印结果是-128。

分析完毕。

考察

1.位运算符,(尤其注意移位操作符的算术右移和逻辑右移);

2.整数在内存中存储:二进制原码反码补码;

3.signed char的数据溢出,数据范围;

4.进制转换(十进制转二进制);

5.乍一看就不想看的函数

“人们常说互联网凛冬已至,要提高自己的竞争力,可我怎么卷都卷不过别人,只好用一些奇技淫
巧让我的代码变得高深莫测。”
这个 func () 函数的功能是什么?是如何实现的?
int func(int a, int b) {
 if (!a) return b;​//a=0时,递归的终止条件。
 return func((a & b) << 1, a ^ b);
}
int main(void) {
 int a = 4, b = 9, c = -7;
 printf("%d\n", func(a, func(b, c)));
}

这确实乍一看就不想看,运行结果是6,是a+b+c的结果。

自己先调试一下代码吧。 观察一下func函数内部a,b的变化。

func(b,c)结果为2,是b+c的和。

func实现的是加法功能啊,没用+算术运算符,而是位运算和二进制就解决了啊。也对,cpu处理器本来就只能处理二进制的加法。

那具体原理还是简单说明一下吧。

a + b = (a ^ b) + ((a & b) << 1)。这里还是有加号啊?没错,这里用递归使得(a & b) << 1这部分为0了,用递归和位运算实现加法

你可以搜一下为什么位运算能实现加法,我在这里就不细述了。

6.自定义过滤

请实现 filter () 函数:过滤满足条件的数组元素。
提示:使用函数指针作为函数参数并且你需要为新数组分配空间。
typedef int (*Predicate)(int);
int* filter(int* array, int length, Predicate predicate,
	int* resultLength); /*补全函数*/
int isPositive(int num) { return num > 0; }
int main(void) {
	int array[] = { -3, -2, -1, 0, 1, 2, 3, 4, 5, 6 };
	int length = sizeof(array) / sizeof(array[0]);
	int resultLength;
	int* filteredNumbers = filter(array, length, isPositive,
		&resultLength);
	for (int i = 0; i < resultLength; i++) {
		printf("%d ", filteredNumbers[i]);
	}
	printf("\n");
	free(filteredNumbers);
	return 0;
}

 理解的要点:

1.typedef关键字 这里Predicate重命名了int (*)(int) ,函数指针类型。

2.int isPositive(int num) { return num > 0; },传递函数指针来调用函数。

3.int length = sizeof(array) / sizeof(array[0]);计算数组的元素个数。

4.main函数free释放了指针变量filteredNumbers存储地址的内存,说明这空间是动态开辟的,且filter函数要返回开辟空间的起始地址给这个变量。

int* filter(int* array, int length, Predicate predicate,
    int* resultLength);

1.第一个参数传递的一维数组(传址调用),第二个参数是数组的长度(遍历原数组),第三个参数是函数指针(回调函数),第四个参数是新分配动态空间的元素个数(传址调用)。

 实现filter函数的定义

1.函数指针作为参数。

2.为新数组分配空间。

int* filter(int* array, int length, Predicate predicate,int* resultLength)
{
	int* p = (int*)malloc(length*sizeof(int));//动态开辟内存,因为主函数后面用了free释放。
	assert(p);//断言,动态内存是否开辟失败。记得包含头文件<assert.h>
	int i = 0;//循环变量
	int count = 0;//计数变量,记录动态内存中存储的整型个数
	for (i = 0; i < length; i++)
	{
		if (isPositive(array[i]))//回调函数
		{
			*(p + count) = array[i];//指针解引用,动态内存空间当作数组处理
			++count;//每存入一个数据就++。
		}
	}
	*resultLength = count;//返回新分配动态数组的元素个数
	return p;//返回动态空间的起始地址
}

 跟上下文填空一样。

1.动态内存malloc,calloc,realloc 和free。

2.传址调用。

7.静...态...

① 如何理解关键字 static
static 变量 结合后有什么作用?
static 函数 结合后有什么作用?
static 指针 结合后有什么作用?
static 如何影响内存分配?

 static关键字的特点就是静态。

static 修饰局部变量

static修饰局部变量会改变局部变量的生命周期,从存储上,改变了其在内存上的存储位置,由栈到静态存储区。

static 修饰全局变量

全局变量被修饰后改变了链接属性,由外部链接属性改变到内部链接属性,总之就是,这个全局变量只能在本源文件使用了。

static 修饰函数

函数被static修饰也有内部链接属性,这个函数称为静态函数,只能在本源文件使用,跟全局变量很像。

static修饰指针

修改指针变量的生命周期,周期和程序一样,数据可在多个函数中共享。

static 如何影响内存分配?

前面说过了。

 考点即标题

8.救命!指针!

数组指针是什么?指针数组是什么?函数指针呢?用自己的话说出来更好哦,下面数据类
型的含义都是什么呢?
int(*p)[10];
const int* p[10];
int (*f1(int))(int*, int);

数组指针即存放数组的指针,int (*p)[10],这里p先和*结合,说明它是指针。这个指针解引用对应的类型呢,即int [10],元素个数10个,元素类型为整型的数组。

指针数组即存放指针的数组,数组里面存放的指针都是同一类型,这里加const修饰表明不能对指针解引用修改对应的值。

int (*f1(int))(int*, int);

f1是变量名,f1后面着的(int)是参数类型,那么剩下int (*) (int*,int)是这个函数的返回类型。

这个函数它的参数是int类型,它的返回类型是int (*)(int* ,int)类型的函数指针

那么这条语句什么意思, 函数返回类型 函数名 函数参数 ;哦,是在进行函数声明。

函数指针是存储相应类型的函数的地址,函数名或者&函数名就是函数的地址

。另外,补充一句,知道函数指针,我们就可以通过函数指针来调用函数了。

 1.函数指针数组指针,二级指针,指针数组函数指针数组

 2.const 关键字,你能分清楚下面的吗?

A.const char *p

B.char const *p

C.char*const p

D.const char *const p

简单记一下,const在*p左边,不能修改*p即指针变量指向的内容,如A,B

const在*p右边呢,不能修改p这个指针变量的值。 C

那两边都有呢,那都不能修改呗。 D

OVER!

9.咋不循环了

程序直接运行,输出的内容是什么意思?
int main(int argc, char* argv[]) {
	printf("[%d]\n", argc);
	while (argc) {
		++argc;
	}
	int i = -1, j = argc, k = 1;
	i++ && j++ || k++;
	printf("i = %d, j = %d, k = %d\n", i, j, k);
	return EXIT_SUCCESS;
}

int main(int argc,char* argv[])

main函数的参数形式
C语言中,argc是传递给main函数的命令行参数的数量,通常情况至少为1,因为第一个参数是程序的名称。

循环内部对argc进行自增操作++argc,导致argc会增大到最大然后溢出到最小。

即argc从1--->INT_MAX->INT_MIN--->0,结束循环。

i++ && j++ || k++;

这个考察一个逻辑运算符的知识点:

逻辑运算符总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是
保证的。如果左侧符合逻辑运算符的条件,就不执行右边了。
就拿这个举例,后置自增运算符,先使用,再++。-1为真,使用完自增到0;执行逻辑与右侧0为假,则整体i++ && j++,结果为假(0),i,j都++了。此时i==0,j==1,由于左侧为假,逻辑或推不出结果,要执行||右侧的表达式,k=1,先使用,右侧为真,再自增,最后k=2。
最终三个自增表达式都执行了。
所以最终打印结果为 i=0,j=1,k=2;

10.到底是不是 TWO

#define CAL(a) a * a * a
#define MAGIC_CAL(a, b) CAL(a) + CAL(b)
int main(void) {
	int nums = 1;
	if (16 / CAL(2) == 2) {
		printf("I'm TWO(ノ>ω<)ノ\n");
	}
	else {
		int nums = MAGIC_CAL(++nums, 2);
	}
	printf("%d\n", nums);
}

 呃,报错了。可能是编译器问题,反正windows下的vs2022跑不了。

这里以下面修改后的代码为例

#define CAL(a) a * a * a
#define MAGIC_CAL(a, b) CAL(a) + CAL(b)
int main(void) {
	int nums = 1;
	if (16 / CAL(2) == 2) {
		printf("I'm TWO(ノ>ω<)ノ\n");
	}
	else {
		int nums = 0;//那初始化一下吧
		nums = MAGIC_CAL(++nums, 2);
	}
	printf("%d\n", nums);
}

按此代码,总结了以下考点

1.函数宏,#define指令

2.宏替换后带来的未定义行为问题

3.块作用域,和同名变量局部优先原则。

#define 这个预处理指令可以实现定义宏,它可以将参数替换到文本。

第一个if语句 中替换后为 16/2*2*2 =32 !=2,表达式为假,执行else后面的语句。

这里不要脑补加括号,也不要先计算结果再替换,因为在编译过程会处理预处理指令(#define),预处理处理所有的#define指令,将文本信息替换。

这里将#define CAL(a) a * a * a   改为  #define CAL(a)  ((a) * (a) *( a)) ,即使文本替换了,由于加了括号修改了优先级,也是先执行里面的运算。这个时候if语句就为真了。

else后面的  MAGIC_CAL(++nums, 2)--->替换为++nums*++nums*++nums+2*2*2。

你清楚这个表达式怎么计算吗?

如果一个表达式的运算符的优先级和结合性都清楚的话,那么表达式的结果就一定确定吗?

答案肯定不确定,++nums*++nums*++nums+2*2*2。这个你可能知道是未定义行为。

 我简单说一下吧,优先级++(自增运算符) > * >+ ,但每个nums带什么值是不确定的。

比如  ++nums*++nums*++nums+2*2*2

nums初始为0,

前置自增+1,代入1;第一个nums为1了,再自增+1,带入第二个nums,第二个num位2;最后自增+1,第三个nums就为3。

则执行一种结果 1*2*3+2*2*2=14

那我也可以这样算

前置自增+1,num都代入0,再前置++两次,0*0*0+2*2*2=8,num最终为3。

虽然优先级和结合性确定了,但什么时候代入什么值,是一起带,还是执行某个运算后在带,C语言并没有规定,结果取决于编译器,所以是未定义行为

那么最终结果num为什么是一,这里涉及到作用域,main函数内申请第一个nums是1,作用域是main函数内;第二个nums是0,在else语句花括号内,出了就销毁了。这里第一个nums在main函数实际范围大于else的花括号,其实也考察了同名变量局部优先的原则。

这里printf打印的是main函数内,else复合语句外的nums,所以打印结果为1。

11.克隆困境

试着运行一下程序,为什么会出现这样的结果?
直接将 s2 赋值给 s1 会出现哪些问题,应该如何解决?请写出相应代码。
struct Student {
 char *name;
 int age;
};

void initializeStudent(struct Student *student, const char *name,
 int age) {
 student->name = (char *)malloc(strlen(name) + 1);
 strcpy(student->name, name);
 student->age = age;
}
int main(void) {
 struct Student s1, s2;
 initializeStudent(&s1, "Tom", 18);
 initializeStudent(&s2, "Jerry", 28);
 s1 = s2;
 printf("s1 的姓名: %s 年龄: %d\n", s1.name, s1.age);
 printf("s2 的姓名: %s 年龄: %d\n", s2.name, s2.age);
 free(s1.name);
 free(s2.name);
 return 0;
}
//声明结构体
struct Student {
	char* name;
	int age;
};

//初始化结构体
void initializeStudent(struct Student* student, const char* name,
	int age) {
	student->name = (char*)malloc(strlen(name) + 1);//动态开辟内存(堆区上的空间)
	strcpy(student->name, name);//拷贝字符串
	student->age = age;//初始结构体成员
}
int main(void) {
	struct Student s1, s2;//两个结构体变量
	initializeStudent(&s1, "Tom", 18);
	initializeStudent(&s2, "Jerry", 28);
	//初始化两个变量s1,s2。
	s1 = s2;//结构体赋值

	//打印结构体成员
	printf("s1 的姓名: %s 年龄: %d\n", s1.name, s1.age);
	printf("s2 的姓名: %s 年龄: %d\n", s2.name, s2.age);

	//释放动态开辟的内存。
	free(s1.name);
	free(s2.name);
	return 0;
}

这个要对结构体赋值有理解。

现在C语言可以进行结构体赋值结构体的操作,当然,前提是同一个结构体类型。

结构体赋值等价于,对应结构体成员的赋值。

这里s1,s2创建后在栈区申请空间,而其成员变量char* 字符指针,指向的是堆区动态开辟的内存。

进行s1=s2的赋值后,s1,s2的char* 指针(name成员)指向了相同的内存地址,即原来的s1.name地址找不着了。s1.name的动态开辟内存没有释放,会造成内存泄漏。

下面的语句都表示释放s2动态开辟空间
    free(s1.name);
    free(s2.name);

free多次释放同一块动态内存,也出问题了

其三,释放完内存,原有指针(变为野指针)指向空间存储未知,滥用会非法访问,需要置空。

这里出错原因分析完毕了,原因就是没有释放完动态内存,重复释放同一块空间报错了,只需创建一个临时变量存储地址就可以了。

struct Student {
	char* name;
	int age;
};

void initializeStudent(struct Student* student, const char* name,
	int age) {
	student->name = (char*)malloc(strlen(name) + 1);
	strcpy(student->name, name);
	student->age = age;
}
int main(void) {
	struct Student s1, s2;
	initializeStudent(&s1, "Tom", 18);
	initializeStudent(&s2, "Jerry", 28);
	
	//创建结构体中间变量来交换两个结构体
	struct Student s3 = s1;
	s1 = s2;
	s2 = s3;//如果不想修改s2,则忽略此句,下面改成free(s3. name)

	printf("s1 的姓名: %s 年龄: %d\n", s1.name, s1.age);
	printf("s2 的姓名: %s 年龄: %d\n", s2.name, s2.age);

	//这里不用担心s3.name的动态内存,因为s3.name==s1.name,释放了。
	free(s1.name);
	free(s2.name);
	s1.name = NULL;//释放完内存,置空处理
	s2.name = NULL;
	return 0;
}

12.你好,我是内存

作为一名合格的 C-Coder,一定对内存很敏感吧~来尝试理解这个程序吧!
struct structure {
	int foo;
	union {
		int integer;
		char string[11];
		void* pointer;
	} node;
	short bar;
	long long baz;
	int array[7];
};
int main(void) {
	int arr[] = { 0x590ff23c, 0x2fbc5a4d, 0x636c6557, 0x20656d6f,
	0x58206f74, 0x20545055, 0x6577202c, 0x6d6f636c,
	0x6f742065, 0x79695820, 0x4c20756f, 0x78756e69,
	0x6f724720, 0x5b207075, 0x33323032, 0x7825005d,
	0x636c6557, 0x64fd6d1d };
	printf("%s\n", ((struct structure*)arr)->node.string);
}

考点

1.结构体的内存对齐

2.联合体在内存中存储

3.大端字节序和小端字节序

x86下运行结果

结构体大小为64字节

x64下结果

结构体大小为72字节

如果你了解结构体的内存对齐和联合体的存储,那么你就往下看详解,否则请自行搜索相关内容学习或者看我写的结构体的博客。

这里是x86下的分析,下面的序号不是偏移量,可以理解为个数

上图什么意思?

struct structure* 这个结构体指针解引用能访问的内存大小,就是这个结构体指针的访问权限。

仔细观察一下main函数的arr数组,它有18个元素。每个元素是以16进制写法的整型(0x整数表示这个整数为16进制)。

这是巧合吗?

这必然是精心设计好的。

((struct structure*)arr)

不同指针的类型决定了解引用访问字节的个数。

arr是数组名,即数组首元素地址,可以认为(int*),解引用只能访问4个字节,现在强制转化成(struct structure*),解引用由上面可知访问64个字节。

下面要以这种方式访问64节空间的整型数组。

 ((struct structure*)arr)->node.string)

这里相当于用结构体内存的访问形式访问arr数组。

1字节有8个二进制位,1个16进制位对应4个二进制位,所以1个字节放2个16进制位。

超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,这就是所谓的字节序问题。

vs2022采用的小端存储(低位放低地址,高位放高地址),printf%s形式打印字符串的,即将每个字节以字符形式打印直到遇到'\0'('\0'对应一字节16进制为00),所以打印结果为蓝色标注。

int arr[] = { 0x590ff23c, 0x2fbc5a4d, 0x636c6557, 0x20656d6f,
    0x58206f74, 0x20545055, 0x6577202c, 0x6d6f636c,
    0x6f742065, 0x79695820, 0x4c20756f, 0x78756e69,
    0x6f724720, 0x5b207075, 0x33323032, 0x7825005d
,
    0x636c6557, 0x64fd6d1d };

    低地址------>高地址

    4d 5a  |  bc 2f  |   57 65 | 6c 63 | 6f 6d | 65 20 | 74 6f |20 58| 55 50| 54 20|

    2c 20  |  77 65 |   6c 63 | 6f  6d |65 20 | 74 6f  | 20 58|69 79| 6f  75| 20 4c|

    69 6e  |  75 78|    20 47| 72  6f  |75 70 |20  5b | 32 30|32 33|5d  00

 掏出ASCII码表16进制对一下就是如下结果:

MZ?/Welcome to XUPT , welcome to Xiyou Linux Group [2023]

13.GNU/Linux (选做)

注:嘿!你或许对 Linux 命令不是很熟悉,甚至你没听说过 Linux。但别担心,这是选做题,了解
Linux 是加分项,但不了解也不扣分哦!
你知道 cd 命令的用法与 / . ~ 这些符号的含义吗?
请问你还懂得哪些与 GNU/Linux 相关的知识呢~
不会,作者还没入门Linux。(暂时没这个打算)请自行了解学习。

后期学习计划

C语言相关博客还没更完,我决定做一个系列栏目,很可惜的是,个人的时间精力有限。

有意做一个0基础入门C语言,总结一下自己入门时踩过的坑,心有余而力不足。总之日后会继续更新C语言系列,数据结构系列,c++系列,和算法系列,可能会随机更新,但最后会整理成合集。

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值