2023 1019 广州akw,笔试,SPC软件工程师

1. 共用体

共用体的出现主要是为了解决以下两个主要痛点:

  1. 内存节省:在一些编程场景中,需要存储多种不同数据类型的数据,但不一定需要同时存储所有这些数据。如果使用独立的变量或结构体来表示这些数据,会浪费内存空间,特别是当内存受限时,这会成为一个问题。共用体允许在相同的内存位置存储不同数据类型的数据,从而减小内存开销。

  2. 数据类型转换:有时候需要将数据从一种类型转换为另一种类型。使用共用体,可以将数据存储在一个共同的内存位置中,并在需要时切换不同的成员以实现数据类型转换,而无需额外的内存复制或分配。这可以提高程序的性能和效率。

共用体的特性,

    union one4all
    {
        int int_val;
        long long_val;
        double double_val;
    };

    one4all pail;
    pail.int_val = 15;
    pail.doubl_val = 1.38;

因此,pail有时可以是int变量,而有时又可以是double变量。成员名称标识了变量的容量。由于共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度。

共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。

    struct StudentInfo4
    {
        std::string name;
        int age;
        union id
        {
            long id_num;
            char id_char[20];
        } id_val;
    };

    struct StudentInfo5
    {
        std::string name;
        int age;
        union // 匿名共用体
        {
            long id_num;
            char id_char[20];
        };
    };

    int main()
    {
        //    StudentInfo4 info {"shenjun", 40};
        //    strcpy(info.id_val.id_char, "id_val");
        //    cout << info.id_val.id_char << endl;
        //    cout << info.id_val.id_num << endl;

        StudentInfo5 info {"shenjun", 40};
        strcpy(info.id_char, "id_char!");
        // 由于共用体是匿名的,因此id_num和id_char被视为info的两个成员,他们的地址相同,所以不需要中间标识符id_val。程序员负责确定哪个成员是活动的。
        cout << info.id_char << endl;

        return 0;
    }

3.位域(大小端,小端数理逻辑,低位开始分配,高位的数存在后面)

字节内也是有大小端问题,与字节中的大小端类似:

1)little endian中的位应该这样排列: 
01234567 
即排在前面的是低位。因此,先分配least significant bits 
2)而在Big endian中,位应该这样排列: 
76543210 
即排在前面的是高位。因此,先分配most significant bits。

在对struct中的成员进行分配的时候,"按排列顺序分配,先分配排在前面的" 1)big endian从高位向低位分配, 
a. 对字节,是先分配低地址的字节,再分配高地址的字节。 
b. 对位域,先分配most significant bits,再分配least significant bits。 
1)little endian从低位向高位分配, 
a. 对字节,是先分配低地址的字节,再分配高地址的字节。 
b. 对位域,先分配least significant bits,再分配most significant bits。 


对于定义如下的结构体:

 

struct yaabou_com{
int a:1;
int b:2;
int c:3;
int d:4;
int e:5;
int f:6;
int g:11;
};
大端:
地址:[00000000][00000001][00000002][00000003]
数据:|abbcccdd||ddeeeeef||fffffggg||gggggggg|
小端:
地址:[00000000][00000001][00000002][00000003]
数据:|ddcccbba||feeeeedd||gggfffff||gggggggg|

所以如果大端定义了上面的结构体,小端要定义相反的结构体:
struct yaabou_com{
int g:11;
int f:6;
int e:5;
int d:4;
int c:3;
int b:2;
int a:1;
};


所以,一定要做网络字节序转换!
 

结构体中位域的定义:

在使用中为了兼容大小端,结构体的定义总是区分了大小端情况:

结构体A描述了在一个字节(byte)内,位域大小端的定义方式——小端将字节内的定义顺序翻转即可;结构体B描述了在一个字(word)内位域的定义方式——小端将一个字内的定义顺序全部翻转,在使用前需要先调用ntohl宏进行转换

