【面试】语言相关题型(二)

1. extern关键字

extern是一个关键字,用于C和C++编程语言中,表示一个变量或函数是在其他文件中定义的,也就是说,它们的定义在其他地方,不在当前文件中。这个关键字对于支持大型项目和模块化编程非常重要,因为它允许程序员在不同的源文件中使用相同的变量或函数。

1.1 原理

extern关键字告诉编译器,变量或函数的定义可能在别的源文件中。这样,编译器在编译时,就会去其他文件中找这个变量或函数的定义。如果找不到,编译就会失败。

1.2 作用

  • 在多个源文件中共享变量: 如果你有一个变量,想在多个源文件中使用,就可以用extern关键字。首先,你在一个文件中定义这个变量,然后在其他文件中用extern关键字声明它。这样,其他文件就可以使用这个变量了。

  • 在多个源文件中共享函数: 如果你有一个函数,想在多个源文件中使用,也可以用extern关键字。首先,你在一个文件中定义这个函数,然后在其他文件中用extern关键字声明它。这样,其他文件就可以调用这个函数了。

1.3 使用场景

  1. 都是C/C++代码

    • 在多个源文件中共享变量:这是最常见的使用场景。例如,你可能有一个全局变量,需要在多个文件中使用。你可以在一个文件中定义这个变量,然后在其他文件中使用extern关键字声明它。
    • 在多个源文件中共享函数:这也是一个常见的使用场景。例如,你可能有一个函数,需要在多个文件中调用。你可以在一个文件中定义这个函数,然后在其他文件中使用extern关键字声明它。
  2. 在C++中调用C的代码

    • 解决名字修饰(Name Mangling):C++对函数名进行名字修饰(Name Mangling)以支持函数重载,而C不这样做。这可能会导致在C++代码中调用C函数时出现问题,因为C++编译器可能无法识别C函数的名字。extern "C"可以告诉C++编译器,这个函数是用C语言写的,不应该对它的名字进行修饰。
    • 链接C库:如果你想在C++代码中链接C语言写的库,你需要使用extern "C"。这是因为C++编译器需要知道这些函数是用C语言写的,不应该对它们的名字进行修饰。

1.4 例子

例1:在多个文件中共享变量

假设我们有两个文件,一个是 main.cpp,一个是 another_file.cpp。我们想在这两个文件中共享一个变量 shared_variable。我们可以这样做:

another_file.cpp 中:

int shared_variable = 10;  // 定义并初始化 shared_variable

main.cpp 中:

extern int shared_variable;  // 声明 shared_variable

int main() {
    std::cout << "Shared Variable: " << shared_variable << std::endl;  // 打印 shared_variable 的值
    return 0;
}

例2:在 C++ 中调用 C 函数

假设我们有一个 C 函数 c_function,我们想在 C++ 代码中调用它。我们可以这样做:

c_functions.c 文件中:

#include <stdio.h>

void c_function() {
    printf("Hello from C function!\n");
}

main.cpp 文件中:

extern "C" {
    void c_function();  // 声明 C 函数
}

int main() {
    c_function();  // 调用 C 函数
    return 0;
}

注意:尽管extern关键字可以使变量和函数在多个源文件中可用,但是过度使用它可能会导致代码的可维护性降低。因为,当你在一个文件中看到一个extern变量或函数时,你必须去其他文件中找到它的定义,这可能会使代码难以理解和维护。因此,一般建议在必要的时候才使用extern关键字,并尽量减少全局变量的使用。

2. malloc、free与new、delete的区别

