迈向 C++ 语言律师之路
迈向 C++ 语言律师之路
1. 现代 C++ 语法特性
1.1 Attributes(属性)
C++11 引入了标准化的属性语法,用于向编译器提供额外的信息。
// C++14: deprecated 属性
[[deprecated("This function is deprecated. Use newFunction() instead.")]]
void oldFunction() {
std::cout << "This is the old function." << std::endl;
}
// C++17: nodiscard 属性
[[nodiscard]] int calculate() {
return 42;
}
// C++17: maybe_unused 属性
void someFunction([[maybe_unused]] int param) {
// param 可能未被使用,但不会产生警告
}
// C++20: likely/unlikely 属性
int processData(int x) {
if (x > 0) [[likely]] {
return x * 2;
} else [[unlikely]] {
return 0;
}
}
// C++20: no_unique_address 属性
struct Empty {};
struct S {
[[no_unique_address]] Empty e;
int data;
};
常用属性总结:
-
[[deprecated]]:标记废弃的函数或类 -
[[nodiscard]]:返回值不应被忽略 -
[[maybe_unused]]:可能未使用的变量/参数 -
[[likely]]/[[unlikely]]:分支预测提示 -
[[no_unique_address]]:空基类优化
1.2 引用限定成员函数(Ref-qualified Member Functions)
C++11 允许成员函数根据对象的值类别进行重载。
struct Bar {
int value = 42;
};
struct Foo {
// 左值引用版本:返回拷贝,保持原对象不变
Bar getBar() & {
std::cout << "lvalue version\n";
return bar;
}
// const 左值引用版本
Bar getBar() const & {
std::cout << "const lvalue version\n";
return bar;
}
// 右值引用版本:可以移动,因为对象即将销毁
Bar getBar() && {
std::cout << "rvalue version\n";
return std::move(bar);
}
// const 右值引用版本
Bar getBar() const && {
std::cout << "const rvalue version\n";
return bar;
}
private:
Bar bar;
};
void testRefQualified() {
Foo foo{};
Bar bar1 = foo.getBar(); // 调用 & 版本
const Foo cfoo{};
Bar bar2 = cfoo.getBar(); // 调用 const & 版本
Bar bar3 = Foo{}.getBar(); // 调用 && 版本
Bar bar4 = std::move(foo).getBar(); // 调用 && 版本
Bar bar5 = std::move(cfoo).getBar(); // 调用 const && 版本
}
1.3 Placement New
在指定内存位置构造对象,不分配新内存。
#include <iostream>
#include <string>#
#include <new>#
// 基本用法
void basicPlacementNew() {
alignas(std::string) char buffer[sizeof(std::string)];
// 在指定位置构造对象
std::string* str = new(buffer) std::string("Hello, Placement New!");
std::cout << *str << std::endl;
// 手动调用析构函数
str->~string();
}
// 联合体中的应用
union ComplexData {
int intValue;
float floatValue;
std::string strValue;
ComplexData() {}
~ComplexData() {}
};
void unionExample() {
ComplexData data;
// 在联合体中构造 string
new(&data.strValue) std::string("Hello, Complex Union");
std::cout << "strValue: " << data.strValue << std::endl;
// 切换到 int,需要先析构 string
data.strValue.~string();
data.intValue = 42;
std::cout << "intValue: " << data.intValue << std::endl;
}
// 内存池应用
template<typename T, size_t N>
class SimplePool {
alignas(T) char storage[N * sizeof(T)];
bool used[N] = {false};
public:
template<typename... Args>
T* construct(Args&&... args) {
for (size_t i = 0; i < N; ++i) {
if (!used[i]) {
used[i] = true;
return new(storage + i * sizeof(T)) T(std::forward<Args>(args)...);
}
}
return nullptr;
}
void destroy(T* ptr) {
size_t index = (reinterpret_cast<char*>(ptr) - storage) / sizeof(T);
if (index < N && used[index]) {
ptr->~T();
used[index] = false;
}
}
};
1.4 尾置返回类型(Trailing Return Type)
解决复杂返回类型和模板推导问题。
/*
1. **返回类型依赖于参数**
当返回类型需要根据函数参数推导时,尾置返回类型可以访问参数列表中的类型。最典型的例子是模板函数中使用`auto`结合`decltype`:
这里,`decltype(t + u)`需要知道`t`和`u`的类型,而传统返回类型语法无法在函数名前访问参数列表。
*/
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
// C++14 简化版本
template <typename T, typename U>
auto add_modern(T t, U u) {
return t + u; // 自动推导
}
/*
2. **复杂返回类型简化可读性**
对于复杂的返回类型(如嵌套模板或函数指针),尾置语法可以使函数头部更清晰:
*/
auto createAdder() -> std::function<int(int, int)> {
return [](int a, int b) { return a + b; };
}
/*
3. **lambda表达式的显式返回类型**
当lambda表达式的返回类型需要显式指定时,必须使用尾置语法:
*/
template<typename T>
auto getMemberFunction() -> int (T::*)() const {
return &T::getValue;
}
/*
4. **元编程与类型推导**
在模板元编程中,尾置返回类型常用于结合`decltype`进行复杂的类型推导:
*/
template <typename Container>
auto getElement(Container& c, size_t index)
-> decltype(c[index]) {
return c[index];
}
// 5. SFINAE 应用
template<typename T>
auto hasSize(T&& t) -> decltype(t.size(), std::true_type{}) {
return {};
}
template<typename T>
std::false_type hasSize(...) {
return {};
}
1.5 ADL(Argument-Dependent Lookup)
也称为 Koenig 查找,根据参数类型在相应命名空间中查找函数。
#include <iostream>
#include <vector>#
namespace Library {
struct Point {
double x, y;
Point(double x, double y) : x(x), y(y) {}
};
// 在 Point 所在命名空间定义相关函数
void print(const Point& p) {
std::cout << "Point(" << p.x << ", " << p.y << ")\n";
}
Point operator+(const Point& a, const Point& b) {
return Point(a.x + b.x, a.y + b.y);
}
}
// 模板函数利用 ADL
template <typename T>
void debugPrint(const T& obj) {
print(obj); // ADL 会在 T 的命名空间中查找 print
}
template <typename T>
void processContainer(const std::vector<T>& vec) {
for (const auto& item : vec) {
print(item); // ADL 查找
}
}
// ADL 的经典应用:swap
namespace MySpace {
struct MyType {
int value;
MyType(int v) : value(v) {}
};
void swap(MyType& a, MyType& b) {
std::cout << "Custom swap called\n";
std::swap(a.value, b.value);
}
}
template<typename T>
void genericSwap(T& a, T& b) {
using std::swap; // 引入 std::swap 作为备选
swap(a, b); // ADL 会优先查找 T 命名空间中的 swap
}
void testADL() {
Library::Point p1(1.0, 2.0);
Library::Point p2(3.0, 4.0);
debugPrint(p1); // 调用 Library::print
auto p3 = p1 + p2; // ADL 查找 operator+
debugPrint(p3);
MySpace::MyType obj1(10);
MySpace::MyType obj2(20);
genericSwap(obj1, obj2); // 调用 MySpace::swap
}
1.6 SIMD 向量化优化
编译器自动向量化技术,利用 CPU 的 SIMD 指令集。
#include <vector>
#include <numeric#>
#include <algorith#m>
#include <execution>#
// 1. 基础向量化示例
void basicVectorization() {
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 1);
// 编译器可能向量化的循环
int sum = 0;
for (size_t i = 0; i < data.size(); ++i) {
sum += data[i]; // 可能被向量化为 4 个元素并行计算
}
// 更好的方式:使用标准算法
int sum2 = std::accumulate(data.begin(), data.end(), 0);
// C++17 并行算法
int sum3 = std::reduce(std::execution::par_unseq,
data.begin(), data.end(), 0);
}
// 2. 向量化友好的代码
void vectorFriendlyCode() {
std::vector<float> a(1000), b(1000), c(1000);
// 向量化友好:连续内存访问
for (size_t i = 0; i < a.size(); ++i) {
c[i] = a[i] + b[i]; // 编译器容易向量化
}
// 更好的方式:使用标准算法
std::transform(std::execution::par_unseq,
a.begin(), a.end(),
b.begin(), c.begin(),
std::plus<float>{});
}
// 3. 编译器指令提示
void compilerHints() {
constexpr size_t N = 1000;
float a[N], b[N], c[N];
// GCC/Clang 向量化提示
#pragma GCC ivdep
for (size_t i = 0; i < N; ++i) {
c[i] = a[i] * b[i] + 1.0f;
}
// 显式 SIMD(需要编译器支持)
#pragma omp simd
for (size_t i = 0; i < N; ++i) {
c[i] = std::sqrt(a[i] * a[i] + b[i] * b[i]);
}
}
1.7 四种类型转换详解
C++ 提供四种显式类型转换,各有特定用途。
#include <iostream>
#include <memory>#
// 基类和派生类用于演示
class Base {
public:
virtual ~Base() = default;
virtual void speak() { std::cout << "Base speaking\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived speaking\n"; }
void derivedMethod() { std::cout << "Derived method\n"; }
};
void demonstrateCasts() {
// 1. static_cast:编译时类型转换
{
int i = 42;
double d = static_cast<double>(i); // 数值转换
// 指针转换(上行转换总是安全的)
std::unique_ptr<Derived> derived = std::make_unique<Derived>();
std::unique_ptr<Base> base = std::move(derived); // 隐式转换
// 下行转换(需要程序员保证安全性)
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 不安全但快速
derivedPtr->derivedMethod();
delete basePtr;
}
// 2. dynamic_cast:运行时类型检查
{
Base* basePtr = new Derived();
// 安全的下行转换
if (Derived* derivedPtr = dynamic_cast<Derived*>(basePtr)) {
derivedPtr->derivedMethod(); // 转换成功
} else {
std::cout << "Cast failed\n";
}
// 引用版本(失败时抛异常)
try {
Base& baseRef = *basePtr;
Derived& derivedRef = dynamic_cast<Derived&>(baseRef);
derivedRef.derivedMethod();
} catch (const std::bad_cast& e) {
std::cout << "Cast failed: " << e.what() << '\n';
}
delete basePtr;
}
// 3. const_cast:修改 const/volatile 属性
{
const int ci = 42;
int& modifiable = const_cast<int&>(ci);
// modifiable = 100; // 未定义行为!不要修改原本是 const 的对象
// 正确用法:移除指向非 const 对象的 const 指针的 const
int original = 42;
const int* cptr = &original;
int* ptr = const_cast<int*>(cptr);
*ptr = 100; // 这是安全的,因为原始对象不是 const
std::cout << "Modified: " << original << '\n';
}
// 4. reinterpret_cast:低级别重新解释
{
int i = 0x12345678;
// 将整数重新解释为字节数组
char* bytes = reinterpret_cast<char*>(&i);
std::cout << "Bytes: ";
for (size_t j = 0; j < sizeof(int); ++j) {
std::printf("%02x ", static_cast<unsigned char>(bytes[j]));
}
std::cout << '\n';
// 指针与整数之间的转换
void* vptr = &i;
uintptr_t addr = reinterpret_cast<uintptr_t>(vptr);
void* vptr2 = reinterpret_cast<void*>(addr);
std::cout << "Address: " << vptr << " == " << vptr2 << '\n';
}
}
2. C++ 内部机制深度解析
2.1 引用的底层实现
在 C++ 中,当我们说”引用”时,我们通常不会说它被”拷贝”,因为引用本身并不占用任何存储空间,它只是一个别名。当你将一个对象作为引用传递给函数时,实际上并没有发生任何拷贝操作。函数接收的是对原始对象的直接引用,而不是任何形式的拷贝。
然而,从底层实现的角度来看,引用在某种程度上可以被视为一个常量指针。当你创建一个引用并将其初始化为一个对象时,编译器会在底层创建一个指向该对象的常量指针。这个指针在初始化后就不能改变,它将一直指向初始化时的那个对象。因此,当你通过引用访问对象时,实际上是通过这个常量指针访问的。
但是,这并不意味着引用是通过拷贝指针来实现的。引用的实现细节可能因编译器和平台的不同而不同。
#include <iostream>
// 引用的实现机制演示
void referenceImplementation() {
int value = 42;
int& ref = value; // 编译器通常实现为 int* const ref = &value;
// 从汇编角度看,以下两种访问方式不同:
value = 100; // 直接内存访问:mov DWORD PTR [rbp-4], 100
ref = 200; // 间接访问:mov rax, [rbp-8]; mov DWORD PTR [rax], 200
std::cout << "value: " << value << ", ref: " << ref << '\n';
}
2.2 栈内存管理机制
如果说 a 和 b 都在栈上,那怎么取 b 的值呢?需要每一次取值都经历出栈和入栈吗?
int main() {
int a = 1;
int b = 2;
// ...
return 0;
}
在函数执行过程中,局部变量 a 和 b 都存储在栈上。栈是一个连续的内存区域,局部变量在栈帧中按顺序分配。取变量 b 的值并不需要每次都经历出栈和入栈操作,而是通过栈帧中的偏移量直接访问。
main:
push ebp ; 保存调用者的基址指针
mov ebp, esp ; 设置当前栈帧的基址指针
sub esp, 8 ; 为局部变量 a 和 b 分配 8 字节的空间
mov DWORD PTR [ebp-4], 1 ; 将 1 存储到 a
mov DWORD PTR [ebp-8], 2 ; 将 2 存储到 b
; 访问 b 的值
mov eax, DWORD PTR [ebp-8] ; 将 b 的值加载到 eax 寄存器
mov esp, ebp ; 恢复栈指针
pop ebp ; 恢复基址指针
ret ; 返回调用者
2.3 编译器如何处理自定义类型
#include <iostream>
#include <cstddef>#
// 演示编译器如何处理结构体
struct ComplexStruct {
char c; // offset 0
int i; // offset 4 (aligned)
double d; // offset 8 (aligned)
char c2; // offset 16
// padding to 24 for next object alignment
};
// 包装结构体(取消对齐)
#pragma pack(push, 1)#
struct PackedStruct {
char c; // offset 0
int i; // offset 1
double d; // offset 5
char c2; // offset 13
// total size: 14
};
#p#ragma pack(pop)
// 模拟编译器生成的访问代码
void demonstrateStructLayout() {
ComplexStruct obj;
// 编译器生成类似这样的偏移计算:
// obj.c -> base_address + 0
// obj.i -> base_address + 4
// obj.d -> base_address + 8
// obj.c2 -> base_address + 16
char* base = reinterpret_cast<char*>(&obj);
std::cout << "ComplexStruct layout:\n";
std::cout << "Total size: " << sizeof(ComplexStruct) << '\n';
std::cout << "c offset: " << (reinterpret_cast<char*>(&obj.c) - base) << '\n';
std::cout << "i offset: " << (reinterpret_cast<char*>(&obj.i) - base) << '\n';
std::cout << "d offset: " << (reinterpret_cast<char*>(&obj.d) - base) << '\n';
std::cout << "c2 offset: " << (reinterpret_cast<char*>(&obj.c2) - base) << '\n';
PackedStruct packed;
std::cout << "\nPackedStruct size: " << sizeof(PackedStruct) << '\n';
}
// 虚函数表的内存布局
class BaseWithVirtual {
public:
virtual void func1() { std::cout << "Base::func1\n"; }
virtual void func2() { std::cout << "Base::func2\n"; }
virtual ~BaseWithVirtual() = default;
private:
int data = 42;
};
class DerivedWithVirtual : public BaseWithVirtual {
public:
void func1() override { std::cout << "Derived::func1\n"; }
void func3() { std::cout << "Derived::func3\n"; }
private:
double extraData = 3.14;
};
void demonstrateVTable() {
// 对象内存布局:[vptr][BaseWithVirtual::data][DerivedWithVirtual::extraData]
DerivedWithVirtual obj;
// 获取 vptr(虚函数表指针)
void** vptr = *reinterpret_cast<void***>(&obj);
std::cout << "Object address: " << &obj << '\n';
std::cout << "VTable address: " << vptr << '\n';
// 通过基类指针调用虚函数
BaseWithVirtual* basePtr = &obj;
basePtr->func1(); // 动态分发到 DerivedWithVirtual::func1
}
99. quiz
1. c++中 NULL 和 nullptr 的区别
在 C 语言里,NULL 一般借助宏定义为#define NULL ((void *)0)。从本质上来说,NULL 代表的是一个空指针。下面这段代码在 C 语言环境中是能够正常编译的:
#def#ine NULL ((void *)0)
int *pi = NULL;
char *pc = NULL;
这是由于在 C 语言中,当把void*类型的空指针赋值给int*或者char*类型的指针时,会自动进行隐式类型转换,从而将void*转换为对应的指针类型。不过,如果使用 C++编译器来编译上述代码,就会出现错误。这是因为 C++属于强类型语言,它不允许将void*类型的指针隐式转换为其他类型的指针。所以,在 C++环境中,编译器提供的头文件对 NULL 的定义进行了调整:
#ifdef# __cplusplus
#define NULL 0#
#else#
#defi#ne NULL ((void *)0)
#endif#
然而,把 NULL 定义为 0 会引发函数重载时的类型匹配问题。例如下面的代码:
void foo(int);
void foo(char*);
foo(NULL); // 这里会调用哪个函数呢?
在 C++里,当执行foo(NULL)这一调用时,实际上传递的实参是整数 0。因为 NULL 被定义成了 0,所以它会优先匹配参数类型为int的重载函数,而不是参数类型为char*的函数。这种情况往往和程序员的预期不符,容易引发隐藏的错误。为了避免这类问题,在 C++11 及后续的版本中,建议使用nullptr来表示空指针。nullptr是一种特殊的空指针类型,能够隐式转换为任意类型的指针,而且不会和整数类型产生混淆。使用nullptr可以让代码的意图更加清晰明确:
2. 一个通过new创建出来的指针,若被delete两次会怎样?
如果一个指针被delete两次,会引发未定义行为(Undefined Behavior)。这是因为在第一次执行delete之后,该指针所指向的已不再是有效的内存区域。再次尝试对其执行delete操作,实际上是对无效内存进行操作,这在程序运行规则中是不允许的。
未定义行为可能引发多种不良后果,其中包括但不限于以下情况:
- 程序崩溃:操作系统或运行时环境检测到非法内存访问,从而强制终止程序运行。
- 数据损坏:对无效内存的操作可能会改写其他合法数据,导致程序后续逻辑出现错误。
- 出现难以预测和解释的行为:由于未定义行为不受特定规则约束,程序的运行结果可能在不同的编译环境、运行环境甚至不同的运行时刻都有所不同,给调试和维护带来极大困难。
为了避免此类情况发生,在编写代码时,务必保证每个new操作都有且仅有一个对应的delete操作,并且每个delete操作仅执行一次。在 C++ 代码中,std::unique_ptr类通过封装new和delete操作,实现了对资源的自动管理,有效避免了这种因重复释放指针而引发的问题。
3. 为什么在 delete 之后,通常都会将指针设置为 nullptr
在 C++编程中,当使用delete释放一个指针所指向的内存后,通常会将该指针设置为nullptr,这主要基于以下几方面原因:
-
防止产生悬挂指针:当使用
delete释放指针所指向的内存后,该指针就成为了悬挂指针(Dangling Pointer)。此时它虽不再指向有效的内存区域,但仍保留着之前的地址值。若后续不小心再次使用这个悬挂指针,就会引发未定义行为,可能导致程序崩溃或出现难以排查的错误。而将指针设置为nullptr,能有效避免这种情况,因为nullptr是一个特殊指针值,表示该指针不指向任何对象,使用指向nullptr的指针进行操作(如解引用)会在大多数情况下引发明确的运行时错误,便于开发者定位问题。 -
安全地重复执行 delete 操作:在 C++语言规则中,对
nullptr执行delete操作是安全的,不会产生任何实际效果。因此,若将已释放内存的指针设置为nullptr,后续即便不小心再次尝试对其执行delete操作,也不会导致未定义行为,从而增强了程序的健壮性。 -
方便检查指针是否已被释放:将指针设置为
nullptr后,通过简单检查该指针是否等于nullptr,就能清晰判断它所指向的内存是否已经被释放。这在复杂的代码逻辑中,对于追踪指针状态、确保内存管理的正确性非常有帮助。
综上所述,在使用delete释放指针后,将其设置为nullptr是一种良好的编程习惯,有助于提高程序的安全性与稳定性,减少因内存管理不当而引发的错误。
5. 静态绑定和虚函数
#include <iostream>
struct A {
virtual void foo (int a = 1) {
std::cout << "A" << a;
}
};
struct B : A {
virtual void foo (int a = 2) {
std::cout << "B" << a;
}
};
int main () {
A *b = new B;
b->foo(); // B1
}
4. 通过指针访问和直接访问的区别是什么?
struct Point3d {
double x;
};
Point3d origin;
Point3d* pt = &origin;
origin.x = 0.0; // (1)
pt->x = 0.0; // (2)
(1) 和 (2) 这两种方式均用于将 Point3d 对象 origin 的 x 成员值设置为 0.0 。它们的差异在于访问方式不同,从编译器层面来看:
- (1)
origin.x = 0.0;这种方式是直接借助对象名与成员名来访问并修改成员值。编译器在编译阶段就能确定origin的内存地址,随后依据偏移量定位到x成员的位置,直接在此处写入0.0。 - (2)
pt->x = 0.0;该方式则是通过指向对象的指针来访问与修改成员值。pt是指向origin的指针,pt->x代表pt所指向对象的x成员。在编译时,编译器无法确定pt所指向的内存地址,因为这一地址是在运行时由操作系统分配的。所以在运行时,编译器需要先读取pt的值(即origin的地址),接着通过偏移量找到x成员的位置,最后在此处写入0.0。
总体而言,这两种实现效果一致,都能将 origin 的 x 成员值设为 0.0 。但是(1)会比(2)要好,(2)会有运行时开销。
6. 一般哪些函数可以使用 noexcept?
在 C++ 中,noexcept关键字用于表明一个函数不会抛出任何异常。这对于提升代码的性能与可靠性有着重要意义。以下是几类常见可使用noexcept的函数:
- 析构函数:析构函数一般不应抛出任何异常。这是因为若在析构函数中抛出异常,极有可能致使程序出现未定义行为。例如,当对象被自动销毁或容器中的对象析构时,若析构函数抛出异常,程序的后续行为将无法预测,可能导致崩溃或数据损坏等严重问题。
- 移动构造函数与移动赋值操作符:这些函数通常应标记为
noexcept。因为它们大多仅涉及指针和基本类型的转移,正常情况下不应抛出异常。而且,标记为noexcept的移动操作能被标准库容器(如std::vector)更高效地运用。比如,当std::vector进行扩容或重新分配内存时,若移动操作标记为noexcept,std::vector可以采用更优化的策略,避免不必要的复制操作,从而显著提升性能。 - 交换函数:像
std::swap这类交换函数,通常也应标记为noexcept。这是由于它们通常仅涉及指针和基本类型的转移,正常情况下不会抛出异常。例如,在进行两个对象的交换操作时,仅仅是交换它们内部的指针或基本数据成员,这种操作的稳定性较高,不易引发异常。 - 其他不会抛出异常的函数:倘若你明确知晓某个函数不会抛出异常,那么就应当使用
noexcept关键字。这不仅有助于编译器对代码进行优化,例如,编译器可能会针对noexcept函数进行特定的优化,生成更高效的机器码;同时也能向其他开发者清晰地表明该函数不会引发异常,使得代码的行为更加明确和可预测。
7. c++怎么定义隐式转换规则
在 C++ 中,隐式转换(也称为自动类型转换)由编译器自动执行。这些转换规则由 C++ 语言明确规定,例如从 int 到 double 的转换,或者从派生类到基类的转换。 然而,你也可以为自定义类型定义隐式转换规则。这可以通过定义转换函数来达成。转换函数是一种特殊的成员函数,它能够将一个类的对象转换为其他类型的对象。 例如,假设你有一个名为 MyClass 的类,并且希望它能隐式转换为 int 类型。你可以在 MyClass 类中定义一个名为 operator int() 的转换函数,如下所示:
class MyClass {
public:
operator int() const {
// 在这里返回一个 int 值
return 0;
}
};
随后,你便可以像这样使用它:
MyClass obj;
int i = obj; // 隐式转换
需要注意的是,尽管隐式转换看似方便,但它可能引发一些意想不到的问题。比如,当函数重载时,隐式转换可能会导致编译器选择错误的函数版本。因此,一般建议尽量避免使用隐式转换,或者至少要确保清楚地知道它何时会发生以及会产生什么效果。
8. 多态场景下,调用哪个方法?
在C++中,考虑以下场景:有两个同名函数func(),一个函数的形参是Foo类型指针,另一个函数的形参是Bar类型指针,并且Bar类型是Foo类型的派生类。如果有Foo* bar = new Bar();,然后调用func(bar),那么会调用哪个函数呢?
答案是会调用形参为Foo类型指针的func函数。因为在C++中,这种情况属于函数重载。函数重载是在编译时根据实参的静态类型来确定调用哪个函数版本。在这个例子中,实参bar的静态类型是Foo*,所以编译器会选择调用形参为Foo类型指针的func函数。
需要注意的是,如果想要实现根据对象的实际类型(动态类型)来决定调用哪个函数,即实现多态中的动态绑定,则需要在基类Foo中将相关函数声明为虚函数,并且在派生类Bar中重写该虚函数,这样通过基类指针或引用调用虚函数时,才会在运行时根据对象的实际类型来确定调用哪个函数版本。但本题中未提及虚函数相关内容,所以按照函数重载的规则进行匹配。
9. 不使用 final 怎么实现 final 效果
在 C++ 中,如果希望一个类 B 不可以被继承,可以采用以下方式。首先,定义一个类 A,将 A 的构造函数私有化。然后让类 B 继承自 A,并声明 B 是 A 的友元。这样,B 能够正常构造,因为作为友元,B 可以访问 A 的私有构造函数。然而,当有类 C 试图继承 B 时,由于 C 不是 A 的友元,不能调用 A 的私有构造函数,所以 C 无法继承 B,从而实现了类似 final 的阻止类被继承的效果。以下是具体代码示例:
class A {
private:
A() {} // A 的构造函数私有化
friend class B; // 声明 B 为 A 的友元
};
class B : public A {
public:
B() {} // B 可以正常构造,因为是 A 的友元
};
// 尝试继承 B
// class C : public B {
// public:
// C() {} // 这里会报错,因为 C 不能调用 A 的私有构造函数
// };
在上述代码中,若取消对 class C 定义部分的注释,编译时会报错,提示无法访问 A 的私有构造函数,从而表明 C 不能继承 B,达成了类似 final 关键字阻止类被继承的效果。
10. 函数声明陷阱
#include <iostream>
struct X {
X() { std::cout << "X"; }
};
int main() {
{
// confusing case: 可能会引起误解
X x(); // 这是声明了一个名为x,返回类型为X的函数,还是声明了一个名为x,使用默认构造的对象?
}
{
// 方法1:省略括号(推荐)
X x; // 正确创建对象x,调用默认构造函数
}
{
// 方法2:使用统一初始化语法(C++11及以后)
X x{}; // 同样正确,避免了语法歧义
}
return 0;
}
11. 黑魔法
for (int i = 0; i < n; i++) {
cout << ans[i] << " \n"[i == n - 1];
}
for (unsigned int i = 10; i >= 0; --i) { ... }
std::vector<int> vec;
vec.push_back(1);
for (auto idx = vec.size(); idx >= 0; idx--) {
cout << "===== \n";
}
- inexplicit conversion
#include <iostream>
void print(char const *str) { std::cout << str; }
void print(short num) { std::cout << num; }
int main() {
print("abc");
print(0);
print('A');
}
// 带括号的会被看成左值表达式,被推出引用。
#include <iostream>
int main() {
int a = 0;
decltype((a)) b = a;
b++;
std::cout << a << b;
}
Enjoy Reading This Article?
Here are some more articles you might like to read next: