C++语言特性层(Language Features Layer)

1.语言基础

(1)指针

  1. 定义

    • 指针是一个变量,用于存储另一个变量的内存地址。
  2. 特性

    • 可变性:指针可以重新指向不同的变量。
    • 空指针:指针可以为空(即指向 nullptr)。
    • 大小sizeof 指针得到的是指针本身的大小(通常是4或8字节,取决于系统)。
    • 操作:指针需要通过解引用操作符(*)才能访问或修改其指向的变量。
    • 常量:可以有 const 和非 const 区别,表示指针本身或指向的数据是否可以修改。
  3. 函数传递

    • 传指针时,将变量的地址传递给函数,函数通过解引用操作指向的变量。

(2)引用

  1. 定义

    • 引用是一个变量的别名,它必须在定义时初始化,并且不能改变其绑定对象。
  2. 特性

    • 不可变性:引用一旦绑定到一个变量,就不能改变其绑定的对象。
    • 不能为空:引用必须在定义时绑定到一个有效的变量,不能为 nullptr
    • 大小sizeof 引用得到的是引用对象的大小。
    • 操作:对引用的操作直接作用于其绑定的对象,无需解引用操作符。
  3. 函数传递

    • 传引用时,函数直接操作传入的变量,无需解引用操作。

(3)C++函数参数传递方式

(1)值传递

A. 特性:

  • 传递实参的拷贝

B. 注意:

  1. 指针传递也是值传递(传递地址值)
(2)引用传递

A. 特性:

  • 本质就是实参别名(实参地址直接作为形参地址,引用在运行时存在,并作为实际的变量进行操作)

B. 注意:

  • 引用必须初始化
  • 引用一旦初始化,不能再绑定其他变量
(3)移动传递

A. 特性:

  • 对象的资源从一个对象转移到另一个对象,而不是复制资源

B. 注意:

  1. 移动构造函数和移动赋值运算符的存在与否
  2. 使用std::move将左值转换为右值引用
  3. 移动后,原对象处于有效但未定义状态(通常为空)
  4. 需要对移动构造函数和移动赋值运算符进行noexcept声明以确保异常安全(noexcept说明该函数不会抛出异常,优化性能)

(4)标识符(identifier)

是用于标识变量、函数、类型、类、对象等各种编程元素的名称。标识符本身不是一种类型,但它可以用于命名特定类型的变量、函数或其他编程元素。

内层标识符 是相对于 外层标识符 而言的,主要涉及作用域的概念。作用域决定了标识符在程序中的可见范围。当一个标识符在某个嵌套作用域中定义时,它被称为内层标识符。

(5)数据类型及其字节大小

基本数据类型
  1. 布尔型 (bool)

    • 通常大小为 1 字节
  2. 字符型 (char)

    • 大小为 1 字节
  3. 宽字符型 (wchar_t)

    • 通常大小为 2 或 4 字节,取决于实现
  4. 整型 (integer)

    • short: 通常大小为 2 字节
    • int: 通常大小为 4 字节
    • long: 通常大小为 4 字节
    • long long: 通常大小为 8 字节
  5. 无符号整型 (unsigned integer)

    • unsigned short: 通常大小为 2 字节
    • unsigned int: 通常大小为 4 字节
    • unsigned long: 通常大小为 4 字节
    • unsigned long long: 通常大小为 8 字节
  6. 浮点型 (floating-point)

    • float: 通常大小为 4 字节
    • double: 通常大小为 8 字节
    • long double: 通常大小为 8 或 16 字节,取决于实现
复合数据类型
  1. 数组 (Array)

    • 大小取决于元素类型和数组长度
  2. 结构体 (Struct)

    • 大小为其成员大小的总和,可能会因对齐(padding)而增加
  3. 联合体 (Union)

    • 大小为其最大成员的大小
  4. 枚举 (Enum)

    • 大小通常与 int 相同,但具体取决于实现
  5. 指针 (Pointer)

    • 大小通常为 4 字节(32 位系统)或 8 字节(64 位系统)

2.语言特性 

1. 智能指针

(1)指针常见问题
悬空指针

指向已经被释放或未分配内存的指针。

delete ptr;
// ptr = nullptr;    // 释放指针指向的内容后,指针仍然指向旧地址

如何避免悬空指针

  1. 释放内存后将指针置为 nullptr

    int* p = new int(42); delete p; p = nullptr;
  2. 避免多次释放同一块内存:确保每块内存只释放一次,防止指针再次指向已释放的内存。

    int* p = new int(42); 
    delete p;
    p = nullptr; // 这样可以避免再次delete
  3. 使用智能指针:智能指针会自动处理内存释放,避免悬空指针的问题。

    std::unique_ptr<int> p = std::make_unique<int>(0);
    
野指针

指向未初始化的指针,指向一个未知的内存位置。

如何避免野指针

  1. 指针初始化:在定义指针时,将其初始化为 nullptr,确保指针在使用前被赋予一个有效的内存地址。

    int* p = nullptr;
  2. 动态分配内存时立即初始化

    int* p = new int(0); // 分配并初始化为 0
  3. 使用智能指针:在 C++11 及更高版本中,使用智能指针如 std::unique_ptrstd::shared_ptr,它们会自动管理内存,避免未初始化指针的使用。

    std::unique_ptr<int> p = std::make_unique<int>(0);

循环引用(Circular References)

  • 当两个 std::shared_ptr 相互引用时,会导致内存泄漏,因为引用计数不会降到 0,资源不会被释放。
  • 解决方案是使用 std::weak_ptr 打破循环引用。
    • 例如:std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; b->a = a;

不正确的使用 std::weak_ptr

  • 访问 std::weak_ptr 前没有提升(lock)为 std::shared_ptr 会导致未定义行为。
    • 例如:std::weak_ptr<int> wp; std::shared_ptr<int> sp = wp.lock(); *sp = 10; 需要确保 wp 有效。

过度使用智能指针

  • 不必要地使用智能指针会增加程序复杂性和性能开销。
    • 例如:在小型对象或不需要动态分配的场合下使用智能指针。

误用 std::unique_ptr

  • std::unique_ptr 不允许复制,只能移动。当误用时会引发编译错误。
    • 例如:std::unique_ptr<int> p1(new int(10)); std::unique_ptr<int> p2 = p1; 应该使用 p2 = std::move(p1);

内存泄漏

  • 虽然智能指针可以自动管理内存,但如果智能指针的生命周期管理不当,仍可能导致内存泄漏。
    • 例如:在容器中存储智能指针时,未正确处理指针生命周期。
