1.6 C/C++ 函数

一、请写个函数在 main 函数执行前或者后执行

#include <stdio.h>
void before() __attribute__((constructor));//设置函数属性
void after() __attribute__((destructor));
void before() {
    printf("this is function %s\n",__func__);
    return;
} 
void after(){
    printf("this is function %s\n",__func__);
    return;
} 
int main(){
    printf("this is function %s\n",__func__);
    return 0;
} /
/ 输出结果
// this is function before
// this is function main
// this is function after

二、为什么析构函数必须是虚函数?

当基类指针指向派生类的时候,在释放基类指针的时候,会先执行派生类的析构函数,再执行基类的析构函数。如果基类析构函数不用 virtual 修饰,则只会调用基类的析构函数,可能会造成内存泄露。因为派生类的析构函数不是虚函数,当派生类继承父类的虚函数表的时候,没有对父类的虚析构函数进行替换。

三、为什么 C++ 默认的析构函数不是虚函数?

因为虚函数需要额外的虚函数指针虚函数表,会占用额外的内存。

四、静态函数和虚函数的区别?

静态函数地址在编译的时候就已经确定,调用的时候会直接跳转到该地址运行;虚函数调用的时候才会进行动态绑定。

静态函数属于类所有,不存在继承和覆盖的概念;虚函数支持继承和覆盖以及重写。

五、重载

重载是一种多个函数之间的平行关系。有 运算符重载、函数模版重载。

规则:

1)方法名必须相同,返回类型可以相同也可以不同,如果函数签名完全相同将导致汇编生成的标号一样,产生链接错误。(特例:除非是 const 成员函数重载)

2)参数列表必须不同,要么数量不同,要么顺序不同,要么类型不同,总之不能产生歧义。

3)在同一个作用域内。

#include <iostream>
using namespace std;

class Foo {
public:
    void bar() const {
        cout << "bar() const" << endl;
    }

    void bar() {
        cout << "bar()" << endl;
    }
};

int main () {
    Foo foo;
    const Foo foo_const;

    foo.bar();  // 输出 bar()
    foo_const.bar();  // 输出 bar() const
    return 0;
}

六、重写

函数重写发生在继承关系中,是一种垂直关系。子类重新实现父类的方法,方法名和参数都必须一样,但实现逻辑不同。

规则: 1)必须有继承关系,没有父子关系就不叫重写。

2)父类方法必须是 virtual。

3)方法名、参数列表、返回值类型都必须相同,需要一摸一样。

4)子类建议使用 override 关键字,可以增强读感。

5)基类虚构函数最好声明为 virtual,避免只调用基类析构函数而造成内存泄漏。

七、 三个陷阱

陷阱一:子类同名函数隐藏父类同名非虚函数

如果子类有和父类同名的函数,则不管参数列表是否相同,父类的函数都会隐藏掉。要想使用父类的同名方法需要用 using 关键字(using 父类 :: 方法名)。

class Base {
public:
    void foo(int x) { cout << "Base::foo(int)" << endl; }
    void foo(double x) { cout << "Base::foo(double)" << endl; }
};

class Derived : public Base {
public:
    void foo(const char* s) { cout << "Derived::foo(const char*)" << endl; }
};

int main() {
    Derived d;
    d.foo("hello");  // ✅ 调用 Derived::foo(const char*)
    d.foo(42);       // ❌ 编译错误:Base::foo(int) 被隐藏
    d.foo(3.14);     // ❌ 编译错误:Base::foo(double) 被隐藏
}



class Derived : public Base {
public:
    using Base::foo; // 引入 Base 的所有 foo 重载
    void foo(const char* s) { cout << "Derived::foo(const char*)" << endl; }
};

int main() {
    Derived d;
    d.foo("hello");  // ✅ Derived::foo(const char*)
    d.foo(42);       // ✅ Base::foo(int)
    d.foo(3.14);     // ✅ Base::foo(double)
}

陷阱二:非虚函数的伪重写

如果父类的函数不是虚函数,通过父类指针调用时,永远调用的是父类版本。

class Base {
public:
    void bar() { cout << "Base::bar()" << endl; } // 非虚函数
};

class Derived : public Base {
public:
    void bar() { cout << "Derived::bar()" << endl; } // 不是真正的重写
};

int main() {
    Derived d;
    Base* pb = &d;

    d.bar();    // ✅ 输出 "Derived::bar()"(子类对象调用子类版本)
    pb->bar();  // ❌ 输出 "Base::bar()"(父类指针调用父类版本)
}

陷阱三:const 成员函数重载,const 也能构成重载条件

规则:非 const 对象优先调用非 const 版本,如果没有则调用 const 版本;const 对象只能调用 const 成员函数版本

八、重载和覆盖(重写)有什么区别?