2.1 定义

  • malloc、free是C中的库函数
  • new、delete是C++中的操作符
  • new的流程
    • 计算所需内存大小:编译器首先需要知道要分配多少内存。对于基本数据类型(如int、double等),这很简单,因为它们的大小是预先知道的。对于类对象,编译器将计算类的大小,包括其所有非静态成员变量。
    • 申请内存:编译器会调用一个函数(通常是operator new或者其它类似的库函数),这个函数会向系统的内存管理器请求分配所需的内存。如果内存管理器无法分配足够的内存,那么operator new会抛出一个std::bad_alloc异常(除非你使用的是new(std::nothrow),在这种情况下,operator new会返回一个nullptr)。
    • 构造对象:如果请求的内存成功分配,编译器接下来会在分配的内存上构造对象。这包括调用对象的构造函数(如果有的话)。如果对象的构造函数抛出异常,那么已经分配的内存将被释放,然后异常将被传播给new运算符的调用者。
    • 返回指针:最后,new运算符返回一个指向新创建的对象的指针。
  • delete的流程
    • 调用析构函数:对于类类型的对象,编译器首先调用对象的析构函数。这是为了执行任何必要的清理工作,例如释放对象可能拥有的其他资源(如动态分配的内存,打开的文件句柄等)。对于基本类型(如intdouble等),没有析构函数要调用。
    • 释放内存:析构函数调用完成后,编译器会调用一个函数(通常是operator delete或其他类似的库函数)来释放对象占用的内存。这个函数告诉内存管理器这块内存现在可以被重新分配给其他对象。
    • 需要注意的是,delete仅仅释放内存,并不会把指向该内存的指针设置为nullptr。在delete之后,任何还在引用被删除对象的指针现在都是悬挂指针,对其的使用是未定义的行为。为了避免这种情况,一种常见的做法是在delete一个指针之后,立即把这个指针设置为nullptr

2.2 使用方式

  • new和delete不仅分配和释放内存,还会调用对象的构造函数和析构函数,malloc和free仅分配和释放内存,不调用构造函数和析构函数
  • new自动计算所需分配的内存,malloc需要手动计算所需分配的内存
  • new和delete是类型安全的。使用new创建对象时,需要指定对象的类型,new会返回正确类型的指针,malloc返回的是void*,需要手动转换为正确的类型
  • delete释放内存时需要该对象类型的指针,free可以接受任何类型的指针,包括void*
  • new分配失败会抛出异常,malloc分配失败会返回NULL
  • new是在free store上分配内存,malloc是在heap上分配内存
  • new和delete运算符可以用于分配和释放任何数据类型的内存,包括数组,malloc和free主要用于分配和释放一块大小已知的内存
  • delete、free调用后,内存不会被立刻释放,指针也不会指向空,为了避免空悬指针,释放内存后,应该要把指针指向null

3. static关键字

  1. 在函数体,代码块或类中声明变量:当在这些上下文中使用时,static意味着该变量的生命周期为程序的整个执行期间,而不是仅仅是其所在的代码块。这样的变量只会被初始化一次,然后在后续的函数调用或代码块执行中保持其值。这对于需要跨多次函数调用或代码块执行保持状态的情况很有用。

    void some_function() {
        static int call_count = 0;
        call_count++;
        std::cout << "Function has been called " << call_count << " times\n";
    }
    
  2. 在类中声明变量:在类中,static关键字用于声明静态成员变量。静态成员变量不是类的每个实例的一部分,而是属于类本身的。所有实例共享同一个静态成员变量。这种静态成员变量是与类本身关联的,而不是与类的任何特定对象关联的。因此,静态成员变量在所有类的对象中都是共享的。静态成员变量必须在类外部初始化。

    class SomeClass {
    public:
        static int static_member;
    };
    SomeClass::static_member = 0;
    
  3. 在类中声明函数:在类中,static关键字也可以用于声明静态成员函数。静态成员函数可以在没有类的实例的情况下调用,也就是说,它们不依赖于特定的对象。静态成员函数只能访问类的静态成员变量,不能访问类的非静态成员,因为静态成员函数没有this指针。

    class SomeClass {
    public:
        static void static_member_function() {
            std::cout << "This is a static member function.\n";
        }
    };
    
  4. 在全局变量或函数前:当static关键字用在全局变量或函数前时,它会更改这些实体的链接属性,使它们在其定义的源文件内部可见,而在文件外部则不可见。这被称为内部链接。这可以防止名称冲突,并帮助保持全局命名空间的清洁。

    // File: main.cpp
    static void some_function() {
        // This function is only visible within main.cpp
    }
    

4. strcpy、sprintf、memcpy的区别

4.1 使用方式

