迈向 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类通过封装newdelete操作,实现了对资源的自动管理,有效避免了这种因重复释放指针而引发的问题。

3. 为什么在 delete 之后,通常都会将指针设置为 nullptr

在 C++编程中,当使用delete释放一个指针所指向的内存后,通常会将该指针设置为nullptr,这主要基于以下几方面原因:

  1. 防止产生悬挂指针:当使用delete释放指针所指向的内存后,该指针就成为了悬挂指针(Dangling Pointer)。此时它虽不再指向有效的内存区域,但仍保留着之前的地址值。若后续不小心再次使用这个悬挂指针,就会引发未定义行为,可能导致程序崩溃或出现难以排查的错误。而将指针设置为nullptr,能有效避免这种情况,因为nullptr是一个特殊指针值,表示该指针不指向任何对象,使用指向nullptr的指针进行操作(如解引用)会在大多数情况下引发明确的运行时错误,便于开发者定位问题。

  2. 安全地重复执行 delete 操作:在 C++语言规则中,对nullptr执行delete操作是安全的,不会产生任何实际效果。因此,若将已释放内存的指针设置为nullptr,后续即便不小心再次尝试对其执行delete操作,也不会导致未定义行为,从而增强了程序的健壮性。

  3. 方便检查指针是否已被释放:将指针设置为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 对象 originx 成员值设置为 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

总体而言,这两种实现效果一致,都能将 originx 成员值设为 0.0 。但是(1)会比(2)要好,(2)会有运行时开销。

6. 一般哪些函数可以使用 noexcept?

在 C++ 中,noexcept关键字用于表明一个函数不会抛出任何异常。这对于提升代码的性能与可靠性有着重要意义。以下是几类常见可使用noexcept的函数:

  1. 析构函数:析构函数一般不应抛出任何异常。这是因为若在析构函数中抛出异常,极有可能致使程序出现未定义行为。例如,当对象被自动销毁或容器中的对象析构时,若析构函数抛出异常,程序的后续行为将无法预测,可能导致崩溃或数据损坏等严重问题。
  2. 移动构造函数与移动赋值操作符:这些函数通常应标记为noexcept。因为它们大多仅涉及指针和基本类型的转移,正常情况下不应抛出异常。而且,标记为noexcept的移动操作能被标准库容器(如std::vector)更高效地运用。比如,当std::vector进行扩容或重新分配内存时,若移动操作标记为noexceptstd::vector可以采用更优化的策略,避免不必要的复制操作,从而显著提升性能。
  3. 交换函数:像std::swap这类交换函数,通常也应标记为noexcept。这是由于它们通常仅涉及指针和基本类型的转移,正常情况下不会抛出异常。例如,在进行两个对象的交换操作时,仅仅是交换它们内部的指针或基本数据成员,这种操作的稳定性较高,不易引发异常。
  4. 其他不会抛出异常的函数:倘若你明确知晓某个函数不会抛出异常,那么就应当使用noexcept关键字。这不仅有助于编译器对代码进行优化,例如,编译器可能会针对noexcept函数进行特定的优化,生成更高效的机器码;同时也能向其他开发者清晰地表明该函数不会引发异常,使得代码的行为更加明确和可预测。

7. c++怎么定义隐式转换规则

在 C++ 中,隐式转换(也称为自动类型转换)由编译器自动执行。这些转换规则由 C++ 语言明确规定,例如从 intdouble 的转换,或者从派生类到基类的转换。 然而,你也可以为自定义类型定义隐式转换规则。这可以通过定义转换函数来达成。转换函数是一种特殊的成员函数,它能够将一个类的对象转换为其他类型的对象。 例如,假设你有一个名为 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,并声明 BA 的友元。这样,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:

  • (七)内核那些事儿:操作系统对网络包的处理
  • (六)内核那些事儿:文件系统
  • (五)内核那些事儿:系统和程序的交互
  • (四)内核那些事儿:设备管理与驱动开发
  • (三)内核那些事儿:CPU中断和信号