(2)auto_ptr

特性

  • 所有权转移,行为更接近于现代 C++ 中的移动语义。

注意

  • auto_ptr 所有权转移机制不直观且容易导致潜在的内存崩溃问题。

    std::auto_ptr<std::string> p1(new std::string("hello"));
    std::auto_ptr<std::string> p2;
    p2 = p1; // p2 剥夺了 p1 的所有权。
    
    std::cout << *p2 << std::endl; // 这可以正常工作,因为 p2 现在拥有该字符串。
    // std::cout << *p1 << std::endl; // 这会导致运行时错误,因为 p1 不再拥有该字符串。
    
(3)unique_ptr

特性

  • 独有式管理,确保一个对象同一时间只有一个 unique_ptr 管理。不允许复制操作(会报错)。

std::unique_ptr<std::string> p3(new std::string("auto")); // 创建一个 unique_ptr,指向字符串 "auto"
std::unique_ptr<std::string> p4; // 创建一个空的 unique_ptr

// 尝试复制 unique_ptr 会导致编译错误
// p4 = p3; // 此行代码会报错,原因如下:

注意

  1. 不能直接传递 unique_ptr 给需要复制参数的函数,而应该传递其引用或使用 std::move 来转移所有权。
  2. 转移所有权后,指针为空。
(4)shared_ptr

特性

  • 指针 ptr1 拷贝复制给另一个指针 ptr2(引用次数加一)。

注意

  1. 初始化两个 shared_ptrptr1ptr2 同时指向同一对象时(两个指针引用次数独立),可能会出现双重释放的问题。
#include <iostream>
#include <memory>

class Car {
public:
    Car() { std::cout << "Car constructed\n"; }
    ~Car() { std::cout << "Car destructed\n"; }
};

int main() {
    Car* rawCar = new Car();  // 手动创建一个 Car 对象

    std::shared_ptr<Car> ptr1(rawCar); // 引用计数为 1
    std::shared_ptr<Car> ptr2(rawCar); // 引用计数为 1(独立的计数)

    std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 1
    std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 输出 1

    // 当 ptr1 和 ptr2 作用域结束时,两者都会尝试删除同一个原始指针
    return 0;
}

2.new/delete和malloc/free

new 和 delete

new:

  1. new 是一个运算符,用来分配内存并初始化对象。
  2. 执行 new 实际上有两个步骤:
    1. 分配内存new 会调用底层的 malloc 函数分配一块未初始化的内存空间。
    2. 初始化对象new 会调用对象的构造函数在分配的内存上构造对象。
  3. new 返回的是经过初始化的对象的指针。
  4. 如果内存分配失败,会抛出 std::bad_alloc 异常,或者被自定义的异常处理函数捕获。
  5. 如果在构造对象时出现异常,new 会自动调用 delete 释放之前分配的内存。

delete:

  1. delete 是一个运算符,用来释放内存并销毁对象。
  2. 执行 delete 实际上有两个步骤:
    1. 销毁对象delete 会调用对象的析构函数,执行清理操作。
    2. 释放内存delete 会调用底层的 free 函数释放内存。
  3. delete 用于释放单个由 new 分配的对象。
  4. delete[] 用于释放由 new[] 分配的数组对象。
malloc 和 free

malloc:

  1. malloc 是一个库函数,用来分配一块未初始化的内存。
  2. malloc 只分配指定字节数的内存,不会调用任何构造函数。
  3. malloc 返回的是void*(通用指针),如果内存分配失败,返回 NULL

分配了 sizeof(int) * 10 字节的内存,并将 void* 转换为 float* 时,虽然语法上允许这种转换,但实际使用中必须确保内存分配的大小和类型匹配,否则会导致未定义行为和内存访问错误。

  1. 内存越界访问:如果 int 的大小与 float 的大小不同,那么在访问数组元素时,可能会导致访问超出实际分配内存的边界。
  2. 数据解释错误:即使内存分配大小足够,类型不匹配会导致数据被错误解释。intfloat 的二进制表示不同,因此读取或写入数据时会出现问题。

free:

  1. free 是一个库函数,用来释放由 malloc 分配的内存。
  2. free 只释放内存,不会调用任何析构函数。
关键区别
  1. 内存初始化

    • new 分配内存并调用构造函数初始化对象。
    • malloc 只分配未初始化的内存。
  2. 内存释放

    • delete 调用析构函数销毁对象,然后释放内存。
    • free 只释放内存,不调用析构函数。
  3. 异常处理

    • new 在内存分配失败时抛出异常。
    • malloc 在内存分配失败时返回 NULL
  4. 类型安全

    • new 是类型安全的,返回特定类型的指针。
    • malloc 只返回 void*,需要手动进行类型转换。
  5. 编译器控制

    • newdelete 是运算符,由编译器控制,可以结合构造函数和析构函数的调用。
    • mallocfree 是库函数,不在编译器的控制范围内,无法自动调用构造函数和析构函数。
为什么需要 new 和 delete?

对于非内部数据类型(如类对象)而言,仅使用 mallocfree 无法满足动态对象管理的需求。对象在创建时需要自动执行构造函数,在销毁前需要自动执行析构函数。由于 mallocfree 是库函数,不是运算符,编译器无法控制其行为,因此不能将构造和析构的任务强加于 mallocfree。这就是 newdelete 存在的原因。

总结起来,newdelete 提供了更高层次的抽象,简化了内存管理,确保了对象的正确构造和析构,是面向对象编程的必要工具。

3. C++ 四种强制转换

  • 上行转换(Upcasting):指从派生类指针或引用转换到基类指针或引用。这是安全的,因为派生类是基类的一种扩展。
  • 下行转换(Downcasting):指从基类指针或引用转换到派生类指针或引用。这可能是不安全的,因为基类指针或引用可能并不实际指向该派生类的对象。
1. static_cast
  • 用途:用于在编译时进行类型转换。
  • 特性:无运行时检查,上行转换安全,下行转换不安全。
  • 理解
    • 无运行时检查static_cast 仅在编译时检查类型兼容性,转换操作在运行时不进行额外的检查。
    • 上行转换安全:派生类指针/引用转换为基类指针/引用,基类是派生类的一部分,所以是安全的。
    • 下行转换不安全:基类指针/引用转换为派生类指针/引用,没有检查转换的合法性,可能会导致未定义行为。
  • int main() {
        double d = 3.14;
        int i = static_cast<int>(d); // 将 double 转换为 int,丢失小数部分
    }
    
2. dynamic_cast
  • 用途:用于在多态类型之间进行安全的类型转换。
  • 特性:有运行时类型检查,下行转换安全,类型不一致时返回空指针。
  • 理解
    • 有运行时类型检查dynamic_cast 会在运行时检查实际类型是否匹配,确保转换的安全性。
    • 下行转换安全:基类指针/引用转换为派生类指针/引用时,如果类型不匹配,返回 nullptr(指针)或抛出异常(引用)。
    • 类型不一致时返回空指针:当尝试转换的类型不兼容时,dynamic_cast 会返回空指针以避免无效转换。
  • #include <iostream>
    
    class Base {
    public:
        virtual ~Base() {}
    };
    
    class Derived : public Base {};
    
    int main() {
        Base* b = new Derived;
        Derived* d = dynamic_cast<Derived*>(b); // 安全的下行转换
        if (d) {
            std::cout << "Conversion successful" << std::endl;
        } else {
            std::cout << "Conversion failed" << std::endl;
        }
        delete b;
    }
    
3. const_cast
  • 用途:用于添加或移除 const 属性。
  • 特性:唯一能操作常量属性的转换符。
  • 理解
    • 添加或移除 const 属性const_cast 可以去除 const 修饰符,允许修改常量数据;也可以添加 const 修饰符,确保数据不被修改。
    • 唯一能操作常量属性const_cast 是四种转换操作符中唯一专门用于修改常量属性的转换操作符。
  • int main() {
        const int a = 10;
        int* p = const_cast<int*>(&a);
        *p = 20; // 修改 const 变量(未定义行为)
    }
    
4. reinterpret_cast
  • 用途:用于进行低级别的类型重新解释。
  • 特性:高危操作,平台相关,需谨慎使用。
  • 理解
    • 低级别的类型重新解释reinterpret_cast 允许将一种类型的指针或引用直接转换为另一种不相关的类型,这种转换不会改变数据的比特位。
    • 高危操作:这种转换可能导致未定义行为,应谨慎使用。
    • 平台相关:转换结果依赖于具体的平台,可能在不同平台上表现不同,不具有可移植性。

4.虚函数

(1)虚函数表和动态多态性

在C++中,虚函数用于实现动态多态性,通过虚函数表(vtable)和虚函数表指针(vptr)机制来实现。

  1. 虚函数表(vtable)

    • 每个包含虚函数的类都有一个独立的虚函数表,存储该类声明的虚函数的地址。
    • 虚函数表在编译时生成,对每个类来说是唯一的。
  2. 虚函数表指针(vptr)

    • 每个包含虚函数的对象都有一个隐藏的虚函数表指针(vptr),指向对象所属类的虚函数表。
    • 在对象创建时,vptr被初始化为指向该对象所属类的虚函数表。
(2)虚函数工作原理:
1.虚函数表地址变化

给每个对象添加一个隐藏成员。隐藏成员中保存一个指向函数地址数组(虚函数表)的指针(虚函数指针),虚函数表存储了为类对象进行声明的虚函数的地址。

  • 基类的虚函数表

    • 基类的虚函数表在内存中是一个独立的实体,其内容在类定义和编译时确定。
    • 基类的虚函数表存储基类声明的虚函数的地址,即使派生类重写了这些虚函数,基类的虚函数表仍然保持不变。
  • 派生类的虚函数表

    • 派生类的虚函数表也是一个独立的实体。
    • 当派生类重写了基类的虚函数时,派生类的虚函数表中相应位置的地址会被更新为派生类实现的虚函数的地址。
    • 派生类没有重写的虚函数,其虚函数表中对应位置的地址会继续存储基类版本的虚函数地址。
  • 虚函数表地址变化

    • 基类和派生类的虚函数表地址在内存中是不同的。
    • 基类的虚函数表地址和内容在派生类重写虚函数后保持不变。
    • 派生类的虚函数表在重写虚函数后更新了相应位置的地址。
class Base {
public:
    // 虚函数func1和func2的定义
    virtual void func1() {
        std::cout << "Base::func1" << std::endl;
    }

    virtual void func2() {
        std::cout << "Base::func2" << std::endl;
    }
    // Base_vtable:
    // [0]  0x1000  (Base::func1)
    // [1]  0x1004  (Base::func2)
};

class Derived : public Base {
public:
    // 重写虚函数func1
    void func1() override {
        std::cout << "Derived::func1" << std::endl;
    }
    // 派生类没有重写func2

    // Derived_vtable:
    // [0]  0x2000  (Derived::func1)
    // [1]  0x1004  (Base::func2)
};
2.this指针

定义

  • this 指针是一个隐含参数,存在于每个非静态成员函数中,指向调用该成员函数的对象(即当前对象)。

特性

  1. 隐含参数

    • 在每个非静态成员函数中,this指针是隐含的,即程序员不需要显式地声明它。
  2. 指向对象

    • this指针指向调用成员函数的对象,即当前对象。可以用来访问该对象的成员变量和其他成员函数。

用途

  1. 访问成员变量

    • this指针可以用来访问对象的成员变量,避免名称冲突。
    class MyClass {
    private:
        int value;
    public:
        MyClass(int value) {
            this->value = value;  // 使用 this 指针访问成员变量
        }
        void display() {
            std::cout << "Value: " << this->value << std::endl;
        }
    };
    

  2. 调用成员函数

    • this指针可以用来调用对象的其他成员函数。
    class MyClass {
    public:
        void func1() {
            std::cout << "func1 called" << std::endl;
        }
        void func2() {
            this->func1();  // 使用 this 指针调用另一个成员函数
        }
    };
    

  3. 返回对象自身

    • 在成员函数中,可以使用this指针返回对象自身的引用。
    class MyClass {
    public:
        MyClass& setValue(int value) {
            this->value = value;
            return *this;
        }
    private:
        int value;
    };
    

(1)为什么 this 指针对虚函数很重要
  • 虚函数的动态绑定

    • 虚函数依赖于this指针来实现动态绑定。每个对象都有一个虚表指针(vptr),指向该类的虚表(vtable)。在调用虚函数时,通过this指针找到对象的虚表,再通过虚表找到正确的函数地址进行调用。
    class Base {
    public:
        virtual void display() {
            std::cout << "Base display" << std::endl;
        }
    };
    
    class Derived : public Base {
    public:
        void display() override {
            std::cout << "Derived display" << std::endl;
        }
    };
    
    int main() {
        Base* obj = new Derived();
        obj->display();  // 输出 "Derived display",通过 this 指针实现动态绑定
        delete obj;
    }
    

(2)不能有 this 指针的函数
  1. 静态成员函数

    • 静态成员函数属于类本身而不是某个对象,因此没有this指针。
    class MyClass {
    public:
        static void staticFunc() {
            // 静态函数没有 this 指针
        }
    };
    

  2. 普通非成员函数

    • 普通非成员函数不属于任何类,因此没有this指针。
    void normalFunc() {
        // 非成员函数没有 this 指针
    }
    

(3)编译器处理虚函数表的过程

在C++中,编译器处理虚函数表的过程主要涉及以下几个步骤,特别是在处理派生类时。这些步骤确保了在运行时能够正确地进行虚函数的调用,从而实现多态性。

  1. 拷贝基类的虚函数表

    • 如果派生类继承自基类,编译器首先拷贝基类的虚函数表。如果是多继承,则拷贝每个有虚函数的基类的虚函数表。
    • 有时会有一个基类被称为主基类(primary base class),其虚函数表可能会被直接继承并共用。
  2. 替换虚函数

    • 编译器查看派生类中是否重写了基类中的虚函数。如果有重写,则在派生类的虚函数表中替换相应的虚函数地址为派生类实现的地址。
  3. 添加派生类自己的虚函数

    • 编译器检查派生类是否有新的虚函数。如果有,则将这些新的虚函数添加到派生类的虚函数表中。
(4)虚继承
特性:
  1. 单一实例化:虚继承确保基类在派生类中只实例化一次,即使通过多条路径继承。
  2. 虚基类表(vbase table):虚继承引入虚基类表,其中包含指向虚基类的指针,确保基类实例的唯一性。
  3. 菱形继承问题的解决:虚继承通过确保基类实例唯一,解决了菱形继承导致的数据冗余和不确定性问题。
class A {
public:
    int value;
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C {
};
注意事项:
  1. 构造函数初始化列表

    • 在派生类的构造函数中,必须显式初始化虚基类。即使 BC 初始化了 A,在 D 中也必须再次初始化 A
    • class A {
      public:
          A(int v) : value(v) {}
          int value;
      };
      
      class B : virtual public A {
      public:
          B(int v) : A(v) {}
      };
      
      class C : virtual public A {
      public:
          C(int v) : A(v) {}
      };
      
      class D : public B, public C {
      public:
          D(int v) : A(v), B(v), C(v) {}
      };
      

2.访问控制

  • 访问虚基类成员时,可能需要使用作用域解析运算符明确指定基类。
  • D obj(10);
    obj.A::value = 20; // 需要指定作用域
    

底层实现

1. 虚基类指针(VBP)
  • 位置:紧跟在虚函数指针(vptr)之后。
  • 功能:指向虚基类表(VBT),用于定位虚基类实例的位置,确保多重继承情况下基类实例的唯一性。
2. 虚基类表(VBT)
  • 记录内容:虚基类表记录了虚基类实例在派生类对象中的偏移量。
  • 编译器的作用:编译器在生成派生类对象时,使用这些偏移量来定位虚基类实例的位置。
3. 数据成员布局
  • 数据成员:对象包含的所有数据成员,包括继承自基类和派生类的成员。
  • 虚基类成员:虚基类的成员位于对象内存布局的最后,确保所有派生类共享一个基类实例。
步骤:

编译器会执行以下步骤:

  1. 获取 D 对象的基地址,例如 base_addr
  2. 通过 BC 的 VBP 访问 VBT
  3. VBT 中获取 A 类实例的偏移量。
  4. 计算 A 实例的地址为 base_addr + offset
  5. 访问 A 实例的 value 成员。
class A {
public:
    int value;
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C {
public:
    D(int v) : A(v), B(v), C(v) {
    // 在这里,编译器会自动调整 B 和 C 的 VBT,使其指向 D 中的 A 实例
}

};


D:
    - B:
        - VBP (虚基类指针)
        - B 的数据成员
        - VBT (虚基类表)
            [A offset] -> 从D对象的基地址到A实例的偏移量
    - C:
        - VBP (虚基类指针)
        - C 的数据成员
        - VBT (虚基类表)
            [A offset] -> 从D对象的基地址到A实例的偏移量
    - A:
        - value (A 的数据成员)
    - D 的数据成员
(5)虚函数使用注意事项
(1)指针,引用隐式向上转换?

当派生类继承基类时,派生类对象包含基类对象的所有特性。这意味着派生类对象在结构上是基类对象的一个超集。由于这种包含关系,派生类对象可以被当作基类对象处理。

  • 当派生类重写了基类的虚函数时,派生类的虚函数表中相应位置的地址会被更新为派生类实现的虚函数地址。
  • 虚函数表的位置在类层次结构中是固定的,因此基类指针或引用指向派生类对象时,通过虚函数表调用虚函数,会调用派生类的实现。
  • 这种机制确保了在运行时能够正确调用派生类重写的虚函数,实现多态性。
(2)析构函数一般写成虚函数的原因

当一个基类指针指向一个派生类对象时,如果基类的析构函数不是虚函数,删除该对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这样会导致派生类的资源无法正确释放,可能会导致内存泄漏。

class Base {
public:
    virtual ~Base() { std::cout << "Base Destructor" << std::endl; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived Destructor" << std::endl; }
};

void func() {
    Base* b = new Derived();
    delete b; // 调用Derived和Base的析构函数
}
(3)构造函数为什么一般不定义为虚函数
  1. 创建对象需要完整信息:创建对象时需要知道对象的确切类型及其所有成员的完整信息,而虚函数只需要知道函数接口。
  2. 虚函数表指针(vptr)依赖于对象实例化后:对象的虚函数指针(vptr)是在对象实例化过程中设置的。只有在对象实例化之后,虚函数指针才存在,并指向对应的虚函数表(vtable)
  3. 如果构造函数是虚的,那么在对象实例化之前,虚函数表指针不存在,无法找到对应的虚函数表来调用虚函数。
(4)构造函数或析构函数中调⽤虚函数
1.理解构造函数中的虚函数调用

当创建一个派生类对象时,构造过程如下:

  1. 首先调用基类构造函数初始化基类部分。
  2. 然后调用派生类构造函数初始化派生类部分。

在基类构造函数执行时,派生类部分还没有被初始化。因此,在基类构造函数中调用虚函数时,只会调用基类的版本,而不会调用派生类的版本。此时,编译器认为对象是基类类型的对象。

2.理解析构函数中的虚函数调用

当销毁一个派生类对象时,析构过程如下:

  1. 首先调用派生类析构函数。
  2. 然后调用基类析构函数销毁基类部分。

在派生类析构函数执行完毕后,派生类部分已经被销毁。如果在基类析构函数中调用虚函数,此时对象的类型被认为是基类类型,派生类部分已经不存在,因此只会调用基类的版本。

(5)哪些函数不能是虚函数

在C++中,不是所有函数都能被声明为虚函数。以下是不能被声明为虚函数的几类函数及其原因:

  1. 构造函数

    • 原因:构造函数用于初始化对象。在构造函数执行时,派生类的构造函数需要知道基类构造函数做了什么。如果构造函数是虚函数,那么在构造基类对象时,虚表指针尚未初始化完毕,因此无法正确调用派生类的构造函数。
    class Base {
    public:
        virtual Base() {}  // 错误,构造函数不能是虚函数
    };
    

  2. 内联函数

    • 原因:内联函数在编译阶段进行函数体替换操作,而虚函数在运行期间进行类型确定。由于这两者在处理时间上的矛盾,内联函数不能是虚函数。
    class Base {
    public:
        virtual inline void func() {}  // 错误,内联函数不能是虚函数
    };
    

  3. 静态函数

    • 原因:静态函数属于类而不是对象,没有 this 指针,虚函数依赖 this 指针进行动态绑定。因此,静态函数不能是虚函数。
     
    class Base {
    public:
        virtual static void func() {}  // 错误,静态函数不能是虚函数
    };
    

  4. 友元函数

    • 原因:友元函数不是类的成员函数,不能被继承。由于没有继承关系,友元函数不能是虚函数。
    class Base {
    public:
        friend virtual void func(Base& b);  // 错误,友元函数不能是虚函数
    };
    

  5. 普通函数

    • 原因:普通函数不是类的成员函数,不具备继承特性,因此也不能是虚函数。
    virtual void globalFunc() {}  // 错误,普通函数不能是虚函数
    

深拷贝和浅拷贝的区别

浅拷贝

定义:浅拷贝仅复制对象的所有成员的值,包括指针成员的地址。

特点

  • 复制对象的所有成员值,但不复制指针所指向的内容。
  • 如果对象包含指针成员,浅拷贝后两个对象的指针成员将指向相同的内存地址。
  • 当其中一个对象被销毁时,指针指向的内存可能被释放,导致另一个对象使用非法内存。

步骤

  • 普通成员:直接复制其值。
  • 指针成员:复制指针的地址,使得多个对象共享同一块内存。
  • 潜在问题:如果一个对象释放了内存,另一个对象仍然试图访问这块内存,会导致悬空指针问题。
#include <iostream>

class ShallowCopy {
public:
    int* data;

    ShallowCopy(int value) {
        data = new int(value);
    }

    // 拷贝构造函数(浅拷贝)
    ShallowCopy(const ShallowCopy& other) : data(other.data) {
    }

    // 析构函数
    ~ShallowCopy() {
        delete data;
    }
};

int main() {
    ShallowCopy obj1(5);
    ShallowCopy obj2 = obj1; // 浅拷贝,obj2.data 指向与 obj1.data 相同的内存

    std::cout << "obj1 data: " << *obj1.data << std::endl; // 输出 5
    std::cout << "obj2 data: " << *obj2.data << std::endl; // 输出 5

    // obj1 析构函数被调用,释放 data 所指向的内存
    // 但 obj2.data 仍然指向同一块内存,导致悬空指针

    return 0; // 当 obj2 被析构时,访问已释放的内存,可能导致程序崩溃
}
(1)为什么拷⻉构造函数必需时引⽤传递,不能是值传递?
当一个对象以值传递的方式传递给函数时,编译器会调用该对象的拷贝构造函数来创建一个新的副本,并将这个副本作为参数传递给函数。在函数内部,对这个副本进行操作,而原始对象保持不变。
如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那
么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造
函数,这就是⼀个⽆限递归。

当一个对象被值传递时,编译器会按照以下步骤处理:

  1. 调用拷贝构造函数: 编译器会调用该类的拷贝构造函数来创建对象的副本。

  2. 执行拷贝构造函数中的复制逻辑: 在拷贝构造函数中,你可以定义如何复制对象的成员变量,包括动态内存分配和其他复杂的复制操作。

class A {
public:
    A(const A a) { // 值传递,错误
        // 构造函数体
    }
};

int main() {
    A a1;
    A a2 = a1; // 这里会调用拷贝构造函数
}
(2)拷贝构造函数会在以下三种情况下被调用:

        1.对象以值传递的方式传入函数: 当一个对象以值传递的方式作为函数参数时,编译器会调用拷贝构造函数来创建该对象的副本,从而将副本传递给函数。

void function(A obj) {
    // 函数体
}

int main() {
    A a1;
    function(a1); // 调用拷贝构造函数
}

        2.对象以值传递的方式从函数返回: 当一个函数以值传递的方式返回对象时,编译器会调用拷贝构造函数来创建该对象的副本,从而将副本作为返回值返回。

class A {
public:
    A() {
        // 默认构造函数
    }
    
    A(const A& other) {
        // 拷贝构造函数
    }
};

A function() {
    A a1;      // 1. 调用默认构造函数,创建局部对象 a1
    return a1; // 2. 返回 a1,编译器会调用拷贝构造函数来创建 a1 的副本 (临时对象)
}

int main() {
    A a2 = function(); // 3. 接收 function 返回的临时对象,编译器会再次调用拷贝构造函数
}

​​​​​​

        3.对象通过另一个对象进行初始化: 当一个对象通过另一个对象进行初始化时,编译器会调用拷贝构造函数来创建新对象的副本。

int main() {
    A a1;
    A a2 = a1; // 调用拷贝构造函数
    A a3(a1); // 调用拷贝构造函数
}
深拷贝

定义:深拷贝不仅复制对象的所有成员的值,还为指针成员分配新的内存并复制指针所指向的内容。

特点

  • 复制对象的所有成员值,并为指针成员分配新的内存,复制内容。
  • 每个对象有自己独立的内存空间,避免了共享内存带来的问题。
  • 更加安全,适用于包含指针成员的类。

步骤

  1. 普通成员:直接复制。
  2. 指针成员:重新分配内存,并将原指针指向的内容复制到新指针所指向的内存中。
#include <iostream>

class DeepCopy {
public:
    int* data;

    DeepCopy(int value) {
        data = new int(value);
    }

    // 深拷贝构造函数
    DeepCopy(const DeepCopy& other) {
        data = new int(*other.data);
    }

