C/C++编程:对象

1059 篇文章 278 订阅

C++程序可以创建、销毁、引用、访问并操作对象

在C++中,一个对象可以拥有这些性质

  • 大小(可以用sizeof获取)
  • 对齐要求(可以使用alignof获取)
  • 存储期(自动、静态、动态、线程局部);
  • 生存期(与存储期绑定或者临时)
  • 类型;
  • 值(可能是不确定的,例如默认初始化的非类类型);
  • 名字(可选)。

以下实体都不是对象:值,引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。

变量由声明所引入,是一个对象或不是对非静态数据成员的引用。

对象创建

对象能由定义、new 表达式、throw表达式、更改联合体的活跃成员和求值要求临时对象的表达式显示创建。显式对象创建中创建的对象时唯一定义的

吟诗生存期类型的对象也可以由下列操作隐式创建

  • 开始 char、unsigned char 或 std::byte (C++17 起)数组生存期的操作,该情况下在该数组中创建这种对象,
  • 调用下列分配函数,该情况下在分配的存储中创建这种对象:
    • operator new
    • operator new[]
    • std::malloc
    • std::calloc
    • std::realloc
    • std::aligned_alloc (C++17 起)
  • 调用下列对象表示复制函数,该情况下在目标存储区域或结果中创建这种对象:
    • std::memcpy
    • std::memmove
    • std::bit_cast (C++20 起)
#include <cstdlib>
struct X { 
    int a, b; 
};

X *MakeX()
{
    // 可能的有定义行为之一:
    // 调用 std::malloc 隐式创建一个 X 类型对象及其子对象 a 与 b ,并返回指向该 X 对象的指针
    X *p = static_cast<X*>(std::malloc(sizeof(X)));
    p->a = 1;
    p->b = 2;
    return p;
}

