结构体与共用体基础

结构体基础用法与共用体简述

  • 1.结构体的定义
  • 2.结构体声明及使用
  • 3.结构体成员初始化
  • 4.结构体占用空间探究
    • 4.1 结构体成员所在地址
    • 4.2 按地址值访问结构体内容
    • 4.3 内存对齐
  • 5.共用体
  • 6.总结

1.结构体的定义

之前的课程中,我们介绍了很多数据类型,如整形、浮点型、双精度实型等,不过想象另一种场景,我们申请了一个变量,希望它能够储存很多类型的数据,该怎么做呢?最简单的方法就是定义一个结构体:

#include
#include
struct Student
{
    int age;
    char name[32];
    char sex;
};

这样我们就有了一个自定义的Student结构,内含int类型的年龄、string类型的名字和char类型的性别。那么我们要如何声明和使用呢?

2.结构体声明及使用

结构体的声明方式有两种,分别为:
2.1 定义后使用结构体类型名进行声明
定义后的结构体可以当做一个自定义的类型名,声明的方法与其他类型一样:

int main()
{
	Student ZhangSan;
}

2.2 定义时直接声明
这种定义方式示例如下:

struct Student
{
    int age;
    char name[32];
    char sex;
}ZhangSan,*LiSi; // LiSi是定义的结构体指针

当然,如果使用这种方式进行定义时,后面我们不再需要申请Student类型的变量时,可以不定义结构体名,例如:

struct
{
    int age;
    char name[32];
    char sex;
}ZhangSan;

3.结构体成员初始化

在为结构体变量赋值时,我们需要首先要通过引用的方式找到需要被赋值的成员,通过点运算符实现。当然如果我们定义的是结构体指针变量,则需要用->进行引用:

#include  // 引用strcpy函数
int main()
{
	Student ZhangSan,*LiSi,lisi;
    ZhangSan.age=10;
    strcpy(ZhangSan.name,"ZhangSan");
    ZhangSan.sex='m';
    cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;
    LiSi=&lisi;
    LiSi->age=20;
    strcpy(LiSi->name,"LiSi");
    LiSi->sex='w';
    cout<<LiSi->age<<" "<<LiSi->name<<" "<<(*LiSi).sex<<endl; // 结构体指针引用结构体内容的两种方法
} 
// 输出为:10 ZhangSan m
//        20 LiSi w

strcpy(a,b)函数可以将b字符串赋值给a字符数组,如果a数组有原始内容则b字符串会对a字符数组进行覆盖,注意不要超界。
这里我再强调一下,如果仅仅定义一个结构体指针的话,是不可以引用的和赋值的,因为这个指针此时没有正确的指向(即没有初始化)。
除了上述赋值方法,我们还可以在声明时就进行赋值,方法如下:

int main()
{
    Student ZhangSan={10,"ZhangSan",'m'};
}

这种方法同样适用于另一种声明方式中:

struct Student
{
    int age;
    char name[32];
    char sex;
}ZhangSan={10,"ZhangSan",'m'};

4.结构体占用空间探究

下面我们来讨论结构体占用的空间有什么样的特点。在开始探究之前,先给大家做点小科普。首先我尝试用cout打印变量所占用的地址:

int main()
{
	int a=10;
	char sex='w';
	cout<<&a<<" "<<&sex<<endl;
}
// 输出为:0x61fe1c w

可以看出,虽然&a打印出了我们希望的16进制地址值,但是&sex还是打印出了w字符。 这是因为C++会将char*类型的地址作为C风格字符串处理。这意味着假如sex的所在地址是0x1000,cout<<&sex仍会尝试将0x1000处的字符及其后面的字符作为字符串打印出来,直到遇到空字符为止,这也就是输出仍为w的原因。想要看到sex所在地址,需要先将char地址类型转换成其他指针类型,如void指针类型:

int main()
{
	int a=10;
	char sex='w';
	cout<<&a<<" "<<static_cast<void*>(&sex)<<endl;
}

这样就可以看到我们想要的结果了。

4.1 结构体成员所在地址

我们先看以下代码:

struct Student
{
    int age;
    char name[32];
    char sex;
};
int main()
{
    Student ZhangSan={10,"ZhangSan",'m'},*p;
    p=&ZhangSan;
    cout<<&ZhangSan<<" "<<&(ZhangSan.age)<<" "<<&(ZhangSan.name)<<" "<<static_cast<void*>(&ZhangSan.sex)<<endl;
    cout<<sizeof(Student);
}
// 输出为:0x61fdf0 0x61fdf0 0x61fdf4 0x61fe14
//        40

可以看出,对结构体变量取地址时拿到的是该变量的首地址,这一点与数组相同,而且看起来这几个16进制数字很像连续地址,也就是说结构体的存储方式也是连续存储。不过可能有小伙伴发出疑问了,Student所占用的空间应该是37字节(int为4字节,char为1字节),可为什么打印出的是40字节呢?再看另一个例子,我们把字符数组换成字符串(string类型,所占空间为32字节)再来打印看看:

// 只将结构体内容换掉,其他不变即可
struct Student
{
    int age;
    string name;
    char sex;
};
// 输出为:0x61fdd0 0x61fdd0 0x61fdd8 0x61fdf8
//        48

这下就更奇怪了,int类型明明是占了4字节,为什么age和name之间差了8个字节呢?32位数组和string类型所占的空间应该是一样的,为什么结构体所占空间却变化了呢?

4.2 按地址值访问结构体内容

带着这个疑问,我们来访问结构体所占地址的存储内容分别是什么。C++为我们提供了一种根据地址值访问空间的方法,我们可以利用这种方法来查看下各空间存储的具体内容,应用方法和注意事项我会写在代码的注释里:

struct Student
{
    int age;
    string name;
    char sex;
};
int main()
{
    Student ZhangSan={10,"ZhangSan",'m'};
    /*
    C++中,指针类型的变量均占8字节,因此我使用char指针接收地址值,因为char类型变量占1个字节,某些情况下更易于处理
    */
    int* ptr = reinterpret_cast<int*>(&ZhangSan);// 由于已知ZhangSan的首个元素已知为int型,
                                                 //这里可以直接申请一个int指针,然后将ZhangSan的起始地址转换成int指针进行记录
    cout<<*ptr<<endl; // 将会输出10
    long long address; // C++中指针类型值占8字节,长长整型才装得下并保证内容不丢失 
    address=reinterpret_cast<unsigned long long>(&ZhangSan); // 将ZhangSan的地址值转换成长长整型,以数字形式记录在address中,之后就可以进行整数加法了
    
    /*在C++中,如果你想在同一个作用域内重新声明一个变量,你需要使用花括号来
    创建一个新的作用域。这样就可以安全地重新声明变量。这里不理解也可以申请不
    重名的变量,不影响效果*/
    {
    	char* ptr=reinterpret_cast<char*>(address+4); // 将address值加4再转换成char*并查看存储内容
    	cout<<*ptr<<endl; // 将只输出换行,有兴趣的小伙伴可以尝试用不同的指针取这个地址的内容
    }
    
    {
        char *ptr=reinterpret_cast<char*>(address+8); // address值+8并记录成char*类型
        cout<<*ptr<<endl; // 将没有实际输出内容,只输出换行
        cout<<*(reinterpret_cast<string*>(ptr))<<endl; // 将输出ZhangSan
    }
}

以4.1节中最后一段代码输出为例,0x61fdd0地址以后的四位才有int类型数据,而0x61fdd4到0x61dd8是四个空字节。为什么会是这样的情况呢?

4.3 内存对齐

解释上述问题就要了解计算机的硬件结构了。本文以64位计算机为例,计算机的ram上有8个chip,CPU读取数据最快速的方法就是从8个chip的相同物理位置并行读出内容。假如我们想要的结构体含有两个整型数据,而存储方式是这样存储的:结构体与共用体基础_第1张图片
图中,每个方块代表一个chip的对应存储位置,四个方块代表一个int类型。含有两个int元素的结构体内容占用8个字节,这样紧凑的排列下CPU就可以一次读取全部的结构体内容。但如果我要的结构体要存放一个short和两个int类型的数据呢?紧密型的存储就成了这样:
结构体与共用体基础_第2张图片