strcpysprintfmemcpy 都是在C和C++中常用的字符串和内存操作函数。

  1. strcpystrcpy函数用于复制字符串。它将源字符串(包括结束字符’\0’)复制到目标字符串。需要注意的是,strcpy不检查目标缓冲区的大小,所以可能会导致缓冲区溢出的问题。因此在使用时必须确保目标缓冲区足够大以容纳源字符串。

    char src[50] = "example";
    char dest[50];
    strcpy(dest, src);
    // Now dest contains the string "example"
    
  2. sprintfsprintf函数用于将格式化的数据写入字符串。这个函数与printf函数类似,只是printf将数据写入到标准输出(通常是屏幕),而sprintf将数据写入到字符串。与strcpy一样,sprintf也不检查目标缓冲区的大小,所以可能导致缓冲区溢出。如果需要防止缓冲区溢出,可以使用snprintf函数,这个函数需要指定最大的写入字符数。

    char buffer[50];
    int a = 10;
    float b = 3.14;
    sprintf(buffer, "a = %d, b = %.2f", a, b);
    // Now buffer contains the string "a = 10, b = 3.14"
    
  3. memcpymemcpy函数用于复制内存区域。它将一个源内存区域的前N个字节复制到目标内存区域。与strcpysprintf不同,memcpy并不关心数据的内容,它只是简单地复制字节。因此,memcpy可以用来复制任何类型的数据,包括字符串、结构、数组等。需要注意的是,memcpy不会检查源和目标内存区域是否重叠,如果重叠,可能会导致未定义的行为。

    char src[50] = "example";
    char dest[50];
    memcpy(dest, src, strlen(src) + 1);
    // Now dest contains the string "example"
    

4.2 区别

  1. 实现功能
    • strcpy实现字符串拷贝,遇到\0结束
    • sprintf用于格式化字符串
    • memcpy可以实现内存块的拷贝,根据size的大小来复制
  2. 执行效率
    • memcpy最快
    • strcpy次之
    • sprintf最慢
  3. 操作对象
    • strcpy操作对象为字符串
    • spintf操作对象可以为多种数据类型
    • memcpy操作对象为内存地址

5. 如何避免野指针

5.1 什么是野指针

"野指针"是指针变量的一个常见问题,它指的是指向"不确定区域"的指针。在以下几种情况下,指针通常被视为野指针:

  • 指针变量没有被初始化。例如,声明了一个指针变量,但没有给它赋值,这个指针就是一个野指针。
  • 指针被释放后没有被置为nullptr。例如,如果你使用deletefree释放了一个指针指向的内存,但没有把指针设置为nullptr,那么这个指针就变成了野指针。
  • 指针超出了其指向的对象或数组的范围。例如,如果你有一个指向数组的指针,并且这个指针移动到了数组的末尾之后,那么这个指针就变成了野指针。

5.2 使用野指针会有什么问题

使用野指针通常会引发问题,因为它可能会导致以下几种类型的错误:

  • 解引用野指针:如果你试图访问野指针指向的内存,结果通常是未定义的。在最好的情况下,你可能会得到一些随机的值。在最坏的情况下,程序可能会崩溃,因为野指针可能指向一个你的程序没有权限访问的内存区域。

  • 内存泄漏:如果你丢失了一个指针(即让一个指针变成了野指针),而这个指针是唯一指向一块动态分配的内存的指针,那么你就无法再释放那块内存。这被称为内存泄漏,它会导致你的程序消耗越来越多的内存。

  • 数据破坏:如果你不小心使用了野指针,可能会无意中覆盖内存中的其他数据。这可能会导致各种难以跟踪的问题,因为它可能会影响到程序中的任何部分。

5.3 如何避免

  1. 初始化指针:当你声明一个新的指针时,立即初始化它。如果没有具体的对象可以指向,那就初始化为nullptr

    int* p = nullptr; // 初始化为 nullptr
    
  2. 为指针对象分配内存:在使用指针操作对象之前,确保已经为这个对象分配了内存。

    p = new int; // 为指针对象分配内存
    // or
    p = (int*)malloc(sizeof(int)); // 在C中,使用malloc为指针对象分配内存
    
  3. 检查内存分配是否成功:在使用newmalloc分配内存后,需要检查指针是否为nullptr。如果是,说明内存分配失败,这时需要进行错误处理,通常是打印错误信息并退出程序。

    p = new(std::nothrow) int; // 使用nothrow版本的new,如果内存分配失败,返回nullptr
    if (p == nullptr) {
        std::cerr << "Memory allocation failed.\n";
        exit(EXIT_FAILURE);
    }
    
  4. 对空间数据置零:在C中,分配内存后,为了安全,通常需要将新分配的内存初始化为零。

    p = (int*)malloc(sizeof(int));
    if (p == nullptr) {
        fprintf(stderr, "Memory allocation failed.\n");
        exit(EXIT_FAILURE);
    }
    memset(p, 0, sizeof(int)); // 将内存空间置零
    
  5. 释放后置空:当你使用deletefree释放一个指针指向的内存后,立即把这个指针设置为nullptr。这样,即使你再次错误地尝试释放这个指针,由于它现在是nullptr,这将是一个无害的操作。

    delete p;
    p = nullptr; // 释放后置空
    
  6. 使用智能指针:在C++11及以后的版本中,你可以使用智能指针(如std::unique_ptrstd::shared_ptr)来自动管理内存。智能指针会在合适的时候自动释放它们所拥有的内存,这可以避免很多与野指针相关的问题。

    std::unique_ptr<int> p(new int(42));
    // Now you don't have to worry about deleting the memory when you're done with it
    

