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。
| 不捕获任何外部变量 |
| 按值捕获 |
| 按引用捕获 |
| 按值 捕获所有外部变量 |
| 按引用 捕获所有外部变量 |
| 默认按值 捕获所有变量, |
| 默认按引用 捕获所有变量, |
- 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() 强制终止程序。
二十一、为什么用成员初始化列表会快一些?
主要是会直接调用成员的拷贝构造函数,而不是先默认构造,再进行赋值。
一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。