C++ 类和对象详解

类的定义

class ClassName {
   // 类体: 由成员函数和成员变量组成
}; // 注意分号

class 为定义类的关键字,ClassName 为类的名字,{ } 中为类的主体,注意类定义结束时后面的分号。

类中的元素称为类的成员,类中的数据称为类的属性或成员变量,类中的函数称为类的方法或成员函数。

类的访问限定符及封装

类的访问限定符

C++ 实现封装的方式:用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

访问限定符

  1. public 修饰的成员在类外可以直接访问
  2. protected 和 private 修饰的成员在类外不能直接访问
    • 将成员变量声明为 private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性
  3. 访问限定符作用域从该访问限定符出现的位置开始,直到下一个访问限定符出现时为止
  4. class 的默认访问限定符为 private,struct 为 public
  5. C++ 中凡处于同一个 access section 的数据,必定保证以其声明顺序出现在内存布局当中(较晚出现的 members 在 class object 中有较高的地址)。然而被放置在多个 access section 中的数据,排列顺序无规定

注意:访问限定符只在编译时有用,当代码加载到内存后,没有任何访问限定符上的区别。

C++ 中 struct 和 class 的区别是什么?

C++ 需要兼容 C 语言,所以 C++ 中 struct 可以当成结构体去使用。

区别是:struct 的默认访问限定符是 public,class 的默认访问限定符是 private;继承时 struct 默认是公有继承,class 默认是私有继承。

struct Q {
  // 默认访问符为 public
  void Test() {
    cout << "测试继承方式" << endl;
  }
};

class G : Q {  // private 继承
};

struct W : Q { // public 继承
};

什么时候一个人应该使用 struct 取代 class?

当它让人感觉比较好的时候,关键字 struct 或 class 本身并不象征其后随之声明的任何东西。

当人们和教科书中说到 struct 时,他们的意思是一个数据集合体,没有 private data,也没有 data 的相应操作(指 member function)。

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员函数,需要使用 :: 作用域解析符指明成员函数属于哪个类。

类的封装

封装:将数据和操作数据的方法结合,隐藏对象的属性和内部实现细节,仅对外公开接口来和对象交互。

封装本质上是一种管理:使用 protected/private 把成员封装起来,开放一些公有的成员函数合理的访问成员。

封装的理解?

如果某些东西被封装,它就不可见。越多东西被封装,越少的人可以看到它。而越少的人可以看到它,我们就有越大的弹性去变化它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此越多东西被封装,我们改变那些东西的能力就越大。推崇封装的原因:它使我们能够改变事物而只影响有限客户

考虑对象内的数据。越少代码可以看到数据(也就是访问它),越多的数据可被封装,而我们也就越能自由地改变对象数据。有多少代码可以看到某块数据呢?采用一种粗糙的测量,越多函数可访问它,数据的封装性就越低。

类对象模型

类的实例化

用类类型创建对象,就是在内存中分配空间的过程,称为类的实例化。

  1. 类只是一个模型,限定了类有哪些成员,定义一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类的非静态成员变量

类对象的存储方式

存储方式:只保存非静态成员变量,成员函数存放在公共的代码段。

需要多大内存来存储一个 class object?

  • 其 nonstatic data members 的总和大小
  • 加上由于 alignment 的需要而填补的空间
  • 加上为了支持 virtual 而由内部产生的额外负担

空类的大小比较特殊,编译器给了空类 1 个字节来唯一标识这个类。

  • C++ 裁定凡是独立(非附属)对象都必须有非零大小,通常会默默安插一个 char 到空对象内
    • 这使得该 class 的不同 objects 得以在内存中配置独一无二的地址
    • 不适用于 derived class 对象内的 base 成分,因为它们并非独立
    • 即使调用 operator new 申请 0 bytes,也会返回一个合法的指针并带有 1 bytes 空间

继承与 data member

C++ 语言保证出现在 derived class 中的 base class subobject 有其完整原样性

class Concrete {
 private:
  int val;
  char c1;
  char c2;
  char c3;
};

在一台 32 位机器中,每一个 Concrete class object 的大小都是 8 bytes。

