- cpp/
2. 面向对象(OOP)
Table of Contents
面向对象#
面向对象程序设计 (Object-oriented programming,OOP) 是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据、特性、代码与方法。对象则指的是类(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象
封装#
- 将客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
- 关键字:
public
,protected
,private
;不写默认为private
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问
继承#
基类(父类)——> 派生类(子类)
什么不能被继承:
- 构造函数
- 析构函数
- 赋值运算符
- 友元函数(非类成员)
公有继承、保护继承、私有继承
定义
class
类别时,外部可直接使用定义为public
的成员,但不能使用定义为protected
或private
的成员(编译报错)子类
public
继承父类父类的成员 子类能否在外部直接使用父类成员? 子类成员能否在内部使用父类成员? public ✔ ✔ protect ✘ ✔ private ✘ ✘ 子类
protected
或private
继承父类父类的成员 子类能否在外部直接使用父类成员? 子类成员能否在内部使用父类成员? public ✘ ✔ protect ✘ ✔ private ✘ ✘
多态#
- 多态,即多种状态(形态)。简单可将多态定义为消息以多种形式显示的能力。
- 多态是以封装和继承为基础的。
- C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
静态多态 (编译期/早绑定)#
函数重载: 根据不同的参数初始化方式区分函数
重载规则:
- 多个函数定义使用相同的函数名称
- 函数参数的数量或类型必须有区别
函数重载匹配原则:
- 函数名查找
- 确定候选函数
- 寻找"最佳匹配", 即形式参数类型与实际参数类型相匹配
注意事项:
某个类型的参数和该类型的引用参数是一样的, 如
max(double x, double y); max(double &x, double &y);
某个类型的参数和该类型的
const
参数也是一样的, 如max(double x, double y); max(const double x, const double y);
函数重载机制可以区分引用(或指针)和const、volatile引用(或指针)
总结: 重载时根据函数参数的数量、类型进行区分,同时当
const
、volatile
限定引用和指针类型参数时,也可区分
C++原理:
- 引入了函数签名,包括函数的名称和参数列表。函数签名在重载时能够通过参数列表的不同来唯一标识不同版本的函数。
- 引入了类型安全和面向对象编程的特性,通过支持函数重载,C++可更方便地表达相似但具有不同参数的操作。
- C++编译器能够利用函数的参数类型和个数生成正确的函数调用代码。函数签名帮助编译器在解析函数调用时准确匹配函数版本。
示例:
#include <iostream> using namespace std; class Over { public: Over() { cout << "Over default constructor\n"; } Over( Over &o ) { cout << "Over&\n"; } Over( const Over &co ) { cout << "const Over&\n"; } Over( volatile Over &vo ) { cout << "volatile Over&\n"; } }; int main() { Over o1; // Calls default constructor. Over o2( o1 ); // Calls Over( Over& ). const Over o3; // Calls default constructor. Over o4( o3 ); // Calls Over( const Over& ). volatile Over o5; // Calls default constructor. Over o6( o5 ); // Calls Over( volatile Over& ). }
为什么C不支持函数重载?
函数唯一标识规则简单:
C函数的唯一标识符是函数的名称。如果允许函数重载,编译器将难以确定应该调用哪个版本的函数,因为函数名无法唯一标识一个函数。
参数类型信息缺失
C函数的声明和调用仅依赖于函数的名称,而不考虑参数的类型和个数。无法通过函数名区分不同的函数版本。
编译器简单性
C语言编译器不支持更复杂的名称解析和调用规则,因此无法支持函数重载。
动态多态 (运行期/晚绑定)#
虚函数:用
virtual
修饰成员函数,使其成为虚函数注意:
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 内联函数不能是表现多态性时的虚函数,解释见: 虚函数可以是内联函数(inline)吗?
动态多态使用
class Shape { // 形状类 public: virtual double calcArea() { ... } virtual ~Shape(); }; class Circle : public Shape { // 圆形类 public: virtual double calcArea(); ... }; class Rect : public Shape { // 矩形类 public: virtual double calcArea(); ... }; int main() { Shape * shape1 = new Circle(4.0); Shape * shape2 = new Rect(5.0, 6.0); shape1->calcArea(); // 调用圆形类里面的方法 shape2->calcArea(); // 调用矩形类里面的方法 delete shape1; shape1 = nullptr; delete shape2; shape2 = nullptr; return 0; }
虚指针vptr#
含有虚函数就会有虚指针
从类的角度,继承会把父类的内存和成员函数继承下来,函数继承是继承它的调用权
虚函数是通过动态绑定的方法,通过对象中绑定的虚指针vptr找到虚函数表vtbl,再从虚函数表中找到对应的虚函数指针,然后调用函数
C语言中编译器通过静态绑定的方法进行,将当前的地址保存起来,调用汇编语言
CALL
指令跳转到调用的函数中,处理完成后再返回来动态绑定(虚机制,实现多态)的实现和条件:
- 必须是指针
- 指针是up-cast的,类型是父类,可以
new
成子类对象 - 必须是虚函数
#include <iostream> using namespace std; class Base { public: virtual void func() { cout << "\nBase::func\n" << endl; }; }; class Derived : public Base { public: virtual void func() { cout << "\nDerived:func\n" << endl; }; }; Base* p = new Derived(); p->func(); // 调用 Derived:func,符合上述3条 // 如果Derived没有func函数,则调用Base::func
虚表vtbl#
C++实现多态使用的动态绑定技术:虚函数表(虚表)
每个包含了虚函数的类都包含一个虚表;虚表是属于类的,而不属于某个具体的对象,一个类只需要一个虚表即可
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是在代码的编译阶段,虚表就被构造出来
定义抽象基类步骤#
- 找出所有子类共通的操作行为
- 找出哪些操作行为与类型相关,即哪些操作行为必须根据不同的派生类而有不同的实现方法
- 找出每个操作行为的访问层级
虚函数与纯虚函数#
虚函数 (virtual)#
虚函数可以被子类继承和覆盖,通常使用动态调度实现,是OOP中 (运行时) 多态的重要组成部分。简言之,虚函数可以给出目标函数的定义,但该目标函数的具体指向在编译期可能无法确定
C++多态(polymorphism)是通过虚函数来实现的
虚函数使用:
# include <iostream> # include <vector> using namespace std; class Animal { public: virtual void eat() const { cout << "I eat like a generic Animal." << endl; } virtual ~Animal() {}//析构函数 }; class Wolf : public Animal{ public: void eat() const { cout << "I eat like a wolf!" << endl; } }; class Fish : public Animal{ public: void eat() const { cout << "I eat like a fish!" << endl; } }; class GoldFish : public Fish{ public: void eat() const { cout << "I eat like a goldfish!" << endl; } }; class OtherAnimal : public Animal{}; int main(){ std::vector<Animal*> animals; animals.push_back( new Animal() ); animals.push_back( new Wolf() ); animals.push_back( new Fish() ); animals.push_back( new GoldFish() ); animals.push_back( new OtherAnimal() ); std::vector<Animal*>::const_iterator it = animals.begin(); for(; it != animals.end(); ++it) { (*it)->eat(); delete *it; } return 0; }; //输出 //I eat like a generic Animal. //I eat like a wolf! //I eat like a fish! //I eat like a goldfish! //I eat like a generic Animal. //当 Animal::eat() 不是被宣告为虚函数时,输出 //I eat like a generic Animal. //I eat like a generic Animal. //I eat like a generic Animal. //I eat like a generic Animal. //I eat like a generic Animal.
纯虚函数#
- 需要被非抽象的派生类覆盖(override)的虚函数
- 包含纯虚方法的类被称作抽象类;抽象类不能被直接实例化。 一个抽象基类的一个子类只有在所有的纯虚函数在该类(或其父类)内给出实现时, 才能直接实例化
- 纯虚方法通常只有声明(签名)而没有定义(实现),但有特例情形要求纯虚函数必须给出函数体定义
纯虚函数的原理#
- 虚函数是通过一张虚函数表来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)
- 每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址
- 在每个带有虚函数的类中,编译器秘密地置入一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化
总结#
- 定义一个函数为虚函数,不代表函数为不被实现的函数,是为了允许用基类的指针来调用子类的这个函数。
- 定义一个函数为纯虚函数,才代表函数没有被实现,为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
- 纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
- 虚函数声明如下:virtual ReturnType FunctionName (Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为: error LNK: unresolved external symbol “public: virtual void __thiscall ClassName::virtualFunctionName(void)”
- 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
- 实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
- 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
- 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
- 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
- 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类,具体见下文虚继承。
虚函数可以是内联函数(inline)吗?#
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。inline virtual
使用:#include <iostream> using namespace std; class Base{ public: inline virtual void who() { cout << "I am Base\n"; } virtual ~Base() {} }; class Derived : public Base{ public: inline void who() { // 不写inline时隐式内联 cout << "I am Derived\n"; } }; int main(){ // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 Base b; b.who(); // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。 Base *ptr = new Derived(); ptr->who(); // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。 delete ptr; ptr = nullptr; system("pause"); return 0; }
虚函数指针、虚函数表#
- 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
- 虚函数表:在程序只读数据段(
.rodata section
,见: 目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
菱形继承与虚继承#
- 菱形继承:
虚继承:用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)
实现原理:
- 与编译器相关,一般通过虚基类指针和虚基类表实现,虚继承的子类有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)
- 虚基类依旧会在子类里面存在拷贝,且最多存在一份
- 当虚继承的子类被当做父类继承时,虚基类指针也会被继承
- vbptr 是虚基类表指针(virtual base table pointer),其指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,就找到了虚基类成员,而虚继承不需要像普通多继承去维持公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
示例:
#include<bits/stdc++.h> using namespace std; class Base{ public: int _base=1; void fun(){cout<<"Base()"<<endl;} }; class A:virtual public Base{ // 虚继承 public: int _base=2; }; class C:virtual public Base{ // 虚继承 public: int _base=3; }; class D:public A,public C{}; int main(){ D d; d.fun();//Base() d.A::fun();//Base() d.C::fun();//Base() cout<<d.Base::_base<<endl;//1 cout<<d.A::_base<<endl;//2 cout<<d.C::_base<<endl;//3 return 0; }
虚继承 vs 虚函数#
- 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
- 不同之处:
- 虚继承
- 虚基类依旧存在继承类中,只占用存储空间
- 虚基类表存储的是虚基类相对直接继承类的偏移
- 虚函数
- 虚函数不占用存储空间
- 虚函数表存储的是虚函数地址
- 虚继承
抽象类vs接口类vs聚合类#
- 抽象类:含有纯虚函数的类
- 接口类:仅含有纯虚函数的抽象类
- 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类,也没有 virtual 函数
以上为类的方法相关内容
类的成员#
静态成员 vs 普通成员#
- 生命周期:
- 静态成员变量从类被加载开始到类被卸载,一直存在
- 普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束
- 共享方式
- 静态成员变量是全类共享
- 普通成员变量是每个对象单独享用的
- 定义位置
- 静态成员变量存储在静态全局区
- 普通成员变量存储在栈或堆中
- 初始化位置
- 静态成员变量在类外初始化
- 普通成员变量在类中初始化
- 默认实参
- 可以使用静态成员变量作为默认实参
成员初始化列表#
优点
- 更高效:少了一次调用默认构造函数的过程
- 必须使用初始化列表的场景:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而直接调用拷贝构造函数初始化
- 成员是按照它们在类中出现的顺序进行初始化的,而不是按照它们在初始化列表出现的顺序初始化的
原理
- 编译器会一 一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前
- list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的
initializer_list 列表初始化#
用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个
std::initializer_list
参数使用
#include <iostream> #include <vector> #include <initializer_list> template <class T> struct S { std::vector<T> v; S(std::initializer_list<T> l) : v(l) { std::cout << "constructed with a " << l.size() << "-element list\n"; } void append(std::initializer_list<T> l) { v.insert(v.end(), l.begin(), l.end()); } std::pair<const T*, std::size_t> c_arr() const { return {&v[0], v.size()}; // 在 return 语句中复制列表初始化 // 这不使用 std::initializer_list } }; template <typename T> void templated_fn(T) {} int main() { S<int> s = {1, 2, 3, 4, 5}; // 复制初始化 s.append({6, 7, 8}); // 函数调用中的列表初始化 std::cout << "The vector size is now " << s.c_arr().second << " ints:\n"; for (auto n : s.v) std::cout << n << ' '; std::cout << '\n'; std::cout << "Range-for over brace-init-list: \n"; for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作 std::cout << x << ' '; std::cout << '\n'; auto al = {10, 11, 12}; // auto 的特殊规则 std::cout << "The list bound to auto has size() = " << al.size() << '\n'; // templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式, // 它无类型,故 T 无法推导 templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK templated_fn<std::vector<int>>({1, 2, 3}); // 也 OK }
类的构造函数(拷贝、赋值、移动构造函数)#
构造函数顺序#
基类构造函数。如有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是在成员初始化表中的顺序。
成员类对象构造函数。如有多个成员类对象,调用顺序是对象在类中被声明的顺序,而不是在成员初始化表中的顺序。
派生类构造函数。
总结:
如果一个类定义了拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符
定义了移动构造函数或移动赋值运算符的类也必须定义自己的拷贝操作。否则,这些成员被默认为删除
拷贝(复制)构造函数、赋值运算符#
接受一个本类型对象的赋值运算符版本。通常,拷贝赋值运算符的参数是一个const的引用,并返回指向本对象的引用。如果类未显示定义拷贝赋值运算符,编译器会自动合成一个。
如果一个构造函数的第一个参数是自身类类型的引用(如果参数不是引用类型,则为了调用拷贝构造函数,必须要拷贝实参,但为了拷贝实参,又需要定义拷贝构造函数,如此递推),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
通常需要分配新的内存,或者
new
使用新的内存,所以比较耗时耗空间iostream
类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。通过不定义拷贝控制成员是不能够阻止的,因为编译器会生成合成的版本。阻止拷贝、赋值构造函数
A(const A&) = delete;//阻止拷贝构造函数 A &operator=(const A()) = delete;//阻止拷贝赋值
移动构造函数、移动赋值运算符#
可以移动而非拷贝对象。在某些情况下,对象拷贝完成之后源对象就立即被销毁,这时,移动而非拷贝对象就会大幅度提升性能。
右值与右值引用
int i = 24; int &&r1 = i;//错误。不能将右值引用绑定到左值上 int &&r2 = i * 24;//正确。可以将右值引用绑定到返回右值的表达式上 int &&r3 = r2;//错误。不能将右值引用绑定到一个变量上,即使这个变量是右值引用类型也不行
标准库
move
函数- 虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。通过标准库函数move可以实现(定义在头文件utility中)
#include <utility> int i = 24; int &r = i; int &&r1 = std::move(r);
移动构造函数(从给定对象接管资源)
- 移动构造函数的第一个参数是该类类型的一个引用,且是右值引用。其它额外的参数都必须有默认实参。
- 移动构造函数不分配任何新内存,它接管源对象的内存。在接管内存之后,将源对象中的指针都置为nullptr。
class A{ private: string *s1; string *s2; string *s3; public: A(A &&a) noexcept//移动操作不应抛出任何异常 : s1(a.s1), s2(a.s2), s3(a.s3)//成员初始化器接管a中的资源 { a.s1 = a.s2 = a.s3 = nullptr;//进入安全状态,可以重新赋值和销毁 } };
移动赋值运算符
class A { private: string *s1; string *s2; string *s3; public: A& operator=(A &&a) noexcept//移动操作不应抛出任何异常 { if (this != &a) { s1 = a.s1; s2 = a.s2; s3 = a.s3; a.s1 = a.s2 = a.s3 = nullptr;//进入安全状态,可以重新赋值和销毁 } } };
类的析构函数#
析构函数调用顺序#
- 调用派生类的析构函数;
- 调用成员类对象的析构函数;
- 调用基类的析构函数。
虚析构函数#
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
使用虚析构函数可以确保正确的析构函数序列被调用
虚析构函数使用
class Shape { public: Shape(); // 构造函数不能是虚函数 virtual double calcArea(); virtual ~Shape(); // 虚析构函数 }; class Circle : public Shape // 圆形类 { public: virtual double calcArea(); ... }; int main() { Shape * shape1 = new Circle(4.0); shape1->calcArea(); delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。 shape1 = NULL; return 0; }
this指针#
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 - 一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为:ClassName * const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName * const
,这说明不能对this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)- 在以下场景中,经常需要显式引用
this
指针:- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。
- 个人理解:用法上相当于Python中的
self
friend 友元类和友元函数#
特性:
- 能访问私有成员
- 破坏 封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
友元函数
- 将非成员函数声明为友元函数
#include <iostream> using namespace std; class Student{ public: Student(char *name, int age, float score) : m_name(name), m_age(age), m_score(score){ }; public: friend void show(Student *pstu); //将show()声明为友元函数 private: char *m_name; int m_age; float m_score; }; //非成员函数 void show(Student *pstu){ cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl; } int main(){ Student stu("小明", 15, 90.6); show(&stu); //调用友元函数 Student *pstu = new Student("李磊", 16, 80.5); show(pstu); //调用友元函数 return 0; }
- 将其他类的成员函数声明为友元函数
#include <iostream> using namespace std; class Address; //提前声明Address类 class Student{ //声明Student类 public: Student(char *name, int age, float score) : m_name(name), m_age(age), m_score(score){ }; public: void show(Address *addr) { cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl; cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl; }; private: char *m_name; int m_age; float m_score; }; class Address{ //声明Address类 private: char *m_province; //省份 char *m_city; //城市 char *m_district; //区(市区) public: Address(char *province, char *city, char *district){ m_province = province; m_city = city; m_district = district; }; //将Student类中的成员函数show()声明为友元函数 friend void Student::show(Address *addr); }; int main(){ Student stu("小明", 16, 95.5f); Address addr("陕西", "西安", "雁塔"); stu.show(&addr); Student *pstu = new Student("李磊", 16, 80.5); Address *paddr = new Address("河北", "衡水", "桃城"); pstu->show(paddr); return 0; }
友元类
- 友元类中的所有成员函数都是另外一个类的友元函数
#include <iostream> using namespace std; class Address; //提前声明Address类 class Student{ //声明Student类 public: Student(char *name, int age, float score) : m_name(name), m_age(age), m_score(score){ }; public: void show(Address *addr) { cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl; cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl; }; private: char *m_name; int m_age; float m_score; }; class Address{ //声明Address类 public: Address(char *province, char *city, char *district) { m_province = province; m_city = city; m_district = district; }; friend class Student; //将Student类声明为Address类的友元类 private: char *m_province; //省份 char *m_city; //城市 char *m_district; //区(市区) }; int main(){ Student stu("小明", 16, 95.5f); Address addr("陕西", "西安", "雁塔"); stu.show(&addr); Student *pstu = new Student("李磊", 16, 80.5); Address *paddr = new Address("河北", "衡水", "桃城"); pstu->show(paddr); return 0; }
explicit(显式)关键字#
explicit 修饰构造函数时,可以防止隐式转换和复制初始化
explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
使用:
struct A { A(int) { } operator bool() const { return true; } }; struct B { explicit B(int) {} explicit operator bool() const { return true; } }; void doA(A a) {} void doB(B b) {} int main() { A a1(1); // OK:直接初始化 A a2 = 1; // OK:复制初始化 A a3{ 1 }; // OK:直接列表初始化 A a4 = { 1 }; // OK:复制列表初始化 A a5 = (A)1; // OK:允许 static_cast 的显式转换 doA(1); // OK:允许从 int 到 A 的隐式转换 if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化 B b1(1); // OK:直接初始化 B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化 B b3{ 1 }; // OK:直接列表初始化 B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化 B b5 = (B)1; // OK:允许 static_cast 的显式转换 doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换 if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换 bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化 return 0; }
default & delete (C++11)#
default
- C++98 和 C++03 编译器在类中会隐式地产生四个函数:默认构造函数、拷贝构造函数、析构函数、赋值运算符函数,被称为特殊成员函数
- C++11 中,“特殊成员函数”还有2个:移动构造函数和移动赋值运算符函数。如果程序申明了6种特殊成员函数,编译器则不会隐式产生。
default
可显示地、强制地要求编译器生成默认版本。 - 示例:
class DataOnly { public: DataOnly() = default; //default constructor ~DataOnly() = default; //destructor DataOnly(const DataOnly& rhs) = default; //copy constructor DataOnly& operator=(const DataOnly & rhs) = default; //copy assignment operator DataOnly(const DataOnly && rhs) = default; //C++11,move constructor DataOnly& operator=(DataOnly && rhs) = default; //C++11,move assignment operator };
delete
delete在C++11被赋予了新的功能,主要有如下几种作用
禁止编译器生成6种特殊成员函数的默认版本
class DataOnly { public: DataOnly() = delete; //default constructor ~DataOnly() = delete; //destructor //copy constructor DataOnly(const DataOnly& rhs) = delete; //copy assignment operator DataOnly& operator=(const DataOnly & rhs) = delete; //C++11,move constructor DataOnly(const DataOnly && rhs) = delete; //C++11,move assignment operator DataOnly& operator=(DataOnly && rhs) = delete; };
在函数重载中,delete可以滤掉一些函数的形参类型
bool isLucky(int number); // original function bool isLucky(char) = delete; // reject chars bool isLucky(bool) = delete; // reject bools bool isLucky(double) = delete; // reject doubles and floats if (isLucky('a'))... // error! call to deleted function if (isLucky(true))... // error! if (isLucky(3.5))... // error!
在模板特例化中,delete可以过滤一些特定的形参类型
class Window { public: template<typename T> void processPointer(T* ptr){} }; // 禁止参数为 void* 的函数调用 template<> void Window::processPointer<void>(void*) = delete;
final (C++11)#
修饰类:用于申明终结类
struct B1 final {}; struct D1 : B1 {}; // 错误!不能从 final 类继承!
修饰虚函数:表明子类不能重写该虚函数,为“终结虚函数”
class B2 { virtual void f() final {} // final 函数 }; class D2: B2 { virtual void f() {} // 错误! };
override (C++11)#
虚函数存在的问题:如果继承基类的虚函数,在重写虚函数时写错了,参数类型或个数不对,但编译不会报错,导致对基类同名函数的隐藏,运行态和设计的行为不同
class Base { virtual void foo() {}; }; class Derived: Base { void f(int a) {}; //未重写,发生隐藏,但不会报编译错误 };
override
辅助编译器检查是否真正重写了基类虚函数,将错误提前暴露在编译期class Base { virtual void gew(int) {} }; class Derived: Base { virtual void gew(int) override {} // OK virtual void gew(double) override {} // Error, 父类没有这个形式的函数 };
override
和virtual
只能在类中申明或重写虚函数时使用,在类外使用将无法通过编译class Base { virtual void gew(int) {} }; class Derived2 : Base { virtual void gew(int) override; // OK }; virtual void Derived2::gew(int) override {} // Error
C++类的大小#
- 首先,类大小的计算遵循结构体的对齐原则
- 类的大小与普通数据成员有关,与成员函数和静态成员无关(即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响)
- 虚函数对类的大小有影响,因为虚函数表指针带来的影响
- 虚继承对类的大小有影响,因为虚基表指针带来的影响
- 空类是特殊情况,大小为1;原因:C++标准禁止对象大小为 0,因为两个不同的对象需要用不同的地址表示
仿函数(function-like classes)#
模仿函数的类,使用方式如同函数。本质是类中重载括弧运算符
operator()
。将某种“操作”当做算法的参数,有两种方法:
先将该“操作”设计为一个函数,再将函数指针当做算法的一个参数
#include <iostream> using namespace std; int RecallFunc(int *start, int *end, bool (*pf)(int)) { int count=0; for(int *i=start;i!=end+1;i++) { count = pf(*i) ? count+1 : count; } return count; } bool IsGreaterThanTen(int num) { return num>10 ? true : false; } int main() { int a[5] = {10,100,11,5,19}; int result = RecallFunc(a,a+4,IsGreaterThanTen); cout<<result<<endl; return 0; }
将该“操作”设计为一个仿函数(本质上是个 class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数
#include <iostream> using namespace std; class IsGreaterThanThresholdFunctor { public: explicit IsGreaterThanThresholdFunctor(int t):threshold(t){} bool operator() (int num) const // 仿函数的关键 { return num > threshold ? true : false; } private: const int threshold; }; int RecallFunc(int *start, int *end, IsGreaterThanThresholdFunctor myFunctor) { int count = 0; for (int *i = start; i != end + 1; i++) { count = myFunctor(*i) ? count + 1 : count; } return count; } int main() { int a[5] = {10,100,11,5,19}; int result = RecallFunc(a, a + 4, IsGreaterThanThresholdFunctor(10)); cout << result << endl; }
转换函数(conversion function)#
一般形式:
operator 类型名称() const { // 实现转换 }
特点:
- 转换函数必须是类的成员函数
- 转换函数不声明返回类型
- 形参列表必须为空
- 类型转换函数通常应该是
const
例子:
#include <iostream> class Fraction { public: Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {} operator double() const { return (double) m_numerator/m_denominator; } private: int m_numerator; // 分子 int m_denominator; // 分母 }; int main(void) { Fraction f(3, 5); double d = 3.2 + f; std::cout << d << std::endl; return 0; }
如何用 C 实现 C++ 类#
C 实现 C++ 的面向对象特性(封装、继承、多态)
- 封装:使用函数指针把属性与方法封装到结构体中
- 继承:结构体嵌套
- 多态:父类与子类方法的函数指针不同