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) |
发生位置 |
同一个类内 |
父子类之间 |
方法名 |
必须相同 |
必须相同 |
参数列表 |
必须不同 |
必须相同 |
返回值类型 |
可以不同 |
必须相同 |
决定时机 |
编译时决定 |
运行时决定 |
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。
