C++面向对象的常见面试题目(一)

1. 面向对象的三大特征

(1)封装:隐藏对象的内部状态,只暴露必要的接口。

#include <iostream>
#include <string>

// 定义一个简单的类 Person
class Person {
private: // 私有成员,外部不可直接访问
    std::string name;
    int age;

public: // 公共方法,外部可以访问
    // 构造函数,用于初始化对象
    Person(std::string n, int a) {
        name = n;
        age = a;
    }

    // 公共方法,用于设置年龄
    void setAge(int a) {
        if (a > 0 && a < 150) { // 简单的年龄验证
            age = a;
        } else {
            std::cout << "Invalid age!" << std::endl;
        }
    }

    // 公共方法,用于获取姓名
    std::string getName() {
        return name;
    }

    // 公共方法,用于获取年龄
    int getAge() {
        return age;
    }
};

int main() {
    // 创建 Person 对象
    Person p1("Alice", 30);

    // 尝试直接访问私有成员(编译错误)
    // std::cout << p1.name << std::endl;

    // 使用公共方法设置年龄
    p1.setAge(35);

    // 使用公共方法获取姓名和年龄
    std::cout << "Name: " << p1.getName() << ", Age: " << p1.getAge() << std::endl;

    return 0;
}

(2)继承:无需修改原有类的情况下对功能实现拓展

#include <iostream>
#include <string>

// 定义一个基类 Animal
class Animal {
protected: // 受保护的成员,子类可以访问
    std::string name;

public:
    // 构造函数,初始化 Animal 的名称
    Animal(std::string n) : name(n) {}

    // 公共方法,用于输出 Animal 的声音
    void makeSound() const {
        std::cout << "Animal " << name << " makes a sound" << std::endl;
    }
};

// 定义一个派生类 Dog,继承自 Animal
class Dog : public Animal {
private:
    std::string breed;

public:
    // 构造函数,初始化 Dog 的名称和品种
    Dog(std::string n, std::string b) : Animal(n), breed(b) {}

    // 重写父类的 makeSound 方法
    void makeSound() const {
        std::cout << "Dog " << name << " barks" << std::endl;
    }

    // 新的方法,用于输出 Dog 的品种
    void showBreed() const {
        std::cout << "Dog " << name << " is of breed " << breed << std::endl;
    }
};

int main() {
    // 创建 Animal 对象
    Animal a("Generic");

    // 调用 Animal 的方法
    a.makeSound();

    // 创建 Dog 对象
    Dog d("Buddy", "Labrador");

    // 调用 Dog 的方法
    d.makeSound();
    d.showBreed();

    // 使用基类指针指向派生类对象,演示多态
    Animal* animalPtr = &d;
    animalPtr->makeSound(); // 调用的是 Dog 的 makeSound 方法

    return 0;
}

(3)多态:同一个函数名在不同对象中有不同的行为。通过时下接口重用增强可拓展性

多态分为2类,静态多态和动态多态。

静态多态:又称编译时多态。通过函数重载和运算符重载实现。

动态多态:又称运行时多态,通过虚函数和继承实现。

虚函数:在基类中声明函数为虚函数,派生类可以覆盖这些虚函数,从而实现不同的行为。在运行时,根据对象的实际类型决定调用哪个版本的函数,这种机制称为动态绑定或后期绑定。

// 静态多态
#include <iostream>

class MathTool {
public:
    // 计算整数平方
    int calculateSquare(int number) {
        return number * number;
    }

    // 计算浮点数平方
    double calculateSquare(double number) {
        return number * number;
    }
};

int main() {
    MathTool tool;

    // 静态多态示例:根据参数类型自动选择合适的方法
    std::cout << "整数5的平方是: " << tool.calculateSquare(5) << std::endl;      // 调用整数版本
    std::cout << "浮点数2.5的平方是: " << tool.calculateSquare(2.5) << std::endl; // 调用浮点数版本

    return 0;
}

// 动态多态
#include <iostream>