struct A{

#ifdef BIG_ENDIAN

ucharq:4;

uchark:4;

#else

uchark:4;

ucharq:4;

#endif

}

struct B{

#ifdef BIG_ENDIAN

int a:1;

int b:2;

int c:3;

int d:4;

int e:5;

int f:6;

int g:11;

#else

        int g:11;

int f:6;

int e:5;

int d:4;

int c:3;

int b:2;

int a:1;

#endif

};
http://blog.csdn.net/qk835320459/article/details/8988175

计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。----百度百科

   由于不同的计算机系统可能存在不同的大小端模式,所以不同的体系系统间通信就需要进行大小端转换。任何在不同系统间的通信信息都经过网络字节(大端)序进行传输,也就是说不管本机是什么模式,都要保证发送端传输的数据转换为网络序,接受端都要把网络序的数据转换为本地序。

    16bit和32bit的大小端转换很常见,一般也不会存在什么问题。但如果定义的数据结构中包含bit位域,该如何转换呢?

   1)低字节都存放在低地址

   2)大端模式首先为字段的高bit位分配空间,小端模式首先为字段的低bit位分配空间

   3)大端模式首先存放在地址的高bit位,小端模式首先存放在地址的低bit位

    一个例子

    struct

    {

        short bit1:4

        short bit2:9

        short bit3:3

    };

   大端模式下在内存中存放的形式如下:

 

            Bit1

 

        Bit2(h4) 

 

           Bit2(l5)

 

          Bit3

   7                                             0 7                                                                                 0

                                                                                      图1

   小端模式下在内存中存放的形式如下:

 

        Bit2(l4)

 

        Bit1 

 

          Bit3

 

             Bit2(h5)

    7                                                0 7                                                      0

                                                                                      图2

    如果我们在小端机器上,数据流按照图2的格式发送到目标端是大端的机器上,明显不能直接通过图1的结构来解码。

    如果为大小端分别定义两套结构呢?定义如下:

    struct

    {

       #ifdef __LITTLE_ENDIAN__

        short bit1:4

        short bit2:9

        short bit3:3

       #else

        short bit3:3

        short bit2:9

        short bit1:4

        #endif

    };

   在大端的机器上我们按照下面的格式进行解析:

 

        Bit3

 

       Bit2(h5)

 

         Bit2(l4)

 

                  Bit1

   7                                               0 7                                                       0

                                                                                  图3

   可是解码的数据还是不对,但观察一下不难发现,如果我们把小端的的数据(图2)前后两个字节颠倒,就和大端机器上的结构(图3)完全一致了。

   综上所述,bit位域的大小端转换如下:

    1: 在机器上定义大小端两套数据结构分别针对大小端

   2:传输的bit域数据需要进行本机序->网络序->本机序的转换过程(bit域数据可以映射为对应长度的short或int类型进行转换)

    struct

    {

        short bitData;

    };

4.单个整型如何进行[6:3]位的改变。

#include 

int main() {
    unsigned int num = 0; // 原整数
    int newValue = 6;    // 新值,位范围[6:3]

    // 将newValue限制在4位以内,即[6:3],以确保不溢出
    newValue &= 0xF; // 0xF 表示二进制 1111,用于限制最大值

    // 将newValue左移到正确的位置,[6:3],然后按位与运算
    num &= ~(0xF << 3);   // 先清除[6:3]的位
    num |= (newValue << 3); // 然后将新值写入[6:3]的位

    printf("整数值: %u\n", num);

    return 0;
}

位域在C语言中具有多种用途,主要用于位级别的操作和内存布局的控制。以下是一些常见的位域用途:

  1. 位级别的数据存储和操作: 位域通常用于存储和操作二进制数据,如位图、标志、掩码和位控制器。它们允许你以比整数类型更细粒度的方式处理数据。

  2. 硬件寄存器控制: 在嵌入式系统和底层编程中,位域通常用于表示和控制硬件寄存器中的各个位。硬件寄存器通常具有明确定义的位域,因此位域非常适合与硬件交互。

  3. 位域压缩数据: 位域可用于压缩存储数据,减小内存消耗。例如,可以使用位域来表示布尔标志或小整数范围,以便在内存中节省空间。

  4. 位域的位运算: 位域可以与位运算操作结合使用,如位与(&)、位或(|)、位异或(^)等,用于执行各种位级别的操作,如位设置、位清除、位翻转等。

  5. 网络协议处理: 位域可用于解析和构建网络协议数据包的头部信息,以便提取或设置各个字段的值。

  6. 跨平台数据序列化: 在跨不同硬件平台的通信或数据序列化中,位域可用于确保数据字段的字节顺序和位宽度一致。

  7. 节省内存: 位域可以减小数据结构的内存占用,尤其是当你需要表示大量布尔标志或小范围整数时。

  8. 位域的位范围操作: 位域允许你以更具可读性的方式操作特定位范围的数据,而不需要手动执行位操作。

volatile 关键字主要解决了以下两个主要问题(解决幸福的烦恼):

  1. 防止编译器优化:编译器在编译代码时会尝试进行各种优化,以提高程序的性能。这种优化可能包括删除对某些变量的读取或写入操作,因为它认为这些操作在程序的语义上没有意义。然而,某些变量的值可能会在程序的控制之外被更改,如硬件寄存器、中断处理等。在这种情况下,使用 volatile 告诉编译器不要对这些变量进行优化,确保对它们的读取和写入都会被保留。

  2. 强制实时访问:在某些嵌入式系统或实时系统中,有时需要确保某些变量的读写是实时的,而不受编译器的优化干扰。使用 volatile 可以确保在程序的不同部分之间对这些变量的访问不会被重新排序或合并,从而确保及时响应硬件事件和确保实时性。

总的来说,volatile 用于解决与编译器优化相关的问题和确保变量在特定情况下的实时性,主要适用于以下情况:

  • 与硬件寄存器或外部设备交互,以确保变量在程序控制之外被更改。
  • 处理中断服务程序,以确保对共享变量的读写在中断上下文中是可见的。
  • 在多线程编程中,用于共享变量的同步,尤其在多线程环境下确保数据的可见性。

需要注意的是,虽然 volatile 在特定情况下非常有用,但它并不是解决所有并发问题的通用工具,因此在多线程编程中,更常见的做法是使用更强大的同步机制,如互斥锁、信号量和原子操作。

volatile 主要适用于以下情况:

  1. 外部硬件寄存器:当你与外部硬件设备(例如传感器、芯片、设备控制器等)交互时,这些设备可能随时更改寄存器的值。在这种情况下,你可以使用 volatile 来声明寄存器变量,以确保编译器不会优化对这些寄存器的读取和写入,以便保持与外部设备的实时通信。
volatile uint8_t *hardware_register = (volatile uint8_t *)0x12345678;
  1. 中断处理程序:在嵌入式系统中,中断处理程序通常会访问共享的变量,这些变量可能会在主程序和中断服务程序之间更改。使用 volatile 可确保这些变量的读写在中断上下文中是可见的,以维护正确的共享状态。
volatile int shared_variable;
  1. 并发编程:在多线程编程中,volatile 可以用于确保在多个线程之间对共享变量的读写是可见的。虽然 volatile 不提供线程同步,但它可以确保变量的值在不同线程之间是可见的,尤其在一些特殊情况下可能有用。
volatile int shared_data;

需要注意的是,volatile 并不提供原子性,不能解决所有的并发问题。在多线程编程中,通常更好的做法是使用互斥锁、信号量、条件变量等更强大的同步机制来确保线程之间的数据同步和正确的并发控制。 volatile 主要用于在特定情况下防止编译器的优化,以及确保实时访问和可见性。

你可能感兴趣的:(面试题目,算法)