这里的继承名字起的很好,当我们链式继承的时候,即A继承B,B继承C,A-B-C-D-E…这样一直继承下去的时候,当全部都使用public继承那么就代表大家都毫无私心毫无保留的公开继承父类的所有属性大家都知道,但凡有层使用了private继承,那么这一层后面的属性都不能直接的访问到爷爷辈的所有除了private外的属性了。因此就像传授武林秘籍一样,我继承的时候不打算分享给子孙们就私有化祖先继承下来的武林秘籍,那么在这一层后武林秘籍可能就会以某种形式慢慢消失在大众眼中。
子类父类同名属性访问方式
访问子类this.属性名 / this->属性名
访问父类this.父类名::属性名 / this->父类名::属性名
访问父类的方式就是给一个属于哪一个父类的作用范围,就跟函数在类内声明类外定义一样的方式告知是哪一个类的成员。
访问父类的静态成员
当 子类的静态成员函数与父类的任意一个函数重名
只有这个父子类之间, 子类覆盖父类的条件为子类的静态的函数或者属性与父类的重名了就会把父类的覆盖掉,子类想要访问就只能通过父类名作用下访问。
这里主要是解释子类静态函数与父类函数发生同名的时候,因为很多时候会以为二者发生重载,但其实不会,子类已经把父类同名的隐藏掉了,发生不了重载。
总结:
开门见山:先构造父类再构造子类,先子类触发析构销毁再销毁父类
虚继承是为了解决菱形继承,就是像菱形的样子的继承链。
Animal
↓ --------------↓
Sheep -----Tuo
||
SheepTuo
假设羊和骆驼结合成为羊驼
Animal中的一个属性m_Age继承给Sheep和Tuo后这俩都被SheepTuo继承了,那么Animal中的属性就出现了二义性,不知道羊驼的m_Age应该是从Sheep中来还是从Tuo中来的,这就是二义性。
总结:虚继承是用来解决菱形继承的,一般不推荐在开发中多继承开发,比如Java语言就直接舍弃了该特性,我目前没有找到菱形继承的妙用,暂且理解为是C++的一个缺陷把,毕竟人家Java用C++开发的也就直接舍弃了这个多继承特性了。
整个面向对象中多态是最妙哉的出现,前面感叹面向对象的继承妙用,这里的多态真的是神一般的存在了,可谓是面向对象的一个最最重要的特性。开发过程中使用多态能够表现出多种多样的现象解决多种需求。
虚函数是用来给子类重写的
虚函数: 一个类中有一个虚函数就代表子类中如果将父类中该函数进行重写了在实例化该子类的时候用父类调用的就不是父类的虚函数而是在子类中重写的该函数。(即:父类指针 变量 = new 实例化子类)
注意:虚函数是可以实例化的
纯虚函数: 一个类中有一个纯虚函数的时候该类就不能够实例化,子类中也必须要将其纯虚函数重写才能把该子类实例化。总的来说,不管子类如何操作,只要子类将父类中的纯虚函数进行重写了,那么该子类就可以实例化,但是如果说子类中忘记实现了父类中的一个纯虚函数都不行都不能进行实例化了,代表继承下来的这个纯虚函数还是在你儿子手上并没有得到实现,所以如果没有特别需求一定要有多少就实现多少。
当然从我理解层面上说,比如我要设计一个层级关系的系统就可能需要这种一级一级的进行放行某些函数,一个类一个类这样进行继承下去的时候慢慢实现纯虚函数实现层级功能的分配。
细节:虚函数需要实现体,纯虚不用实现体只要=0即可,虚函数的类可以实例化,纯虚函数的类不能实例化。
class Animal{
public:
virtual void show() = 0; //纯虚函数千万别加实现体
virtual void show2(){
} //虚函数需要实现体
};
class dog:public Animal{
public:
void shwo(){} //实现体
//void show2(){}//虚函数可以不实现,不影响dog的实例化
};
虚析构
虚析构的作用是当我们实现多态的时候,即:父类指针接受子类对象时,我们只能delete父类指针变量,代码只会走父类的析构函数,不会走子类的析构函数。因为父类指针虽然指向子类的对象,但是你delete的意思其实本质还是delete父类指针,所以我们没有用虚析构的话,就代表我们没有让子类重写我们的这个析构函数,这个函数就直接会从父类开始找,就只有走了父类的析构,子类没有碰到。
(简单理解就是:父类没将析构写成虚的,子类无法重写,那么多态的时候就会直接走父类的函数(析构),走不到子类的析构)
纯虚析构
有了虚析构还要纯虚析构干啥?我当时也是很懵的状态。
解释:纯虚的意识首先当然是不可实例化该类的意思,一般用于父类。其实虚析构和纯虚的本意都是为了解决多态中无法走子类的析构函数问题,那么纯虚其实是为了强制的让子类们都实现析构,必须显示的实现析构函数而已(当然肯定有其他更恰到好处的用途,纯虚析构存在即合理。)
总结:纯虚无法实例化对象且不用写实现体,除非是纯虚析构函数就必须写实现体,但是纯虚析构的实现体必须是类内声明类外实现。虚函数要写实现体。不管是哪种虚,带一个虚字的函数都是为了子类在继承后重写该父类的函数,唯一的区别就是纯虚带有更强的权利,必须子类实现该函数,否则无法将该类实例化(当然除非你本身就不希望他的子类实例化的时候就无须实现可以只继承不实现)
在C++ 中,实例化一个类对象就和定义一个数据类型一样,
假如说有一个学生类class student
,
想要实例化这个学生类:
student s1
;student *s1 = new student()
student *s1 =new student(参数...)
student s1 = student();
(显示法)student student(参数...)
(括号法)student s1 = 10;
(利用参数的方法进行对象实例化,我测试了一下,这种方法只适用于一个参数,多个参数我的编译器不通过直接报错
,不推荐使用这种,可读性差!)student s2; student s1 = s2
(拷贝构造,也是隐式创建对象的一种)student s1;
/ student s1(参数列表)
student *p_s1 = new student([参数列表,.])
student s1 = s2
—这种是拷贝构造函数,用于复制对象的,拷贝构造也属于构造初始化一种,只不过是用别人的数据初始化自己的数据。类名(const 引用类型 变量名)
,例子:const int& a;student reStu(){
student stu;
return stu;
}
student myStu = reStu();
既然如此,我们出现拷贝的原因是因为出现了实参到形参的复制,那我们想要防止这种情况就是直接不使用形参传递就好了,使用实参传递,那对于类的实参传递有两种
在一个对象的生命中,三个默认就存在的函数。
收获:其实在学习这个C++面向对象相比Java粒度变得十分的细致,能够十分自如的掌握一个对象的整个生命周期,但同时也感到C++十分重,单单一个实例化对象就涵盖几种多种方法,当然我肯定是不够火候,还没悟道这多种实例化对象所应用的场景,我相信他这多种实例化对象有实际的应用场景的。
在构造函数的参数列表右边直接给属性赋值。
class Person{
int m_A;
int m_B;
Person():m_A(10),m_B(20){} //允许空实现构造函数,只用来初始化赋值
};
在实例化的时候就是默认将我们写进去的两个属性初始化为10和20,当然可以只写一个,不一定全部属性都要写。
在整个程序运行开始到结束都存在,静态俩字就说明不论是变量还是函数都是存在于一个存储区内,并且整个程序运行起来就开辟好存储在一个地址块内,然后程序运行的时候都存储在这个区内,只有一份,且用的时候都是用这个区的这一份数据。
编译阶段就会为所有静态的东西分配内存,且在同一个内存区内。
全局范围的静态变量和函数的作用域仅仅在本源文件,出了该文件就访问不到。(因为这是全局的静态,不同于类内的全局,固定在一个了类中,能够将其头文件与源文件分开声明定义,我们的全局静态一旦出了本源文件,就无法在另外的文件链接到。)
题外话:私有的静态变量我第一时间想到的就是我们玩游戏的时候隐藏的属性,不会明摆出来告诉你的事情,比如说你抽奖,当你充一次钱就会调用一个私有静态函数,将其抽奖中奖概率提高,这个概率就是私有的静态变量,我们无法再外部进行访问。(临时想到的,写下来。)
通过下面的例子还学习到当我们类内声明,类外定义的时候,类外定义的范围还属于类内部,因此可以调用到私有成员。
class Person{
public:
static void count();
private:
static void static_fun(){
cout << "调用静态函数" << endl;
}
};
void Person::count(){
static_fun(); //同时可以调用到类内的私有成员
}
注意:全局的静态函数或者变量的作用域都是在本源文件中能够访问到。
在结构体和类中有一个内存对齐原则。
下面的例子中可以很直白的说明问题了。
#include
using namespace std;
class P1 {};
class P2 {
int m_Num;
};
class P3 {
int m_Num;
static int m_StaticNum;
};
class P4 {
int m_Num;
static int m_StaticNum;
void print() {}
};
class P5 {
int m_Num;
static int m_StaticNum;
void print() {}
char ch;
int m_Num2;
};
class P6 {
int m_Num;
static int m_StaticNum;
void print() {}
char ch;
int m_Num2;
double money;
};
int main() {
P1 p1;
P2 p2;
P3 p3;
P4 p4;
P5 p5;
P6 p6;
cout << "sizeof:P1=" << sizeof(p1) << endl;
cout << "sizeof:P2=" << sizeof(p2) << endl;
cout << "sizeof:P3=" << sizeof(p3) << endl;
cout << "sizeof:P4=" << sizeof(p4) << endl;
cout << "sizeof:P5=" << sizeof(p5) << endl;
cout << "sizeof:P6=" << sizeof(p6) << endl;
return 0;
}
内存对齐就是找到类或结构体中某个类型属性所占的空间最大,然后按照该类型的空间大小作为该类扩展空间的倍数,然后这些成倍数扩展好的空间就是该类的最终内存空间大小
class Person{
Person& returnP(){
return *this
}
};
解释:首先常函数的本意其实是当我们不希望在该函数内将其中的成员属性值修改,代码量非常庞大或者放着被别人修改的时候我们就可以加上const就知道该成员函数有没有将某个属性修改过,在写之前加上const又能够防止自己不小心将其某个属性修改掉,所以常函数在我角度看来还是非常有用的。
#include
using namespace std;
class Person {
public:
int m_Age;
int mutable m_mutablAge;
void fun() {
cout << "普通函数" << endl;
}
void count(){}
void fun() const { //加上const的本意是不希望我们在该函数中修改属性值,常函数
// 常函数谁都可以访问,但是常变量只能够访问常函数,
// 原因是我们将其变为常变量目的就是不希望修改其中的成员属性值
// 普通函数既然能够随意修改属性变量值,那我们常变量固然不可以访问
// 就代表我们的常函数不能够访问普通函数,即只能够访问常函数
int age = this->m_Age; //不修改成员变量是正常的
//this->m_Age = 100 //报错,不可以修改成员变量值
cout << "const成员函数" << endl;
//尝试访问普通成员函数
this.count(); //必然报错
//尝试访问mutable修饰的成员属性
this->m_mutablAge = 100;
}
};
int main() {
Person const* p;
return 0;
}
友元的意思是希望某个函数/类能够访问到自己的私有成员
我的理解:本类把某个函数或者类当作朋友,那么对方就能肆无忌惮的闯入你的生活(bushi),对方能够随意的访问你的私有的成员。但是,一个残酷的现实就是,你把人家列入了friend名单,但是对方可不一定把你列入friend,所有你把人家列入friend名单的时候,对方没有把你列入朋友那你可不能访问对方的私有成员,因为你把人当朋友,别人却不把你当朋友。
我只是举一个反例,也很贴合现实生活,但是我们代码中的关系是可以自己操控的, 因此如果希望双方都能互相访问对方的私有属性的时候就可以两两都互相把对方列入friend列表。
友元是写在类中的,可理解成该类给别人的一种特权。
用两个类作为例子讲解三种友元
class Girl;
class Boy;
void G_Fun(Boy& boy) {
cout << "全局函数" << endl;
cout << "访问男生的私有属性:" << boy.m_Money << endl;
}
class Boy {
friend void G_Fun(Boy& boy);
friend Girl;
public:
Girl* girl;
Boy() {
girl = new Girl();
}
int m_Age;
void showGirl();
private:
int m_Money;
};
class Girl {
friend void Boy::showGirl();
public:
Boy* boy;
Girl() {
boy = new Boy();
}
void show();
private:
int m_Age;
int m_Money;
};
void Boy::showGirl() {
girl->m_Age = 100;
}
void Girl::show() {
boy->m_Money = 100;
}
void G_Fun(Boy& boy)
在上述两个类中,有一个全局函数被Boy类列为了友元函数,即Boy将该函数作为了自己的朋友关系,那么该函数就能够访问Boy的私有属性了。
在Boy类中写上:friend void G_Fun(Boy& boy);
,该函数就能够随意访问Boy的属性了。
这就是友元全局函数。(理解了这个格式就知道后面的套路是如何写友元的了。)
friend Girl;
就表示Boy把Girl类当作朋友了,那么对于Girl来说就可以随意访问Boy,但是Girl并没有把Boy当作友元,因此Boy这时候并不能够访问Girl的私有属性。友元成员函数:必须在类内部声明,在类外部定义
在C++中使用友元最坑的点不是忘记具体的格式怎么写,是我们的C++在书写代码的顺序,一旦写了友元,那么类与类之间的关系就变得很密切,因此我们就需要按照一种:先声明再定义and类内声明类外定义 的习惯进行书写C++代码,这样就能够确保我们在写友元的时候不会发生未定义的错误。
首先要明白我们重载运算符主要的需求就是希望对象之间能够使用运算符进行运算。
operator运算符即我们写函数的时候的名字,运算符决定了你重载的函数的主要作用
在重载运算符中有两种形式,一种在全局重载,另一种是在类内重载。
区别:
需要注意的一点就是,一般的,写全局重载运算符函数的时候,我们类与类之间可能需要运算的属性是私有属性,那我们就需要将该函数作为运算某个类的友元函数,这样就可以在全局函数内访问到该类的私有属性了。
operator+
重载加号运算符
operator++
首先这个不管是前置还是后置都能在类内或者全局来重载实现,所以我们首先最简单的就是类内实现
前置:operator++(),后置:operator++(int)
说明:这里的后置中写了一个int占位类型,仅仅是用来占位的,因为我们前置和后置的函数名是一样的,所以我们需要实现重载就需要用参数来区分函数实现重载,至于为什么后置是有一个int而前置不用的原因:可以这么一想,由于我们是写类内的看不太出来,我们后面写全局的时候就知道了。
上述是类内的写法,下面说全局的写法
前置:operator++(MyInteger t1) 后置:operator(MyInteger t1,int)
后置在这里可以看出为什么要加int了,是因为我们的第一个参数t1是要进行++运算的,所以在后面加上int占位就可以理解为将++后置的意思,之所以在类内看不出来是因为我们直接把本类该对象作为了第一个参数,所以看不出来为啥要用参数int进行占位操作,在operator全局中就很明显看出来意思是后置++
#include
using namespace std;
class MyInteger {
friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
MyInteger() {
m_Num = 10;
m_height = new int(10);
}
MyInteger(const MyInteger& myint) { //拷贝函数
cout << "拷贝构造发生" << endl;
this->m_Num = myint.m_Num;
this->m_height = new int(*(myint.m_height));
}
~MyInteger() {//析构函数
if (this->m_height != NULL) {
delete this->m_height;
this->m_height = NULL;
}
}
void fun(MyInteger myint) {
cout << ++myint.m_Num << endl;
}
MyInteger operator+(MyInteger& myint) { //加号运算符
//看情况是否返回引用类型,这里返回局部
MyInteger temp;
temp.m_Num = this->m_Num + myint.m_Num;
return temp;
}
MyInteger& operator++() { //前置++
this->m_Num++;
return *this;//因为返回的是引用类型,所以我们解引用解出来即可
}
MyInteger operator++(int) { //后置自增
//后置++就没必要实现链式编程了,
//因为我们后置自增就是不是立马自增,而是执行了之后下一行才将其原本的数自增,因此仿要仿的像才行
MyInteger temp = *this;
this->m_Num++;
return temp;
}
MyInteger& operator=(MyInteger& myint) {
//重载等号运算符
cout << "执行重载等号运算符" << endl;
cout << "等号两边都已经存在的对象的时候就会执行重载等号运算符。" << endl;
this->m_height = new int(*(myint.m_height));
this->m_Num = myint.m_Num;
return *this;
}
bool operator<(MyInteger myint) {
//重载小于运算符
if (this->m_Num < myint.m_Num) {
return true;
}
else return false;
}
bool operator>(MyInteger myint) {
//重载大于运算符
if (this->m_Num > myint.m_Num) {
return true;
}
else return false;
}
void operator()(string str) {
cout << str << endl;
}
private:
int m_Num;
int* m_height;
};
//左移运算符,Java的toString方法底层估计就是这个
ostream& operator<<(ostream& cout, MyInteger myint) {
cout << myint.m_Num;
return cout;
}
int main(int) {
MyInteger myint;
MyInteger myint2;
MyInteger myint3 = myint + myint2;
cout << "myint=" << myint << endl;
/* myint3 = myint + myint2;*/
cout << "myint3=" << myint3 << endl;
cout << "前置自增后myint=" << ++myint << endl;
cout << "执行前置自增myint=" << myint++ << endl;
cout << "执行后myint=" << myint << endl;
cout << "测试拷贝构造与等号重载符号哪个执行" << endl;
MyInteger a;
MyInteger b;
a = b;
cout << "myint:" << myint << ",myint3:" << myint3 << endl;
cout << "myint是否小于myint3:" << (myint < myint3) << endl;
cout << "myint是否大于myint3:" << (myint > myint3) << endl;
MyInteger()("555"); //使用匿名类调用
b("555");//直接使用实例化出来的对象调佣
return 0;
}