C/C++ 学习笔记五(结构体、字符与字符串)

结构体

C语言中复杂的数据结构都需要使用结构体表示,在这里说一下结构体的使用要点。

结构体内存分布以及对齐问题

编译器在为结构体分配内存时,并不会分配和所有成员数据长度和恰好相等的内存空间,而是会考虑到计算机cpu的读取性能,对结构按照某个模数(alignment modulus)进行对齐。

例如结构体中拥有int (4个字节),char(1个字节)两个变量,但在使用sizeof进行大小输出时,并不是简单的各个成员数据的大小相加(4+1 = 5字节),而是经过编译器对齐后的8个字节

struct TestStruct {

    int a;
    char b;
}testStruct;

printf("%p \n",&testStruct); // 8

出现这样的结果,是因为编译器在对数据成员的内存地址上进行了对齐。而这种对齐是与计算机CPU相关的一种优化技术,计算机系统中对基本的数据类型在内存中的存放位置有限制,他们会要求这些数据的内存地址是某一个数的k倍,这个k值也被称为对齐模数(alignment modulus)

空结构体c与c++的sizeof得出的长度是不一致的

这是一个有意思的问题,对于无成员的结构体长度,c与c++的长度却是不一致的。空结构体的长度在c中为0,而c++中则为1。

struct NoPropertyStruct {

}noPropertyStruct;

printf("%lu \n",sizeof(noPropertyStruct)); // C时输出0,C++时输出1

查看C、C++两者的汇编代码

C`main:
    0x100000f20 <+0>:  pushq  %rbp
    0x100000f21 <+1>:  movq   %rsp, %rbp
    0x100000f24 <+4>:  subq   $0x20, %rsp
    0x100000f28 <+8>:  leaq   0x62(%rip), %rax          ; "sizeof(TestStruct) = %lu \n"
->  0x100000f2f <+15>: xorl   %ecx, %ecx                ;对ecx做异或,也就是ecx置为0
    0x100000f31 <+17>: movl   %ecx, %edx
    0x100000f33 <+19>: movl   $0x0, -0x4(%rbp)
    0x100000f3a <+26>: movl   %edi, -0x8(%rbp)
    0x100000f3d <+29>: movq   %rsi, -0x10(%rbp)
    0x100000f41 <+33>: movq   %rax, %rdi
    0x100000f44 <+36>: movq   %rdx, %rsi
    0x100000f47 <+39>: movb   $0x0, %al
    0x100000f49 <+41>: callq  0x100000f5e               ; symbol stub for: printf
    0x100000f4e <+46>: movl   $0x1, %ecx
    0x100000f53 <+51>: movl   %eax, -0x14(%rbp)
    0x100000f56 <+54>: movl   %ecx, %eax
    0x100000f58 <+56>: addq   $0x20, %rsp
    0x100000f5c <+60>: popq   %rbp
    0x100000f5d <+61>: retq   


  CXX`main:
    0x100000f30 <+0>:  pushq  %rbp
    0x100000f31 <+1>:  movq   %rsp, %rbp
    0x100000f34 <+4>:  subq   $0x20, %rsp
    0x100000f38 <+8>:  leaq   0x53(%rip), %rax          ; "sizeof(TestStruct) = %lu \n"
->  0x100000f3f <+15>: movl   $0x1, %ecx                ;立即数0x1直接赋值给ecx
    0x100000f44 <+20>: movl   %ecx, %edx
    0x100000f46 <+22>: movl   $0x0, -0x4(%rbp)
    0x100000f4d <+29>: movl   %edi, -0x8(%rbp)
    0x100000f50 <+32>: movq   %rsi, -0x10(%rbp)
    0x100000f54 <+36>: movq   %rax, %rdi
    0x100000f57 <+39>: movq   %rdx, %rsi
    0x100000f5a <+42>: movb   $0x0, %al
    0x100000f5c <+44>: callq  0x100000f72               ; symbol stub for: printf
    0x100000f61 <+49>: movl   $0x1, %ecx
    0x100000f66 <+54>: movl   %eax, -0x14(%rbp)
    0x100000f69 <+57>: movl   %ecx, %eax
    0x100000f6b <+59>: addq   $0x20, %rsp
    0x100000f6f <+63>: popq   %rbp
    0x100000f70 <+64>: retq

