C++第三天
01. 复习构造和析构
class CObj
{
private:
// 私有不会被外界直接访问的
char* str;
public:
// 一个构造函数,如果没有指定任何的构造函数
// 编译器就会默认生成一个无参的没有意义的构造
CObj(int size)
{
// 开辟一块空间并且初始化为 0
str = new char[size] {};
// FILE* f = fopen()
}
// 使用初始化列表,初始化列表中执行的是初始化
// - 常量、引用和没有无参构造的成员对象必须初始化
// - 初始化列表的初始化顺序和数据再类内的定义顺序相关
CObj() : str(nullptr)
{
// 赋值操作,执行这条语句的时候
// str 已经被创建出来了。
str = new char[10]{ 0 };
}
// 析构函数通常用于执行清理工作,主要包括关闭文
// 件,释放堆空间等操作,和构造是对应关系
~CObj() // 没有参数且只能有一个
{
// 释放空间,和构造函数对应
if (str) delete[] str;
// fclose(f)
}
};
int main()
{
return 0;
}
02. 复习拷贝构造函数
#include <iostream>
using namespace std;
class CObj
{
private:
// 当成员中包含指针的时候需要提供拷贝构造函数
// 如果没有提供拷贝构造函数,就会生成一个浅拷
// 贝的构造函数,执行的就是简单的赋值操作
char* name;
public:
// 无参构造: 啥事也不做
CObj() : name(nullptr)
{
cout << "这里调用了构造函数\n";
}
// 构造函数: 传入一个字符串,拷贝到申请的空间
CObj(const char* n)
{
// 1. 获取源字符串的长度,包含空字符
int size = strlen(n) + 1;
// 2. 申请空间用于保存字符串
name = (char*)malloc(sizeof(char)*size);
// 3. 将字符串拷贝到申请的空间
memcpy(name, n, size);
// 浅拷贝: name = n;
}
// 析构函数
~CObj()
{
// new -> delete; new[] -> delete[]
if (name) free(name);
cout << "这里调用了析构函数\n";
}
// 拷贝构造函数: 参数是当前类对象的(常量)引用
CObj(CObj& obj)
{
// 根据传入的字符串申请空间,并将传入的
// 字符串拷贝到申请的堆空间中
this->name = _strdup(obj.name);
}
};
int main()
{
CObj xiaoming("xiaoming");
CObj xiaoming2hao(xiaoming);
// new 会调用构造函数,malloc 不会调用
CObj* pObj1 = new CObj;
CObj* pObj2 = (CObj*)malloc(sizeof(CObj));
// 相应的进行释放堆空间
delete pObj1;
free(pObj2);
return 0;
}
03. 转换构造函数
// 转换构造函数: 是一个特殊的构造函数,通常把
// 只有一个参数的构造函数称为转换构造,转换构
// 造用于将一个其它类型的数值转换为当前的类类型
class CObj
{
private:
int number;
char m_chr;
double dbl;
public:
// 一个转换构造函数
CObj(int number)
{
this->number = number;
}
// 一个转换构造: 如果使用了这个关键字,就
// 不能进行隐式转换,只能手动转换
explicit CObj(double d) : dbl(d) { }
// 一个转换构造
CObj(char chr) : m_chr(chr) {}
};
// 可能会出现非常多的问题
void test(CObj obj)
{
// 函数的参数要求必须是一个类对象,
// 假设当前编写了转化构造函数,那么
// 可以传入的值就不仅仅是类对象了
}
int main()
{
// 一个整数类型
int number = 10;
// 使用转换构造: 将 numbe [隐式]转
// 换成一个对象, 使用这个对象对 obj
// 进行初始化操作
CObj obj = number;
test(number);
// 使用的是 CObj(char) 转换构造
CObj obj2 = 'a';
// 没有提供字符串类型的
// CObj obj3 = "str";
// 不能进行隐式转换
test(1.1);
// 可以手动构造对象再传递
test(CObj(1.1));
return 0;
}
04. 第一个单继承
#include <iostream>
using namespace std;
// 继承可以提供代码的复用性
// 继承是类与类之间的一种关系,会产生在两个
// 和两个以上的类中,如果说类 A 继承自了类
// B,那么基类(父类)是 B,派生类(子类)就是 A。
// 父类
class CParent
{
public:
int number = 10;
// 成员函数用于输出变量 number 的值
void print();
};
// 在类外定义成员函数需要添加类域(类的作用域)
void CParent::print()
{
printf("number = %d\n", this->number);
}
// 子类,继承自 CParent, 冒号后面就是父类
// 以及继承的方式: 私有、公有、受保护的
class CChild : public CParent
{
// 当存在继承时,子类中会保留父类的所有属性
// 以及父类中的所有函数。除此之外,还可以在
// 父类的基础上增加新的属性和函数
public:
int child_number = 100;
};
int main()
{
// 创建一个子类对象
CChild object;
// 在子类中可以访问到父类的属性和函数
object.number = 200;
object.print();
// 除了父类的属性,还可以有自己的东西
object.child_number = 200;
return 0;
}
// 假设不存在继承,写法是下面这样的
/*
class CParent
{
public:
int number = 10;
// 成员函数用于输出变量 number 的值
void print()
{
printf("number = %d\n", this->number);
}
};
class CChild
{
public:
int child_number = 100;
// 单独的再次定义出父类的所有数据
int number = 10;
void print()
{
printf("number = %d\n", this->number);
}
};
*/
05. 更加完整的例子
#include <iostream>
using namespace std;
// 游戏中可能会使用到二维坐标和三维坐标
// 通过继承的方式来实现这样的两个类
// 二维坐标类,用于描述二维坐标和对坐标执行的操作
class CPoint2D
{
public:
// 保存 x 和 y 的数据成员
int x, y;
public:
// 构造函数,用于初始化两个成员
CPoint2D(int X = 0, int Y = 0)
: x(X), y(Y) { }
// 单独的设置坐标的位置
void SetPoint2D(int X = 0, int Y = 0)
{
this->x = X;
this->y = Y;
}
// 显示当前坐标的位置
void ShowPoint2D()
{
printf("x: %d\n", this->x);
printf("y: %d\n", this->y);
}
};
// 三维坐标具有二维坐标的一切属性还有自己的新的属性
class CPoint3D : public CPoint2D
{
private:
// 继承的时候,已经继承了 x, y
int z;
public:
// 构造一个三维坐标
CPoint3D(int X, int Y, int Z) :
// 初始化列表中不能直接填写父类的成员
// 如果想要对父类进行初始化,只能通过
// 手动在初始化列表中调用父类构造函数
z(Z), CPoint2D(X, Y) { }
// 不存在默认构造函数的父类必须在初始化列表中进行初始化
// 单独的设置一个三维坐标的位置
void SetPoint3D(int X, int Y, int Z)
{
// 可以借助父类已经编写好的函数进行设置
SetPoint2D(X, Y);
// 再单独编写自己类添加属性
this->z = Z;
/* 没有体现出继承的优越性
this->x = X;
this->y = Y;
this->z = Z;
*/
}
// 显示一个坐标
void ShowPoint3D()
{
// 最好加上父类的作用域
CPoint2D::ShowPoint2D();
printf("z: %d\n", z);
}
};
int main()
{
CPoint3D Point(1, 2, 3);
Point.ShowPoint3D();
// 调用父类中的函数
Point.ShowPoint2D();
// 设置新的坐标
Point.SetPoint3D(3, 2, 1);
Point.ShowPoint3D();
return 0;
}
06. 继承方式 - 公有
// 通过修改继承时的继承方式测试三种访问属性的
// 数据成员再子类中会变成什么形式。
// 拥有三种不同访问权限成员的父类
class CObj
{
public:
int PublicValue = 0;
private:
int PrivateValue = 0;
protected:
int ProtectedValue = 0;
};
// class 子类名称 : [继承方式] 父类名称
class CChild : public CObj
{
// 在子类中通过对不同成员的访问
// 判断继承后的具体访问属性
void test()
{
// 类内[√] 类外[√]
this->PublicValue = 10; // public -> public
// 类内[√] 类外[×]
this->ProtectedValue = 10; // protected -> protected
// 类内[×] 类外[×]
this->PrivateValue = 10; // 在类内外无法访问
}
};
// 当使用公有方式进行继承的时候,除了私有的不可访问之外
// 其它的两个访问属性在子类中保存不变
int main()
{
CChild object;
// 类外能访问的只有 public
object.PublicValue = 10;
// 在类外访问父类中受保护的属性
object.ProtectedValue = 100;
return 0;
}
07. 继承方式 - 私有
// 通过修改继承时的继承方式测试三种访问属性的
// 数据成员再子类中会变成什么形式。
// 拥有三种不同访问权限成员的父类
class CObj
{
public:
int PublicValue = 0;
private:
int PrivateValue = 0;
protected:
int ProtectedValue = 0;
void test()
{
// 在类内可以通过 VS 的自动提示
// 判断变量的访问属性
}
};
// class 子类名称 : [继承方式] 父类名称
class CChild : private CObj
{
// 在子类中通过对不同成员的访问
// 判断继承后的具体访问属性
void test()
{
// 类内[√] 类外[×]
this->PublicValue = 10; // public -> private
// 类内[√] 类外[×]
this->ProtectedValue = 10; // protected -> private
// 类内[×] 类外[×]
this->PrivateValue = 10; // 在类内外无法访问
}
};
// 当使用私有方式进行继承的时候,除了私有的不可访问之外
// 其它的两个访问属性在子类中都变成了私有的属性
int main()
{
CChild object;
// 父类中的公有和受保护的都变成
// 不可直接在类外访问的
object.PublicValue = 10;
object.ProtectedValue = 100;
return 0;
}
08. 继承方式 - 保护
// 通过修改继承时的继承方式测试三种访问属性的
// 数据成员再子类中会变成什么形式。
// 拥有三种不同访问权限成员的父类
class CObj
{
public:
int PublicValue = 0;
private:
int PrivateValue = 0;
protected:
int ProtectedValue = 0;
void test()
{
// 在类内可以通过 VS 的自动提示
// 判断变量的访问属性
}
};
// class 子类名称 : [继承方式] 父类名称
class CChild : protected CObj
{
// 在子类中通过对不同成员的访问
// 判断继承后的具体访问属性
void test()
{
// 类内[√] 类外[×]
this->PublicValue = 10; // public -> protected
// 类内[√] 类外[×]
this->ProtectedValue = 10; // protected -> protected
// 类内[×] 类外[×]
this->PrivateValue = 10; // 在类内外无法访问
}
};
// 当使用保护方式进行继承的时候,除了私有的不可访问之外
// 其它的两个访问属性在子类中都变成了保护的属性
int main()
{
CChild object;
// 父类中的公有和受保护的都变成
// 不可直接在类外访问的
object.PublicValue = 10;
object.ProtectedValue = 100;
return 0;
}
// 如果一个变量不想被外界直接的访问到,但是又想被子类继承下去,就应该设置成受保护的
// 如果一个变量不想被外界直接的访问到,就应该设置成私有的
// 通常公有的都是用于操作数据提供的接口
09. 修改访问属性
class CObj
{
public:
// 在父类中这是一个公有的访问属性
int numberA = 10;
int numberB = 10;
};
// 当不填写继承方式的时候,类(class)的继承
// 方式默认是私有的,结构体(struct)的继承
// 方式默认是公有的。
class CChild : /*private*/ CObj
{
// 因为继承方式是私有的,所以父类中公有
// 属性的变量也变成了私有的,在子类中
// 不能直接使用子类对象进行访问
public:
// 可以在子类中重新声明父类中变量的访问
// 属性,使子类对象可以访问到它。
CObj::numberB;
};
int main()
{
CChild object;
// 无法访问
object.numberA;
// 可以访问
object.numberB;
return 0;
}
0A. 对不可访问的探索
父类私有存在子类中
输出类的大小,一个类如果什么都没有大小为 1
强行访问私有数据 int number = (int)& Child;
#include <iostream>
using namespace std;
// 父类的私有成员不能够在子类中被访问到,那么它
// 是否存在于子类对象的内存中呢?
class CObj
{
private:
int numberA = 10; // 是否存在子类中?
public:
void show()
{
printf("numberA = %d\n", numberA);
}
};
class CChild : public CObj
{
// 什么都不写可能会有两种情况
// 1. 父类私有存在子类中,sizoef(CChild) -> 4
// 2. 父类私有不存在子类中,sizoef(CChild) -> 1
// 子类中继承了父类的 show 函数,可以显示 number
// 如果父类的私有不存在子类的内存中,会导致父类继承
// 下的所有操作 number 的函数都没有任何意义。(将
// 父类的私有保存到子类内存的原因是提供给父类继承给
// 子类用于操作 number 的函数使用的)。
};
int main()
{
// 输出类的大小,一个类如果什么都没有大小为 1
printf("sizeof(CChild) = %d\n", sizeof CChild);
// 根据测试结果,父类中的私有虽然在子类中无法访问
// 但是实际上确实存在于子类的内存中,为什么存在?
CChild Child;
Child.show();
// 了解: 强行访问私有数据
int number = *(int*)& Child;
return 0;
}
0B. 构造和析构的顺序
// 构造顺序: 基类 -> 成员类 -> 当前类
// 析构顺序: 当前类 -> 成员类 -> 基类
#include <iostream>
class CMember
{
public:
CMember() { printf("CMember::CMember()\n"); }
~CMember() { printf("CMember::~CMember()\n"); }
};
class CParent
{
public:
CParent() { printf("CParent::CParent()\n"); }
~CParent() { printf("CParent::~CParent()\n"); }
};
// 父类 + 子类 + 成员类
class CChild : public CParent
{
public:
CChild() { printf("CChild::CChild()\n"); }
~CChild() { printf("CChild::~CChild()\n"); }
private:
CMember member;
};
int main()
{
// 构造顺序: 基类 -> 成员类 -> 当前类
// 析构顺序: 当前类 -> 成员类 -> 基类
CChild* object = new CChild;
delete object;
return 0;
}
0C. 成员的重定义
重定义:
子类中定义的成员和父类同名,从而 覆盖了父类的成员,导致在子类中无法[直接] 访问到父类的成员。
要求:
作用域不同(父类\子类),函数名相同,函数的参数可以相同也可以不同。
通过指定作用域运算符访问父类的数据 在需要访问的数据前,加上父类的作用域
重载的要求:
作用域必须相同,函数名相同,参数必须不同(类型\个数\顺序),返回值无关
#include <iostream>
class CParent
{
public:
// 父类中的 number
int number = 10;
public:
// 一个不带参的成员函数
void PrintHello()
{
printf("CParent: Hello World\n");
}
// 一个带参的成员函数
void PrintNumber(const char* str)
{
printf("str = %s\n", str);
}
};
// 在子类中定义和父类成员同名的成员
class CChild : public CParent
{
public :
// 子类中新定义的 Number
int number = 100;
public:
// 保持函数原型不变的同名函数
void PrintHello()
{
printf("CChild: Hello World\n");
}
// 修改了参数类型但是函数名称相同的函数
void PrintNumber(double n)
{
printf("n = %lf\n", n);
}
};
int main()
{
CChild child;
// 当子类中的成员和父类重名时,默认输出的是子类的
printf("number = %d\n", child.number);
// 子类的同名成员实际覆盖了父类的同名成员
child.PrintHello();
// 覆盖后,父类的同名成员就不能被直接访问了
// child.PrintNumber("nihao");
child.PrintNumber(1.23456789);
// 通过指定作用域运算符访问父类的数据
// 在需要访问的数据前,加上父类的作用域
printf("number = %d\n", child.CParent::number);
child.CParent::PrintHello();
child.CParent::PrintNumber("123456789");
return 0;
}
0D. 多继承的例子
#include <iostream>
// 蜘蛛类: 提供了蜘蛛的一些行为
class CSpiler
{
public:
void 吐丝() { printf("CSpiler::吐丝()\n"); }
void 结网() { printf("CSpiler::结网()\n"); }
};
// 人类: 提供人的一些行为
class CHuman
{
public:
void 上学() { printf("CHuman::上学()\n"); }
void 吃饭() { printf("CHuman::吃饭()\n"); }
};
// 蜘蛛侠: 又有人的行为,又有蜘蛛的行为
class CSpilerMan : public CHuman, public CSpiler
{
// 多继承的写法 class 子类名 : 继承方式1 父类1, 继承方式2 父类2 { };
public:
// 子类会同时继承所有父类中的所有属性
// 除此之外,可以添加自己的一些行为
void 打击犯罪() { printf("CSpilerMan::打击犯罪()\n"); };
};
int main()
{
CSpilerMan Peter;
Peter.上学();
Peter.吐丝();
Peter.吃饭();
Peter.结网();
Peter.打击犯罪();
return 0;
}
0E. 多继承中的二义性1
会保存有人类的 weight
会保存有蜘蛛的 weight
类内同时存在两个 weight,访问的过程中会产生二义性
#include <iostream>
// 当类内同时存在同名的数据
// 1. 通过作用域运算符进行访问
// 2. 改名字 spiler_weight
// 蜘蛛类: 提供了蜘蛛的一些行为
class CSpiler
{
public:
// 描述的是蜘蛛的重量
int weight = 0x11111111;
};
// 人类: 提供人的一些行为
class CHuman
{
public:
// 描述的是人的重量
int weight = 0xFFFFFFFF;
};
// 蜘蛛侠
class CSpilerMan : public CHuman, public CSpiler
{
public:
// 蜘蛛侠的血量
int Bloods = 0x88888888;
};
int main()
{
CSpilerMan Peter;
// 虽然可以指定访问的是哪个,但是
// 内存中不应该保存有两份体重。
Peter.CSpiler::weight = 100;
Peter.CHuman::weight = 100;
// 当前的内存状态
// 0x006FFABC ffffffff 人: weight \ 理论应该合并成一份
// 0x006FFAC0 11111111 蜘蛛: weight /
// 0x006FFAC4 88888888 蜘蛛侠: Bloods
return 0;
}
0F. 多继承中的二义性2
如何完全解决这种二义性问题
转换为菱形继承 -> 转换为虚继承(将内存中的爷爷类转换成一虚继承可以用于解决菱形继承中产生的二义性问题。
需要在两个父类继承自爷爷类时,在继承方式的前面或后面添加 virtual 关键
创建一个新的爷爷类,在爷爷类中添加蜘蛛类
和人类都包含有的一些属性。
#include <iostream>
class CAnimal
{
public:
// 描述动物的属性
int weight = 0x11111111;
};
// 蜘蛛类
class CSpiler : virtual public CAnimal
{
public:
// CAnimal::weight;
int a = 0x22222222;
};
// 人类
class CHuman : public virtual CAnimal
{
public:
// CAnimal::weight;
int b = 0x33333333;
};
// 蜘蛛侠
class CSpilerMan : public CHuman, public CSpiler
{
// 没有使用虚继承的情况
// 0x012FF6FC 11111111 CAnimal::weight
// 0x012FF700 33333333 CHuman::b
// 0x012FF704 11111111 CAnimal::weight
// 0x012FF708 22222222 CSpiler::a
// 0x012FF70C 44444444 CSpilerMan::Bloods
// 使用了虚继承的情况,爷爷类只有一份了
// 当一个类存在虚继承就会生成一个虚基表指针
// 虚基表指针指向一个数组,数组的第一个元素
// 始终为0,第二个原始是一个,在菱形继承中
// 这个偏移加上当前的地址可以找到唯一的爷爷类。
// 0x00EFF930 00297bdc 虚基表指针 -> 0x14
// 0x00EFF934 33333333 CHuman::b
// 0x00EFF938 00297be4 虚基表指针 -> 0x0C
// 0x00EFF93C 22222222 CSpiler::a
// 0x00EFF940 44444444 CSpilerMan::Bloods
// 0x00EFF944 11111111 CAnimal::weight
public:
// 蜘蛛侠的血量
int Bloods = 0x44444444;
};
int main()
{
CSpilerMan Peter;
Peter.weight = 10;
return 0;
}