// 基类,含有虚函数
class Animal {
public:
    virtual ~Animal() {} // 虚析构函数,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数
    virtual void makeSound() {
        std::cout << "Some animal makes a sound." << std::endl;
    }
};

// 派生类1
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks." << std::endl;
    }
};

// 派生类2
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows." << std::endl;
    }
};

int main() {
    Animal* animalPtr; // 基类指针

    // 动态分配内存给派生类对象
    animalPtr = new Dog();
    animalPtr->makeSound(); // 运行时动态决定调用Dog的makeSound()

    animalPtr = new Cat();
    animalPtr->makeSound(); // 运行时动态决定调用Cat的makeSound()

    delete animalPtr; // 由于基类析构函数为虚函数,可以安全删除派生类对象

    return 0;
}

2. 多态的实现原理

(1)静态多态

原理:函数名修饰。编辑器会根据函数的签名(包括参数类型、数量和顺序)对内部的函数名称进行编码,生成一个唯一的标识符。这个编码后的名称包含了足够的信息,使得编译器能够准确地区分不同的重载版本,即使它们的外部名称相同。

编译过程:

        预编译:把头文件中的函数声明拷贝到源文件,避免编译过程中语法分析找不到函数定义。

        编译:语法分析,同时进行符号汇总

        汇编:生成函数名到函数地址的映射,方便之后通过函数名照到函数定义从而执行函数。

        链接:讲过个文件的符号表汇总合并

早绑定:编译器编译时就已经确定对象调用的函数的地址。静态多态依赖于早绑定。

(2)动态多态

原理:虚函数重写。当一个类中声明了至少一个虚函数时,编译器会为该类生成一个虚函数表。这是一个存储虚函数指针的数组,每个指针指向类中相应虚函数的实现。含有虚函数的类实例在内存中除了包含数据成员外,还会有一个指向其所属类的虚函数表的指针(通常称为vptr)。这个vptr是在对象创建时由编译器自动初始化的。在派生类中,当你定义了一个与基类中虚函数同名且签名相同的函数时,这就是虚函数的重写。派生类的虚函数表中,对应的条目会存储派生类中该函数的地址,而非基类的。

虚函数重写:基类函数上加上virtual关键字,在派生类重写虚函数。运行时会根据对象的类型调用相应的函数。如果对象的类型是基类,那么调用基类的函数,如果对象的类型是派生类诶,则调用派生类的函数

晚绑定:程序运行时才确定对象调用的函数的地址。动态多态依赖于晚绑定。C++中,晚绑定是通过virtual关键字实现的。

3. 怎么解决菱形继承

因为c++有多重继承的特性,导致一个子类可能继承多个基类。这些基类可能继承自相同的基类,从而造成了菱形继承。

菱形继承的问题主要是数据冗余并且造成二义性。

二义性:当一个派生类(我们称之为D)从两个不同的基类(比如B1B2)继承,而这两个基类又都继承自同一个基类(A),那么在D中直接访问从A继承来的成员时,编译器无法确定应该使用B1继承的版本还是B2继承的版本。这种情况下,编译器会报错,指出存在二义性。

数据冗余:如果不使用虚拟继承(virtual关键字),每个派生类(B1B2)都会包含基类A的一个完整副本。因此,当D继承自B1B2时,它将拥有两份A的成员,导致数据冗余。这不仅浪费存储空间,还可能导致逻辑上的混乱,尤其是在修改这些成员时,可能会忘记更新所有副本,从而引发一致性问题。

使用虚继承可以解决菱形继承问题。在派生类对共同基类进行虚继承时,编译器会确保只有一份共同基类的实例,而不是每个中间类都有自己的一份。

4. 关键字override、final的作用

子类继承基类的虚函数之后,可能存在这样的问题:子类不希望这个函数会自己的子类被进一步重写;子类想重写一个新函数,但是错误的重写了基类虚函数;子类本意是重写基类的虚函数但是签名不一致导致重新构建了一个虚函数;子类希望自己绝后!但是没有办法从语法上完成这件事。

