读陈浩的《C语言结构体里的成员数组和指针》总结,零长度数组
原文链接:C语言结构体里的成员数组和指针
复制例如以下:
单看这文章的标题,你可能会认为好像没什么意思。你先别下这个结论,相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接。微博截图例如以下。我认为好多人对这段代码的理解还不够深入。所以写下了这篇文章。
为了方便你把代码copy过去编译和调试,我把代码列在以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <stdio.h> struct str{ int len; char s[0]; }; struct foo { struct str *a; }; int main( int argc, char ** argv) { struct foo f={0}; if (f.a->s) { printf ( f.a->s); } return 0; } |
你编译一下上面的代码,在VC++和GCC下都会在14行的printf处crash掉你的程序。@Laruence 说这个是个经典的坑。我认为这怎么会是经典的坑呢?上面这代码,你一定会问。为什么if语句推断的不是f.a?而是f.a里面的数组?写这样代码的人脑子里在想什么?还是用这种代码来玩票?无论怎么样,看过原微博的回复。我个人认为大家主要还是对C语言理解不深,假设这算坑的话,那么全都是坑。
接下来,你调试一下,或是你把14行的printf语句改成:
1
|
printf ( "%x\n" , f.a->s); |
你会看到程序不crash了。
程序输出:4。 这下你知道了。訪问0x4的内存地址,不crash才怪。于是,你一定会有例如以下的问题:
1)为什么不是 13行if语句出错?f.a被初始化为空了嘛,用空指针訪问成员变量为什么不crash?
2)为什么会訪问到了0x4的地址?靠,4是怎么出来的?
3)代码中的第4行,char s[0] 是个什么东西?零长度的数组?为什么要这样玩?
让我们从基础開始一点一点地来解释C语言中这些诡异的问题。
结构体中的成员
首先,我们须要知道——所谓变量,事实上是内存地址的一个抽像名字罢了。在静态编译的程序中,全部的变量名都会在编译时被转成内存地址。
机器是不知道我们取的名字的,仅仅知道地址。
所以有了——栈内存区。堆内存区,静态内存区。常量内存区。我们代码中的全部变量都会被编译器预先放到这些内存区中。
有了上面这个基础,我们来看一下结构体中的成员的地址是什么?我们先简单化一下代码:
1
2
3
4
|
struct test{ int i; char *p; }; |
上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。
假设我们有这种代码:
1
|
struct test t; |
我们用gdb跟进去。对于实例t,我们能够看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# t实例中的p就是一个野指针 ( gdb ) p t $1 = {i = 0, c = 0 '\000' , d = 0 '\000' , "1\355I\211\..." } # 输出t的地址 ( gdb ) p &t $2 = (struct test *) 0x7fffffffe5f0 #输出(t.i)的地址 ( gdb ) p &(t.i) $3 = (char **) 0x7fffffffe5f0 #输出(t.p)的地址 ( gdb ) p &(t.p) $4 = (char **) 0x7fffffffe5f4 |
我们能够看到,t.i的地址和t的地址是一样的,t.p的址址相对于t的地址多了个4。说白了,t.i 事实上就是(&t + 0x0), t.p 的事实上就是 (&t + 0x4)。
0x0和0x4这个偏移地址就是成员i和p在编译时就被编译器给hard code了的地址。
于是。你就知道,无论结构体的实例是什么——訪问其成员事实上就是加成员的偏移量。
以下我们来做个实验:
1
2
3
4
5
6
7
8
9
10
|
struct test{ int i; short c; char *p; }; int main(){ struct test *pt=NULL; return 0; } |
编译后,我们用gdb调试一下。当初始化pt后,我们看看例如以下的调试:(我们能够看到就算是pt为NULL,訪问当中的成员时,事实上就是在訪问相对于pt的内址)
1
2
3
4
5
6
7
8
|
( gdb ) p pt $1 = (struct test *) 0x0 ( gdb ) p pt->i Cannot access memory at address 0x0 ( gdb ) p pt->c Cannot access memory at address 0x4 ( gdb ) p pt->p Cannot access memory at address 0x8 |
注意:上面的pt->p的偏移之所以是0x8而不是0x6,是由于内存对齐了(我在64位系统上)。关于内存对齐。可參看《深入理解C语言》一文。
好了,如今你知道为什么原题中会訪问到了0x4的地址了吧,由于是相对地址。
相对地址有非常好多处,其能够玩出一些有意思的编程技巧。比方把C搞出面向对象式的感觉来。你能够參看我正好11年前的文章《用C写面向对像的程序》(用指针类型强转的危急玩法——相对于C++来说,C++编译器帮你管了继承和虚函数表,语义也清楚了非常多)
指针和数组的区别
有了上面的基础后,你把源码中的struct str结构体中的char s[0];改成char *s;试试看。你会发现,在13行if条件的时候,程序由于Cannot access memory就直接挂掉了。为什么声明成char s[0]。程序会在14行挂掉,而声明成char *s。程序会在13行挂掉呢?那么char *s 和 char s[0]有什么区别呢?
在说明这个事之前。有必要看一下汇编代码,用GDB查看后发现:
- 对于char s[0]来说。汇编代码用了lea指令,lea 0x04(%rax), %rdx
- 对于char*s来说,汇编代码用了mov指令,mov 0x04(%rax), %rdx
lea全称load effective address,是把地址放进去。而mov则是把地址里的内容放进去。
所以,就crash了。
从这里。我们能够看到。訪问成员数组名事实上得到的是数组的相对地址,而訪问成员指针事实上是相对地址里的内容(这和訪问其他非指针或数组的变量是一样的)
换句话说,对于数组 char s[10]来说,数组名 s 和 &s 都是一样的(不信你能够自己写个程序试试)。在我们这个样例中,也就是说。都表示了偏移后的地址。
这样。假设我们訪问 指针的地址(或是成员变量的地址),那么也就不会让程序挂掉了。
正如以下的代码。能够执行一点也不会crash掉(你汇编一下你会看到用的都是lea指令):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct test{ int i; short c; char *p; char s[10]; }; int main(){ struct test *pt=NULL; printf ( "&s = %x\n" , //等价于 printf("%x\n", &(pt->s) ); printf ( "&i = %x\n" , //由于操作符优先级。我没有写成&(pt->i) printf ( "&c = %x\n" , printf ( "&p = %x\n" , return 0; } |
看到这里,你认为这能算坑吗?不要出什么事都去怪语言。大家要想想是不是问题出在自己身上。
关于零长度的数组
首先,我们要知道。0长度的数组在ISO C和C++的规格说明书中是不同意的。这也就是为什么在VC++2012下编译你会得到一个警告:“arning C4200: 使用了非标准扩展 : 结构/联合中的零大小数组”。
那么为什么gcc能够通过而连一个警告都没有?那是由于gcc 为了预先支持C99的这样的玩法,所以。让“零长度数组”这样的玩法合法了。关于GCC对于这个事的文档在这里:“Arrays
of Length Zero”,文档中给了一个样例(我改了一下,改成能够执行的了):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <stdlib.h> #include <string.h> struct line { int length; char contents[0]; // C99的玩法是:char contents[]; 没有指定数组长度 }; int main(){ int this_length=10; struct line *thisline = ( struct line malloc ( sizeof ( struct line) thisline->length = this_length; memset (thisline->contents, 'a' , return 0; } |
上面这段代码的意思是:我想分配一个不定长的数组,于是我有一个结构体,当中有两个成员,一个是length,代表数组的长度,一个是contents。代码数组的内容。后面代码里的 this_length(长度是10)代表是我想分配的数据的长度。(这看上去是不是像一个C++的类?)这样的玩法英文叫:Flexible Array,中文翻译叫:柔性数组。
我们来用gdb看一下:
1
2
3
4
5
6
7
8
|
( gdb ) p thisline $1 = (struct line *) 0x601010 ( gdb ) p *thisline $2 = {length = 10, contents = 0x601010 "\n" } ( gdb ) p thisline->contents $3 = 0x601014 "aaaaaaaaaa" |
我们能够看到:在输出*thisline时。我们发现当中的成员变量contents的地址竟然和thisline是一样的(偏移量为0x0?
?
!!)。
可是当我们输出thisline->contents的时候,你又发现contents的地址是被offset了0x4了的,内容也变成了10个‘a’。(我认为这是一个GDB的bug,VC++的调试器就能非常好的显示)
我们继续,假设你sizeof(char[0])或是 sizeof(int[0]) 之类的零长度数组,你会发现sizeof返回了0,这就是说,零长度的数组是存在于结构体内的,可是不占结构体的size。
你能够简单的理解为一个没有内容的占位标识,直到我们给结构体分配了内存,这个占位标识才变成了一个有长度的数组。
看到这里,你会说,为什么要这样搞啊,把contents声明成一个指针,然后为它再分配一下内存不行么?就像以下一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct line { int length; char *contents; }; int main(){ int this_length=10; struct line *thisline = ( struct line malloc ( sizeof ( struct line)); thisline->contents = ( char *) malloc ( sizeof ( char ) thisline->length = this_length; memset (thisline->contents, 'a' , return 0; } |
这不一样清晰吗?并且也没什么怪异难懂的东西。是的,这也是普遍的编程方式,代码是非常清晰,也让人非常easy理解。即然这样,那为什么要搞一个零长度的数组?有毛意义?!
这个事情出来的原因是——我们想给一个结构体内的数据分配一个连续的内存!这样做的意义有两个优点:
第一个意义是。方便内存释放。假设我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free能够释放结构体,可是用户并不知道这个结构体内的成员也须要free。所以你不能指望用户来发现这个事。所以,假设我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针。用户做一次free就能够把全部的内存也给释放掉。
(读到这里,你一定会认为C++的封闭中的析构函数会让这事easy和干净非常多)
第二个原因是,这样有利于訪问速度。
连续的内存故意于提高訪问速度。也故意于降低内存碎片。
(事实上,我个人认为也没多高了,反正你跑不了要用做偏移量的加法来寻址)
我们来看看是怎么个连续的。用gdb的x命令来查看:(我们知道,用struct line {}中的那个char contents[]不占用结构体的内存。所以。struct line就仅仅有一个int成员,4个字节,而我们还要为contents[]分配10个字节长度,所以。一共是14个字节)
1
2
3
|
( gdb ) x /14b thisline 0x601010: 10 0 0 0 97 97 97 97 0x601018: 97 97 97 97 97 97 |
从上面的内存布局我们能够看到,前4个字节是 int length,后10个字节就是char contents[]。
假设用指针的话,会变成这个样子:
1
2
3
4
5
6
|
( gdb ) x /16b thisline 0x601010: 1 0 0 0 0 0 0 0 0x601018: 32 16 96 0 0 0 0 0 ( gdb ) x /10b this->contents 0x601020: 97 97 97 97 97 97 97 97 0x601028: 97 97 |
上面一共输出了四行内存。当中,
- 第一行前四个字节是 int length。第一行的后四个字节是对齐。
- 第二行是char* contents。64位系统指针8个长度,他的值是0x20 0x10 0x60 也就是0x601020。
- 第三行和第四行是char* contents指向的内容。
从这里,我们看到。当中的区别——数组的原地就是内容。而指针的那里保存的是内容的地址。
后记
好了,我的文章到这里就结束了。可是。请同意我再唠叨两句。
1)看过这篇文章,你认为C复杂吗?我认为并不简单。
某些地方的复杂程度不亚于C++。
2)那些学不好C++的人一定是连C都学不好的人。
连C都没学好,你们根本没有资格歧视C++。
3)当你们在说有坑的时候。你得问一下自己,是真有坑还是自己的学习能力上出了问题。
假设你认为你的C语言还不错。欢迎你看看《C语言的谜题》还有《谁说C语言非常easy?》还有《语言的歧义》以及《深入理解C语言》一文。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn ,请勿用于不论什么商业用途)
自己的总结
一、printf 的參数
首先对14行的 printf(f.a->s); 使用方法感到非常陌生,这个要输出的是什么?printf 还能够直接输出一个变量、前面没有不论什么双引號(输出格式说明)吗?类似地。我们试试输出成员变量 len。
printf(f.a->len);
这样直接报错:invalid conversion from `int' to `const char*'
查看 printf 的函数声明,例如以下:
int printf ( const char * format, ... );
第一个是const char* 型,后面是可变參数。注意,第一个是const char*,也就是字符指针!
所以直接printf(f.a->s)当然能够,由于f.a->s就是字符指针。!而我们寻常所写的printf("..."); 当中的双引號字符串就是const char*类型!
这样,再写一个简单的測试程序:
#include <stdio.h>
#include <stdlib.h> int main(int argc, char** argv) {
char *s="abc";
printf(s);
system("pause");
return 0;
}
能够看到,能够正常输出abc。
就是输出字符指针所指向的内容。
而我们知道。对于一个指向struct的null指针来说,取得其成员变量的地址是能够的。而取其成员变量则会出问题(详细原因见上面陈浩原文解释),这个类似于C++中一个指向class的null指针,能够通过该指针调用其成员函数,而通过该指针获得成员变量则会出问题。
二、零长度数组
见上文作者总结。
读陈浩的《C语言结构体里的成员数组和指针》总结,零长度数组的更多相关文章
- C语言结构体里的成员数组和指针
struct test{ int i; char *p; }; struct test *str; ; char *b = "ioiodddddddddddd"; str = (s ...
- 在C语言结构体中添加成员函数
我们在使用C语言的结构体时,经常都是只定义几个成员变量,而学过面向对象的人应该知道,我们定义类时,不只是定义了成员变量,还定义了成员方法,而类的结构和结构体非常的相似,所以,为什么不想想如何在C语言结 ...
- C语言 结构体中的成员域偏移量
//C语言中结构体中的成员域偏移量 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> # ...
- 深入理解指针—>结构体里的成员数组和指针
单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个结论,相信这篇文章会对你理解C语言有帮助.这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接.微博截图如 ...
- Linux C语言结构体-学习笔记
Linux C语言结构体简介 前面学习了c语言的基本语法特性,本节进行更深入的学习. 预处理程序. 编译指令: 预处理, 宏定义, 建立自己的数据类型:结构体,联合体,动态数据结构 c语言表达式工具 ...
- C语言结构体的强制类型转换
陈浩师兄03年的一篇博客<用C写有面向对象特点的程序>描述了用C语言来实现类似C++类继承的方法,这样方法的核心要点就是结构体的强制类型转换,让我来简单分析分析C语言中的结构体强制类型转换 ...
- 失落的C语言结构体封装艺术
Eric S. Raymond <esr@thyrsus.com> 目录 1. 谁该阅读这篇文章 2. 我为什么写这篇文章 3.对齐要求 4.填充 5.结构体对齐及填充 6.结构体重排序 ...
- C语言 结构体的内存对齐问题与位域
http://blog.csdn.net/xing_hao/article/details/6678048 一.内存对齐 许多计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地 ...
- (转)失落的C语言结构体封装艺术
目录1. 谁该阅读这篇文章 2. 我为什么写这篇文章 3.对齐要求 4.填充 5.结构体对齐及填充 6.结构体重排序 7.难以处理的标量的情况 8.可读性和缓存局部性 9.其他封装的技术 10.工具 ...
随机推荐
- creat-react-app/dva静态项目,用nginx部署在次级域名路径(如a.com/sub/)需要注意的几点
因为要把dist文件夹部署在一个域名的次级目录,没想到和运维同学一起折腾了一下午.. 放在这里备忘,也给后来的同学一些可查的中文资料: 1,dva/cra给你的模板index.html是在public ...
- TOJ1550: Fiber Communications
1550: Fiber Communications Time Limit(Common/Java):1000MS/10000MS Memory Limit:65536KByteTotal ...
- 【Luogu】P3332K大数查询(树套树)
题目链接 这题我费尽心思不用标记永久化终于卡过去了qwq 权值线段树下面套一个区间线段树.然后乱搞搞即可. // luogu-judger-enable-o2 #include<cstdio&g ...
- [POJ3728]The merchant(tanrjan_lca + DP)
传送门 比着题解写还错... 查了两个小时没查出来,心态爆炸啊 以后再查 ——代码(WA) #include <cstdio> #include <cstring> #incl ...
- [USACO12DEC]第一!First! (Trie树,拓扑排序)
题目链接 Solution 感觉比较巧的题啊... 考虑几点: 可以交换无数次字母表,即字母表可以为任意形态. 对于以其他字符串为前缀的字符串,我们可以直接舍去. 因为此时它所包含的前缀的字典序绝对比 ...
- centos7配置国内yum源
文章目录 1.什么是yum仓库? 2.yum仓库配置 2.1.阿里镜像仓库配置 2.1.1.配置步骤 2.1.2.epel源 安装和配置 2.1.3.查看yum源 2.2.配置 清华大学镜像仓库 1. ...
- 主机ping不通虚拟机,但是虚拟机能ping通主机
一.虚拟机网络连接方式选择Nat 二. 关闭Linux防火墙命令:service iptables stop / service firewalld stop 查看Linux防火墙状态命令:servi ...
- lucas定理 +证明 学习笔记
lucas定理 p为素数 \[\dbinom n m\equiv\dbinom {n\%p} {m\%p} \dbinom {n/p}{m/p}(mod p)\] 左边一项直接求,右边可递归处理,不包 ...
- hdu 1277 AC自动机
全文检索 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submis ...
- DataSet中的表动态设置主键外键的方法
原文发布时间为:2008-08-01 -- 来源于本人的百度文章 [由搬家工具导入] protected void pk_Click(object sender, EventArgs e) { ...