前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C/C++ 学习笔记五(结构体、字符与字符串)

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

原创
作者头像
Celebi
修改于 2017-08-23 01:42:41
修改于 2017-08-23 01:42:41
2.3K0
举报
文章被收录于专栏:Celebi的专栏Celebi的专栏

结构体

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

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

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

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

代码语言:txt
AI代码解释
复制
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。

代码语言:txt
AI代码解释
复制
struct NoPropertyStruct {

}noPropertyStruct;

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

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

代码语言:txt
AI代码解释
复制
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。

代码语言:txt
AI代码解释
复制
 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指定的数值当前结构体最大长度成员的的较小值的整数倍。

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

代码语言:txt
AI代码解释
复制
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字节。

代码语言:txt
AI代码解释
复制
//优化前
typedef struct {
    char a;
    double b;
    char c;
}
//优化后
typedef struct {
    char a;
    char c;
    double b;
}

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

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

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

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

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

代码语言:txt
AI代码解释
复制
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。

代码语言:txt
AI代码解释
复制
 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。

代码语言:txt
AI代码解释
复制
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字符数组,便无法预知返回的接口是多少。

代码语言:txt
AI代码解释
复制
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中去

代码语言:txt
AI代码解释
复制
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数组找中。

代码语言:txt
AI代码解释
复制
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字符而导致的问题

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
c语言学习之数组3
#include <stdio.h> int main() {   char cArr[] = {'I', 'L', 'O', 'V', 'E', 'C'};   char sArr[] = "ilovec";   int i = sizeof(cArr);   int j = sizeof(sArr);   printf("字符数组的长度:%d, 字符串的长度: %d", i, j);     return 0; }
py3study
2020/01/14
4210
C语言之字符、整数、数组、字符串笔记
每种类型占用内存空间不一样,比如char占一个字节,short占2个字节,int占4个字节,double占8个字节
tandaxia
2018/09/27
2.8K0
C语言之字符、整数、数组、字符串笔记
c语言基础学习05_数组和字符串
============================================================================= 涉及到的知识点有:for循环有两种写法、数组、一维数组定义与使用、一维数组的初始化、 如何得到一个一维数组的成员数量、查找出一维数组中成员最大值、查找一维数组的第二大元素的值、 一维数组的逆置、一维数组排序:冒泡排序、二维数组、二维数组的初始化、三维数组初始化、三维数组排序、 字符串与字符数组、字符数组的初始化、字符数组的使用(以及字符数组和字符串的区别)、去除输出字符串结尾处的空格、 现在要去掉字符串最右面的空格,而不能去掉字符串中间的空格呢、随机数产生函数rand与srand、 自动的变种子、控制随机数的范围、用scanf来输入字符串、如何把两次输入的字符串放到新的字符串里去、 scanf缓冲区溢出的危险的解释、字符串的逆置。 ============================================================================= for循环有两种写法:
黑泽君
2018/10/11
2.5K0
C/C++ 学习笔记七(内存管理)
相对于其他语言,C、C++的一大利器便是可以非常灵活的控制内存。与此同时,另一方面灵活的带来的要求也是十分严格,否则会出现令人头疼的分配错误、内存越界、内存泄漏等众多内存问题。 程序内存结构 C程序的
Celebi
2017/08/25
2K0
C/C++ 学习笔记七(内存管理)
一文带你了解c++和c中字符串的使用
对于c语言当中,你好像没有看到有关于字符串定义的关键字,不像我们常规的整型、浮点型、字符类型、指针、数组、结构体等数据类型,都能够一眼就能看出他们是什么数据类型,但是如果你对c语言理解不是很深的话,那你可能就不能"享受"到这里面的"美味"用法了,既然标题都标注了这个,我也不卖关子,下面会有总结分享的。说完了c,那么对于我们的c++来说,它定义字符串就简单多了,因为有关键字来定义,你一看就知道。那么下面大家就随着我的笔步一起来看看究竟吧!
用户6280468
2022/03/21
8040
一文带你了解c++和c中字符串的使用
redis学习 - sds字符串
Redis 设计与实现:如果想要知道redis底层,这本书可以给予不少的帮助,非常推荐每一位学习redis的同学去翻一翻。
阿东
2021/08/16
3280
redis学习 - sds字符串
C语言——I/深入理解指针(五)
sizeof是操作符,计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的大小。 sizeof 只关注占⽤内存空间的大小,不在乎内存中存放什么数据。
用户11015888
2024/03/11
810
初识C语言·指针(5)
sizeof是C语言的关键字,被用来计算某个数据在内存中占的空间大小,不会关心存放的是什么数据。sizeof有个值得注意的点就是sizeof后面是类型的话一定加括号,如果是变量的话可以不用加,其次就是sizeof的返回值是size_t类型的,打印的时候需要用%zd打印,最后的结果单位是字节。
_lazy
2024/10/16
560
初识C语言·指针(5)
[C语言]初阶指针和结构体
内存:内部存储器,暂存程序/数据——掉电丢失 SRAM,DRAM,DDR,DDR2,DDR3。
IT编程爱好者
2023/04/12
5380
[C语言]初阶指针和结构体
C/C++ 学习笔记三(函数)
导语 函数在编程语言中可谓“头等公民”,理解函数的实现原理,函数的一些方法论对于编程非常有好处。 我将从函数的实现原理以及编写函数的一些建议两个的角度来重新认识一下C、C++中的函数。 那具体函数在汇
Celebi
2017/08/21
1.2K0
C/C++ 学习笔记三(函数)
C语言字符串详解
字符串是一种非常重要的数据类型,但是C语言不存在显式的字符串类型,C语言中的字符串都以字符串常量的形式出现或存储在字符数组中。同时,C 语言提供了一系列库函数来对操作字符串,这些库函数都包含在头文件 string.h 中。
小林C语言
2020/12/25
3.9K0
C语言字符串详解
聊点基础的--sizeof,strlen,数组,字符串在一起能整哪些坑?
对于初学者来说,sizeof,strlen,数组,字符串整在一起是痛苦的,它总能在某些莫名其妙的时候整一个措手不及。本文看看它们在一起能挖什么坑。
编程珠玑
2019/11/24
1K0
redis为什么不直接使用C字符串,而要自定义简单动态字符串?
Redis (一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。)没有直接使用 C 语言传统的字符串表示redis中的字符串,而是使用了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串。
编程珠玑
2020/05/22
1.5K0
C++学习笔记---------基础知识sizeof用法
返回值的类型是标准库命名为size_t的类型,size_t类型定义在cstddef头文件中,该头文件是C标准库的头文件stddef.h的C++版本。他是一个和 机器相关的unsigned类型,其大22:14:53小足以保证内存中对象的大小。
ccf19881030
2019/04/29
5830
【C】数组和指针练习
这里的printf("%d\n", strlen(*arr)); printf("%d\n", strlen(arr[1]))在运行时会报错,所以下面的运行结果不包括这两个,具体原因见后文
阿伟@t
2023/10/10
2050
【C】数组和指针练习
【C 语言】字符串操作 ( strlen 与 sizeof 函数 | 计算 字符串长度 与 内存块大小 )
strlen() 函数的作用是获取字符串大小 , 其原理是 从 内存某个首地址 开始计数 , 知道碰到 '\0' 字符结束 , 计算字符串长度 , 其中 计数中 不包含 '\0' 字符 ;
韩曙亮
2023/03/29
1.3K0
【C 语言】字符串操作 ( strlen 与 sizeof 函数 | 计算 字符串长度 与 内存块大小 )
【重拾C语言】六、批量数据组织(三)数组初值;字符串、字符数组、字符串数组;类型定义 typedef
本文介绍了C语言:数组初值;字符串、字符数组、字符串数组;类型定义 typedef
Qomolangma
2024/07/30
1220
【重拾C语言】六、批量数据组织(三)数组初值;字符串、字符数组、字符串数组;类型定义 typedef
C语言进阶——字符串&&内存函数
  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉
北 海
2023/07/01
5220
C语言进阶——字符串&&内存函数
C语言0长度数组(可变数组/柔性数组)详解
众所周知, GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展.
用户6280468
2022/06/09
6.2K0
C语言0长度数组(可变数组/柔性数组)详解
Apple 操作系统可执行文件 Mach-O
Mach-O 的全称是 Mach Object File Format。可以是可执行文件,目标代码或共享库,动态库。Mach 内核的操作系统比如 macOS,iPadOS 和 iOS 都是用的 Mach-O。Mach-O 包含程序的核心逻辑,以及入口点主要功能。
用户7451029
2020/06/16
3K0
推荐阅读
相关推荐
c语言学习之数组3
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档