C/C++八股面试题(八)

目录:

1.请说明struct和class并且说明他们有什么区别?

2.什么是数组名,数组名和指针有什么区别?

3.简单介绍malloc如何使用?

4.malloc底层实现的流程?

5.内存分配的方式有哪些?

内容:

1.请说明struct和class并且说明他们有什么区别?

结构体struct

定义结构体

  • 我们可以使用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.xp1.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

    类的定义

  • 一个类定义包括成员变量、成员函数、构造函数、析构函数等。类的成员变量和成员函数可以具有不同的访问控制(publicprivateprotected),默认访问权限为 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;
    }
    
    

    类的组成

    • 成员变量(属性):定义类的状态,通常是数据成员。它们存储对象的相关信息。
    • 成员函数(方法):定义类的行为,操作数据成员或执行某些任务。
    • 构造函数:用于初始化类的对象,通常在创建对象时自动调用。
    • 析构函数:用于清理类的资源,通常在对象销毁时自动调用。
    • 访问控制:控制类成员的访问权限(publicprivateprotected)。

    成员访问控制:

    • 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
    };
    
    
    • DerivedStructBase 继承时,使用的是 公有继承,可以直接访问基类的成员。
    • DerivedClassBase 继承时,使用的是 私有继承,无法直接访问基类的成员。

    结构体的使用场景:

    • 用于存储一组相关的数据,但没有复杂的操作和逻辑。
    • 当数据的封装比行为更重要时,例如在处理图形、坐标、日期等数据时。
    • 当你需要将数据序列化/反序列化为二进制或其他格式时。
    • 作为轻量级的数据容器,适用于性能要求较高的情况。

    类的使用场景

    • 当你需要封装数据并附加操作和行为时,类更适合,因为它允许你将数据和操作封装在一起。
    • 在面向对象编程中,用于建模现实世界的对象,例如人、车辆、银行账户等。
    • 当你需要使用继承和多态来实现代码的扩展和重用。
    • 为了实现更复杂的数据结构,如链表、树、图等。

    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 可能会直接向操作系统请求更大的内存块(例如通过 sbrkmmap 等系统调用)。

    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 会自动销毁。

    堆内存分配

    堆内存分配是由程序员显式管理的内存分配方式,使用 malloccallocreallocfree 等函数来申请和释放内存。堆内存的分配和释放需要程序员手动控制,适用于在程序运行时需要动态调整内存大小的情况。

    特点:

    • 堆内存分配是在运行时动态进行的,用于存储动态分配的内存块。
    • 使用堆来管理内存分配,通过函数如 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/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中

    全部评论
    佬有没有刷题的推荐推荐
    1 回复 分享
    发布于 12-23 16:05 陕西

    相关推荐

    先说说我的情况吧,我的学历是民办二本,我也知道学校不好,所以我没指望过靠学校的什么光环或者说靠学校的招聘会什么的,也是一直从大一寒假开始,直到现在一直学的Java,其实我到大二暑假的时候就已经接触微服务了,可是当时学得不怎么扎实,八股文也是大三就知道有这么个东西了,期间也是断断续续的看,就是效率不咋高,因为我平常没啥总结的,就是看到啥算啥,现在想着有有点后悔的,早知道一开始就扎扎实实的学了,项目也是实操得少,简单过了一遍&nbsp;看了视频&nbsp;大致了解过程&nbsp;当时我想得是赶快过几个项目,然后就马上去找实习,结果是boss找实习根本面试都没几个,还是暑假的时候去了一家外包实习就是亚信科技,但是也没学什么,简历也是包装了,现在简历就是一段实习,然后两个烂大街的项目,然后现在八股也在看,但是我是真的不想直接背,还是想理解为主,所以其实八股没怎么认真看过,一直在看视频学习,还买了很多书,想着好好看一下,平常也做好笔记,扎扎实实学好,但是现在看来感觉没啥时间了,我现在已经是大四了,真不知道该怎么办了,不知道是好好沉淀备战春招呢,还是说去找个实习再加一段实习经历呢?之前有两个实习我因为我一直想着沉淀,而且是外包,而且还是外地,就一直没想着去,现在感觉有那么点后悔了,其实是不是混段经历也不错呢&nbsp;还是武汉3500&nbsp;上海3300包住的&nbsp;虽然是外包,但是薪资还行吧,但是我都放弃了,还有一个本地的,就是要去工厂里面驻场,也是放弃了,现在也是焦虑得不得了,不知道怎么办了,现在家里还有驾照要考,唉,都凑在一块了。兄弟们你们说到底怎么办呢,兄弟们,客观来说,我接下来应该怎么办呢?😭😭😭
    KPLACE:我一样,但是事情得一件一件来,不重要的先放掉,我们不可能每一件事都做好,找准一两件再去做
    点赞 评论 收藏
    分享
    评论
    5
    13
    分享
    牛客网
    牛客企业服务