Concrete

现在把 Concrete 分裂为三层结构:

class Concrete1 {
 private:
  int val;
  char bit1;
};

class Concrete2 : public Concrete1 {
 private:
  char bit2;
};

class Concrete3 : public Concrete2 {
 private:
	char bit3;
};

此时它们的内存布局如下:

Concrete 内存布局

为什么 Concrete2 不利用 Concrete1 用于对齐的内存,反而像这样占用更多的内存呢?

先来声明一组指针:

Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;
pc1_2 = pc2;	// pc1_2 指向 Concrete2 对象
// 有如下操作
*pc1_2 = *pc1_1;

不管 pc1_1 指向 Concrete1、Concrete2 或是 Concrete3,复制操作应只复制被指 object 的 Concrete1 那一部分,不能破坏 pc1_2 中属于 Concrete2 和 Concrete3 的成员。

继承对象

上图 pc1_2 指向 Concrete2 类型的对象,它复制指向 Concrete1 类型对象的 pc1_1。在 Concrete1 中用于对齐的部分,覆盖了 Concrete2 的 bit2,破坏了对象完整性。

this 指针

this 指针的引出

C++ 编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体中所有对成员变量的操作,都通过该指针访问。只不过所有的操作对用户是透明的,即不需要用户来传递,编译器自动完成。

this 指针的特性

  1. this 指针的类型:ClassType *const
    • 不能改变 this 指针的指向
  2. this 指针本质上是非静态成员函数一个的形参,是对象调用成员函数时,将对象地址作为实参传递给函数,所以对象中不存储 this 指针
class Qgw {
 public:
  void Fun();
  int val = 0;
};

void Qgw::Fun() {
  cout << val << endl;
}

实际上是这样:

void Fun(Qgw *const this) {
  cout << this->val << endl;
}

this 的使用

  1. 调用成员函数时,不能显示传实参给 this
  2. 定义成员函数时,也不能显示声明形参 this
  3. 在成员函数内部,可以显示使用 this

使用类类型空指针调用函数会发生什么?

当类类型指针指向空,调用该类的成员函数时,可能会出现解引用空指针引发崩溃。

Qgw* p = nullptr;
p->Fun();			// 若 Fun() 中有访问成员变量的操作,则会崩溃

默认成员函数

4 个默认成员函数:

  • 默认构造函数:不需要提供实参就能调用的构造函数(用于初始化该类类型的对象)
  • 析构函数:释放对象可能在它的生命周期获得的资源
  • 复制构造函数:提供一个相同类类型实参就能调用的构造函数
  • 复制赋值运算符:提供一个相同类类型实参就能调用,用于复制该实参的非静态成员函数

惟有当这些函数被编译器需要的时候,它们才会被编译器创建出来。为驳回编译器自动提供的函数,可将相应的成员函数声明为 private 并且不予实现或使用 delete 关键字。

这些函数做了什么呢?

default ctor 和 dtor 主要是给编译器一个地方用来放置「藏身幕后」的代码,像是唤起 base classes 以及 non-static members 的 ctors 和 dtors。

编译器所产生的析构函数是个 non-virtual,除非这个 class 的 base class 自身声明有 virtual 析构函数。

至于 copy ctor 和 copy assignment operator,编译器合成版只是单纯将 source object 的每一个 non-static data members 拷贝到 destination object。

构造函数

概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个初始值,并且在对象的生命周期内只调用一次。

特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名字叫构造,但是构造函数的任务并不是开空间创建对象,而是初始化对象

其特征如下

  1. 构造函数没有名字且无法被直接调用,它们在发生初始化时被调用
  2. 如果类中没有显式定义构造函数,并且当编译器需要的时候,编译器会自动生成一个默认构造函数
  3. 如果类中有显式定义构造函数,但不满足编译器需要的话,编译器会扩充已有的构造函数,以满足其需求
  4. 不需要提供实参就能调用的构造函数称为默认构造函数
    • 默认构造函数只能有一个:语法上可以同时存在,调用时会报错,编译器不知道该调用哪个来构造新对象
  5. C++ 将类型分成内置类型和自定义类型
    • 编译器生成的构造函数,对内置类型不做初始化处理
    • 对于自定义类型的成员变量会去调用它的默认构造函数初始化,如果没有就会报错

