C语言面试高频(二)

1.结构体和共用体的区别

1.定义

结构体:

  • 成员在内存中独立存储,每个成员占用独立的内存空间。
  • 内存占用是成员之和,每个成员都占用独立的空间。
  • 成员可以同时被访问,通过成员名字来访问。
  • 适合存储和处理多个不同类型的数据,如员工信息、图形对象等。

共用体:

  • 成员共享同一块内存空间,只能存储一个成员的值。
  • 内存占用是最大成员的大小,所有成员共享该空间。
  • 成员只能同时访问其中的一个,存取时要明确指定。
  • 适合存储和处理只使用其中一种类型的数据,可以节省内存空间或进行数据类型转换。

2.举例(便于理解)

#include <stdio.h>
struct MyStruct {
  int x;
  float y;
};
union MyUnion {
  int a;
  float b;
};
int main() {
  struct MyStruct myStruct;
  union MyUnion myUnion;
  printf("结构体的大小: %zu 字节\n", sizeof(myStruct));
  printf("共用体的大小: %zu 字节\n", sizeof(myUnion));
  
  return 0;
}

结构体的大小: 8 字节
共用体的大小: 4 字节

2.简述C++有几种传值方式之间的区别⭐

值传递(pass by value):参数以值的方式传递给函数,函数内部对参数的修改不会影响到原始数据。函数会创建参数的副本,在函数的作用域内使用副本进行操作。常用于传递简单类型的参数。

#include <iostream>
void modifyValue(int x) {
  x = 10;
}
int main() {
  int num = 5;
  modifyValue(num);
  std::cout << "原始值: " << num << std::endl;
  return 0;
}
 输出结果:原始值: 5

引用传递(pass by reference):参数以引用的方式传递给函数,函数内部对参数的修改会影响到原始数据。函数直接操作原始数据,没有创建副本。可以用于传递任意复杂类型的参数,比如对象或容器。

#include <iostream>
void modifyValue(int& x) {
  x = 10;
}
int main() {
  int num = 5;
  modifyValue(num);
  std::cout << "修改后的值: " << num << std::endl;
  return 0;
}
输出:修改后的值: 10

指针传递(pass by pointer):参数以指针的方式传递给函数,函数内部可以通过指针来访问和修改原始数据。函数通过指针间接操作原始数据,需要注意空指针和指针的解引用。可以用于传递需要动态分配内存的参数,或者需要返回多个值的情况。

#include <iostream>
void modifyValue(int* x) {
  *x = 10;
}
int main() {
  int num = 5;
  modifyValue(&num);
  std::cout << "修改后的值: " << num << std::endl;
  return 0;
}
输出:修改后的值: 10

3.数组指针与指针数组的区别⭐

数组指针(pointer to an array):数组指针是指向数组的指针变量。数组指针的类型声明中,指针符号 * 出现在数组名之前,用于表示数组指针的类型。数组指针可以指向整个数组,而不仅仅是数组的第一个元素。

#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int (*ptr)[5];  // 声明一个指向包含5个整数的数组的指针
  ptr = &arr;  // 数组指针指向数组
  printf("数组元素通过指针访问: ");
  for (int i = 0; i < 5; i++) {
    printf("%d ", (*ptr)[i]); // 使用指针访问数组元素需要解引用
  }
  return 0;
}
数组元素通过指针访问: 1 2 3 4 5

指针数组(array of pointers):指针数组是一个数组,数组的元素都是指针。指针数组的类型声明中,指针符号 * 出现在数组名的后面,用于表示指针数组的类型。指针数组的每个元素指向一个独立的内存块,可以指向不同的变量或数据。

#include <stdio.h>
int main() {
  int num1 = 1, num2 = 2, num3 = 3, num4 = 4, num5 = 5;
  int* arr[5];  // 声明一个包含5个整型指针的指针数组
  arr[0] = &num1;
  arr[1] = &num2;
  arr[2] = &num3;
  arr[3] = &num4;
  arr[4] = &num5;
  printf("指针数组元素的值: ");
  for (int i = 0; i < 5; i++) {
    printf("%d ", *arr[i]); // 解引用指针数组的元素以获取其值
  }
  return 0;
}
指针数组元素的值: 1 2 3 4 5

4.指针函数与函数指针的区别⭐

指针函数(function returning a pointer):指针函数是一种返回指针类型的函数。指针函数的返回类型是一个指针,指向特定类型的数据。调用指针函数时,会返回一个指向函数计算结果的指针。

#include <stdio.h>
int* addIntegers(int a, int b) {
  int* result = (int*)malloc(sizeof(int));
  *result = a + b;
  return result;
}

int main() {
  int* sum = addIntegers(5, 3);
  printf("和: %d\n", *sum);
  free(sum);
  return 0;
}
输出:和: 8

函数指针(pointer to a function):函数指针是指向函数的指针变量。函数指针的类型声明中,指针符号 * 出现在函数名之前,用于表示函数指针的类型。函数指针可以用于直接调用指向的函数,或者作为参数传递给其他函数。

#include <stdio.h>
int addIntegers(int a, int b) {
  return a + b;
}
int main() {
  int (*ptr)(int, int);  // 声明一个指向以两个整数为参数并返回整数的函数的指针变量
  ptr = addIntegers;  // 将函数地址赋值给函数指针
  int sum = ptr(5, 3);  // 通过函数指针调用函数
  printf("和: %d\n", sum);
  return 0;
}
输出:和: 8

5.原码、反码、补码的定义

  • 原码:最高位表示符号,其余位表示数值的绝对值。正数的原码就是二进制表示,符号位为0。负数的原码符号位为1,数值位根据绝对值的二进制表示。
  • 反码:正数的反码与原码相同,负数的反码是对原码除符号位外的每一位取反(0 变为 1,1 变为 0)。
  • 补码:正数的补码与原码和反码相同,负数的补码是对反码加 1。

在这里插入图片描述

6.内存分布模型⭐

在这里插入图片描述
上图是比较经典的内存分布的模型图,下面将对上图中的不同的组成部分进行详细解释(从低地址到高地址)注:必须知道组成结构但是具体的含义只需要理解。

1. 代码段:存放程序的机器指令(即二进制代码)。通常是只读的,因为程序的指令在执行过程中不应该被修改。
2. 数据段:存放已初始化的全局变量和静态变量。这些变量在程序开始运行时已经赋予了初始值。
3. BSS段:存放未初始化的全局变量和静态变量。它们在程序开始运行时会自动初始化为0或者空指针。
4. 堆区:动态分配的内存空间,用于存放程序运行时动态申请的内存。(程序员可以通过函数(如malloc、calloc等)或者操作系统提供的接口来申请和释放堆内存,堆从低地址向高地址增长。)
5. 栈区:存放函数的局部变量、函数参数值以及函数调用和返回时的相关信息。栈区是按照“先进后出”的原则进行管理,内存的分配和释放是自动进行的,栈从高地址向低地址增长。是一块连续的空间。
6. 共享区:也称为文件映射或共享内存,用于实现不同进程之间的内存共享。

7.malloc和calloc的区别

malloc

  • malloc 分配的内存是未初始化的,其中的字节内容是不确定的(可能是随机值)。
  • 如果内存分配失败,malloc 返回一个空指针NULL,可以通过检查返回值来判断是否分配成功。

calloc

  • calloc 分配的内存会被初始化为全0。
  • calloc 在分配失败时会自动抛出错误(异常),可以使用异常处理机制来捕获和处理错误。

8.malloc的底层原理⭐

1.结论:

  1. 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

2.具体实现

  1. 当调用 malloc(size)时,它首先计算需要分配的内存块大小,包括用户请求的大小以及内存管理所需的额外空间(例如内存块的管理信息)。
  2. malloc会遍历一个数据结构(例如空闲链表或空闲块列表),查找合适大小的空闲内存块。
  3. 如果找到了合适的内存块,malloc会将其标记为已分配,并返回一个指向该内存块的指针给用户。
  4. 如果没有足够大的空闲内存块可用,malloc可能需要扩展程序的虚拟内存空间。它通过系统调用(例如 brk 或 mmap)向操作系统请求更多的连续内存空间。
  5. 当操作系统提供了更多的内存空间后,malloc 可以从新的空间中分配出合适大小的内存块,并将其标记为已分配。
  6. 在内存块被释放时,通过调用free 函数,malloc 将其标记为未分配,并将该内存块添加到空闲内存块的列表中,以便后续的内存分配可以重复使用它们。

3.简易代码实现:

#include <unistd.h>   // 包含系统调用相关的头文件

typedef struct Block {
    size_t size;       // 内存块的大小
    struct Block* next; // 指向下一个内存块的指针
} Block;

Block* freeList = NULL;   // 空闲链表的头指针

