【学习笔记】C++面向对象高级开发-侯捷

文章目录


学习笔记源自博览网侯捷大师的C++课程,在原视频及讲义的基础上填充注释。
如有侵权,请联系删除,抱歉。


0 书籍推荐

  • C++ Primer【C++首个编译器的作者】
  • The C++ Programming Language【C++之父】
  • Effective C++
  • Effective Modern C++【C++11/14】
  • The C++ Standard Library
  • STL源码剖析

1 面向对象高级开发(上)

1.1 头文件与类的声明

防卫式声明

complex.h

#ifndef __COMPLEX__
#define __COMPLEX__ 

//0.前置声明 forward declarations
class ostream;
class complex;
complex& __doapl (complex *ths, const complex& r); 
    
//1.类声明 class declarations
class complex{
    ...
};

//2.类定义 class definition
complex::function ...
     
#endif

注:等价于#pragma once

示例:复数类-类的声明(类模板、默认参数、初始化列表、常函数、重载运算符、友元函数)

template<typename T>	//类模板
class complex{
private:
    T re, im;	//模板
    friend complex& __doapl (complex *ths, const complex& r); 	//友元函数

public:
    complex(T r = 0, T i = 0) : re(r), im(i){}	//默认参数、初始化列表
    complex& operator+=(const complex&);	//重载运算符
    T real() const { return re; }		//成员函数-常函数
    T imag() const { return im; }
};
int main(){
	complex<double> c1(2.5, 1.5);
	complex<int> c2(1, 2);
	...
}

1.2 构造函数

1.2.1 内联函数

注1:函数若在class body内定义完成,则自动成为inline内联函数

注2:内联函数仅是对编译器的建议,是否实际为内联函数,由编译器决定。


1.2.2 访问权限

private:封装数据成员,在类外无法直接访问

public:被外界调用的函数,在类外可直接访问

protected:在子类中可直接访问


1.2.3 构造函数

//默认参数、初始化列表
complex(T r = 0, T i = 0) : re(r), im(i){}

//等价写法:赋值(效率相对较差)
complex(T r = 0, T i = 0){
    re = r;
    im = i;
}

注1:构造函数可有多个,即构造函数的重载。

注2:若某个构造函数已包含默认参数,则构造函数重载时不能产生冲突。

complex(T r = 0, T i = 0) : re(r), im(i){}	//包含默认参数的构造函数
//complex() : re(0), im(0){}	//与已有构造函数存在冲突,编译器不知道如何调用

例:
complex c1;
//complex c2();

注3:构造函数可以被private修饰,则不能被外部调用创建对象,如单例模式Singleton

class A{
public:
    static A& getInstance();	//对外部提供获取对象的接口
    setup(){...}
    
private:
    A();				//默认无参构造
    A(const A& rhs);	//拷贝构造
};

A& A::getInstance(){
    static A a;			//唯一对象
    return a;
}

//对象调用
A::getInstance().setup();

1.3 参数传递与返回值

1.3.1 常成员函数

常成员函数不修改类中数据成员的内容,使用const修饰(中括号()和花括号{}之间)。

template<typename T>	//类模板
class complex{
public:
  	//成员函数-常函数
    T real() const { return re; }		
    T imag() const { return im; }
};

1.3.2 参数传递:值传递 vs 引用传递

值传递:pass by value

引用传递:pass by reference(to const)

C语言中,参数传递可使用指针;C++语言中,参数传递建议使用引用

注1:C++中建议函数参数尽量使用引用传递,但bool/char/short等短数据类型可使用值传递。

注2:传递引用时若不希望参数被修改,函数参数可使用const修饰:void func(const Object& obj){}

引用的本质为指针常量T* const p

指针常量的指针指向不可变;但指针常量指向的值可改变(解引用,重新赋值)。


1.3.3 返回值传递:返回值 vs 返回引用

值传递:return by value

引用传递:return by reference(to const)

注1:C++中建议函数的返回值类型尽量返回引用

注2:不能返回引用的情况:函数作用域内的局部变量/临时对象


1.3.4 友元friend

友元:类中声明的友元,可直接获取类的private数据成员

template<typename T>	//类模板
class complex{
private:
    T re, im;	//模板
    
    //友元函数的声明
    friend complex& __doapl (complex *ths, const complex& r); 	//友元函数
	...
};

//友元函数的定义
inline complex& __doapl(complex *ths, const complex& r){
    //自由获取friend的private成员
    ths->re += r.re;
    ths->im += r.im;
    return *ths;
}

注:友元破坏了类的封装性。

相同/同一class的各个对象(objects),互为友元(friends)

class complex{
private:
    double re, im;
    
public:
    complex(double r = 0, double i = 0) : re(r), im(i){}
    
    //当前对象可直接访问另一对象的private私有数据成员
    int func(const complex& param){
        return param.re + param.im;
    }
};

//相同/同一class的各个对象(objects),互为友元(friends)
int main(){
    complex c1(2, 1);
    complex c2;
    
    //对象c2可直接访问另一对象c1的private私有数据成员
    c2.func(c1);
}

1.4 操作符重载与临时对象

1.4.1 操作符重载-成员函数(含this)

注:任何成员函数,均隐含this指针,指向当前成员函数的调用者。

inline complex& __doapl(complex* ths, const complex& r){
    ths->re += r.re;
    ths->im += r.im;
    return *ths;
}

inline complex& complex::operator+=(const complex &r){
    return __doapl(this, r);	//this表示当前对象
}

int main(){
    complex c1(2, 1);
    complex c2(5);
    
    c2 += c1;
    ...
}

1.4.2 操作符重载-非成员函数/全局函数(不含this)

临时对象/匿名对象(temp object):typename()

/* 操作符重载 */
inline complex operator+(const complex& x, const complex& y){
    //临时对象/匿名对象
    return complex(real(x) + real(y), imag(x) + imag(y));
}

inline complex operator+(const complex& x, double y){
    return complex(real(x) + y, imag(x));
}

inline complex operator+(double x, const complex& y){
    return complex(x + real(y), imag(y));
}

int main(){
    //临时对象/匿名对象
    int(7);
    complex();
    complex(4, 5);
    cout << complex(2) << endl;
    
    complex c1(2, 1);
    complex c2;
}
#include <iostream>
ostream& operator<<(ostream& os, const complex& x){
    return os << "(" << real(x) << "," << imag(x) << ")";
}

int main(){
    complex c1(1, 2);
    complex c2(3, 4);
    
    cout << c1 << endl;	//(1, 2)
    cout << c1 << c2 << endl;	//(1, 2)(3, 4)
}

1.5 构造函数和析构函数

class String{
public:
    String(const char* cstr = 0);
    String(const String& str);
    String& operator=(const String& str);
    ~String();
    char* get_c_str() const {return m_data};

private:
    char* m_data;
};

inline String::String(const char* cstr = 0){
    if(cstr){
        m_data = new char[strlen(cstr) + 1];
        strcpy(m_data, cstr);
    }else{
        m_data = new char[1];
        *m_data = '\0';
    }
}

//拷贝构造函数
/* 同一个类的不同对象之间互为友元,可直接访问private成员 */
inline String::String(const String& str){
	m_data = new char[strlen(str.m_data) + 1];
    strcpy(m_data, str.m_data);
}

//拷贝赋值函数
/* 先释放左值;再创建与右值相同大小的空间;最后深拷贝 */
inline String& String::operator=(const String& str){
    //检测自我赋值(self assignment)
	if(this == &str){
        return *this;
    }
    
    //1.释放原有空间
    delete[] m_data;
    //2.创建新空间
    m_data = new char[strlen(str.m_data) + 1];
	//3.深拷贝
    strcpy(m_data, str.m_data);
    return *this;
}

//输出函数-重载左移运算符
ostream& operator<<(ostream& os, const String& str){
    os << str.get_c_str();
    return os;
}

//析构函数
inline String::~String(){
    delete[] m_data;
}

1.5.1 具有指针成员的类必须包含拷贝构造函数和operator=函数

具有指针成员的类,必须自定义拷贝构造函数拷贝赋值函数operator=,否则会导致浅拷贝,并造成内存泄露重复释放

//拷贝构造函数
/* 同一个类的不同对象之间互为友元,可直接访问private成员 */
inline String::String(const String& str){
	m_data = new char[strlen(str.m_data) + 1];
    strcpy(m_data, str.m_data);
}

//拷贝赋值函数
/* 先释放左值;再创建与右值相同大小的空间;最后深拷贝 */
inline String& String::operator=(const String& str){
    //检测自我赋值(self assignment)
	if(this == &str){
        return *this;
    }
    
    //1.释放原有空间
    delete[] m_data;
    //2.创建新空间
    m_data = new char[strlen(str.m_data) + 1];
	//3.深拷贝
    strcpy(m_data, str.m_data);
    return *this;
}

1.5.2 一定要在拷贝赋值函数operator=中检查是否自我赋值

检测自我赋值的作用

  1. 直接返回对象本身,效率高;
  2. 若未判断是否自我赋值,则释放原有空间后,访问右值时会产生不确定行为。
//拷贝赋值函数
/* 先释放左值;再创建与右值相同大小的空间;最后深拷贝 */
inline String& String::operator=(const String& str){
    //检测自我赋值(self assignment)
    //1.直接返回对象本身,效率高
    //2.若未判断是否自我赋值,则释放原有空间后,访问右值时会产生不确定行为
	if(this == &str){
        return *this;
    }
    
    //1.释放原有空间
    delete[] m_data;
    //2.创建新空间
    m_data = new char[strlen(str.m_data) + 1];
	//3.深拷贝
    strcpy(m_data, str.m_data);
    return *this;
}

1.6 栈stack和堆heap

Stack:存在于某作用域scope的一块内存空间。

  • 当调用函数时,函数本身会形成一个stack用于存储所接受的函数参数及返回地址
  • 函数体内声明的任何变量(含临时对象/匿名对象),所使用的的内存块均取自上述stack。

Heap:即system heap,由操作系统提供的一块global全局内存空间,程序可动态分配(dynamic allocated),从中获取若干区块(blocks)。

通过动态内存分配获取的堆内存,需由程序员主动释放(delete)。


1.6.1 stack object栈对象的生命周期

栈对象的生命周期存在于作用域内,被称为auto object。

析构函数在作用域结束时被自动调用。

class Complex{...};
...
{
    Complex c(1, 2);
}

1.6.2 static local object静态局部对象的生命周期

静态局部对象的生命周期,在作用域结束后仍存在,直到整个程序结束。

class Complex{...};
...
{
    static Complex c(1, 2);
}

1.6.3 global object全局对象的生命周期

全局对象的生命周期,在整个程序结束后才结束,作用域是整个程序(全局作用域)。

class Complex{...};
...
Complex c(1, 2);

int main()
{
    ...
}

1.6.4 heap object堆对象的生命周期

堆对象的生命周期,在其被delete后结束。

class Complex{...};
...
{
    Complex *p = new Complex(1, 2);
    //若未delete,会导致内存泄露
    delete p;
}

注:若未delete指向堆对象的指针p,则当作用域结束后,p指向的堆内存未被释放,当指针p本身已被释放,无法再delete,导致内存泄露。


1.6.5 new的作用

new的作用:先分配内存,再调用构造函数。

Complex* pc = new Complex(1, 2);
/* 编译器内部实现 */
Complex* pc;

//1.分配内存
//new操作符内部调用malloc()函数
void* mem = operator new(sizeof(Complex));

//2.静态类型转换
pc = static_cast<Complex*>(mem);

//3.调用构造函数
//pc调用构造函数,则pc即隐藏的this指针
pc->Complex::Complex(1, 2);

构造函数属于类的成员函数,隐含了this指针。

Complex::Complex(pc, 1, 2);

Complex::Complex(this, 1, 2);

new的内部实现


1.6.6 delete的作用

delete的作用:先调用析构函数,再释放内存。

class String{
public:
    ~String(){
        delete[] m_data;
    }
    ...
private:
    char* m_data;
}
String* ps = new String("Hello");
...
delete ps;

/* 编译器内部实现 */
//1.调用析构函数
String::~String(ps);
//2.释放内存
//delete操作符内部调用free(ps)函数
operator delete(ps);

delete的内部实现


1.6.7 malloc动态内存分配

单个对象的动态内存分配

![malloc动态内存分配原理图请添加图片描述

  • 目标对象:Complex类对象,8字节
  • 目标对象上下侧空间(调试模式下):4字节 * 8 + 4字节
  • 上下侧Cookie:4字节 + 4字节,记录区块的大小
  • 填充内存大小:4字节 * 3(pad)

注1:VC环境下,每个内存区块的大小一定为16的倍数,否则需要进行填充。

注2:操作系统需要依赖额外空间包含的信息,对内存区块进行回收。

注:Cookie值:十六进制数0x00000041。

实际为0x40,借用最后1位表示申请(1)或回收(0)内存。

倒数第2位:4*16=64字节;

倒数第1位:1,表示申请内存。

对象数组的动态内存分配

malloc动态内存分配数组原理图

注:在堆区动态创建对象数组时,会使用4字节的内存空间记录数组元素的个数。

array new和array delete搭配使用

array new和array delete搭配使用

  • 不含指针类型成员时,仅使用delete p时,只会调用1次析构函数,但不会造成数组的内存泄露,操作系统会根据Cookie值释放动态分配的堆区数组内存。
  • 包含指针类型成员时,仅使用delete p时,只会调用1次析构函数,会造成指针类型成员的内存泄露,但不会造成数组的内存泄露,操作系统会根据Cookie值释放动态分配的堆区数组内存。

注:new对象/变量数组,必须delete对象/变量数组。new/delete不对应,可能造成指针类型成员的内存泄露。


1.7 扩展补充:static、类模板、函数模板

1.7.1 static

类的成员函数只有1份,但需处理多个不同对象,成员函数通过this指针获取不同需处理的对象。


静态成员:只存在1份。

静态成员函数:只存在1份,不含this指针只能处理静态成员/数据

class Account{
public:
    static double m_rate;
    static void set_rate(const double& x){ m_rate = x; }
};
//静态变量的定义
double Account::m_rate = 8.0;

int main(){
    /* 静态成员函数的调用 */
    //1.通过类名调用
    Account::set_rate(5.0);
    
    //2.通过对象调用
    Account a;
    a.set_rate(6.0);
}

1.7.2 构造函数私有化-单例模式

单例模式-饿汉式:类加载时,初始化对象。

优点:获取对象的速度快;

缺点:类加载较慢。

class A{
public:
    //通过静态函数,获取唯一的静态对象
    static A& getInstance(){ return a; }
    void setup(){...}
    
private:
    //构造函数私有化,外部无法通过构造函数创建对象
    A();
    A(const A& rhs);
    //静态对象-唯一
    static A a;
};

int main(){
    //通过类名调用静态成员函数(获取静态对象),进而调用非静态成员函数
    A::getInstance().setup();
}

单例模式-懒汉式延迟加载对象;类加载时,不初始化对象,在调用静态函数时才创建对象。

优点:类加载较快;

缺点:获取对象的速度慢。

class A{
public:
    //通过静态函数,获取唯一的静态对象
    static A& getInstance(){ 
        //仅当静态函数调用时,才创建唯一的静态对象
        //当函数作用域结束时,唯一的静态对象仍存在
    	static A a;
        return a;
    }
    void setup(){...}
    
private:
    //构造函数私有化,外部无法通过构造函数创建对象
    A();
    A(const A& rhs);
};

int main(){
    //通过类名调用静态成员函数(获取静态对象),进而调用非静态成员函数
    A::getInstance().setup();
}

1.7.3 类模板(class template)

类模板使用时,必须显式指定具体的泛型类型。

template<typename T>	//类模板
class complex{
private:
    T re, im;	//模板
    friend complex& __doap1 (complex *ths, const complex& r); 	//友元函数

public:
    complex(T r = 0, T i = 0) : re(r), im(i){}	//默认参数、初始化列表
    complex& operate+=(const complex&);	//重载运算符
    T real() const { return re; }		//成员函数-常函数
    T imag() const { return im; }
};
int main(){
	complex<double> c1(2.5, 1.5);
	complex<int> c2(1, 2);
	...
}

1.7.4 函数模板(function template)

注1:关键字class可替换为typename

注2:函数模板使用时,不必显式指定具体的泛型类型。编译器会对函数模板进行实参推导/类型推导(argument deduction)。类模板使用时,需显式指定具体的泛型类型。

//函数模板
template<class T>
inline const T& min(const T& a, const T& b){
    return b < a ? b : a;
}

class stone{
public:
    stone(int w, int h, int we) : _w(w), _h(h), _weight(we){}
    //重载运算符<
    bool operator< (const stone& rhs){
        return this->_weight < rhs._weight;
    }
    
private:
    int _w, _h, _weight;
}

int main(){
    stone r1(2, 3), r2(3, 3), r3;
    //调用模板函数min,类型推导
    //类型推导T为stone类型,进一步调用stone::operator<
    r3 = min(r1, r2);	
}

1.7.5 命名空间namespace

语法

namespace std
{
    ...
}
using编译指令(using directive)

using编译指令使用指定命名空间。使用using编译指令后,可直接访问命名空间的成员,而无需使用::作用域运算符

语法using namespace 命名空间名;

#include <iostream>
using namespace std;

int main(){
    cin >> ... ;
    cout << ... ;
    
    return 0;
}
using声明(using declaration)

using声明使用指定命名空间的成员。使用using声明后,可直接访问命名空间的成员,而无需使用::作用域运算符
语法using 命名空间名::成员;

#include <iostream>
using std::cout;

int main(){
    std::cin >> ... ;
    cout << ... ;
    
    return 0;
}
#include <iostream>

int main(){
    std::cin >> ... ;
    std::cout << ... ;
    
    return 0;
}

1.7.6 待深入学习的细节

待深入学习的细节


1.8 组合与继承

类与类之间的关系:

  • 继承Inheritance
  • 复合/组合Composition
  • 委托Delegation

1.8.1 复合/组合Composition,表示has-a

复合/组合:一个类包含另一个类。

具有组合关系的两个类,生命周期相同(同步)

适配器模式Adapter

/* 利用已有的功能强大的容器deque,构造queue,仅开放部分函数接口 */
template <class T, class Sequence = deque<T>>
class queue{
    ...
protected:
    //底层容器
    Sequence c;	//等价于 deque<T> c;
public:
    //利用底层容器c的操作函数完成
    bool empty() const { return c.empty(); }
    size_type size() const { return c.size(); }
    reference front() { return c.front(); }
    reference back() { return c.back(); }
    
    //deque为双端可进出队列,queue为末端进、首端出(先进先出)
    void push(const value_type& x) { c.push(x); }
    void pop() { c.pop_front(); }
};

组合关系的内存示意图

组合关系的内存示意图


组合关系下的构造函数/析构函数调用顺序

构造函数的调用由内向外

Container的构造函数先调用Component的默认构造函数(由编译器完成),再执行本身。

析构函数的调用由外向内

Container的析构函数先执行本身,再调用Component的析构函数(由编译器完成)。

组合关系下的构造函数、析构函数调用顺序


1.8.2 委托Delegation,Composition by reference

委托:两个类之间使用指针相连。

具有委托关系的两个类,生命周期不相同(不同步),委托类的对象先被创建,当需要使用被委托类(指针指向的类)时,才创建被委托类的对象。

委托类对外接口;调用被委托类服务(进行具体实现)。

被委托类具体实现

/* Handle */
// file String.hpp
class StringRep;
class String {
public:
    String();
    String(const char* s);
    String(const String& s);
    String& operator=(const String& s);
    ~String();
    ...
        
private:
    /* pImpl——pointer to implementation */
    //Handle / Body
    //该指针可指向不同的实现类
    StringRep* rep;	//指向具体实现的指针
};
/* Body */
// file String.cpp
#include "String.hpp"
namespace {
clase StringRep {
	friend class String;
    StringRep(const char* s);
    ~StringRep();
    
    int count;
    char* rep;
};
}

String::String(){...}

pImpl(Handle / Body,委托类/被委托类):编译防火墙,可灵活地指向不同的实现类。
委托类的指针成员指向具体的实现类(被委托类)。

委托Delegation案例


1.8.3 继承Inheritance,表示is-a

C++的3种继承方式:

  • public
  • private
  • protected
struct _List_node_base
{
    _List_node_base* _M_next;
    _List_node_base* _M_prev;
};

template<typename _Tp>
struct _List_node : public _List_node_base
{
    _Tp _M_data;
};

父类的数据成员,被子类完整地继承。


继承关系下的构造函数/析构函数调用顺序

构造函数的调用由内向外

子类/派生类的构造函数先调用父类/基类的默认构造函数(由编译器完成),再执行本身。

析构函数的调用由外向内

子类/派生类的的析构函数先执行本身,再调用父类/基类的析构函数(由编译器完成)。

父类/基类的构造函数必须为虚函数,由子类进行重写,否则会导致未定义的行为。

继承关系下的构造函数、析构函数调用顺序


1.9 虚函数与多态

1.9.1 继承与虚函数

非虚函数(non-virtual):不希望派生类重新定义(重写,override)。

虚函数(virtual):希望派生类重新定义(重写,override),且已有默认定义。

纯虚函数(pure virtual):希望派生类一定重新定义(重写,override),且无默认定义。

class Shape{
public:
    //纯虚函数:子类必须重写
    virtual void draw() const = 0;
    //虚函数:子类可重写
    virtual void error(const std::string& msg);
    //非虚函数:子类不可重写
    int objectID() const;
};

class Rectangle : public Shape{..};
class Ellipse : public Shape{..};

1.9.2 委托(Delegation) + 继承(Inheritance)

观察者模式(Observer)
class Subject{
	int m_value;
    /* 委托Delegation:使用指针指向另一个类 */
	vector<Observer*> m_views;

public:
	//注册
	void attach(Observer *obs){
		m_views.push_back(obs);
	}
    
    void set_val(int value){
        m_value = value;
        notify();
    }
    
    //通知
    void notify(){
        for(int i = 0; i < m_views.size(); i++){
            m_views[i]->update(this, m_value);
        }
    }

    //注销
};
class Observer{
public:
    /* 继承Inheritance */
    //纯虚函数:子类必须重写
    virtual void update(Subject* sub, int value) = 0;
};

观察者模式


1.10 委托设计案例Delegation

1.10.1 组合模式(Composite)

委托设计案例

  • Primitive类和Composite类抽象出一个共同基类Component类。

  • Composite类中vector容器的元素类型vector<Component*>,必须使用指向Component类的指针(Component*),而不能使用Component类的子类对象(PrimitiveComposite)。

    注:vector存储的元素类型必须相同

    基类指针类型(Component*)的数据类型大小相同;

    而基类的子类对象(PrimitiveComposite)的数据类型大小可能不同。

    因此,必须使用vector<Component*>


1.10.2 原型模式(Prototype)

基类创建未来可能扩展的子类(对象)。

注:clone()函数不能是静态成员函数。

静态成员函数的调用方式为类名::静态成员函数(),需要类名;但子类的类名尚未确定。

原型模式


原型模式-基类

原型模式-基类


原型模式-子类

原型模式-子类


原型模式-测试程序
原型模式-测试程序


2 面向对象高级开发(下)

2.1 转换函数 Conversion Function

2.1.1 转换函数 Conversion Function

需求:将目标类的对象,转换为指定数据类型。

例:包含分子、分母的分数类,转为double类型。

class Fraction
{
public:
    Fraction(int num, int den = 1)
      : m_numerator(num), m_denoinator(den) {}
    
    //重载的函数名与目标返回值类型相同,可省略返回值类型
    operator double() const {
        return (double)(m_numerator / m_denoinator);
    }
    
private:
    int m_numerator;	//分子
    int m_denoinator;	//分母
};

int main(){
    Fraction f(3, 5);
    double d = 4 + f;  //调用operator double()函数,将f转为0.6
}

注:可包含多个转换函数,如将整数转换为double/string类型。


2.1.2 无explicit修饰的单实参构造函数non-explicit-one-argument ctor

注:argument表示实参;parameter表示形参。

class Fraction
{
public:
    Fraction(int num, int den = 1)
      : m_numerator(num), m_denoinator(den) {}

    Fraction operator+(const Fraction& f) {
        return Fraction(...);
    }
    
private:
    int m_numerator;	//分子
    int m_denoinator;	//分母
};

int main(){
    Fraction f(3, 5);
    //重载运算符operator+()的右操作数为Fraction对象
    //构造函数未使用explicit修饰,操作数4与默认参数可隐式转换为Fraction对象
    Fraction d = f + 4;	/* 编译正常 */
}

2.1.3 conversion function vs. non-explicit-one-argument ctor

class Fraction
{
public:
    Fraction(int num, int den = 1)
      : m_numerator(num), m_denoinator(den) {}
    
    //重载的函数名与目标返回值类型相同,可省略返回值类型
    operator double() const {
        return (double)(m_numerator / m_denoinator);
    }

    Fraction operator+(const Fraction& f) {
        return Fraction(...);
    }
    
private:
    int m_numerator;	//分子
    int m_denoinator;	//分母
};

int main(){
    Fraction f(3, 5);
    //路径1:调用operator double()函数,将f转换为double类型;3/5即0.6与操作数4相加得到4.6,隐式转换为Fraction类型
    //路径2:构造函数未使用explicit修饰,操作数4可隐式转换为Fraction对象;再调用operator+()函数
    Fraction d2 = f + 4;	/* 编译报错:存在歧义ambiguous */
}

2.1.4 explicit-one-argument ctor

class Fraction
{
public:
    explicit Fraction(int num, int den = 1)
      : m_numerator(num), m_denoinator(den) {}
    
    //重载的函数名与目标返回值类型相同,可省略返回值类型
    operator double() const {
        return (double)(m_numerator / m_denoinator);
    }

    Fraction operator+(const Fraction& f) {
        return Fraction(...);
    }
    
private:
    int m_numerator;	//分子
    int m_denoinator;	//分母
};

int main(){
    Fraction f(3, 5);
    
    /* 单参构造函数使用explicit修饰 */
    //1.右操作数4不会被隐式转换为Fraction对象,继而无法调用operator+()函数
    //2.调用operator double()将f转换为double,求和后,无法将double转换为Fraction类型
    Fraction d3 = f + 4;	/* Error:无法将double转换为Fraction类型 */
}

2.1.5 转换函数的其它应用场景

转换函数的其它应用场景


2.2 pointer-like classes

2.2.1 智能指针

智能指针需要具备指针允许的所有操作。智能指针类的成员一定包含一般指针。

智能指针通用写法:

template<class T>
class shared_ptr
{
public:
    //重载运算符 *
    T& operator*() const{
        return *px;
    }
    //重载运算符 ->
    T* operator->() const{
        return px;
    }
    //智能指针的有参构造函数,使用初始化列表对原指针(成员)进行赋值
    shared_ptr(T* p) : px(p) {}
    
private:
    T* px;		//被智能指针包装的指针
    long* pn;
    ...
};

使用场景:

struct Foo
{
    ...
    void method(void) { ... }
};

//使用构造函数初始化,初始化列表使用new FOO对类成员px赋值
shared_ptr<Foo> sp(new Foo);

//使用场景1:原始类Foo的拷贝构造函数
Foo f(*sp);

//使用场景2:调用函数
sp->method();	
//等价于px->method();

注:sp->相当于px

特殊:C++中重载运算符->后需要继续使用->作用下去,即sp->实际应该为sp->->px->


2.2.2 迭代器

迭代器表示容器中当前指向的元素的位置。

注1:迭代器相当于智能指针,存在运算符重载

注2:link_type node表示实际指针,指向双向链表的节点;迭代器是包装实际指针的智能指针

迭代器的运算符重载

双向链表的迭代器


2.3 function-like classes 仿函数

仿函数:类中重载函数调用运算符(),即重载operator()。该类创建的对象为函数对象

():函数调用运算符(function call operator)

注:仿函数类的对象,称为函数对象

仿函数

select1st类仿函数的调用:select1st<Pair>()(Pair pair)

第1个()表示创建匿名对象

第2个()表示调用重载的函数调用运算符(),即operator()(…)

标准库中的仿函数

/* 继承一元操作符类unary_function */
template<class T>
struct identity : public unary_function<T, T> {
	const T& operator()(const T& x) const {
        return x;
    }  
};

template<class Pair>
struct select1st : public unary_function<Pair, typename Pair::first_type> {
	const typename Pair::first_type& operator()(const Pair& x) const {
        return x.first;
    }  
};

template<class Pair>
struct select2nd : public unary_function<Pair, typename Pair::second_type> {
	const typename Pair::second_type& operator()(const Pair& x) const {
        return x.second;
    }  
};


/* 继承二元操作符类binary_function */
template<class T>
struct plus : public binary_function<T, T, T> {
	T operator()(const T& x, const T& y) const {
        return x + y;	// x + y为新创建的局部变量,返回值需要使用T接收,而不能使用引用T&
    }  
};

template<class T>
struct minus : public binary_function<T, T, T> {
	T operator()(const T& x, const T& y) const {
        return x - y;	// x - y为新创建的局部变量,返回值需要使用T接收,而不能使用引用T&
    }  
};

template<class T>
struct equal_to : public binary_function<T, T, T> {
	bool operator()(const T& x, const T& y) const {
        return x == y;
    }  
};

template<class T>
struct less : public binary_function<T, T, T> {
	bool operator()(const T& x, const T& y) const {
        return x < y;
    }  
};

仿函数继承的基类
仿函数继承的基类


2.4 命名空间 namespace

namespace中可定义变量、函数、结构体和类等。

using namespace std;

//---------------------------
#include <iostream>
#include <memory> //shared_ptr

namespace ns01
{
    void test_member_template(){
        ...
    }
}	//namespace ns01
//---------------------------

#include <iostream>
#include <list>

namespace ns02
{
    template<typename T>
    using Lst = list<T, allocator<T>>;
    void test_template_template_param(){
        ...
    }
}	//namespace ns02
//---------------------------

/* 测试程序 */
int main(int argc, char** argv)
{
    // 多个命名空间之间无任何关联
    ns01::test_member_template();
    ns02::test_template_template_param();
}

2.5 模板 template

2.5.1 类模板 class template

调用者在使用时再指定泛型的具体类型

template<typename T>	//类模板
class complex{
private:
    T re, im;	//模板
    friend complex& __doap1 (complex *ths, const complex& r); 	//友元函数

public:
    complex(T r = 0, T i = 0) : re(r), im(i){}	//默认参数、初始化列表
    complex& operate+=(const complex&);	//重载运算符
    T real() const { return re; }		//成员函数-常函数
    T imag() const { return im; }
};

2.5.2 函数模板 function template

注1:关键字class可替换为typename

注2:函数模板使用时,不必显式指定具体的泛型类型。编译器会对函数模板进行实参推导/类型推导(argument deduction)。类模板使用时,需显式指定具体的泛型类型。

//函数模板
template<class T>
inline const T& min(const T& a, const T& b){
    return b < a ? b : a;
}

class stone{
public:
    stone(int w, int h, int we) : _w(w), _h(h), _weight(we){}
    //重载运算符<
    bool operator< (const stone& rhs){
        return this->_weight < rhs._weight;
    }
    
private:
    int _w, _h, _weight;
}

int main(){
    stone r1(2, 3), r2(3, 3), r3;
    //调用模板函数min,类型推导
    //类型推导T为stone类型,进一步调用stone::operator<
    r3 = min(r1, r2);	
}

2.5.3 成员模板 member template

成员模板:类模板中的成员(函数/属性)也为模板。

template <class T1, class T2>
struct pair {
    typedef T1 first_type;
    typedef T2 second_type;
    
    // 成员变量
    T1 first;
    T2 second;
    
    // 构造函数
    pair() : first(T1()) , second(T2()) {}	// 匿名对象T1()、T2()
    pair(const T1& a, const T2& b) : first(a), second(b) {}	// 有参构造
    
    /* 成员模板 */
    template <class U1, class U2>
    pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {}	// 拷贝构造
};

应用场景:STL中的**拷贝构造函数//有参构造函数**常设计为成员模板。

成员模板的泛型类型类模板的泛型类型子类

注:成员模板的泛型类型U1/U2,继承自类模板的泛型类型T1/T2。反之不成立

class Base1 {};
class Derived1 : public Base1 {};

class Base2 {};
class Derived2 : public Base2 {};
pair</* U1 */Devired1, /* U2 */Devired2> p;
pair<Base1, Base2> p2(p);

/* 等价写法 */
// 使用成员模板创建匿名对象,用于类的拷贝构造函数
pair</*T1*/Base1, /*T2*/Base2> p2(pair</*U1*/Devired1, /*U2*/Devired2>());

示例:智能指针类成员模板的应用

智能指针子类shared_ptr有参构造函数的参数(被包装的指针),是智能指针父类__shared_ptr有参函数构造函数的参数(被包装的指针)的子类

template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
    ...
    /* 成员模板 */
    template<typename _Tp1>
    // 构造函数
    // 子类构造函数shared_ptr的参数__p,对父类构造函数__shared_ptr<_Tp>赋值
    explicit shared_ptr(/*Devired*/_Tp1* __p) : __shared_ptr</*Base*/_Tp>(__p) {}
    ...
};
/* 指针类的多态 */
Base1 *ptr = new Devired1;	// 向上转型up-cast
/* 智能指针类的多态 */
// 使用子类对象
shared_ptr<Base1*> sptr(new Devired1);	// 模拟向上转型up-cast

2.6 特化 specialization

2.6.1 类模板的全特化 template specialization

  • 泛化(generalization):使用时再指定模板参数的具体类型。
  • 特化(specialization):将模板泛化的部分进行局部的特征化。显式指定部分或全部模板参数的具体类型。
    • 全特化(full specialization):显式指定全部模板参数的具体类型。
    • 偏特化/局部特化(partial specialization):指定部分模板参数的具体类型。

注1:特化的优先级高于泛化。当存在合适的特化版本时,编译器优先调用相应的特化版本。

注2:全特化的所有模板参数均被指定,故模板参数列表为空,即template <>

示例:类模板的泛化和特化

/* 一般的泛化 */
template <class Key>
struct hash {};
/* 不同版本的特化 */
// 本案例只包含1个模板参数,特化后即全特化
template<>	// 全特化的所有参数均被指定,故模板参数列表为空。
struct hash<char> {
    size_t operator()(char x) const {
        return x;
    }
};

template<>
struct hash<int> {
    size_t operator()(int x) const {
        return x;
    }
};

template<>
struct hash<long> {
    size_t operator()(long x) const {
        return x;
    }
};
/* 测试案例 */
// 1.当未指定类型时,则使用泛化的类模板
// 2.当指定特定类型时,则使用相应特化版本的类模板
cout << hash<long>()(1000);	// 第1个()表示匿名对象;第2个()表示调用函数调用运算符

STL源码特化的示例


2.6.2 类模板的偏特化/局部特化 partial specialization

偏特化/局部特化(partial specialization):指定部分模板参数的具体类型。

个数的偏特化

若干数量的模板参数进行特化(显式指定类型)

注1:偏特化的模板参数,必须从左到右连续指定(如偏特化第1/2/3个),而不能穿插指定(如偏特化第1/3/5个),类似于默认参数必须从参数列表的最右侧往左连续。

注2:偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表,否则报错。

/* 泛化的类模板 */
template<typename T, typename Alloc = .../*默认参数*/>	// 模板参数
class vector
{
	... 
};
/* 偏特化的类模板-个数的偏特化 */
// 通过偏特化/局部特化,模板参数的2个泛型类型变为1个
// 偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表
template<typename Alloc = .../*默认参数*/>
class vector<bool, Alloc>	// 偏特化/局部特化:使用指定的bool类型,绑定泛型类型T
{
	... 
};

范围的偏特化

将模板参数类型的表示范围缩小

  • 将泛型类型T缩小为对应的指针类型T*
  • 将泛型类型T缩小为const限定符修饰T const

示例:类模板的泛化和偏特化

/* 泛化的类模板 */
template<typename T>	// 模板参数
class C
{
	... 
};
/* 偏特化的类模板-范围的偏特化:指针类型偏特化 */
template<typename T>	// 模板参数
class C<T*>
{
	... 
};

// 等价写法
template<typename U>	// 模板参数
class C<U*>
{
	... 
};

/* 偏特化的类模板-范围的偏特化:const偏特化 */
template<typename T>	// 模板参数
class C<T const>
{
	... 
};
/* 测试案例 */
/* 使用泛化版本的类模板 */
C<string> obj1;

/* 使用偏特化版本的类模板-范围的偏:指针类型偏特化 */
C<string*> obj2;	// 任意类型T → 任意指针类型T*

/* 使用偏特化版本的类模板-范围的偏:const偏特化 */
C<string const> obj3;	// 任意类型T → const类型T const

示例:类模板的泛化、全特化及偏特化的调用顺序

#include <iostream>
using namespace std;

/* 泛化的类模板 */
template<typename T1, typename T2>
class Test {
public:
	Test(T1 a, T2 b) : _a(a), _b(b) {
		cout << "泛化的类模板" << endl;
	}
private:
	T1 _a;
	T2 _b;
};

/* 全特化的类模板 */
template<>  // 全特化的所有参数均被指定,故模板参数列表为空
class Test<int, double> {
public:
	Test(int a, double b) : _a(a), _b(b) {
		cout << "全特化的类模板" << endl;
	}
private:
	int _a;
	double _b;
};

/* 偏特化的类模板-个数的偏特化 */
template<typename T2>   // 偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表
class Test<int, T2> {
public:
	Test(int a, T2 b) : _a(a), _b(b) {
		cout << "偏特化的类模板-个数的偏特化" << endl;
	}
private:
	int _a;
	T2 _b;
};

/* 偏特化的类模板-范围的偏特化:指针类型偏特化 */
template<typename T1, typename T2>
class Test<T1*, T2*> {  // 将泛型类型 T 缩小为对应的指针类型 T*
public:
	Test(T1* a, T2* b) : _a(a), _b(b) {
		cout << "偏特化的类模板-范围的偏特化:指针类型偏特化" << endl;
	}
private:
	T1* _a;
	T2* _b;
};

/* 偏特化的类模板-范围的偏特化:const偏特化 */
template<typename T1, typename T2>
class Test<T1 const, T2 const> {    // 将泛型类型 T 缩小为const限定符修饰 T const
public:
	Test(T1 a, T2 b) : _a(a), _b(b) {
		cout << "偏特化的类模板-范围的偏特化:const偏特化" << endl;
	}
private:
	T1 _a;
	T2 _b;
};

int main()
{
	Test<string, double> test1("hello", 1.1);   // 泛化的类模板
	Test<int, double> test2(1, 2.2);            // 全特化的类模板
	Test<int, string> test3(2, "world");        // 偏特化的类模板-个数的偏特化
	Test<int*, int*> test4(nullptr, nullptr);   // 偏特化的类模板-范围的偏特化:指针类型偏特化
	Test<const int, const int> test5(1, 2);     // 偏特化的类模板-范围的偏特化:const偏特化
	return 0;
}

2.6.3 函数模板的全特化

函数模板只有全特化没有偏特化

注:C++存在函数重载:形参列表不同(即参数个数、类型或顺序不同)的同名函数,编译器根据函数类型判断调用的重载函数。若函数模板存在偏特化(如个数的偏特化),会与函数重载产生冲突。

示例:函数模板的泛化及全特化

#include <iostream>
using namespace std;

// 函数模板只有全特化,没有偏特化
/* 泛化的函数模板 */
template<typename T1, typename T2>
void func(T1 a, T2 b) {
	cout << "泛化的函数模板" << endl;
}

/* 全特化的函数模板 */
template<>
void func(int a, double b) {
	cout << "全特化的函数模板" << endl;
}

int main()
{
	int a = 1;
	char ch = 'x';
	double b = 2.0;
	func(a, ch);	// 泛化的函数模板
	func(a, b);		// 全特化的函数模板
	return 0;
}

2.7 模板模板参数 template template parameter

模板模板参数模板参数列表中的模板参数(泛型类型)仍是模板。

注:只有在模板参数列表的尖括号<>内,关键字classtypename才等价。

2.7.1 模板模板参数的场景

示例1:模板参数包含容器类型(容器的类模板通常包含多个模板参数

/* 模板模板参数 */
template<typename T,
		 template<typename T>
         class Container	// 第2个模板参数(泛型类型)仍是模板
		>
class XCLs
{
private:
	Container<T> c;		// 第2个模板参数Container以第1个模板参数T作为元素类型   
public:
    ...
};
/* 期望的使用方式 */
int main(){
    // 容器包含多个模板参数,实际不止template<typename T>,其它模板参数通常具有默认值
    //XCLs<string, list> mylist1;	// 编译失败
}
/* 正确的使用方式 */
// 容器包含多个模板参数,如链表list的类模板:list<T, allocator<T>>
template<typename T>
using lst = list<T, allocator<T>>;	// C++ 2.0语法

int main(){
    XCLs<string, lst> mylist2;	// 编译成功
    /* 等价写法 */
    // 第2个模板参数的泛型类型未被完全绑定
    XCLs<string, list<T, allocator<T>> mylist3;	// 编译成功
}

示例2:模板参数包含智能指针类型(智能指针的类模板通常只包含1个模板参数

/* 模板模板参数 */
template<typename T,
		 template<typename T>
         class SmartPtr	// 第2个模板参数(泛型类型)仍是模板
		>
class XCLs
{
private:
	SmartPtr<T> sp;    
public:
    XCLs() : sp(new T) {}	// 构造函数 + 初始化列表
};
/* 使用传入的智能指针SmartPtr作为第2个模板参数的类型,并使用第1个模板参数T作为智能指针的类型 */
int main(){
    XCLs<string, shared_ptr> p1;
    XCLs<long, auto_ptr> p2;
	//XCLs<double, unique_ptr> p3;	// unique_ptr特性不支持
    //XCLs<int, weak__ptr> p4;		// weak_ptr特性不支持
}

2.7.2 非模板模板参数的场景

template<class T,
		 class Sequence = deque<T>	// 默认值为deque<T>,而不是类模板
         >
class stack {
    friend bool operator== <> (const stack&, const stack&);
    friend bool operator< <> (const stack&, const stack&);

protected:
    Sequence c;	// 底层容器
    ...
};
/* 使用方式 */
int main(){
    stack<int> s1;				// 第2个模板参数为默认值deque<int>
    stack<int, list<int>> s2;	// 第2个模板参数为指定值list<int>,泛型类型已完全绑定,不再是模板
}

2.8 c++标准库STL(Standard Library)

  • 容器 Containers
  • 算法 Algorithms
  • 迭代器 Iterators
  • 仿函数 Functors

2.9 不定长的模板参数/可变模板参数 variadic templates

语法

  • 模板参数列表:template<typename T, typename... Types>
  • 函数形参列表或类的泛型列表:Types... argsTypes&... args
  • 调用点:func(args...)
// 形参列表为空的print函数
void print(){}

// 形参列表为 单参 + 可变模板参数 的print函数
/* 可变模板参数 */
template <typename T, typename... Types>
// 将调用者传入的模板参数分为1个和1小包
void print(const T& firstArg, const Types&... args)
{
    cout << firstArg << endl;
    /* 递归调用print()函数 */
    // 递归终止条件:可变模板参数均已访问完毕,最后调用形参列表为空的print函数
    print(args...);
}

int main()
{
    // 依次传入:
    // (1)7.5 和 {"hello", bitset<16>(377), 42}
    // (2)"hello" 和 {bitset<16>(377), 42}
    // (3)bitset<16>(377) 和 42
    // (4)42 和 空参
	// (5)空参
    print(7.5, "hello", bitset<16>(377), 42);	// bitset类包含重载的<<运算符
	// 7.5
    // hello
    // 0000000101111001
    // 42 
}

注1:空参版本和单参 + 可变模板参数版本的print函数必须同时存在,否则当可变模板参数均已访问完毕后,调用出错。

注2:在包含不定长模板参数的模板中,可使用sizeof...(args)操作符,获取不定长模板参数的个数

不定长模板参数


2.10 auto关键字

使用auto关键字声明变量或对象时,编译器自动对其类型进行推导。【语法糖】

/* 正常写法 */
list<string> c;
...
list<string>::iterator ite;
ite = find(c.begin(), c.end(), targetStr);

/* 使用auto关键字 */
list<string> c;
...
auto ite = find(c.begin(), c.end(), targetStr);

注:使用auto关键字声明变量或对象的同时,必须进行定义(显式赋值操作)。

/* 错误写法 */
list<string> c;
...
// auto ite;
// ite = find(c.begin(), c.end(), targetStr);

2.11 基于范围的for循环 ranged-base for

语法

for( decl : coll ) {
	statement
}

for( 声明 : 容器 ) {
	语句
}

注:该写法比迭代器、for_each()简单。【语法糖】

示例1

for(int i : {1, 3, 5, 7, 9}) {
    cout << i << endl;
}

示例2

vector<int> vec
...
    
/* 传值:pass by value */
// 拷贝容器中的元素并赋值给局部变量elem——不影响容器中的原有元素
for(auto elem : vec) {
    cout << elem << endl;
}

/* 传引用:pass by reference */
// 对容器中的元素进行操作——影响容器中的原有元素
for(auto& elem : vec) {
    elem *= 3;
}

2.12 引用 reference

2.12.1 引用的概念

引用:变量或对象的别名。

注1:声明引用时必须显式初始化赋值,且设定初值后不可以再改变引用所代表的变量或对象,但可以修改其值。

注2:引用与指针不等价,但大多数编译器底层使用指针实现引用

注3:变量或对象和其引用的内存大小相同地址相同。(均为假象

int& r = x;

sizeof(r) == sizeof(x); // 内存大小相同

&r = &x; // 地址相同

注4:Java中,所有变量均为引用(reference)。

int x = 0;		// 4字节
// 指针类型:指向变量的地址
int* p = &x;	// 32位系统下指针类型为4字节
// 引用类型:变量的别名,代表该变量
int& r = x;		// 4字节

错误示例

/* 1.声明引用时,必须显式初始化赋值 */
// int& r1;		// 错误
// r1 = x;

/* 2.引用设定初值后,不可再改变 */
int a = 1;
int b = 5;
int& r2 = a;
// 引用声明后,不能再重新代表其它变量或对象
r2 = b;		// r2表示a,将r2和a的值由1修改为5(表示的变量不变,但重新赋值)

示例:变量或对象和其引用的内存大小相同地址相同。(均为假象

typedef struct Stag {int a, b, c, d} S;

int main(){
    double x = 0;
    double* p = &x;	// p指向x,p的值是x的地址
    double& r = x;	// r代表x,r、x的值均为0
    
    cout << sizeof(x) << endl; // 8
    cout << sizeof(p) << endl; // 4(32位系统下)
    cout << sizeof(r) << endl; // 8(假象:引用底层实现为指针,应为4,但表现为8)
    cout << p << endl;	// 0065FDFC
    cout << &x << endl;	// 0065FDFC
    cout << &r << endl;	// 0065FDFC(假象:引用实际存在自己的内存地址,但表现为与变量相同)
	cout << *p << endl;	// 0
    cout << x << endl; 	// 0
    cout << r << endl; 	// 0
    
    S s;
    S& rs = s;
    cout << sizeof(s) << endl;	// 16
    cout << sizeof(rs) << endl;	// 16(假象)
    cout << &s << endl;		// 0065FDE8
    cout << &rs << endl;	// 0065FDE8(假象)
}

2.12.2 引用的常见用途

引用通常较少用于直接声明变量或对象,常用于参数传递(函数参数类型和返回类型的表示)。

/* 非同名函数的调用 */
// 指针传递:pass by pointer
void func1(Cls* pobj) { pobj->xxx(); }
// 值传递:pass by value
void func2(Cls obj) { obj.xxx(); }
// 引用传递:pass by reference
void func3(Cls& obj) { obj.xxx(); }

Cls obj;
func1(&obj);
func2(obj);	// 实参传递时会拷贝对象,效率较差
func3(obj);

注:同名函数的签名,形参列表的非引用版本和引用版本不可同时存在不构成函数重载。否则存在歧义,编译器不清楚应该调用哪个函数。

/* 同名函数的签名,非引用版本和引用版本,不可同时存在,不构成函数重载 */
double func(const double& d) {...}
//double func(const double d) {...}		// Ambiguity(二义性)

注: const是函数签名的一部分,未使用const的版本和使用const的版本可同时存在,构成函数重载

/* 同名函数的签名,未使用const和使用const,可同时存在,构成函数重载 */
string function(const string& str) const {...}
string function(const string& str) {...}

2.13 继承与组合关系下的构造和析构

2.13.1 继承关系下的构造函数/析构函数

继承关系:从内存/数据的角度而言,子类对象包含父类的成分。

构造函数的调用由内向外

子类/派生类的构造函数先调用父类/基类的默认构造函数(由编译器完成),再执行本身。

Derived::Devired(...) : Base() { ... } // 编译器自动调用父类默认构造


析构函数的调用由外向内

子类/派生类的的析构函数先执行本身,再调用父类/基类的析构函数(由编译器完成)。

Derived::~Devired(...) { ... ~Base() } // 执行完子类析构后,再执行父类析构

父类/基类的构造函数必须为虚函数,由子类进行重写,否则会导致未定义的行为。

继承关系下的构造函数、析构函数调用顺序


2.13.2 组合关系下的构造函数/析构函数

组合/复合关系:从内存/数据的角度而言,Container对象包含Component类的成分。

构造函数的调用由内向外

Container的构造函数先调用Component的默认构造函数(由编译器完成),再执行本身。

Container::Container(...) : Component() { ... } // 编译器自动调用Component类默认构造


析构函数的调用由外向内

Container的析构函数先执行本身,再调用Component的析构函数(由编译器完成)。

Container::~Container(...) { ... ~Component() } // 执行完Container类析构后,再执行Component类析构

组合关系下的构造函数、析构函数调用顺序


2.13.3 继承和组合关系下的构造函数/析构函数

从内存/数据的角度而言,Devired对象包含父类和Component类的成分。

构造函数的调用由内向外

Devired的构造函数先调用Base的默认构造函数,再调用Component的默认构造函数,最后执行本身。

Devired::Devired(...) : Base(), Component() { ... }


析构函数的调用由外向内

Devired的析构函数先执行本身,再调用Component的析构函数,最后调用Base的析构函数。

Devired::~Devired(...) { ... ~Component(), ~Base() }

继承+组合关系下的构造函数、析构函数调用顺序


2.14 对象模型(Object Model):虚指针vptr和虚表vtbl

虚指针(virtual pointer):当类包含虚函数时,其对象包含指向虚函数表的虚指针。

虚函数表/虚表(virtual function table):存储的元素均为函数指针,指向虚函数的内存地址

注:继承虚函数,是继承函数的调用权,而不是函数所占的内存大小。

对象模型-虚指针与虚函数表

静态绑定(C语言) vs. 动态绑定(C++语言)

静态绑定(C语言):编译器针对调用的动作,编译为CALL_XXX(地址),当调用某个函数时,编译器进行解析并跳转至对应地址,随后return返回。

动态绑定(C++语言):通过指向对象的指针(可向上转型,即多态),依次查找虚指针、虚函数表及指向的虚函数地址。指向当前对象的指针即this指针

虚机制(virtual mechanism):满足动态绑定的3个条件时,编译器会以动态绑定的形式进行编译。

  1. 通过指向对象的指针调用;
  2. 指向对象的指针向上转型(up-cast),即多态(polymorphism);
  3. 调用虚函数。

使用C语言的语法,实现的动态绑定:

(*(p->vptr)[n])(p);(* p->vptr[n])(p);

注:具体调用哪个类的虚函数,由传入对象的具体类型决定。

注:多态、虚函数、动态绑定(虚指针与虚函数表)为同一概念。


示例:存储不同形状的容器

  • 不同形状对应的类,其对象所占用的内存大小可能不同。
  • C++容器只能存储相同类型的元素。

为保证容器可以存储不同形状的对象,需将容器的元素类型定义为指向形状基类的指针类型

例如:list<Shape*> shapeList;
多态的应用示例-虚指针与虚函数表


2.15 对象模型(Object Model):this指针

this指针:当前调用成员变量或成员函数的对象的地址

注1:C++中所有成员函数均包含隐藏的this指针,表示当前调用该成员函数的对象。
注2:this指针即指向当前对象的指针,可通过动态绑定,依次查找虚指针、虚函数表及指向的虚函数地址

虚函数应用的场景:

  • 多态(Polymorphism)
  • 模板方法(Template Method)

模板方法:在抽象基类中定义整个算法流程,而将若干具体实现步骤(通过使用虚函数延迟子类中。
模板方法使得子类可以不改变抽象基类的算法流程,即可重定义该算法的某些特定步骤(重写虚函数)。
不同的子类能够以不同的逻辑实现特定步骤,而不影响抽象基类的算法流程。

对象模型-this指针


2.16 对象模型(Object Model):动态绑定(Dynamic Binding)

动态绑定与汇编1


动态绑定与汇编2


2.17 const关键字

常成员函数:const修饰的成员函数只访问且不修改类的数据成员。
语法T func() const {}


(常)对象调用(常)成员函数的规则
规则一

对象调用成员函数的关系常对象
const object
(不可修改数据成员)
非常对象
non-const object
(可修改数据成员)
常成员函数
const member functions
(保证不修改数据成员)
非常成员函数
non-const member functions
(不保证不修改数据成员)
×

规则二

当成员函数的const版本和non-const版本同时存在时(即函数重载):

  • 常对象(const object)只会(只能)调用成员函数的const版本;
  • 非常对象(non-const object)只会(只能)调用成员函数的non-const版本。

注:非常成员函数(non-const)可调用常成员函数(const),反之不行。

否则报错:connot convert 'this' pointer from 'const class X' to 'class X &'. Conversion loses qualifiers.(转换丢失限定符)

示例
template class std::basic_string<..> 包括2个同名成员函数。
STL字符串类通过引用计数实现,对象拷贝后共享同一份数据内容。
当涉及共享操作时,需考虑数据修改的问题。

/* const版本 */
// 常量字符串调用const版本,无需考虑COW
charT operator[](size_type pos) const 
{ 
	/* 不必考虑COW(Copy On Write) */
}

/* 非const版本 */
// 非常量字符串调用非const版本,需考虑COW
reference operator[](size_type pos)
{ 
	/* 必须考虑COW(Copy On Write) */
}

注:const属于函数签名的一部分,使用和未使用const的同名函数构成函数重载


2.18 new和delete的补充

2.18.1 new表达式和delete表达式

new表达式:先分配内存,再调用构造函数。

Complex* pc = new Complex(1, 2);
/* 编译器内部实现 */
Complex* pc;

//1.分配内存
//new操作符内部调用malloc()函数
void* mem = operator new(sizeof(Complex));

//2.静态类型转换
pc = static_cast<Complex*>(mem);

//3.调用构造函数
//pc调用构造函数,则pc即隐藏的this指针
pc->Complex::Complex(1, 2);

delete表达式:先调用析构函数,再释放内存。

String* ps = new String("Hello");
...
delete ps;

/* 编译器内部实现 */
//1.调用析构函数
String::~String(ps);
//2.释放内存
//delete操作符内部调用free(ps)函数
operator delete(ps);

注:new表达式的步骤(分配内存、静态类型转换、调用构造函数)和delete表达式的步骤(调用析构函数、释放内存)不可改变;但operator new()operator delete()函数可重载


2.18.2 重载全局函数::operator new、::operator delete、::operator new[]、::operator delete[]

注:::operator new::operator delete::operator new[]::operator delete[]均为全局函数。重载后的全局函数由编译器调用。

void* myAlloc(size_t size)
{
    return malloc(size);
}

void myFree(void* ptr)
{
    free(ptr);
}

/* 重载全局函数::operator new */
inline void* operator new(size_t size)
{
    cout << "self global new()" << endl;
    return myAlloc(size);
}

/* 重载全局函数::operator new[] */
inline void* operator new[](size_t size)
{
    cout << "self global new[]()" << endl;
    return myAlloc(size);
}

/* 重载全局函数::operator delete */
inline void operator delete(void* ptr)
{
    cout << "self global delete()" << endl;
    myFree(ptr);
}

/* 重载全局函数::operator delete[] */
inline void operator delete[](void* ptr)
{
    cout << "self global delete[]()" << endl;
    myFree(ptr);
}

2.18.3 重载成员函数operator new/delete、operator new[]/delete[]

注:重载成员函数operator new/deleteoperator new[]/delete[],可改变调用者的行为。

重载成员函数operator new/delete
class Foo {
public:
    /* per-class allocator */
	void* operator new(size_t);
    void operator delete(void*, size_t /* optional */);
};
/* 调用成员函数operator new */
Foo* p = new Foo;

/* 调用成员函数operator new的底层步骤 */
try{
    // 1.调用重载后的成员函数operator new
    void* mem = operator new(sizeof(Foo));
    // 2.静态类型转换:void* → Foo*
    p = static_cast<Foo*>(mem);
    // 3.调用构造函数
    p->Foo:Foo();
}
/* 调用成员函数operator delete */
delete p;

/* 调用成员函数operator delete的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo();
// 2.调用重载后的成员函数operator delete
operator delete(p);

重载成员函数operator new delet


重载成员函数operator new[]/delete[]
class Foo {
public:
    /* per-class allocator */
	void* operator new[](size_t);
    void operator delete[](void*, size_t /* optional */);
};
/* 调用成员函数operator new[] */
Foo* p = new Foo[N];

/* 调用成员函数operator new[]的底层步骤 */
try{
    // 1.调用重载后的成员函数operator new[]
    void* mem = operator new(sizeof(Foo) * N + 4);
    // 2.静态类型转换:void* → Foo*
    p = static_cast<Foo*>(mem);
    // 3.调用构造函数
    p->Foo:Foo();	/* 调用N次构造函数 */
}
/* 调用成员函数operator delete[] */
delete[] p;

/* 调用成员函数operator delete[]的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo();	/* 调用N次析构函数 */
// 2.调用重载后的成员函数operator delete
operator delete(p);

重载成员函数operator new[] delete[]


2.18.4 示例

示例1:调用全局函数及成员函数的operator new/delete

注:若调用者使用全局函数::operator new/delete,则编译器底层调用全局的void* ::operator new(size_t)void operator delete(void*)

全局函数及成员函数的operator new delete


示例2:调用重载的成员函数operator new[]/delete[]

示例_调用重载的成员函数operator new[] delete[]


示例3:调用重载的全局函数::operator new[]/delete[]

当调用者使用全局函数::operator new[]/delete[]时,编译器底层会强制使用全局函数版本。

示例_调用重载的全局函数operator new[] delete[]


2.18.5 重载placement new / placement delete

重载placement new,即new()

类成员函数operator new()可进行函数重载,需满足如下条件:

  • 每个重载版本的函数声明,必须有唯一的参数列表(不同重载版本的参数列表不能相同);
  • 参数列表的第1个参数,必须是size_t类型;
  • 参数列表的其余参数,以new所指定的placement argument为初值,即出现在new(...)小括号中的参数。

例:Foo* pf = new(300, 'c') Foo; // 调用无参构造函数,300和’c’均为placement argument


示例:重载new()

/* 构造不同重载版本的operator new() */
class Foo {
public:
    // 无参构造
    Foo() {cout << "Foo:Foo()" << endl;}
    // 有参构造
    Foo(int) {
        cout << "Foo:Foo(int)" << endl;
    	// 特意抛出异常,用于测试placement operator delete
        throw Bad();	// 异常类:class Bad {};
    }
    
    /* operator new()的不同重载版本 */
    // (1) 一般重载版本:不包含placement argument
    void* operator new(size_t size) {
        return malloc(size);
    }
    
    // (2) STL提供的placement new()的重载形式
	void* operator new(size_t size, void* start) {
        return start;
    }
    
    // (3) 自定义的placement new版本
	void* operator new(size_t size, long extra) {
        return malloc(size + extra);
    }

    // (4) 自定义的placement new版本
	void* operator new(size_t size, long extra, char init) {
        return malloc(size + extra);
    }
    
    /*
    // (5) 特意遗漏第1参数的placement new版本:第1参数必须是size_t,因此构造抛出异常的场景
	//错误信息:
    //[Error]'operator new' takes type 'size_t'('unsigned int') as first parameter
    void* operator new(long extra, char init) {
        return malloc(extra);
    }
    */
};

重载placement delete,即delete()

类成员函数operator delete()可进行函数重载(非强制),但重载版本一定不会被delete所调用

仅当new所调用的构造函数抛出异常Exception),才会调用重载版本的operator delete()

注:operator delete()仅能在该条件下呗调用,主要作用是释放未完全创建成功的对象所占用的内存。


示例:重载delete()

注:operator delete()即使未与operator new()一一对应,也不会出现任何报错,编译器会视为:放弃处理构造函数抛出的异常。

VC6 warning C4291: ‘void’ *__cdecl Foo::operator new(~~~) no matching operator delete found; memory will not be freed if initialization throws an exception.

/* 针对不同重载版本的operator new()构造的operator delete() */
// 仅当构造函数抛出异常时,与operator new()对应版本的operator delete()才会被调用
// 作用:释放对应operator new()分配的内存
    
/* operator delete()的不同重载版本 */
// (1) 一般重载版本:不包含placement argument
void* operator delete(void*, size_t /* 可选 */) {
    cout << "operator delete(void*, size_t)" << endl;
}

// (2) STL提供的placement delete()的重载形式
void* operator delete(void*, void*) {
    cout << "operator delete(void*, void*)" << endl;
}

// (3) 自定义的placement delete版本
void* operator delete(void*, long) {
    cout << "operator delete(void*, long)" << endl;
}

// (4) 自定义的placement delete版本
void* operator delete(void*, long, char) {
    cout << "operator delete(void*, long, char)" << endl;
}
/* 测试程序 */
Foo start;
// 调用无参构造函数
Foo* p1 = new Foo;
Foo* p2 = new(&start) Foo;
Foo* p3 = new(100) Foo;
Foo* p4 = new(100, 'a') Foo;

// 调用有参构造函数
Foo* p5 = new Foo(1);	// 调用时已抛出异常,后续代码不会继续执行
// Foo* p6 = new(&start) Foo(1);
// Foo* p7 = new(100) Foo(1);
// Foo* p8 = new(100, 'a') Foo(1);

2.18.6 重载placement new/delete的STL案例

STL案例_重载placement new delete

  • 9
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值