C/C++八股面试题(八)
目录:
1.请说明struct和class并且说明他们有什么区别?
2.什么是数组名,数组名和指针有什么区别?
3.简单介绍malloc如何使用?
4.malloc底层实现的流程?
5.内存分配的方式有哪些?
内容:
1.请说明struct和class并且说明他们有什么区别?
结构体struct
定义结构体
#include <iostream> using namespace std; struct Point { int x; // 成员变量 x int y; // 成员变量 y void print() { // 成员函数,打印结构体成员 cout << "x: " << x << ", y: " << y << endl; } };
创建结构体变量
int main() { // 创建结构体对象 Point p1; p1.x = 10; // 访问成员并赋值 p1.y = 20; // 调用成员函数 p1.print(); // 输出:x: 10, y: 20 return 0; }
Point p1;
创建了一个Point
类型的变量p1
。- 通过
p1.x
和p1.y
访问并设置Point
类型结构体的成员。 p1.print();
调用print()
成员函数,输出结构体的值。
结构体的初始化
struct Point { int x; int y; }; int main() { // 直接初始化 Point p1 = {10, 20}; // x = 10, y = 20 // 使用成员赋值 Point p2; p2.x = 30; p2.y = 40; cout << "p1.x: " << p1.x << ", p1.y: " << p1.y << endl; cout << "p2.x: " << p2.x << ", p2.y: " << p2.y << endl; return 0; }
结构体的访问权限
- 结构体(
struct
) 的成员默认是 公有的 (public
),可以在类外部直接访问。 - 类(
class
) 的成员默认是 私有的 (private
),只能通过公有成员函数访问。
struct Person { string name; // 默认 public int age; // 默认 public }; int main() { Person p1; p1.name = "John"; // 直接访问 public 成员 p1.age = 30; cout << "Name: " << p1.name << ", Age: " << p1.age << endl; return 0; }
C++中的类Class
类的定义
public
、private
、protected
),默认访问权限为 private
。#include <iostream> using namespace std; class Rectangle { private: double length; // 私有成员变量 double width; // 私有成员变量 public: // 公有成员函数 void setLength(double l) { length = l; } void setWidth(double w) { width = w; } double getArea() { return length * width; } void print() { cout << "Length: " << length << ", Width: " << width << ", Area: " << getArea() << endl; } }; int main() { Rectangle rect; rect.setLength(5.0); // 设置长 rect.setWidth(3.0); // 设置宽 rect.print(); // 打印矩形的属性 return 0; }
类的组成
- 成员变量(属性):定义类的状态,通常是数据成员。它们存储对象的相关信息。
- 成员函数(方法):定义类的行为,操作数据成员或执行某些任务。
- 构造函数:用于初始化类的对象,通常在创建对象时自动调用。
- 析构函数:用于清理类的资源,通常在对象销毁时自动调用。
- 访问控制:控制类成员的访问权限(
public
、private
、protected
)。
成员访问控制:
public
:公有成员,可以在类外部直接访问。private
:私有成员,只能在类的成员函数内部访问,外部无法直接访问。protected
:保护成员,只有类及其派生类的成员函数可以访问。- 默认情况下,类的成员的访问权限是private。
构造函数和析构函数
class Student { private: std::string name; public: Student(const std::string &n); ~Student(); };
成员函数
this
指针在成员函数中访问当前对象。class Circle { private: double radius; public: Circle(double r); double calculateArea(); }; double Circle::calculateArea() { return 3.14 * radius * radius; }
结构体Struct和类Class之间的区别
默认访问权限
- struct:结构体的成员默认访问权限是公共的(public),这意味着结构体的成员在外部可以直接访问。
- class: 默认情况下,class中的成员(包括变量和函数)是 私有的(private),即它们只能在类的成员函数内部访问,外部无法直接访问。
继承
- struct: 当一个 struct 继承自另一个 struct或 class 时,默认的继承方式是 公有继承(public)。这意味着基类的公有成员和保护成员会变成派生类的公有和保护成员。
- class: 当一个 class继承自另一个 struct 或 class时,默认的继承方式是 私有继承(private)。这意味着基类的公有和保护成员会变成派生类的私有成员,无法在外部访问。
例子:
struct Base { int a; // 默认 public }; struct DerivedStruct : Base { // 默认 public 继承 void printA() { std::cout << a << std::endl; } // 可以访问 Base 的 a }; class DerivedClass : Base { // 默认 private 继承 void printA() { std::cout << a << std::endl; } // 不能直接访问 Base 的 a };
DerivedStruct
从Base
继承时,使用的是 公有继承,可以直接访问基类的成员。DerivedClass
从Base
继承时,使用的是 私有继承,无法直接访问基类的成员。
结构体的使用场景:
- 用于存储一组相关的数据,但没有复杂的操作和逻辑。
- 当数据的封装比行为更重要时,例如在处理图形、坐标、日期等数据时。
- 当你需要将数据序列化/反序列化为二进制或其他格式时。
- 作为轻量级的数据容器,适用于性能要求较高的情况。
类的使用场景:
- 当你需要封装数据并附加操作和行为时,类更适合,因为它允许你将数据和操作封装在一起。
- 在面向对象编程中,用于建模现实世界的对象,例如人、车辆、银行账户等。
- 当你需要使用继承和多态来实现代码的扩展和重用。
- 为了实现更复杂的数据结构,如链表、树、图等。
2.什么是数组名,数组名和指针有什么区别?
- 数组名:是一个常量,表示数组的首元素的地址。数组名在大多数情况下会自动转换为指向数组首元素的指针,但它本身并不是一个指针变量。
- 指针:是一个变量,可以存储某个数据的地址,指针可以在程序运行时改变,指向不同的地址。
例子:
int arr[5] = {1, 2, 3, 4, 5}; // 定义一个整型数组 int* ptr = arr; // ptr 是一个指针,指向数组的首元素
arr
是数组名,它代表数组的地址,等同于&arr[0]
,但是它本身是不可修改的。ptr
是指针,它是一个可以修改的变量,指向arr
的首元素。
本质
- 数组名是常量指针:数组名
arr
是一个常量指针,指向数组的第一个元素。数组名本身不可修改,也不能指向数组之外的其他位置。 - 不可修改的指针:数组名作为指针常量,它指向数组的第一个元素,一旦数组定义后,数组名的值(即它的地址)不能改变。
arr = arr + 1; // 错误:不能修改数组名
指针是具有灵活性的
- 指针是可修改的:指针
ptr
可以指向任意内存地址(合法的内存区域)。你可以通过修改指针的值,使它指向数组中的任何位置,甚至指向数组之外的内存。
示例:
ptr = &arr[2]; // ptr 指向数组中的第三个元素
数组名与指针的相似之处
- 都表示内存地址:数组名和指针在表达数组地址时是相似的,通常数组名
arr
和指针ptr
都可以用于访问数组元素。
例子:使用数组名和指针访问元素
int arr[3] = {10, 20, 30}; int* ptr = arr; // 通过数组名访问数组元素 cout << arr[0] << endl; // 输出:10 // 通过指针访问数组元素 cout << *(ptr + 1) << endl; // 输出:20
arr[0]
访问数组的第一个元素。*(ptr + 1)
通过指针ptr
访问数组的第二个元素。
数组名与指针的不同之处
定义 | 数组名是一个常量,代表数组首元素的地址。 | 指针是一个变量,存储地址,指向任意内存。 |
可修改性 | 数组名不能修改,它总是指向数组的首元素。 | 指针是可修改的,可以指向不同的地址。 |
内存分配 | 数组的大小在编译时确定,内存是静态分配的。 | 指针可以动态分配内存,也可以指向动态或静态内存。 |
类型 | 数组名在表达式中通常会被当作指向首元素的指针。 | 指针变量有明确类型,可以指向任何数据类型。 |
大小 | 数组名的大小是固定的,通常是数组的大小。 | 指针的大小通常是固定的(如 4 或 8 字节),取决于系统架构。 |
可以做什么 | 不能做算术运算(如加法、减法),不能修改指向位置。 | 可以进行指针运算(如指针加法、指针减法),可以修改指向位置。 |
3.简单介绍malloc如何使用?
malloc
的定义与语法
void* malloc(size_t size);
- size:需要分配的内存大小,单位是字节(byte)。通常是整数类型的字节数。
- 返回值:成功时,malloc 返回一个指向分配的内存区域的指针,类型为 void*,即一个通用指针。失败时,返回 NULL。失败的原因通常是系统内存不足或无法满足分配请求。
malloc
的工作原理
malloc
函数向操作系统请求一块连续的内存区域,并返回该内存区域的起始地址。你可以通过这个地址来操作内存中的数据。与局部变量不同,使用 malloc
分配的内存是动态分配的,它的生命周期通常由开发者手动管理,直到通过 free
函数释放。基本用法
malloc
函数的典型用途是为动态分配的变量或数组分配内存。由于 malloc
返回 void*
类型的指针,因此通常需要将它转换为目标类型的指针。例子:动态分配单个变量
#include <iostream> #include <cstdlib> // malloc 的头文件 using namespace std; int main() { int* ptr = (int*)malloc(sizeof(int)); // 为一个 int 类型变量分配内存 if (ptr == nullptr) { cout << "Memory allocation failed!" << endl; return 1; } *ptr = 10; // 使用分配的内存 cout << "Value: " << *ptr << endl; // 输出:Value: 10 free(ptr); // 释放分配的内存 return 0; }
例子:动态分配数组
#include <iostream> #include <cstdlib> using namespace std; int main() { int* arr = (int*)malloc(5 * sizeof(int)); // 为一个包含 5 个 int 元素的数组分配内存 if (arr == nullptr) { cout << "Memory allocation failed!" << endl; return 1; } for (int i = 0; i < 5; ++i) { arr[i] = i * 2; // 给数组元素赋值 } for (int i = 0; i < 5; ++i) { cout << arr[i] << " "; // 输出:0 2 4 6 8 } cout << endl; free(arr); // 释放分配的内存 return 0; }
内存分配失败
malloc
无法成功分配所请求的内存时,它会返回 NULL
。在实际开发中,调用 malloc
后需要检查其返回值,确保内存分配成功。int* ptr = (int*)malloc(100 * sizeof(int)); // 为 100 个 int 类型的元素分配内存 if (ptr == nullptr) { // 处理内存分配失败的情况 cout << "Memory allocation failed!" << endl; return 1; }
释放内存:free
函数
malloc
动态分配的内存不会自动释放,必须手动释放。为此,C 和 C++ 提供了 free
函数,它用来释放通过 malloc
(或 calloc
, realloc
等)分配的内存。free(ptr); // 释放之前通过 malloc 分配的内存
ptr
指针并不会被自动置为 NULL
,因此需要特别注意。为了避免出现“悬空指针”问题,通常在 free
后将指针设置为 NULL
。使用malloc 的注意事项
- 避免内存泄漏:每次调用
malloc
后都需要确保调用free
来释放分配的内存。如果没有释放内存,就会发生内存泄漏。 - 初始化内存:
malloc
分配的内存不会被初始化。如果需要将内存初始化为某个值,可以使用calloc
(它会初始化内存)或手动初始化分配的内存。 - 错误处理:当
malloc
返回NULL
时,应当检查内存分配是否成功,并采取相应的错误处理措施。 - 使用
realloc
:如果需要调整已经分配的内存块的大小,可以使用realloc
函数。realloc
会尝试重新分配一块更大的内存,如果失败,它会返回NULL
,并且原始内存块保持不变。
4.malloc底层实现的流程?
内存分配
malloc
函数分配的内存通常是从操作系统的堆(heap)中获取的。堆是程序运行时用于动态分配内存的区域。堆与栈不同,栈内存的分配和释放是由编译器自动管理的,而堆内存的分配和释放则需要程序员显式管理。
分配策略
操作系统和 C 库通常使用以下几种策略来实现内存分配和管理:
- 空闲列表:维护一个记录空闲内存块的链表。每个内存块由一个头部结构(通常包括块的大小)和数据部分组成。每当一个内存块被释放,
malloc
会将其添加回空闲列表,以供后续分配。 - 分块管理:堆内存被分割成多个固定大小的块,每次
malloc
请求时,会根据请求的大小分配相应的块。如果请求的内存大小与现有块的大小不匹配,则可能会合并或拆分块。 - 页式管理:操作系统将虚拟内存分为固定大小的页面,通常为 4 KB 或更大。
malloc
会向操作系统请求一页内存或更多内存,并将这些内存页分配给应用程序。当不再需要时,内存会被释放回操作系统。 - 内存池:一些
malloc
实现会维护自己的内存池。这个内存池预先分配一大块内存,并在应用程序需要内存时从这个池中提供内存。这减少了与操作系统的交互,提高了内存分配效率。
malloc
的底层实现步骤
下面是一个简化的 malloc
底层实现步骤,展示了 malloc
如何分配内存的过程:
1.内存请求
当调用 malloc(size_t size)
时,malloc
会根据请求的大小确定应该分配多大的内存块。通常,malloc
会向操作系统请求一块足够大的内存,来满足该请求。
- 如果请求的内存块较小,
malloc
会检查现有的空闲内存块(空闲列表)是否有合适大小的块。 - 如果请求的内存块较大,
malloc
可能会直接向操作系统请求更大的内存块(例如通过sbrk
或mmap
等系统调用)。
2. 查找合适的内存块
当 malloc
向堆管理器请求内存时,它首先会检查是否有合适的空闲内存块可用。如果有,malloc
会从空闲链表中获取一个适当大小的块。
- 空闲块的合并和拆分:如果一个空闲块的大小大于请求的大小,
malloc
可能会将这个块拆分成两个部分。拆分后的较大块的一部分将分配给请求者,剩余的部分重新加入空闲列表。如果空闲块正好与请求大小匹配,则直接分配该块。 - 如果没有合适的空闲块,
malloc
会向操作系统请求更多内存来满足需求。
3. 向操作系统请求内存
当 malloc
没有找到合适的空闲块时,它会通过系统调用向操作系统请求内存。
- sbrk:早期的 Unix 系统使用
sbrk
系统调用来扩展进程的堆空间。sbrk
将堆的末尾向前扩展,返回扩展后内存的起始地址。 - mmap:现代系统可能使用
mmap
系统调用来请求大块内存区域。mmap
返回一块内存区域,可以直接用于分配和映射。
4. 分配和返回指针
malloc
返回一个指向已分配内存的指针,这个指针通常是内存块的起始地址。在返回之前,malloc
可能会设置一个块头,存储该块的大小和状态(例如,是否被分配)。
5. 内存块的释放
当内存块不再使用时,程序调用 free(ptr)
来释放该内存块。free
会将该内存块返回到空闲列表中。如果该内存块前后都有空闲块,free
可能会将它们合并成一个更大的块,防止内存碎片。
5.内存分配的方式有哪些?
静态内存分配
静态内存分配是指在程序编译时就决定好内存大小和分配位置的方式。静态内存的生命周期从程序开始执行到程序结束,期间不会改变。这种方式的内存分配通常用于存储程序的全局变量、静态变量和常量数据。
特点:
- 静态内存分配是在程序编译阶段完成的,使得内存分配在程序运行期间保持不变。
- 静态分配的内存从程序启动到程序结束都存在。
- 因为内存空间在编译时已经确定,分配和访问的速度较快。
- 内存大小是在编译时确定的,无法在运行时动态改变。
例子:
#include <stdio.h> int global_var = 10; // 全局变量 static int static_var = 20; // 静态变量 int main() { static int local_static = 30; // 局部静态变量 printf("Global: %d, Static: %d, Local Static: %d\n", global_var, static_var, local_static); return 0; }
global_var
是一个全局变量,在程序的整个生命周期内都存在。static_var
是一个静态变量,它在程序运行期间只初始化一次,且保持其值直到程序结束。local_static
是一个局部静态变量,在函数调用时不会销毁,且其值会保持不变。
栈内存分配
栈内存分配是指由编译器自动管理的内存,通常用于局部变量和函数调用时的临时数据存储。栈是内存的一种数据结构,以“后进先出”(LIFO)的方式管理内存。每当函数调用时,栈会为该函数的局部变量分配内存,并在函数返回时自动释放。
特点:
- 栈内存分配是由编译器自动进行的,用于存储函数的局部变量和函数调用信息。
- 使用栈来管理内存分配,分配和释放内存的速度非常快。
- 内存大小是在编译时确定的,不能在运行时动态改变。
- 栈内存分配的生命周期与其所在的函数相对应,在函数执行完毕后,内存会自动释放。
例子:
#include <stdio.h> void function() { int local_var = 5; // 局部变量 printf("Local variable: %d\n", local_var); } int main() { function(); return 0; }
local_var
是栈分配的局部变量。当function
函数执行完毕时,local_var
会自动销毁。
堆内存分配
堆内存分配是由程序员显式管理的内存分配方式,使用 malloc
、calloc
、realloc
和 free
等函数来申请和释放内存。堆内存的分配和释放需要程序员手动控制,适用于在程序运行时需要动态调整内存大小的情况。
特点:
- 堆内存分配是在运行时动态进行的,用于存储动态分配的内存块。
- 使用堆来管理内存分配,通过函数如 malloc 和 free 或 new 和 delete 进行操作。
- 内存大小可以在运行时动态改变,可以根据需要分配和释放内存。
- 堆内存分配需要手动管理内存的分配和释放,避免出现内存泄漏或悬挂指针等问题。
- 堆内存分配的生命周期由程序员控制,需要显示地释放已分配的内存。
例子:
#include <stdio.h> #include <stdlib.h> int main() { int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存 if (ptr == NULL) { printf("Memory allocation failed!\n"); return 1; } *ptr = 100; // 使用分配的内存 printf("Heap variable: %d\n", *ptr); free(ptr); // 释放堆内存 return 0; }
malloc
分配了堆内存,程序员通过指针访问堆内存。- 使用
free
释放堆内存,防止内存泄漏。
内存池
内存池是一种特殊的内存管理技术,通常用于避免频繁的内存分配和释放所带来的性能问题。通过将一块大的内存区域预先分配好,程序从中分配小块内存,而不是每次都向操作系统申请内存。这种方式常用于对内存分配有严格性能要求的应用程序,如游戏开发、实时系统等。
特点:
- 程序在启动时分配一个较大的内存池,之后从中分配小块内存。
- 避免了频繁的系统调用,提高内存分配的效率。
内存分配方式总结
静态内存分配 | 用于全局变量、静态变量 | 程序启动至结束 | 编译时决定 | 简单、速度快 | 不灵活,无法动态调整大小 |
栈内存分配 | 用于局部变量、函数调用 | 函数调用期间 | 自动管理 | 快速,自动管理 | 内存有限,容易发生栈溢出 |
堆内存分配 | 用于动态数据结构(如数组、链表等) | 程序运行期间 | 程序员手动管理 | 灵活,大小可动态变化 | 慢,容易发生内存泄漏 |
内存池 | 高效的内存分配机制 | 根据池的大小和使用情况 | 手动管理 | 高效,减少内存分配的系统调用 | 需要管理池的大小和状态 |
本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中