    ~DeepCopy() {
        delete data;
    }
};

int main() {
    DeepCopy obj1(5);
    DeepCopy obj2 = obj1; // 深拷贝

    std::cout << "obj1 data: " << *obj1.data << std::endl;
    std::cout << "obj2 data: " << *obj2.data << std::endl;

    return 0;
}

3.关键字

(1)const:

const 关键字用于表示常量,保护数据不被修改。其应用包括:

  1. 修饰基本数据类型

    • 修饰符位置:const 可以在类型说明符前或后使用,效果相同。使用时不可修改其值。
    • const int a = 10;  // a 是一个不可修改的整数
      int const b = 20;  // b 也是一个不可修改的整数
  1. 修饰指针和引用变量

    • 指针本身为常量:const 在星号右侧,表示指针本身不可变,指向的整数可以修改
    • int* const ptr = &value;  
      
    • 指向常量的指针: const 在星号左侧,表示指针指向值不变
    • const int* ptr = &value;  // ptr 是一个指向常量整数的指针
      int const* ptr2 = &value;  // ptr2 也是一个指向常量整数的指针
      
    • 引用常量:     不可以通过这个引用修改这个值
    • int value = 10; 
      const int& ref = value;
  2. 修饰函数参数

        1.保护参数:

void display(const std::string& text) {
    // 函数体内不能修改 text
    std::cout << text << std::endl;
}

        2.保护返回值:

const std::string& getName() const {
    return name;
}

 3.在类中的应用

1. const 成员变量

定义和用法

  • const 成员变量在某个对象的生命周期内是常量,但对于整个类而言可以不同。
  • 由于类的对象在没有创建时,编译器不知道 const 数据成员的值是什么,所以不能在类的声明中初始化 const 数据成员。
  • const 数据成员的初始化只能在类的构造函数的初始化列表中进行。
#include <iostream>

class MyClass {
public:
    const int value;

    MyClass(int val) : value(val) {}  // 在构造函数初始化列表中初始化

    void showValue() const {  // const 成员函数
        std::cout << "Value is: " << value << std::endl;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2(20);

    obj1.showValue();  // 输出:Value is: 10
    obj2.showValue();  // 输出:Value is: 20

    return 0;
}
2. const 成员函数

具体解释

  • const 成员函数:该函数不会修改对象的成员变量(只会读取对象的状态,而不会改变它)
  • mutable 关键字: const 成员函数中能够被修改的成员变量,可以使用 mutable 关键字进行修饰。

注意:

(1)常量对象只能调用常量成员函数

定义和用法

  • 一个被 const 修饰的对象称为常量对象。(对象不可变)
  • 常量对象只能调用 const 成员函数,不能调用其他会修改对象状态的成员函数。

进一步理解:

(1)类中每个成员函数都有一个隐式指针指向调用对象(当前类对象)

非const成员函数:有一个classname * const 的常量隐式指针(故可以改变对象成员变量)

const成员函数:   有一个const classname * 的指向常量的隐式指针(不可改变对象成员变量)

3. 非常量对象的调用规则
  • 非常量对象可以调用 const 成员函数和非 const 成员函数。
  • 非常量对象调用 const 成员函数时,this 指针类型为 ClassName* const,表示指针本身是常量,但指向的对象内容可以被修改。
  • 非常量对象调用非 const 成员函数时,可以自由修改对象的状态。

(2)inline 

1.特性
  1. 编译器提示

    • 描述inline 关键字提示编译器将函数尽量内联,但最终决定权在编译器。
  2. 减少函数调用开销

    • 描述:内联函数通过将函数体直接插入调用点来减少函数调用的开销,尤其对小型和频繁调用的函数有效。
  3. 类型检查

    • 描述:内联函数与普通函数一样支持类型检查,避免了宏定义中的类型问题。
    • inline double square(double x) { return x * x; }
  4. 作用域和命名空间

    • 描述:内联函数遵循 C/C++ 的作用域和命名空间规则,可以是全局或局部的。
    • inline static int increment(int x) { return x + 1; }
2.注意事项
  1. 递归限制

