一: 拷贝构造函数只会在初始化时调用
拷贝构造函数只会在初始化时调用,初始化之后的赋值,不会调用拷贝构造函数,而是简单的复制成员变量.
二、类型转换构造函数
可以看到,执行c1 = 9
的赋值操作时,9会自动类型转换构造函数去生成一个临时的Complex对象.
其实我们可以看到,前面那个例子里面的那个构造函数,既可以作为隐式的类型转换,也可以作为显式的初始化,为了让编译器确切知道是哪种,引入了explicit
关键词,加了explicit的就不能用于类型转换了,编译器会报错.
三、析构函数
变量生命期结束时,就会调用析构函数.
new出来的这个对象是不会自动析构的,只有在delete的时候才会析构.
三、静态成员变量、函数
静态成员变量的初始化,必须要拿到class外面声明一下,声明的时候可以选择初始化或不初始化.
class Complex {
public:
static int cc;
double real,imag;
};
int Complex::cc = 1;
四、成员对象和封闭类
- 有成员对象的话,必须写一个构造函数,不然不知道那个成员对象用什么构造函数进行初始化的
- 成员对象要用初始化列表进行初始化.
五、常量对象、常量成员函数
- 常量的对象只能使用构造、析构和const的常量函数.
- const函数不能修改成员变量.
- 这里很特别,是否const的函数居然是重载关系.
六、友元函数和友元类
- 一个类的友元函数可以访问该类的私有成员
- ModifyCar中之所以可以访问Car的私有成员price,就是因为规定了友元
- MostExpensiveCar函数之所以可以...,也是应为规定了友元
下面这个代码,来规定CDriver类是CCar类的友元类
friend class CDriver; //声明CDriver为友元类
七、运算符重载
1.重载的基础知识
2.赋值运算符的重载
- 可以看到,String的默认构造函数里面执行
str(new char[1])
和str[0]=0
,来形成一个空串 - 重载赋值运算符,当被赋值的为char *的,那么就先
delete []str
,然后重新new一个,然后strcpy来得到 - 要注意这个赋值函数的返回值是
String &
,我试了其他作为返回值都不行,暂时记住吧.
String s2 = "hello"
这个是初始化语句,调用的是构造函数.
这个是浅拷贝,因为没有重载String到String的赋值,所以这个就是浅拷贝,拷贝这个str指针.这样会导致大灾难,如下:
之后S1或S2其中一个去用char*赋值的时候,就会把同一个str给delete掉,或者其中一个析构的时候,也会把他给delete掉.所以要加下面这样的代码:
但是这样还是有问题的,见下:
如果s=s的话,就把唯一一个str给delete掉了,就错误了!!!所以正确的办法是需要加一个检查,如果相等的话,那么直接返回了!
噢!这里解释了为什么要返回String&,是因为我们经常会连=来赋值
- void不行,因为这样就不能连等
- String不行,因为在执行
(a=b)=c
这样,其实是让a=c,我们需要让a=b的结果为a的引用,这样才能做到a=c
最后的问题是,如果不写复制构造函数,那么编译器缺省的那个是浅拷贝.
String( String & s) {
str= new char[strlen(s.str)+1];
strcpy(str,s.str);
}
3.运算符重载为友元函数
一般情况下,我们都会将运算符重载为class的成员函数,而不是全局函数,但是有时候必须是全局函数,比如在5+c的情况,我们总不可能给int做一个重载. 全局函数的话,如果要访问私有变量,那么只能弄成友元了.
4.流(插入/提取)运算符的重载
- 做一个全局函数,class里面写<<重载如上,接收一个ostream & 和 CStudent类型的参数,然后用
o << s.nAge
来输出,然后返回这个ostream的引用
ok,写之前,需要知道:
- cin是istream类型的
- cout是ostream类型的
- cout是很简单的
- cin比较复杂一点,不深究.
5.类型转换运算符 & 自增自减运算符 的重载
- 类型转换运算符即类的名字,比如double可以将数据转换成double类型
operator double() { // 类型转换的重载不需要写上返回类型的关键字double
return real;
}
- 这个就是给double类型转换做一个重载,返回real. 可以看到,这个重载的写法有点特殊,不用写返回类型的,因为必须是double!
- 不仅仅显示的(double)c做强制类型转换时会用到,隐式的double n = 2 + c也会用到
6.自增自减运算符的重载
- ++i:作为一元的
- i++:作为二元的,要多写一个没用的参数
#include <iostream>
using namespace std;
class CDemo {
private:
int n;
public:
CDemo(int n_=0) : n(n_) {
}
CDemo & operator++(); // 重载前置++
CDemo operator++(int); // 重载后置++
CDemo & operator--(); // 重载前置--
CDemo operator--(int); // 重载后置--
operator int() { // (int)类型强制转换
return n;
}
};
CDemo & CDemo::operator++() { // 重载前置++
++n;
return *this;
}
CDemo CDemo::operator++(int) { // 重载后置++
CDemo tmp(*this);
n++;
return tmp;
}
CDemo & CDemo::operator--() { // 重载前置--
--n;
return *this;
}
CDemo CDemo::operator--(int) { // 重载后置--
CDemo tmp(*this);
n--;
return tmp;
}
int main()
{
CDemo d(5);
cout << (d++ ) << ","; //等价于 d.operator++(0);
cout << d << ",";
cout << (++d) << ","; //等价于 d.operator++();
cout << d << endl;
cout << (d-- ) << ","; //等价于 d.operator--(0);
cout << d << ",";
cout << (--d) << ","; //等价于 d.operator--();
cout << d << endl;
return 0;
}
输出:
5,6,7,7
7,6,5,5
注意:
- 我们
cout << d++
,并没有重载<<,而是通过一个int强制转换,来搞成int. - 注意到,
++d
我们的返回值是CDemo &
,是一个引用,因为我们经常会++i = 1.d++
的返回值是CDemo
,是一个值,因为我们不能i++ = 1,i++是返回一个临时的值,然后实际的值已经变了,所以看这个代码就很清楚了:
CDemo & CDemo::operator++() { // 重载前置++
++n;
return *this;
}
CDemo CDemo::operator++(int) { // 重载后置++
CDemo tmp(*this);
n++;
return tmp;
}
八、继承
1.继承和派生的概念
-
继承:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类,而把B作为基类的一个派生类(也称子类)
- 派生类是通过对基类进行修改和扩充得到的。
- 在派生类中,可以扩充新的成员变量和成员函数。
- 派生类一经定义后,可以独立使用,不依于基类。
-
派生类拥有基类的全部成员函数和成员变量,不论是private、protected、public
- 在派生类的各个成员函数中,不能访问基类中的private成员
-
派生类的写法 class 派生类名:public 基类名 { };
-
覆盖
- 派生类可以定义一个和基类成员同名的成员,这叫覆盖
- 在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员
- 要在派生类中访问由基类定义的同名成员时,要使用作用域符号::
- 一般来说,基类和派生类不定义同名成员变量,但有同名的成员函数很常见
-
派生类对象的内存空间
- 派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。
- 在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前
继承实例程序:学籍管理:
#include <iostream>
using namespace std;
class CStudent { // 基类
private:
string name; // 名字
string id; // id
char gender; // 性别
int age; // 年龄
public:
void SetInfo(const string name_, const string id_, int age_, char gender_);
string GetName();
void PrintInfo();
};
void CStudent::SetInfo(const string name_, const string id_, int age_, char gender_) {
name = name_;
id = id_;
gender = gender_;
age = age_;
}
string CStudent::GetName() {
return name;
}
void CStudent::PrintInfo() {
cout << "Name: " << name << endl;
cout << "ID: " << id << endl;
cout << "Gender: " << gender << endl;
cout << "Age: " << age << endl;
}
class CUndergraduateStudent : public CStudent { //派生类,继承CStudent类
private:
string department;
public:
void SetInfo(const string name_, const string id_, int age_, char gender_, string department_); // 覆盖基类的方法
void QualifiedForBaoyan(); // 覆盖基类的方法
void PrintInfo(); // 覆盖基类的方法
};
void CUndergraduateStudent::QualifiedForBaoyan() {
cout << "qualified for baoyan" << endl;
}
void CUndergraduateStudent::SetInfo(const string name_, const string id_, int age_, char gender_, string department_) {
CStudent::SetInfo(name_, id_, age_, gender_); // 调用基类的方法
department= department_;
}
void CUndergraduateStudent::PrintInfo() {
CStudent::PrintInfo(); // 调用基类的方法
cout << "Department: " << department << endl;
}
int main()
{
CUndergraduateStudent s2;
s2.SetInfo("Harry Potter ", "118829212", 19, 'M', "Computer Science");
cout << s2.GetName() << " " ;
s2.QualifiedForBaoyan ();
s2.PrintInfo ();
return 0;
}
输出:
Harry Potter qualified for baoyan
Name: Harry Potter
ID: 118829212
Gender: M
Age: 19
Department: Computer Science
2.继承和复合关系
- 这是继承和复合的核心区别.
- 再来看看复合关系:
3.派生类覆盖基类成员
4.类的保护成员
- 相比private成员,给protected成员多了一个权限,即派生类的成员函数可以访问基类的protected成员. 所以,如果想要让派生类的函数可以用基类的一些变量或者函数,那么就用protected.
- 注意了,protected也仅仅是在派生类的成员函数里面可以访问.
5.派生类的构造函数
规范用初始化列表来初始化派生类中包含的那个基类对象,看下面的例子.
这个错误的构造函数的错误原因在于,基类的legs和color是private的,父类不能访问.
-
在创建派生类的对象时:
- 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
- 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
- 最后执行派生类自己的构造函数
-
在派生类对象消亡时:
- 先执行派生类自己的析构函数
- 再依次执行各成员对象类的析构函数
- 最后执行基类的析构函数
-
析构函数的调用顺序与构造函数的调用顺序相反
6.public继承的赋值兼容规则
-
派生类的对象可以赋值给基类对象
- b = d;
-
派生类对象可以初始化基类引用
- base & br = d;
-
派生类对象的地址可以赋值给基类指针
- base * pb = & d
7.直接基类和间接基类
- 当派生类的对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数
- 当派生类的对象消亡时,先执行自身的析构函数,然后再从底向上执行各个基类的析构函数 看一个例子:
#include <iostream>
using namespace std;
class Base { // 基类
protected:
int n;
public:
Base(int n_=0) : n(n_) {
cout << "Base constructed" << endl;
}
~Base(){
cout << "Base destructed" << endl;
}
};
class Derived : public Base {
public:
Derived(int n_) :Base(n_) {
cout << "Derived constructed" << endl;
}
~Derived(){
cout << "Derived destructed" << endl;
}
};
class MoreDerived : public Derived { // 直接基类Derived, 不用声明间接基类Base
public:
MoreDerived() : Derived(0) {
cout << "MoreDerived constructed" << endl;
this->n = 0;
}
~MoreDerived() {
cout << "MoreDerived destructed" << endl;
}
};
int main()
{
MoreDerived m;
return 0;
}
输出:
Base constructed
Derived constructed
MoreDerived constructed
MoreDerived destructed
Derived destructed
Base destructed
九、多态
1.虚函数与多态的基本概念
- 在类的定义中,前面有 virtual 关键字的成员函数就是虚函数
- virtual关键字只用在类定义里的函数声明中,写函数体时不用
- 静态成员函数不能是虚函数
-
派生类的指针可以赋给基类指针
-
通过基类指针调用基类和派生类中的同名虚函数时:
- 若该指针指向一个基类的对象,那么被调用是基类的虚函数
- 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数
这种机制就叫做“多态”
-
派生类的对象可以赋给基类引用
-
通过基类引用调用基类和派生类中的同名虚函数时:
- 若该引用引用的是一个基类的对象,那么被调用是基类的虚函数
- 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数
这种机制也叫做“多态”
看一个例子:
#include <iostream>
using namespace std;
class Base {
public:
virtual void someVirtualFunction() {
cout << "Base someVirtualFunction()" << endl;
}
};
class Derived : public Base {
public:
virtual void someVirtualFunction() {
cout << "Derived someVirtualFunction()" << endl;
}
};
int main()
{
Derived d;
Base *pb, b;
// 指针
pb = &d;
pb->someVirtualFunction(); // 输出“Derived someVirtualFunction()”
pb = &b;
pb->someVirtualFunction(); // 输出“Base someVirtualFunction()”
// 引用
Base &br1 = d;
br1.someVirtualFunction(); // 输出“Derived someVirtualFunction()”
Base &br2 = b;
br2.someVirtualFunction(); // 输出“Base someVirtualFunction()”
return 0;
}
2.多态实例:魔法门之英雄无敌
- 游戏中有很多种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象, 比如说有Dragon,Wolf, Ghost等
- 每种怪物都有生命力、攻击力这两种属性
- 怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的
- 游戏版本升级时,要增加新的怪物--雷鸟。如何编程才能使升级时的代码改动和增加量较小?
下面先看一下非多态的实现:
- 需要有N个Attack和FightBack函数,非常麻烦,而且新增一个怪物的时候,要去增加所有怪物类的Attack和FightBack,程序改动较大
下面来看一下多态的实现:
- 可以看到,只要在每个怪物类里面写一个Attack,Hurted和FightBack即可.
#include <iostream>
using namespace std;
class Creature {
protected:
int nLifeValue, nPower;
public:
virtual void Attack(Creature *p) {}
virtual void Hurted(int power) {}
virtual void FightBack(Creature *p) {}
};
class Dragon : public Creature {
public:
virtual void Attack(Creature *p) {
p->Hurted(nPower);
p->FightBack(this);
}
virtual void FightBack(Creature *p) {
p->Hurted(nPower/2);
}
virtual void Hurted(int power) {
nLifeValue -= power;
}
};
class Wolf : public Creature {
public:
virtual void Attack(Creature *p) {
p->Hurted(nPower);
p->FightBack(this);
}
virtual void FightBack(Creature *p) {
p->Hurted(nPower/2);
}
virtual void Hurted(int power) {
nLifeValue -= power;
}
};
class Ghost : public Creature {
public:
virtual void Attack(Creature *p) {
p->Hurted(nPower);
p->FightBack(this);
}
virtual void FightBack(Creature *p) {
p->Hurted(nPower/2);
}
virtual void Hurted(int power) {
nLifeValue -= power;
}
};
int main()
{
Dragon dragon;
Wolf wolf;
Ghost ghost;
dragon.Attack(&wolf);
dragon.Attack(&ghost);
return 0;
}
3.多态实例:几何形体程序
Sample Input:
3
R 3 5
C 9
T 3 4 5
Sample Output
Triangle: 6
Rectangle:15
Circle:254.34
CShape
是所有的基类,他的Area
和PrintInfo
是一个纯虚函数,是没有函数体的,因为一个CShape的对象并没有面积和Info.- 派生出的矩形类是
CRectangle
,多了一个w,h表示宽和高...以及圆CCircle
等等.
这里写了派生类的Area
和PrintInfo
CShape *pShapes[100];
intMyCompare(constvoid *s1, constvoid *s2);
intmain()
{
inti;
intn;
CRectangle *pr;
CCircle *pc;
CTriangle *pt;
cin >> n; // 输出个数
for (i = 0; i < n; i++)
{
char c;
cin >> c;
switch (c)
{
case 'R':
pr = new CRectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr;
break;
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes, n, sizeof(CShape *), MyCompare);
for (i = 0; i < n; i++)
pShapes[i]->PrintInfo();
return 0;
}
int MyCompare(const void *s1, const void *s2)
{
double a1, a2;
CShape **p1; // s1,s2 是void * ,不可写“* s1”来取得s1指向的内容
CShape **p2;
p1 = (CShape **)s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape*
p2 = (CShape **)s2; // 故p1,p2都是指向指针的指针,类型为CShape**
a1 = (*p1)->Area(); // * p1 的类型是Cshape* ,是基类指针,故此句为多态
a2 = (*p2)->Area();
if (a1 < a2)
return -1;
else if (a2 < a1)
return 1;
elsereturn 0;
}
4.在非构造/析构的成员函数中调用虚函数,是多态
例子:
#include <iostream>
using namespace std;
class Base {
public:
void fun1() {
fun2(); // 在非构造函数和非析构函数的成员函数中调用虚函数,是多态
}
virtual void fun2() {
cout << "Base fun2()" << endl;
}
};
class Derived:public Base {
public:
void fun2() { // 派生类的虚函数可以不关键字virtual
cout << "Derived fun2()" << endl;
}
};
int main()
{
Derived d;
Base *pb = &d;
pb->fun1();
return 0;
}
可以看到func1是正常的函数,他调用了虚函数func2,main里面执行func1,会多态地执行func2,所以输出:
Derived fun2()
在构造函数和析构函数中调用虚函数,不是多态
- 编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数
如下例:
#include <iostream>
using namespace std;
class Base {
public:
virtual void Hello() {
cout << "Base Hello()" << endl;
}
virtual void Bye() {
cout << "Base Bye()" << endl;
}
};
class Derived:public Base {
public:
void Hello() {
cout << "Derived Hello()" << endl;
}
void Bye() {
cout << "Derived Bye()" << endl;
}
Derived() {
Hello();
}
~Derived() {
Bye();
}
};
class MoreDerived:public Derived {
public:
void Hello() {
cout << "MoreDerived Hello()" << endl;
}
void Bye() {
cout << "MoreDerived Bye()" << endl;
}
MoreDerived() {
Hello();
}
~MoreDerived() {
Bye();
}
};
int main()
{
MoreDerived md; // 输出:Derived Hello() , MoreDerived Hello()
cout << endl;
Base *pb = &md;
pb->Hello(); // 多态 输出:MoreDerived Hello()
cout << endl;
return 0;
// 析构 输出:MoreDerived Bye() , Derived Bye()
}
5.虚函数的访问权限,取决于基类虚函数的访问权限
6.虚函数只需要基类+virtual,子类可加可不加
7.多态的实现原理
- “多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 ---- 这叫“动态联编”
-
多态实现的关键 -- 虚函数表
- 每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针
- 虚函数表中列出了该类的虚函数地址
- 多出来的4个字节(64-bit应该是8字节)就是用来放虚函数表的地址的,且放在对象的开头.
- 多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令
简单地说,多态的调用函数并不是简单的call一个地址,而是要去虚函数表里面去找那个虚函数地址
这段程序就是证明了前面的说法,让pa的开头8字节变成a的开头8字节,这样就改变了pa的虚函数表的地址,那么调用Func的时候,就去调用A的func
8.虚析构函数
虚析构函数是推荐做法! 问题:
- 如果不给基类做一个虚析构函数的话,那么通过基类的指针去delete派生类对象时,通常只会调用基类的析构函数. 解决办法:
- 把基类的析构函数声明为virtual
-
通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数
- 但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
-
解决办法:把基类的析构函数声明为virtual
- 派生类的析构函数可以virtual不进行声明
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
-
一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。
-
注意:不允许以虚函数作为构造函数
9.纯虚函数和抽象类
class A {
private:
int a;
public:
virtual void Print( ) = 0 ; //纯虚函数
};
-
包含纯虚函数的类叫抽象类
- 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
- 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
- 之所以抽象类的成员函数可以调用纯虚函数,是因为基类的成员函数里调用虚函数是多态的形式。
- 之所以构造/析构函数不能调用纯虚函数,因为此时不是多态,所以不能调用一个函数体都没有的函数.
- 继承自抽象类,必须实现所有纯虚函数才不是抽象类,这个和Java很像.
十、输入输出和文件操作
1.输入输出流相关的类
- ios是抽象的基类,派生出istream和ostream
- istream是用于输入的流类,cin就是该类的对象
- ostream是用于输出的流类,cout就是该类的对象
- ifstream是用于从文件读取数据的类
- ofstream是用于向文件写入数据的类
- iostream是既能用于输入,又能用于输出的类
- fstream 是既能从文件读取数据,又能向文件写入数据的类
- cin对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据
- cout对应于标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写入数据
- cerr对应于标准错误输出流,用于向屏幕输出出错信息
- clog对应于标准错误输出流,用于向屏幕输出出错信息
- cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信息;而输出到clog中的信息先会被存放在缓冲区,缓冲区满或者刷新时才输出到
如果是从文件输入的话,那么读到尾部算结束。如果是键盘输入,接收到Ctrl+Z
算结束。
还有一个比较难的点,就是虽然operator>>重载的返回值是istream &,不应该拿来做while的条件,但是istream是做了强制转换运算符的重载,给重载成了bool类型.
代码示例如下:
#include "bits/stdc++.h"
using namespace std;
int main(){
int x;
while (cin >> x){
cout << "echo : " << x << endl;
}
system("pause");
}
输出如下:
PS D:\leetcode> g++ a.cpp ; ./a
1 2 3 4
echo : 1
echo : 2
echo : 3
echo : 4
^D
请按任意键继续. . .
istream& getline(char * buf, int bufSize)
这个getline是以\n
为分隔符
istream& getline(char * buf, int bufSize,char delim);
这个getline以delim
为分隔符,不以\n
为分割符
- 这个getline很棒的一点是,这个bufSize是包括了结尾的\0在内的,所以char数组有多大,这个bufSize就有多大. 如果达到或超过了
- getline在读到了结尾,或者超过了bufSize时返回0.
cin.eof();//判断输入流是否结束
cin.peek();//读输入流的下一个字符,但不会从流中去掉(瞥一眼)
cin.pushback();//字符放到输入流
cin.ignore(int nCount=1,int delim=EOF);//输入流中最多删掉nCount个字符,或者遇到EOF了就结束
用freopen
来做输入/输出重定向.
- 在这里,标准输出的输出到test.txt中,标准错误输出是输出到终端中
- 标准输入重定向到了一个文件.