1、重载是同一个类的同名函数(只是函数特征标不同)之间的关系,是一种水平关系;覆盖是父类和子类的关系,是一种垂直关系。

2、所有重载函数处于同一个作用域内;覆盖是发生在基类和派生类两个不同的作用域内。

3、派生类中重写的函数必须与基类的虚函数具有相同的函数名、参数列表和返回类型。否则,派生类的同名函数将隐藏掉基类的同名函数。

4、重载:采用静态绑定(也叫早绑定),在编译阶段,编译器就会根据调用函数时传递的实参类型和数量,生成一个唯一的标号,确定要调用的具体重载函数。覆盖:使用动态绑定(也叫晚绑定),在运行时,根据对象的实际类型来决定调用哪个函数。这就需要通过基类的指针或引用调用虚函数才能实现。

特征

函数重载(Overload)

函数重写(Override)

发生位置

同一个类内

父子类之间

方法名

必须相同

必须相同

参数列表

必须不同

必须相同

返回值类型

可以不同

必须相同

决定时机

编译时决定

运行时决定

关键词

无特殊关键词

virtual + override

目的

提供多种调用方式

改变父类行为

C++特色

支持操作符重载

需要virtual才能多态

九、 重写 和 隐藏 的区别?

重写:是子类对父类虚函数的重写,父类必须是虚函数。

隐藏:不要求父类的函数是虚函数,但如果子类写了和父类同名的函数,直接调用子类对象的方法会隐藏掉父类所有的同名方法。无论对象实际类型如何,均按变量类型调用(无多态)。

十、 虚函数表具体是怎样实现运行时多态的?

1、当定义一个包含虚函数的类的时候,编译器会为这个类创建一个虚函数表,里面存储了所有虚函数的地址;

2、当创建这个类的实例的时候,编译器会为这个对象设置虚函数表指针指向该类对应的虚函数表

3、当使用基类指针或引用调用虚函数时,编译器会通过对象的虚函数表指针找到对应的虚函数表,然后从表中找到对应的虚函数地址,进而调用该函数。

动态绑定的原因:

#include <iostream>
#include <cstdlib>  // 用于 rand()
using namespace std;

// 基类
class Animal {
public:
    virtual void speak() { cout << "Animal\n"; }
    virtual ~Animal() {} // 虚析构函数(重要!)
};

// 派生类 Cat
class Cat : public Animal {
public:
    void speak() override { cout << "Meow\n"; }
};

// 派生类 Dog
class Dog : public Animal {
public:
    void speak() override { cout << "Woof\n"; }
};

// 工厂函数
Animal* createAnimal() {
    if (rand() % 2) return new Cat();  // 50%概率返回Cat
    else return new Dog();             // 50%概率返回Dog
}

int main() {
    srand(time(0));  // 初始化随机种子

    Animal* pet = createAnimal();
    pet->speak();    // 动态绑定到实际对象类型

    delete pet;      // 通过虚析构函数正确释放内存
    return 0;
}

十一、 什么是内联函数,为什么使用内联函数?需要注意什么?

内联函数是指在函数声明前加上 inline 关键字的函数,它的作用是告诉编译器在调用函数的地方直接将函数体插入,而不是通过函数调用的方式执行。使用内联函数的主要目的是减少函数调用的开销,因为函数的调用会涉及栈帧的创建和销毁、参数传递等操作,而将函数体直接插入调用点则无需进行这些操作。缺点是会增大可执行程序的体积。

内联函数适用于函数体简单、调用频繁的情况。如果函数体较大或调用频率较低,使用内联函数可能会导致代码膨胀,产生更多的代码复制,甚至可能导致性能下降。虚函数不能使用内联函数,因为虚函数的调用是通过虚表进行的,无法在编译时确定调用的具体函数。内联函数是在编译阶段将函数调用的代码直接插入到被调用的地方。二者阶段不同。

十二、为什么虚函数不能是模板函数?

1、模板实例化发生在编译期间:模板函数的实例化是在编译器编译整个程序时发生的。编译器根据模板参数的类型生成特定的函数实例。这意味着,模板函数的实例化是基于类型的,并且所有实例化都会在编译时决定。

2、虚函数的调用依赖于运行时:虚函数的调用依赖于运行时的动态绑定。在运行时,基类指针或引用会根据实际对象的类型调用相应的虚函数。虚函数表是运行时创建并维护的,编译器无法在编译时为虚函数生成固定的实现。

3、虚函数表与模板实例化的冲突:虚函数表存储了类的虚函数地址,并且是基于类的。对于模板函数来说,实例化的个数是基于不同的类型而定的,因此编译器无法为每个实例化生成一个虚函数表。因此,模板函数和虚函数在机制上是相互矛盾的:虚函数依赖于运行时的动态绑定,而模板函数依赖于编译时的类型实例化。

十三、被隐藏的基类函数如何调用?

直接通过作用域解析符调用 基类::方法

