1.10 C++ 面向对象(重头戏)

一、面向对象和面向过程区别

面向过程:强调过程的抽象化和模块化以过程为中心处理客观世界问题

面向对象:强调把解决问题的方法直接绑定到对象身上

二、 面向对象的基本特征有哪些?

抽象(抽象出共同的、本质的特征)、继承(基类与派生类)、封装(将数据和方法封装在一个类中,只提供一些公有接口来访问)、多态(相同方法在不同对象上的表现不同)。

三、什么是深拷贝和浅拷贝

主要是针对指针成员变量来说的,深拷贝要求重新申请一段新的内存,将源对象指针所指向的内存内容拷贝到新对象中。浅拷贝就会简单的进行指针赋值。

四、什么是友元

一些类会给成员以外的函数或类(即友元函数、友元类)提供访问、操作自身成员变量(包括私有变量和保护变量)的机会,但是这种赋权是单向的(友元类、友元方法可以操作该类的私有成员,但反过来不行)

#include <iostream>
using namespace std;

class BankAccount {
private:
    double balance;  // 私有成员,外部无法直接访问

public:
    BankAccount(double b) : balance(b) {}

    // 声明审计函数为友元(允许访问私有成员)
    friend void audit(const BankAccount& account);
};

// 友元函数定义(可以访问BankAccount的私有成员)
void audit(const BankAccount& account) {
    cout << "[审计] 当前余额: " << account.balance << endl;  // 直接访问私有成员
}

int main() {
    BankAccount acc(1000.0);
    audit(acc);  // 输出: [审计] 当前余额: 1000
    return 0;
}

特性

友元函数

友元类

访问权限

可以访问目标类的私有(private)和保护(protected)成员

该类的所有成员函数均可访问目标类的私有和保护成员

定义方式

在类内使用 friend [返回值] 函数名(参数);声明

在类内使用 friend class 类名;声明

访问方式

需通过对象实例或指针/引用访问成员(如 obj.private_var

可直接访问目标类成员(无需通过实例)

关系方向

单向:目标类成员函数不能访问友元函数的局部变量

单向:目标类成员函数不能访问友元类的私有成员(除非友元类反向声明)

典型应用场景

1. 运算符重载(<</>>)2. 工具函数3. 单元测试辅助函数

1. 紧密协作的类(如容器-迭代器)2. 工厂模式3. 复杂系统内部模块

五、基类的构造函数、析构函数能否被派生类继承?

不能。首先二者名字就不一样,其次基类与派生类的资源管理需求也不一样,构造函数、析构函数有其自身的调用机制。

六、初始化列表和构造函数初始化的区别?

初始化列表会先于构造函数执行,也就是在定义阶段执行const或引用类型成员初始化时,只能用初始化列表的方式;以及派生类对基类传递值进行初始化的时候;以及没有默认构造函数的成员类(因为如果不进行显示初始化,编译器将自动调用其默认构造函数,但是没有默认构造函数,然后就会报错)。

七、类的成员变量的初始化顺序

按在类中声明的顺序依次初始化,与初始化列表的顺序无关。

若使用构造函数初始化,则与构造函数中的初始化顺序有关。

在 c++11 以前,普通变量成员不能进行类内初始化;c++11 之后可以,类内初始化会被编译器合并到构造函数的初始化列表中,若同时存在类内初始化和构造函数初始化列表,后者会覆盖前者

在 c++17 以前,静态成员必须在类外单独初始化;c++17 引入 内联静态成员,允许在类内初始化:

class MyClass {
    static int count;       // 声明
    static constexpr float PI = 3.14f;  // 声明+初始化
};
int MyClass::count = 0;    // 必须类外定义
class MyClass {
    static inline int count = 0;  // 声明+定义+初始化
    static constexpr float PI = 3.14f;  // 同C++11
};
// 无需类外定义!

底层原理

  • inline告诉编译器:
  • 该变量的定义可以出现在多个翻译单元(.cpp文件)中。
  • 链接器会合并所有重复定义,最终只保留一个实例,在程序全局数据区有唯一地址。

八、public 继承、protected 继承、private 继承的区别?

公有继承会向下传递,公有部分在其子孙派生类中将一直是公有部分。

保护继承会使得只能通过其最近派生类访问被继承的类。基类的公有部分和保护部分全都将变为派生类的保护部分。

私有继承使基类的一切都将变为私有,在第三代的时候会导致完全屏蔽,第三代只能通过第二代的公有接口访问基类的公有接口,进而访问基类的私有成员。

九、虚继承

解决棱形继承的问题。避免对爷爷成员变量访问的二义性。会由编译器选择一个中间类的虚基类指针找到基类的数据。

内存布局:

普通继承

D的内存布局:[B::A::data] [C::A::data] [D的成员](两份 data)。

虚继承

D的内存布局:[虚基类指针] [B的成员] [C的成员] [A::data] [D的成员](只有一份 data)。

十、C++ 类内可以定义引用数据成员吗?

可以,必须通过成员函数初始化列表初始化。

十一、泛型编程是什么?

让类型参数化,是程序可以从逻辑功能上抽象,把被处理对象的类型作为参数传递。c++ 里面主要通过模板实现(函数模板、类模板)。类模板可以进行 类型参数化值参数化

template <typename T, int size>  // 使用值参数 size

class Array {
private:
    T arr[size];
public:
    T& operator[](int index) {
        return arr[index];
    }
}

模板特化(模板函数、模板类):

template <typename T>
void print(T value) {
    cout << "Generic: " << value << endl;
}

//针对 int类型的特化
template
void print<int>(int value) {
    cout << "Specialized for int: "<< value << endl;
}

int main() {
    print(10);  // 调用特化版本
    print(3.14);  // 调用通用版本
    return 0;
}


template <typename T>
class Box {
public:
    void display() {
        cout << "Generic Box" << endl;
    }
}

//针对 int类型的特化
template
class Box<int> {
public:
    void display() {
        cout << "Specialized Box for int" << endl;
    }
}

十二、什么是右值,跟左值又有什么区别?

左值和右值的概念:

左值:能对表达式取地址、或具名的对象/变量。一般指表达式结束后依然存在的持久对象。

右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

右值和左值的区别:

1、左值可以寻址,而右值不可以。

2、左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

3、左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

4、左值不能直接移动(通过std::move将左值强制转换为右值(将亡值),允许资源转移(如指针交换),避免深拷贝。),右值可以被直接移动。

十三、左值引用 与 右值引用

左值引用绑定到一个可修改的对象,通过左值引用,可以访问原始对象并修改它。

右值引用用于绑定到 右值,即表示 临时对象不可修改的值。右值引用最常用的场景是 移动语义,它允许 资源从一个对象转移到另一个对象,从而避免不必要的拷贝,提高效率。

十四、完美转发

std::forward() 能够保持参数传递时的类别不变(左值还是右值),能够在一个函数里面将参数完美精确、原封不动转发到另一个函数中,避免不必要的拷贝和移动。主要是为了当一个参数以右值引用的方式传递进一个函数,如果在这个函数里面又将这个参数传递给另外一个函数,就会变成左值传递,因为在第一个函数里面参数已经是一个有名字的引用了,在第一个函数内部表现为左值。

只有模板可以自动进行引用折叠推导:可以保持传递参数的类型。

左值引用折叠:T& & 折叠为 T&;T&& & 折叠为 T &;T& && 折叠为 T&。

右值引用折叠:T&& && 折叠为 T&&。

#include <iostream>
#include <utility>  // for std::forward

// 下层处理函数(验证值类别)
void process(int& x) { 
    std::cout << "处理左值: " << x << std::endl; 
}
void process(int&& x) { 
    std::cout << "处理右值: " << x << std::endl; 
}

// 包装函数(完美转发)
template <typename T>
void wrapper(T&& arg) {
    std::cout << "包装函数接收到: ";
    if constexpr (std::is_lvalue_reference_v<T&&>) {
        std::cout << "左值" << std::endl;
    } else {
        std::cout << "右值" << std::endl;
    }
    process(std::forward<T>(arg));  // 关键:完美转发
}

int main() {
    int x = 42;
    wrapper(x);       // 传递左值
    wrapper(100);     // 传递右值
    wrapper(std::move(x)); // 传递将亡值(右值)
    return 0;
}

十五、 C++ 中空类默认产生哪些类成员函数?

#include <iostream>

class EmptyClass {
public:
    // 默认构造函数
    EmptyClass() { std::cout << "Default constructor called\n"; }

    // 默认析构函数
    ~EmptyClass() { std::cout << "Destructor called\n"; }

    // 复制构造函数
    EmptyClass(const EmptyClass& other) { std::cout << "Copy constructor called\n"; }

    // 拷贝赋值运算符
    EmptyClass& operator=(const EmptyClass& other) {
        std::cout << "Copy assignment operator called\n";
        return *this;
    }

    // 移动构造函数
    EmptyClass(EmptyClass&& other) noexcept { std::cout << "Move constructor called\n"; }

    // 移动赋值运算符
    EmptyClass& operator=(EmptyClass&& other) noexcept {
        std::cout << "Move assignment operator called\n";
        return *this;
    }
};

1、如果定义了析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符中的任意一个,编译器会认为你可能需要手动管理资源,因此不再自动生成移动语义相关的函数(但保留拷贝语义的函数)。

2、如果定义了拷贝构造函数或拷贝赋值运算符。

不会自动生成:移动构造函数、移动赋值运算符。

仍然自动生成:默认构造函数、拷贝赋值运算符、析构函数。

3、如果定义了移动构造函数或移动赋值运算符

不会自动生成:拷贝构造函数、拷贝赋值运算符(因为移动语义通常意味着资源独占)。

仍然自动生成:默认构造函数、析构函数。

十六、多态的三个条件

继承、虚函数、基类指针/引用指向子类对象实例。

十七、静态多态与动态多态

静态多态是在编译时通过函数重载模板机制实现的多态性。这意味着多态行为在编译阶段就已确定。动态多态是在运行时通过虚函数机制实现的多态。它允许基类指针或引用在运行时调用派生类的函数。

特性

静态绑定

动态绑定

绑定时间

编译时

运行时

适用情况

非虚函数、函数重载、模板实例化

虚函数(多态性)

运行效率

高(无需运行时决定)

稍慢(需查找虚函数表)

灵活性

较低

高(支持多态)

示例方法

void func()

virtual void func()

十八、静态成员函数与普通成员函数的区别?

特性

静态成员函数

普通成员函数

调用方式

通过类名调用,无需创建对象

必须通过对象或指针调用

访问权限

只能访问静态成员变量和静态成员函数

可以访问所有成员,包括静态和非静态成员

存储方式

共享内存,不影响对象大小

所有对象共享同一份函数代码,不单独占用对象内存

this指针

没有 this指针

有隐含的 this指针

类作用域

属于类的作用域,可以直接访问静态成员

属于对象的作用域,需要通过对象访问

十九、运算符重载

返回值类型 operator 被重载的运算法 ( 形参列表 ){}

/* 不能重载的运算符:
    ::    (作用域解析运算符)
    .      (成员访问运算符)
    .*     (成员指针运算符)
    sizeof (大小运算符)
    typeid (类型识别运算符)
    ?:     (条件运算符)

二十、虚函数指针 与 虚函数表

当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类对象便会包含一张虚函数表。而当一个子类继承并重写了基类的虚函数时,它的实例也会有自己的一张虚函数表。

当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
            void func1();
            void func2();
private:
    int m_data1, m_data1;
};

class B : public A {
public:
    virtual void vfunc1();
            void func2();
private:
    int m_data3;
};

class C : public B {
public:
    virtual void vfunc1();
            void func2();
private:
    int m_data1, m_data4;
};

父类指针指向子类对象时的函数调用规则:

1、调用虚函数时 通过对象的虚函数指针(vptr)找到子类的虚函数表(vtable)。 根据函数在虚函数表中的偏移量定位具体实现。 最终调用的是子类重写的版本(如果子类有重写)。

2、调用非虚函数时 直接静态绑定到父类的函数实现。 完全忽略对象的实际类型(子类),仅根据指针的声明类型(父类)决定调用。

C++/嵌入式开发 秋招面经 文章被收录于专栏

一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。

全部评论

相关推荐

评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务