原文链接: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>
structstr{
    intlen;
    chars[0];
};
 
structfoo {
    structstr *a;
};
 
intmain(intargc,
char** argv) {
    structfoo f={0};
    if(f.a->s) {
        printf( f.a->s);
    }
    return0;
}

你编译一下上面的代码,在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
structtest{
    inti;
    char*p;
};

上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。

假设我们有这种代码:

1
structtest 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',
p = 0x4003e0
"1\355I\211\..."}
 
# 输出t的地址
(gdb) p &t
$2 = (structtest
*) 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
structtest{
    inti;
    shortc;
    char*p;
};
 
intmain(){
    structtest *pt=NULL;
    return0;
}

编译后,我们用gdb调试一下。当初始化pt后,我们看看例如以下的调试:(我们能够看到就算是pt为NULL,訪问当中的成员时,事实上就是在訪问相对于pt的内址)

1
2
3
4
5
6
7
8
(gdb) p pt
$1 = (structtest
*) 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
structtest{
    inti;
    shortc;
    char*p;
    chars[10];
};
 
intmain(){
    structtest *pt=NULL;
    printf("&s = %x\n",
pt->s);
//等价于 printf("%x\n", &(pt->s) );
    printf("&i = %x\n",
&pt->i);
//由于操作符优先级。我没有写成&(pt->i)
    printf("&c = %x\n",
&pt->c);
    printf("&p = %x\n",
&pt->p);
    return0;
}

看到这里,你认为这能算坑吗?不要出什么事都去怪语言。大家要想想是不是问题出在自己身上。

关于零长度的数组

首先,我们要知道。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>
 
structline {
   intlength;
   charcontents[0];
// C99的玩法是:char contents[]; 没有指定数组长度
};
 
intmain(){
    intthis_length=10;
    structline *thisline = (structline
*)
                     malloc(sizeof(structline)
+ this_length);
    thisline->length = this_length;
    memset(thisline->contents,'a',
this_length);
    return0;
}

上面这段代码的意思是:我想分配一个不定长的数组,于是我有一个结构体,当中有两个成员,一个是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
structline {
   intlength;
   char*contents;
};
 
intmain(){
    intthis_length=10;
    structline *thisline = (structline
*)
malloc(sizeof(structline));
    thisline->contents = (char*)malloc(sizeof(char)
* this_length );
    thisline->length = this_length;
    memset(thisline->contents,'a',
this_length);
    return0;
}

这不一样清晰吗?并且也没什么怪异难懂的东西。是的,这也是普遍的编程方式,代码是非常清晰,也让人非常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 ,请勿用于不论什么商业用途)

——=== 訪问 酷壳404页面 寻找遗失儿童。

 ===——

自己的总结

一、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语言结构体里的成员数组和指针》总结,零长度数组的更多相关文章

  1. C语言结构体里的成员数组和指针

    struct test{ int i; char *p; }; struct test *str; ; char *b = "ioiodddddddddddd"; str = (s ...

  2. 在C语言结构体中添加成员函数

    我们在使用C语言的结构体时,经常都是只定义几个成员变量,而学过面向对象的人应该知道,我们定义类时,不只是定义了成员变量,还定义了成员方法,而类的结构和结构体非常的相似,所以,为什么不想想如何在C语言结 ...

  3. C语言 结构体中的成员域偏移量

    //C语言中结构体中的成员域偏移量 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> # ...

  4. 深入理解指针—>结构体里的成员数组和指针

    单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个结论,相信这篇文章会对你理解C语言有帮助.这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接.微博截图如 ...

  5. Linux C语言结构体-学习笔记

    Linux C语言结构体简介 前面学习了c语言的基本语法特性,本节进行更深入的学习. 预处理程序. 编译指令: 预处理, 宏定义, 建立自己的数据类型:结构体,联合体,动态数据结构 c语言表达式工具 ...

  6. C语言结构体的强制类型转换

    陈浩师兄03年的一篇博客<用C写有面向对象特点的程序>描述了用C语言来实现类似C++类继承的方法,这样方法的核心要点就是结构体的强制类型转换,让我来简单分析分析C语言中的结构体强制类型转换 ...

  7. 失落的C语言结构体封装艺术

    Eric S. Raymond <esr@thyrsus.com> 目录 1. 谁该阅读这篇文章 2. 我为什么写这篇文章 3.对齐要求 4.填充 5.结构体对齐及填充 6.结构体重排序 ...

  8. C语言 结构体的内存对齐问题与位域

    http://blog.csdn.net/xing_hao/article/details/6678048 一.内存对齐 许多计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地 ...

  9. (转)失落的C语言结构体封装艺术

    目录1. 谁该阅读这篇文章 2. 我为什么写这篇文章 3.对齐要求 4.填充 5.结构体对齐及填充 6.结构体重排序 7.难以处理的标量的情况 8.可读性和缓存局部性 9.其他封装的技术 10.工具 ...

随机推荐

  1. Get Sauce(状压DP)

    描述 In order to celebrate the 8th anniversary of ZOJ, LCLL goes to a sauce factory to "Get Sauce ...

  2. ubuntu 安装tomcat<服务器>

    一.下载tomcat 可以先下载到本地,然后ftp到服务器 官方 Apache Tomcat 的下载页面(下面的链接是apache自己的镜像服务器的地址,不同网络连接的话,apache会给出不同的镜像 ...

  3. HDU 2594 kmp算法变形

    Simpsons’ Hidden Talents Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java ...

  4. hdu 6108 小C的倍数问题

    小C的倍数问题 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Sub ...

  5. 【CF1015C】Songs Compression(贪心)

    题意: 给定n和m,n组(a[i],b[i]),每一组a[i]可以压缩为b[i],求最少只需要压缩几个,使得m可以存下所有数据,无解输出-1 思路:按差贪心,排序 #include<cstdio ...

  6. 12深入理解C指针之---指针多层间接引用

    该系列文章源于<深入理解C指针>的阅读与理解,由于本人的见识和知识的欠缺可能有误,还望大家批评指教. 一.指针多层引用 1.定义:指针可以用不同的间接引用层级,通常使用多重指针或字符数组来 ...

  7. Git开发必知必会

    比如说你现在准备写一个自己的视频资源网站,在创业初期,你的项目暂时还是测试阶段,没有用户的时候,你可能只有一个人在开发,你每天都以写的内容和时间作为文件名的命名,这样其实是可以满足你对版本控制的基本需 ...

  8. configure.ac:3: error: Autoconf version 2.68 or higher is required

    configure.ac:3: error: Autoconf version 2.68 or higher is required 参考博客:https://blog.csdn.net/pretty ...

  9. 2017CCPC 哈尔滨 B

    这题没有考虑到m这个东西,所以就没有往二分答案的方向想 二分答案 check的时候,我们找的是大于等于x的数有多少个被加入到那个数组中.如果 >= m说明这个数可能是答案,否则就不是. 用尺取来 ...

  10. Oracle PL/SQL块 多表查询(emp员工表、dept部门表、salgrade工资等级表)

    范例: 查询每个员工的编号,姓名,职位,工资,工资等级,部门名称 ●确定要使用的数据表 |- emp表:员工的编号.姓名.职位.工资 |- salgrade表:工资等级 |- dept表:部门名称 ● ...