    • 描述:内联函数不能递归调用,否则会导致无限递归展开。
    • 示例
      inline int factorial(int n) { 
          return n <= 1 ? 1 : n * factorial(n - 1); 
      // 不推荐 }
  2. 代码膨胀

    • 描述:过度使用内联函数可能导致代码膨胀(增大可执行文件大小)。
    • 示例
      inline int largeFunction(int a, int b, int c, int d, int e) { 
      // 大量代码 return a + b + c + d + e; }
  3. 编译器优化

    • 描述:编译器可能会忽略 inline 建议,尤其是函数体过大或调用频率低时。
  4. 调试复杂性

    • 描述:内联函数的调试可能较复杂,因为在内联后,调试信息可能不完整(由于是在调用点展开,设置断点可能无效,因为内联函数在编译后可能不会保留原始的函数边界)

(3)typedef 

1.特性
  1. 简化复杂类型

    • 描述typedef 可以简化复杂类型的声明,特别是在涉及指针、函数指针和结构体时。
    • 示例
      typedef unsigned long int ulint; 
      ulint num = 123456789; // num 是一个 unsigned long int 类型的变量
  2. 提高代码可读性

    • 描述:通过使用有意义的别名,可以使代码更容易理解和维护。
    • 示例
      //  typedef 为函数指针创建了一个别名 func_ptr
      typedef int (*func_ptr)(int, int); 
      
      int add(int a, int b) { return a + b; } 
      
      func_ptr f = add; // f 是一个指向函数 add 的函数指针
  3. 平台无关性

    • 描述typedef 可以用于定义平台无关的类型,提高代码的可移植性。
    • 示例
      // 定义一个平台无关的 64 位整数类型
      #ifdef _WIN32
      typedef __int64 int64;
      #else
      typedef long long int int64;
      #endif
  4. 结合结构体使用

    • 描述:与结构体结合使用,使结构体的定义和声明更加简洁。
    • 示例
      typedef struct { int x; int y; } Point; 
      
      Point p = {10, 20}; 
      // p 是一个 Point 类型的变量
2.注意事项
  1. 不能创建新类型

    • 描述typedef 只是为现有类型创建别名,而不是创建新的类型。
    • typedef int INTEGER; INTEGER a = 10; // a 是 int 类型
  2. 与宏的区别

    • 描述#define 也可以创建类型别名,但不进行类型检查,而 typedef 进行类型检查。
    • 示例
      #define UINT unsigned int typedef unsigned int UINT_T;
      
      UINT a = 10; // 宏定义,不进行类型检查 UINT_T b = 20;
       // typedef 定义,进行类型检查
  3. 作用域限制

    • 描述typedef 的作用域仅限于其定义所在的代码块或文件,而宏没有作用域限制。
    • 示例
      void func() { typedef int LOCAL_INT; LOCAL_INT a = 10; // a 是 int 类型 } 
      
      // LOCAL_INT 在这里是不可见的

(4)explicit 

特性
  1. 防止隐式转换

    • explicit 关键字修饰的构造函数不能用于隐式类型转换。
    • 只有显式调用时才会触发构造函数。
  2. 用于构造函数和转换运算符

    • 主要用于构造函数以避免不必要的隐式类型转换。
    • 也可以用于转换运算符以防止隐式类型转换操作。
  3. 编译时检查

    • 编译器在编译时强制执行 explicit 关键字的规则,确保类型转换是显式的。
注意事项
  1. 防止意外类型转换

    • 使用 explicit 关键字可以防止意外的类型转换,确保代码行为是明确和可控的。
  2. 提高代码可读性

    • 通过显式类型转换,代码中所有的类型转换都是清晰和可读的,减少了调试和维护的复杂性。
  3. 适用范围

    • explicit 关键字在转换运算符中的使用与在构造函数中的使用类似,都是为了控制类型转换的行为。
#include <iostream>

class MyClass {
public:
    MyClass() : value(0) {
        std::cout << "Default constructor called" << std::endl;
    }

    explicit MyClass(int x) : value(x) {
        std::cout << "Explicit int constructor called" << std::endl;
    }

    MyClass(double y) : value(static_cast<int>(y)) {
        std::cout << "Implicit double constructor called" << std::endl;
    }

    void display() const {
        std::cout << "Value: " << value << std::endl;
    }

private:
    int value;
};

void printValue(const MyClass& obj) {
    obj.display();
}

int main() {
    MyClass obj1;           // 调用默认构造函数
    MyClass obj2 = MyClass(5);  // 显式调用 int 构造函数
    MyClass obj3 = 3.14;    // 隐式调用 double 构造函数

    printValue(MyClass(10));    // 显式调用 int 构造函数
    printValue(2.71);           // 隐式调用 double 构造函数

    // MyClass obj4 = 5;       // 错误,隐式调用被 explicit 阻止
    // printValue(5);          // 错误,隐式调用被 explicit 阻止

    return 0;
}

4.STL 容器和算法

C++ 标准模板库(STL,Standard Template Library)是一组通用的类和函数模板,提供了强大的数据结构和算法。STL 的设计极为灵活和高效,其核心组件包括容器、算法、迭代器、仿函数、配接器和配置器。它们的组合使用提供了强大的编程功能。

主要组件
  1. 容器(Containers)

    • 定义:用于存储和管理数据的类模板。
    • 常见容器vectorlistdequesetmap
    • 功能:提供数据存储和管理功能,支持动态内存分配。
  2. 算法(Algorithms)

    • 定义:一组通用的算法,用于处理容器中的数据。
    • 常见算法sort(排序)、search(搜索)、copytransform
    • 功能:提供常见的数据处理方法,独立于具体的容器实现。
  3. 迭代器(Iterators)

    • 定义:用于遍历容器的类模板,提供类似指针的接口。
    • 类型:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。
    • 功能:实现对容器的抽象访问,使算法可以独立于具体容器实现。
    • std::vector<Student> students;
      // 使用 std::vector<Student>::iterator
      std::vector<Student>::iterator it = students.begin();
      
      // 使用 auto
      auto it = students.begin();
      
  4. 仿函数(Functors)

    • 定义:重载了 operator() 的类或类模板,允许对象像函数一样调用。
    • 功能:用于定制和扩展算法的行为。
    • // 仿函数,检查是否为偶数,并记录调用次数
      struct IsEven {
          int count = 0; // 状态:记录调用次数
      
          bool operator()(int x) {
              ++count;
              return x % 2 == 0;
          }
      };
    • 现代C++中的替代方案:Lambda表达式

      在现代C++(C++11及以后的版本)中,Lambda表达式提供了一种简洁的方式来定义内联仿函数,并且Lambda表达式也可以捕获外部变量,实现状态保持。

  5. 配接器(Adapters)

    • 定义:用于修饰容器、仿函数或迭代器接口的组件。(可以看作是对容器、仿函数或迭代器的进一步抽象和封装。)
    • 类型:堆栈适配器(stack)、队列适配器(queuepriority_queue)、仿函数适配器。
    • 标准模板库(STL)中提供了几种常见的容器配接器:
    • 堆栈适配器(stack)

      • 提供堆栈(后进先出,LIFO)的功能。
      • 基于其他容器(默认是 std::deque)实现。
    • 队列适配器(queue)

      • 提供队列(先进先出,FIFO)的功能。
      • 基于其他容器(默认是 std::deque)实现。
    • 优先队列适配器(priority_queue)

      • 提供优先队列的功能,元素按照优先级排序。
      • 基于 std::vector 容器实现。
  6. 配置器(Allocators)

    • 定义:管理内存分配和释放的类模板。
    • 功能:负责内存的动态分配、管理和释放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/771150.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

20240705 每日AI必读资讯

&#x1f4da;Retool 刚刚发布了最新2024上半年《人工智能现状报告》 - 收集了约750名技术人员的意见 - 包括开发者、数据团队和各行业的领导者&#xff0c;了解如何利用人工智能产生真正的影响。 &#x1f517; 2024上半年《人工智能现状报告》Retool刚刚发布了最新-CSDN b…

瑞数信息:智能防护新时代,看AI如何筑起网络防线

AI时代&#xff0c;网络安全危与机并行。 尤其是近年来大火的大模型&#xff0c;对于网络安全行业的影响与其他行业有所不同&#xff0c;一方面&#xff0c;AI能够通过大幅降低了安全攻击的门槛&#xff0c;网络威胁的复杂性和多样性不断增加&#xff0c;如自动化攻击、零日漏…

记录问题:解决vscode找不到Python自定义模块,报错No module named ‘xxx‘

1. 背景 我非要用vscode&#xff0c;不用pycharm&#xff0c;哼&#xff01; 2. 问题 由于 import xx 自定义的模块&#xff0c; python run 的时候会报错 No module named ‘xxx‘ 报错信息&#xff1a; Traceback (most recent call last):File "d:\work\sf_financ…

原创作品 —(金融行业)年金系统交互和视觉设计

金融行业软件交互设计要点&#xff1a;“简化操作流程&#xff0c;确保流畅易用&#xff0c;同时注重交易环境的安全可靠&#xff0c;通过个性化体验提升用户满意度&#xff0c;并及时收集反馈以持续优化。” 2.UI设计要点&#xff1a;“注重视觉效果与用户体验的平衡&#xff…

创新与技术管理国际研讨会(ISITM 2024)

随着全球科技日新月异的进步&#xff0c;创新与技术管理在国际舞台上的地位愈发重要。在这样的背景下&#xff0c;创新与技术管理国际研讨会&#xff08;ISITM 2024&#xff09;应运而生&#xff0c;将于2024年12月6日至8日在中国长沙隆重举行。本次会议将聚焦创新与技术管理等…

【Linux开发实战指南】基于TCP、进程数据结构与SQL数据库:构建在线云词典系统(含注册、登录、查询、历史记录管理功能及源码分享)

目录 项目演示&#xff1a; 1. 主界面 技术讲解&#xff1a; TCP连接 进程的并发 链表 SQLite3 IO对文件的读写 功能实现 实现逻辑 我遇到的问题&#xff1a; 服务器端代码思路解析 必要条件 步骤详解 客户端代码思路解析 步骤详解 服务器源码如下&#xff1a;…

论文学习——基于区域多向信息融合的动态多目标优化引导预测策略

论文题目&#xff1a;Guided prediction strategy based on regional multi-directional information fusion for dynamic multi-objective optimization 基于区域多向信息融合的动态多目标优化引导预测策略&#xff08;Jinyu Feng a, Debao Chen b,c,d,∗, Feng Zou b,c, Fan…

【Git-驯化】一文学会git配置用户信息,git config用法细节

【Git-驯化】一文学会git配置用户信息&#xff0c;git config用法细节 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取相关内容文档…

深度解码:需求跟踪的艺术与实战应用

文章目录 引言一、需求跟踪的定义二、需求跟踪矩阵2.1 需求跟踪矩阵包含的内容2.2 跟踪矩阵层级2.3 需求属性2.4 参考表格 三、需求跟踪的收益3.1 确保商业价值最大化3.2 满足客户期望3.3 范围管理3.4 决策支持3.5 提高效率和效果3.6 文档化和沟通3.7 变更管理3.8 测量和改进 四…

ll命令在ubuntu下不能使用的解决方案

ll命令在ubuntu下不能使用的解决方案 问题&#xff1a; ll命令在ubuntu下不能使用&#xff0c; 在Ubuntu终端里执行ll,提示:command not found 解决方案&#xff1a; 打开当前用户目录下的.bashrc文件 找到下面的内容&#xff0c;将前面的“#”去掉 #alias llls -alF 然…

S272钡铼技术4G无线RTU支持多路DIN输入和模拟量转换至4G网络

钡铼第四代RTU S272是一款先进的工业级4G远程遥测终端&#xff0c;为各种远程工业数据采集和控制系统提供了高效解决方案。结合了现代通信技术和多功能的输入输出接口&#xff0c;S272不仅支持多路数字量和模拟量输入&#xff0c;还具备灵活的扩展性和强大的控制功能&#xff0…

数据库表导出到excel:前置知识1 ALL_TAB_COLS

ALL_TAB_COLS 当前用户可访问的表、视图和群集的列的相关信息 其中几个字段: OWNER&#xff1a;表&#xff0c;视图及群集的Owner   TABLE_NAME&#xff1a; 表&#xff0c;视图及聚簇的名称   COLUMN_NAME&#xff1a; 字段名   DATA_TYPE &#xff1a;字段的数据类型…

君子签区块链+AI,驱动组织实现高效合同管理、精准风险控制

在传统合同签署的过程中&#xff0c;企业、组织、机构都面临着合同签署与管理的诸多问题和挑战&#xff1a;合同种类繁多、数量庞大导致起草效率低下&#xff1b;管理流程繁琐、权限分散使得审批周期冗长且效率低下&#xff1b;合同签订版本难以精准复核&#xff0c;风险防控更…

7.基于SpringBoot的SSMP整合案例-表现层开发

目录 1.基于Restfu1进行表现层接口开发 1.1创建功能类 1.2基于Restful制作表现层接口 2.接收参数 2使用Apifox测试表现层接口功能 保存接口&#xff1a; 分页接口&#xff1a; 3.表现层一致性处理 3.1先创建一个工具类&#xff0c;用作后端返回格式统一类&#xff1a;…

如何利用小程序容器技术搭建小程序生态?

小程序&#xff0c;作为现代移动互联网生态中的重要基础设施&#xff0c;正以其独特的创新性和便捷性展现出勃勃生机。截至2021年&#xff0c;全网小程序的数量已经突破了700万&#xff0c;其中微信小程序的开发者达到了300万之多。这一数字不仅代表了小程序在技术层面的成熟度…

Java项目总结3

1.抽象类与抽象方法 注意&#xff1a; 抽象类不能实例化 抽线类中不一定有抽i像方法&#xff0c;有抽象方法的类一定是抽象类 可以有构造方法 抽象类的子类要么重写抽象类中的所有抽象方法&#xff0c;要么是抽象类 抽象类的作用&#xff1a; 抽取共性时&#xff0c;无法确定方…

Linux:网络配置命令

目录 一、查看网络接口信息 ifconfig 二、修改网络配置文件 三、设置网络接口参数 ifconfig 四、查看主机名称 hostname 五、查看路由表条目route 5.1、查看路由 5.2、添加、删除静态路由条目 5.3、添加、删除默认网关记录 六、netstat命令 七、ss 命令 八、测试网络…

java web 部分

jsp作用域由大到小 过滤器有哪些作用&#xff1f; 过滤器的用法&#xff1f;&#xff08;对客户端的请求统一编码和对客户端进行认证&#xff09; JSP和Servlet中的请求转发分别如何实现&#xff1f; JSP 和 Servlet 有哪些相同点和不同点&#xff0c;他们之间的联系是什么…

恭喜!H医生一个月内荣获美国芝加哥大学访问学者邀请函

➡️【院校背景】 芝加哥大学&#xff08;英文&#xff1a;The University of Chicago&#xff0c;简称UChicago、“芝大”&#xff09;由石油大王约翰洛克菲勒于1890年创办&#xff0c;坐落于美国伊利诺伊州芝加哥市&#xff0c;一所私立研究型大学&#xff0c;属于全球大学校…

vue3 滚动条滑动到元素位置时,元素加载

水个文 效果 要实现的思路就是&#xff0c;使用IntersectionObserver 检测元素是否在视口中显示&#xff0c;然后在通过css来进行动画载入。 1.监控元素是否视口中显示 const observer new IntersectionObserver((entries) > {entries.forEach((entry) > {if (entry.i…