构造语义学

一般而言编译器对 constructor 的扩充操作大约如下:

  1. 记录在 member initalization list 中的 data members 初始化操作会被放进 constructor 的函数体,并以 members 的声明顺序为顺序
  2. 如果有一个 member 并没有出现在 member initalization list 之中,但它有一个 default constructor,该 default constructor 会被调用
  3. 在那之前,如果 class object 有 virtual table pointer(s),它必须被设定初值,指向适当的 virtual table(s)
  4. 在那之前,所有上一层的 base class constructors 必须被调用,以 base class 的声明顺序为顺序
    • 如果 base class 被列于 member initialization list 中,那么任何显式指定的参数都应该传递过去
    • 如果 base class 没有被列于 member initialization list 中,而它有 default constructor(或 default memberwise copy constructor),那么就调用它
    • 如果 base class 是多重继承下的第二或后继的 base class,那么 this 指针必须有所调整
  5. 在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅
  6. 如果 class 被列于 member initiazation list 中,那么如果有任何显式指定的参数,都应该传过去。若没有列于 list 之中,而 class 有一个 default constructor,也应该调用它
  7. 此外,class 中的每一个 virtual base class subobject 的偏移位置必须在执行期可被存取
  8. 如果 class object 是最底层(most-derived)的 class,其 constructors 可能被调用;某些用以支持这一行为的机制必须被放进来

Default Constructor 的构造操作

编译器需要的时候,才会合成出一个 default constructor,被合成出来的 constructor 只执行编译器所需的行动。

对于 class X,如果没有任何 user-declared constructor,那么会有一个 default constructor 被隐式(implicitly)声明出来。一个被隐式声明出来的 default constructor 将是一个 trivial (浅薄而无能,没啥用的)constructor。

一个 nontrivial default constructor 在 C++ Annotated Reference Manual(ARM)的术语中就是编译器所需要的那种,必要的话会由编译器合成出来。

带有 Default Constructor 的 Member Class Object

如果一个 class 没有任何 constructor,但它内含一个 member object,而后者有 default constructor,那么这个 class 的 implicit default constructor 就是 nontrivial,编译器需要为该 class 合成出一个 default constructor。不过这个合成操作只有在 constructor 真正被调用时才会发生。

在 C++ 不同的编译模块中,编译器如何避免合成出多个 default constructor 呢?

解决方法就是把合成的 default constructor、copy constructor、destructor、assignment copy operator 都以 public 且 inline 方式完成。如果函数太复杂,不适合做成 inline,就会合成出一个 explicit non-inline static 实例。

class Foo {
public:
    // Foo 类的默认构造函数
	Foo() { ... }
};

class Bar {
public:
    Foo foo;
    char* str;
};

被合成的 default constructor 看起来可能如下:

inline Bar::Bar() {
    // 被合成的 default constructor 只满足编译器的需要,而不是满足程序员的需要
    // 将 str 初始化是程序员的责任
	foo.Foo::Foo();
}

假设程序员经由下面的 default constructor 提供了 str 的初始化操作:

Bar::Bar() {
	str = nullptr;
}

这时,我们已经显式定义了 default constructor,编译器没办法合成第二个

编译器的行动是:如果 class A 内含一个或一个以上的 member class objects,那么 class A 的每一个 constructor 必须调用每一个 member classes 的 default constructor。编译器会扩张已存在的 constructor,在其中安插一些代码,使得 user code 被执行之前,先调用必须的 default constructor。于是,上述扩张后的 constructors 可能像这样:

// 扩张后的 default constructor
Bar::Bar() {
	foo.Foo::Foo();
    str = nullptr;
}

如果有多个 class member objects 都要求 constructors 初始化操作,C++ 语言要求以 member objects 在 class 中的声明顺序来调用各个 construcors。这些代码将被安插在 explicit user code 之前。

带有 Default Constructor 的 Base Class