override可以指定子类的一个虚函数复写基类的一个虚函数;并且保证该重写的虚函数与子类的虚函数有相同的签名。

final可以指定某个虚函数不能在派生类中覆盖,或者某个类不能被派生。加在类前面就可以阻塞类的进一步派生。

5. C++类型推导的用法

C++类型推导主要有三个方面:auto、decltype、模板类型推导

(1)auto:用于推导变量的类型,通过强制声明一个变量的初始值,编译器会通过初始值进行推导类型

(2) decltype

用于推导表达式的类型,编译器只分析表达式类型而不参与运算

 6. C++11 中function、lambda、bind之间的关系

std::function是一个抽象了函数参数和函数返回值的类模板。它把任意函数包装成了一个对象,该对象可以保存传递以及复制。也可以进行动态绑定,只需要修改该对象,实现类似多态的效果

#include <iostream>
#include <functional>

// 函数对象(仿函数)
struct Adder {
    int operator()(int a, int b) const {
        return a + b;
    }
};

// 普通函数
int subtract(int a, int b) {
    return a - b;
}

// 成员函数
struct Calculator {
    int multiply(int a, int b) {
        return a * b;
    }
};

int main() {
    // 使用 std::function 声明不同类型的可调用对象
    std::function<int(int, int)> func1 = Adder();  // 函数对象
    std::function<int(int, int)> func2 = subtract; // 普通函数

    Calculator calc;
    std::function<int(Calculator&, int, int)> func3 = &Calculator::multiply;  // 成员函数

    // 调用并输出结果
    std::cout << "Adder result: " << func1(10, 5) << std::endl;
    std::cout << "Subtract result: " << func2(10, 5) << std::endl;
    std::cout << "Multiply result: " << func3(calc, 10, 5) << std::endl;

    return 0;
}

lambda表达式:一种方面的创建匿名函数的语法糖

原理:编译的时候把表达式转变为一个函数对象,然后根据表达式惨呼列表重载operate ()

demo:

// 简单表达式
auto sayHello = []{ std::cout << "Hello, World!\n"; };
sayHello();

// 捕获外部变量
int x = 10, y = 20;
auto incrementAndPrint = [x, &y]() {
    x++; // 按值捕获,不会改变外部x
    y++; // 按引用捕获,会改变外部y
    std::cout << "x: " << x << ", y: " << y << "\n";
};
incrementAndPrint(); // 输出:x: 11, y: 21
std::cout << "After: x=" << x << ", y=" << y << "\n"; // 输出:x=10, y=21


// 指定返回类型
int x = 10, y = 20;
auto incrementAndPrint = [x, &y]() {
    x++; // 按值捕获,不会改变外部x
    y++; // 按引用捕获,会改变外部y
    std::cout << "x: " << x << ", y: " << y << "\n";
};
incrementAndPrint(); // 输出:x: 11, y: 21
std::cout << "After: x=" << x << ", y=" << y << "\n"; // 输出:x=10, y=21


std::bind:用来通过绑定函数以及函数参数的方式生成函数对象的模板函数,提供占位符,实现灵活的参数绑定

#include <iostream>
#include <functional>

// 通用加法函数
int add(int x, int y) {
    return x + y;
}

int main() {
    // 使用std::bind固定add的第一个参数为5
    auto addFive = std::bind(add, 5, std::placeholders::_1);

    // 现在addFive是一个新的可调用对象,只需要一个参数
    std::cout << "Adding 5 to 3 gives: " << addFive(3) << '\n'; // 输出: 8
    std::cout << "Adding 5 to 10 gives: " << addFive(10) << '\n'; // 输出: 15

    return 0;
}

总结:function 用来描述函数对象的类型;lambda 表达式用来生成函数对象(可以访问外部变量的匿名函数);bind 也是用来生成函数对象(函数和参数进行绑定生成函数对象);

这是一条吃饭博客,由挨踢零声赞助。学C/C++就找挨踢零声,加入挨踢零声,面试不挨踢!

  • 37
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值