void* malloc(size_t size) {
    // 检查参数是否合法
    if (size <= 0) {
        return NULL;
    }
    
    // 计算需要分配的内存大小
    size_t blockSize = sizeof(Block) + size;
    
    // 在空闲链表中查找符合要求的内存块
    Block* prevBlock = NULL;
    Block* currBlock = freeList;
    while (currBlock != NULL) {
        if (currBlock->size >= blockSize) {
            // 找到合适大小的空闲块
            if (prevBlock != NULL) {
                // 删除这个空闲块
                prevBlock->next = currBlock->next;
            } else {
                // 这个空闲块是链表的头节点
                freeList = currBlock->next;
            }
            
            // 返回指向内存块的指针
            return (void*)(currBlock + 1);
        }
        prevBlock = currBlock;
        currBlock = currBlock->next;
    }
    
    // 没有找到可用的内存块,请求更多内存空间
    Block* newBlock = sbrk(blockSize);
    if (newBlock == (void*)-1) {
        return NULL;   // 请求失败,返回 NULL
    }
    
    // 返回指向新内存块的指针
    return (void*)(newBlock + 1);
}

void free(void* ptr) {
    // 检查参数是否合法
    if (ptr == NULL) {
        return;
    }
    
    // 获取指向内存块起始位置的指针
    Block* block = ((Block*)ptr) - 1;
    
    // 将内存块标记为未分配状态,然后将其添加到空闲链表中
    block->next = freeList;
    freeList = block;
}

9.在1G内存的计算机中能否malloc(1.2G)?为什么?⭐

是有可能申请1.2G的内存的。

malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。根据 malloc 函数的作用和原理,应用程序通过 malloc 函数在虚拟地址空间中申请内存,并且与物理内存没有直接的关系。 malloc 返回的是在虚拟地址空间中的地址,而物理内存的分配是由操作系统完成的。

假设我们需要申请的内存大小为 1.2GB,转换为字节为 2^30 × 1.2 Byte,这个数值仍然在 unsigned int 的表示范围内。因为 malloc 函数需要一个 unsigned int 类型的参数来指定内存大小。

在当前使用的 Windows 环境中,可申请的最大内存空间通常超过 1.9GB。然而,具体可申请的内存大小受到操作系统版本、程序本身的大小、动态/共享库的使用情况、程序栈的大小等因素的影响。每次运行的结果可能存在差异,因为有些操作系统使用随机地址分布技术,导致进程的堆空间变小。

综上所述,根据 malloc(size_t) 函数的调用,是有可能申请超过机器物理内存大小的内存块的。

10.如果有一个地址是0X3355,如何在这个地址赋值成79⭐

在 C 语言中,要在地址为 0x3355 的内存位置赋值为 79,可以使用指针来实现。以下是一个示例代码:

unsigned char* p = (unsigned char*)0x3355;  // 将地址转换为无符号字符型指针类型
*p = 79;  // 赋值操作

在这个例子中,我们将地址 0x3355 转换为无符号字符型指针,通过指针将值 79 存储到该地址对应的内存位置上。请注意,我们使用了无符号字符型指针 (unsigned char*),因为这是一个字节大小的数据类型。这里我们需要知道这种赋值方式,但是在平时的使用上我们要谨慎使用。

11.简述大端和小端⭐

这两种字节序的选择取决于计算机系统架构的约定。大部分个人电脑和服务器采用小端字节序,例如 x86 架构的计算机。而一些嵌入式系统和网络协议则常使用大端字节序。

  1. 大端字节序:将多字节数据的高位字节存储在低地址位置,低位字节存储在高地址位置。类比:高位字节在前,低位字节在后。举例:整数值
    0x12345678 在大端字节序中存储为 12 34 56 78,即高位字节 12 存储在低地址位置,低位字节 78存储在高地址位置。
  2. 小端字节序:将多字节数据的低位字节存储在低地址位置,高位字节存储在高地址位置。类比:低位字节在前,高位字节在后。举例:整数值 0x12345678 在小端字节序中存储为 78 56 34 12,即低位字节 78 存储在低地址位置,高位字节 12存储在高地址位置。

12.重写memcpy()函数以及需要注意哪些问题⭐

数组在内存中存放的位置取决于数组的类型以及其定义的位置。在大多数情况下,数组被存储在栈内存或堆内存中。

  1. 栈内存:局部变量和函数参数通常存储在栈内存中。当您在一个函数内部定义数组时,它会分配一块连续的内存区域,用于存储数组元素。这种分配的内存空间在函数执行完毕后会自动回收。
  2. 堆内存:当使用 malloc()、calloc() 或 new等动态内存分配函数分配数组时,它会存储在堆内存中。堆内存需要手动分配和释放,可以在函数执行期间或跨函数访问。