如果一个没有任何 constructors 的 class 派生自一个带有 default constructor 的 base class,那么这个 derived class 的 default constructor 会被视为 nontrivial,并因此需要合成出来。它将调用上一层 base classes 的 default constructor(根据它们的声明顺序)。对于一个后继派生的 class 而言,这个合成的 constructor 和一个被显式提供的 default constructor 没有任何差异。

如果设计者提供多个 constructors,但其中都没有调用基类 default constructor 呢?

编译器会扩张现有的每一个 constructors,将用来调用所有必要的 default constructor 的程序代码加进去。

带有 Virtual Funtion 或 Virtual Base Class 的 Class

另有两种情况,也需要合成出 default constructor。

  1. class 声明(或继承)一个 virtual funtion
  2. class 派生自一个继承串链,其中有一个或更多的 virtual base classes

下面两个扩张动作会在编译期间发生:

  1. 一个 virtual funtions table(在 cfront 中被称为 vtbl)会被编译器产生出来,内放 class 的 virtual funtions 的地址
  2. 在每一个 class object 中,一个额外的 pointer member(也就是 vptr)会被编译器合成出来,内含相关的 class vtbl 的地址
总结

有 4 种情况,会造成编译器必须为未声明 constructor 的 classes 合成一个 default constructor。C++ Standard 把那些合成物称为 implicit nontrivial default constructors。没有存在那 4 种情况而又没有声明任何 constructor 的 classes,我们说它拥有的是 implicit trivial default constructor,它们实际上并不会被合成出来。

在合成出来的 default constructor 中,只有 base class subobjects 和 member class objects 会被初始化。所有其他的 nonstatic data member(如整数、整数指针、整数数组等等)都不会被初始化。

成员初始化器列表

任何构造函数的函数定义的函数体可以在复合语句的花括号之前包含 成员初始化器列表,其语法是冒号字符 : 后随一个或多个 成员初始化器 的逗号分隔列表,每项均具有以下语法:

  1. 类或标识符 ( 表达式列表 )
  2. 类或标识符 花括号初始化列表
class Date {
 public:
  Date(int year, int month, int day)
      : _year(year), 
        _month(month), 
        _day(day) {}

 private:
  int _year;
  int _month;
  int _day;
};
  1. 每个成员变量在初始化器列表中只能出现一次
  2. 下列 4 种情况,必须使用 member initialization list
    • 初始化一个 reference member 时
    • 初始化一个 const member 时
    • 当需要用一组参数调用一个 base class 的 constructor 时
    • 当需要用一组参数调用一个 member class 的 constructor 时
  3. 成员变在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
    • 多继承的声明顺序就是继承的顺序
    • 编译器会操作 initialization list,以适当顺序在 constructors 之内安插初始化操作,并且在任何 explicit user code 之前

对于一个 member class 有参数却不使用 initialization list 的情形:

class Word {
 public:
  Word() {
    _name = 0;
    _cnt = 0;
  }

 private:
  // 不是标准库的 string,只是为了演示
  String _name;
  int _cnt;
};

// 编译器处理后可能如下
Word::Word() {
	// 调用 String 的默认构造
	_name.String::String();
	String temp(0);
	_name.String::operator=(temp);
	temp.String::~String();
	_cnt = 0;
}

更好的方式如下:

Word::Word()
    : _name(0) {
  _cnt = 0;
}

// 编译器处理后如下
Word::Word() {
	// 调用 String(int) constructor
	_name.String::String(0);
	_cnt = 0;
}

类型转换运算符

类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:

operator type() const;

其中 type 表示某种类型。类型转换运算符可以面向任意类型(除了 void 之外)进行定义,只要该类型能作为函数的返回类型。类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。

尽管编译器一次只能执行一个用户定义的类型转换,但用户定义的类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。

explicit 说明符

  1. 指定构造函数或转换函数为显式,即它不能用于隐式转换和复制初始化

explicit 说明符只能在类定义之内的构造函数或转换函数的 声明说明符序列 中出现。

不以说明符 explicit 声明且可以用单个参数调用(C++11 前)的构造函数被称为 转换构造函数

