1. C++中 explicit 的作用
回答重点
关键字explicit 的主要作用是防止构造函数或转换函数在不合适的情况下被隐式调用。
例如,如果有一个只有一个参数的构造函数,加上explicit 关键字后,编译器就不会自动用该构造函数进行隐式转换。这可以避免由于意外的隐式转换导致的难以调试的行为。
cpp
class Foo
{
public:
explicit Foo(int x) : value(x) {}
private:
int value;
};
void func(Foo f)
{
// ...
}
int main()
{
Foo foo = 10; // 错误,必须使用 Foo foo(10) 或 Foo foo = Foo(10)
func(10); // 错误,必须使用 func(Foo(10))
}
如果没有explicit关键字,Foofoo=10;以及func(10);这样的代码是可以通过编译的,这会导致一些意想不到的隐式转换。
扩展知识
1. 历史背景
explicit 关键字在C++98标准中引入,用来增强类型安全,防止不经意的隐式转换。从C++11开始,explicit 可以用于conversion operator。
2. 使用场景
-
防止单参数构造函数隐式转换
如果一个类的构造函数接受一个参数,而你并不希望通过隐式转换来创建这个类的实例,就应该在构造函数前加explicit。这也是它最主要的作用。
cppclass Bar { public: explicit Bar(int x) : value(x) {} private: int value; }; Bar bar = 10; // 错误,无法隐式转换 -
防止conversion operator 隐式转换
类中有时会定义一些转换操作符,但有些转换是需要显式调用的,这时也可以使用
explicit。cppclass Double { public: explicit operator int() const { return static_cast<int>(value); } private: double value; }; Double d; int i = d; // 错误,无法隐式转换 int j = static_cast<int>(d); // 正确,显式转换3. 复杂构造函数
对于那些带有默认参数的复杂构造函数,explicit 尤其重要,它们可能会被意外地调用。
cpp
class Widget
{
public:
explicit Widget(int x = 0, bool flag = true)
: value(x), flag(flag) {}
private:
int value;
bool flag;
};
这种情况下,如果不加explicit,没有任何参数传递给构造函数也可能会进行隐式转换,引发难以察觉的错误。
2. C++中 final 关键字的作用
回答重点
final关键字在C++11中引入,它主要用于防止类被继承或防止虚函数被覆盖。
- 防止类被继承:当一个类被声明为
final,这个类不能被进一步继承。
cpp
class Base final
{
// 类的实现
};
// 下面的代码会导致编译错误
class Derived : public Base
{
// 类的实现
};
- 防止虚函数被覆盖: 当一个虚函数被声明为
final,这个虚函数在派生类中不能被重新定义。
cpp
class Base
{
public:
virtual void doSomething() final
{
// 函数实现
}
};
class Derived : public Base
{
public:
// 下面的代码会导致编译错误
virtual void doSomething() override
{
// 函数实现
}
};
扩展知识
通过final关键字,代码在设计初期就可明确意图,避免了不必要的继承操作和函数重写。
-
设计意图清晰:使用
final明确表明某些部分不应该被更改,有助于其他开发者理解类的设计意图,减少误用。 -
性能优化:在某些情况下,编译器可以利用final的信息进行优化,例如在调用
final函数时可以直接展开,减少虚函数调用的开销。 -
与其它
C++11特性的结合使用:
override关键字: final 通常和 override 关键字一起使用,可以显式指出该函数是覆盖基类中的某个虚函数,并且不允许再被派生类覆盖。例如,结合override和final:
cpp
class Base {
public:
virtual void displayMessage() const {
std::cout << "Base class message" << std::endl;
}
};
class Derived : public Base {
public:
void displayMessage() const override final {
std::cout << "Derived class message" << std::endl;
}
};
// 下面的代码会导致编译错误
class MoreDerived : public Derived {
public:
void displayMessage() const override {
std::cout << "MoreDerived class message" << std::endl;
}
};
3. C++ 中野指针和悬挂指针的区别
回答重点
两者都可能导致程序产生不可预测的行为。但它们有明显的区别:
**1. 野指针:**一种未被初始化的指针,通常会指向一个随机的内存地址。这个地址不可控,使用它可能会导致程序崩溃或数据损坏。
cpp
int *p;
std::cout<< *p << std::endl;
**2. 悬挂指针:**一个原本合法的指针,但指向的内存已被释放或重新分配。当访问此指针指向的内存时,会导致未定义行为,因为那块内存数据可能已经不是期望的数据了。
cpp
int main(void)
{
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
扩展知识
- 如何避免野指针
- 初始化指针:在声明一个指针时,立即赋予它一个明确的数值,可以是一有效的地址,也可以是nullptr。
cpp
int *ptr = nullptr; // 初始化
- 使用智能指针:C++中的智能指针(如
std::unique_ptr和std::shared_ptr)可以帮助自动管理指针的生命周期,减少手动管理的错误。
cpp
std::unique_ptr<int> ptr(new int(10));
- 如何避免悬挂指针
- 在删除对象后,将指针设置为nullptr,确保指针不再指向已经释放的内存。
cpp
delete ptr;
ptr = nullptr;
- 尽量使用智能指针,它们会自动处理指针的生命周期,减少悬挂指针的产生。
cpp
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
{
std::shared_ptr<int> ptr2 = ptr1;
// 当 ptr2 离开作用域后,资源仍然被 ptr1 管理
}
// 仍然可以使用 ptr1
- 检测工具
静态分析工具 (如Clang-Tidy、cppcheck) 和动态分析工具 (Valgrind、AddressSanitizer) 可以帮助检测这些错误,确保代码质量。
4. 什么是内存对齐?为什么要内存对齐?
内存对齐是指计算机在访问内存时,会根据一些规则来为数据指定一个合适的起始地址。
计算机的内存是以字节为基本单位进行编址,但是不同类型的数据所占据的内存空间大小是不一样的,在C++语言中可以用sizeof(来获得对应数据类型的字节数,一些计算机硬件平台要求存储在内存中的变量按照自然边界对齐,也就是说必须使数据存储的起始地址可以整除数据实际占据内存的字节数,这叫做内存对齐。
通常,这些地址是固定数字的整数倍。这样做,可以提高CPU的访问效率,尤其是在读取和写入数据时。为什么要内存对齐?主要有以下几个原因:
-
性能提升:对齐的数据操作可以让CPU在一次内存周期内更高效地读取和写入,减少内存访问次数。
-
硬件限制:某些架构要求数据必须对齐,否则可能会引发硬件异常或需要额外的处理时间。
-
可移植性:代码在不同架构上运行时,遵从内存对齐规则可以减少潜在的问题。