C程序如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>struct student_t
{char *id;char *name;char *score;
};typedef struct student_t *Student_t;void print_stu_info(Student_t stu)
{printf("%s %s %s\n", stu->id, stu->name, stu->score);
}int main()
{Student_t stu = malloc(sizeof(*stu));stu->id = "001";stu->name = "xiaonan";stu->score = "95";print_stu_info(stu);*((char **)((char *)stu + 2*sizeof(char *))) = "100";print_stu_info(stu);return 0;
}
程序运行结果如下:
majn@tiger:~/C_Project/dl_project$ ./hack_struct
001 xiaonan 95
001 xiaonan 100
问题一
Student_t stu = malloc(sizeof(*stu));
第一次见这种写法,真的没有问题吗?写成
Student_t stu = malloc(sizeof(*Student_t));
为什么编译报错?
在这行代码中出现了错误:
Student_t stu = malloc(sizeof(*Student_t));
问题在于 *Student_t
。在这里,Student_t
是一个类型,而不是一个变量。不能对类型进行解引用。
正确的方式是,使用 sizeof(struct student_t)
或 sizeof(*stu)
来获取结构体的大小。如果我们要分配一个结构体的大小并返回一个指向该结构体的指针,应该这样写:
Student_t stu = malloc(sizeof(struct student_t));
或者:
Student_t stu = malloc(sizeof(*stu));
将上述更正后的代码行替换原有代码,应该能够成功编译。
关于第一种写法的问题,这实际上与C语言的声明和初始化语法有关。
在C语言中,当我们写:
Student_t stu = malloc(sizeof(*stu));
以下是发生的事情的简化描述:
-
编译器首先处理
Student_t stu
部分。这里,它知道声明了一个名为stu
的Student_t
类型的指针变量。从这一点开始,stu
已经被声明并可以在其后的代码中使用。 -
接下来,编译器处理
= malloc(sizeof(*stu))
部分。这时,由于stu
已经被声明,所以可以在sizeof(*stu)
中使用它。
因此,尽管 =
是右结合的,但在整个声明和初始化过程中,声明总是先于初始化发生。
如果将这两个步骤分开,那么确实需要确保在使用一个变量之前已经声明了它。但在同一语句中进行声明和初始化时,该语句的声明部分会首先被处理。
问题二
如何通过直接操作内存地址的方式来修改 stu->score 的值?
如果想通过直接操作内存地址来修改 stu->score
的值,首先,需要确保准确地计算了偏移量,并且将正确的值赋给正确的地址。
在 struct student_t
中,字段按以下顺序出现:
char *id
char *name
char *score
所以,stu
指针首先指向 id
,然后是 name
,最后是 score
。
我们可以通过增加 stu
的地址来修改 score
,但需要知道系统的指针大小(通常是 4 字节(32位系统)或 8 字节(64位系统))。
以下是可能的写法(假设在 64 位系统上):
*((char **)((char *)stu + 2*sizeof(char *))) = "100";
解释:
- 首先,我们将
stu
转换为char *
类型,使我们能够按字节递增其地址。 - 接下来,我们增加了
2 * sizeof(char *)
字节,这样我们就可以跳过前两个char *
字段,指向score
。 - 然后我们将结果转换回
char **
类型,以便我们可以将字符串的地址("100"
)赋给它。
下面,我们来详细地探讨这行代码的每一个部分:
首先,来看这个代码的目的:它旨在直接更改 stu->score
的指针值,使其指向新的字符串字面量 "100"
。
现在,我们一步一步来分析这行代码:
-
(char *)stu:
这将stu
指针从Student_t
类型转换为一个字符指针 (char *
)。这样做允许我们以字节为单位进行指针算术。 -
2*sizeof(char *):
这里我们计算两个指针的大小。每个char *
指针的大小通常为 4 字节(32位系统)或 8 字节(64位系统)。因此,这部分代码计算stu
结构中前两个char *
指针(id
和name
)所占用的总字节。 -
(char *)stu + 2*sizeof(char *):
这将上述两个部分结合起来,即将stu
的起始地址增加了前两个char *
指针所占用的字节,从而将地址定位到score
字段。 -
(char **)((char *)stu + 2*sizeof(char *)):
现在,我们再次进行类型转换,将计算出的score
地址从char *
转换为char **
。因为我们最终的目标是更改一个指针的值(指向一个新的字符串),而不仅仅是更改一个字符。 -
((char **)((char *)stu + 2*sizeof(char *))) = “100”:
最后,我们使用解引用运算符*
来访问和更改score
指针的值,使其指向新的字符串"100"
。
直接操作内存地址是非常危险的,并且容易引入错误。这种做法破坏了抽象,使代码变得难以阅读和维护。因此,在实际编程中,应尽量避免使用这样的方式。