13.指针和引用的区别以及转换⭐

引用指针的定义

指针(Pointer):

  • 定义:指针是一种变量,存储了一个地址,该地址指向内存中的另一个变量。
  • 特点:可以修改指针的指向,使其指向其他变量或空地址。可以进行指针运算,如指针加法和减法。可以通过解引用(Dereference)操作符来访问指针所指向的变量。

引用(Reference):

  • 定义:引用是变量的别名,它引用了同一块内存空间。
  • 特点:引用一旦绑定到一个变量,便无法更改其引用的目标。操作引用和操作原变量是等价的,对引用的修改会反映在原变量上。引用不能指向空地址。

引用和指针的区别:

  1. 指针是一个实体,而引用仅是个别名

  2. 指针和引用的自增(++)运算意义不一样,指针是对内存地址的自增,引用是对值的自增;量或对象的地址)的大小;

  3. 引用使用时无需解引用(*),指针需要解引用;

  4. 引用只能在定义时被初始化一次,之后不可变;指针可变;

  5. 引用不能为空,指针可以为空;

  6. 引用没有const,指针有const;(本人当初看到这句话表示疑问,这里解释一下:指针有“指针常量”即int * const a,但是引用没有int& const a,不过引用有“常引用”即const int &a = 1)

  7. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32位系统指针变量一般占用4字节内存。

转换代码示例:

指针转引用:

int num = 10;
int* ptr = #     // 声明指针并指向变量
int& ref = *ptr; // 将指针转换为引用

// 使用引用
ref = 20;
cout << num; // 输出:20

引用转指针:

int num = 30;
int& ref = num;    // 声明引用
int* ptr = &ref;   // 将引用转换为指针

// 使用指针
*ptr = 40;
cout << num; // 输出:40

14.函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解

-函数参数压栈顺序是指在函数调用过程中,函数的参数是如何在栈上被压入内存的顺序。

在C/C++中,有两种常见的调用方式:__stdcall和__cdecl。下面是对它们的简要说明:

  1. __stdcall调用方式:在__stdcall调用方式下,函数的参数是从右往左依次压入栈中。被调函数负责清理栈上的参数。__stdcall调用方式通常被用于Win32 API,因为它具有固定的参数顺序,方便调用方和被调用方之间的交互。
  2. __cdecl调用方式:在__cdecl调用方式下,函数的参数是从右往左依次压入栈中。调用方负责清理栈上的参数。__cdecl调用方式是C/C++默认的函数调用方式。

使用__stdcall调用方式的主要原因是为了确保与其他代码库或操作系统的交互的正确性和一致性,两种不同的调用方式的区别就是为什么应对不同库的一致性和兼容性。

15.简述内存泄漏,如何检测和避免?

内存泄漏是指在程序运行过程中,分配的内存没有被及时释放,导致这部分内存无法再被程序使用。长时间运行或发生频繁的内存泄漏可能导致系统资源不足,性能下降或程序崩溃。

常见内存泄露情况:

  1. new和malloc申请资源使用后,没有用delete和free释放;
  2. 子类继承父类时,父类析构函数不是虚函数。
  3. 比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
  4. shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。

检测内存泄漏:

  • 使用内存分析工具:使用工具如Valgrind(Linux),Dr.
    Memory(Windows),Instruments(macOS)等来检测内存泄漏。这些工具可以检查程序运行时的内存分配和释放情况,并报告潜在的泄漏位置。

避免内存泄漏:

  • 使用智能指针:C++中的智能指针(如std::shared_ptr、std::unique_ptr)可以自动管理内存释放,避免手动释放内存的疏忽。
  • 务必正确配对分配和释放:确保每次内存分配都有相应的释放,释放内存的操作必须与分配内存的操作对应(malloc和free一定要malloc和free的次数一致否则就泄露)。
  • 减少全局变量和长时间存活的对象:全局变量和长时间存活的对象可能导致无法释放的内存,尽量避免过多使用。
  • 使用容器类的自动销毁机制:使用容器类如std::vector、std::map等,它们在销毁时会自动释放内部元素的内存。
  • 注意循环引用:避免出现对象之间的循环引用,可以使用弱引用或断开引用关系的方式解决。
  • 规范化资源管理:对于文件、网络连接等资源,确保在使用后及时释放,避免产生不必要的资源泄漏
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值