6. 引用和指针的作用及区别

6.1 作用

指针的作用

  1. 传递参数:指针可以作为函数参数,允许函数访问和修改它们指向的对象。这样可以避免对象数据的复制,特别是在处理大型数据结构时,使用指针而非值传递可以显著提高性能。

    void update(int* p) {
        *p = 10; // 修改p指向的值
    }
    
  2. 实现多态:在面向对象的编程中,指针可以用来实现多态。基类的指针可以指向派生类的对象,并调用虚函数,这样在运行时就能根据对象的实际类型执行正确的函数。

    class Base {
    public:
        virtual void print() const {
            std::cout << "Base\n";
        }
    };
    
    class Derived : public Base {
    public:
        void print() const override {
            std::cout << "Derived\n";
        }
    };
    
    Base* p = new Derived;
    p->print(); // Prints "Derived"
    delete p;
    
  3. 代码复用:指针可以帮助实现代码复用。例如,你可以写一个通用的函数,这个函数接受指向不同类型的对象的指针,并对这些对象执行相同的操作。

引用的作用

  1. 传递参数:引用也可以作为函数参数,允许函数访问和修改它们引用的对象。使用引用参数可以避免对象的复制,同时语法上比使用指针更清晰。

    void update(int& ref) {
        ref = 10; // 修改ref引用的值
    }
    
  2. 函数返回值:函数可以返回对象的引用,这样可以避免对象数据的复制。例如,你可以返回对象的成员变量的引用,或者返回数组元素的引用。

    int& getFirst(std::vector<int>& vec) {
        return vec[0]; // 返回vec的第一个元素的引用
    }
    

指针和引用在很多情况下可以互换使用,但它们各自都有一些特定的用途,例如指针可以用来实现动态多态,而引用可以用来创建别名。在选择使用指针还是引用时,通常应该考虑代码的具体需求和设计意图。

6.2 区别

指针和引用在C++中都被用于间接访问对象,但它们之间存在几个关键的区别:

  1. 是否需要初始化

    • 指针:在声明时不一定需要初始化,但出于安全考虑,最好初始化。如果没有具体的对象可以指向,可以将其初始化为nullptr

      int* p = nullptr; // 指针初始化为 nullptr
      
    • 引用:必须在声明时初始化,并且一旦被初始化后,就不能改变其绑定的对象。引用不能被初始化为无对象。

      int a = 10;
      int& ref = a; // 引用初始化,绑定到a
      
  2. 是否允许为空

    • 指针:可以为空,即指向nullptr
    • 引用:不允许为空,它必须始终绑定到一个具体的对象。
  3. 是否直接操作对象

    • 指针:通过某个指针变量指向一个对象,对它所指向的变量进行间接操作。

      *p = 20; // 间接操作p所指向的对象
      
    • 引用:是目标对象的别名,对引用操作就是直接对目标对象操作。

      ref = 20; // 直接操作引用的目标对象
      
  4. 是否是对象

    • 指针:是一个对象,它有自己的地址,并可以被其他指针指向,即可以定义指针的指针。

      int** pp = &p; // 定义指针的指针
      
    • 引用:不是一个对象,它没有实际地址,不能定义引用的引用或引用的指针。

这些区别使得指针和引用在不同的场合有不同的用途。例如,如果你需要一个可以为空的、可以重新绑定的、或者可以指向不同类型对象的“间接访问”,你可能需要使用指针。而如果你需要一个始终绑定到某个对象、并且可以像操作实际对象一样操作的“间接访问”,你可能需要使用引用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值