对象表示与值表示

  • 对于一个 T 类型的对象,其`对象表示 是和它开始于同一个地址,且长度为 sizeof(T) 的一段 unsigned char(或等价的 std::byte) (C++17 起)类型的对象序列
  • 对象的值表示 则是用于持有它的类型 T 的值的位的集合
  • 对于可平凡赋值类型,其值表示是对象表示的一部分,这意味着复制该对象在存储中所占据的字节就足以产生另一个具有相同值的对象(除非这个值是该类型的一个“陷阱表示”,将它读取到 CPU 中会产生一个硬件异常,就像浮点值的 SNaN(“Signaling NaN 发信非数”)或整数值的 NaT(“Not a Thing 非事物”))。
  • 反过来不一定是对的:可平凡复制 (TriviallyCopyable) 类型的两个具有不同对象表示的对象可能表现出相同的值。例如,浮点数有多种位模式都表示相同的特殊值 NaN 。更常见的是,对象表示的一些位可能根本不参与值表示;这些位可能是为了满足对齐要求,位域的大小等得以满足而填充其间的。
#include <cassert>
struct S {
    char c;  // 1 字节值
             // 3 字节填充(假设 alignof(float) == 4 )
    float f; // 4 字节值  (假设 sizeof(float) == 4 )
    bool operator==(const S& arg) const { // 基于值的相等
        return c == arg.c && f == arg.f;
    }
};
 
void f() {
    static_assert(sizeof(S) == 8);
    S s1 = {'a', 3.14};
    S s2 = s1;
    reinterpret_cast<unsigned char*>(&s1)[2] = 'b'; // 更改填充的第 2 字节
    assert(s1 == s2); // 值并未更改
}
  • 对于charsigned charunsigned char类型的对象,除非它们是大小过大的位域,否则其对象表示的每个位都参与其值表示,而且每一个位模式都表示一个独立的值(没有填充位或陷阱位,不允许值的多种表示)

子对象

一个对象可以拥有子对象。子对象包括

  • 成员对象
  • 基类子对象
  • 数组对象

不是其他任何对象的子对象的对象称为完整对象

如果子对象是下面之一,则它潜在重叠

  • 基类子对象,或
  • 声明有 [[no_unique_address]] 属性的非静态数据成员。
    (C++20 起)

完整对象、成员对象和数组对象也被称为最终派生对象,也便和基类子对象分开。既非潜在重叠亦非位域的对象的大小不能为零(基类子对象的大小可能为零,即使无 [[no_unique_address]] 也是如此 (C++20 起):参见空基类优化

一个对象能含有其他对象,该情况下被含有的对象内嵌于前序对象。若符合下列条件,则对象 a 内嵌于另一对象 b :

  • a 是 b 的子对象,或
  • b 为 a 提供存储,或
  • 存在对象 c ,其中 a 内嵌于 c 而 c 内嵌于 b 。

任何两个具有交叠的生存期的(非位域)对象必然有不同的地址,除非其中一个对象内嵌于另一个对象,或者两个对象都是同一个完整对象中的不同类型的子对象,且其中一个是大小为零的子对象。

static const char c1 = 'x';
static const char c2 = 'x';
assert(&c1 != &c2); // 值相同,地址不同

多态对象

声明或继承了至少一个虚函数的类类型的对象是多态对象

每个多态对象中,实现都会储存额外的信息(在所有现存的实现中,如果没被编译器优化掉的话,这就是一个指针),它被用于进行虚函数的调用,RTTI 功能特性(dynamic_cast 和 typeid)也用它在运行时确定对象创建时所用的类型,而不管使用它的表达式是什么类型。

对于非多态对象,值的解释方式由使用对象的表达式所确定,这在编译器就已经决定了

#include <iostream>
#include <typeinfo>
struct Base1 {
    // 多态类型:声明了虚成员
    virtual ~Base1() {}
};
struct Derived1 : Base1 {
    // 多态类型:继承了虚成员
};
 
struct Base2 {
    // 非多态类型
};
struct Derived2 : Base2 {
    // 非多态类型
};
 
int main()
{
    Derived1 obj1; // object1 创建为类型 Derived1
    Derived2 obj2; // object2 创建为类型 Derived2
 
    Base1& b1 = obj1; // b1 指代对象 obj1
    Base2& b2 = obj2; // b2 指代对象 obj2
 
    std::cout << "b1 的表达式类型: " << typeid(decltype(b1)).name() << '\n'
              << "b2 的表达式类型: " << typeid(decltype(b2)).name() << '\n'
              << "b1 的对象类型: " << typeid(b1).name() << '\n'
              << "b2 的对象类型: " << typeid(b2).name() << '\n'
              << "b1 的大小: " << sizeof b1 << '\n'
              << "b2 的大小: " << sizeof b2 << '\n';
}

在这里插入图片描述

对齐

  • 每个对象类型都有被称为对齐要求的性质,它是一个整数(类型为 std::size_t,总是 2 的幂),表示这个类型的不同对象所能分配放置的连续相邻地址之间的字节数
  • (自C++11起)可以用alignof或者std::alignment_of来查询类型的对其要求。可以使用指针对齐函数 std::align来获取某个缓冲区中经过适当对齐的指针,
  • 每个对象类型在该类型的所有对象上强制该类型的对齐要求,(C++11 起) 可以使用 alignas 来要求更严格的对齐(更大的对齐要求)。
  • 为了使类中的所有非静态成员都符合对齐要求,会在一些成员后面插入一些填充。
#include <iostream>
 
// S 类型的对象可以在任何地址上分配
// 因为 S.a 和 S.b 都可以在任何地址上分配
struct S {
  char a; // 大小:1,对齐:1
  char b; // 大小:1,对齐:1
}; // 大小:2,对齐:1
 
// X 类型的对象只能在 4 字节边界上分配
// 因为 X.n 必须在 4 字节边界上分配
// 因为 int 的对齐要求(通常)就是 4
struct X {
  int n;  // 大小:4,对齐:4
  char c; // 大小:1,对齐:1
  // 三个填充字节
}; // 大小:8,对齐:4
 
int main()
{
    std::cout << "sizeof(S) = " << sizeof(S)
              << " alignof(S) = " << alignof(S) << '\n';
    std::cout << "sizeof(X) = " << sizeof(X)
              << " alignof(X) = " << alignof(X) << '\n';
}

在这里插入图片描述
最弱的对齐(最小的对齐要求)是char、signed char 和 unsigned char 的对齐,等于 1 ;所有类型中最大的基础对齐(fundamental alignment)是实现定义的,并等于 std::max_align_t的对齐。 (C++11 起)

当使用 alignas 使某个类型的对齐比 std::max_align_t 的更严格(更大)时,称其为具有扩展对齐要求的类型。

  • 具有扩展对齐的类型或包含具有扩展对齐的非静态成员的类类型称为过对齐类型。
  • new 表达式、 (C++17 前)std::allocator::allocate 和 std::get_temporary_buffer (C++20 前) 是否支持过对齐类型是由实现定义的。
  • 以过对齐类型实例化的分配器 (Allocator) 允许在编译期发生实例化失败,在运行时抛出 std::bad_alloc 异常,静默忽略不支持的对齐要求,也允许正确地处理它们

alignof

作用

  • 查询类型的对齐要求

语法

  • alignof( 类型标识 )

返回值:

  • 返回 std::size_t 类型的值。

解释:

  • 返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以是完整对象类型、元素类型完整的数组类型或者是到这些类型之一大的引用类型
  • 若类型为引用类型,则运算符返回被引用类型的对齐;
  • 若类型为数组类型,则返回元素类型的对齐要求。
#include <iostream>
 
struct Foo {
    int   i;
    float f;
    char  c;
};
 
// 注:下面的 `alignas(alignof(long double))` 如果需要可以简化为 
// `alignas(long double)`
struct alignas(alignof(long double)) Foo2 {
    // Foo2 成员的定义...
};
 
struct Empty {};
 
struct alignas(64) Empty64 {};
 
int main()
{
    std::cout << "对齐字节数"  "\n"
        "- char             :" << alignof(char)    << "\n"
        "- 指针             :" << alignof(int*)    << "\n"
        "- Foo 类           :" << alignof(Foo)     << "\n"
        "- Foo2 类          :" << alignof(Foo2)     << "\n"
        "- 空类             :" << alignof(Empty)   << "\n"
        "- alignas(64) Empty:" << alignof(Empty64) << "\n";
}

在这里插入图片描述

#include <iostream>
#include <cstddef>

struct Storage {
    char a;
    int b;
    double c;
    long long d;
};
struct alignas(std::max_align_t) AlignasStorage {
    char a;
    int b;
    double c;
    long long d;
};
int main() {
    std::cout << alignof(Storage) << std::endl;
    std::cout << alignof(AlignasStorage) << std::endl;
    return 0;
}

在这里插入图片描述

std::alignment_of

头文件

  • <type_traits>

原型

template< class T >
struct alignment_of;

作用:

  • alignof一样
#include <iostream>
#include <type_traits>
 
class A {};
 
int main() 
{
    std::cout << std::alignment_of<A>::value << '\n';
    std::cout << std::alignment_of<int>() << '\n'; // 另一种语法
    std::cout << std::alignment_of_v<double> << '\n'; // c++17 另一种语法
}

在这里插入图片描述

std::align

头文件

  • <memory>

原型

void* align( std::size_t alignment,
            std::size_t size,
            void*& ptr,
            std::size_t& space );

参数:

  • alignment - 欲求的对齐量
  • size - 要被对齐的存储的大小
  • ptr- 指向至少有 space 字节的连续存储的指针
    space - 要在其中操作的缓冲区的大小

返回值:

  • ptr 的调整值,或若提供空间太小则为空指针值。

作用:

  • 给定指针 ptr 指定大小为 space 的缓冲区,返回按指定 alignment 为 size 字节数对齐的指针,并减小 space 参数对齐所用的字节数。返回首个对齐的地址。
  • 仅以给定对齐量对齐入缓冲区的所需字节数合适,函数才会修改指针。若缓冲区太小,则函数不做任何事并返回 nullptr
  • 若 alignment 不是二的幂,则行为未定义。

官方文档:https://zh.cppreference.com/w/cpp/memory/align

#include <iostream>
#include <memory>
 
template <std::size_t N>
struct MyAllocator
{
    char data[N];
    void* p;
    std::size_t sz;
    MyAllocator() : p(data), sz(N) {}
    template <typename T>
    T* aligned_alloc(std::size_t a = alignof(T))
    {
        if (std::align(a, sizeof(T), p, sz))
        {
            T* result = reinterpret_cast<T*>(p);
            p = (char*)p + sizeof(T);
            sz -= sizeof(T);
            return result;
        }
        return nullptr;
    }
};
 
int main()
{
    MyAllocator<64> a;
 
    // 分配一个 char
    char* p1 = a.aligned_alloc<char>();
    if (p1)
        *p1 = 'a';
    std::cout << "allocated a char at " << (void*)p1 << '\n';
 
    // 分配一个 int
    int* p2 = a.aligned_alloc<int>();
    if (p2)
        *p2 = 1;
    std::cout << "allocated an int at " << (void*)p2 << '\n';
 
    // 分配一个 int ,对齐于 32 字节边界
    int* p3 = a.aligned_alloc<int>(32);
    if (p3)
        *p3 = 2;
    std::cout << "allocated an int at " << (void*)p3 << " (32 byte alignment)\n";
}

在这里插入图片描述

std::aligned_storage

头文件

  • <type_traits>

原型

template< std::size_t Len, std::size_t Align = /*default-alignment*/ >
struct aligned_storage;

官方文档:https://zh.cppreference.com/w/cpp/types/aligned_storage

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: UML状态图是一种描述系统中对象状态和状态转换的图形化工具,可以用来帮助软件开发人员更好地进行系统设计和开发。这里介绍的《UML状态图的实用C/C++设计》一书是一本针对C/C++程序员的较为实用的指南,主要内容包括UML状态图的基本概念、语法和实践中的应用。该书主要分为两个部分,第一部分介绍了UML状态图的基本概念和语法,包括状态、转移、事件、动作等重要概念。第二部分则探讨了UML状态图在实践中的应用,包括状态机、有限状态机、反应堆等应用场景,还介绍了如何使用C/C++来实现这些状态机。 本书的一大亮点是它提供了许多实用的示例代码,这些代码可以帮助读者更好地理解UML状态图的实际应用。此外,该书还提供了一些有用的技巧和指南,例如如何使用状态模式来设计应用程序、如何使用状态机来控制流程等。总之,《UML状态图的实用C/C++设计》是一本适合C/C++程序员阅读的指南,可以帮助他们更好地理解和应用UML状态图的相关知识,提高软件开发质量和效率。 ### 回答2: UML状态图是一种重要的面向对象设计工具,它可用于描述对象在系统中的状态、转移和行为。在C/C++设计中,使用UML状态图可帮助程序员更好地理解程序的状态和行为,从而更好地进行设计和编码。对于C/C++程序员来说,掌握UML状态图的基本概念和应用是非常必要的。 “UML状态图的实用C/C++设计”是一份很不错的PDF文档,它详细地介绍了UML状态图的基础知识、语法、样例应用等。该文档首先介绍了状态图的基本建模元素,包括状态、转移、事件、活动等,以及状态图的构成、规则和应用场景。接下来,文档就针对C/C++编程的实际需求,分别给出了状态图的样例应用场景和使用方法,包括商店售货状态图、冰箱状态图、基于事件的状态机模型等,每个状态图都有详细的解释和代码实现示例,具有非常实用的参考价值。 笔者认为,UML状态图作为一种重要的设计工具,在C/C++编程的实践中具有广泛的应用前景,不仅可以帮助程序员提高代码质量和软件开发效率,还可以促进团队协作和项目管理。因此,希望更多的C/C++程序员能够学习和掌握UML状态图的使用方法,深入理解其在软件设计中的价值和作用。 ### 回答3: UML状态图是一种描述对象系统状态转换的图形化表示法。它是UML的一个重要组成部分,具有直观、简明易懂、规范的特点,可以将对象的行为和状态进行清晰的建模和描述。 在实际应用中,UML状态图常用于软件设计中,尤其是C/C++程序设计。它可以帮助开发人员更好地理解和分析系统的各种状态和状态转换规则,避免出现实现漏洞和逻辑错误。在设计过程中,通过使用状态图,人们可以更加高效地完成需求分析、架构设计、代码实现、系统测试等工作,从而大大提高软件的质量和效率。 除此之外,UML状态图还可以用于设计和测试硬件系统、模拟控制系统、制定自动化流程等场合,具有广泛的应用前景和发展潜力。 总之,UML状态图是一种非常实用和有用的设计工具,它可以帮助开发人员更加深入地了解系统结构和性能,更好地应对各种挑战和需求。通过熟练掌握它的建模技巧和规范规则,可以有效提升软件开发的质量和效率,实现更加优秀的软件设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值