只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。地位相当于「被调用的成员函数所隶属的那个对象」,即 this 指针绝不是隐式转换的合格参与者。

如果需要为某个函数的所有参数(包括 this 指针)进行类型转换,那么这个函数必须是个 non-member。

struct A {
  // 因为 C++11 新增的列表初始化,让 A a4 = {4, 5}; 中 {4, 5} 可以分解逐一传给构造函数
  // 因此 explicit 关键字用法扩展了,不只限于禁止可以单参调用的,也可用于禁止多参调用的
  A(int) {}		  // 转换构造函数
  A(int, int) {}  // 转换构造函数(C++11)
};

struct B {
	explicit B(int) {}
	explicit B(int, int) {}
};

int main() {
  A a1 = 1;			 // OK:复制初始化选择 A::A(int)
  A a3{ 4, 5 };		 // OK:直接列表初始化选择 A::A(int, int)
  A a4 = { 4, 5 };	 // OK:复制列表初始化选择 A::A(int, int)
  
  //  B b1 = 1;		 // 错误:复制初始化不考虑 B::B(int)
  B b3{ 4, 5 };		 // OK:直接列表初始化选择 B::B(int, int)
  //  B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
}

析构函数

概念

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

应该拒绝「对称策略」的想法:你已经定义了一个 constructor,所以你认为提供一个 destructor 也是天经地义的事。事实上,你应该因为「需要」而非「感觉」来提供 destructor。

析构函数不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获异常,然后不传播它们或结束程序。如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数执行该操作。

特性

其特征如下

  1. 一个类有且只有一个析构函数,若未显式定义,在编译器需要时,系统会自动生成默认的析构函数
  2. 每当对象的生存期结束时都会调用析构函数
  3. 默认生成的析构函数对内置类型成员变量不做处理,对自定义类型成员变量,会去调它的析构函数

析构语意学

如果 class 没有定义 destructor,那么只有在 class 内含的 member object(或 class 自己的 base class)拥有 destructor 的情况下,编译器才会自动合成一个出来。

析构函数的执行顺序:

  1. destructor 的函数本体首先被执行
  2. 如果 class 拥有 member class objects,而后者拥有 destructors,那么它们会以其声明顺序的相反顺序被调用
  3. 如果 object 内含一个 vptr,现在被重新设定,指向适当的 base class 的 virtual table
  4. 如果有任何直接的 nonvirtual base classes 拥有 destructor,它们会以其声明顺序的相反顺序被调用
  5. 如果有任何 virtual base classes 拥有 destructor,而目前讨论这个 class 是最尾端的 class,那么它们会以原来的构造顺序的相反顺序被调用

复制构造函数

复制构造函数应该确保复制「对象内的所有成员变量」及「所有 base class 成分」。

不要尝试以某个复制构造函数实现另一个复制构造函数。应该将共同机能放进第三个函数中,并由两个复制构造函数共同调用。

概念

给定该类的类型为 T,第一个形参的类型是 T&、const T&、volatile T& 或 const volatile T&,并且要么没有其他形参,要么其他形参都有默认实参。

Qgw::qin(const Qgw& q, int num = 0) { ... }

复制构造函数会在对象从同类型的另一对象(以直接初始化或复制初始化)初始化时调用,情况包括:

  • 初始化:T a = b;T a(b);,其中 b 的类型是 T
  • 函数实参传递:f(a);,其中 a 的类型是 T 而 fvoid f(T t)
  • 函数返回:在像 T f() 这样的函数内部的 return a;,其中 a 的类型是 T 且它没有移动构造函数

特征

其特征如下

  1. 复制构造函数的 class type 参数必须使用引用传参,使用传值方式会引发无穷递归调用
  2. 若未显示定义,且编译器需要的话,会生成默认的复制构造函数
    • 默认的复制构造函数对内置类型成员逐位完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
    • 对自定义类型成员,会调用它的复制构造函数

Copy Constructor 的构造操作

Default Memberwise Initialization

如果 class 没有提供一个 explicit copy constructors,当 class objects 以相同的 class 的另一个 object 作为初值,其内部是以 default memberwise initialization 手法完成的,也是就把每一个内建的或派生的 data member 的值,从某个 object 拷贝一份到另一个 object 身上。不过它并不会拷贝其中的 member class object,而是对其以递归的方式执行 memberwise initialization。

Default constructors 和 copy constructors 在必要的时候才由编译器产生出来。必要意指当 class 不展现 bitwise copy semantics 时。展现出 bitwise semantics 时,没有必要调用 copy constructions,编译器会执行更快的 memcpy 等内存操作函数。

Bitwise Copy Semantics(位逐次拷贝)

在下面 4 种情况下,一个 class 不展现出 bitwise copy semantics。

  1. 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor 时(不论是被 class 设计者显式的声明,或是被编译器合成)
  2. 当 class 继承自一个 base class 而后者存在一个 copy constructor 时
  3. 当 class 声明了一个或多个 virtual funtions 时
  4. 当 class 派生自一个继承串链,其中有一个或多个 virtual base classes 时

在前两种情况中,编译器必须将 member 或 base class 的 copy constructors 调用操作安插到被合成的 copy constructors 中。

重新设定 Virtual Table 的指针
class Bear : public ZooAnimal {
 public:
  ...
  virtual void Rotate();
};

Bear b;
ZooAnimal za = b;	// 发生切片
za.Rotate();		// 调用的是 ZooAnimal::Rotate()

为什么 Rotate() 所调用的是 ZooAnimal 实例而不是 Bear 实例?此外,如果初始化函数将一个 object 内容完整拷贝到另一个 object 去,为什么 za 的 vptr 不指向 Bear 的 virtual table?

第一个问题的答案是:za 并不是一个 Bear,它是一个 ZooAnimal。

第二个问题的答案是,编译器在初始化和指定操作之间做出了仲裁。编译器必须确保如果某个 object 含有一个或一个以上的 vptrs,那些 vptrs 的内容不会被 source object 初始化或改变。

The compiler must ensure that if an object contains one or more vptrs, those vptr values are not initialized or changed by the source object.

也就是说,合成出来 ZooAnimal copy constructor 会显式设定 object 的 vptr 指向 ZooAnimal class 的 virtual table,而不是从赋值号右边的 class object 中将其 vptr 值拷贝过来。

复制赋值运算符

运算符重载

C++ 为了提高代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,同样有返回值类型、函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

但事与愿违,C++ 的运算符重载并没能提高代码的可读性,反而是一些滥用的重载,降低了代码可读性。

注意

  • 每个「重载操作符」必须至少有一个类类型或枚举类型的操作数
  • 重载的运算符表现尽可能与内建运算符相似
  • 作为成员函数重载时,其形参看起来比操作数少 1,因为有 this 作为第一个形参
  • 不能重载 ::(作用域解析)、.(成员访问)、.*(通过成员指针的成员访问)及 ?:(三元条件)运算符
  • 赋值(=)、下标([ ])、调用(( ))和成员访问箭头(->)的重载必须为成员函数

重载符号

// 前置 ++,返回自增后的值,且返回的是一个左值
int& operator++() {
	*this += 1;
	return *this;
}

// 后置 ++,返回自增前的值,且返回的是一个右值
// 后置 ++,其参数的唯一目的是为了与前置区分,调用时编译器指定为 0
const int operator++(int) {
	int temp = *this;
	operator++();
	return temp;
}
不要重载

千万不用重载 &&、|| 和 ,操作符。

C++ 对于真假表达式采用骤死式评估方式。意思是:一旦该表达式的真假值确定,即使表达式中还有部分尚未检验,整个评估工作仍告结束。

重载后将是 C++ 的函数语义:

  • 当函数调用动作被执行,所有参数值都必须评估完成
  • C++ 语言规范并未明确定义函数调用动作中各参数的评估顺序

复制赋值运算符重载

