目录
1.类的关系
在类的设计中,往往需要考虑到类与类之间的关系,类的关系往往决定了这个类该怎么去设计/类的部分内容,较目前常见的关系,一般是如下两种:
关联关系:两个类存在着一定的关联,一般是类A是不是属于类B/类A是不是类B的成员类,或者说,类A的某个行为是否需要依赖于类B,所以关联关系一般会划分为3个小类:
依赖关系:类A的某个行为是否需要依赖于类B 例子: 屠夫杀猪,屠夫杀猪,需要一头猪 。
组合关系:是一种强关联属性的关系,表示一个物体有若干个部件组成,这些部件是不可分割的。
例子: 计算机主要组成部件(主板,电源,内存,硬盘),对于一台计算机来说是不可拆分的
聚合关系:是一种强关联属性的关系,表示一个物体有若干个部件组成,这些部件是可以分割的。
例子: 车子的某些组成部件拆出来是可以单独使用的
以上关系一般可以通过友元/成员类解决这种问题
类的泛化关系:泛化 => 衍生 ,是由一个物体衍生出了其他物体 / 亦或者说某一些物体属于同一个类别。
用户:普通用户,vip用户,超级vip用户.... => 用户的叫法不同,权限也不同,但是他们都是用户
员工:普通员工,项目经理,部门主管,技术总监
动物分类:胎生动物,卵生动物
马科:马 , 骡,驴
泛化关系的表示,在面对对象编程中,往往是以继承的方式是解决的
继承 =》 见下文
2.继承概念
继承的基本概念,和我们生活中的继承是差不多的,都是指从某个实体中得到了一些东西(属性/行为),而类的继承一般是指,新有的类从已有的类那里得到属性和行为的一种方式,除了从已有类中获取数据之外,还可以自己去新建一些行为+属性(青出于蓝,而胜于蓝),在类的继承关系中,新的类一般叫做派生类/子类(derived class),而已有的类一般叫做 父类/基类(base class)/超类(古老版本叫法,你只会在软考上看到这种东西),具体见图:
派生类从基类中继承属性+行为,继承之后也可以自己定义新的属性+行为,其结构一般如下
3.继承使用
先了解下基本的语法:
class 派生类名:继承方式 基类名,继承方式 基类名1 ...
{
约束:
行为/属性
约束:
行为/属性
};
继承方式:和约束是一样的 public,private,protected,这三种继承方式会改变从基类继承过来数据的访问权限。
具体如下表
继承方式 | 基类private | 基类public | 基类protected |
---|---|---|---|
public | 派生类不可访问 | 变成派生类的public | 变成派生类的protected |
private | 派生类不可访问 | 变成派生类的private | 变成派生类的private |
protected | 派生类不可访问 | 变成派生类的protected | 变成派生类的protected |
怎么记呢? 三种权限从高到底 public - protected - private, 继承过程中,低权限继承方式会把高权限内容下降到低权限位置,低权限内容不改变 (这就像一个门槛,高于门槛的要削减,低于门槛的直接原属性继承)
示例代码: usr.cpp
看看继承方式对数据的影响
基类的私有成员访问,只能通过基类的函数去访问(你如果通过指针强行操作,我也没辙)
//usr.cpp代码文件
#include<iostream>
using namespace std;
//用户类
class usr
{
private:
char *name; //8 0x1008
int age; //4 0x1010
public:
void work();
int m; //测试用 //4 0x1000
protected://类外不可访问,但是类内可访问
int test;//4 0x1014
};
void usr::work()
{
age = 100;
cout << "working" << endl;
}
//普通用户类
class normal_usr :public usr
{
public:
void listen_music();
private:
float money;//4 0x1018
};
void normal_usr::listen_music()
{
//age = 100;//基类的私有成员,派生类不可以直接访问
test = 100;//访问基类继承过来的protected成员
cout << "listen_music" << endl;
}
//测试方式: 继承过来的属性是public,使用类外访问方式
//protected:类内成员访问 =》 可访问
//基类的private:类内成员访问 =》 不可访问
int main()
{
normal_usr nu;
nu.m = 100;
nu.work();//public继承方式下,基类public成员在派生类中依旧是public
cout << sizeof(nu) << endl;
//从这句话中,我们测出了,其实就算基类成员是私有的,但是还是会继承
//既然继承了,那么肯定会有访问的方式
}
settings.json文件代码
{
//settings.json文件代码
"files.associations": {
"iostream": "cpp",
"array": "cpp",
"atomic": "cpp",
"*.tcc": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"string": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iosfwd": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"ostream": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"typeinfo": "cpp"
}
}
4.派生类对象的构建与基类对象构建的关系
基类拥有构造函数的 => 用于在构建基类对象时,初始化基类成员的。
派生类也会有构造函数 => 用于在构建派生类对象时,初始化成员的。
问题是,我基类的私有成员是继承过来的,但是我基类的私有成员,我派生类无法直接访问,那么基类的私有成员怎么在派生类中进行初始化呢?
=> 构造函数初始化列表
派生类对象构建,我们需要在构造函数上添加构造函数初始化列表,以调用基类构造函数初始化基类对象。
示例代码:inherit.cpp
严重性代码说明项目文件行错误C4996 | strcpy
C4996 ‘strcpy’: This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
出现这个错误,直接在头文件中添加以下这行代码即可(注意(disable:4996)也是需要添加的,不要只复制#pragma warning,下面的这行代码是一个整体,需要都复制):
#pragma warning(disable:4996)
//inherit.cpp代码文件
#include<iostream>
#include<cstring>
//
#pragma warning(disable:4996)
// ///
using namespace std;
//用户类
class usr
{
private:
char *name; //8 0x1008
int age; //4 0x1010
public:
void work();
usr(const char *ptr_name, int vage);
~usr();
};
usr::usr(const char *ptr_name, int vage)
{
name = new char[strlen(ptr_name) + 1];
strcpy(name, ptr_name);
age = vage;
cout << "usr(char *,int)" << endl;
}
usr::~usr()
{
delete []name;
cout << "~usr()" << endl;
}
void usr::work()
{
cout << "working" << endl;
}
//普通用户类
class normal_usr :public usr
{
private:
float money;
public:
void listen_music();
//因为基类的构造函数调用需要通过派生类构造函数,
//所以派生类构造函数参数列表需要包含基类构造函数参数
normal_usr(const char *ptr_name, int vage, float vmoney);
~normal_usr();
void work(int);
};
//派生类构造函数初始化列表,通过写基类类名调用基类构造函数
normal_usr::normal_usr(const char *ptr_name, int vage, float vmoney) :usr(ptr_name, vage), money(vmoney)
{
cout << "normal_usr()" << endl;
}
normal_usr::~normal_usr()
{
cout << "~normal_usr()" << endl;
}
void normal_usr::listen_music()
{
cout << "listen_music" << endl;
}
void normal_usr::work(int a)//类内有同名函数,那么基类函数会被覆盖
{//因为作用域不同,函数重载条件不成立,只要是同名,直接覆盖了,无关参数列表
cout << "qiaodaima" << endl;
}
//继承关系中,
// 构造顺序: 基类先构造,派生类后构造
// 析构顺序: 派生类先析构,基类后析构
int main()
{
normal_usr nu("xwy", 22, 4500.0);
nu.work(10);
//nu.work();
nu.usr::work();//如果我想使用基类的,那么只需要写个作用域就行
}
/*
usr(char *, int)"
"normal_usr()"
qiaodaima"
"working"
"~normal_usr()"
"~usr()"
*/
练习:
1.居民 - 成年人 - 党员
居民: 身份证、姓名、性别、出生日期
成年人:最高学历、职业
党员: 入党时间
建立以上三个类: 成年人继承居民 ,党员继承成年人
构造函数/析构函数完成
再写个show/运算符重载>> 输出内容
2.汽车 - 小车 - 卡车
汽车:车轮个数,车重(皆为保护属性)
小车: 载人数
卡车: 载人数 ,载重量
小车,卡车皆为汽车的私有派生
构造函数/析构函数完成
再写个show/运算符重载>> 输出内容
3.动物类 - 猫 - 狗
动物类: 行为: 发声
猫: 发声 喵喵喵
狗:发声 汪汪汪
猫,狗皆为动物类继承
居民代码文件:
//居民代码文件
#include <iostream>
#include <cstring>
// //
#pragma warning(disable:4996)
// /
using namespace std;
/*
练习:
1.居民一成年人一党员
居民:身份证、 姓名、性别、出生日期
成年人:最高学历、职业
党员:入党时间
建立以上三个类:成年人继承居民,党员继承成年人。
构造函数/析构函数完成
再写个show/运算符重载>>输出内容
*/
class resident
{
private:
char *num_id;
char *name;
char *sex;
char *born_data;
public:
resident(const char *id, const char *vn, const char *vs, const char *vb);
~resident();
void show();
};
class adult : public resident
{
private:
char *highest_edu;
char *profess;
public:
adult(const char *id, const char *vn, const char *vs, const char *vb, const char *vh, const char *vp);
~adult();
void show();
};
class party_mem : public adult
{
private:
char *join_party_time;
public:
party_mem(const char *id, const char *vn, const char *vs, const char *vb, const char *vh, const char *vp, const char *vj);
~party_mem();
void show();
};
resident::resident(const char *id, const char *vn, const char *vs, const char *vb)
{
num_id = new char[strlen(id) + 1];
strcpy(num_id, id);
name = new char[strlen(vn) + 1];
strcpy(name, vn);
sex = new char[strlen(vs) + 1];
strcpy(sex, vs);
born_data = new char[strlen(vb) + 1];
strcpy(born_data, vb);
cout << "resident(char *,char *,char *,char *)" << endl;
}
resident::~resident()
{
delete []num_id;
delete []name;
delete []sex;
delete []born_data;
cout << "~resident()" << endl;
}
adult::adult(const char *id, const char *vn, const char *vs, const char *vb, const char *vh, const char *vp) :resident(id, vn, vs, vb)
{
highest_edu = new char[strlen(vh) + 1];
profess = new char[strlen(vp) + 1];
strcpy(highest_edu, vh);
strcpy(profess, vp);
//strcpy_s(target, length, source);
cout << "adult(char *,char *,char *,char *,char *,char *)" << endl;
}
adult::~adult()
{
delete []highest_edu;
delete []profess;
cout << "~adult()" << endl;
}
party_mem::party_mem(const char *id, const char *vn, const char *vs, const char *vb, const char *vh, const char *vp, const char *vj) :adult(id, vn, vs, vb, vh, vp)
{
join_party_time = new char[strlen(vj) + 1];
strcpy(join_party_time, vj);
cout << "party_mem(char *,char *,char *,char *,char *,char *,char *)" << endl;
}
party_mem::~party_mem()
{
delete []join_party_time;
cout << "~party_mem()" << endl;
}
void resident::show()
{
cout << "身份证:" << num_id << endl << "姓名:" << name << endl << "性别:" << sex << endl << "出生日期:" << born_data << endl;
}
void adult::show()
{
resident::show();
cout << "最高学历:" << highest_edu << endl << "职业:" << profess << endl;
}
void party_mem::show()
{
adult::show();
cout << "人党时间:" << join_party_time << endl;
}
int main()
{
//你要写个重载。。。 cin >> 不建议写cin,建议写点局部变量,直接复制,更好
party_mem p("666666202203224444", "小尾羊", "男", "2022/03/22", "学历A级", "特务A级", "不是党员");
p.show();
}
待看代码
代码1
//Car.cpp代码文件
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
class Automobile
{
protected:
int wheel;
int aweight;
};
class Car:private Automobile
{
private:
int cpeoples;
public:
Car(int cw,int ca,int ic);
friend ostream & operator<<(ostream &out,Car& a);
~Car();
};
Car::~Car(){}
Car::Car(int cw,int ca,int ic)
{
cpeoples = ic;
wheel = cw;
aweight = ca;
}
ostream & operator<<(ostream &out,Car& a)
{
cout<<"小车 "<<"车轮数量:"<<a.wheel<<"车重:"<<a.aweight<<"载人数量:"<<a.cpeoples<<"\n";
}
class Truck:private Automobile
{
private:
int tpeople;
int weight;//如果基类不需要构造对象 => 接口类 ,接口类的存在一般为接下来继承这个接口类的
//派生类提前定义一些属性 / 空的函数(虚函数),要求派生类使用这些内容
public:
Truck(int wc,int ac,int ti,int we);
friend ostream & operator<<(ostream &out,Truck& a);
~Truck();
};
Truck::~Truck(){};
Truck::Truck(int wc,int ac,int ti,int we)//基类的构造函数任然调用了
{//但是调用的是,编译器默认生成的无参构造函数,如果基类的构造函数是无参的,那么不需要显示的调用
wheel = wc;
aweight = ac;
tpeople = ti;
weight = we;
}
ostream & operator<<(ostream &out,Truck& a)
{
cout<<"卡车 "<<"车轮数量:"<<a.wheel<<"车重:"<<a.aweight<<"载人数量:"<<a.tpeople<<"载重量:"<<a.weight<<"\n";
}
int main()
{
Car a(1,1,1);
Truck b(1,1,1,1);
cout<<a<<"\n";
cout<<b<<"\n";
return 0;
}
5.类型强转运算符
在C语言中,我们类型强转的方式都是 (新类型)对象/表达式,但是C语言强转类型的方式,在C++看来存在一些问题
1) 太简单粗暴了,可以强转任意类型,不在乎强转之后数据是否会丢失什么的,这在C++看来是一个危险的行为 2) 使用强转运算符之后,不好定位代码中那些位置使用了强转类型操作 (类型) =》(int *) 不好通过ctrl+f查找
C++中为了解决这种问题,就自己定义了4种类型强转运算符,以替代C强转运算符的使用
1) static_cast
```
一般表示静态转换 => 类型转换,在编译期间就能完成
限制:只能使用与基本类型转换(int,char,float,double,long),能用于有继承关系类对象/指针之间的转换(派生类能转基类,基类不可转派生类(包含)),非只读 -> 只读示例代码: cast.cpp
``
2) const_cast
```
去除只读属性 => 把只读数据变成非只读的,但是作用对象只能用于指针/引用
```
3) reinterpret_cast
```
重定义类型转换
规则:在不改变数据长度的情况下,重新解释该数据的意思
int * => char *
int * => int //err 数据长度改变,不行
```
4) dynamic_cast
```
动态类型转换:用于有继承关系的类指针之间的转换
```
//cast.cpp代码文件
//cast.cpp代码文件
#include<iostream>
using namespace std;
class base
{
public:
virtual void func();
};
void base::func()
{
cout << "base" << endl;
}
class derived:public base
{
public:
virtual void func();
};
void derived::func()
{
cout << "derived" << endl;
}
/*
静态转换示例
格式:
static_cast<强转的类型>(表达式/对象);
*/
void static_cast_exam()
{
int a = 10;
char c = static_cast<char>(a); //这种操作都有隐式类型强转
base b;
derived d;
//d = static_cast<derived>(b);//基类转派生类是不行的
b = static_cast<base>(d);//这种操作都有隐式类型强转,派生类对象可以直接赋值基类(包含关系)
}
/*
const_cast转换示例
格式:
const_cast<强转的类型>(表达式/对象);
*/
void const_cast_exam()
{
int a = 10;
const int &b = a;
const int *p = &b;
int &c = const_cast<int &>(b);//int &c = b 不行,const赋值 非const => 数据安全性丢失
int *q = const_cast<int *>(p);//int *q = p 不行,const赋值 非const => 数据安全性丢失
const int a2 = 10;
//int &b = const_cast<int>(a2);//const 变量不能赋值给 非const 变量
//const_cast<int> 是不可操作的,未定义的操作
}
/*
reinterpret_cast转换示例
格式:
reinterpret_cast<强转的类型>(表达式/对象);
*/
void reinterpret_cast_exam()
{
base *a = new derived();//以派生类对象初始化一个基类对象
derived *b = reinterpret_cast<derived*>(a);//reinterpret_cast 可以重新解释指针,但是这个操作的后果需要你自己承担
long c = reinterpret_cast<long>(a);
//long d = reinterpret_cast<int>(a);//在不改变数据长度的情况下,重新解释该数据的意思
//不能把a的长度改变 8 - 4 是不行的
int a1 = 100;
b = reinterpret_cast<derived*>(a1);//不会检测短数据变长数据
}
/*
dynamic_cast转换示例
格式:
dynamic_cast<强转的类型>(表达式/对象);
*/
void dynamic_cast_exam()
{
base *b = new derived();
b->func(); //多态概念,基类指针指向派生对象,会调用派生类的虚函数
derived* d = dynamic_cast<derived*>(b);
d->func(); //派生类对象调用自己的虚函数
}
int main()
{
//static_cast_exam();
dynamic_cast_exam();
}
6.异常处理(exception)
C++中,错误一般分为两种,编译时错误(语法问题,文件路径问题)和运算时错误(逻辑问题),C++,异常指的是程序运行过程中,由于系统条件,代码中某些操作不当使得程序崩溃,而产生的问题(run time err => 运行是错误),在C语言中,一般当程序运行时出错,会提供一个错误码和一段对应的错误信息给你,然后就提前结束程序了,但是在C++中,认为程序的运行在某个地方出错,不应该直接一次性结束程序,应该给程序一次机会,让其能够继续运行,而这种操作就是C++中异常处理机制
异常处理相关使用非常简单,分3步:
1) 使用throw 抛出异常
```
float a = 100;
float b = 0;if(b == 0)//操作数不合理
{
//抛出异常,告诉系统,当前程序出问题了,"这段代码"马上结束运行
//格式 :throw 临时对象内容/系统预定义对象内容;
throw "heheh";
}
else
{
a/b 操作
}
```2.使用catch 接收/捕捉异常信息
```
格式:
try
{
//包含了调用throw语句的代码段,可以是函数调用
}catch(throw抛出数据类型 对象名字)
{
//3.对异常进行处理
}catch(throw抛出数据类型 对象名字)
{
//3.处理异常
}
示例代码:
exception.cppC++标准提供一些错误信息类以描述各种错误信息,常用的
runtime_error 运行时错误 => 值不对劲
out_of_range 逻辑错误越界
bad_alloc 内存分配失败 new => 返回nullptr
...
在这些错误信息类中会有一个共有的成员函数 what,用于输出一个字符内容 =》 对应的错误信息
cplusplus.com
creference.com =>有中文版如果你写的函数,你觉得绝对不会有问题,那么你可以在这个函数的声明后面添加一个 noexecpt 表示这个函数是个无异常函数
```
//exception.cpp代码文件
//exception.cpp代码文件
#include<iostream>
#include<cstdio>
using namespace std;
int sum(int a,int b) noexcept;
int sum(int a,int b)
{
return a+b;
}
void cout_str(char * p)
{
if(p == nullptr)
{
throw runtime_error("hehehe");//抛出异常
//当我们使用throw抛出异常之后,当前代码段内会直接结束,在这个代码段内构造的对象会被自动的析构
//如果异常抛出的类型是不存在的,那么会报错,且程序崩溃
}
printf("%s\n",p);//上面抛出异常后,这个代码就不会运行了
}
int main()
{
char *p = nullptr;
try //异常机制的搭配使用/固定结构
{
cout_str(p);
}
catch(const char *info)//catch用于捕捉异常,根据返回的数据类型去捕捉异常
{
cerr << info << '\n';//异常处理语句
p = new char[100];
cin >> p;
cout_str(p);//修正操作,使得cout_str能够再次执行
}
catch(const int a)
{
cerr << a << '\n';
}
catch(runtime_error &e)
{
cerr << e.what() <<endl;
}
}