本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:cpp_primer:7_classes [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:cpp_primer:7_classes [2024/01/14 13:47] (当前版本) – ↷ 链接因页面移动而自动修正 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======类====== | ||
+ | C++ Primer 笔记 第七章\\ | ||
+ | ---- | ||
+ | |||
+ | 在C++中,我们用类来创建自定义类型。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 类的主要概念有两个:< | ||
+ | 数据抽象又主要分为两个部分: | ||
+ | * **interface(接口)**:为类的使用者提供操作功能 | ||
+ | *** implementation(实现)**:类设计者用于定义类的内容。该部分内容用户不能访问;其中包括类的数据成员和操作这些成员的,非一般性使用的函数等等。 | ||
+ | 数据抽象和封装定义了抽象数据类型(// | ||
+ | |||
+ | ====定义抽象数据类型==== | ||
+ | |||
+ | 首先我们来看看以前我们定义的 // | ||
+ | <code cpp> | ||
+ | struct Sales_data { | ||
+ | std::string book_no; | ||
+ | unsigned units_sold = 0; | ||
+ | double revenue = 0.0; | ||
+ | } | ||
+ | </ | ||
+ | 这是一个自定义的类型;但不是一个抽象数据类型。这个类型里所有的数据成员大家都可以访问,并没有 interface 和 implementation 的区别。相比之下书中第一章使用的 // | ||
+ | |||
+ | ===定义成员函数=== | ||
+ | * 类成员函数,属于 implantation,**必须在类中声明**,但可以定义在类的外部。 | ||
+ | * interface 相关的函数,比如一些操作函数等,定义声明都放置于类外部。 | ||
+ | 定义成员函数的写法与普通函数一致: | ||
+ | <code cpp> | ||
+ | std::string isbn() const { return book_no;}; | ||
+ | </ | ||
+ | ==this指针== | ||
+ | <color # | ||
+ | 默认情况下,任意对类成员的**直接使用**,会引用 '' | ||
+ | <code cpp> | ||
+ | total.isbn(); | ||
+ | </ | ||
+ | 实际上该语句完成了三步操作: | ||
+ | - 使用成员访问符,获取了当前对象 '' | ||
+ | - 编译器(隐式的)初始化了名为 '' | ||
+ | - 调用成员函数的时候,编译器将 '' | ||
+ | 整个调用过程实际上可以写成: | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | </ | ||
+ | <color # | ||
+ | 很显然,'' | ||
+ | <code cpp> | ||
+ | (*this).bookno; | ||
+ | this -> bookno;// | ||
+ | </ | ||
+ | 那么先前我们定义的函数 '' | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | </ | ||
+ | <color # | ||
+ | 由于 '' | ||
+ | ==const 成员函数== | ||
+ | <color # | ||
+ | 目的是不希望通过该成员函数修改调用的对象。\\ \\ | ||
+ | <color # | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | </ | ||
+ | 直接在成员函数后加 const 即可。需要注意的是,为了实现上述的效果,这里的 '' | ||
+ | <color # | ||
+ | * 成员函数实际上是通过 '' | ||
+ | * '' | ||
+ | * 如果希望成员函数不能改变调用的对象,'' | ||
+ | * 正常情况下应该直接将 '' | ||
+ | * 由于 '' | ||
+ | 我们将此类的函数称为**常量成员函数**(// | ||
+ | <color # | ||
+ | * const 对象 | ||
+ | * pointer / reference to const | ||
+ | |||
+ | ==类的 scope 和成员函数== | ||
+ | 编译器处理类按照以下顺序处理: | ||
+ | * **优先编译成员的声明** | ||
+ | * 其次才是函数体的处理 | ||
+ | 因此在类中,成员函数对成员的使用**不受成员声明位置的影响**。 | ||
+ | ==在类外部定义函数== | ||
+ | 在类外部定义的成员函数需要满足两个条件: | ||
+ | * 函数的声明与类中必须**完全一致** | ||
+ | * 类外的函数声明前需要**注明来自哪个类** | ||
+ | 我们通过 类名 + scope operator 来注明来源类,比如: | ||
+ | <code cpp> | ||
+ | double Sale_data:: | ||
+ | if (units_sold) { | ||
+ | return revenue / units_sold; | ||
+ | } else { | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | scope operator 意味着将之后的函数所在 scope 都转到了类中。 | ||
+ | ==定义返回当前对象的函数== | ||
+ | 如果需要返回当前对象,则可以使用下列语句: | ||
+ | <code cpp> | ||
+ | return *this; | ||
+ | </ | ||
+ | 实例中的逻辑:当前类的内容 += 新的内容则可以实现为下面的形式: | ||
+ | <code cpp> | ||
+ | Sales_data& | ||
+ | units_sold += rhs.units_sold; | ||
+ | revenue += rhs.revenue; | ||
+ | return *this; //return the object on which the function was called. | ||
+ | } | ||
+ | </ | ||
+ | 再以 | ||
+ | <code cpp> | ||
+ | total.combine(trans); | ||
+ | </ | ||
+ | 的形式调用。注意这里的返回类型是// | ||
+ | ===定义类相关非成员函数=== | ||
+ | <color # | ||
+ | 如果函数在概念上属于类,但不会在类中声明与定义,那这部分函数可以算作非成员函数。非成员函数属于 interface 的一部分,通常用于定义对类的操作。\\ \\ | ||
+ | <color # | ||
+ | 非成员的函数应该在**其对应类所在 header 进行声明**。 | ||
+ | ==使用 i/ostream 类来定义 read 和 print 函数== | ||
+ | |||
+ | I/ | ||
+ | <code cpp> | ||
+ | istream & | ||
+ | { | ||
+ | double price = 0; | ||
+ | is >> item.bookNo >> item.units_sold >> price; | ||
+ | item.revenue = price * item.units_sold; | ||
+ | return is; | ||
+ | } | ||
+ | </ | ||
+ | <code cpp> | ||
+ | ostream & | ||
+ | { | ||
+ | os << item.isbn() << " " << item.units_sold << " " | ||
+ | << | ||
+ | return os; | ||
+ | } | ||
+ | </ | ||
+ | 几个注意事项: | ||
+ | * iostream 类 '' | ||
+ | * 使用iostream 类进行读写的时候一定会造成对对象的修改,因此初始化的时候使用**普通引用**。 | ||
+ | * 定义 read 的时候,因为对 items 中的 revenue 成员进行了写操作,因此 Sales_data 通过普通引用传递。 | ||
+ | * '' | ||
+ | ==定义 Add 函数== | ||
+ | add 函数不修改两个对象,但需要得到两个对象的和,因此我们使用一个临时对象存储结果。由于存在临时对象,因此需要按值传递的方式返回: | ||
+ | <code cpp> | ||
+ | Sales_data add(const Sales_data &lhs, const Sales_data &rhs) { | ||
+ | Sales_data sum = lhs; // copy data members from lhs into sum | ||
+ | sum.combine(rhs); | ||
+ | return sum; | ||
+ | } | ||
+ | </ | ||
+ | ===构造函数=== | ||
+ | |||
+ | 构造函数类用来控制对象初始化的一类特殊的函数。任何时候类对象被创建,都会用构造函数来初始化。构造函数有几个特点: | ||
+ | * 构造函数的**名字与类相同** | ||
+ | * 构造函数**没有返回类型** | ||
+ | * 构造函数可以有**多个**,类似于函数的重载:根据函数参数的类型和数量来区分不同的初始化过程。 | ||
+ | * 构造函数**不能被声明为 //const//** : const 这个属性要激活,必须经过构造函数的初始化。 | ||
+ | ==合成默认构造函数== | ||
+ | 如果我们没有显式的给指定类定义构造函数,那么编译器自己会定义一个构造函数给类。我们称这个构造函数为:**合成的默认构造函数**(// | ||
+ | <color # | ||
+ | 默认构造函数按下面的策略初始化类成员: | ||
+ | - 如果类成员有任何初始值,使用该值初始化成员 | ||
+ | - 如果没有,则对该成员进行**默认初始化** | ||
+ | ==默认构造函数可能无法满足某些类的要求== | ||
+ | - 需要多个构造函数的情况下:在没有用户定义的构造函数时,编译器才会提供构造函数。 | ||
+ | - 默认构造函数可能导致初始化出现问题,比如没有初始化的 Bulid-in type,交予默认构造函数则是 undefined。 | ||
+ | - 默认构造函数无法为某些复杂类初始化:比如类中有类成员,而这个子类没有默认构造函数,那么编译器就无法初始化。 | ||
+ | |||
+ | ==构造函数的定义== | ||
+ | 下列定义了 4 个构造函数: | ||
+ | <code cpp> | ||
+ | Sales_data() = default; //ask compiler to generate the constructor for us | ||
+ | Sales_data(string& | ||
+ | Sales_data(string& | ||
+ | Sales_data(std:: | ||
+ | </ | ||
+ | <color # | ||
+ | <code cpp> | ||
+ | Sales_data() = default; | ||
+ | </ | ||
+ | 这样的写法等同于**手动为当前类提供一个合成默认函数**。'' | ||
+ | <WRAP center round important 100%> | ||
+ | 使用默认构造函数的前提是所有类成员完成初始化。某些编译器可能不支持类成员的初始化,这种情况下必须使构造函数初始化列表来进行手动初始化。 | ||
+ | </ | ||
+ | <color # | ||
+ | 例子中的第二个,第三个构造函数均为正常的自定义构造函数。我们使用**构造函数初始化列表**来进行初始化: | ||
+ | <code cpp> | ||
+ | Sales_data(const std::string s, unsigned n, double p) : | ||
+ | book_no &s, unit_sold(n), | ||
+ | </ | ||
+ | 构造函数的 Parameter 定义了一些用于初始化类成员的变量;而冒号后的列表则被称为初始化列表,用于初始化类成员。\\ \\ | ||
+ | 还有一种写法如第三个构造函数: | ||
+ | <code cpp> | ||
+ | Sales_data(string& | ||
+ | </ | ||
+ | 这种情况下,其他未被构造函数初始化的类成员会以默认构造函数初始化成员的方式进行初始化。\\ \\ | ||
+ | <color # | ||
+ | 函数体为构造函数提供额外的功能。如果类成员只需要初始化列表就能完成初始化,那么函数体就会留空。 | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | // | ||
+ | read(is, *this); | ||
+ | } | ||
+ | </ | ||
+ | ==在类外定义构造函数== | ||
+ | 与外部定义的成员函数一样,外部定义的构造函数需要加上类的作用域: | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | read(is, *this); //read will read a transaction from is into the oject | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | 首x先,该函数只接收了一个 istream 类型参数 '' | ||
+ | <code cpp> | ||
+ | Sales_data item1; | ||
+ | double price = 0; | ||
+ | std::cin >> item1.book_no >> item1.units_sold >> price; | ||
+ | </ | ||
+ | <color # | ||
+ | //read(is, *this)// | ||
+ | |||
+ | ===拷贝,复制,析构=== | ||
+ | C++ 中,编译器不仅支持类的默认初始化,同时也控制类与对象的拷贝,赋值以及析构(// | ||
+ | <code cpp> | ||
+ | total = trans; | ||
+ | </ | ||
+ | 实际上编译器帮你做的是: | ||
+ | <code cpp> | ||
+ | total.bookNo = trans.bookNo; | ||
+ | total.unit_sold = trans.unit_sold; | ||
+ | total.revenue = trans.revenue; | ||
+ | </ | ||
+ | ==某些类无法依赖默认合成的版本== | ||
+ | 某些情况下,默认合成的版本是无法正确的工作的:比如管理动态内存。当然,如果希望编译器进行有效的动态内存管理,可以使用 Vector 和 String。以上三种操作这两种标准库类型都可以以默认的形式正确运行。 | ||
+ | ====访问控制 / 封装==== | ||
+ | <color # | ||
+ | 为了实现封装,C++ 提供了关键 //Public// / //Private// 来对访问进行控制: | ||
+ | * // | ||
+ | * // | ||
+ | <color # | ||
+ | * specifier 的数量不受限置 | ||
+ | * specifier 的作用范围是从当前 specifier 到下一个 specifier 之间 | ||
+ | ===Class & Struct=== | ||
+ | <color # | ||
+ | \\ \\ | ||
+ | 两者的主要区别在于如何定义成员函数: | ||
+ | * struct 中,默认成员都是 public 成员。 | ||
+ | * class 中,默认成员都是 private 成员。 | ||
+ | ===友元 Friend=== | ||
+ | 私有成员是不能被类外部的对象访问的。但我们知道有些非成员函数其实是属于 interface,其实现可能需要访问类成员。我们可以在类里面用 //friend// 关键字使这些函数获得访问类成员的权限,比如: | ||
+ | <code cpp> | ||
+ | friend Sales_data add(const Sales_data&, | ||
+ | </ | ||
+ | 需要注意的是,**友元的声明必须在类里**。当然出现的位置是随意的,不过一般都把友元的声明集体的放到类的开头。 | ||
+ | |||
+ | ==友元函数的声明== | ||
+ | 友元函数的声明**不能代替正经的函数声明**。它的作用只在于**指定访问权限**。如果用户需要调用友元函数,那么我们还需要对被友元的函数重新声明一次。\\ \\ | ||
+ | 通常的做法是在 **class 被定义的头文件** 对友元函数分开提供一般性的声明。 | ||
+ | |||
+ | ====类的其他特性==== | ||
+ | |||
+ | ===类成员的特性=== | ||
+ | |||
+ | 本章范例: | ||
+ | * Screen 类 | ||
+ | * string: 存放 screen 的内容 | ||
+ | * string:: | ||
+ | |||
+ | |||
+ | |||
+ | ==如何在类中使用 type alias 简化类型== | ||
+ | <code cpp> | ||
+ | class Screen { | ||
+ | public: | ||
+ | typedef std:: | ||
+ | using pos2 = std:: | ||
+ | private: | ||
+ | statements...; | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | * 简化类型 | ||
+ | * 将简化的类型交给用户使用,隐藏真正的类型,实现类型的封装 | ||
+ | <color # | ||
+ | 将用户使用的类型在 //public// 中进行 alias 即可: | ||
+ | <code cpp> | ||
+ | public: | ||
+ | typedef std:: | ||
+ | using pos = std:: | ||
+ | </ | ||
+ | <color # | ||
+ | 与一般类成员不同,**定义类型的类成员需要在使用前可见**。因此,type members 一般都**放到类的开头**。 | ||
+ | |||
+ | ==inline成员函数== | ||
+ | |||
+ | 之前提到过,所有类中定义的成员函数都会自动转化为 //inline// 函数。因此,类中定义的函数之前的 inline 关键字是可以省略的。利用这个性质,可以只**对外部定义成员函数添加 inline关键字**,从而达到提高代码可读性的效果: | ||
+ | <code cpp> | ||
+ | inline Screen & | ||
+ | statements; | ||
+ | } | ||
+ | </ | ||
+ | 书上提出了两点建议: | ||
+ | - //inline// 关键字只用在类外定义函数的地方;这样的写法有助于代码的可读性。 | ||
+ | - //inline// 函数最好和对应他的类定义在同一个 header 里。 | ||
+ | |||
+ | ==成员函数的重载== | ||
+ | 成员函数也可以进行重载,重载规则与一般函数一致: | ||
+ | <code cpp> | ||
+ | char get() const { return contents[cursor]; | ||
+ | char get(pos ht, pos wd) const; | ||
+ | </ | ||
+ | ==Mutable数据成员== | ||
+ | <color # | ||
+ | 使用 //Mutable// 关键字可以使目标获得被修改的权限。被 mutable 修饰以后,被修饰的对象将永远可以被修改,即使: | ||
+ | * 对象自身是 const | ||
+ | * 对象自身是 const 对象的的一部分 | ||
+ | * 修改该对象的函数是 const | ||
+ | <color # | ||
+ | * 在一个 const 对象中,你希望只有一小部分成员可以被修改 | ||
+ | * 你希望某个对象可以被 const 函数修改 | ||
+ | 比如下面的例子: | ||
+ | <code cpp> | ||
+ | class SomeClass { | ||
+ | public: | ||
+ | void some_func() const; | ||
+ | private: | ||
+ | mutable size_t ctr; // can change even if in a const object; | ||
+ | } | ||
+ | void SomeClass:: | ||
+ | ctr++; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==类数据成员的初始化== | ||
+ | 书上希望使用 '' | ||
+ | class Window_mgr { | ||
+ | private: | ||
+ | std:: | ||
+ | } | ||
+ | </ | ||
+ | 这里使用了构造函数为 vector 的第一个元素进行了初始化,等于创建了一个空白屏幕。因此可知,在为在类中对成员初始化时,可以由以下两种方式实现: | ||
+ | - 通过 " = " 进行初始化 | ||
+ | - 通过 " { } " 进行初始化 | ||
+ | |||
+ | ===返回 *this 的成员函数=== | ||
+ | 返回值为 '' | ||
+ | <color # | ||
+ | 除了提高程序的性能以外,由于// | ||
+ | <code cpp> | ||
+ | Screen myScreen; | ||
+ | myScreen.move(4, | ||
+ | </ | ||
+ | |||
+ | ==Const 成员函数返回 *this== | ||
+ | 假设我们新建一个成员函数 '' | ||
+ | <code cpp> | ||
+ | const Screen&; | ||
+ | </ | ||
+ | 由于 reference to const 是不能作为左值的,因此下面的调用是有问题的: | ||
+ | <code cpp> | ||
+ | Screen myScreen; | ||
+ | myScreen.move(4, | ||
+ | </ | ||
+ | |||
+ | ==使用重载解决上述的问题== | ||
+ | <color # | ||
+ | 之前的例子实际上说明了一种应用,即同时存在 // non-const// 和 // | ||
+ | 由于常量成员函数会隐性的将 '' | ||
+ | <color # | ||
+ | 函数的重载有一个性质:// | ||
+ | - 一个功能实现函数负责实现功能,比如下例的 '' | ||
+ | - 两个成员函数负责控制返回值的 const 属性,通过编译器让 // | ||
+ | <code cpp> | ||
+ | class Screen { | ||
+ | public: | ||
+ | Screen & | ||
+ | { do_display(os); | ||
+ | const Screen & | ||
+ | { do_display(os); | ||
+ | private: | ||
+ | void do_display(std:: | ||
+ | </ | ||
+ | <color # | ||
+ | - 首先,两个函数都会调用 '' | ||
+ | - 其次,non-const 版本的 '' | ||
+ | - 再次,在返回的时候,两个函数都通过 '' | ||
+ | - 最后,使用重载函数区分 const 的特性,就根据返回对象的 constness 自动选择对应的重载版本了。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 以 *this 解引用形式返回对象是使用重载保证 constness 的关键。 | ||
+ | </ | ||
+ | |||
+ | <code cpp> | ||
+ | Screen myScreen(5, | ||
+ | const Screen blank(5, 3); | ||
+ | myScreen.set('#' | ||
+ | blank.display(cout); | ||
+ | </ | ||
+ | <color # | ||
+ | - 复用性:避免在不同的地方写重复的代码 | ||
+ | - 可读性:随着函数的复杂度提高,这么写会更加让人易懂 | ||
+ | - debug 更方便:如果功能出现问题,只对核心函数 do_display() 排查 bug 就可以 | ||
+ | - 避免额外开销:do_display() 是 inline 函数。 | ||
+ | 总而言之,设计良好的 C++ 程序更家倾向于拥有**多而小的功能实现函数**。需要使用的时候,再通过一系列的其他函数对其调用。 | ||
+ | |||
+ | ===Class Types=== | ||
+ | 对于类来说,每个类都定义了一个不同的类型。**两个类即使成员完全一样,他们也是不同的类型。** | ||
+ | |||
+ | ==类的提前声明== | ||
+ | <color # | ||
+ | 与函数相同,**没有类定义的声明**被称为**类的提前声明**(// | ||
+ | <color # | ||
+ | - 用于定义指向该类的指针、引用。 | ||
+ | - 在**声明函数**的时候,将类类型作为函数的返回值类型: | ||
+ | <code cpp> | ||
+ | class link_screen { | ||
+ | link_screen *prev; | ||
+ | link_screen *next; | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | * 创建对象之前,类必须被定义:编译器需要知道该类对内存空间的要求。 | ||
+ | * 使用该类的指针 / 应用之前,类必须被定义。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 由于上述的要求,类的对象是不能作为该类的成员的。 | ||
+ | </ | ||
+ | |||
+ | ===友元函数再探=== | ||
+ | ==类的友元== | ||
+ | 与函数相同,类也可以作为友元的对象。\\ \\ | ||
+ | <color # | ||
+ | 当我们需要**控制类**,即使用另外一个类对目标类的私有成员进行操作的时候。比如书中的例子,使用 '' | ||
+ | |||
+ | <color # | ||
+ | 与友元函数相同,友元类的声明在类定义的开头: | ||
+ | <code cpp> | ||
+ | class Screen { | ||
+ | friend class Window_mgr; // define Window_mgr as a friend of Screen | ||
+ | } | ||
+ | </ | ||
+ | 将 '' | ||
+ | <code cpp> | ||
+ | class Window_msr { | ||
+ | public: | ||
+ | //screen ID | ||
+ | using screen_index = std:: | ||
+ | |||
+ | //reset screen at given position | ||
+ | void clear(screen_index i); | ||
+ | private: | ||
+ | std:: | ||
+ | } | ||
+ | |||
+ | void | ||
+ | Window_msr:: | ||
+ | Screen &s = screens[i]; | ||
+ | s.contents = string(s.height * s.width, ' '); | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round info 100%> | ||
+ | **友元关系并不能传递。因此如果目标类有友元类,其友元类的友元类无法访问目标类的私有成员(你朋友的朋友并不是是你的朋友)**。 | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==类成员函数的友元== | ||
+ | 不仅类可以作为友元的对象,类的**成员函数**也可以**单独作为友元的对象**。比如上例中,单独对 '' | ||
+ | <code cpp> | ||
+ | class Screen { | ||
+ | friend void Window_mgr:: | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | 友元类成员函数时,需要仔细考虑对应实体的声明 / 定义依赖关系。一个总的原则是,**如果某个定义需要其他类型的声明,那么该声明一定要提前完成**,比如上例: | ||
+ | * <color # | ||
+ | 因为接下来 '' | ||
+ | * <color # | ||
+ | '' | ||
+ | <code cpp> | ||
+ | using screen_index = std:: | ||
+ | std:: | ||
+ | </ | ||
+ | 如果这里没有 '' | ||
+ | * <color # | ||
+ | | ||
+ | * <color # | ||
+ | 如果没有对 '' | ||
+ | * <color # | ||
+ | 此时 '' | ||
+ | |||
+ | Ref: [[https:// | ||
+ | ==函数的重载和友元== | ||
+ | 重载过的函数尽管名字相同,但却是两个不同的函数。因此,**每一个需要友元的重载版本都需要进行对应的友元声明**。 | ||
+ | ==友元函数和 scope== | ||
+ | <color # | ||
+ | 友元函数的声明要求在**类外**必须要有该函数对应的一般声明。\\ \\ | ||
+ | |||
+ | <color # | ||
+ | 归根结底还是函数的作用域问题。由于友元函数的声明**只能保证访问权**而没有创建作用域的功能,因此这种声明只能作出一种假定:我假定该函数作用域已经存在了;也就是说,友元函数声明的时候,默认该函数已经声明过了。\\ \\ | ||
+ | 由于友元函数声明在类中的位置,为了确保该函数的作用域可见,最好的办法就是在类外先声明该函数。 | ||
+ | |||
+ | ====类的 scope==== | ||
+ | 每个类都有自己的 socpe.如果要从外部访问类内部数据,通常有两种方法: | ||
+ | - 通过指针,引用或者对象来进行访问(操作符使用 " | ||
+ | - 通过 scope 操作符("::" | ||
+ | 但是无论哪种方法,访问者都需要有访问权限,比如: | ||
+ | <code cpp> | ||
+ | Screen::pos ht = 24, wd = 80; //using pos to define screen. | ||
+ | Screen scr(ht, wd, ' ' | ||
+ | Screen *p = &scr; // p point to a object scr; | ||
+ | char c = scr.get(); //using object to access member function get(); | ||
+ | p -> get(); // using pointer to access member function. | ||
+ | </ | ||
+ | ==类的 Scope 与外部定义的成员== | ||
+ | 如果想在外部定义类成员,必须指定类的 scope。这点可以通过 类名 + scope 操作符实现: | ||
+ | <code cpp> | ||
+ | void Window_mgr:: | ||
+ | statements.... | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | |||
+ | 默认情况下,外部定义的函数返回类型并没有处于类的 scope下。如果返回类型属于类成员,那么**必须给该返回类型也指定类的 scope**。比如下面的函数需要返回一个类成员类型: | ||
+ | <code cpp > | ||
+ | // | ||
+ | screen_index add_scrren(const Screen& | ||
+ | |||
+ | // | ||
+ | Window_msr:: | ||
+ | Window_msr:: | ||
+ | screens.pushback(scr); | ||
+ | return screens.size() - 1; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===名字查找和类作用域=== | ||
+ | **名字查找**(// | ||
+ | - 首先查找当前 block 是否有对应 name 的声明,只有 name 没有使用过的声明才有效 | ||
+ | - 如果没有结果,查找外层 scope(s)。 | ||
+ | - 若最终还是没有找到,则程序返回错误。 | ||
+ | |||
+ | 但对于**类成员函数中使用的变量名**,编译器有不同的策略: | ||
+ | - 首先处理所有类成员的名字声明(假设成员函数会使用类中的名字) | ||
+ | - **类可见后**,才会处理该函数的定义部分。 | ||
+ | <color # | ||
+ | 为了方便成员函数的定义。如果按照一般声明可见后才可以使用类型的规则,那么函数中的变量名只能用已经可见的哪些。\\ \\ | ||
+ | <color # | ||
+ | 只能应用于**成员函数内部**中出现的 name。成员函数自身的**返回值类型**、**parameter 类型** 需要遵循一般的 name lookup 规则,也就是使用该类型之前必须声明该类型。 | ||
+ | ===用于类成员的名字查找=== | ||
+ | 对于类层级的名字(类成员的名字,成员函数的 parameter 和 返回值名),遵循一般的规则: | ||
+ | - **使用前声必须可见**:类型的声明必须先于使用 | ||
+ | - **由内往外找**:如果在**类定义**中找不到该类型的声明,那么编译器会转到该类被定义的作用域里去找 | ||
+ | 比如下面的例子: | ||
+ | <code cpp> | ||
+ | typedef double Money; | ||
+ | string bal; | ||
+ | class Account { | ||
+ | public: | ||
+ | Money balance() { return bal; } | ||
+ | private: | ||
+ | Money bal; | ||
+ | </ | ||
+ | 根据之前的规则: | ||
+ | * 由于 //Money// 名字是返回类型,因此需要遵循一般的 name lookup 三步原则: | ||
+ | - 查找**当前 scope 中**,名字**首次**出现**之前的部分**,这里的 scope 是类 '' | ||
+ | - 继续查找外层的 scope,此时查找到 //Money// 的声明。 | ||
+ | * 而 '' | ||
+ | - 首先查找**整个类**,查找到了 '' | ||
+ | - 由于编译器会先处理类的声明使其可见,再处理成员函数的定义,因此 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 这里看出明显的区别。遵循一般名字查找规则的名字只会搜索当前 scope 中**名字第一次可见之前的区域**;而满足要求的类中名字,则会搜索**整个类**的 scope。 | ||
+ | </ | ||
+ | |||
+ | ==Type Names 需要特殊处理== | ||
+ | 需要注意的是,不同于一般名字的特性,**C++ 不允许经过 type alias 的名字在内部和外部 scope 同时使用**。比如下例的 //Money// 类型,由于外部已经有定义了,因此不允许在类中再定义,即便是同类型也不可以: | ||
+ | <code cpp> | ||
+ | typedef double Money; | ||
+ | string bal; | ||
+ | class Account { | ||
+ | public: | ||
+ | Money balance() { return bal; } //using outter type name Money | ||
+ | private: | ||
+ | Money bal; | ||
+ | typedef double Money; //error, redefined | ||
+ | </ | ||
+ | <WRAP center round tip 100%> | ||
+ | 一般来说,**type alias 的声明应该放到类的起始部位**,这样接下来的声明都可以使用。 | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==成员函数定义中的名字查找== | ||
+ | - 首先查找该函数函数体中的声明。该声明查找的区域只限于**名字第一次出现之前的区域** | ||
+ | - 如果没有查找到该名字,会返回到类中查找;此时查找的区域是**整个类** | ||
+ | - 还没有找到,去类scope 的上一级 scope 查找,查找的范围为**成员函数定义的位置之前** | ||
+ | |||
+ | <WRAP center round important 100%> | ||
+ | 注意:请**不要**为**类成员**和**成员函数内的变量**指定**相同的名字**。 | ||
+ | </ | ||
+ | 下面这个问题在 [[cs: | ||
+ | <code cpp> | ||
+ | int height; // defines a name subsequently used inside Screen | ||
+ | class Screen { | ||
+ | public: | ||
+ | typedef std:: | ||
+ | void dummy_fcn(pos height) { | ||
+ | cursor = width * height; // which height? the parameter | ||
+ | } | ||
+ | private: | ||
+ | pos cursor = 0; | ||
+ | pos height = 0, width = 0; | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,因为 // | ||
+ | <code cpp> | ||
+ | cursor = width * this-> | ||
+ | cursor = width * Screen:: | ||
+ | </ | ||
+ | 此时最外层的 '' | ||
+ | <code cpp> | ||
+ | cursor = width * ::height; //global ' | ||
+ | </ | ||
+ | |||
+ | ==成员函数在外部有定义时的名字查找== | ||
+ | 之前在类中的 name look up 规则依然适用于**外部定义的成员函数**。成员函数在外部进行定义的 scope 会被当做**函数体内部的 scope 处理**。 | ||
+ | <code cpp> | ||
+ | int height; // defines a name subsequently used inside Screen | ||
+ | class Screen { | ||
+ | public: | ||
+ | typedef std:: | ||
+ | void setHeight(pos); | ||
+ | pos height = 0; // hides the declaration of height in the outer scope | ||
+ | }; | ||
+ | Screen::pos verify(Screen:: | ||
+ | void Screen:: | ||
+ | // var: refers to the parameter | ||
+ | // height: refers to the class member | ||
+ | // verify: refers to the global function | ||
+ | height = verify(var); | ||
+ | } | ||
+ | </ | ||
+ | 除此之外,第三条中 “查找的范围为成员函数定义的位置之前” 可以解释这里 '' | ||
+ | - '' | ||
+ | - 此时跳到类 '' | ||
+ | - 最后到外层 scope 查找。由于 '' | ||
+ | |||
+ | ==稍微总结一下== | ||
+ | * 三种查找 | ||
+ | * 普通名字查找 | ||
+ | * 类中的名字查找 | ||
+ | * 成员函数定义中的名字查找 | ||
+ | Ref: | ||
+ | ====再谈构造函数==== | ||
+ | |||
+ | ===构造函数的初始化列表=== | ||
+ | 如果不使用构造函数的初始化列表,也能进行函数的初始化: | ||
+ | <code cpp> | ||
+ | Sales_data:: | ||
+ | bookNo = s; | ||
+ | } | ||
+ | </ | ||
+ | <color # | ||
+ | 严格的说来,该方法并不是初始化,而是先定义了 '' | ||
+ | * 不支持某些必须初始化的类型(比如引用,const 常量,成员没有默认值的类) | ||
+ | * 赋值过程会带来额外的复制开销 | ||
+ | 由此看来,使用初始化列表是非常有必要的。 | ||
+ | |||
+ | ==成员初始化顺序== | ||
+ | **类成员初始化的顺序**与构造函数初始化列表中的顺序无关,**只与类成员在类中出现的位置有关**。\\ \\ | ||
+ | <color # | ||
+ | 如果某个初始化的过程与初始化的顺序有关,那么很可能会带来问题: | ||
+ | <code cpp> | ||
+ | class X { | ||
+ | int i; | ||
+ | int j; | ||
+ | public: | ||
+ | X(int val): j(val), i(j) { } // | ||
+ | </ | ||
+ | 上面的过程,本意是想让 '' | ||
+ | \\ \\ <color # | ||
+ | * 避免使用类成员作为其他类成员的初始值: | ||
+ | <code cpp> | ||
+ | X(int val): i(val), j(val) { }; | ||
+ | </ | ||
+ | * 统一类成员的位置和构造函数初始化的顺序 | ||
+ | ==Default Arguments and Constructors== | ||
+ | 如果一个构造函数**使用其默认初始值**初始化**所有**成员,那么该构造函数即为默认构造函数。 | ||
+ | ===委托构造函数=== | ||
+ | 委托构造函数(// | ||
+ | |||
+ | <code cpp> | ||
+ | class Sales_data { | ||
+ | public: | ||
+ | // nondelegating constructor initializes members from corresponding arguments | ||
+ | Sales_data(std:: | ||
+ | bookNo(s), | ||
+ | // remaining constructors all delegate to another constructor | ||
+ | Sales_data(): | ||
+ | Sales_data(std:: | ||
+ | Sales_data(std:: | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | 上例中有总共 4 个构造函数。实际上,所有的构造函数都通过委托第一个构造函数实现了对应的初始化功能: | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | 注意多重委托的例子: | ||
+ | * 这里的函数体是 istream 函数的。 | ||
+ | * 执行的顺序永远都是从被委托函数开始,从初始化列表到函数体依次执行。委托函数的函数体会在最后执行。 | ||
+ | <WRAP center round info 100%> | ||
+ | 委托函数提供的 arugment 列表必须与被委托函数的 parameter list 匹配。 | ||
+ | </ | ||
+ | |||
+ | ===默认构造函数的角色=== | ||
+ | <color # | ||
+ | 因为在**默认初始化**或者**值初始化**的时候,**必须要使用默认构造函数**来处理。\\ \\ | ||
+ | 默认初始化的场合: | ||
+ | * 定义非静态成员 / 数组时不给出初始值 | ||
+ | * 类自身拥有使用默认合成构造函数初始化的成员 | ||
+ | * 类类型的成员没有显式的使用构造函数初始化列表进行初始化 | ||
+ | 值初始化的场合: | ||
+ | * 数组初始化的时候,提供的初始值个数小于数组维度大小 | ||
+ | * 局部静态变量定义时,没有给定 // | ||
+ | * 使用 //Type()// 的形式进行显式的**值初始化**,比如 // | ||
+ | **缺少默认构造函数将导致以上场合中的变量 / 对象无法进行初始化。** | ||
+ | <code cpp> | ||
+ | class NoDefault { | ||
+ | public: | ||
+ | NoDefault(const std:: | ||
+ | // additional members follow, but no other constructors | ||
+ | }; | ||
+ | struct A { // my_mem is public by default; see § 7.2 (p. 268) | ||
+ | NoDefault my_mem; | ||
+ | }; | ||
+ | A a; // | ||
+ | struct B { | ||
+ | B() {} // error: no initializer for b_member | ||
+ | NoDefault b_member; | ||
+ | }; | ||
+ | </ | ||
+ | 上面的例子中,'' | ||
+ | ==使用默认构造函数== | ||
+ | **默认构造的调用不能写成函数的形式**: | ||
+ | <code cpp> | ||
+ | Sales_data obj(); //error, obj() is a function that returns an object of type ' | ||
+ | Sales_data obj; //ok, obj is a class object | ||
+ | </ | ||
+ | ===类的隐式转换=== | ||
+ | <color # | ||
+ | 指在特定的条件下,会将某个 parameter 直接转换为对应的类类型。\\ \\ | ||
+ | <color # | ||
+ | * 接收**单个** argument 的构造函数 | ||
+ | <code cpp> | ||
+ | string null_book = " | ||
+ | item.combine(" | ||
+ | </ | ||
+ | * 隐式转换只能发生**一次** | ||
+ | <code cpp> | ||
+ | item.combine(" | ||
+ | </ | ||
+ | <color # | ||
+ | 转换过程中建立的临时类对象在相关操作完成之后会丢失掉,比如: | ||
+ | <code cpp> | ||
+ | item.combine(std:: | ||
+ | </ | ||
+ | 该例中,输入的信息会保存到临时类对象中;但当 combine 的操作结束后,输入的信息就会随着临时类对象的消亡而丢失。 | ||
+ | |||
+ | |||
+ | ==阻止这样的隐式转换== | ||
+ | |||
+ | 如果我们不想执行上述的隐式转换,我们可以用 // | ||
+ | <code cpp linenums: | ||
+ | explicit Sale_data(std:: | ||
+ | item.combine(cin); | ||
+ | </ | ||
+ | 几个注意点: | ||
+ | * // | ||
+ | * // | ||
+ | * 对于标准库中的类: | ||
+ | * //string// 的构造函数不是 // | ||
+ | * //vector// 的构造函数是 // | ||
+ | 同时,通过手动指定转换类型的方式也能达到 explicit 的效果: | ||
+ | <code cpp> | ||
+ | item.combine(Sales_data(" | ||
+ | item.combine(static_cast< | ||
+ | </ | ||
+ | ===聚合类(aggregate class)=== | ||
+ | |||
+ | 聚合类是一种用户可以直接访问成员的形式。他的特点是: | ||
+ | * 所有成员都是 //public// 的。 | ||
+ | * 没有定义任何构造函数。 | ||
+ | * 没有类初始值。 | ||
+ | * 没有基类,也没有虚函数。 | ||
+ | 下面就是一个聚合类的例子: | ||
+ | <code cpp> | ||
+ | struct Data { | ||
+ | int ival; | ||
+ | string s; | ||
+ | }; | ||
+ | </ | ||
+ | 对这样的类我们可以使用列表初始化: | ||
+ | <code cpp> | ||
+ | Data vall = { 0, " | ||
+ | </ | ||
+ | 几个要点: | ||
+ | * 列表中的初始值顺序要与聚合类中变量顺序一致。 | ||
+ | * 如果初始化列表中初始值小于类需要的值,那么靠后的成员被**值初始化**。 | ||
+ | * 初始化列表的元素不能超过类成员数量。 | ||
+ | ===字面值常量类=== | ||
+ | |||
+ | <color # | ||
+ | * 类成员都是 //Literal Type// 的聚合类,是**字面值常量类**(// | ||
+ | * 不是聚合类的类,要归纳入 //literal class// 必须满足以下条件: | ||
+ | * 数据成员都是 //literal type//。 | ||
+ | * 类必须**至少包含一个** // | ||
+ | * 如果一个成员有 In-class initializer: | ||
+ | * 如果该成员是 //build-in type// | ||
+ | * 如果该成员是 //class type// | ||
+ | * 必须使用默认定义的析构函数(默认情况下销毁所有对象)。 | ||
+ | ==Constexpr 构造函数== | ||
+ | 对于 //Literal class// 的构造函数,我们可以声明为 // | ||
+ | \\ \\ | ||
+ | 对于 // | ||
+ | * 要么用" | ||
+ | * 要么声明为删除函数形式。 | ||
+ | * 要么遵循构造函数和 // | ||
+ | <code cpp> | ||
+ | class Debug { | ||
+ | public: | ||
+ | constexpr Debug(bool b = true): hw(b), io(b), other(b) {} | ||
+ | constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {} | ||
+ | constexpr bool any() { return hw || io || other; } | ||
+ | void set_io(bool b) { io = b; } | ||
+ | void set_hw(bool b) { hw = b; } | ||
+ | void set_other(bool b) { hw = b; } | ||
+ | private: | ||
+ | bool hw; // hardware errors other than IO errors | ||
+ | bool io; // IO errors | ||
+ | bool other; // other errors | ||
+ | }; | ||
+ | </ | ||
+ | 有几点要注意的是: | ||
+ | * // | ||
+ | * // | ||
+ | <code cpp> | ||
+ | constexpr Debug io_sub(false, | ||
+ | if (io_sub.any()) // equivalent to if(true) | ||
+ | cerr << "print appropriate error messages" | ||
+ | constexpr Debug prod(false); | ||
+ | if (prod.any()) // equivalent to if(false) | ||
+ | cerr << "print an error message" | ||
+ | </ | ||
+ | ====类的静态成员==== | ||
+ | 类的静态成员(// | ||
+ | |||
+ | ===静态类成员的声明=== | ||
+ | 静态类成员的声明在之前添加关键字 '' | ||
+ | <code cpp> | ||
+ | static double interestRate; | ||
+ | static double initRate(); | ||
+ | </ | ||
+ | 注意: | ||
+ | * 静态类成员**存在于类对象之外**,**不与类对象绑定** | ||
+ | * 类对象中没有静态类成员,因此无法对静态类成员使用 '' | ||
+ | <code cpp> | ||
+ | class Account { | ||
+ | public: | ||
+ | void calculate() { amount += amount * interestRate; | ||
+ | static double rate() { return interestRate; | ||
+ | static void rate(double); | ||
+ | private: | ||
+ | std:: | ||
+ | double amount; | ||
+ | static double interestRate; | ||
+ | static double initRate(); | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | ===静态类成员的使用=== | ||
+ | |||
+ | 一般情况下,我们用 scope 运算符访问静态类成员: | ||
+ | <code cpp linenums> | ||
+ | double r; | ||
+ | r = Account:: | ||
+ | </ | ||
+ | 不过即便静态类成员不属于具体的类的对象,我们也依然可以用类的对象,类的引用,或者类的指针来访问它们: | ||
+ | <code cpp> | ||
+ | Account ac1; | ||
+ | Account *ac2 = &ac1; | ||
+ | r = ac1.rate(); //through an Account object or reference | ||
+ | r = ac2-> | ||
+ | </ | ||
+ | 而且,**成员函数可以直接使用静态变量**。 | ||
+ | |||
+ | ===静态类成员的定义=== | ||
+ | * 静态成员需要在**类中声明** | ||
+ | * 静态成员需要在**类外初始化**,初始化时需要加上类 scope | ||
+ | <code cpp> | ||
+ | class Account | ||
+ | { | ||
+ | static double inserestRate; | ||
+ | }; | ||
+ | //init | ||
+ | double Account:: | ||
+ | |||
+ | </ | ||
+ | < | ||
+ | //Static// 关键字只需要在成员**声明**的地方使用**一次**即可。定义该成员不需要再使用。\\ \\ | ||
+ | < | ||
+ | * 必须在类的**外部定义和初始化** | ||
+ | * 只能定义一次 | ||
+ | * 静态成员定义的位置最好是在类定义的文件中,与 non-Inline 函数的定义放置在一起。 | ||
+ | < | ||
+ | 没有。由于静态类成员不是类对象的一部分,因此构造函数不会对其进行初始化。\\ \\ | ||
+ | < | ||
+ | 静态类成员会一直存在到程序结束。其 scope 为整个类的 scope。**静态类成员函数享有一切类成员函数的权限。** | ||
+ | ==在类中初始化静态成员== | ||
+ | * 静态类成员可以在类中以 const integral type 的常量表达式完成初始化 | ||
+ | * constexpr 类型的静态类成员**必须**以 const integral type 的常量表达式完成初始化 | ||
+ | 比如使用初始化的静态类成员作为数组的维度: | ||
+ | <code cpp> | ||
+ | static constexpr int period = 30; //period is a constant expression | ||
+ | double daily_tbl[period]; | ||
+ | </ | ||
+ | 需要注意的是,即便我们在这里对 '' | ||
+ | <WRAP center round tip 100%> | ||
+ | 比较保险的做法是,在外部**重新定义**一下在类内部初始化过的的 constexpr 静态类成员。 | ||
+ | </ | ||
+ | Ref: | ||
+ | ===静态成员和普通成员的区别=== | ||
+ | |||
+ | 静态成员和普通成员的区别主要在于使用的场景: | ||
+ | * 静态成员可以是 incomplete type。这种 incomplete type 可以是其所属类的类型(普通成员必须以指针或者应用的形式才能作为 incomplete type): | ||
+ | <code cpp> | ||
+ | class Bar { | ||
+ | public: | ||
+ | // ... | ||
+ | private: | ||
+ | static Bar mem1; // ok: static member can have incomplete type | ||
+ | Bar *mem2; // ok: pointer member can have incomplete type | ||
+ | Bar mem3; // error: data members must have complete type | ||
+ | }; | ||
+ | </ | ||
+ | * 静态成员可以作为 default argument,因为其独立于类对象之外。如果使用非静态成员作为 default argument,那么该成员实际上是来源于一个没有完成初始化的类对象,是无效的,不存在的 argument。 | ||