当前位置: 首页 > 面试经验 >

C++面试高频(一)

优质
小牛编辑
106浏览
2023-10-06

C++面试高频(一)

1.new和malloc的区别(使用和原理)

new的定义:

new是C++的关键字,用于动态分配内存并创建对象。它可以根据类型自动计算所需内存空间,并调用对象的构造函数进行初始化。在使用new分配内存后,需要使用delete来释放这些内存空间,以防止内存泄漏。

malloc的定义:

malloc是C语言的库函数,用于动态分配一块指定大小的内存块,并返回其地址。需要注意的是,使用malloc分配内存后,需要使用free来释放这些内存空间,以防止内存泄漏。

new与malloc的区别:(简单理解)

语法:new是C++的关键字,而malloc是C语言的库函数。

类型安全:new操作符会根据类型自动计算所需内存大小,并进行类型匹配,返回的是对象类型指针;而malloc需要手动计算内存大小,并使用强制类型转换,返回的是void指针。

构造函数与析构函数的调用:new会自动调用对象的构造函数进行初始化,而malloc不会调用构造函数,得到的内存空间内容是未初始化的。

内存泄漏的检测:new可以通过异常机制检测内存分配失败,而malloc在分配失败时返回NULL,需要手动检查。

重载和自定义类型:new操作符可以重载,并能够与自定义类型的构造函数和析构函数配合使用;而malloc是库函数,不会调用自定义类型的构造和析构函数

new与malloc使用区别

#include <iostream>
#include <cstdlib>

int main() {
    // 使用new进行动态内存分配和释放
    int* newPtr = new int(10);
    std::cout << "Value allocated with new: " << *newPtr << std::endl;
    delete newPtr;

    // 使用malloc进行内存分配和释放
    int* mallocPtr = (int*)malloc(sizeof(int));
    if (mallocPtr != nullptr) {
        *mallocPtr = 20;
        std::cout << "Value allocated with malloc: " << *mallocPtr << std::endl;
        free(mallocPtr);
    }

    return 0;
}

2.struct和class的区别

  1. 默认访问权限:struct中的成员默认为公共(public),而class中的成员默认为私有(private)。
  2. 默认继承方式:struct中的继承方式默认为公共(public),class中的继承方式默认为私有(private)。
  3. 使用习惯:struct适合用于简单的数据结构,class适合用于复杂的数据类型和实现面向对象编程。
  4. 成员变量和成员函数:struct中的成员变量和成员函数默认为公共,而class中的成员变量和成员函数默认为私有。
  5. 访问控制:struct中的成员在外部可直接访问,而class中的成员需要使用公共的成员函数来访问。
  6. 默认的构造函数和析构函数:class中会自动生成默认的构造函数和析构函数,而struct中不会。

3.char和int之间的转换

  1. 将char转换为int:可以直接将char类型的变量赋值给int类型的变量,将字符对应的ASCII码值赋给int变量。
char c = 'A';
int i = c;  // 将字符'A'的ASCII码值赋给i

2.将int转换为char:可以使用强制类型转换 (static_cast<char>) 将int类型的变量转换为char类型的变量,该方法只会截取int变量的低位字节作为字符。

int i = 65;
char c = static_cast<char>(i);  // 将整数65转换为对应的字符'A'

需要注意的是,对于转换为char的int值,如果超出了char类型的范围(-128至127),将会发生溢出,只保留最低位字节的值。

4.什么是野指针和悬挂指针

野指针(Dangling Pointer):野指针是指指向已释放或无效的内存的指针。当指针指向的内存被释放后,该指针仍然保留着原来的地址,但是指向的内存已经无效。对野指针进行解引用操作或者修改指针指向的内存可能导致程序崩溃或产生未定义的行为。

举例:

int* getDanglingPointer() {
    int* ptr = new int(5); // 动态分配内存
    delete ptr; // 释放内存
    return ptr; // 返回野指针
}

int main() {
    int* danglingPtr = getDanglingPointer();
    // 在此时,danglingPtr 是一个野指针
    // 对野指针进行解引用或操作将导致未定义的行为
    *danglingPtr = 10; // 可能导致程序崩溃
    return 0;
}

悬挂指针(Dangling Reference):悬挂指针是指引用了已被销毁或无效的对象的引用。当引用的对象被销毁后,悬挂指针仍然保留着之前的引用,但是该对象已经不存在。对悬挂指针进行访问或操作可能导致未定义的行为。

示例代码:

int& getDanglingReference() {
    int x = 5;
    int& ref = x; // 创建一个引用,引用了局部变量
    return ref; // 返回悬挂指针
}

