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"
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 称呼上的统一
- class内部实现成员函数,“成员函数的定义”。
class A {
public:
void func() {
}
};
- class内部只声明成员函数,“成员函数的声明”;外部实现,“成员函数的实现”。
class A {
public:
void func();
};
void A::func() {
}
14.2.2 构造函数
构造函数,特殊的成员函数,与类名相同,创建类对象时,系统自动调用,用于初始化类对象的数据成员(成员变量)。
- 构造函数无返回值,不写函数头。
- 不可以手工调用构造函数。
- 正常情况下,构造函数应该被声明为public。
- 构造函数中若有参数,创建对象时要指定参数。
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);
};
TIme myTime23 = 14;
TIme myTime24 = (12, 13, 14, 15, 16);
TIme myTime100 = {14};
void func(Time tmp) {
return;
}
func(16);
构造函数中声明explicit(显示),只能用于初始化和显示类型转换
class Time {
public:
explicit Time();
explicit TIme(int h);
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) {
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;
};
int Time::mystatic = 5;
void Time::staticfunc(int 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) {
Hour = h+2;
}
private:
int Hour{0};
};
14.4.3 const成员变量初始化
只能类内初始化或使用初始化列表初始化,不能构造函数内部赋值操作(构造函数不能声明成const)。
class Time {
public:
Time(int h):Hour(h) {
}
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;”
- =default;
class Time {
public:
Time(int v){};
Time() = defult;
private:
int Hour = 0;
};
Time::Time() = defalut;
- =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 几个话题
- 构造函数的成员初始化
类类型成员变量的初始化,尽量放在构造函数的初始化列表,不要放在构造函数的函数体里面进行,可以避免多次不必要的成员函数调用。
#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){
cout <<"Time()\n";
}
~Time(){
cout <<"~Time()\n";
}
private:
TmpClass tmp;
};
int main() {
Time time;
cout << "Hello World!" << endl;
return 0;
}
- 析构函数的成员销毁
- 对象中的成员变量是在析构函数体执行完后,系统隐含销毁。
- 类中先定义的成员变量先进行初始化,然后类的构造函数;销毁时,先类的析构函数,然后先定义的成员变量后销毁。
- malloc/new分配内存(构造函数),需要自己free/delete释放(析构函数)。
- 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();
Human::samenamefunc(120);
}
}
Men men;
men.samenamefunc();
men.samenamefunc(1);
men.Human::samenamefunc();
men.Human::samenamefunc(120);
class Men: public Human {
public:
using Human::samenamefunc;
void samenamefunc(int) {
cout <<"Men::samenamefunc(int)\n";
}
}
Men men;
men.samenamefunc();
men.samenamefunc(1);
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";
}
}
Human *phuman = new Men;
phuman->funchuman();
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;
phuman->eat();
class Human {
public:
virtual void eat() {
cout <<"Human::eat()\n";
}
};
class Men: public Human {
public:
virtual void eat() override {
cout <<"Men::eat\n";
}
};
Human *phuman = new Human;
phuman->eat();
delete phuman;
phuman = new Men;
phuman->eat();
phuman->Human::eat();
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;
phuman->eat();
delete phuman;
14.8.5 父类的析构函数一般写成虚函数
子类对象定义和销毁过程中,先执行父类构造函数,子类构造函数,然后执行子类析构函数,父类析构函数。
#include <iostream>
using namespace std;
class Human {
public:
Human() {
cout <<"Human()\n";
}
~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";
}
};
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)。
friend void func(const Tmp &tmp);
14.9.2 友元类
让类B成为类A的友元类,类B就能访问类A的所有成员(变量和函数),无论使用什么修饰符(public、protected、private)。
class B;
class A {
friend class 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;
#ifndef B_H
#define B_H
class A;
class B {
public:
void callBAF(int x, A& a);
};
#endif
#ifndef A_H
#define A_H
class A {
friend void B::callBAF(int x, A& a);
private:
int data;
};
#endif
void B::callBAF(int x, A& a) {
a.data = x;
}
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);
p->test();
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) {
cout <<"error\n";
}
14.10.3 typeid运算符
返回常量对象(标准库类型type_info,类类型)的引用。
两种形式:
- typeid(类型)
- typeid(表达式)或者typeid(变量)
Human phuman = new Men;
Human &q = *phuman;
cout <<typeid(*phuman).name() <<endl;
cout <<typeid(q).name() <<endl;
char a[10] = {5, 1};
int b = 120;
cout <<typeid(a).name() <<endl;
cout <<typeid(b).name() <<endl;
cout <<typeid(19.6).name() <<endl;
cout <<typeid("asd").name() <<endl;
- 指针定义时类型(静态类型)相同,无论指向父类或子类,指针类型相同。
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类
Human *phuman = new Men;
const std::type_info &tp = typeid(*phuman);
cout <<tp.name() <<endl;
- ==和!=,判断两个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;
if(tp != tp3)
cout <<"类型不相同" <<endl;
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";}
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{};
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,等号右侧使用对象的值(右值属性),等号左侧使用对象在内存中的地址(左值属性)。左值可能同时具有左值属性和右值属性。
用到左值的运算符:
- 赋值运算符=
赋值运算符=左侧对象是左值,整个赋值语句的结果也是左值,只是输出时使用右值属性。
int a;
printf("%d\n", a=4);
(a=4) = 8;
- 取地址运算符&
int a = 5;
&a;
- string、vector的下标运算符[],迭代器的递增、递减运算符都要用到左值
string abc = "you";
abc[0];
vector<int>::iterator iter;
iter++;
iter--;
- 其他运算符。
运算符在字面值上不能操作,就要用到左值。i++可以,3++不可以,++用到左值。
左值表达式就是左值,右值表达式就是右值。
左值代表地址,左值表达式求值结果得是对象,得有地址。
14.12.2 引用分类
int value = 10;
int& refval = value;
refval = 13;
const int& refval2 = value;
int&& refrightvalue = 3;
refrightvalue = 5;
三种形式引用:
- 左值引用(绑定到左值),引用那些希望改变值的对象。
- const引用(常量引用),也是左值引用,引用那些不希望改变值的对象,如常量等。
- 右值引用(绑定到右值),所引用对象的值在使用之后就无须保留了,如临时变量。
14.12.3 左值引用
左值引用就是引用左值的,绑定到左值的引用。
无空引用,引用一定要对应或绑定到一个对象,必须要初始化引用。
int a = 1;
int& b{a};
const int& c = 1;
int tempvalue = 1;
const int& c = tempvalue;
14.12.4 右值引用
右值引用就是引用右值的,绑定到右值的引用,主要用来绑定到那些即将销毁/临时的对象。
int&& refrightvalue = 3;
int value = 10;
int& refval = value;
string strtest{"you"};
string& r1{strtest};
const string& r3{"you"};
string&& r5{"you"};
int i = 10;
int& ri = i;
const int& ri4 = i *100;
int&& ri5 = i * 1000;
int i = 5;
(++i) = 20;
int i = 1;
int&& r1 = i++;
int& r2 = i++;
int& r3 = ++i;
int&& r4 = ++i;
重点强调:
- &&r1绑定到右值,但r1本身是左值(可看作变量),位于左边且能被左值引用绑定。
- 所有变量是左值,有地址。
- 函数形参都是左值,void f(int &&w),w类型是右值引用(需绑定到右值),但w本身是左值。
- 右值引用通过把复制对象变成移动对象,提高程序运行效率。
移动对象,老对象中很多分配出去的内存并没有被回收而是转移到了新对象,避免了老对象的delete和新对象的new,直接将老对象的内存空间给新对象使用同时切断老对象与内存空间的联系。
移动对象,通过&&移动构造函数和移动赋值运算符实现。
14.12.5 std::move函数
move函数就是把一个左值强制转换成一个右值。
void fff(int&& v){}
int&& i = 10;
int&& ri = std::move(i);
fff(std::move(i));
ri = 15;
i = 25;
string st = "you";
const char *p = st.c_str();
string def = std::move(st);
const char *q = def.c_str();
string&& def = std::move(st);
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) {
using ReturnType = remove_reference_t<T> &&;
return static_cast<ReturnType>(param);
}
14.12.6 左值、右值总结说明
左值是持久值,右值是短暂值,右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。该临时对象引用的对象将要被销毁,该对象没有其他用户,所以右值引用能自由地接管所引用的对象资源。
14.13 临时对象深入探讨、解析与提高性能手段
14.13.1 临时对象概念
i++会产生临时对象(系统自己产生),然后立即释放。右值引用绑定该临时对象后,就不会立即释放该临时对象了。
因为代码书写问题而产生的临时对象,会额外消耗系统资源。临时对象的产生和销毁有成本,会影响程序的执行性能和效率,可以通过优化代码有效减少临时对象的产生。
14.13.2 产生临时对象的情况和解决方案
- 函数传值方式产生临时对象
#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(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
TmpValue(const TmpValue&)
~TmpValue()
value=20
~TmpValue()
- [隐式]类型转换生成临时对象
#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;
return 0;
}
TmpValue(int)
value=0
TmpValue(int)
value=100
TmpValue& operator=(const TmpValue&)
~TmpValue()
~TmpValue()
void calc(const string& str) {}
char mystr[100] = "you";
calc(mystr);
- 函数返回对象时产生临时对象
#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);
}
int main() {
TmpValue tm=100;
Double1(tm);
Double2(tm);
TmpValue tm3 = Double2(tm);
return 0;
}
- 类外运算符重载的优化
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) {
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;
}
private:
B *m_pb;
};
static A getA() {
A a;
return a;
}
A a = getA();
A a1(a);
A &&a3(std::move(a));
A a2(std::move(a));
A&& ady = getA();
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) {
m_pb->v = (a.m_pb)->v;
}
cout <<"A& operator=(const A&)" <<endl;
return *this;
}
A& operator=(A&& a) noexcept {
if(this != &a) {
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);
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;
};
子类定义的构造函数会覆盖掉继承(using A::A)的构造函数。
14.15.2 多重继承
- 多重继承的概念
从多个父类产生出子类,叫多重继承。
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();
- 静态成员变量
静态成员变量跟着类走。
public:
static int m_static;
int Grand::m_static = 5;
Grand::m_static = 1;
A::m_static = 2;
C::m_static = 4;
ctest.m_static = 5;
- 派生类构造函数与析构函数
- 构造派生类对象时将同时构造并初始化它的所有基类子对象。
- 派生类的构造函数初始化列表中能初始化它的直接基类。
- 派生类构造函数的初始化列表将实参分别传递给每个直接基类,基类构造顺序与定义时相同,与基类的初始化顺序无关。
- 每个类的析构函数都只负责销毁类本身分配的资源。派生类中new了内存,派生类析构函数中delete这块内存。
- 构造函数函数体执行顺序和析构函数和函数体执行顺序相反。构造函数,爷爷、父亲、孩子,析构函数,孩子、父亲、爷爷。
C(int i, int j, int k):A(i),B(j),m_c(k){}
C(int i, int j, int k):A(i),m_c(k){}
- 多个父类继承构造函数
class A {
public:
A(int tv) {}
};
class B {
public:
B(int tv) {}
};
class C : public A, public B{
public:
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 虚基类与虚继承(虚派生)
- 派生列表中,同一个基类只能出现一次。
- 派生类可以通过两个直接基类分别继承同一个间接基类。
- 派生类可以直接继承某个基类,然后通过另一个基类间接继承该类。
#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);
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 {
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;
};
class C:public B, public A, public A2 {
public:
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 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;
int k = ti2 + 5;
int k2 = ti2.operator int() + 5;
return 0;
}
- 显示类型转换运算符
#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;
int k = static_cast<int>(ti2) + 5;
int k2 = ti2.operator int() + 5;
return 0;
}
- 类对象转换为指针
#include <iostream>
using namespace std;
using typoint = int(*)(int);
static int mysfunc(int v) {
return v;
}
class TestInt {
public:
TestInt(int x = 0):v(x){}
explicit operator typoint() const {
return mysfunc;
}
private:
int v;
};
int main() {
TestInt ti2(12);
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 = 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(CT1(12));
14.16.4 类成员函数指针
类成员函数指针,指向类成员函数的指针,与类对象无关。
- 普通成员函数
类名::*函数指针变量名定义(声明)普通成员函数指针;
&类名::成员函数名获取类成员函数地址。
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;
}
};
void (CT::*myfpointpt)(int);
myfpointpt = &CT::ptfunc;
类成员函数地址与类对象(实例)无关,但使用它时必须绑定到一个类对象。
CT ct;
CT *pct = &ct;
(ct.*myfpointpt)(100);
(pct->*myfpointpt)(100);
- 虚成员函数
void (CT::*myfpointvirtualfunc)(int) = &CT::virtualfunc;
(ct.*myfpointvirtualfunc)(100);
(pct->*myfpointvirtualfunc)(100);
- 静态成员函数
类名::*函数指针变量名定义(声明)静态成员函数指针;
&类名::成员函数名获取类成员函数地址。
void (CT::*myfpointstaticfunc)(int) = &CT::staticfunc;
myfpointstaticfunc(100);
静态成员函数跟着类走,与具体的类对象无关,可看作全局函数。
14.16.5 类成员变量指针
- 普通成员变量
public:
int m_a;
int CT::*mp = &CT::m_a;
CT ct;
ct.*mp = 189;
cout <<ct.*mp <<endl;
- 静态成员变量
静态成员变量属于类,与类对象无关。静态成员变量指针不是偏移量,而是真正的地址。
public:
static int m_stca;
int CT::m_stca = 1;
int *stcp = &CT::m_stca;
*stcp = 796;
cout <<*stcp <<endl;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)