通过阅读代码,可以看到rax存的是字符串”sizeof(TestStruct) = %lu \n”的位置,而rsi 存放的是sizeof(noPropertyStruct)的数值。追溯rsi的赋值,可以看到最终是通过rdx传递给rsi的。即sizeof的计算是由rdx传递的。而rdx的初始化正是两者c、c++两者不同的地方(上面代码中两处箭头处)。

上面代码c语言汇编代码中xorl %ecx, %ecx对ecx做异或,也就是ecx置为0

而c++语言汇编代码中movl $0x1, %ecx立即数0x1直接赋值给ecx,从汇编代码层面便是十分简单的做了赋值,看出这其实是编译器做了不同的事情,那为什么c的空结构体大小是0而c++的却是1呢?

其实在从C99的标准中有说到

If the struct-declaration-list contains no named members, the behavior is undefined.

便是说空结构体在C标准中是一种未定义的行为,而在很多的编译器中(如GCC,VC)则对此做了兼容,是被允许使用。

那作为C的子集C++又为什么空指针的长度为1?

网上有非常有关此问题的讨论,浏览了众多的解释中,这一篇说得非常的有理有据。

其中最重要的原因是C++肩负这面向对象的设计初衷,而class的底层是由结构体来进行描述,若延续C中对于空结构体长度为0的定义,在遇到声明空class时便遇到了困难。

1.class需分配与释放,若空结构体长度为0,则会遇到多个空class对象无法分配与释放的问题

2.对于空结构体对象,因为空结构体长度为0,无法用地址偏移描述多个空结构体对象

如下例子Foo的两个成员均是来自空结构体Bar,若空结构体中长度为0,便无法使用地址偏移用于描述数组对象a与结构体对象b。

 struct Bar { };
  struct Foo {
    struct Bar a[2];
    struct Bar b;
  };
  Foo f;

究于上面的原因,c++标准中将空结构体长度默认设置为1,与此同时,因为class的底层实现也为结构体,空class的长度也为1。

结构体对齐规则

1.第一个成员数据在偏移地址为0的位置

2.对于每个数据成员,当前成员起始位置为取#pragma pack指定的数值当前数据成员的较小值的整数倍。

3.调整结构体大小,使之为#pragma pack指定的数值当前结构体最大长度成员的的较小值的整数倍。

以一个简答的结构体作为例子说明对齐的规则

typedef struct 
{
    char a;
    long double b;
}testStruct

1.根据结构体对齐规则1,a的偏移地址为0

2.对于long double b,此时的默认的对齐模数为8,long double的长度8,按照结构体对齐规则1,取两者小值,b的地址偏移位置为7,编译器自动为结构体填充7个字节,此时结构体大小为 1+7+8 = 16

3.根据规则3,默认的对齐模数为8,结构体最大长度成员8字节的较小值8。使结构体调整为8的整数倍,此时结构体已经是8的整数倍16,无需调整。

通过优化结构体成员数据位置节省空间

因为有结构体对齐的存在,我们在使用结构体时,可能会因为成员数据排序的不同,编译器为我们分配了无用的内存空间,导致内存空间的浪费。这时候我们可以通过调整成员数据的位置来节省空间。

例如,下方例子默认对齐模数为8时,长度为24。经过将两个char数据提前后,结构体的长度减少为16字节。

//优化前
typedef struct {
    char a;
    double b;
    char c;
}
//优化后
typedef struct {
    char a;
    char c;
    double b;
}

避免结构体之间进行逐字节的比较

同样由于字节对齐的原因,结构体之间进行逐字节比较、或者说内存的比较时,很可能会导致比较的结果并不相同。

这是因为编译器字节对齐时,对于多出的字节未经过初始化,他们包含的内存很可能是任意的,即使有效成员数据相同,也会因为这一部分的数据导致比较时结果不相同。

下例子中因为字节对齐,a与b之前会有2个字节的数据是由编译器取分配的。

即使我们将s1与s2的成员数据设置成相同的值,但在使用memcmp对比时依然返回不为0(两者不相同)

typedef struct  {
    char a;
    int b;
}TestStruct;

TestStruct * s1 = (TestStruct *) malloc(sizeof(TestStruct));
TestStruct * s2 = (TestStruct *) malloc(sizeof(TestStruct));

s1->a ='1';
s2->a ='1';

s1->b =2;
s2->b =2;
printf("%d \n" ,memcmp(s1,s2,sizeof(TestStruct)) ); \\s1与s2不相同,返回不为0的数

依然延续上例子,如果我们在使用结构体前对其数据进行初始化,则可以避免这个问题。此时的s1与s2的每个字节数据全部都一样。当使用memcmp进行内存比较时返回0。

 TestStruct * s1 = (TestStruct *) malloc(sizeof(TestStruct));
 TestStruct * s2 = (TestStruct *) malloc(sizeof(TestStruct));

 memset(s1, 0, sizeof(TestStruct));
 memset(s2, 0, sizeof(TestStruct));

 s1->a ='1';
 s2->a ='1';

 s1->b =2;
 s2->b =2;
 printf("%d \n" ,memcmp(s1,s2,sizeof(TestStruct)) ); //s1与s2相同,此处返回0

字符串

字符、字符数组、字符串区别

字符(字符常量)是由一对单引号括起来的单个字符,在内存中占一个字节,存放的是ASCII码值。

字符串是由一对双引号括起来的字符序列,并在最后自动加上字符终止符’\0’。

字符数组是类型为char的数组,与其他类型的数组一样,是在计算机中表现为一段连续的内存空间。

对于字符的概念比较简单,不做赘述。

这说下字符串与字符数组的区别。

对于字符数组而言,它首先是一个数组,再者数组每个元素存放的数据中都为字符。它与字符串的区别在于,字符会在最后的字符后自动添加终止符’\0’。

也正是因为字符串自动加上’\0’的原因,使用sizeof进行长度会比所看到的数量多1.

如下字符数组cArr有7个元素,sArr虽然只有7个字符,但因为自动补齐’\0’的缘故,输出的长度为8。

char  cArr[] = { 'e','x','a','m','p','l','e'};
char  sArr[] = "example";

printf("sizeof(cArr) =  %lu \n",sizeof(cArr)); //sizeof(cArr) =  7
printf("sizeof(sArr) =  %lu \n",sizeof(sArr)); //sizeof(sArr) =  8

字符数组、字符串的区别

  • 字符数组长度是固定的,并且任何一个元素不做限制,也可以都为\0字符
  • 字符串则必须以’\0’ 结尾,字符串一定是字符数组,它的最后一个字符为’\0’

sizeof 与 strlen 区别

继续回到字符串、字符数组长度的问题,来说下sizeof和strlen的区别。

strlen是一个函数,它的作用是统计从指字符串数组第一个元素开始,到最后一个非null指针的长度。

在上例子中稍做修改,将cArr的第四个元素’p’改成 ‘\0’。

可以看到strlen(cArr)的结果为3,strlen(sArr)因为第8个元素为 ‘\0’,因此长度为7。

再有一个值得注意的是,当对已知元素中都无’\0’的字符数组使用strlen时会得到无法预料的值,如例子中的randomCArr字符数组,便无法预知返回的接口是多少。

char  cArr[] = { 'e','x','a','\0','p','l','e'};
char  sArr[] = "example";
char  randomCArr[] = {'r','a','n','d','o','m'};

printf("strlen(cArr) =  %lu \n",strlen(cArr));  //strlen(cArr) =  3 
printf("strlen(sArr) =  %lu \n",strlen(sArr)); //strlen(sArr) =  7 
printf("strlen(randomCArr) =  %lu \n",strlen(randomCArr)); // strlen(randomCArr) = ????

而sizeof是一个单目运算符,它仅会在编译时使用,它的作用是计算此对象的缓冲区大小。

strcpy与memcpy的区别

使用strcpy的作用是将src源字符串中的第一个开始到’\0’所有字符拷贝至dst目的字符串中。

memcpy的作用是将源src地址开始,拷贝len个字节的数据至dst地址中。

下例子中,将仅会将s,r,c,\0四个字符拷贝至dst字符数组中,并不会src中所有的8个字符拷贝到dst中去

char src[] = {'s','r','c','\0','D','A','T','A'};
char dst[] = {'d','s','t','-','d','a','t','a'};
strcpy(dst, src);
printf("%s \n",dst); // src

在使用strcpy时,因不对src字符的具体字符串数组排列做校验,也没有对目的字符串数组的长度做校验,这便要求我们使用该函数时需要格外的注意。

1.src字符数组有可以预期的终止符\0

2.dst目的字符数组需要有足够的空间

3.避免源地址和目的地址有重叠的部分

而相对于与strcpy的种种需要注意的点,memcpy则显得十分的灵活而言方便。

再以上面的例子,memcopy可以使很方便的将src的所有数据拷贝至dst数组找中。

memcpy(dst, src, sizeof(src));
for (int i = 0; i<sizeof(src); i++) {
    printf("%c",dst[i]);
 }; 
printf("\n"); //打印结果为 srcDATA

小结下strcpy与memcpy

  1. strcpy因为是方便字符串使用,所以在使用时需要考虑\0的情况。
  2. 因为源与目的内存的不可预知,需要考虑两者内存在拷贝是是否有足够的空间、是否会发生重叠、是否会发生还缓冲区溢出

小结

1.结构体的对齐概念非常重要,在对结构体具体关于大小的操作时,需要考虑编译器为结构体多生成的数据中的影响。

2.字符串是字符数组,但字符数组并一定是字符串。

3.在使用有关字符串的c函数时,需要时刻考虑字符串末尾\0字符而导致的问题

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏全沾开发(huā)

学习zepto.js(对象方法)[2]

学习zepto.js(对象方法)[2] 今天来说下zepto那一套dom操作方法, ['prepend', 'append', 'pr...

2976
来自专栏进击的君君的前端之路

this_原型链_继承

972
来自专栏编程

使用dict和set

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。-...

18910
来自专栏猿人谷

C++ 虚拟继承

1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D...

2058
来自专栏前端新视界

立即执行函数表达式(IIFE)

原文:immediately-invoked-function-expression 译者:nzbin 也许你还没有注意到,我是一个对术语比较坚持的人。因此...

1855
来自专栏开源FPGA

Python学习笔记

一、Python基础知识 1. Print()函数是默认以换行符作为其结束值的。下一次对print()的调用将会在下一行显示。 2. 插入引号:单引号用\’,双...

1767
来自专栏快乐八哥

JavaScript内置对象--Math对象

在JavaScript开发中,除了简单加减乘除运算之外,有时候开发,特别是动画或者游戏开发中,需要使用复杂的数学运算。JavaScript中Math对象提供了一...

2145
来自专栏河湾欢儿的专栏

第二节单利、工厂、构造函数、原型链、call、bind、apply、sort

722
来自专栏大前端_Web

javascript语言精粹(蝴蝶书)-笔记

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1733
来自专栏debugeeker的专栏

《coredump问题原理探究》Linux x86版5.5节C风格数据结构内存布局之基本数据类型构成的结构体

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

701

扫码关注云+社区