恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C/C++结构体成员访问全攻略:点号、箭头与内存对齐
首页
资讯中心
/
C/C++结构体成员访问全攻略:点号、箭头与内存对齐
C/C++结构体成员访问全攻略:点号、箭头与内存对齐
发布时间:2026/10/5 4:45:27
写C/C这么多年要说用得最频繁、也最容易在细节上翻车的操作访问结构体成员绝对算一个。点号.和箭头-两个运算符看起来简单但几乎所有跟结构体相关的疑难杂症——从段错误到数据错乱从数组越界到内存对齐——最后都能追溯到“成员访问”这一步出了问题。这篇文章就围绕结构体成员的访问操作把定义、初始化、指针、数组、链表、文件读写、嵌入式寄存器映射、QT跨类传递这些场景全串一遍该踩的坑我都会指出来适合刚学结构体的新人也适合写过几年工程但没系统梳理过的老手。我最早被结构体“教训”是在一个串口协议解析的项目里。当时定义了一个协议帧结构体明明每个字段都看得懂结果用指针访问成员时直接硬核段错误排查了半天才发现是指针没有指向任何有效内存。后来看得多了发现身边同事踩的坑也都大同小异。所以这篇不只是讲语法更想把成员访问背后的内存逻辑、常见误区和实战判断方法一次说清楚。1. 结构体C/C里最实用的“自定义打包工具”1.1 为什么访问成员值得单独拿出来讲很多人觉得结构体成员访问不过就是“变量名.成员名”有什么好讲的但实际工程里结构体承担的角色远不是“存几个字段”这么简单。它本质上是把多个相关数据打包成一种自定义数据类型让一块内存区域有了清晰的语义。举个例子你收到一串网络报文里面包含帧头、长度、类型、有效数据和校验和。如果用单独的变量去存你会得到一堆松散的名字比如frameHead、dataLen、dataType、payload、checksum。一旦报文多起来你根本管理不过来。但你要是定义一个结构体struct Frame { uint8_t head; uint16_t len; uint8_t type; uint8_t data[64]; uint8_t checksum; };那么一份完整的报文就是一个struct Frame类型的变量访问里面的任何一个数据都只是“其中一个成员”的事。成员访问操作本质上是“从一块内存里按设计好的布局取出/写入某个字段”的语法化表达。你写的frame.type 0x01编译器会把它翻译成“在这块内存的偏移量3处写入一个字节”但对你来说你只是在给报文类型赋值完全不用管偏移量怎么算。这就是结构体的价值把内存布局的复杂度封装起来让代码接近人的思维方式。而要真正用好这个封装就必须把成员访问的几种途径、底层行为、边界情况彻底搞懂。1.2 定义结构体与变量创建的方式先看最基本的定义语法。C语言里常见两种姿势// 方式一直接定义结构体类型 struct Point { int x; int y; }; // 方式二用typedef起别名 typedef struct { int x; int y; } Point;方式一在声明变量时要写struct Point p;每次带struct关键字比较啰嗦。方式二直接写Point p;在很多工程里更常见。C里struct定义出来的本身就是类型可以直接用Point p;不用 typedef。创建变量也有讲究// 不初始化栈上分配成员值不确定 Point p1; // 全部初始化为0成员全为0这是最稳妥的起点 Point p2 {0}; // 顺序初始化x3, y5 Point p3 {3, 5}; // C99/C11之后支持指定初始化C语言里尤其好用 Point p4 {.x 3, .y 5};这里有一个新手很容易踩的点Point p1;这种写法在栈上分配内存但不会把内存清零。如果你立刻去读p1.x拿到的是栈上残留的垃圾值。所以工程上默认都写成 {0}除非你明确知道下一步就要给所有成员赋值。C里还有构造函数的方式struct Point { int x; int y; Point(int a, int b) : x(a), y(b) {} }; Point p(3, 5);但嵌入式C和纯C项目里最通用的还是 {0}和指定初始化。指定初始化有个好处不怕结构体中间插成员。比如原来Point只有x和y以后在中间加了z你再Point p {.x 3, .y 5};依然正确但如果用顺序初始化{3, 5}很可能变成x3、z5逻辑全错。1.3 结构体在内存里到底长什么样理解了结构体的内存布局访问成员的底层逻辑就透明了。C标准规定结构体成员按声明顺序排列但可能会在成员之间和结构体末尾填充“空洞”这就是内存对齐的由来。比如struct Test { char a; int b; char c; };初学者可能会认为它占用 1416 字节。但很多平台上实际是12字节。因为 int 类型通常要求4字节对齐编译器会在a后面填充3个字节b才能对齐到偏移量4c占用1字节后整个结构体还得对齐到最大成员对齐数的整数倍所以末尾又补了3个字节。如果你用sizeof(struct Test)得到的是12而不是6。这种填充会影响什么影响你往文件里写、往内存缓冲区里拷贝、在网络上传。这也是很多序列化问题的根源。你本想把char a; int b; char c;连续写成6字节结果中间多了3个空洞字节文件里就是一堆“随机值”。后面讲文件读取和缓冲区写入时这个问题会反复出现。现在你只需要记住一个核心结论结构体成员之间不是百分百紧挨着的编译器为了访问效率牺牲了空间连续。这个结论会直接影响你对“结构体成员访问”的认知——很多坑都是从这里冒出来的。2. 成员访问的两把钥匙点号与箭头2.1 点号访问普通变量的成员如果你手里是一个结构体变量不是指针访问成员用点号Point p {3, 5}; p.x 10; int y p.y;这几乎是零门槛的操作。点号访问也适用于嵌套结构体struct Line { Point start; Point end; }; struct Line line {{0, 0}, {3, 5}}; line.start.x 1; // 一层一层点下去 printf(%d, line.end.y);嵌套结构体成员访问的要点就是“逐层解引用”每个点号的左边必须是一个结构体变量或结构体表达式。编译器会帮你计算每一层的偏移量你不需要关心内存细节。点号访问数组元素也很直观Point points[3] {{0, 0}, {1, 2}, {3, 4}}; points[0].x 99;这里一定要分清优先级下标运算符[]的优先级高于点号.所以points[0].x是先取出数组第0个元素再访问它的x成员。不要写成points.x[0]那是另一个意思而且大概率编译不过。2.2 箭头访问结构体指针的成员指针访问成员就得用箭头Point p {3, 5}; Point *ptr p; ptr-x 10;箭头运算符在语义上等价于(*ptr).x。先解引用指针拿到结构体变量再用点号访问成员。箭头是这两个步骤的语法糖。为什么要有这个糖因为(*ptr).x里的括号太容易漏而且写法啰嗦。你写*ptr.x会被编译器理解为*(ptr.x)而ptr是指针根本没有 x 成员直接编译报错。箭头的使用场景非常广尤其是链表、树这些动态数据结构。举个例子单链表遍历typedef struct Node { int data; struct Node *next; } Node; Node *head create_list(); for (Node *cur head; cur ! NULL; cur cur-next) { printf(%d\n, cur-data); }这里cur-data读当前节点的数据cur-next跳到下一个节点。如果没有箭头运算符链表代码会变成(*cur).next既难看又容易错。2.3 点还是箭头最实用的判断方法很多人刚开始会纠结我什么时候用点什么时候用箭头其实一个判断标准就够用你手上是结构体变量就用点是结构体指针就用箭头。但工程里经常出现“看起来是变量其实是引用”的情况比如C的引用void setX(Point p) { p.x 10; // p是引用表面上是变量用点号 }还有C里传递对象给函数普通变量传参用点传指针/引用用箭头或点引用用点。再补充一个混淆点结构体指针数组。比如Node *nodeArr[10]这是一个数组每个元素都是Node*指针。要访问第3个指针指向的节点的data要写nodeArr[2]-data而不是nodeArr[2].data。因为nodeArr[2]的类型是Node*指针就用箭头。我见过不少工作几年的同事在这种“指针数组”上翻车。其实只要记住最终你手里那个“对象”是什么类型就用对应的运算符。如果你自己在心里模棱两可可以先写一小段代码让编译器告诉你报错一改就清楚了。3. 结构体初始化的各种姿势与成员访问的实操细节3.1 初始化方法横向对比结构体初始化直接决定了成员里存的“第一份数据”工程里非常关键。我把常用方式整理成一张表方便对照方式语法示例适用场景风险零初始化Point p {0};绝大多数工程场景无顺序初始化Point p {3, 5};简单结构体明确知道成员顺序中间插入成员会错位指定初始化Point p {.x 3, .y 5};C99及以上、大型结构体依赖编译器支持C构造函数Point p(3, 5);C项目不够C兼容动态分配后赋值Point *p malloc(...); p-x 3;需要指针的场景不初始化就访问是未定义行为我个人的工程建议C语言项目一律默认 {0}成员多的时候用指定初始化。顺序初始化只适合结构体非常稳定、字段少的场景。有人觉得 {0}太“粗”把浮点和指针也清零了但这恰恰是安全的起点——至少不会出现垃圾值导致的随机故障。3.2 结构体数组与链表的成员访问结构体数组在实际项目里极其常见因为它天然适合“一组同类型记录”的场景。比如管理100个学生的成绩typedef struct { char name[32]; int score; } Student; Student stu[100]; // 输入时逐个访问成员 scanf(%s, stu[i].name); scanf(%d, stu[i].score);注意访问结构体成员的stu[i].score在scanf里要写成stu[i].score因为scanf需要地址。这也是新手高频错误忘了取地址结果scanf把解码后的值写到某个未知地址轻则数据不对重则直接崩溃。链表则是另一种形态它不要求内存连续每个节点动态分配typedef struct Node { int data; struct Node *next; } Node;遍历和查找都靠箭头访问成员。这里有个很重要的点是“成员访问”不只是读还包括写。往链表里插入节点时你要写newNode-next cur-next; cur-next newNode;这些都是在通过箭头修改“next成员”的指向。C里结构体还可以直接用标准库容器配合比如struct Task { int id; std::string name; }; std::vectorTask tasks; tasks.push_back({1, task1}); for (auto t : tasks) { std::cout t.name std::endl; }这里t是引用所以用点号访问。实际上C里更推荐用引用而非指针能减少很多空指针风险。3.3 结构体与函数传参值传递、指针传递、引用传递结构体作为函数参数是成员访问的重要场景。直接传值可以但性能堪忧void printPoint(Point p) { printf(%d %d\n, p.x, p.y); }这段代码每次调用都会把整个结构体复制一份到栈上。如果结构体很大比如包含一个1KB的数组每次复制都是全量内存拷贝函数调用一多性能和栈空间都有压力。嵌入式开发里尤其谨慎因为栈往往很小几KB。结构体超过几个int大小就建议传指针或C引用void printPoint(const Point *p) { printf(%d %d\n, p-x, p-y); }加const的意思是“只读访问成员”。这给了编译器优化的空间也防止函数内部不小心改了调用者的数据。C还可以传引用void printPoint(const Point p) { std::cout p.x p.y std::endl; }引用和指针的成员访问运算符不同引用用点指针用箭头但“传引用”比“传值”少一次拷贝比传指针更安全引用不能为空。经验法则结构体小于等于两个int大小且不需要修改原对象传值。结构体较大或需要修改原对象传指针/引用。只读访问const指针/const引用。绝对不要用“传值”去修改原对象改了个寂寞。3.4 fscanf/fprintf与结构体成员访问文件IO的坑热词里提到了fscanf结构体这确实是一个知识点密集区域。比如你要从文件里读一组点坐标FILE *fp fopen(points.txt, r); Point p; while (fscanf(fp, %d %d, p.x, p.y) 2) { printf((%d, %d)\n, p.x, p.y); } fclose(fp);这里p.x是对结构体成员取地址。在标准C里p.x的类型是int*和%d匹配没问题。但要注意连续用fscanf读结构体文件格式必须和成员顺序、类型严格一致否则读到一半全部错位。如果直接向文件写整个结构体fwrite(p, sizeof(Point), 1, fp);那就要命了。因为结构体存在内存对齐sizeof(Point)大于等于实际字段总和写入文件的是“结构体内存快照”包含那些填充字节。更麻烦的是不同编译器、不同平台对结构体对齐策略不同直接写出的二进制文件换个环境可能就无法读回。而且成员如果是int在不同字节序的机器上文件内表示也不同。所以实际工程里把结构体写入文件或者通过网络传输标准做法是“逐字段序列化”一个一个成员地写而不是整个结构体fwrite。比如fprintf(fp, %d %d\n, p.x, p.y);或者用二进制协议时自己手动把成员按明确布局组织成一个字节缓冲区再发送。这引出了下一个热点写入内存缓冲区。3.5 结构体写入内存缓冲区手动序列化与对齐处理热词里有“写入内存缓冲区”这对应网络编程、协议封装、嵌入式通信里非常常见的操作把结构体数据拷贝到一个uint8_t buffer[64]里再通过串口/网卡发出去。最直接的想法是Point p {0x1234, 0x5678}; uint8_t buffer[64]; memcpy(buffer, p, sizeof(Point)); send(buffer, sizeof(Point));听起来没问题但隐患很多。第一是内存对齐填充buffer里会混入结构体padding字节这些字节的值是不确定的。第二是字节序如果你的机器是小端序x86、ARM默认小端0x1234在内存里实际是34 12两个字节接收方如果是大端序某些网络协议栈、PowerPC直接读就成了0x3412。第三是内核结构体的指针成员——如果你拷贝的是含指针的结构体发送的只是指针的地址值接收方根本没有那块内存崩溃是迟早的事。正确的做法是“手动按字节填充缓冲区”uint8_t buffer[64]; int offset 0; buffer[offset] (p.x 8) 0xFF; buffer[offset] p.x 0xFF; buffer[offset] (p.y 8) 0xFF; buffer[offset] p.y 0xFF; send(buffer, offset);这样绕开了结构体padding也明确了字节序这里是大端。从buffer读回结构体时同样手动解析p.x (buffer[0] 8) | buffer[1]; p.y (buffer[2] 8) | buffer[3];这套手动序列化的思路在嵌入式协议栈、网络报文解析里用得非常多。你可以封装一组小的pack/unpack函数以后所有结构体都按这个模式做稳定且可控。3.6 C结构体链表的基本语法热词里提到“c结构体链表基本语法”这里额外补充一下C里结构体链表的常见写法。C结构体与C结构体一个显著区别是结构体里可以有成员函数。比如struct Node { int data; Node *next; Node(int val) : data(val), next(nullptr) {} };创建节点时不需要再malloc直接new Node(5)Node *head new Node(1); head-next new Node(2); head-next-next new Node(3);访问成员依然是箭头。但C11以后有另一种选择用std::forward_list或std::list日常业务代码完全没必要手写链表。不过手写链表对于理解指针和成员访问仍然很有价值包括面试也常考。4. 不同开发环境下的结构体成员访问实践4.1 Keil5嵌入式环境怎么“引出”结构体成员热词里有“keil5怎么引出结构体成员”这个说法可能是从keil的调试视图角度问的。在Keil MDK里调试ARM单片机打开Watch窗口把结构体变量拖进去点开左边的三角形展开就能看到每个成员的名字和值。如果成员比较多还有Quick Watch窗口直接输入变量名.成员名回车立刻看到数值。但更关键的是嵌入式里用结构体访问寄存器。比如STM32的标准外设库把寄存器映射成结构体typedef struct { __IO uint32_t CR; __IO uint32_t CFGR; __IO uint32_t CIR; ... } RCC_TypeDef; #define RCC ((RCC_TypeDef *)RCC_BASE)然后你就可以RCC-CFGR 0x00000000;这个-的左边是一个宏宏展开是“基址强转到结构体指针”。它的本质是把某个内存地址解释成结构体布局然后访问成员。这就是嵌入式访问寄存器外设的经典套路。实际调试时你想看某一位是1还是0可以在Watch窗口输入RCC-CFGR然后在Peripherals菜单里找到对应的外设寄存器视图那里会按位解码比看十六进制直观得多。如果成员访问出来是乱值第一件事是确认外设时钟有没有使能第二是确认结构体类型定义和芯片手册寄存器偏移一致——很多“引出成员”失败其实是结构体定义里少了保留位或者顺序不对。嵌入式还有一个高频需求把结构体变量直接“送”进内存缓冲区。比如DMA发送缓冲区uint16_t adc_buf[64]; typedef struct { uint16_t ch0; uint16_t ch1; uint16_t ch2; } ADCResult; ADCResult result; memcpy(adc_buf, result, sizeof(ADCResult));这和前面讲的一致只是嵌入式里尤其要小心对齐——有些Cortex-M内核支持非对齐访问但有的编译器开了严格对齐选项非对齐访问直接进HardFault。解决方案是#pragma pack(push, 1)或__attribute__((packed))。4.2 Qt环境下结构体成员访问与应用Qt开发里结构体的使用非常普遍。热词里就有qt。一个典型的场景是界面上填好参数打包成一个结构体交给后台线程处理或者通过信号槽传递。先看信号槽传递结构体。如果你发送的信号参数是自定义结构体struct Config { QString host; int port; bool enable; }; signals: void configChanged(const Config cfg);注意跨线程队列连接时QString和自定义结构体都必须是“已注册类型”否则Qt不知道该怎么做元组拷贝qRegisterMetaTypeConfig(Config);如果不注册运行时会有警告“Cannot queue arguments of type Config”信号槽变成无效。这是Qt里最常见的结构体相关坑之一很多人只改了代码没注册类型。Qt里结构体成员访问也和普通C一样用点或箭头。但要注意Qt容器和结构体配合时的成员访问QListConfig cfgs; for (const Config cfg : cfgs) { qDebug() cfg.host cfg.port; }在Qt中不少人是foreach (Config cfg, cfgs)这种老写法默认按值拷贝性能差且改不了原对象。建议统一用基于范围的for循环加引用。如果要把结构体传入QDataStream做序列化比如存到文件或网络发送需要自己重载运算符QDataStream operator(QDataStream out, const Config cfg) { out cfg.host cfg.port cfg.enable; return out; } QDataStream operator(QDataStream in, Config cfg) { in cfg.host cfg.port cfg.enable; return in; }这里接线是out cfg.host本质是对结构体字段顺序访问的序列化。如果直接把整个结构体writeRawData((char*)cfg, sizeof(cfg))同样会踩到对齐和字节序的坑。4.3 VS中使用MySQL API获取结构体数据热词里有一条“vs中mysql的结构体”这很可能是说Visual Studio环境下调用MySQL C API来处理查询结果。MySQL C API里最核心的返回对象是MYSQL_RES和MYSQL_ROW。注意MYSQL_ROW是char**不是结构体数组它只是代表一行数据的“字符串数组”。典型流程MYSQL *conn mysql_init(NULL); mysql_real_connect(conn, host, user, pass, db, port, NULL, 0); mysql_query(conn, SELECT id, name, score FROM student); MYSQL_RES *res mysql_store_result(conn); MYSQL_ROW row; while ((row mysql_fetch_row(res)) ! NULL) { int id atoi(row[0]); const char *name row[1]; int score atoi(row[2]); } mysql_free_result(res); mysql_close(conn);这里不存在row-xxx这种成员访问因为row是字符串数组你要按照列的下标去访问位置。但如果自己定义一个结构体来承接结果就是典型的“把外部数据映射到结构体成员”typedef struct { int id; char name[32]; int score; } Student; Student stu; stu.id atoi(row[0]); strncpy(stu.name, row[1], sizeof(stu.name) - 1); stu.name[sizeof(stu.name) - 1] \0; stu.score atoi(row[2]);这里有两个常见的坑一是MySQL的row返回的都是字符串不自动转int二是strncpy在源字符串太长时不会自动补结尾空字符所以你要手动stu.name[31] \0否则后面printf(%s, stu.name)会越界读取。MYSQL C API的官方头文件里还定义了一批内部结构体比如MYSQL,MYSQL_RES,MYSQL_FIELD。你在写通用查询代码时经常访问res-fields或field-name来动态获取列名MYSQL_FIELD *fields mysql_fetch_fields(res); int cols mysql_num_fields(res); for (int i 0; i cols; i) { printf(Column: %s\n, fields[i].name); }这里的fields[i]是一个结构体变量所以用点号.name。很多初学者会写成fields[i]-name编译器会直接报错因为fields[i]不是指针。这种混用点号和箭头的错误本质上还是2.3节说的判断法则没吃透。5. 结构体成员访问的典型错误与排查要点5.1 段错误指针没指向有效内存就访问结构体指针成员访问最经典的问题就是野指针。看这段代码Node *node; node-data 10; // node没有初始化悬空指针运行时崩溃这里的node是个局部变量里面的值是不确定的垃圾地址。你用node-data就是往一个随机地址写数据不崩溃只能说你运气好。正确做法是Node *node (Node *)malloc(sizeof(Node)); if (node ! NULL) { node-data 10; } free(node);另一个变种是结构体内部有指针成员typedef struct { char *name; } User; User u; strcpy(u.name, hello); // u.name 未初始化崩溃要想到u.name本身是个指针要么指向合法的已分配内存要么先给它分配u.name (char *)malloc(32);访问结构体成员时首先要保证“这个成员所在的存储是合法的”。这个道理听起来简单但排查崩溃的时候很多人只盯着“访问那行代码”忘记看上面的指针分配。排查工具上Linux下用gdb看core文件能直接告诉你崩溃在哪个文件哪一行如果有内存检测工具valgrind会提示非法读写。平时写代码如果遇到“结构体莫名被篡改”先检查是不是有野指针越界写到了结构体附近的内存。5.2 结构体对齐成员访问“看似错乱”的元凶前面讲过编译器会在结构体成员之间插入填充字节。如果你用“偏移量”的思维去手动读取结构体内存比如从缓冲区偏移某些字节解析字段必须清楚结构体本身的对齐规则。最常见的对齐坑你定义了结构体然后在两个不同源文件里或者两个不同编译选项下编译得到不同布局。或者你用sizeof(struct Test)作为缓存区大小实际写入单个成员时越界。解决手段有两个方向显式控制对齐GCC/Clang用__attribute__((packed))MSVC用#pragma pack(push, 1)可以让结构体按紧凑方式排布没有填充字节。不依赖结构体布局序列化时逐成员读写前面已经讲了这个思想。嵌入式场景里__attribute__((packed))很常用因为通信协议帧往往要求没有填充。但有代价——编译器访问未对齐成员时有些平台会生成多字节读取代码性能下降甚至有些ARM内核直接硬件报错。所以不是所有结构体都该packed只有真正需要映射到外部协议时才用。另外注意C里如果结构体里有std::string之类的非平凡类型packed会更多问题不要轻易对整个C结构体做无脑pack。5.3 浅拷贝结构体赋值与成员覆盖的隐患C/C结构体变量的直接赋值是逐字节/逐成员拷贝这本身没问题但如果结构体里有指针就变成了浅拷贝struct Buffer { int size; char *data; }; Buffer a; a.size 4; a.data (char *)malloc(4); a.data[0] A; Buffer b a; // b.size a.size; b.data a.data; 是同一个指针现在b.data和a.data指向同一块内存你修改b.data[0]会直接影响a。如果两个变量都执行了free就是双重释放程序直接崩溃。这类问题在C里建议用std::string、std::vector这类自带深拷贝语义的容器替代裸指针。写纯C的时候要么手动实现一个“深拷贝函数”逐个成员赋值并重新分配指针指向的内存要么干脆禁止直接赋值结构体做成clone(const Buffer*)返回新结构体。5.4 常见问题速查表问题现象可能原因排查思路用指针访问成员时崩溃指针未初始化或已释放检查指针赋值gdb/valgrind结构体占内存比预期大内存对齐填充用offsetof查看成员偏移文件写出的数据乱二进制写入了填充字节逐字段写避免fwrite整个结构体网络传输后数据不对字节序不一致手动打包时明确大小端结构体含字符串赋值后改动一个变量影响另一个浅拷贝深拷贝或改用C容器Keil调试看到成员值不合理外设寄存器基址/时钟未正确配置对照芯片手册确认偏移Qt跨线程发结构体信号失败未qRegisterMetaType注册自定义meta类型MySQL结果用结构体承接后字符串截断strncpy末尾未手动加\0手动补结尾空字符6. 写在最后几个亲测有效的习惯最后分享三个我实际写结构体相关代码时养成的习惯不一定写进教科书但很管用。第一个习惯所有结构体变量定义时直接 {0}。不管后面会不会马上赋值先把内存铺平。这能避免一类“某个成员忘了赋值读出来是垃圾值”的问题真的能省掉很多Debug时间。C里如果有默认构造函数那也尽量在构造函数里把所有成员初始化而不是只初始化一部分。第二个习惯只要涉及指针成员先想清楚“谁拥有这块内存”。我见过太多崩溃都是因为几个结构体共享同一块堆内存A释放了B还在用。解决办法是在设计阶段就明确结构体是“视图”只引用外部内存还是“所有者”负责分配和释放。如果是所有者记得写深拷贝或者禁用拷贝。第三个习惯在成员访问运算上始终保持类型意识。写node-next-data时心里默念“node是指针next也是指针data是int”。每次用点号或箭头之前问自己“点号左边是什么类型”。这个习惯养成后编译错误会少一大半。结构体成员访问这个课题说难不难说简单也不简单。希望这篇文章能帮你把点号、箭头、偏移量、对齐这些概念串联起来以后写结构体相关代码的时候不只靠“大概”而是真正心中有数。