十四、为什么静态成员函数不能访问非静态成员?

因为静态成员函数设计的目的是与对象无关。不属于对象实例,而属于类,其没有 this 指针。静态成员函数不能知道或访问与特定对象实例相关的非静态成员。

十五、 Lambda 表达式

本质是 匿名函数对象,也可以取名。

[capture list] (parameter list) option -> return type { function body }
  • capture list(捕获列表):指定 Lambda 表达式可以访问的外部变量,可以捕获 this,& 会隐式捕获 this。

[]

不捕获任何外部变量

[var]

按值捕获 var

[&var]

按引用捕获 var

[=]

按值 捕获所有外部变量

[&]

按引用 捕获所有外部变量

[=, &var]

默认按值 捕获所有变量,var例外按引用捕获

[&, var]

默认按引用 捕获所有变量,var例外按值捕获

  • parameter list(参数列表):Lambda 的参数,就像普通函数的参数一样。(可选)
  • option(函数选项,可选):mutable:允许修改按值捕获的变量。exception:说明 lambda 是否抛出异常及异常类型。attribute:声明属性。
  • return type(返回类型):使用 -> 指定返回值类型。(可选,编译器可推导)
  • function body(函数体):Lambda 表达式的实际执行逻辑。

十六、返回类型后置

允许将函数的返回类型放置在参数列表之后,并通过 -> 关键字进行指定。主要是在模板里面用,因为不知道传入的参数类型,也就没办法先验知道返回类型。

#include <iostream>
using namespace std;

template <typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
    return a * b;
}
int main() {
    cout << multiply(3,4.5)<< endl; //输出:13.5
return 0;
}

十七、 基于范围的 for 循环

基于范围的 for 循环(range-based for loop)允许对容器中的每个元素进行迭代,无需使用迭代器或索引。

for(declaration : container){
    //body of loop
}
// declaration:用于声明一个变量,该变量将逐一绑定到容器中的每个元素。
// 变量可以是值类型(拷贝元素)或引用类型(直接操作元素)。
// container:容器,通常是一个数组、std::vector、std::list 等。

#include <iostream>
#include <map>

using namespace std;

int main () {
    map<string, int> m = {{"apple", 3}, {"banana", 5}, {"cherry", 2}};

    // 基于范围的 for 循环遍历 map
    for (const auto& pair : m) {
        cout << pair.first << ": " << pair.second << endl;
    }

    return 0;
}

for (const auto& [key, value] : map) // 解构pair

十八、仿函数

本质是对象,不是函数:仿函数是类的实例,但可以像函数一样调用。是行为类似函数的对象,通过重载 operator()实现。它既具有函数的调用特性,又能像对象一样存储状态。

struct Adder {
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Adder adder;          // 创建函数对象
    int sum = adder(3, 4); // 调用 operator()
    std::cout << sum;     // 输出: 7
}

十九、std::function 函数包装器

是一个 通用的函数包装器,可以存储、复制和调用任何可调用对象(如普通函数、Lambda、仿函数、成员函数等)。它提供了一种类型安全的方式来实现 回调机制动态函数绑定

std::function<返回值类型(参数类型1, 参数类型2, ...)>

#include <functional>
#include <iostream>

int add(int a, int b) {
    return a + b;
}

// 1、包装普通函数
std::function<int(int, int)> func = add;
std::cout << func(3, 4);  // 输出: 7

// 2、存储 lambda
std::function<int(int, int)> func = [](int a, int b) {
    return a * b;
};
std::cout << func(3, 4);  // 输出: 12

// 3、函数对象
struct Adder {
    int operator()(int x) { return x + 1; } 
};
std::function<int(int)> f = Adder();

// 4、成员函数
class A {
    public: void foo() { ... } 
};
std::function<void()> f = std::bind(&A::foo, obj);

// 5、静态成员函数
class A {
    public: static void bar() { ... } 
};
std::function<void()> f = &A::bar;

实现回调

void process_data(int x, std::function<void(int)> callback) {
    int result = x * 2;
    callback(result);
}

int main() {
    process_data(5, [](int r) { std::cout << "Result: " << r; });
    // 输出: Result: 10
    return 0;
}

二十、析构函数能否抛出异常?

不能。如果析构中断,可能导致部分销毁,造成资源泄漏。另外程序会立即调用 std::terminate() 强制终止程序。

二十一、为什么用成员初始化列表会快一些?

主要是会直接调用成员的拷贝构造函数,而不是先默认构造,再进行赋值。

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

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

全部评论

相关推荐

想踩缝纫机的小师弟练...:不理解你们这些人,要放记录就把对方公司名字放出来啊。不然怎么网暴他们
点赞 评论 收藏
分享
徐徐图之徐徐图之:同一个部门同一个岗位同一个时间同一张感谢信哈哈哈哈
27届求职交流
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

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