这种情况下,CPU想要读取到结构体的所有内容就必须读取两次RAM了,而且大家应该也注意到了,读取c的时候需要先读前两个地址内容,在读后两个地址内容,然后再拼接到一起才可以,这样很影响读取效率。那么怎么存比较方便读取呢?计算机会选取结构体中所占空间最大的类型作为参考标准,如果这个类型所占字节数小于8,会以这个类所占字节数为标准大小(本例中为4),对其他所占字节小于该类型的其他类型进行“扩容”,令这些类型所占空间为标准大小,也就是这样存储:
结构体与共用体基础_第3张图片
这样一来,虽然CPU仍需读取两次才能得到完整的结构体内容,但不再需要将得到的地址重新拼接整理,也就达到了节约时间的目的。当然,这个“扩容”并不是将short类型所占的字节强行扩大为4,而是在short内容后补充两个空内容字节进行占位。如果结构体存储内容中占用空间最大的类型大于8,那么标准大小则默认为8。这就是鼎鼎大名的内存对齐,典型的用空间换时间的方法。
讲到这里,细心的小伙伴可以观察一下C++中是否存在占用空间小于8且不是8的整数因数的类型。

以上图为例,含有两个int和一个short类型的结构体所占空间应该为12字节,我们用代码验证一下:

struct test
{
    int a;
    short b;
    int c;
};
int main()
{
    cout<<sizeof(test)<<endl;
}
// 输出为:12

看起来我们的猜想是正确的。那么回到刚才的问题:

struct Student
{
    int age;
    string name;
    char sex;
};

这样定义的结构体所占空间是多少呢?由于string所占空间为32,因此内存对齐的标准大小为8,int类元素后会补4个字节,char类后会补7个字节。因此这个Student类型所占空间为8+32+8=48。如果用长度为32的字符数组声明name,尽管字符数组所占的空间也为32,但是内存对齐并不会考虑数组,只会对比各个类型所占的空间大小,因此此时的Student结构体内存对齐的标准大小仍为4,也就是

struct Student
{
    int age;
    char name[32];
    char sex;
};

这个Student结构体所占空间为40(4+32+4)的原因了。
当然,C++也为我们设计了自行修改内存对齐方式的方法:

#pragma pack(1) // 设置此后结构体内容内存对齐的标准大小为1
struct Student
{
    int age;
    char name[32];
    char sex;
};
#pragma pack() // 此后恢复默认内存对齐方式

这样设置的Student结构体内容就会“紧密”排列,它的大小就是37字节了。实操中,我们可以用这种方法改变结构体内存对齐方式,但最好能平衡内存消耗和时间消耗。

5.共用体

共用体在C++中应用不多,他与结构体在写法上有些类似,我们用union定义一个共用体如:

union test
{
	int a;
	double b;
	char c;
};

声明和使用方式与结构体完全一样,这里不再赘述。与结构体不同的是,共用体所占空间取决于共用体中所占空间最大的类型,上例中double所占空间最大,因此test共用体所占空间为8。另外,如果共用体中有数组,如将上例中的char c换成char c[32],该共用体所占空间就变为32这一点也是与结构体明显不同。
我们看以下例子:

union Student
{
    int age;
    char name[32]; // 共用体不能使用string类
    char sex;
};
int main()
{
    Student ZhangSan;
    ZhangSan.age=10;
    strcpy(ZhangSan.name,"ZhangSan");
    ZhangSan.sex='m';
    cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;
    strcpy(ZhangSan.name,"ZhangSan");
    ZhangSan.sex='m';
    ZhangSan.age=10;
    cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;
    ZhangSan.sex='m';
    ZhangSan.age=10;
    strcpy(ZhangSan.name,"ZhangSan");
    cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;
}
// 输出为:1851877485 mhangSan m
//        10
//
//
//        1851877466 ZhangSan Z

可以看到,只有最后赋值的变量才可以正确的存入内容。
共用体的特点,也是与结构体最大的区别总结如下:
1.在每一瞬间只能存有一个正确的内容,不可以同时存放几种不同类型的数据
2.在为共用体的另一类型变量赋值时,之前赋值的变量将失去作用
3.公用体所有成员的地址和公用体本身的地址都是相同的
4.不可以在定义共用体时对其进行初始化,不能使用共用体变量名作为函数的参数。
共用体的应用不多,所以我们不需要太深的理解。以上区别如果不能理解,记住也是没有问题的。

6.总结

本节我们讨论了结构体和公用体的基本用法,以及结构体的内存对其原则,下一节开始我们将讨论结构体的更高级用法。

你可能感兴趣的:(c++,c++)