第1节 成员函数、对象拷贝、私有成员
一、综述
二、类基础:struct、class不混用
三、成员函数:类定义是可以多次include的
四、对象的拷贝:每个成员变量逐个拷贝(可以控制:在Time中重新定义“=”)
五、私有成员:只能被成员函数调用
第2节 构造函数详解、explicit、初始化列表
一、构造函数:创建类对象时系统自动调用的名字和类名相同的成员函数。
特点:① 没有void,无返回值 ② 不可以手动调用 ③ 声明为public ④ 构造函数中如果有参数,创建对象时也要加上
//声明
Time(int, int, int);
//定义
Time::Time(int a, int b, int c){}
//调用
Time mytime = Time(10,12,15);
二、多个构造函数:只要有不同的参数就行
//对象拷贝:调用的是拷贝构造函数
Time mytime2 = mytime;
三、函数默认参数(声明的时候赋初值)
1、默认值只能放在函数声明中;
2、有一个是默认值,后面的也必须是默认值;
3、有默认值,对象初始化时可以少参数,也可以替换默认值。
四、隐式转换和explicit
Time mytime3 = 16; //可以调用一个参数的构造函数,相当于把一个整型转为一个对象
在构造函数声明前加explicit可以阻止隐式类型转换,一般单参数的构造函数都声明为explicit。
五、构造函数的初始化列表(提倡!)
Time::Time(int a,int b):Hour(a),Minute(b){}
这种初始化方式比在 { } 里初始化早,而且省去了赋值的过程,这里的执行顺序取决于成员变量的定义顺序!
第3节 inline、const、mutable、this、static
一、在类定义中实现成员函数inline
直接在类定义中实现的成员函数会被当做inline函数处理。
二、成员函数末尾的const(常量成员函数)
成员函数(普通函数不行)的声明和定义后都需要加const:不能修改成员变量的值。
若const Time abc; 则如果一个成员函数没有声明成const,该对象就不能调用此成员函数。
带const的成员函数可以被带/不带const的对象调用!
三、mutable(不稳定、容易改变的意思)→ 突破const限制
针对二的情况,若想在const成员函数中修改成员变量的值,则需要mutable int a;
四、返回自身对象的引用,this
当调用成员函数时,编译器负责把这个对象的地址(&mytime)传递给this形参
1、this指针只在成员函数中使用,全局/静态函数中都不能使用;
2、在普通成员函数中,this是一个指向非const对象的const指针(Time * const this);
3、在const成员函数中,this是一个指向const对象的const指针(const Time * const this)。
//声明
Time& addhour(int tmphour);
//定义
Time& Time::addhour(int tmphour) {
Hour += tmphour; //将Hour改为this->Hour依然正确,可以防止形参和成员变量同名
return *this;
}
//这种返回类型可以连续调用
//使用
Time mytime;
mytime.addhour(3);
五、static成员
1、全局 int g_abc =0; 别的cpp中只要 extern int g_abc; 即可使用该变量;
2、全局 static int g_abc; 系统自动给初值0,只能本cpp使用;
void func(){
static int abc = 8; //再次调用该函数时这条语句不再执行
abc = 5;
}
3、static成员变量:属于整个类的成员变量,每个对象调用的结果相同,不属于某一个对象!
//声明
static int mystatic;
//定义(分配内存),只能写在一个cpp里且要写在最前面
int Time::mystatic = 15; //可以不给初值
第4节 类内初始化、默认构造函数、=default
一、类相关非成员函数
二、类内初始化
三、const成员变量初始化:在构造函数的初始化列表里进行!
四、默认构造函数:没有参数的构造函数
※ 若类中没有任何构造函数,编译器会隐式自动定义一个默认构造函数
Time() = default; //适用于默认构造函数
Time() = delete; //禁止默认构造函数
第5节 拷贝构造函数
拷贝构造函数是类的构造函数:第一个参数是该类的引用,如果有其他参数,都有默认值(声明中)。
Time(const Time &tmptime, int a = 10); //习惯于加const,前面不加explicit
1、成员变量为整型等,直接拷贝;为类类型,调用此类的拷贝构造函数;
2、自己写的拷贝构造函数覆盖系统自动生成的“合成拷贝构造函数”;
3、其他调用拷贝构造函数的情况:
① 将一个对象作为实参传递给非引用的形参 ② 从一个函数中返回一个对象
Time func(){
Time tmptime;
return tmptime; //产生了临时对象并拷贝
}
第6节 重载运算符、拷贝赋值运算符、析构函数
一、重载运算符:一个函数名为“operator运算符”的成员函数,函数体里写一个比价逻辑。
二、拷贝赋值运算符
//声明
Time& operator=(const Time&);
//定义
Time& Time::operator=(const Time& tmpobj) { //const防止修改右侧值
std::cout << "调用了operator=重载" << std::endl;
return *this;
}
//使用
Time mytime;
Time mytime2
mytime2 = mytime; //不重载是无法使用的
最后一行,等号左侧为this对象,右侧为operator=的参数!
三、析构函数(对象销毁时自动调用)
无返回值,无任何参数,不能被重载,一个类只有一个!
默认析构函数函数体为空,不会释放如构造函数中new的内存。
构造函数初始化:1、函数体之前 2、函数体之中
析构函数:1、函数体(销毁new出的内存) 2、函数体之后(系统销毁成员变量等)
第7节 派生类、调用顺序、访问等级、函数遮蔽
一、派生类概念
父类(基类、超类) VS 子类(派生类)
class 子类名 : 继承方式 父类名 {}
二、派生类对象定义时调用构造函数的顺序:父先子后(析构函数子先父后)
三、public、protected、private
protected访问权限:只允许本类或子类的成员函数来访问。
四、函数遮蔽
子类中如果有一个与父类同名的函数,则子类对象无法访问父类中的任何与此函数同名的函数;
如果想调用父类的函数,在子类的成员函数中用“父类::函数名(...)”强制调用。
第8节 基类指针、纯虚函数、多态性、虚析构函数
一、基类指针、派生类指针
Human *phuman = new Men;
这样写父类指针phuman只能调用父类的成员函数eat()!
基类指针可以指向一个派生类对象,这是因为编译器可以隐式执行这种派生类到基类的转换,转换成功的原因是每个派生类对象都包含一个基类对象部分。所以,基类的引用或指针是可以绑到派生类对象这部分上来的。
二、虚函数
比如父类和子类有同名同参函数 eat( ) 怎么才能调用子类的函数呢?
方法:在父类中,将此函数声明为虚函数,在函数声明前加 virtual!
一旦父类中声明为虚函数,则子类中相应的函数也自动声明为虚函数。
为了避免在子类中写错虚函数,【c++11】中可以在子类函数声明后加 override,这样只要参数或函数名不一致就会报错!
若想要子类无法覆盖父类的虚函数,在父类的函数声明末尾加 final 关键字:
virtual void eat() final;
三、多态性(只是针对虚函数的!)
※ 体现在具有继承关系的父类和子类之间,子类重写父类的成员函数 eat ( ),父类的 eat ( ) 声明为 virtual 类型即可。通过父类指针,到程序运行时,找到动态绑定的对象,系统内部查一个虚函数表,找到函数 eat ( ) 入口地址调用,这就是运行时期的多态性。
四、纯虚函数
纯虚函数为在父类中声明的虚函数,没有定义,但是要求子类中必须定义对该函数自己的实现方法(每个都要)。
virtual void eat2() = 0; //只声明即可
※ 一旦类中有纯虚函数了,就不能生成该类的对象了,这个类也被叫做抽象类(不能实例化)。
五、基类的析构函数一般写成虚函数
如果代码这么写:
Human *phuman = new Men;
delete phuman;
显然只会调用父类的析构函数而不会调用子类的析构函数,造成内存没有释放的严重后果,所以:
如果一个类想要做父类,务必把这个类的析构函数写成 virtual 的!!!
第9节 友元函数、友元类、友元成员函数(打破权限修饰符的限制)
一、友元函数
//外界函数
void func(const Men& tmp){
tmp.funmen();
}
如果 funmen() 函数定义为 private 权限,则 func() 中无权调用 funmen();
但是只要让 func() 成为类 Men 的友元函数,func() 就能访问类 Men 中所有成员变量和成员函数。
只需在类 Men 中任意位置加上:
friend void func(const Men& tmp);
二、友元类:类可以把其他类定义为友元类,其他类就能访问该类的所有成员。
1、友元类不能被继承
2、友元关系是单向的
3、友元关系没有传递性
三、友元成员函数
比如在类A中可以这样声明一个类C中的友元函数:
friend void C::ctest(int,A&);
但要记住:只有public的函数才能成为其他类的友元成员函数!
第10节 RTTI、dynamic_cast、typeid、虚函数表
一、RTTI(Run Time Identification):运行时类型识别
程序能够使用父类的指针或引用来检查这些指针或引用所指对象的实际派生类型。
二、dynamic_cast(基类指针 => 派生类指针,不能转成其他类类型指针)
Human *phuman = new Men;
phuman->menfunc();
如上代码,若menfunc()函数只存在于类Men中,则第二行是无法调用的,因为phuman终归是Human类的一个指针。
Human *phuman = new Men;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
此时加入一行将类Human的指针转成pmen类的指针即可。
三、typeid
返回指针或引用所指对象的实际类型,主要为了比较两个指针是否指向同一种类的对象。
Human *phuman = new Men;
Human *phuman2 = new Women;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
此时phuman和phuman2同类型,pmen和phuman、phuman2都不同类型。
※ 若想二、三起作用,父类中必须有一个虚函数!!!
※ 只有虚函数存在了,这两个运算符才会使用指针或者引用所绑定的对象的动态类型!
四、type_info类
Human *phuman = new Men;
Men *pmen = dynamic_cast<Men*>(phuman);
pmen->menfunc();
cout << typeid(phuman).name() << endl;
cout << typeid(pmen).name() << endl;
const type_info &tp = typeid(*phuman);
cout << tp.name() << endl;
//输出
//class Human *
//class Men *
//class Men
第11节 基类与派生类关系的详细再探讨
一、派生类对象模型简介
二、派生类构造函数
如何传递参数给基类构造函数?在子类的初始化列表里进行!
//父类
A(int i):m_valuea(i){}
//子类
B(int i,int j):A(j),m_valueb(i){} //一般成员变量声明为m_xxx意为member
三、既当父类又当子类
继承关系一直传递,最终son类会包含直接基类的成员以及每个间接基类的成员。
四、不想当基类的类
【c++11】final 加在类名后边,此类就无法做基类了!
五、静态类型与动态类型
1、静态类型:变量声明时的类型(编译时已知)
2、动态类型:指针或引用表达的内存中对象的类型(运行时才知)
所以只有这种情况才谈得到静态类型和动态类型:
Human *phuman = new Men;
Human &q = *phuman;
六、派生类向基类的隐式转换
并不存在从基类到派生类的自动类型转换,若基类中有虚函数,可以用dynamic_cast转换!
七、父类子类之间的拷贝与赋值
用派生类对象定义并初始化基类对象,导致Human的拷贝构造函数执行。
Men men;
Human human(men);
第12节 左值、右值、左值引用、右值引用、move
一、左值和右值
左值:能在赋值语句左侧的东西,不是左值,就是右值!
用到左值的运算符:
a)赋值运算符 (a=4)=8 b)取地址& c)string、vector下标、迭代器 d)i++
二、左值引用
//允许
const int &c = 1;
//等价于
int temp = 1;
const int &c = temp;
临时变量被系统当右值!
三、右值引用
string &&c{"xzh"};
五、总结
返回左值表达式:返回左值引用的函数、赋值、下标、解引用、前置递增(减)运算符
返回右值表达式:返回非引用类型的函数、算术、关系、位、后置递增(减)运算符
i++:先产生一个临时变量temp,temp = i,再i加1,返回的是temp
++i:系统直接给变量i加1,然后返回i本身
int i = 1;
int &r1 = ++i;
int &&r2 = i++;
1、r2是右值引用,但r2是一个左值
2、变量一般都是左值
3、函数的形参都是左值
4、临时对象都是右值
右值引用的目的:c++11引入,&&代表一种新数据类型,提高了系统效率,把拷贝对象变成了移动对象。
六、move函数:把一个左值强制转换成一个右值(没有移动操作)
int i = 1;
int &&r = std::move(i); //r、i穿一条裤子了
第13节 临时对象深入探讨、解析、提高性能手段
代码书写问题产生临时变量:
1、以传值方式给函数传递参数
结果:多调用一次拷贝构造函数和析构函数
方法:把形参改为引用
2、类型转换
A a;
a = 100;
对于第二行,系统首先创建临时对象,以100为第一个参数调用构造函数,再依次调用拷贝赋值运算符和析构函数。
方法:用 A a = 100;这种方式替代
3、函数返回对象的时候
有时候return temp的时候,因为temp是函数内的临时对象,无法返回到外面,故系统会自动生成一个临时对象,额外调用一次拷贝构造函数和析构函数。
方法:尽量用一个对象去接这个函数的返回对象。
第14节 对象移动、移动构造函数、移动赋值运算符
一、移动构造函数
移动并不是地址迁移,只是所有者的转移!移动后,被移动的无法再继续使用了!
class A {
public:
//构造函数
A():m_pb(new B()){
cout << "A的构造函数" << endl;
}
//拷贝构造函数
A (const A& tmp) noexecpt :m_pb(new B(*(tmp.m_pb))){
cout << "A的拷贝构造函数" << endl;
}
//移动构造函数
A(A&& tmp):m_pb(tmp.m_pb){ //临时对象指向对象B
tmp.m_pb = nullptr; //删除原来的指向
cout << "A移动构造函数" << endl;
}
//析构函数
virtual ~A(){
delete m_pb; //记得删除new的内存
cout << "A的析构函数" << endl;
}
public:
B *m_pb;
};
习惯性在移动构造函数的声明、定义后加noexcept,为了使编译器不抛出异常。
A a = geta(); 调用移动构造函数
A a1(a); 调用拷贝构造函数,因为a是左值
A a2(std::move(a)); 调用移动构造函数
A &&a3(std::move(a)); 只是a多了个别名a3
二、移动赋值运算符
要想调用移动赋值运算符,也需要将左值转成右值!
//拷贝赋值运算符
A& operator=(const A& tmp) {
if (this == &tmp)
return *this;
delete m_pb;
m_pb = new B(*(tmp.m_pb));
cout << "A的拷贝赋值运算符" << endl;
return *this;
}
//移动赋值运算符
A& operator=(A&& tmp) noexcept{
delete m_pb;
m_pb = tmp.m_pb;
tmp.m_pb = nullptr;
cout << "A的移动赋值运算符" << endl;
return *this;
}
三、合成的移动操作(某些条件下编译器能自动合成一、二)
1、如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符、析构函数,编译器就不会自动合成一和二
2、如果一个类没有一和二,编译器调用拷贝构造函数和拷贝赋值运算符代替
3、只有一个类没有定义任何自己的拷贝构造成员且每个非静态成员都可以移动(内置类型或有移动操作的类类型),系统才会自动调用一和二
※ 尽量给类增加一和二来减少拷贝构造函数和拷贝赋值运算符的使用!
※ 不要忘记noexcept、nullptr、delete的使用!
第15节 继承的构造函数、多重继承、虚继承
一、继承的构造函数
一个类只继承其直接基类的构造函数,默认、拷贝、移动构造函数时不能被继承的。
在子类B中写 using A::A 可以继承父类的构造函数,若A的构造函数有默认参数,系统会在B中生成多个构造函数,每次省略一个参数(若子类有自己的同参的构造函数,子类的构造函数会覆盖继承下来的构造函数)。
二、多重继承
1、多重继承:一个孩子多个爹
※ 子类构造函数初始化列表:只管自己爹
C继承A和B:若A、B都要myinfo(),c.myinfo()需要加作用域,若C中也有myinfo(),直接覆盖父类中的myinfo()。
2、静态成员变量
静态成员变量是属于类的,不是属于对象的!
//声明
static int m_static;
//定义(分配内存)
int Grand::m_static = 0;
//使用
Grand::m_static = 1; //类A和C都可以访问、或用对象名访问
3、派生类构造函数与析构函数
1)构造一个子类对象将同时构造并初始化所有父类子对象;
2)子类的构造函数初始化列表只作用于直接父类(若没有对父类进行初始化,系统自动调用父类的默认构造函数隐式初始化父类);
3)用 2)构造时,父类的构造顺序与派生列表(class C:public A,public B)顺序一致,与子类初始化列表顺序无关。
4、从多个父类继承构造函数
如果一个类从他的父类们中继承了相同的构造函数,那么这个类必须写同参数构造函数自己的版本。
三、类型转换
Grand *gr = new C(1,2,3);
四、虚基类、虚继承(虚派生)
派生列表中同一个基类只能出现一次,以下两种情况除外:
a)Grand → A,A2 → C b)G → A,G → C,A → C
这两种情况会导致Grand的构造函数执行两次,不仅占用空间,而且会有名字冲突!
解决办法:变与Grand相关的继承为虚继承,使Grand类变为虚基类
class A : virtual public Grand {}
或
class A : public virtual Grand {}
说明:若C没有孩子,由C初始化Grand而不是A和A2;若C有了孩子,则永远由最低层子类初始化虚基类,初始化顺序为先初始化虚基类,再按照派生列表顺序初始化!
第16节 类型转换构造函数、运算符、类成员指针
一、类型转换构造函数
将某个其他的数据类型转换为该类类型的对象。
特点:a)只有一个参数(不是本类的const引用)
b)要指定转换的方法(在函数中要干什么)
TestInt ti = 12; //有隐式类型转换,类型转换构造函数可被explicit屏蔽
TestInt ti(22); //仅调用类型转换构造函数,不会被explicit屏蔽
二、类型转换运算符(函数)
能力与一相反,是一个成语函数。
operator type() const;
const不必须、无形参、不能指定返回类型、需定义为成员函数
TestInt ti(77);
int k = ti.operator int() + 23;
int k = static_cast<int>(ti) + 23;
//k = 100
三、类型转换的二义性问题
比如有时候转换成double和int都可以计算,所以尽量一个类里只有一个类型转换运算符
四、类成员函数指针(指向类成员函数的指针)
1、对于普通成员函数(real address)
标准格式:
类名::*函数指针变量名 //声明指针
&类名::普通成员函数名 //获取普通成员函数地址
类对象名.*函数指针变量名 //使用
实例:
void(CT::*ptpoint)(int); //定义一个指向某函数的指针
ptpoint = &CT::ptfun; //给这个指针该普通成员函数的地址
//或写成 void(CT::*ptpoint)(int) = &CT::ptfun;
CT ct,*pct; //创建对象或指针用于调用该普通成员函数
pct = &ct;
(ct.*ptpoint)(100);
(ct->*ptpoint)(200);
2、对于虚成员函数(real address):同1
3、对于静态成员函数(real address)
标准格式:
*函数指针变量名 //声明指针
&类名::普通成员函数名 //获取普通成员函数地址
*函数指针变量名(参数) //使用
实例:
void(*staticpoint)(int) = &CT::ptfun;
(*staticpoint)(100);
五、类成员变量指针
1、对于普通成员变量(unreal address)
int CT::*mp = &CT::m_a;
CT ct;
ct.*mp = 12;
经调试,mp为0x00000004,不是真正意义上的指针,这个*mp指针指向的是该成员变量(m_a)与该类对象指针(一般指向首地址)的偏移量。因为这个类中有虚函数,编译器会生成一个虚函数表(基于类的)。当生成一个对象的时候(CT ct)会产生虚函数表指针(隐含的占4个字节)用来指向这个类中的虚函数,所以m_a的偏移量是4,前面的为虚函数表指针。
2、对于静态成员变量(real address)
int *staticp = &CT::m_static;
*staticp = 45;