C++ Primer(二) 类与对象
类与对象
类(class)定义了一种数据类型(成员变量),以及一组与其关联的一组操作(成员函数)。类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量(具体实例,Instance)。
在类体中与类体外定义成员函数的区别为:在类体中定义的成员函数会自动成为内联函数(inline),在类体外定义的不会。
this指针
成员函数可以通过名为this的额外隐式参数来访问调用它的对象(实例化的类)。this只能在成员函数内部使用,其它地方非法,且只有当对象被创建后this才有意义,所有也不能在static成员函数中使用。
this关键字实际是成员函数的隐式形参,在编译阶段由编译器添加到参数列表中,在调用成员函数时将对象的地址作为实参传递给this。
this关键字实际上也是一个const指针(常量指针,顶层const),this指针的值(不是this指针所指对象的值)不能被修改(例如:赋值,递增,递减)。
const成员
默认情况下this指针是指向非常量的指针常量(可以通过指针改变对象值,但不能改变指针指向),它是不可以指向常量对象,所以const类型的类对象不能调用普通的成员函数。
const成员函数(常量成员函数, const member function)的形式是在参数列表的括号后面加上一个const关键字,const关键字修改this指针的类型,使之变成指向常量的指针常量,所以const对象(以及常量对象的引用或指针)只能调用const成员函数。
如果成员被声明为常量成员函数,则它的定义也必须在参数列表后明确指针const属性。
一个const成员函数如果以引用的形式返回this,那么它的返回类型将是*常量引用。
如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
同名的const的成员函数与非const成员函数可以构成重载。
构造函数
构造函数(constructor)实现类对象的初始化过程,构造函数名与类名相同,没有返回类型。构造函数不能被声明成const,const对象直到构造函数完成初始化过程,对象才能真正取得常量属性,所有构造函数在const对象的构造过程中可以向其写值。
如果没有显示定义构造函数,编译器会隐式地定义一个默认构造函数(default constructor)。默认构造函数没有任何参数。如果存在类内初始值,则用它来初始化成员,否则默认初始化该成员。
在c++11中,=default来要求编译器生成构造函数。=default如果在类内部,则默认构造函数是内联的。我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。
构造函数初始值列表
如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
成员的初始化顺序与它们在类定义中的出现顺序一致。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数
委托构造函数(delegating constructor)使用它所属类的其他构造函数执行它自己的初始化过程。
1 | class Sales{ |
转换构造函数
转换构造函数(converting constructor),只接受一个实参,实际上定义了转换为此类类型的隐式转换机制。
只允许一步类类型转换。
类类型转换不是总有效。
构造函数声明为explicit,来抑制构造函数定义的隐式转换。explicit只对一个实参的构造函数有效,多个实参的构造函数不能用于执行隐式转换,所以对这些构造函数无效。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
1 | class Sales{ |
使用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用,不能用于拷贝形式的初始化过程(使用=),而且编译器将不会在自动转换过程中使用该构造函数。
尽管编译器不会将explicit构造函数用于隐式转换过程,但是可以使用这样的构造函数显示地强制进行转换。
1 | item.combine(static_cast<Sales>(cin)); |
访问控制与封装
定义在public说明符之后的成员在整个程序内可被访问。
定义在private说明符之后的成员可以被类的成员函数访问。
struct关键字,在第一个访问说明符之前的成员是public的。
class关键字,在第一个访问说明符之前的成员是private的。
友元
类允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。
通常把友元的声明与类本身放置在同一个头文件中(类的外部)。
友元关系不存在传递性,每个类负责控制自己的友元类或友元函数。
友元函数
友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。可以访问当前类中的所有成员,包括 public、protected、private 属性的。
友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。
一组重载函数声明友元,需要对每一个分别声明。
友元类
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
静态成员
static关键字将成员声明为静态成员,类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
静态成员函数也不与任何对象绑定,它们不包含this指针。静态成员函数不能声明成const的。
静态成员不属于类的某个对象,但是任然可以使用类的对象,引用或者指针来访问静态成员。
在类外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
字面值常量类型的静态成员可以在类的内部初始化,初始值必须是常量表达式。
静态数据成员可以是不完全类型。静态数据成员可以使它所属的类类型,而非静态数据成员只能声明成它所属类的指针或引用。
静态成员可以作为默认实参,非静态成员不能作为默认实参,因为它的值本身属于对象的一部分。
1 | class Bar{ |
构造构造函数
拷贝构造函数(copy constructor)第一个参数是自身类类型的引用,且任何额外参数都有默认值。拷贝构造函数通常不应该是explicit的。
拷贝初始化发生在:
- 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
- 某些类类型还会对它们所分配的对象使用拷贝初始化。
析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数没有参数,不能被重载,所有对于一个给定类,只有有唯一一个析构函数。
1 | class Foo{ |
隐式销毁一个内置指针类型的成员不会delete它所指的对象。智能指针是类类型,具有析构函数,在析构阶段会被自动销毁。
自动调用析构函数的时期:
- 变量在离开其作用域时被销毁。
- 当一个对象被销毁时,其成员被销毁。
- 容器(标准库或数组)被销毁时,其元素被销毁。
- 对于动态分配的对象,当对指向它的指针应用
delete运算符时被销毁。 - 对于临时对象,当创建它的完整表达式结束时被销毁。
- 当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
拷贝控制与资源管理
拷贝赋值运算符
重载运算符的本质是函数赋值运算符就是一个名为operator=函数。
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数,返回一个指向左侧运算对象的引用。
1 | class Foo{ |
行为像值的类
当编写赋值运算符时,需注意:
- 如果将一个对象赋予它自身,赋值运算符必须能正确工作。
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
编写赋值运算符一个好的模式:先将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有成员,拷贝临时对象到左侧运算对象中。
1 | HasPtr& HasPtr::operator=(const HasPtr &rhs){ |
行为像指针的类
引用计数的方式:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,记录多少对象与正在创建的对象共享状态。
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户共享。
- 析构函数递减计数器,指出共享状态的用户少了一个,计数器变为0,则析构函数释放状态。
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器,如果左侧运算对象计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
1 | class HasPtr{ |
三/五法则
需要析构函数的类也需要拷贝和赋值操作。
需要拷贝操作的类也需要赋值操作,反之亦然。
阻止拷贝
在函数的参数列表后加上=delete来定义删除的函数(deleted function),以阻止拷贝构造函数与拷贝赋值运算符的拷贝行为。
1 | struct NoCopy{ |
=delete必须出现在函数第一次声明的时候,且可以对任何函数指定=delete。
对于析构函数已删除的类型,不能定义该类型的变量或(可以动态分配这种类型的对象)释放指向该类型动态分配对象的指针。
当不可能拷贝,赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
交换操作
如果自定义一个swap函数,那么算法将使用自定义的版本,否则使用标准库的swap。
1 | class HasPtr{ |
定义swap的类通常用swap来定义它们的赋值运算符, 拷贝并交换(copy and swap),将左侧运算对象与右侧运算对象的一个副本进行交换。
使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
1 | HasPtr& HasPtr::operator=(HasPtr rhs){ //rhs是副本非指针 |
对象移动
右值引用
右值引用(rvalue reference)&&只能绑定到一个将要销毁的对象。
不能将一个右值引用绑定到一个右值引用类型的变量上。变量是左值,不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
move函数可以获得绑定到左值上的右值引用。调用move后意味着:我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
使用move代码,应该使用std::move而不是move,这样可以避免潜在的名字冲突。
1 | int &&rr1 = 42; |
移动构造函数
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
1 | class StrVec{ |
移动赋值运算符
1 | StrVec &StrVec::operator=(StrVec &&rhs) noexcept{ |
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或者移动赋值时,编译器才会为它合成移动构造函数或者移动赋值运算符。
定义了一个移动构造函数或者移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地定义为删除的。
区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&。
1 | void push_back(const X&); //拷贝:绑定到任意类型的X |
引用限定符(reference qualifier),强制左侧运算对象(即,this指向的对象)是一个左值。
引用限定符可以是&或&&(&用于左值,&&用于右值),分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。
函数可以同时用const和引用限定。引用限定符必须跟随在const限定符之后。
1 | class Foo{ |
如果成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
聚合类
聚合类(aggregate class)使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
- 所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual函数。
1 | struct Data{ |
字面值常量类
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 数据成员都必须是字面值类型。
- 类必须至少含有一个constexper构造函数(字面值常量类的构造函数可以是constexpr)。
- 如果一个数据成员含有类内初始值,则内置数据成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数可以声明成=default形式或者删除函数形式。constexpr构造函数体一般来说应该为空。
1 | class Debug{ |
类成员指针
成员指针(pointer to member)是指可以指向类的非静态成员的指针。成员指针的类型囊括了类的类型以及成员的类型。
数据成员指针
**初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。**成员指针指定了成员而非成员所属的对象,只有当解引用成员指针时,才提供对象的信息。
1 | //指向Screen类的const string成员的指针 |
返回数据成员指针的函数:
1 | class Screen{ |
成员函数指针
指向成员函数指针也需要指定目标函数的返回类型和形参列表(重载说明),如果成员函数具有const成员或引用成员,则我们必须将const限定符或引用限定符包含进来。
1 | char (Screen::*pmf2) (Screen::pos, Screen::pos) const; |
成员指针类型别名
1 | using Action = char (Screen::*) (Screen::pos, Screen::pos) const; |
成员指针函数表
1 | class Screen{ |
可调对象
成员函数指针进行函数调用时,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上,所有成员指针不是一个可调用对象,不支持函数调用运算符。不能将一个指向成员函数的指针传递给算法。
function
1 | vector<string*> pvec; |
mem_fn
1 | find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); |
bind
1 | auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1)); |
嵌套类
嵌套类(nested class)或嵌套类型(nested type):定义在另一个类内部的类。嵌套类是一个独立的类,与外层基本没关系。外层类的对象和嵌套类的对象是相互独立的。嵌套类的对象中不包含任何外层类定义的成员,外层类的对象中也不包含任何嵌套类定义的成员。
嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。嵌套类的名字不会和别的作用域中的同一个名字冲突。
嵌套类在其外层类中定义了一个类型成员。该类型的访问权限由外层类决定。
- 位于外层类public部分的嵌套类可以被随处访问。
- 位于外层类protected部分的嵌套类只能被外层类及其友元和派生类访问。
- 位于外层类private部分的嵌套类定义的类型只能被外层类的成员和友元访问。
和成员函数一样,嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部。
1 | class TextQuery{ |
union
联合(union)是一种特殊的类,一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。分配给union对象的存储空间至少要能容纳它的最大数据成员。
默认情况下,union的成员都是公有的,与struct相同。union不能含有引用类型的成员,含有构造函数或析构函数的类类型也可以作为union的成员类型。
union可以定义包含构造函数和析构函数在内的成员函数, 但是union既不能继承自其它类,也不能作为基类使用,所有union中不能含有虚函数。
为union的一个数据成员赋值会令其它数据成员变成未定义的状态。
1 | union Token{ |
匿名union
匿名(union)是一个未命名的union,不能包含受保护的成员或私有成员,也不能定义成员函数。
1 | union { |
含有类类型成员的union
1 | class Token { |
局部类
局部类(local class)定义在某个函数的内部,局部类的所有成员(包括函数在内)都必须完整的定义在类的内部。
局部类中不允许声明静态数据成员,局部类只能访问外层作用域定义的类型名,静态变量以及枚举类型,如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。
1 | int a, val; |
可以在局部类的内部在嵌套一个类,嵌套类的定义可以出现在局部类之外,但是嵌套类必须定义在局部类相同的作用域中。
1 | void foo() |
