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

来源:互联网 发布:水文地质钻孔数据 编辑:程序博客网 时间:2024/06/08 16:59

结构体

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) =  7printf("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字符而导致的问题

原创粉丝点击