int main() {
    int& danglingRef = getDanglingReference();
    // 在此时,danglingRef 是一个悬挂指针
    // 对悬挂指针进行访问或操作将导致未定义的行为
    danglingRef = 10; // 可能导致不正确的结果
    return 0;
}

5.NULL和nullptr区别

类型不同:NULL是宏定义或整数值0,而nullptr是C++11引入的关键字,表示空指针。

安全性不同:NULL可能导致函数调用二义性问题,nullptr更安全,不会被错误解释为整型。

上下文匹配不同:NULL可以用于整型类型的上下文,nullptr只能用于指针类型的上下文。

6.智能指针⭐⭐(结合使用场景进行理解)

智能指针是C++中用于管理动态分配对象的一种特殊指针类型,它能够自动地分配和释放内存,避免内存泄漏和悬挂指针的问题。常用的智能指针有unique_ptrshared_ptrweak_ptr auto_ptr(已弃用)。

unique_ptr

  1. unique_ptr是独占所有权的智能指针,用于管理动态分配的对象。
  2. 它禁止多个unique_ptr指向同一对象,可以通过std::move转移所有权。
  3. 适用于需要独占所有权的场景,能够避免内存泄漏。
  4. 4.unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。

举例代码:

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> uniquePtr(new int(10));
    if (uniquePtr) {
        std::cout << *uniquePtr << std::endl; // 输出10
    }
    uniquePtr.reset(); // 手动释放内存
    return 0;
}

shared_ptr:

  1. shared_ptr允许多个指针共享对同一对象的所有权,通过引用计数来追踪当前有多少个指针共享一个对象。
  2. 当最后一个shared_ptr超出作用域或被重置时,才会释放所管理的对象。
  3. 它可以通过std::make_shared来创建,并且允许拷贝和移动。

举例代码:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(10);
    std::shared_ptr<int> sharedPtr2 = sharedPtr1;

    std::cout << *sharedPtr1 << " " << *sharedPtr2 << std::endl; // 输出10 10

    sharedPtr1.reset(); // 释放sharedPtr1所指向的对象

    if (sharedPtr2) {
        std::cout << *sharedPtr2 << std::endl; // 输出10
    }

    return 0;
}

weak_ptr:

  1. weak_ptr是一种不共享所有权的智能指针,用于解决shared_ptr的循环引用问题。
  2. weak_ptr可以从shared_ptr创建,但不能直接访问所管理的对象。
  3. 它可以使用lock()方法来获取一个有效的shared_ptr,用于访问所管理的对象。

举例代码:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
    std::weak_ptr<int> weakPtr(sharedPtr);

    if (auto lockedPtr = weakPtr.lock()) {
        std::cout << *lockedPtr << std::endl; // 输出10
    }

    sharedPtr.reset(); // 释放sharedPtr,引用计数为0

    if (weakPtr.expired()) {
        std::cout << "Weak pointer expired" << std::endl;
    }

    return 0;
}

7.C++四种类型转换符⭐⭐

C++中有四种类型转换符可用于在不同类型之间进行类型转换。static_cast、dynamic_cast、const_cast和reinterpret_cast。

static_cast

  1. 基本类型之间的转换,例如将int转换为double等。
  2. 向上或向下进行继承关系的指针或引用转换。
  3. 显式调用转换构造函数或转换操作符。
  4. 进行其他合法的转换,例如指针与整数类型之间的转换。

示例:

int num = 10;
double convertedNum = static_cast<double>(num);

class Base {};
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr);

dynamic_cast

  1. 向上转换:将派生类指针或引用转换为基类指针或引用。
  2. 安全向下转换:将基类指针或引用转换为派生类指针或引用,仅当基类指针或引用实际指向派生类对象时才有效。
  3. 运行时类型检查:dynamic_cast会在运行时检查转换的安全性,如果转换失败,返回空指针(对于指针转换)或

抛出std::bad_cast异常(对于引用转换)

示例:

class Base { virtual void foo() {} };
class Derived : public Base {};

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
    // 转换成功
}

const_cast

  1. const_cast用于去除指针或引用的const属性。
  2. 可以修改被const修饰的对象。
  3. 仅能去除直接指针或引用的const属性。
  4. 使用const_cast需谨慎,因为修改被const修饰的对象会导致未定义行为。仅在确保安全性的前提下使用。

示例:

const int num = 10;
int* nonConstPtr = const_cast<int*>(&num);
*nonConstPtr = 20; // 合法:修改nonConstPtr的值

reinterpret_cast

  1. reinterpret_cast是C++中用于执行低级别的类型转换的关键字(使用reinterpret_cast需要格外谨慎)。
  2. 它可以将一个指针或引用转换为不同类型的指针或引用,甚至是完全无关的类型。
  3. reinterpret_cast在类型转换时只进行位模式的重新解释,不执行任何类型检查或转换操作。
  4. 错误的使用reinterpret_cast可能导致程序行为不确定或非法。
  5. 因此,除非绝对必要,否则应避免使用reinterpret_cast,并且使用前需要确保类型转换的合法性。

示例代码:

int num = 10;
double* doublePtr = reinterpret_cast<double*>(&num);  // 不安全,可能导致未定义行为

int* intPtr = reinterpret_cast<int*>(doublePtr);  // 转回原始类型

8.重载、重写和隐藏的区别⭐

重载(Overloading):

  • 重载是在同一个作用域内定义多个相同名称但参数列表不同的函数或方法。
  • 重载函数可以根据不同的参数数量或类型来执行不同的操作。
  • 重载通过函数名和参数列表来区分不同的函数。

举例代码:

#include <iostream>

void printNumber(int num) {
    std::cout << "Integer number: " << num << std::endl;
}

void printNumber(double num) {
    std::cout << "Floating-point number: " << num << std::endl;
}

int main() {
    printNumber(10);
    printNumber(3.14);
    return 0;
}

重写(Override):

  • 重写是指子类重新定义从父类继承的虚函数,使其具有不同的实现。
  • 重写的函数签名(函数名、参数列表和返回类型)必须与被重写函数相同。
  • 在运行时,根据具体的对象类型,调用的是子类重写的版本。

举例代码:

#include <iostream>

class Base {
public:
    virtual void sayHello() {
        std::cout << "Hello from Base class!" << std::endl;
    }
};

class Derived : public Base {
public:
    void sayHello() override {  // 使用 override 关键字表明重写了父类的函数
        std::cout << "Hello from Derived class!" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->sayHello();  // Output: "Hello from Derived class!"
    delete basePtr;
    return 0;
}

隐藏(Hiding):

  • 隐藏是指在派生类中定义与父类具有相同名称的成员函数,使其隐藏父类中的同名函数。
  • 隐藏函数与父类的函数没有多态性,只有通过对象的实际类型调用时才会调用相应的函数。

举例代码:

#include <iostream>

class Base {
public:
    void sayHello() {
        std::cout << "Hello from Base class!" << std::endl;
    }
};

class Derived : public Base {
public:
    void sayHello() {
        std::cout << "Hello from Derived class!" << std::endl;
    }
};

int main() {
    Base baseObj;
    Derived derivedObj;
    
    baseObj.sayHello();    // Output: "Hello from Base class!"
    derivedObj.sayHello(); // Output: "Hello from Derived class!"
    
    Base* basePtr = new Derived();
    basePtr->sayHello();   // Output: "Hello from Base class!"
    
    delete basePtr;
    return 0;
}

9.简述面向对象的三大特性

面向对象编程(OOP)的三大特性是封装、继承和多态。下面对每个特性进行简要说明:

封装(Encapsulation):

  • 封装是将数据和操作封装在一个单元(类)中的机制。
  • 通过封装,实现类的成员变量和成员函数作为一个整体进行管理和操作。
  • 封装隐藏了数据的具体实现细节,只暴露出必要的接口,提供了更好的安全性和可维护性。
  • 通过访问修饰符(公有、私有、保护),控制对类的成员的访问权限。

继承(Inheritance):

  • 继承是通过创建派生类来扩展和重用已有类的机制。
  • 基类(父类)是已经定义的类,派生类(子类)继承了基类的属性和方法。
  • 子类可以自定义新的属性和方法,也可以覆盖或扩展继承的父类的属性和方法。
  • 继承实现了类与类之间的关系,实现了代码的重用和扩展。

多态(Polymorphism):

  • 多态是指同一个接口可以由不同的对象以不同的方式进行实现和响应的能力。
  • 多态允许使用基类的指针或引用来引用派生类的对象,实现了多种形态的使用。
  • 编译时多态使用函数重载和运算符重载;运行时多态通过虚函数实现(动态绑定)。
  • 多态提高了代码的灵活性和可扩展性,使得程序更具有可读性和可维护性。

10.什么是多态?

利用虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。 基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

代码举例:

#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "This is the Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "This is the Derived class" << std::endl;
    }
};

int main() {
    Base* basePtr;
    
    Base baseObj;
    Derived derivedObj;
    
    basePtr = &baseObj;
    basePtr->print();  // 此时使用基类的成员函数来打印消息
    
    basePtr = &derivedObj;
    basePtr->print();  // 此时使用派生类的成员函数来打印消息
    
    return 0;
}

#晒一晒我的offer##24届软开秋招面试经验大赏##我发现了面试通关密码##如何判断面试是否凉了#
 类似资料: