本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版 | |||
cs:programming:cpp:courses:cpp_basic_deep:chpt_12 [2024/11/12 06:02] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:courses:cpp_basic_deep:chpt_12 [2024/11/12 06:02] (当前版本) – ↷ 页面名由cs:programming:cpp:courses:cpp_basic_deep:chpt_7改为cs:programming:cpp:courses:cpp_basic_deep:chpt_12 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======类与面向对象编程====== | ||
+ | //第 11 章笔记// | ||
+ | ---- | ||
+ | ====结构体与对象聚合==== | ||
+ | * 将多个对象放置聚合在一起,视作整体 | ||
+ | * 结构体的定义:需要接分号结束(C语言的原因,需要一个分号来确定是结构体的定义) | ||
+ | * 结构体的声明:incomplete type | ||
+ | <code cpp> | ||
+ | // 不可以声明结构体 | ||
+ | Str1 myStr1; //error | ||
+ | // 但可以声明结构体的指针 | ||
+ | Str1* myStr1; | ||
+ | </ | ||
+ | * 一处定义原则:类和结构体都处于单元级别。每个单元都只能有一个结构体定义,但程序可能拥有多个结构体定义。编译的时候结构体必须对翻译单元可见。 | ||
+ | ===数据成员的初始化=== | ||
+ | * 不能使用 auto 定义数据成员: | ||
+ | <code cpp> | ||
+ | // 结构体定义 | ||
+ | struct Str | ||
+ | { | ||
+ | // 类中初始化 | ||
+ | int x = 3; | ||
+ | decltype{3} y; // C++11:使用 decltype 定义变量 | ||
+ | // 不能使用 auto | ||
+ | auto z; // error | ||
+ | // 可以使用 const,引用,限定 | ||
+ | const int g = 3; | ||
+ | |||
+ | }; | ||
+ | // 隐式定义了结构体的内部成员 | ||
+ | // 定义发生在这里,而不是在结构体中 | ||
+ | Str m_str; | ||
+ | |||
+ | // 类中元素会进行默认初始化 | ||
+ | // 默认初始化跟结构体中的变量排序和数量有关 | ||
+ | Str m_str2 = {3}; // x = 3, y = 0, g = 3 | ||
+ | |||
+ | // | ||
+ | Str m_str3 = {.x=3, .y = 4, .g = 5}; | ||
+ | </ | ||
+ | ==mutable 限定符== | ||
+ | * 当结构体对象为 const 时,无法改变结构体内部的值 | ||
+ | * 这时可以将需要修改的变量修饰为 mutable,即可对其修改 | ||
+ | <code cpp> | ||
+ | struct Str1 | ||
+ | { | ||
+ | | ||
+ | }; | ||
+ | // 修改常量对象中的变量 | ||
+ | const Str1 myStr1; | ||
+ | myStr1.x = 1; | ||
+ | |||
+ | </ | ||
+ | ===静态数据成员=== | ||
+ | * 多个对象共享的类数据成员 | ||
+ | * 一般静态成员在**类外定义**,'' | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | // 声明 | ||
+ | static int x; | ||
+ | int y; | ||
+ | }; | ||
+ | |||
+ | // 类外定义(c++98) | ||
+ | // Str::表明 x 属于 Str 域内 | ||
+ | // 为了共享,专门引入一个文件用于定义 | ||
+ | // 编译器处理顺序 header-> | ||
+ | int Str::x; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | Str str1; | ||
+ | Str str2; | ||
+ | |||
+ | str2.x = 100; | ||
+ | // 两个 x 都为100 | ||
+ | std::cout << str1.x << " "<< | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | c++98 中 提供了在类中可以定义静态成员的功能。这是因为某些静态成员需要作为类型的一部分进行初始化。比如使用静态成员作为数组的长度,此时该静态成员是常量。如果使用该静态成员定义数组,如果不允许在类中初始化静态成员,那么对应的数组将无法定义。因此 C++ 98 中规定可以在类中这么用: '' | ||
+ | 其限制在于,不能使用其地址对其访问(undefined reference)。因为值替换是编译期行为,不会构造存储空间给 '' | ||
+ | </ | ||
+ | ==内联静态成员== | ||
+ | C++ 17 中提供了内联静态成员。由于静态成员和内联函数的相似性(多个翻译单元 / 多个类对象共用一份),因此可以如下方式在类中声明静态成员: | ||
+ | <code cpp> | ||
+ | // 只保留一份静态成员的定义 | ||
+ | // 不需要是常量 | ||
+ | // 还可以使用 auto 推断成员 | ||
+ | struct Str | ||
+ | { | ||
+ | inline int array_size = 100; | ||
+ | inline auto array_size2 = 100; | ||
+ | |||
+ | }; | ||
+ | // 还可以直接进行修改 | ||
+ | Str str; | ||
+ | str.array_size = 50; | ||
+ | Str *strPt = &str; | ||
+ | // 直接访问需要指定域 | ||
+ | // 静态成员能被所有对象访问 | ||
+ | Str:: | ||
+ | // 可以通过指针访问 | ||
+ | strPt-> | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 一般会专门提供一个文件用于存放静态成员的定义。 | ||
+ | </ | ||
+ | ==类内部声明相同类型的静态数据成员== | ||
+ | <code cpp> | ||
+ | // 不使用 inline | ||
+ | // 类外定义 | ||
+ | Str Str:: | ||
+ | // 使用 inline | ||
+ | // 类中使用 inline 是不完全类型,需要在外部定义 | ||
+ | inline Str Str:: | ||
+ | </ | ||
+ | ===成员函数=== | ||
+ | * 在结构体中定义的函数,作为结构的一部分 | ||
+ | * 结构体与其成员和成员函数组成类,类是一种抽象数据类型 | ||
+ | * 结构体和外部的桥梁:对内操作数据,对外提供接口 | ||
+ | * 关键字 '' | ||
+ | * 类拥有自己的域 | ||
+ | ==声明和定义== | ||
+ | * 类内定义在类结构简单的时候使用 | ||
+ | * 类外定义在类结构复杂的的使用;外部定义做成链接库,用户通过 header 去调用 | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | // 类内定义 | ||
+ | // 隐式内联,避免多次包含 header 导致的重定义 | ||
+ | void fun1() {}; | ||
+ | |||
+ | // 类外定义的声明 | ||
+ | void fun2(); | ||
+ | }; | ||
+ | |||
+ | // 类外定义 | ||
+ | // 非内联 | ||
+ | // 将声明存储于 Header, 将定义存储于另外一个翻译单元 | ||
+ | void Str::fun2() {}; | ||
+ | // 类外内联定义 | ||
+ | inline void Str::fun2() {}; | ||
+ | </ | ||
+ | ==类会经过编译器的两遍处理== | ||
+ | 类中处理函数和数据成员的逻辑与外部不同。简单来说,不是通过从上到下的顺序来执行的。对于类来说,成员函数较为重要(接口),往往实现会先写函数(约定俗成)。这种情况下,如果按照外部的编译顺序,当函数调用内部数据成员时,成员是不可见的。为了处理这个问题,C++ 会对类进行两遍处理: | ||
+ | - 函数可见时,并不会立即处理函数,而是接着处理其余的部分 | ||
+ | - 函数内部的逻辑会在第二次扫描中处理。 | ||
+ | <WRAP center round important 100%> | ||
+ | 两次处理区分的是函数外部的内容与函数内部的逻辑内容。 | ||
+ | </ | ||
+ | ==尾部类型返回与成员函数== | ||
+ | 通常情况下,C++ 在做函数的类外定义时,必须指定所有参与定义部分的来源(域) | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | int x; | ||
+ | using MyRes = int; | ||
+ | MyRes fun(); | ||
+ | }; | ||
+ | |||
+ | // 注意返回类型 MyRes 是在类中定义的 | ||
+ | // 因此必须指定域 | ||
+ | Str::MyRes Str::fun() | ||
+ | { | ||
+ | return x; | ||
+ | } | ||
+ | </ | ||
+ | 但如果使用尾部返回的写法,C++可以自动推断出该返回类型的来源: | ||
+ | <code cpp> | ||
+ | // 通常返回复杂的类型可以使用这种写法 | ||
+ | auto Str::fun() -> MyRes | ||
+ | { | ||
+ | return x; | ||
+ | } | ||
+ | </ | ||
+ | ==成员函数与this指针== | ||
+ | * '' | ||
+ | * '' | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | int x = 3; | ||
+ | // 实际上参数是 fun(Str *this) | ||
+ | void fun() | ||
+ | { | ||
+ | std::cout << x << std::endl; | ||
+ | } | ||
+ | void fun2(int x) | ||
+ | { | ||
+ | // 如果不使用 this, 则返回的是 fun2 的参数 x(局部变量) | ||
+ | // std::cout << x << std::endl; | ||
+ | // 使用 this 返回类对象中的 x | ||
+ | std::cout << this->x << std::endl; | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | Str myStr; | ||
+ | Str* myStrP = &myStr; | ||
+ | // 调用的是 Str:: | ||
+ | myStr.fun(); | ||
+ | // 使用箭头操作符 | ||
+ | // 等同 (*myStr).fun2() | ||
+ | // 打印 类中的 x,值为 3 | ||
+ | myStrP-> | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | '' | ||
+ | </ | ||
+ | ==常量成员函数== | ||
+ | 由于 '' | ||
+ | <code cpp> | ||
+ | // 不允许该接口改变类成员 | ||
+ | // 实际上,是将 this 的类型从 const Str* 转换为了 const Str* const | ||
+ | void fun() const {...} | ||
+ | </ | ||
+ | ==基于 const 的重载== | ||
+ | 基于上述的特性,C++ 允许同名的函数基于 constness 进行重载: | ||
+ | <code cpp> | ||
+ | // 这是两个不同的函数 | ||
+ | // 参数类型不同 | ||
+ | void fun() {...} // plain this, const Str* | ||
+ | void fun() const {...} // low-const this, const Str* const | ||
+ | </ | ||
+ | |||
+ | ==成员函数的查找与隐藏== | ||
+ | * 函数内部的名称 > 类内部的名称 > 类外部的名称 | ||
+ | * 都找不到会返回错误 | ||
+ | * 需要访问指定的名字时,需要指定**域**(依赖性查找) | ||
+ | ==静态成员函数== | ||
+ | * 可以被所有类对象**共享**的函数 | ||
+ | * 用于描述与类(而不是对象)相关的内容 | ||
+ | * 比如表述固定长度数组的类,其数组长度与类对象的具体信息无关。如果想取回该类所表示数组的长度信息,可以使用静态成员函数来处理。 | ||
+ | * 可以返回静态成员 | ||
+ | * 只能对共享对象进行操作 | ||
+ | <WRAP center round box 100%> | ||
+ | 从原理上来说,成员函数使用 '' | ||
+ | </ | ||
+ | 使用静态成员函数的两种方式: | ||
+ | <code cpp> | ||
+ | // 使用静态函数返回静态成员 | ||
+ | struct Str { | ||
+ | static int size() | ||
+ | { | ||
+ | return x; | ||
+ | } | ||
+ | inline static int x = 11; | ||
+ | }; | ||
+ | // 对象调用 | ||
+ | myStr1.fun(); | ||
+ | // 域调用 | ||
+ | Str::fun(); | ||
+ | </ | ||
+ | ==注意局部静态成员与类静态成员的区别== | ||
+ | <code cpp> | ||
+ | static int size() | ||
+ | { | ||
+ | // 局部静态成员,生存周期从函数被调用到程序结束 | ||
+ | static int x; | ||
+ | // 会返回上面的 x,而不是类中静态成员 x | ||
+ | return x; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | * 这个特性可以利用起来。当处理需要按需使用的共享资源时,可以将静态存储于静态成员函数中;这样只有调用静态成员函数时,才会申请该资源。 | ||
+ | * 另外一种应用更有名:singleton | ||
+ | </ | ||
+ | |||
+ | ==基于引用限定符的重载== | ||
+ | <code cpp> | ||
+ | // 该重载基于调用者的左右值属性来重载 | ||
+ | // C++11 的写法:不可与 98 混用。只要后面有一个加了 & | ||
+ | // 调用者为左值时调用 | ||
+ | void foo() &; | ||
+ | // 调用者为右值时调用 | ||
+ | void foo() &&; | ||
+ | // 调用者为常量左值时 | ||
+ | void foo() const &; | ||
+ | // 通常不使用 | ||
+ | void foo() const &&; | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 关键字:ref-qualified-member-functions。 | ||
+ | </ | ||
+ | ====访问限定符和友元==== | ||
+ | * 允许对内部数据进行封装,再使用成员函数作为外部接口 | ||
+ | * struct 的默认访问权限是 // | ||
+ | * 三种限定符: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | ===友元函数 / 类=== | ||
+ | * 允许从类外访问类中的私有成员 | ||
+ | * 关键字 '' | ||
+ | * 声明在**类中声明** | ||
+ | <WRAP center round box 100%> | ||
+ | * 友元破坏封装,慎用 | ||
+ | * 友元的权限由**被访问的类**提供 | ||
+ | * 友元的访问是**单向**的 | ||
+ | * 友元不受访问限定符的限定 | ||
+ | </ | ||
+ | ==函数 / 类的声明可以以友元的方式在类中声明== | ||
+ | 函数的定义受声明顺序的影响,因此在某些函数的定义牵涉到在其之后(不可见的)类型时,需要对这些类型进行前置声明。如果函数会被定义为友元函数,C++ 允许以友元的方式在**类中**完成首次声明: | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | // fun() 被声明,并视作为 Str 的友元函数 | ||
+ | friend void fun(); | ||
+ | // Str2 类被声明,并被视作 Str 的友元类 | ||
+ | friend class Str2; | ||
+ | | ||
+ | // 注意:使用域限定符会破坏友元的首次声明 | ||
+ | // 此时必须要提前对其作出声明 | ||
+ | // friend void ::fun(); | ||
+ | | ||
+ | int x = 100; | ||
+ | }; | ||
+ | |||
+ | class Str2 { | ||
+ | public: | ||
+ | void print() | ||
+ | { | ||
+ | Str str; | ||
+ | std::cout << str.x << std::endl; | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | void fun() | ||
+ | { | ||
+ | Str str; | ||
+ | std::cout << str.x << std::endl; | ||
+ | } | ||
+ | </ | ||
+ | ==友元函数的类内定义和类外定义== | ||
+ | 在类中定义的友元函数会被隐藏: | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | // 隐藏友元 | ||
+ | // fun() 是友元,不是成员 | ||
+ | // 因此 fun() 的作用域处于 Str 外部 | ||
+ | // 此时编译器会隐藏友元的首次定义,也就是认为 fun() 并没有声明 | ||
+ | friend void fun() | ||
+ | { | ||
+ | Str val; | ||
+ | std::cout << val.x << std::endl; | ||
+ | } | ||
+ | int x = 100; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | // undefined | ||
+ | fun(); | ||
+ | | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | * 减轻编译器负担:友元函数的声明会使搜寻范围扩大。 | ||
+ | * 使用友元函数的类外定义即可解决问题 | ||
+ | * 或使用参数,使用 const 实参类型的依赖查找来实现 | ||
+ | </ | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | // 使用实参依赖关系进行类中的友元函数定义 | ||
+ | friend void fun2(Str& | ||
+ | { | ||
+ | std::cout << val.x << std::endl; | ||
+ | } | ||
+ | int x = 100; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | Str val; | ||
+ | fun2(val); | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | ====特殊成员:构造/ | ||
+ | ===构造函数=== | ||
+ | * 与类同名,无返回值,允许重载 | ||
+ | ==委托构造函数== | ||
+ | * 某个构造函数调用已有构造函数的功能,提高复用性 | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | |||
+ | // 委托构造函数 | ||
+ | // 委托单参数版本给 x 赋值 3 | ||
+ | |||
+ | // 2. 再执行委托构造函数 | ||
+ | Str(): | ||
+ | |||
+ | // 1. 先执行被调用的函数 | ||
+ | Str(int x) | ||
+ | { | ||
+ | this->x = x; | ||
+ | } | ||
+ | |||
+ | void fun() const | ||
+ | { | ||
+ | std::cout << x << std::endl; | ||
+ | } | ||
+ | private: | ||
+ | int x; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | Str str; | ||
+ | str.fun(); | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | ==构造函数的初始化列表== | ||
+ | * 初始化:使用初始值 | ||
+ | * 赋值:复制,使用函数体 | ||
+ | * 提高初始化的性能 | ||
+ | * 不能通过拷贝构造的成员必须通过初始化列表初始化(比如引用) | ||
+ | * 初始化顺序取决于声明顺序,与初始化列表的顺序无关 | ||
+ | * 初始化列表会覆盖类成员的初始化 | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | Str(const std:: | ||
+ | { | ||
+ | // val 以默认初始化构造 | ||
+ | // 复制初始化,低效 | ||
+ | val = strVal; | ||
+ | } | ||
+ | |||
+ | // 初始化列表版本 | ||
+ | // 可读性:初始化列表需要与声明顺序一致 | ||
+ | // 初始化列表会覆盖类内成员初始化 | ||
+ | Str(const std:: | ||
+ | |||
+ | std::string val; | ||
+ | // y 被初始化为 0,而不是 2 | ||
+ | int y = 2; | ||
+ | // 必须通过初始化列表初始化 | ||
+ | int& ref; | ||
+ | }; | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | C++ 中构造与销毁是一个栈的结构,先创建后销毁。如果变量的初始化根据初始化列表中的顺序进行,那么变量的构造销毁顺序就是基于构造函数来进行。这样做会导致 C++ 必须记录每一个构造函数的初始化列表顺序来进行构造和销毁,这对性能会有非常大的影响。因此,初始化顺序只与类中成员声明顺序有关。 | ||
+ | </ | ||
+ | ==默认构造函数== | ||
+ | * 不需要提供任何参数就能进行初始化的构造函数 | ||
+ | * 如果类中没有定义构造函数,则编译器会自动合成一个默认构造函数 | ||
+ | * 合成默认构造函数通过拷贝的方式来进行构造,如果元素中存在无法拷贝初始化的类型,那么合成默认构造函数无法创建 | ||
+ | * 对于抽象数据类型,会调用该类型的默认构造函数 | ||
+ | * 调用缺省构造函数不应该加上括号:'' | ||
+ | * 显式定义合成默认构造函数:'' | ||
+ | ==单一参数构造函数== | ||
+ | * 可以视作一种类型转换函数(比如从buit-in 类型到抽象类型) | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | Str(int x) | ||
+ | : val(x) | ||
+ | {} | ||
+ | |||
+ | int val; | ||
+ | }; | ||
+ | |||
+ | void fun(Str str) | ||
+ | {} | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | // 内置-> | ||
+ | Str str = 3; | ||
+ | // 该转换支持隐式转换 | ||
+ | // 函数调用时,int 转换为了 Str 类型 | ||
+ | fun(3); | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | * 如果希望阻止该转换,则使用 '' | ||
+ | <code cpp> | ||
+ | |||
+ | explicit Str(int x): val(x) {} | ||
+ | // 使用括号,大括号显式转化即可 | ||
+ | fun(Str{3}); | ||
+ | fun(Str(3)); | ||
+ | // 或者使用显式的 cast | ||
+ | Str a = static_cast< | ||
+ | fun(a); | ||
+ | </ | ||
+ | ==拷贝构造函数== | ||
+ | * 用于拷贝相同类型的对象 | ||
+ | * 第一个参数是常量类类型的引用 '' | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | Str() = default; | ||
+ | // | ||
+ | Str(const Str& x): | ||
+ | |||
+ | int val = 3; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | Str m1; | ||
+ | // 第一种拷贝方式 | ||
+ | Str m2 = m1; | ||
+ | // 第二种拷贝方式 | ||
+ | Str m3(m1); | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | *// 为什么使用// | ||
+ | 拷贝构造函数需要类对象的拷贝作为参数进行初始化;如果不使用引用,那么参数的传递需要通过拷贝构造函数来进行。这样就进入了一个死循环。 | ||
+ | </ | ||
+ | ==默认拷贝构造函数== | ||
+ | <code cpp> | ||
+ | // 默认拷贝构造函数的定义 | ||
+ | Str(const Str&) = default; | ||
+ | </ | ||
+ | ==移动构造函数== | ||
+ | * 从输入的对象接过,而不是拷贝资源(类似指针换指向,而不是将指针指向的内容复制到新的空间) | ||
+ | * 输入的对象不会再使用,所以接收参数的类型是 xvalue,**类对象的右值引用** '' | ||
+ | * 基于 '' | ||
+ | * 偷窃资源之后,传入的对象需要保证是合法的(可访问) | ||
+ | * 没有拷贝构造函数时会尝试自动合成移动构造函数(如果存在不能移动的成员那么尝试会失败) | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | Str() = default; | ||
+ | Str(const Str&) = default; | ||
+ | |||
+ | // 拷贝内置类型,移动抽象类型 | ||
+ | // 注意不能加 const,移动本身就会修改类对象的内容,而拷贝不会 | ||
+ | Str(Str&& | ||
+ | |||
+ | // 打印函数 | ||
+ | void fun() | ||
+ | { | ||
+ | std::cout << val << " " << a << std::endl; | ||
+ | } | ||
+ | |||
+ | private: | ||
+ | int val = 3; | ||
+ | std::string a = " | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | |||
+ | Str myStr; | ||
+ | myStr.fun(); | ||
+ | // 调用拷贝构造函数 | ||
+ | Str myCopiedStr = myStr; | ||
+ | // 调用移动构造函数 | ||
+ | Str myMovedStr = std:: | ||
+ | // 3 是拷贝的,myStr 中存在 3 | ||
+ | // " | ||
+ | myStr.fun(); | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 编译器能否成功合成特殊函数的逻辑类似: | ||
+ | * 合成的核心点是对每一个数据成员都采取相同的构造方法(比如拷贝都拷贝,移动都移动) | ||
+ | * 如果是内置类型,则直接拷贝,如果是抽象类型,则调用抽象类型中对应的构造函数(拷贝构造调用拷贝构造等等) | ||
+ | * 如果没有对应的构造函数时: | ||
+ | * 拷贝构造会失败 | ||
+ | * 移动构造会查看是否有拷贝构造的方式,如果没有,也失败 (//C++ 14//) | ||
+ | </ | ||
+ | <code cpp> | ||
+ | |||
+ | struct Str2 | ||
+ | { | ||
+ | // 没有该构造函数时,Str 也无法使用合成构造函数 | ||
+ | // 构造抽象类型成员时,所有的合成构造函数都会调用其本身的构造函数 | ||
+ | Str2() = default; | ||
+ | Str2(const Str2&) {std::cout << "copy cstr called."; | ||
+ | // 开启或关闭看看区别 | ||
+ | // Str2(Str2&& | ||
+ | }; | ||
+ | |||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | Str() = default; | ||
+ | Str(const Str&) = default; | ||
+ | Str(Str&& | ||
+ | |||
+ | // 打印函数 | ||
+ | void fun() | ||
+ | { | ||
+ | std::cout << val << " " << a << std::endl; | ||
+ | } | ||
+ | |||
+ | private: | ||
+ | int val = 3; | ||
+ | std::string a = " | ||
+ | // 当成员包含抽象数据成员时,会调用对应类型的拷贝 / 移动构造函数 | ||
+ | Str2 str2; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | |||
+ | Str myStr; | ||
+ | // 有 Str2 的移动构造函数则会调用移动构造函数 | ||
+ | // 否则,有拷贝构造函数调用拷贝构造函数 | ||
+ | // 否则报错 | ||
+ | Str myMovedStr = std:: | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | ==移动构造函数与异常== | ||
+ | * 异常通常会出现在拷贝过程中 | ||
+ | * 移动操作不涉及拷贝,不会抛出异常 | ||
+ | * 这种情况下,通常使用 '' | ||
+ | <code cpp> | ||
+ | Str(Str&& | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 不引入异常的好处: | ||
+ | * 异常会带来额外的逻辑,会带来性能上的损失 | ||
+ | * 移动机制决定了其不能抛出异常。假设移动的过程是从旧的内存区域到新的区域(比如 vector 的扩容),如果移动过程中出现异常,则: | ||
+ | * 旧区域的内容已经被移动 | ||
+ | * 内容没有移动到新的区域 | ||
+ | 结果就会导致数据完全丢失。实际上,vector 的实现中,如果移动构造函数没有 '' | ||
+ | </ | ||
+ | <WRAP center round important 100%> | ||
+ | '' | ||
+ | </ | ||
+ | ==右值引用对象作为表达式时是左值== | ||
+ | <code cpp> | ||
+ | // 函数体中的 x 是左值 | ||
+ | // 参数的类型是右值引用,意味着可以从里面做移动操作 | ||
+ | // 但在具体的执行中,我们决定不移动操作,而是进行访问 | ||
+ | // 此时是将 x 作为左值来使用 | ||
+ | Str(Str&& | ||
+ | </ | ||
+ | ===Copy & Move Assignment=== | ||
+ | * 本质是以类对象为左算子,对 '' | ||
+ | * 返回类型为 '' | ||
+ | * 参数的 constness 与特殊构造函数一致:拷贝不改变参数(const),移动改变参数 | ||
+ | * 函数返回值为 '' | ||
+ | ==自我赋值的处理== | ||
+ | 以移动元素为例,如果参数是指针,通常赋值经过三步: | ||
+ | * 将被赋值的对象占用的资源释放(通过指针):也就是 '' | ||
+ | * 使用当前的指针指向需要移动的对象所在位置:'' | ||
+ | * 然后将被移动对象的指针空置,避免不必要的访问:即 '' | ||
+ | 但问题在于,如果赋值的两遍都是同一个对象,上面这套逻辑会在第一步就清除掉所有信息。在第二步进行的时候,无论是 '' | ||
+ | 解决办法是提前做一次判断:如果赋值运算符两边为同一个对象,则直接返回当前的类对象: | ||
+ | <code cpp> | ||
+ | if(&rhs == this) { return *this; } | ||
+ | </ | ||
+ | ===析构函数=== | ||
+ | * 负责对象资源的释放 | ||
+ | * 不需要返回类型,不需要提供参数,与类同名 '' | ||
+ | * 先创建后释放;内存会在析构函数**调用完毕之后**才会释放。析构函数的函数体可以用来做一些额外收尾操作(主要是对象的销毁) | ||
+ | * 如果不主动声明,析构函数会合成一个,内部逻辑是调用类成员自身的析构函数。 | ||
+ | * 析构函数不应该出异常。 | ||
+ | ===特殊成员函数的补充=== | ||
+ | ==指针类== | ||
+ | <code cpp> | ||
+ | class PtrStr | ||
+ | { | ||
+ | public: | ||
+ | // 构造函数 | ||
+ | PtrStr(): ptr(new int()) {} | ||
+ | |||
+ | // 拷贝构造函数 | ||
+ | PtrStr(const PtrStr& rhs): | ||
+ | { | ||
+ | std::cout << "call copy cstr\n"; | ||
+ | *ptr = *(rhs.ptr); | ||
+ | } | ||
+ | |||
+ | // 拷贝赋值运算符 | ||
+ | PtrStr& operator=(const PtrStr& rhs) | ||
+ | { | ||
+ | std::cout << "call copy assignment\n"; | ||
+ | if(this == &rhs) | ||
+ | { | ||
+ | return *this; | ||
+ | } | ||
+ | // 如果不需要 reallocate | ||
+ | *ptr = *(rhs.ptr); | ||
+ | |||
+ | // 如果需要 reallocate | ||
+ | // delete ptr; | ||
+ | // ptr = new int(*rhs.ptr); | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | // 移动构造函数 | ||
+ | PtrStr(PtrStr&& | ||
+ | : | ||
+ | { | ||
+ | std::cout << "call move cstr\n"; | ||
+ | rhs.ptr = nullptr; | ||
+ | } | ||
+ | | ||
+ | // 移动赋值运算符 | ||
+ | PtrStr& operator=(PtrStr&& | ||
+ | { | ||
+ | std::cout << "call move assignment\n"; | ||
+ | if(this == &rhs) | ||
+ | { | ||
+ | return *this; | ||
+ | } | ||
+ | delete ptr; | ||
+ | ptr = rhs.ptr; | ||
+ | rhs.ptr = nullptr; | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | // 析构函数 | ||
+ | // 析构函数只能释放当前对象拥有的资源 | ||
+ | ~PtrStr() | ||
+ | { | ||
+ | std::cout << "call dstr\n"; | ||
+ | delete ptr; | ||
+ | } | ||
+ | private: | ||
+ | int* ptr; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | PtrStr myStr; | ||
+ | PtrStr myCopyStr = myStr; | ||
+ | PtrStr myMoveStr = std:: | ||
+ | PtrStr myCopyAss, myMoveAss; | ||
+ | myCopyAss = myCopyStr; | ||
+ | myMoveAss = std:: | ||
+ | return 0; | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | ==default 关键字== | ||
+ | * 只对特殊函数有效 | ||
+ | ==delete 关键字== | ||
+ | * 对所有函数都有效 | ||
+ | * 表示函数**无法被调用** | ||
+ | * 定义方式: | ||
+ | <code cpp> | ||
+ | void fun(int) = delete; | ||
+ | </ | ||
+ | * 和未声明的区别:一个是无法调用,一个是找不到 | ||
+ | * 无法调用的一个大的影响是,某些合成函数不会自动进行合成。 | ||
+ | * 不要为移动构造 / 赋值引入 '' | ||
+ | * 希望可以拷贝,但不能移动:只需要定义拷贝构造即可。编译器不会合成移动 | ||
+ | * 如果不需要拷贝,那么申明拷贝构造为 '' | ||
+ | * C++17以及以后:C++ 17 中,移动初始化会被其他方式顶替。C++17 以后的编译器会忽视被删除的移动构造函数。 | ||
+ | ==特殊成员的合成行为列表== | ||
+ | * 行:用户声明(定义)的特殊函数 | ||
+ | * 列:基于用户声明的特殊函数,编译器是否声明,或者生成合成函数的结果。 | ||
+ | * 红色:可能会被废除的行为(查看标准)\\ | ||
+ | {{ : | ||
+ | [[https:// | ||
+ | ====字面值类 / 成员指针 / bind==== | ||
+ | ===字面值类=== | ||
+ | 字面值类(// | ||
+ | * 所有数据成员必须是数据类型 | ||
+ | * 构造函数必须是 '' | ||
+ | * 必须要有一个平凡的析构函数 | ||
+ | * 成员函数必须是 '' | ||
+ | << | ||
+ | * '' | ||
+ | * '' | ||
+ | </ | ||
+ | <WRAP center round info 100%> | ||
+ | C++ 14 之后允许在 '' | ||
+ | </ | ||
+ | |||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | // 常量版本 | ||
+ | constexpr Str(int val):x(val) {}; | ||
+ | // 运行期执行版本 | ||
+ | Str(double val):x(val) {}; | ||
+ | |||
+ | // 平凡的析构函数 | ||
+ | ~Str() = default; | ||
+ | |||
+ | // 接口成员函数 | ||
+ | // 需要是 constexpr 或 consteval | ||
+ | // C++14 之后默认 constexpr 函数不带 const | ||
+ | constexpr int fun() const | ||
+ | { | ||
+ | return x + 1; | ||
+ | } | ||
+ | private: | ||
+ | // 字面值类型 | ||
+ | int x = 3; | ||
+ | }; | ||
+ | |||
+ | // error | ||
+ | // 字面值类要求构造函数的类型必须是 constexpr 或 consteval | ||
+ | constexpr Str Stra(1); | ||
+ | </ | ||
+ | |||
+ | ==注意 constexpr 与 consteval 的混用问题== | ||
+ | 由于 '' | ||
+ | <code cpp> | ||
+ | // error, b 不是常量表达式,无法调用 consteval 成员 | ||
+ | class Str | ||
+ | { | ||
+ | //.... | ||
+ | consteval int fun() const | ||
+ | { | ||
+ | return x + 1; | ||
+ | } | ||
+ | } | ||
+ | int main { | ||
+ | // error, b 不是常量表达式,无法调用 consteval 成员 | ||
+ | Str b(3.0); | ||
+ | b.fun(); | ||
+ | } | ||
+ | </ | ||
+ | ===类成员指针=== | ||
+ | C++ 允许定义对指定类域成员进行访问的指针。 | ||
+ | <WRAP center round info 100%> | ||
+ | 类成员指针**不支持**指针的**相减**操作。 | ||
+ | </ | ||
+ | |||
+ | ==类成员指针声明== | ||
+ | 声明只要求有**类**的声明,不需要类的定义: | ||
+ | <code cpp> | ||
+ | class Str | ||
+ | { | ||
+ | public: | ||
+ | int x; | ||
+ | void fun() {}; | ||
+ | }; | ||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | // 可以用 auto 简化声明的写法 | ||
+ | |||
+ | // 数据成员指针的声明 | ||
+ | // 类名 + 指针名 | ||
+ | int Str:: | ||
+ | |||
+ | // 成员函数指针的声明 | ||
+ | // 返回类型 + 域:: | ||
+ | void (Str:: | ||
+ | } | ||
+ | </ | ||
+ | ==通过成员指针访问== | ||
+ | 当通过成员指针访问指定成员时,访问的是具体对象中的成员。因此,成员必须实例化。访问方式有两种: | ||
+ | * 使用成员指针访问:操作符 '' | ||
+ | * 使用对象指针访问:操作符 '' | ||
+ | <code cpp> | ||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | // 访问前必须初始化类对象 | ||
+ | Str obj; | ||
+ | // 定义类指针 | ||
+ | Str *ptr_obj = &obj; | ||
+ | // 通过成员指针访问 x | ||
+ | obj.*mem_ptr; | ||
+ | // 通过成员指针访问函数 fun | ||
+ | // 注意括号不能省 | ||
+ | (obj.*memFunc_ptr)(); | ||
+ | // 通过类对象指针访问 x | ||
+ | ptr_obj-> | ||
+ | // 通过类对象指针访问成员函数 fun | ||
+ | (ptr_obj-> | ||
+ | } | ||
+ | </ | ||
+ | ===std:: | ||
+ | '' | ||
+ | <code cpp> | ||
+ | std:: | ||
+ | </ | ||
+ | 应用实例: | ||
+ | <code cpp> | ||
+ | class MyClass { | ||
+ | public: | ||
+ | void display(int x) { | ||
+ | cout << " | ||
+ | } | ||
+ | |||
+ | int add(int a, int b) { | ||
+ | return a + b; | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | int main() { | ||
+ | MyClass obj; | ||
+ | |||
+ | // 使用 std::bind 绑定 display 成员函数 | ||
+ | auto boundDisplay = std:: | ||
+ | boundDisplay(10); | ||
+ | |||
+ | // 使用 std::bind 绑定 add 成员函数 | ||
+ | auto boundAdd = std:: | ||
+ | int result = boundAdd(5, 7); // 调用 boundAdd,相当于 obj.add(5, 7) | ||
+ | cout << " | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </ |