《C++新经典》第14章 类

2023-05-16

《C++新经典》第14章 类

  • 14.1 成员函数、对象复制与私有成员
    • 14.1.1 总述
    • 14.1.2 类基础
    • 14.1.3 成员函数
    • 14.1.4 对象的复制
    • 14.1.5 私有成员
  • 14.2 构造函数、explicit与初始化列表
    • 14.2.1 称呼上的统一
    • 14.2.2 构造函数
    • 14.2.3 多个构造函数
    • 14.2.4 默认参数函数
    • 14.2.5 隐式转换和explicit
    • 14.2.6 构造函数初始化列表
  • 14.3 inline、const、mutable、this与static
    • 14.3.1 类定义中实现成员函数inline
    • 14.3.2 成员函数末尾的const
    • 14.3.3 mutable
    • 14.3.4 this
    • 14.3.5 static成员
  • 14.4 类内初始化、默认构造函数、“=default”和“=delete”
    • 14.4.1 类相关非成员函数
    • 14.4.2 类内初始值
    • 14.4.3 const成员变量初始化
    • 14.4.4 默认构造函数
    • 14.4.5 “=default;”和“=delete;”
  • 14.5 拷贝构造函数
  • 14.6 重载运算符、拷贝赋值运算符与析构函数
    • 14.6.1 重载运算符
    • 14.6.2 拷贝赋值运算符(赋值运算符)
    • 14.6.3 析构函数(释放函数)
    • 14.6.4 几个话题
  • 14.7 子类、调用顺序、访问等级与函数遮蔽
    • 14.7.1 子类概念
    • 14.7.2 子类对象定义时调用构造函数的顺序
    • 14.7.3 访问等级(public、protected与private)
    • 14.7.4 函数遮蔽
  • 14.8 父类指针、虚/纯虚函数、多态性与析构函数
    • 14.8.1 父类指针与子类指针
    • 14.8.2 虚函数
    • 14.8.3 多态性
    • 14.8.4 纯虚函数与抽象类
    • 14.8.5 父类的析构函数一般写成虚函数
  • 14.9 友元函数、友元类与友元成员函数
    • 14.9.1 友元函数
    • 14.9.2 友元类
    • 14.9.3 友元成员函数
  • 14.10 RTTI、dynamic_cast、typeid、type-info与虚函数表
    • 14.10.1 RTTI
    • 14.10.2 dynamic_cast运算符
    • 14.10.3 typeid运算符
    • 14.10.4 type-info类
    • 14.10.5 RTTI与虚函数表
  • 14.11 基类与派生类关系的详细探讨
    • 14.11.1 派生类对象模型简介
    • 14.11.2 派生类构造函数
    • 14.11.3 既当父类又当子类
    • 14.11.4 不想当基类的类
    • 14.11.5 静态类型与动态类型
    • 14.11.6 派生类向基类的隐式类型转换
    • 14.11.7 父类、子类之间的复制与赋值
  • 14.12 左值、右值、左值引用、右值引用与move
    • 14.12.1 左值和右值
    • 14.12.2 引用分类
    • 14.12.3 左值引用
    • 14.12.4 右值引用
    • 14.12.5 std::move函数
    • 14.12.6 左值、右值总结说明
  • 14.13 临时对象深入探讨、解析与提高性能手段
    • 14.13.1 临时对象概念
    • 14.13.2 产生临时对象的情况和解决方案
  • 14.14 对象移动、移动构造函数与移动赋值运算符
    • 14.14.1 对象移动概念
    • 14.14.2 移动构造函数与移动赋值运算符概念
    • 14.14.3 移动构造函数演示
    • 14.14.4 移动赋值运算符演示
    • 14.14.5 合成的移动操作
    • 14.14.6 总结
  • 14.15 继承的构造函数、多重继承、类型转换与虚继承
    • 14.15.1 继承的构造函数
    • 14.15.2 多重继承
    • 14.15.3 类型转换
    • 14.15.4 虚基类与虚继承(虚派生)
  • 14.16 类型转换构造函数、运算符与类成员指针
    • 14.16.1 类型转换构造函数
    • 14.16.2 类型转换运算符(类型转换函数)
    • 14.16.3 类型转换的二义性问题
    • 14.16.4 类成员函数指针
    • 14.16.5 类成员变量指针

14.1 成员函数、对象复制与私有成员

14.1.1 总述

类是自定义的新的数据类型。

14.1.2 类基础

  • 类是新的数据类型。
  • 类由成员变量、成员函数等构成。
  • 对象名.成员或对象指针->成员访问类中成员。
  • public供外界调用,private封装。
  • struct是默认public的class。
  • class默认private。

14.1.3 成员函数

Time.h

#ifndef __Time__
#define __Time__
class Time {
public:
	int Hour;
	int Minute;
	int Second;
	void initTime(int hour, int min, int sec);
}
#endif

Time.cpp

#include "Time.h"
//::作用域运算符,表示initTime函数属于Time类
void Time:: initTime(int hour, int min, int sec) {
	Hour = hour;
	Minute = min;
	Second = sec;
}

类是特殊存在,允许重复定义多次(多次include类头文件)。类定义也称为类声明。

14.1.4 对象的复制

对象的复制,就是定义新对象时用老对象里面的内容进行初始化,逐个成员复制(默认浅拷贝,可重写)。

Time myTime2 = myTime;
Time myTime3(myTime);
Time myTime4{myTime};
Time myTime5 = {myTime};
Time myTime6;
myTime6 = myTime; //赋值运算符复制对象

14.1.5 私有成员

类的私有成员变量和私有成员函数只能在类的成员函数内调用,外界是无法直接调用的。

14.2 构造函数、explicit与初始化列表

14.2.1 称呼上的统一

  1. class内部实现成员函数,“成员函数的定义”。
class A {
	public:
		void func() {
			//成员函数的定义
		}
};
  1. class内部只声明成员函数,“成员函数的声明”;外部实现,“成员函数的实现”。
class A {
	public:
		void func();//成员函数的声明
};

void A::func() {
	//成员函数的实现
}

14.2.2 构造函数

构造函数,特殊的成员函数,与类名相同,创建类对象时,系统自动调用,用于初始化类对象的数据成员(成员变量)。

  1. 构造函数无返回值,不写函数头。
  2. 不可以手工调用构造函数。
  3. 正常情况下,构造函数应该被声明为public。
  4. 构造函数中若有参数,创建对象时要指定参数。
class Time {
public:
	TIme(int h, int m, int s);
private:
	int Hour;
	int Minute;
	int Second;
};

Time::Time(int h, int m, int s) {
	Hour = h;
	Minuute = m;
	Second = s;
}
//调用构造函数
Time myTime = Time(12, 13, 52);
Time myTime2(12, 13, 52);
Time myTime3 = Time{12, 13, 52};
Time myTime4{12, 13, 52};
Time myTime5 = {12, 13, 52};

//错误
Time myTime6();
Time myTime7(12, 13);
Time myTime(12, 13, 52);

//调用拷贝构造函数
Time myTime2 = myTime;
Time myTime3(myTime);
Time myTime4{myTime};
Time myTime5 = {myTime};

14.2.3 多个构造函数

Time::Time(int h, int m, int s) {
	Hour = h;
	Minuute = m;
	Second = s;
}


Time::Time() {
	Hour = 12;
	Minuute = 59;
	Second = 59;
}
//调用无参构造函数
Time myTime10 = Time();
Time myTime12;
Time myTime13 = Time{};
Time myTime14{};
Time myTime14 = {};

14.2.4 默认参数函数

默认参数一般放在函数声明而不放在函数实现(定义)中,除非该函数没有申明只有定义。
多参数函数中的默认参数必须出现在非默认参数右侧。

class Time {
public:
	TIme(int h, int m = 58, int s = 12);
private:
	int Hour;
	int Minute;
	int Second;
};

Time::Time(int h, int m, int s) {
	Hour = h;
	Minuute = m;
	Second = s;
}

14.2.5 隐式转换和explicit

class Time {
public:
	TIme(int h);
};

//隐式类型转换
//传递14调用单参数构造函数
//生成临时对象,然后将值复制给myTime23的成员变量
//或者不涉及临时对象
TIme myTime23 = 14; 
TIme myTime24 = (12, 13, 14, 15, 16);//传递最后一个数字16调用单参数构造函数

//系统明确调用单参数构造函数
TIme myTime100 = {14}; 

void func(Time tmp) {
	return;
}
func(16);//传递16调用单参数构造函数,生成临时对象tmp,函数结束后释放

构造函数中声明explicit(显示),只能用于初始化和显示类型转换

class Time {
public:
	explicit Time();
	explicit TIme(int h);//一般单参数构造函数都声明为explicti
	explicit TIme(int h, int m, int s);
};

Time myTime5 = {12, 23, 55}; //错误,隐式初始化(构造并初始化)
Time myTime4{12, 23, 55};//正确,显示初始化(直接初始化)

//错误,隐式初始化
Time myTime100 = {16};
Time myTime101 = 16;
func(16);

//修改,显示初始化
TIme myTime100(16);
TIme myTime100{16};
TIme myTime100 = Time(16);
TIme myTime100 = Time{16};
func(Time(16));

Time time1{};//正确,显示转换
Time time2 = {}; //错误,隐式初始化

//错误,隐式转换
func({});
func({12, 23, 34});

//正确,显式转换
func(Time{});
func(Time{12, 23, 34});

14.2.6 构造函数初始化列表

冒号括号式写法,只能用于构造函数定义(实现)中。调用构造函数时,初始化成员变量值。

//初始化列表先于函数体执行
Time::Time(int h, int m, int s):Hour(h), Minute(n), Second(s) {}


Time::Time(int h, int m, int s) { //函数体内赋值
	Hour = h;
	Minute = m;
	Second = s;
}

类类型的成员变量,初始化列表初始化比函数体内赋值初始化效率更高,因为少调用了该成员变量类的各种特殊成员函数,比如构造函数等。

初始化列表赋值顺序,不依据初始化列表从左到右的顺序,而是依据类成员变量从上到下定义的顺序。

//错误
Time::Time(int h, int m, int s):Hour(h), Second(s), Minute(Second) {}

14.3 inline、const、mutable、this与static

14.3.1 类定义中实现成员函数inline

class Time {
public:
	void addH(int h) { //类定义中实现的成员函数会被当做inline内联函数处理。
		Hour += h;
	}
};

14.3.2 成员函数末尾的const

  • 成员函数的声明和实现都需要const,表示成员函数不会修改类对象的任何状态,常量成员函数。
  • const对象只能调用const成员函数,非const对象能调用const和非const成员函数。
  • const成员函数能被const和非const对象都能调用,非const成员函数只能被非const对象都能调用。

14.3.3 mutable

不稳定的,容易改变的,用于突破const的限制。
mutable修饰成员变量,成员变量永远处于可变状态,即使是在const成员函数中。此时const和非const对象都能调用const成员函数,且其中mutable修饰的成员变量值能被修改。

class Time {
public:
	void noone() const {
		Hour += 3;
	}
private:
	matable int Hour;
};

14.3.4 this

this是系统保留字,是一个常量指针(指向固定,指向值能改变),指向本对象的指针,*this表示本对象。

class Time {
public:
	Time& rtnhour(int t) {
		Hour += t;
		return *this;
	}
private:
	int Hour;
};

Time mytime;
mytime.rtnhour(3);

调用成员函数时,编译器将成员函数对象地址传递给成员函数中隐藏的this形参。

Time &Time::rtnhour(int t);

//实际定义
Time &Time::rthhour(Time * const this, int t);

//实际调用
mytime.rtnhout(&mytime, 3);
  • this只能在成员函数(普通成员函数+部分特殊成员函数)中使用,全局函数、静态函数等不能使用this。
  • 普通成员函数中,this是指向非const对象的指针常量。
  • const成员函数中,this是指向const对象的const指针。
class Time {
public:
	Time& rtnhour(int t) {
		Hour += t;
		return *this;
	}
	Time& rtnminute(int m) {
		Minute += m;
		return *this;
	}
private:
	int Hour;
	int Minute
};

Time mytime;
mytime.rtnhour(3).rtnminute(5);

14.3.5 static成员

static成员变量(静态成员变量),不属于某个对象,而是属于整个类,可以通过对象名或类名访问和修改,所有该类的对象共享,cpp中定义时可以不添加static关键字。

static成员函数,只能操作static成员变量,不能操作对象成员变量,实现时可以不添加static关键字。

cpp中定义static成员变量,并初始化。

class Time {
public:
	static int mystatic;
	static void staticfunc(int t);
private:
	int Minute;
};

//cpp
int Time::mystatic = 5;//无初值时系统默认给0,static可省略
void Time::staticfunc(int t) { //static可以省略
	//Minute = t;//错误,对象成员变量不能出现在静态成员函数中。
	mystatic = t;
}


cout <<Time::mystatic <<endl;

Time mytime;
mytime.mystatic = 12;
cout <<Time::mystatic <<endl;
cout <<mytime.mystatic <<endl;

Time::staticfunc(22);
mytime.staticfunc(11);
cout <<Time::mystatic <<endl;
cout <<mytime.mystatic <<endl;

14.4 类内初始化、默认构造函数、“=default”和“=delete”

14.4.1 类相关非成员函数

void writeTime(const Time &time) {
	cout <<time.Hour <<endl;
}

14.4.2 类内初始值

class Time {
public:
	Time(int h):Hour(h) {//2
		Hour = h+2; //3
	}
private:
	int Hour{0}; //1
	//int Hour = 0;//会被构造函数初始化列表或构造函数赋初值给覆盖掉
};

14.4.3 const成员变量初始化

只能类内初始化或使用初始化列表初始化,不能构造函数内部赋值操作(构造函数不能声明成const)。

class Time {
public:
	Time(int h):Hour(h) {//2
		//Hour = h+2; //错误
	}
private:
	const int Hour = 0;//会被构造函数初始化列表赋初值给覆盖掉
};

14.4.4 默认构造函数

无参数的构造函数称为默认构造函数。

若类无构造函数,编译器在一定情形下(int Hour{0}会生成,int Hour不会生成,测试。)会合成默认构造函数。

class Time {
public:
	explicit Time() {
		Hour = 12; 
	}
private:
	int Hour{0};
};

14.4.5 “=default;”和“=delete;”

  1. =default;
class Time {
public:
	Time(int v){};
	Time() = defult;//生成默认构造函数
	//Time() {}; //与上面等价
private:
	int Hour = 0;
};

//cpp中定义默认构造函数
Time::Time() = defalut;//不具备inline特性
  1. =delete;
class Time {
public:
	Time(int v){};
	Time() = delete;//显示禁止生成默认构造函数
private:
	int Hour = 0;
};

14.5 拷贝构造函数

构造函数的第一个参数是所属类类型引用(一般const),若有额外参数,这些额外参数都有默认值,这种构造函数叫拷贝构造函数。

Time(const Times& time, int a=3);

Time::Time(const Times& time, int a) {
}
Time time;
//调用拷贝构造函数
Time time2 = time; //隐式类型转换
Time time3(time);
Time time4{time};
Time time5 = {time};//隐式类型转换

//没有调用拷贝构造函数
Time time6;
time6 = time;
  • 单参数构造函数一般声明为explicit;拷贝构造函数一般不声明为explicit。
  • 类无拷贝构造函数,编译器根据具体需要可能合成拷贝构造函数,进行浅拷贝。

14.6 重载运算符、拷贝赋值运算符与析构函数

14.6.1 重载运算符

重载运算符本质上是函数,具有返回类型和参数列表。
一般形式:返回值 operator运算符(参数列表);

bool Time::operator==(const Time &t) {
	if(this == &t)
		return true;
	if(Hour == t.Hour)
		return true;
	return false;
}

class TmpClass {
public:
	TmpClass& operator=(const TmpClass& t) {
		//...
		return *this;
	}
}
;

14.6.2 拷贝赋值运算符(赋值运算符)


class Time {
public:
	Time & operator=(const Time & t) {
		//...
		return *this;
	}
};
Time time;

Time time2 = time; //调用构造函数

TIme time6;
time6 = time; //调用赋值运算符重载函数

14.6.3 析构函数(释放函数)

对象销毁时,调用析构函数。
构造函数中new了一段内存,需要析构函数中delete释放掉。
对象离开作用域,或被delete显示销毁时,析构函数会被系统调用。

14.6.4 几个话题

  1. 构造函数的成员初始化
    类类型成员变量的初始化,尽量放在构造函数的初始化列表,不要放在构造函数的函数体里面进行,可以避免多次不必要的成员函数调用。
#include <iostream>
using namespace std;

class TmpClass {
public:
    TmpClass(int v){
        cout <<"TmpClass()\n";
    }
    ~TmpClass(){
        cout <<"~TmpClass()\n";
    }
};

class Time {
public:
    Time(int v=5):tmp(5){ //tmp(5)只调用一次构造函数+一次析构函数(释放时)
        cout <<"Time()\n";
        //tmp = v;  // 产生一次构造函数+一次operator=赋值运算符+一次析构函数(离开Time构造函数释放临时对象)
    }
    ~Time(){
        cout <<"~Time()\n";
    }
private:
    TmpClass tmp; //声明,未生成实例
};


int main() {
    Time time;
    cout << "Hello World!" << endl;
    return 0;
}
  1. 析构函数的成员销毁
  • 对象中的成员变量是在析构函数体执行完后,系统隐含销毁。
  • 类中先定义的成员变量先进行初始化,然后类的构造函数;销毁时,先类的析构函数,然后先定义的成员变量后销毁。
  • malloc/new分配内存(构造函数),需要自己free/delete释放(析构函数)。
  1. new和delete对象
    new和new()的区别
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {};
    ~MyClass() {};
public:
    int getValue() {
        return _value;
    }
private:
    int _value;
};

class MyClass2 {
public:
    int getValue(){
        return _value;
    }
private:
    int _value;
};

int main() {
    MyClass *a = new MyClass;    /*无括号*/
    MyClass *b = new MyClass();  /*有括号*/
    cout << "a = " << a->getValue() << endl;
    cout << "b = " << b->getValue() << endl;
    delete  a; a = nullptr;
    delete  b; b = nullptr;

    MyClass2 *a2 = new MyClass2;
    MyClass2 *b2 = new MyClass2();
    cout << "a2 = " << a2->getValue() << endl;
    cout << "b2 = " << b2->getValue() << endl;
    delete  a2; a2 = nullptr;
    delete  b2; b2 = nullptr;

    int *c = new int;
    int *c2 = new int();
    cout <<"c=" <<*c <<endl;
    cout <<"c2=" <<*c2 <<endl;
    delete c; c = NULL;
    delete c2;c2 = NULL;

    int *c3 = new int[5];
    int *c4 = new int[5]();
    cout <<"c3=" <<c3[2] <<endl;
    cout <<"c4=" <<c4[2] <<endl;
    delete [] c3;c3 = NULL;
    delete [] c4;c4 = NULL;

    return 0;
}
Time * time = new Time(); //构造函数
delete time; //析构函数

14.7 子类、调用顺序、访问等级与函数遮蔽

14.7.1 子类概念

父亲类(父类/基类/超类),孩子类(子类/派生类);子类能从父类继承成员函数和成员变量。

class 子类名: 继承方式 父类名 {}
  • 继承方式(访问等级/访问权限):public、protecte、private。
  • 父类:一个子类可继承多个父类。
class Human {
public:
	Human() {
		cout <<"Human()\n";
	}
	~Human() {
		cout <<"~Human()\n";
	}
private:
	int age;
	char name[32];
};

class Men: public Human {
public:
	Men() {
		cout <<"Men()\n";
	}
}

14.7.2 子类对象定义时调用构造函数的顺序

先执行父类构造函数,再执行子类构造函数。

14.7.3 访问等级(public、protected与private)

访问权限修饰符(修饰类中成员变量、成员函数)

  • public,任意实体访问。
  • protected,只允许本类或子类成员函数访问。
  • private,只允许本类成员函数访问。

三种继承方式,public、protected与private继承。

  • public继承,父类所有成员(函数和变量)在子类中访问权限不改变;
  • protected继承,父类public成员变成子类protected成员;
  • private继承,父类所有成员在子类访问权限变成private;
  • 父类private成员不受继承方式影响,子类永远无权访问;
  • 对于父类,尤其是成员函数,不想外面访问,设置private;想让子类访问,设置protected;想公开,设置为public。

14.7.4 函数遮蔽

只要子类与父类中的函数名相同(无论函数参数,返回值),子类中的函数都会遮蔽掉父类中的同名函数。

class Human {
public:
	void samenamefunc() {
		cout <<"Human::samenamefunc()\n";
	}
	void samenamefunc(int) {
		cout <<"Human::samenamefunc(int)\n";
	}
};

class Men: public Human {
public:
	void samenamefunc(int) {
		cout <<"Men::samenamefunc(int)\n";
		Human::samenamefunc();//调用父类samenamefunc()函数
		Human::samenamefunc(120);//调用父类samenamefunc(int)函数
	}
}

Men men;
men.samenamefunc(); //错误,无法调用父类samenamefunc()函数
men.samenamefunc(1); //调用子类samenamefunc()函数
//必须public继承
men.Human::samenamefunc();//调用父类samenamefunc()函数
men.Human::samenamefunc(120);//调用父类samenamefunc(int)函数
class Men: public Human {
public:
	using Human::samenamefunc; //让父类所有同名函数samenamefunc在子类中可见,即父类同名函数在子类中以重载方式使用
	void samenamefunc(int) {
		cout <<"Men::samenamefunc(int)\n";
	}
}

Men men;
men.samenamefunc(); //调用父类samenamefunc()函数
men.samenamefunc(1); //调用子类samenamefunc()函数

14.8 父类指针、虚/纯虚函数、多态性与析构函数

14.8.1 父类指针与子类指针

class Human {
public:
	void funchuman() {
		cout <<"Human::funchuman()\n";
	}
};

class Men: public Human {
public:
	void funcmen() {
		cout <<"Men::funcmen\n";
	}
}

//Men *pmen = new Human;//错误,子类指针不可以new父类对象
Human *phuman = new Men;//正确,父类指针可以new子类对象
phuman->funchuman();//正确,父类指针调用父类成员函数
//phuman->funcmen();//错误,父类指针不可以调用子类成员函数

14.8.2 虚函数

class Human {
public:
	void eat() {
		cout <<"Human::eat()\n";
	}
};

class Men: public Human {
public:
	void eat() {
		cout <<"Men::eat\n";
	}
};

Human *phuman = new Men;//正确,父类指针可以new子类对象
phuman->eat();//正确,父类指针调用父类成员函数
class Human {
public:
	virtual void eat() { //虚函数,父类函数声明必须添加virtual,定义中不能添加
		cout <<"Human::eat()\n";
	}
};

class Men: public Human {
public:
	virtual void eat() override {//父类添加virtual情况下,子类可加可不加virtual,方便他人阅读添加virtual。
	//override只用于子类虚函数中,避免子类写错虚函数名称,形参等。
	//final用于类表示最终类,之后不能被继承,class Base final {};
	//class Base {}; class Sub final: public Base {};
	//final只能用于修饰虚函数,表示终结,不能被子类覆盖。class Human {public: virtural void eat() final {} };
	//父类虚函数,在所有子类中同名函数都是虚函数
	//子类虚函数的形参要与父类完全一致,否则是不同函数
		cout <<"Men::eat\n";
	}
};

Human *phuman = new Human; 
phuman->eat();//正确,父类指针调用父类成员函数
delete phuman;

phuman = new Men;//正确,父类指针可以new子类对象
phuman->eat();//正确,父类指针调用子类成员函数,因为virtual
phuman->Human::eat();//正确,父类指针调用父类成员函数,因为virtual
delete phuman;

虚函数返回值:

  • 如果父类虚函数返回的是基础数据类型,派生类虚函数的返回类型要与父类严格一致,否则报错。
  • 如果父类虚函数返回的是某个父类(Base)的指针或引用,派生类虚函数的返回类型可以是Base类或者Base派生类(Drive)的指针或引用(这个地方要注意,只能是Base或者Base的派生,Base的father是不可行的,会报错)。

virtual关键字定义的虚函数的作用:
用父类指针指向子类实例,调用virtual虚函数成员时,执行动态绑定函数。动态绑定,运行时,根据父类指针具体指向子类实例,决定调用的具体函数。

14.8.3 多态性

只有虚函数才存在多态性。

  • 体现在具有继承关系的父类和子类间。子类重新定义(覆盖/重写)父类virtual虚函数。
  • 通过父类指针,指向子类实例,程序运行时,决定具体调用函数。系统内部实际查找类的虚函数表,找到函数入口地址并调用,这是运行时的多态性。

14.8.4 纯虚函数与抽象类

只在父类中声明,要求任何子类必须定义(=0)的虚函数,叫纯虚函数(公共接口,规范)。
带纯虚函数的类叫抽象类,不能用于生成实例,目的是统一管理子类。
未实现父类纯虚函数的子类也是抽象类。

class Human {
public:
    virtual void eat() = 0;
};

class Men: public Human {
public:
    virtual void eat() override final {
        cout <<"Men::eat\n";
    }
};

Human *phuman = new Men;//正确,父类指针可以new子类对象
phuman->eat();//正确,父类指针调用父类成员函数
delete phuman;
//Human phuman2;//错误,抽象类不能实例化

14.8.5 父类的析构函数一般写成虚函数

子类对象定义和销毁过程中,先执行父类构造函数,子类构造函数,然后执行子类析构函数,父类析构函数。

#include <iostream>
using namespace std;

class Human {
public:
    Human() {
        cout <<"Human()\n";
    }
    //virtual
    ~Human() { //父类析构函数未添加virtual,当父类指针new子类对象,delete时,子类析构函数不会执行
        cout <<"~Human()\n";
    }
    virtual void eat() {
        cout <<"Human::eat\n";
    }
};

class Men: public Human {
public:
    Men() {
        cout <<"Men()\n";
    }
    ~Men() { //父类析构函数添加了virtual,子类析构函数自动成为虚函数,可以不添加virtual
        cout <<"~Men()\n";
    }
    virtual void eat() override final {
        cout <<"Men::eat\n";
    }
};

int main() {
    Human *phuman;

    phuman= new Human;
    phuman->eat();
    delete phuman;

    phuman = new Men;
    phuman->eat();
    delete phuman;

    return 0;
}

结论:

  • 父类析构函数务必添加virtual,能保证delete父类指针时正确调用析构函数(不漏掉子类析构函数)。
  • 有孩子的父类,务必添加virtual析构函数。
  • 虚函数(virtual)会增加内存和执行效率上的开销,因为存在虚函数表,里面存放函数地址等信息。
  • 父类析构函数添加virtual,保证delete指向子类实例的父类指针时,系统能够依次调用子类析构函数(未添加virtual,不会调用子类析构函数。释放子类中开辟内存空间)和父类析构函数。

14.9 友元函数、友元类与友元成员函数

14.9.1 友元函数

class Tmp {
public:
	void func() const {
		cout <<"Tmp::func() const" <<endl;
	}
};

void func(const Tmp &tmp) {
	tmp.func();
}

Tmp tmp;
func(tmp);

让func函数成为Tmp类的友元函数,func函数就能访问Tmp类的所有成员(变量和函数),无论使用什么修饰符(public、protected、private)。

//Tmp.h中声明
friend void func(const Tmp &tmp); //声明,表明该函数是Tmp类的友元函数

14.9.2 友元类

让类B成为类A的友元类,类B就能访问类A的所有成员(变量和函数),无论使用什么修饰符(public、protected、private)。

class B; //前置声明类B
class A {
	friend class B; //友元类声明,不需要修饰符public、protected、private,不需要类B的定义
private:
	int data;
};

class B {
public:
	void callBAF(int x, A& a) {
		a.data = x;
	}
};

A a;
B b;
b.callBAF(3, a);

每个类负责控制自己的友元类和友元函数,注意点:

  • 友元关系不能被子类继承。
  • 友元关系是单向的。
  • 友元关系无传递性。

14.9.3 友元成员函数

让类B中的某些成员函数成为类A的友元函数,成为类A友元函数的类B的某些成员函数就能访问类A的所有成员(变量和函数),无论使用什么修饰符(public、protected、private)。

#include <iostream>
using namespace std;

//B.h
#ifndef B_H
#define B_H

class A; //前置声明
class B {
public:
    void callBAF(int x, A& a);
};

#endif

//A.h
#ifndef A_H
#define A_H
//#include "B.h"

class A {
    friend void B::callBAF(int x, A& a);
private:
    int data;
};

#endif

//B.cpp
//#include ".h"
//#include "B.h"
void B::callBAF(int x, A& a) {
    a.data = x;
}

//A.cpp
//#include "A.h"

int main() {
    A a;
    B b;
    b.callBAF(3, a);

    return 0;
}

友元概念优缺点:

  • 优点:允许特定情况下非成员函数或类访问类的protected或private成员。
  • 缺点:破坏类的封装性(private用意是不允许外界访问),降低类的可靠性和可维护性。

14.10 RTTI、dynamic_cast、typeid、type-info与虚函数表

14.10.1 RTTI

Run Time Type Indetification,运行时类型识别,程序使用父类指针或引用来检查其指向对象的实际(子)类型。

Human *phuman = new Men;
Human &q = *phuman;

Men men;
Human &f = men;
  • dynamic_cast运算符,将父类指针或引用安全地转换为子类指针或引用。
  • typeid运算符,返回指针或引用所指对象的实际类型。
    这两个运算符需要父类至少有一个虚函数才能正常工作。

14.10.2 dynamic_cast运算符

class Human {
public:
    Human() {
        cout <<"Human()\n";
    }
    virtual ~Human() { 
        cout <<"~Human()\n";
    }
    virtual void eat() {
        cout <<"Human::eat\n";
    }
};

class Men: public Human {
public:
    Men() {
        cout <<"Men()\n";
    }
    ~Men() {
        cout <<"~Men()\n";
    }
    virtual void eat() override final {
        cout <<"Men::eat\n";
    }
    void test() {
        cout <<"Men::test()\n";
    }
};

Human *phuman = new Men;
Men *p = (Men *)(phuman); //c强制类型转换
p->test();

//使用dynamic_cast的前提条件是父类Human至少有一个虚函数,否则编译报错。
Men *pmen = dynamic_cast<Men *>(phuman);
if(pmen != nullptr) {
	pmen ->test();
} else {
	cout <<"error\n";
}
Men men;
Human &human = men;
try {
	Men &f = dynamic_cast<Men&>(human);
	f.test();
} catch (bad_cast) { //引用转换异常,抛出std::bad_cast
	cout <<"error\n";
}

14.10.3 typeid运算符

返回常量对象(标准库类型type_info,类类型)的引用。
两种形式:

  • typeid(类型)
  • typeid(表达式)或者typeid(变量)
Human phuman = new Men;
Human &q = *phuman;
cout <<typeid(*phuman).name() <<endl; //class Men
cout <<typeid(q).name() <<endl;//class Men
char a[10] = {5, 1};
int b = 120;
cout <<typeid(a).name() <<endl; //char [10]
cout <<typeid(b).name() <<endl; //int
cout <<typeid(19.6).name() <<endl; //double
cout <<typeid("asd").name() <<endl; //char const [4]
  • 指针定义时类型(静态类型)相同,无论指向父类或子类,指针类型相同。
Human *phuman = new Men;
Human *phuman2 = new Women;
if(typeid(human) == typeid(human2))
	cout<<"phuman和phuman2指针的定义类型相同" <<endl;
  • 指针运行时指向类型相同,无论定义时类型,指针取值后类型相同。
Human *phuman = new Men;
Men *phuman2 = new Men;
Human *phuman3 = phuman2 ;
if(typeid(*human) == typeid(*human2))
	cout<<"*phuman和*phuman2指针的运行类型相同,都指向Men" <<endl;
if(typeid(*human3) == typeid(*human2))
	cout<<"*phuman3和*phuman2指针的运行类型相同,都指向Men" <<endl;

父类含有虚函数时,编译器才会对typeid中的表达式求值(返回真正指向的子类信息);否则,直接返回定义时的静态类型(无需求值,定义时的父类信息)。

Human *phuman = new Men;
if(typeid(*phuman) == typeid(Human))
	cout <<"父类(Human)无虚函数时成立" <<endl;

14.10.4 type-info类

  • 成员函数name,获取类型名字信息
Human *phuman = new Men;
const std::type_info &tp = typeid(*phuman);

cout <<tp.name() <<endl;//父类有虚函数,class Men;父类无虚函数,class Human。
  • ==和!=,判断两个type_info对象是否表示同一种类型
Human *phuman = new Men;
const std::type_info &tp = typeid(*phuman);

Human *phuman2 = new Men;
const std::type_info &tp2 = typeid(*phuman2);

Human *phuman3 = new Women;
const std::type_info &tp3 = typeid(*phuman3);

if(tp == tp2) 
	cout <<"类型相同" <<endl; //成立,都是Men
	
if(tp != tp3) 
	cout <<"类型不相同" <<endl; //Men和Women不同;若基类无虚函数,都是Human。

14.10.5 RTTI与虚函数表

类存在虚函数,编译器针对该类会产生一个虚函数表,表中每一项都是指向类中各个虚函数入口地址的指针。虚函数第一项或其之前内存位置的指针,指向该类关联的type_info对象信息(类对象信息来源处)。

14.11 基类与派生类关系的详细探讨

14.11.1 派生类对象模型简介

派生类组成部分=继承基类成员+自己定义成员。

14.11.2 派生类构造函数

派生类使用基类构造函数初始化基类部分,通过派生类构造函数的初始化列表实现。

class A {
public:
	A(int a):va(a){};
	virtual ~A(){cout<<"~A()\n";}
private:
	int va;
};

class B: public A {
public:
    B(int a, int b):A(a), vb(b) {cout<<"B()\n";}
    //virtual ~B(){cout<<"~B()\n";}
private:
	int vb;
};

14.11.3 既当父类又当子类

class gra{};
class fa:public gra{};
class son:public fa{};

14.11.4 不想当基类的类

class AA final{};//不能当基类
class BB : public AA{};//错误

class AA {};
class BB final : public AA{};
class CC : public BB{};//错误,BB不能当基类

14.11.5 静态类型与动态类型

  • 静态类型就是变量声明时的类型,编译时已知。
  • 动态类型就是指针或引用所代表(表达)的内存中的对象类型,运行(执行到代码处)时才知。
  • 只有(含virtual)基类指针或引用才存在静态类型和动态类型不一致的情况。

14.11.6 派生类向基类的隐式类型转换

Human *phuman = new Men();
Human &q = *phuman;

基类指针指向派生类对象,或者基类引用绑定到派生类对象,因为编译器隐式执行了派生类到基类的转换。

14.11.7 父类、子类之间的复制与赋值

Human(const Human& h);
Human& operator=(const Human& h);

Men men;
Human human(men);
human = men;

用派生类对象为一个基类对象初始化或者赋值时,只有派生类对象的基类部分会被复制或者赋值,派生类部分被忽略掉。

14.12 左值、右值、左值引用、右值引用与move

14.12.1 左值和右值

int i =10;

i是对象,一块存储区域。
左值,能用在赋值语句等号左侧的内容(它得代表一个地址);
右值,不能出现在赋值语句中等号的左侧(不能作为左值的值)。
C++中表达式,要么是左值,要么是右值。

i = i + 1;

对象i,等号右侧使用对象的值(右值属性),等号左侧使用对象在内存中的地址(左值属性)。左值可能同时具有左值属性和右值属性。

用到左值的运算符:

  1. 赋值运算符=
    赋值运算符=左侧对象是左值,整个赋值语句的结果也是左值,只是输出时使用右值属性。
int a;
printf("%d\n", a=4);
(a=4) = 8;
  1. 取地址运算符&
int a = 5;
&a; //&作用于左值对象,返回地址(指针)是右值。
//&123,错误
  1. string、vector的下标运算符[],迭代器的递增、递减运算符都要用到左值
string abc = "you";
abc[0];
//123[0],错误

vector<int>::iterator iter;
iter++;
iter--;
//9--,错误
  1. 其他运算符。
    运算符在字面值上不能操作,就要用到左值。i++可以,3++不可以,++用到左值。

左值表达式就是左值,右值表达式就是右值。
左值代表地址,左值表达式求值结果得是对象,得有地址。

14.12.2 引用分类

int value = 10;

int& refval = value; //左值引用
refval = 13;

const int& refval2 = value;//常量引用
//refval2 = 18;//错误

int&& refrightvalue = 3; //绑定到常量
refrightvalue = 5;	//可以修改值

三种形式引用:

  1. 左值引用(绑定到左值),引用那些希望改变值的对象。
  2. const引用(常量引用),也是左值引用,引用那些不希望改变值的对象,如常量等。
  3. 右值引用(绑定到右值),所引用对象的值在使用之后就无须保留了,如临时变量。

14.12.3 左值引用

左值引用就是引用左值的,绑定到左值的引用。
无空引用,引用一定要对应或绑定到一个对象,必须要初始化引用。

int a = 1;
int& b{a};
//int& c;//错误,引用必须初始化(绑定一个对象)
//int& c = 1;//错误,c要绑定到左值,不能绑定到右值1上
const int& c = 1;//正确,const引用可以绑定到右值上
//等价于
int tempvalue = 1; //tempvalue看成临时变量
const int& c = tempvalue;

14.12.4 右值引用

右值引用就是引用右值的,绑定到右值的引用,主要用来绑定到那些即将销毁/临时的对象。

int&& refrightvalue = 3;

int value = 10;
int& refval = value;
//int& refval2 = 5; //错误

string strtest{"you"};
string& r1{strtest};
//string& r2{"you"}; //错误,左值引用不能绑定到临时变量
const string& r3{"you"};//正确,const引用可以绑定到左值,也可以绑定到右值(临时变量)。
//string&& r4{strtest};//错误,右值引用不能绑定到左值
string&& r5{"you"};//正确,右值引用可以绑定到临时变量

int i = 10;
int& ri = i;
//int&& ri2 = i;//错误,右值引用不能绑定到左值
//int& ri3 = i * 100;//错误,左值引用不能绑定到右值
const int& ri4 = i *100;//const引用可以绑定到右值
int&& ri5 = i * 1000;//乘法结果是右值
int i = 5;
(++i) = 20; //正确,最后返回i本身是左值
//(i++) = 20;//错误,产生临时变量存放i值,返回临时变量后又释放,不能再被赋值,是右值
int i = 1;
int&& r1 = i++;
int& r2 = i++;//错误,左值引用不能绑定到右值表达式
int& r3 = ++i;
int&& r4 = ++i;//错误,右值引用不能绑定到左值表达式

重点强调:

  1. &&r1绑定到右值,但r1本身是左值(可看作变量),位于左边且能被左值引用绑定。
  2. 所有变量是左值,有地址。
  3. 函数形参都是左值,void f(int &&w),w类型是右值引用(需绑定到右值),但w本身是左值。
  4. 右值引用通过把复制对象变成移动对象,提高程序运行效率。

移动对象,老对象中很多分配出去的内存并没有被回收而是转移到了新对象,避免了老对象的delete和新对象的new,直接将老对象的内存空间给新对象使用同时切断老对象与内存空间的联系。
移动对象,通过&&移动构造函数和移动赋值运算符实现。

14.12.5 std::move函数

move函数就是把一个左值强制转换成一个右值。

void fff(int&& v){}

int&& i = 10; //i可看作新的变量,类型是右值引用,本身是左值
//int&& ri = i; //错误,右值引用不能绑定到左值i
int&& ri = std::move(i); //正确,右值引用可以绑定到右值std::move(i)
fff(std::move(i));

//ri引用是i的别名,两者同一个变量
ri = 15;
i = 25;
string st = "you";
const char *p = st.c_str();
string def = std::move(st);//string里的移动构造函数将st的内容(st为空"")转移到了def(“you”)中去
const char *q = def.c_str();//p,q值不相等

//std::move(st),触发移动构造函数,实际是重新开辟了一块内存(生成临时对象),将"you"复制进去,并清空st。
string&& def = std::move(st);//不会触发移动构造函数,st值不会清空,只是将st转为右值并绑定到def
//st和def指向同一个对象
st = "agc";
def = "def";

std::move函数意味着承诺:除了对move中的参数赋值或者销毁外,将不再使用它。

  • string def = std::move(st)触发了string类移动构造函数,后续代码不应使用st对象,因为st已经被清空了。
  • string&& def = std::move(st)并没有触发string类移动构造函数,只是单纯的绑定动作,def和st指向同一个对象。

std::move可能源码

template <typename T>
decltype(auto) move(T&& param) {//decltype类型推导,T&&万能引用
	using ReturnType = remove_reference_t<T> &&;//右值引用类型
	return static_cast<ReturnType>(param);
}

14.12.6 左值、右值总结说明

左值是持久值,右值是短暂值,右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。该临时对象引用的对象将要被销毁,该对象没有其他用户,所以右值引用能自由地接管所引用的对象资源。

14.13 临时对象深入探讨、解析与提高性能手段

14.13.1 临时对象概念

i++会产生临时对象(系统自己产生),然后立即释放。右值引用绑定该临时对象后,就不会立即释放该临时对象了。

因为代码书写问题而产生的临时对象,会额外消耗系统资源。临时对象的产生和销毁有成本,会影响程序的执行性能和效率,可以通过优化代码有效减少临时对象的产生。

14.13.2 产生临时对象的情况和解决方案

  1. 函数传值方式产生临时对象
#include <iostream>
using namespace std;

class TmpValue {
public:
    TmpValue(int v):value(v){
        cout <<"TmpValue(int)" <<endl;
        cout <<"value=" <<value <<endl;
    }
    TmpValue(const TmpValue& t):value(t.value){
        cout <<"TmpValue(const TmpValue&)" <<endl;
    }
    virtual ~TmpValue(){
        cout <<"~TmpValue()" <<endl;
    }
    //int Add(TmpValue t){
    int Add(const TmpValue& t){//减少拷贝构造函数和析构函数的调用
        value += t.value;
        return value;
    }
private:
    int value;
};

int main() {
    TmpValue tm(10);
    int value = tm.Add(tm);
    cout <<"value=" <<value <<endl;
    return 0;
}
TmpValue(int)
value=10

//tm.Add(tm)会导致拷贝构造函数和析构函数的调用
TmpValue(const TmpValue&)
~TmpValue()

value=20
~TmpValue()
  1. [隐式]类型转换生成临时对象
#include <iostream>
using namespace std;

class TmpValue {
public:
    TmpValue(int v=0):value(v){
        cout <<"TmpValue(int)" <<endl;
        cout <<"value=" <<value <<endl;
    }
    TmpValue(const TmpValue& t):value(t.value){
        cout <<"TmpValue(const TmpValue&)" <<endl;
    }
    virtual ~TmpValue(){
        cout <<"~TmpValue()" <<endl;
    }
    TmpValue& operator=(const TmpValue& t){
        cout <<"TmpValue& operator=(const TmpValue&)" <<endl;
        value = t.value;
		return *this;
	}
    int Add(const TmpValue& t){//减少拷贝构造函数和析构函数的调用
        value += t.value;
        return value;
    }
private:
    int value;
};

int main() {
    TmpValue tm;
    tm = 100;

	//=非赋值运算符,而是定义时初始化。
    //TmpValue tm = 100;
    return 0;
}
TmpValue(int)
value=0

//tm=100会导致构造函数,拷贝赋值运算符和析构函数的调用
TmpValue(int)
value=100
TmpValue& operator=(const TmpValue&)
~TmpValue()

~TmpValue()
void calc(const string& str) {}
char mystr[100] = "you";
calc(mystr);
//编译器利用mystr参数生成了string临时对象
//编译器只会为const引用产生临时对象,calc不加const,calc(mysty)调用会报错
  1. 函数返回对象时产生临时对象
#include <iostream>
using namespace std;

class TmpValue {
public:
    TmpValue(int v=0):value(v){
        cout <<"TmpValue(int)" <<endl;
        cout <<"value=" <<value <<endl;
    }
    TmpValue(const TmpValue& t):value(t.value){
        cout <<"TmpValue(const TmpValue&)" <<endl;
    }
    virtual ~TmpValue(){
        cout <<"~TmpValue()" <<endl;
    }
    TmpValue& operator=(const TmpValue& t){
        cout <<"TmpValue& operator=(const TmpValue&)" <<endl;
        value = t.value;
        return *this;
    }
    int Add(const TmpValue& t){//减少拷贝构造函数和析构函数的调用
        value += t.value;
        return value;
    }
    int value;
};

//一次构造函数,一次析构函数
void Double1(const TmpValue& t) {
    TmpValue tm;
    tm.value = t.value;
}

//无论返回值是否使用
//一次构造函数,一次析构函数
//系统有优化
TmpValue Double2(const TmpValue& t) {
    TmpValue tm;
    tm.value = t.value;
    return tm;
}

//建议
TmpValue Double3(const TmpValue& t) {
    return TmpValue(t.value); //TmpValue(t)
}

int main() {
    TmpValue tm=100;
    Double1(tm);
    Double2(tm);

	//等价于TmpValue&& tm3 = Double2(tm);
	//临时对象就是一种右值
    TmpValue tm3 = Double2(tm);
    return 0;
}
  1. 类外运算符重载的优化
class mynum{
public:
	mynum(int x=0, int y=0):num1(x),num2(y) {}
	int num1;
	int num2;
};

mynum operator+(const mynum& tmp1, const mynum& tmp2) {
	mynum result;
	result.num1 = tmp1.num1 + tmp2.num1;
	result.num2 = tmp1.num2 + tmp2.num2;
	return resutl;
}

//优化
mynum operator+(const mynum& tmp1, const mynum& tmp2) {
	return mynum(tmp1.num1 + tmp2.num1, tmp1.num2 + tmp2.num2);
}

14.14 对象移动、移动构造函数与移动赋值运算符

14.14.1 对象移动概念

将不想用了的对象A中一些有用对象提取出来,构造新对象B时直接使用。

14.14.2 移动构造函数与移动赋值运算符概念

复制对象A到对象B,对象A里数据还能用。移动对象A(部分数据)到对象B,对象A残缺,不能被使用。移动,指对象中内存所有者的转移。
移动构造函数,第一个参数是右值引用参数(&&),其他额外参数都要有默认值。
移动构造函数与移动赋值运算符要实现资源的移动。

14.14.3 移动构造函数演示

class B {
public:
	B():v(100) {
		cout <<"B()" <<endl;
	}
	B(const B& b) {
		v = b.v;
		cout <<"B(const B&)" <<endl;
	}
	virtual ~B() {
		cout <<"~B()" <<endl;
	}
	int v;
};

B *pb = new B();
pb->v = 19;
B* pb2 = new B(*pb);
delete pb2;
delete pb;

class A {
public:
	A():m_pb(new B()) {
		cout <<"A()" <<endl;
	}
	A(const A& a) {
		//delete m_pb;
		//m_pb = new B(*(a.m_pb));
		m_pb->v = (a.m_pb)->v;
		cout <<"A(const A&)" <<endl;
	}
	 ~A() {
	 	delete m_pb;
		cout <<"~A()" <<endl;
	}
	A(A&& a) noexceptm_pb(a.m_pb) { 
	//移动构造函数声明和定义都习惯性加noexcept,通知编译器移动构造函数不抛出任何异常
	//提高编译器工作效率,否则编译器会为可能抛出异常的函数做一些额外的处理准备工作
		a.m_pb = nullptr;
		cout <<"A(A&&)" <<endl;
	}
private:
	B *m_pb;
};

static A getA() {
	//return A();
	A a;
	return a;//调用A的移动构造函数,将对象a的数据移动给要返回的临时对象
}

A a = getA(); //会调用移动构造函数
A a1(a); //拷贝构造函数
A &&a3(std::move(a));//不产生新对象,也不调用移动构造函数,a2和a代表同一个对象
A a2(std::move(a));//调用移动构造函数,之后不使用a

A&& ady = getA(); //getA返回的临时对象绑定到ady上

14.14.4 移动赋值运算符演示

#include <iostream>
using namespace std;

class B {
public:
    B():v(100) {
        cout <<"B()" <<endl;
    }
    B(const B& b) {
        v = b.v;
        cout <<"B(const B&)" <<endl;
    }
    virtual ~B() {
        cout <<"~B()" <<endl;
    }
    int v;
};

class A {
public:
    A():m_pb(new B()) {
        cout <<"A()" <<endl;
    }
    A(const A& a) {
        m_pb->v = (a.m_pb)->v;
        cout <<"A(const A&)" <<endl;
    }
     ~A() {
        delete m_pb;
        cout <<"~A()" <<endl;
    }
    A(A&& a) noexcept:m_pb(a.m_pb) {
        a.m_pb = nullptr;
        cout <<"A(A&&)" <<endl;
    }
    A& operator=(const A& a) {//拷贝赋值运算符
        if(this != &a) {
            //delete m_pb;
            //m_pb = new B(*(a.m_pb));
            m_pb->v = (a.m_pb)->v;
        }
        cout <<"A& operator=(const A&)" <<endl;
        return *this;
    }
    A& operator=(A&& a) noexcept {//移动赋值运算符,末尾加noexcept
        if(this != &a) {
            //delete m_pb;
            //m_pb = a.m_pb;
            //a.m_pb = nullptr;
            m_pb->v = (a.m_pb)->v;
        }
        cout <<"A& operator=(A&&)" <<endl;
        return *this;
    }
private:
    B *m_pb;
};

static A getA() {
    return A();
}

int main() {
   A a;
   A a2;
   a2 = a; //拷贝赋值运算符
   a2 = std::move(a);//移动赋值运算符
   return 0;
}

14.14.5 合成的移动操作

  • 类定义了拷贝构造函数、拷贝赋值运算符或析构函数之一,编译器就不会合成移动构造函数和移动赋值运算符。
  • 未提供移动构造函数和移动赋值运算符的类,会调用拷贝构造函数和拷贝赋值运算符代替。
  • 类未定义任何版本的拷贝构造函数、拷贝赋值运算符、析构函数,且类的每个非静态成员都可以移动,编译器才会为该类合成移动构造函数和移动赋值运算符。可移动成员:内置类型(int,float等),含有移动操作相关函数的类。
struct TC {
	int i;
	std::string s;
};

TC a;
a.i = 100;
a.s = "you";

const char *p = a.s.c_str();
cout <<&p <<endl;

TC b = std::move(a); //合成移动构造函数,调用string的移动构造函数
const char *q = a.s.c_str();
cout <<&q <<endl;

cout <<a.s <<endl;
cout <<b.s <<endl;

14.14.6 总结

  • 使用new分配大量内存类时,需要移动构造函数和移动赋值运算符。
  • 不抛出异常的移动构造函数和移动赋值运算符应该加上noexcept。
  • 对象移动完数据后,内部指针赋nullptr值。
  • 未提高移动构造函数和移动赋值运算符时,系统会调用拷贝构造函数和拷贝赋值运算符代替。

14.15 继承的构造函数、多重继承、类型转换与虚继承

14.15.1 继承的构造函数

类只继承其直接基类的构造函数。

class A {
public:
	A(int i, int j, int k){}
};
class B : public A {
public:
	B(int i, int j, int k):A(i, j, k){} //貌似子类继承了父类的构造函数
};

B b(3, 4, 5);
class B::public A {
public:
	using A::A; 
	//继承A的所有构造函数
	//A类的每一个构造函数,编译器都在B类中生成形参列表相同的构造函数(函数体为空)
	//B(相同的构造函数形参列表):A(A类构造函数形参列表){}
	//B(int i, int j, int k):A(i, j, k){} 
	//若A类构造函数有默认参数,B类构造多个构造函数
	//A(int i, int j, int k=5) {}
	//B(int i, int j, int k):A(i, j, k){} 
	//B(int i, int j):A(i, j){} 
	//B ad(3, 4);
};

子类定义的构造函数会覆盖掉继承(using A::A)的构造函数。

14.15.2 多重继承

  1. 多重继承的概念
    从多个父类产生出子类,叫多重继承。
class Grand {
public:
	Grand(int i):m(i){}
	virtual ~Grand(){}
	void myinfo() {cout <<m <<endl;}
	int m;
};
class A : public Grand {
public:
	A(int i):Grand(i), m_a(i){} //子类构造负责自己父类的初始化问题。
	virtual ~A(){}
	void myinfo() {
		cout <<m_a <<endl;
	}
	int m_a;
};

class B  {
public:
	B(int i): m_b(i){}
	virtual ~B(){}
	void myinfo() {
		cout <<m_b <<endl;
	}
	int m_b;
};


class C:public A, public B {
public:
	C(int i, int j, int k):A(i), B(j), m_c(k) {};
	virtual ~C() {}
	void myinfo() {
		cout <<m_c <<endl;
	}
	void myinfoC() {
		cout <<m_c <<endl;
		A::myinfo();
		B::myinfo();
		myinfo();
	}
	int m_c;
};

C ctest(10, 20, 50);
ctest.myinfoC();
ctest.A::myinfo();
ctest.myinfo();
  1. 静态成员变量
    静态成员变量跟着类走。
public:
	static int m_static;

int Grand::m_static = 5;//未使用时,可以不定义。

Grand::m_static = 1;
A::m_static = 2;
//B::m_static = 3; //错误
C::m_static = 4;
ctest.m_static = 5;
  1. 派生类构造函数与析构函数
  • 构造派生类对象时将同时构造并初始化它的所有基类子对象。
  • 派生类的构造函数初始化列表中能初始化它的直接基类。
  • 派生类构造函数的初始化列表将实参分别传递给每个直接基类,基类构造顺序与定义时相同,与基类的初始化顺序无关。
  • 每个类的析构函数都只负责销毁类本身分配的资源。派生类中new了内存,派生类析构函数中delete这块内存。
  • 构造函数函数体执行顺序和析构函数和函数体执行顺序相反。构造函数,爷爷、父亲、孩子,析构函数,孩子、父亲、爷爷。
//显示初始化基类
C(int i, int j, int k):A(i),B(j),m_c(k){}


//假设B存在无参数的构造函数,public: B() {};
//隐式初始化基类
C(int i, int j, int k):A(i),m_c(k){}


  1. 多个父类继承构造函数
class A {
public:
	A(int tv) {}
};


class B {
public:
	B(int tv) {}
};


class C : public A, public B{
public:
	//using A::A;//等价于C(int tv):A(tv){}
	//using B::B;//等价于C(int tv):B(tv){}
	C(int tv) : A(tv), B(tv) {}
};

14.15.3 类型转换

基类的引用或指针可以绑定到派生类对象中的基类部分,多重继承中一样成立。

Grand *pg = new C(1, 2, 3);
A *pa = new C(1, 2, 3);
B *pb = new C(1, 2, 3);
C c(6, 7, 8);
Grand mygrand(c);

14.15.4 虚基类与虚继承(虚派生)

//class C:public A, public B, public A {}; //错误,A两次
  • 派生列表中,同一个基类只能出现一次。
  • 派生类可以通过两个直接基类分别继承同一个间接基类。
  • 派生类可以直接继承某个基类,然后通过另一个基类间接继承该类。
#include <iostream>
using namespace std;


class Grand {
public:
    Grand(int i):m(i){cout <<"Grand(int)" <<endl;}
    virtual ~Grand(){cout <<"~Grand(int)" <<endl;}
    void myinfo() {cout <<m <<endl;}
    int m;
};
class A :public Grand {
public:
    A(int i):Grand(i), m_a(i){cout <<"A(int)" <<endl;} //子类构造负责自己父类的初始化问题。
    virtual ~A(){cout <<"~A()" <<endl;}
    void myinfo() {
        cout <<m_a <<endl;
    }
    int m_a;
};
class A2 : public Grand {
public:
    A2(int i):Grand(i), m_a2(i){cout <<"A2(int)" <<endl;} //子类构造负责自己父类的初始化问题。
    virtual ~A2(){cout <<"~A2()" <<endl;}
    void myinfo() {
        cout <<m_a2 <<endl;
    }
    int m_a2;
};

class B  {
public:
    B(int i): m_b(i){cout <<"B(int)" <<endl;}
    virtual ~B(){cout <<"~B()" <<endl;}
    void myinfo() {
        cout <<m_b <<endl;
    }
    int m_b;
};

class C:public B, public A, public A2 {
public:
    C(int i, int j, int k): B(j), A(i), A2(i),m_c(k) {};
    virtual ~C() {}
    void myinfo() {
        cout <<m_c <<endl;
    }
    void myinfoC() {
        cout <<m_c <<endl;
        A::myinfo();
        B::myinfo();
        myinfo();
    }
    int m_c;
};

int main() {
    C ctest(10, 20, 50);
    //ctest.m = 10; //错误
    return 0;
}

虚基类(virtual base class),无论这个类在继承体系中出现多少次,派生类中都只会包含唯一一个共享的该类子内容。

#include <iostream>
using namespace std;


class Grand {
public:
    Grand(int i):m(i){cout <<"Grand(int)" <<endl;}
    virtual ~Grand(){cout <<"~Grand(int)" <<endl;}
    void myinfo() {cout <<m <<endl;}
    int m;
};
class A : virtual public Grand {//virtual和public顺序可以互换
public:
    A(int i):Grand(i), m_a(i){cout <<"A(int)" <<endl;} //子类构造负责自己父类的初始化问题。
    virtual ~A(){cout <<"~A()" <<endl;}
    void myinfo() {
        cout <<m_a <<endl;
    }
    int m_a;
};
class A2 : public virtual Grand {
public:
    A2(int i):Grand(i), m_a2(i){cout <<"A2(int)" <<endl;} //子类构造负责自己父类的初始化问题。
    virtual ~A2(){cout <<"~A2()" <<endl;}
    void myinfo() {
        cout <<m_a2 <<endl;
    }
    int m_a2;
};

class B  {
public:
    B(int i): m_b(i){cout <<"B(int)" <<endl;}
    virtual ~B(){cout <<"~B()" <<endl;}
    void myinfo() {
        cout <<m_b <<endl;
    }
    int m_b;
};

//虚基类Grand子部分最先初始化,然后B,A,A2
class C:public B, public A, public A2 {
public:
    //虚基类Grand由最底层派生类初始化。
    //Grand(i)只会执行一次,A,A2中不再执行
    C(int i, int j, int k): Grand(i), B(j), A(i), A2(i), m_c(k) {};
    virtual ~C() {}
    void myinfo() {
        cout <<m_c <<endl;
    }
    void myinfoC() {
        cout <<m_c <<endl;
        A::myinfo();
        B::myinfo();
        myinfo();
    }
    int m_c;
};

int main() {
    C ctest(10, 20, 50);
    ctest.m = 10;
    return 0;
}

14.16 类型转换构造函数、运算符与类成员指针

14.16.1 类型转换构造函数

构造函数主要特点:

  • 类名作为函数名
  • 无返回值。

类型转换构造函数主要用于将其他类型数据(对象)转换为该类类型的对象,可看做带了形参的普通的构造函数。
类型转换构造函数特点:

  • 只有一个非本类const引用的形参(待转换的数据类型)
  • 类型转换构造函数中,指定转换办法
  • explicit可禁止隐式类型转换
class TestInt {
public:
	explicit TestInt(int x = 0):v(x){}
private:
	int v;
};

//TestInt it = 12; // 隐式类型转换
TestInt it2(22);

14.16.2 类型转换运算符(类型转换函数)

特殊成员函数,将该类类型对象转换成其他类型数据。
operator 类型名() const { return 其他类型}

  • const可选
  • 类型名,一般不包括数组类型或函数类型,但可以转换成数组指针、函数指针、引用等。
  • 无形参,隐式执行。不能指定返回类型,但会返回对应类型的实例。
  • 必须定义为类的成员函数。

在类类型和要转换的目标类型间存在明显关系时才使用,一般通过定义类成员函数比使用类型转换函数更好。

#include <iostream>
using namespace std;

class TestInt {
public:
    TestInt(int x = 0):v(x){}
    TestInt& operator=(const TestInt& a) {//拷贝赋值运算符
        if(this != &a) {
            v = a.v;
        }
        return *this;
    }
    operator int() const {
        return v;
    }
private:
    int v;
};

int main() {
    TestInt ti2;
    ti2 = 6;//隐式类型转换将6转成临时TestInt对象,然后赋值运算符给ti2赋值
    int k = ti2 + 5; //调用ti2的operator int()函数
    int k2 = ti2.operator int() + 5;//调用ti2的operator int()函数
   return 0;
}
  1. 显示类型转换运算符
#include <iostream>
using namespace std;

class TestInt {
public:
    TestInt(int x = 0):v(x){}
    TestInt& operator=(const TestInt& a) {//拷贝赋值运算符
        if(this != &a) {
            v = a.v;
        }
        return *this;
    }
    explicit operator int() const {
        return v;
    }
private:
    int v;
};

int main() {
    TestInt ti2;
    ti2 = 6;//隐式类型转换将6转成临时TestInt对象,然后赋值运算符给ti2赋值
    int k = static_cast<int>(ti2) + 5; //调用ti2的operator int()函数
    int k2 = ti2.operator int() + 5;//调用ti2的operator int()函数
   return 0;
}
  1. 类对象转换为指针
#include <iostream>
using namespace std;

//函数指针类型定义
//typedef int(*typoint)(int);
using typoint = int(*)(int);

static int mysfunc(int v) {
    return v;
}

class TestInt {
public:
    TestInt(int x = 0):v(x){}
    explicit operator typoint() const { //const非必要
        return mysfunc;
    }
private:
    int v;
};

int main() {
    TestInt ti2(12);
    //int k0 = ti2(123);//未添加explicit时隐式类型转换
    int k1 = static_cast<typoint>(ti2)(123);
    int k2 = ti2.operator typoint()(123);
   return 0;
}

14.16.3 类型转换的二义性问题

类型转换间多种情况都行。

operator int(){};
operator double(){};

TestInt aa;
//int abc = aa + 12;//二义性(不明确)问题
int abc = static_cast<int>(aa) + 12;
class CT1 {
public:
	CT1(int c){};
};
class CT2 {
public:
	CT2(int c){};
};

void testfunc(const CT1& ct){};
void testfunc(const CT2& ct){};

//testfunc(12);//二义性(不明确)问题
testfunc(CT1(12));

14.16.4 类成员函数指针

类成员函数指针,指向类成员函数的指针,与类对象无关。

  1. 普通成员函数
    类名::*函数指针变量名定义(声明)普通成员函数指针;
    &类名::成员函数名获取类成员函数地址。
class CT {
public:
	void ptfunc(int v) {
		cout <<"void ptfunc(int), value=" <<v <<endl;
	}
	virtual void virtualfunc(int v) {
		cout <<"virtual void virtualfunc(int), value=" <<v <<endl;
	}
	static void staticfunc(int v) {
		cout <<"static void staticfunc(int), value=" <<v <<endl;
	}
};

//定义类成员函数指针变量,变量名字为myfpointpt 
void (CT::*myfpointpt)(int);

//类成员函数指针变量myfpointpt被赋值
myfpointpt = &CT::ptfunc;

类成员函数地址与类对象(实例)无关,但使用它时必须绑定到一个类对象。

CT ct;
CT *pct = &ct;
(ct.*myfpointpt)(100);
(pct->*myfpointpt)(100);
  1. 虚成员函数
void (CT::*myfpointvirtualfunc)(int) = &CT::virtualfunc;
(ct.*myfpointvirtualfunc)(100);
(pct->*myfpointvirtualfunc)(100);
  1. 静态成员函数

类名::*函数指针变量名定义(声明)静态成员函数指针;
&类名::成员函数名获取类成员函数地址。

void (CT::*myfpointstaticfunc)(int) = &CT::staticfunc;
myfpointstaticfunc(100);

静态成员函数跟着类走,与具体的类对象无关,可看作全局函数。

14.16.5 类成员变量指针

  1. 普通成员变量
public:
	int m_a;

int CT::*mp = &CT::m_a; //定义类成员变量指针
//指针存放数字0x00000004(或8),该指针不是指向内存中的地址,而是该成员变量与该类对象首地址间的偏移量,CT对象包含虚函数表指针,4或8字节

CT ct;
//通过类成员变量指针修改类成员变量值,等价于ct.m_a = 189;
ct.*mp = 189;
cout <<ct.*mp <<endl;
  1. 静态成员变量
    静态成员变量属于类,与类对象无关。静态成员变量指针不是偏移量,而是真正的地址。
public:
	static int m_stca; //声明类静态成员变量

int CT::m_stca = 1;//定义类静态成员变量

//定义静态成员变量指针
int *stcp = &CT::m_stca;

*stcp = 796;
cout <<*stcp <<endl;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《C++新经典》第14章 类 的相关文章

随机推荐