赋值运算符重载主要有四点:

  1. 参数类型
    • 是否把传入的参数类型声明为常量引用
    • 如果传入的参数不是引用而是实例,那么从实参到形参会调用一次复制构造函数,把参数声明为引用可以避免这样的无谓消耗,提高代码的效率
    • 在赋值运算符内不会改变传入的实例的状态,因此应该为传入的引用参数加上 const 关键字
  2. 返回值
    • 是否把返回值的类型声明为类类型的引用,并且返回 *this
    • 只有返回一个 reference to *this,才可以连续赋值
  3. 检测是否自己给自己赋值
    • 是否判断传入的参数和当前的实例是不是同一个实例,如果是同一个,则不进行赋值操作,直接返回
    • 如果不事先判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当 *this 和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了
  4. 如果没有显式定义赋值运算符重载,且编译器需要的话,会生成默认的赋值运算符重载
    • 内置类型成员,会逐位完成复制
    • 自定义类型成员,会调用它的 operator=
合成时机

一个class 对于默认的 copy assignment operator,在以下情况,不会表现出 bitwise copy 语意:

  1. 当 class 内含一个 member object,而其 class 有一个 copy assignment operator 时
  2. 当一个 class 的 base class 有一个 copy assignment operator 时
  3. 当一个 class 声明了任何 virtual functions 时
  4. 当 class 继承自一个 virtual base class 时

面对「内含 const 成员」或「内含 reference 成员」的类,编译器不会合成。

const 修饰成员函数

将 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能修改类的任何非静态成员。编译器强制实施 bitwise constness,但在实际编写时应该使用 「概念上的常量」(conceptual constness)。

当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。

利用 C++ 一个与 const 相关的摆动场:mutable(可变的)可以在 const 函数中修改成员变量,mutable 释放掉 non-static 成员变量的 bitwise constness 约束。

class Qgw {
 public:
  void Test() const {
    num1 = 1002;	// 无报错
    num2 = 1002;	// 表达式必须是可修改的左值
  }
 private:
  mutable int num1 = 1230;
  int num2 = 1231;
};

static 成员

C++ 对「定义于不同的编译单元内的 non-local static 对象」的初始化相对次序无明确的定义。这是因为,决定它们的初始化次序相当困难,有时根本无解。

编译单元(translation unit)是指产出单一目标的那些源码,基本上是单一源码文件加上其所含入的头文件。

解决该问题的方法很简单,只需要:将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象,这就是单例模式的一个常见实现手法。

这个手法的基础在于:C++ 保证,函数内的 local static 对象会在「该函数被调用期间」「首次遇上该对象定义式」时被初始化。

对于 C 语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。

class Qgw { ... };
Qgw& Fun() {
  static Qgw q;
  return q;
}

概念

声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称为静态成员变量;用 static 修饰的成员函数,称为静态成员函数。静态成员变量一般要在类外初始化

class A {
 private:
	static int _nub;
};

// 需要加上类作用域限定,否则就是创建一个变量
int A::_nub = 0;

注意:若类内定义的整形静态成员变量用 const 修饰,那么可以给初使值。

class A {
 private:
	const static int _nub = 0; 
};

static data 特性

  1. 静态成员被所有类对象所共享,不属于某个具体的实例
  2. 静态成员变量要在类外定义,定义时不添加 static 关键字
  3. 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员和类的普通成员一样,也有 public、protected、private 3 种访问级别

static 成员函数

实际上 member function 被内化为 nonmember 的形式。下面是转化的步骤:

  1. 改写函数的原型以安插一个额外的参数到 member function 中,用以提供一个存取通道,通常被称为 this 指针
  2. 将每一个对 nonstatic data member 的存取操作改为经由 this 指针来存取
  3. 将 member function 重新写成一个外部函数,将函数名经过命名修饰处理,形成新的独一无二的词语

static member function 的特性:

  1. 没有 this 指针,不能直接存取其 class 中的 nonstatic member
  2. 它不能被声明为 const,volatile 或 virtual
  3. 它不需要经由 class object 才被调用

假如有如下的调用,cnt 为类的静态成员函数,下列代码会发生什么转化?

if (fun().cnt() > 1002) {
  ...
}

它会被转化为一个直接调用操作:

// fun() 还是会被调用,以保存副作用
fun();
if (Qgw::cnt() > 1002) {
	...
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值